Mercurial > libervia-backend
annotate sat.tac @ 12:ef8060d365cb
whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 28 Oct 2009 00:39:29 +0100 |
parents | 5799493fa548 |
children | bd9e9997d540 |
rev | line source |
---|---|
0 | 1 #!/usr/bin/python |
2 # -*- coding: utf-8 -*- | |
3 | |
4 """ | |
5 SAT: a jabber client | |
6 Copyright (C) 2009 Jérôme Poisson (goffi@goffi.org) | |
7 | |
8 This program is free software: you can redistribute it and/or modify | |
9 it under the terms of the GNU General Public License as published by | |
10 the Free Software Foundation, either version 3 of the License, or | |
11 (at your option) any later version. | |
12 | |
13 This program is distributed in the hope that it will be useful, | |
14 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
16 GNU General Public License for more details. | |
17 | |
18 You should have received a copy of the GNU General Public License | |
19 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
20 """ | |
21 | |
22 | |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
23 from twisted.application import internet, service |
12
ef8060d365cb
whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
Goffi <goffi@goffi.org>
parents:
6
diff
changeset
|
24 from twisted.internet import glib2reactor, protocol, task |
0 | 25 glib2reactor.install() |
26 | |
27 from twisted.words.protocols.jabber import client, jid, xmlstream, error | |
28 from twisted.words.xish import domish | |
29 | |
30 from twisted.internet import reactor | |
31 import pdb | |
32 | |
33 from sat_bridge.DBus import DBusBridge | |
34 import logging | |
35 from logging import debug, info, error | |
36 | |
37 import signal, sys | |
38 import os.path | |
39 | |
40 from tools.memory import Memory | |
41 from glob import glob | |
42 | |
43 | |
44 ### logging configuration FIXME: put this elsewhere ### | |
45 logging.basicConfig(level=logging.DEBUG, | |
46 format='%(message)s') | |
47 ### | |
48 | |
49 | |
50 | |
51 | |
52 class SAT: | |
53 | |
54 def __init__(self): | |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
55 #self.reactor=reactor |
0 | 56 self.memory=Memory() |
57 self.server_features=[] #XXX: temp dic, need to be transfered into self.memory in the future | |
58 self.connected=False #FIXME: use twisted var instead | |
59 | |
60 self._iq_cb_map = {} #callback called when ns is found on IQ | |
61 self._waiting_conf = {} #callback called when a confirmation is received | |
62 self._progress_cb_map = {} #callback called when a progress is requested (key = progress id) | |
63 self.plugins = {} | |
64 | |
65 self.bridge=DBusBridge() | |
66 self.bridge.register("connect", self.connect) | |
1 | 67 self.bridge.register("disconnect", self.disconnect) |
0 | 68 self.bridge.register("getContacts", self.memory.getContacts) |
69 self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus) | |
70 self.bridge.register("sendMessage", self.sendMessage) | |
71 self.bridge.register("setParam", self.setParam) | |
72 self.bridge.register("getParam", self.memory.getParam) | |
73 self.bridge.register("getParams", self.memory.getParams) | |
74 self.bridge.register("getParamsCategories", self.memory.getParamsCategories) | |
75 self.bridge.register("getHistory", self.memory.getHistory) | |
76 self.bridge.register("setPresence", self.setPresence) | |
77 self.bridge.register("addContact", self.addContact) | |
78 self.bridge.register("delContact", self.delContact) | |
79 self.bridge.register("isConnected", self.isConnected) | |
80 self.bridge.register("confirmationAnswer", self.confirmationAnswer) | |
81 self.bridge.register("getProgress", self.getProgress) | |
82 | |
83 self._import_plugins() | |
5 | 84 #self.connect() |
0 | 85 |
86 | |
87 def _import_plugins(self): | |
88 """Import all plugins found in plugins directory""" | |
89 #TODO: manage dependencies | |
90 plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob ("plugins/plugin*.py"))] | |
91 | |
92 for plug in plug_lst: | |
93 plug_path = 'plugins.'+plug | |
94 __import__(plug_path) | |
95 mod = sys.modules[plug_path] | |
96 plug_info = mod.PLUGIN_INFO | |
97 info ("importing plugin: %s", plug_info['name']) | |
98 self.plugins[plug_info['import_name']] = getattr(mod, plug_info['main'])(self) | |
99 | |
100 def connect(self): | |
6
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
101 if (self.connected): |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
102 info("already connected !") |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
103 return |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
104 print "connecting..." |
5 | 105 reactor.connectTCP(self.memory.getParamV("Server", "Connection"), 5222, self.factory) |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
106 |
6
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
107 def disconnect(self): |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
108 if (not self.connected): |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
109 info("not connected !") |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
110 return |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
111 info("Disconnecting...") |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
112 self.factory.stopTrying() |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
113 if self.xmlstream: |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
114 self.xmlstream.sendFooter() |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
115 |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
116 def getService(self): |
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
117 print "GetService !" |
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
118 """if (self.connected): |
0 | 119 info("already connected !") |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
120 return""" |
5 | 121 info("Getting service...") |
0 | 122 self.me = jid.JID(self.memory.getParamV("JabberID", "Connection")) |
123 self.factory = client.XMPPClientFactory(self.me, self.memory.getParamV("Password", "Connection")) | |
6
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
124 self.factory.clientConnectionLost = self.connectionLost |
0 | 125 self.factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,self.authd) |
126 self.factory.addBootstrap(xmlstream.INIT_FAILED_EVENT,self.failed) | |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
127 return internet.TCPClient(self.memory.getParamV("Server", "Connection"), 5222, self.factory) |
0 | 128 |
129 def run(self): | |
130 debug("running app") | |
131 reactor.run() | |
132 | |
133 def stop(self): | |
134 debug("stopping app") | |
135 reactor.stop() | |
136 | |
137 def authd(self,xmlstream): | |
6
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
138 self.xmlstream=xmlstream |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
139 roster=client.IQ(xmlstream,'get') |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
140 roster.addElement(('jabber:iq:roster', 'query')) |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
141 roster.addCallback(self.rosterCb) |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
142 roster.send() |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
143 debug("server = %s",self.memory.getParamV("Server", "Connection")) |
0 | 144 |
6
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
145 ###FIXME: tmp disco ### |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
146 self.memory.registerFeature("http://jabber.org/protocol/disco#info") |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
147 self.disco(self.memory.getParamV("Server", "Connection"), self.serverDisco) |
0 | 148 |
149 | |
6
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
150 #we now send our presence status |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
151 self.setPresence(status="Online") |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
152 |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
153 # add a callback for the messages |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
154 xmlstream.addObserver('/message', self.gotMessage) |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
155 xmlstream.addObserver('/presence', self.presenceCb) |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
156 xmlstream.addObserver("/iq[@type='set' or @type='get']", self.iqCb) |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
157 print "********** CONNECTED **********" |
12
ef8060d365cb
whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
Goffi <goffi@goffi.org>
parents:
6
diff
changeset
|
158 self.connected=True |
ef8060d365cb
whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
Goffi <goffi@goffi.org>
parents:
6
diff
changeset
|
159 self.keep_alife = task.LoopingCall(self.xmlstream.send, " ") #Needed to avoid disconnection (specially with openfire) |
ef8060d365cb
whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
Goffi <goffi@goffi.org>
parents:
6
diff
changeset
|
160 self.keep_alife.start(180) |
ef8060d365cb
whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
Goffi <goffi@goffi.org>
parents:
6
diff
changeset
|
161 |
6
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
162 #reactor.callLater(2,self.sendFile,"goffi2@jabber.goffi.int/Psi", "/tmp/fakefile") |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
163 |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
164 def connectionLost(self, connector, unused_reason): |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
165 print "********** DISCONNECTED **********" |
12
ef8060d365cb
whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
Goffi <goffi@goffi.org>
parents:
6
diff
changeset
|
166 if self.keep_alife: |
ef8060d365cb
whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
Goffi <goffi@goffi.org>
parents:
6
diff
changeset
|
167 self.keep_alife.stop() |
6
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
168 self.connected=False |
5799493fa548
connection and disconnection management
Goffi <goffi@goffi.org>
parents:
5
diff
changeset
|
169 |
0 | 170 |
171 def sendMessage(self,to,msg,type='chat'): | |
172 #FIXME: check validity of recipient | |
173 debug("Sending jabber message to %s...", to) | |
174 message = domish.Element(('jabber:client','message')) | |
175 message["to"] = jid.JID(to).full() | |
176 message["from"] = self.me.full() | |
177 message["type"] = type | |
178 message.addElement("body", "jabber:client", msg) | |
179 self.xmlstream.send(message) | |
180 self.memory.addToHistory(self.me, self.me, jid.JID(to), message["type"], unicode(msg)) | |
181 self.bridge.newMessage(message['from'], unicode(msg), to=message['to']) #We send back the message, so all clients are aware of it | |
182 | |
183 def setParam(self, name, value, namespace): | |
184 """set wanted paramater and notice observers""" | |
185 info ("setting param: %s=%s in namespace %s", name, value, namespace) | |
186 self.memory.setParam(name, value, namespace) | |
187 self.bridge.paramUpdate(name, value, namespace) | |
188 | |
189 def setRoster(self, to): | |
190 """Add a contact to roster list""" | |
191 to_jid=jid.JID(to) | |
192 roster=client.IQ(self.xmlstream,'set') | |
193 query=roster.addElement(('jabber:iq:roster', 'query')) | |
194 item=query.addElement("item") | |
195 item.attributes["jid"]=to_jid.userhost() | |
196 roster.send() | |
197 #TODO: check IQ result | |
198 | |
199 def delRoster(self, to): | |
200 """Remove a contact from roster list""" | |
201 to_jid=jid.JID(to) | |
202 roster=client.IQ(self.xmlstream,'set') | |
203 query=roster.addElement(('jabber:iq:roster', 'query')) | |
204 item=query.addElement("item") | |
205 item.attributes["jid"]=to_jid.userhost() | |
206 item.attributes["subscription"]="remove" | |
207 roster.send() | |
208 #TODO: check IQ result | |
209 | |
210 | |
211 def failed(self,xmlstream): | |
212 debug("failed: %s", xmlstream.getErrorMessage()) | |
213 debug("failed: %s", dir(xmlstream)) | |
214 | |
215 def isConnected(self): | |
216 return self.connected | |
217 | |
218 ## jabber methods ## | |
219 | |
220 def disco (self, item, callback, node=None): | |
221 """XEP-0030 Service discovery Feature.""" | |
222 disco=client.IQ(self.xmlstream,'get') | |
223 disco["from"]=self.me.full() | |
224 disco["to"]=item | |
225 disco.addElement(('http://jabber.org/protocol/disco#info', 'query')) | |
226 disco.addCallback(callback) | |
227 disco.send() | |
228 | |
229 | |
230 def setPresence(self, to="", type="", show="", status="", priority=0): | |
231 """Send our presence information""" | |
232 presence = domish.Element(('jabber:client', 'presence')) | |
233 if not type in ["", "unavailable", "subscribed", "subscribe", | |
234 "unsubscribe", "unsubscribed", "prob", "error"]: | |
235 error("Type error !") | |
236 #TODO: throw an error | |
237 return | |
238 | |
239 if to: | |
240 presence.attributes["to"]=to | |
241 if type: | |
242 presence.attributes["type"]=type | |
243 | |
244 for element in ["show", "status", "priority"]: | |
245 if locals()[element]: | |
246 presence.addElement(element).addContent(unicode(locals()[element])) | |
247 | |
248 self.xmlstream.send(presence) | |
249 | |
250 def addContact(self, to): | |
251 """Add a contact in roster list""" | |
252 to_jid=jid.JID(to) | |
253 self.setRoster(to_jid.userhost()) | |
254 self.setPresence(to_jid.userhost(), "subscribe") | |
255 | |
256 def delContact(self, to): | |
257 """Remove contact from roster list""" | |
258 to_jid=jid.JID(to) | |
259 self.delRoster(to_jid.userhost()) | |
260 self.bridge.contactDeleted(to) | |
261 | |
262 def gotMessage(self,message): | |
263 debug (u"got_message from: %s", message["from"]) | |
264 for e in message.elements(): | |
265 if e.name == "body": | |
266 self.bridge.newMessage(message["from"], e.children[0]) | |
267 self.memory.addToHistory(self.me, jid.JID(message["from"]), self.me, "chat", e.children[0]) | |
268 break | |
269 | |
270 ## callbacks ## | |
271 | |
272 def add_IQ_cb(self, ns, cb): | |
273 """Add an IQ callback on namespace ns""" | |
274 debug ("Registered callback for namespace %s", ns) | |
275 self._iq_cb_map[ns]=cb | |
276 | |
277 def iqCb(self, stanza): | |
278 info ("iqCb") | |
279 debug ("="*20) | |
280 debug ("DEBUG:\n") | |
281 debug (stanza.toXml().encode('utf-8')) | |
282 debug ("="*20) | |
283 #FIXME: temporary ugly code | |
284 uri = stanza.firstChildElement().uri | |
285 if self._iq_cb_map.has_key(uri): | |
286 self._iq_cb_map[uri](stanza) | |
287 #TODO: manage errors stanza | |
288 | |
289 def presenceCb(self, elem): | |
290 info ("presence update for [%s]", elem.getAttribute("from")) | |
291 debug("\n\nXML=\n%s\n\n", elem.toXml()) | |
292 presence={} | |
293 presence["jid"]=elem.getAttribute("from") | |
294 presence["type"]=elem.getAttribute("type") or "" | |
295 presence["show"]="" | |
296 presence["status"]="" | |
297 presence["priority"]=0 | |
298 | |
299 for item in elem.elements(): | |
300 if presence.has_key(item.name): | |
301 presence[item.name]=item.children[0] | |
302 | |
303 ### we check if the status is not about subscription ### | |
304 #TODO: check that from jid is one we wants to subscribe (ie: check a recent subscription asking) | |
305 if jid.JID(presence["jid"]).userhost()!=self.me.userhost(): | |
306 if presence["type"]=="subscribed": | |
307 debug ("subscription answer") | |
308 elif presence["type"]=="unsubscribed": | |
309 debug ("unsubscription answer") | |
310 elif presence["type"]=="subscribe": | |
311 #FIXME: auto answer for subscribe request, must be checked ! | |
312 debug ("subscription request") | |
313 self.setPresence(to=presence["jid"], type="subscribed") | |
314 else: | |
315 #We keep presence information only if it is not for subscription | |
316 self.memory.addPresenceStatus(presence["jid"], presence["type"], presence["show"], | |
317 presence["status"], int(presence["priority"])) | |
318 | |
319 #now it's time to notify frontends | |
320 self.bridge.presenceUpdate(presence["jid"], presence["type"], presence["show"], | |
321 presence["status"], int(presence["priority"])) | |
322 | |
323 def rosterCb(self,roster): | |
324 for contact in roster.firstChildElement().elements(): | |
325 info ("new contact in roster list: %s", contact['jid']) | |
326 #and now the groups | |
327 groups=[] | |
328 for group in contact.elements(): | |
329 if group.name!="group": | |
330 error("Unexpected element !") | |
331 break | |
332 groups.append(str(group)) | |
333 self.memory.addContact(contact['jid'], contact.attributes, groups) | |
334 self.bridge.newContact(contact['jid'], contact.attributes, groups) | |
335 | |
336 def serverDisco(self, disco): | |
337 """xep-0030 Discovery Protocol.""" | |
338 for element in disco.firstChildElement().elements(): | |
339 if element.name=="feature": | |
340 debug ("Feature dectetee: %s",element["var"]) | |
341 self.server_features.append(element["var"]) | |
342 elif element.name=="identity": | |
343 debug ("categorie= %s",element["category"]) | |
344 debug ("features= %s",self.server_features) | |
345 | |
346 ## Generic HMI ## | |
347 | |
348 def askConfirmation(self, id, type, data, cb): | |
349 """Add a confirmation callback""" | |
350 if self._waiting_conf.has_key(id): | |
351 error ("Attempt to register two callbacks for the same confirmation") | |
352 else: | |
353 self._waiting_conf[id] = cb | |
354 self.bridge.askConfirmation(type, id, data) | |
355 | |
356 | |
357 def confirmationAnswer(self, id, accepted, data): | |
358 """Called by frontends to answer confirmation requests""" | |
359 debug ("Received confirmation answer for id [%s]: %s", id, "accepted" if accepted else "refused") | |
360 if not self._waiting_conf.has_key(id): | |
361 error ("Received an unknown confirmation") | |
362 else: | |
363 cb = self._waiting_conf[id] | |
364 del self._waiting_conf[id] | |
365 cb(id, accepted, data) | |
366 | |
367 def registerProgressCB(self, id, CB): | |
368 """Register a callback called when progress is requested for id""" | |
369 self._progress_cb_map[id] = CB | |
370 | |
371 def removeProgressCB(self, id): | |
372 """Remove a progress callback""" | |
373 if not self._progress_cb_map.has_key(id): | |
374 error ("Trying to remove an unknow progress callback") | |
375 else: | |
376 del self._progress_cb_map[id] | |
377 | |
378 def getProgress(self, id): | |
379 """Return a dict with progress information | |
380 data['position'] : current possition | |
381 data['size'] : end_position | |
382 """ | |
383 data = {} | |
384 try: | |
385 self._progress_cb_map[id](data) | |
386 except KeyError: | |
387 pass | |
388 #debug("Requested progress for unknown id") | |
389 return data | |
390 | |
391 | |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
392 application = service.Application('SàT') |
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
393 sat = SAT() |
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
394 service = sat.getService() |
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
395 service.setServiceParent(application) |
0 | 396 |
2
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
397 |
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
398 #app.memory.save() #FIXME: not the best place |
c49345fd7737
refactoring: moved sat to sat.tac, now a twisted application so we can use twistd.
Goffi <goffi@goffi.org>
parents:
1
diff
changeset
|
399 #debug("Good Bye") |