Merge lp:~oubiwann/ubuntu-accomplishments-system/946850-twisted-app into lp:~jonobacon/ubuntu-accomplishments-system/trophyinfo

Proposed by Duncan McGreggor
Status: Merged
Merge reported by: Jono Bacon
Merged at revision: not available
Proposed branch: lp:~oubiwann/ubuntu-accomplishments-system/946850-twisted-app
Merge into: lp:~jonobacon/ubuntu-accomplishments-system/trophyinfo
Diff against target: 1248 lines (+504/-345)
7 files modified
accomplishments/daemon/api.py (+276/-279)
accomplishments/daemon/app.py (+36/-11)
accomplishments/daemon/dbusapi.py (+46/-38)
accomplishments/daemon/service.py (+80/-0)
accomplishments/gui/TrophyinfoWindow.py (+1/-1)
accomplishments/util/__init__.py (+37/-1)
bin/daemon (+28/-15)
To merge this branch: bzr merge lp:~oubiwann/ubuntu-accomplishments-system/946850-twisted-app
Reviewer Review Type Date Requested Status
Jono Bacon Approve
Review via email: mp+98325@code.launchpad.net

Description of the change

Okay, this branch provides a twistd daemon. The wiki will need to be updated for usage:

To run the daemon under non-development conditions (e.g., from an rc script) one needs to invoke the daemon in the following manner:

 $ twistd -y ./bin/daemon

This will write a log file and a pid file to the working directory.

From an rc script, you'd (obviously) need to give the full path and have the accomplishments code installed on the Python path for the version that the rc script would be using. Also, for an rc script, you will probably want to pass options for the pid file and log file directories, and maybe even uid/guid options.

When developing, one can use the following so that the daemon is not daemonized, but instead writes it log file to stdout:

 $ twistd -noy ./bin/daemon

Usage notes aside, this branch makes some big changes. First and foremost, it introduces a set of Twisted application services and nests them so that we can have fine-grained control over what gets started, stopped, etc., and what does it first, what depends on another thing, etc.

The second big change is the use of an asyncapi attribute to reference all the async calls. An AsyncAPI class was created to keep things straight (what calls depend upon Twisted and deferreds, and which ones don't). The intent with this move is to continue this trend with the other code (in future branches): a ConfigAPI and configapi attribute, ExtraInfoAPI and extrainfoapi attribute, etc.

The other thing to keep in mind with this branch is that it adds lots of TODOs (look for XXX comments) as inline comments and makes recommendations on refactoring a significant portion of the api.py code. In particular, all blocking calls that are going to be made in functions/methods that return deferreds should either be converted to async code that utilizes Twisted to accomplish the same thing, or be run in a thread with deferToThread. Otherwise, you will get poor (blocking) performance and not receive the benefits of Twisted.

Another big refactor that needs to happen is breaking the methods up: they are large and usually have more responsibility than they should. Several TODO comments were made that point the direction for this (e.g., "split this out into sync and share methods"),

No attempt to change any of that was done in this branch, as the focus here was simply to get a daemon created that was twistd-friendly.

To post a comment you must log in.
Revision history for this message
Duncan McGreggor (oubiwann) wrote :

Oh, another note for future work: there are a bunch of pyflakes. Install pyflakes (sudo apt-get install pyflakes) and run it against the accomplishments directory. There's lots of stuff to be fixed.

Similarly with the pep8 tool.

Revision history for this message
Jono Bacon (jonobacon) wrote :

Thanks so much, Duncan, for taking the time to work on this. I have a few questions, bearing in mind some of my more limited Twisted knowledge.

 * It looks like I needed to set my PYTHONPATH, is this the case?
 * I see you created AsyncAPI - does this literally just put these async orientated methods into a different class for the purposes of classification/naming, or does this do anything differently?
 * It looks like each of the classes in service.py overload a set of methods in Twisted (startService, stopService etc) and then applicationFactory in app.py gets everything up and running. Is this right?
 * In the notes you say "all blocking calls that are going to be made in functions/methods that return deferreds should either be converted to async code that utilizes Twisted to accomplish the same thing, or be run in a thread with deferToThread" - I thought that by using inline callbacks (as my code does already) this was doing this the Twisted way and not blocking?

Thanks!

   Jono

Revision history for this message
Duncan McGreggor (oubiwann) wrote :
Download full text (3.8 KiB)

On Thu, Mar 22, 2012 at 1:23 PM, Jono Bacon <email address hidden> wrote:
> Thanks so much, Duncan, for taking the time to work on this. I have a few questions, bearing in mind some of my more limited Twisted knowledge.
>
>  * It looks like I needed to set my PYTHONPATH, is this the case?

I was never able to duplicate the issues you had with importing python
files from the working directory. I added a sys.path.insert(0, ".") to
hopefully overcome that for you. If that works, you should be all set.
If not, we'll have to do some trial and error to see what does work.
There should be no need to set any environment variables for this to
work, though.

>  * I see you created AsyncAPI - does this literally just put these async orientated methods into a different class for the purposes of classification/naming

Yup.

The idea is that all the different types of API calls that are mixed
in the Accomplishments class right now would be organized into their
own class and then instantiated like AsyncAPI is (e.g., self.asyncapi
= AsyncAPI()). This is simply to help with code organization,
readability, etc.

>  * It looks like each of the classes in service.py overload a set of methods in Twisted (startService, stopService etc)

It doesn't overload. It simply implements the method defined by the
interface. Each service can potentially do lots of difference things
in its startService and stopService methods -- all different from each
other, since each service is of a different type and has different
responsibilities.

This would be a good place to do any DBus main loop tweaks, if needed,
for instance.

(Btw, Python can't really do classic overloading, since a
method/function can only be defined once in the same scope. However,
you can achieve a mostly similar-seeming result with positional and
named args and a dispatch mechanism. You could also override and then
call the superclass's method, that also gets you something similar to
overloading.)

> and then applicationFactory in app.py gets everything up and running. Is this right?

Almost. applicationFactory creates an application object (with lots of
child components) and assigns that to the variable name "application"
in the .tac file. twistd loads the .tac file, looks for the
"application" variable name, loads the object stored there, and fires
of the chain of startService methods on each child object.

So it's twistd that gets everything up and running. applicationFactory
just applies the configuration to various instantiations and then
passes the application object back.

>  * In the notes you say "all blocking calls that are going to be made in functions/methods that return deferreds should either be converted to async code that utilizes Twisted to accomplish the same thing, or be run in a thread with deferToThread" - I thought that by using inline callbacks (as my code does already) this was doing this the Twisted way and not blocking?

Nope. This is one of the many pitfalls of using inlineCallbacks.

inlineCallbacks is basically a wrapper for deferreds. It doesn't
magically make code non-blocking any more than using deferreds
directly does. There's a famous saying in the Twisted community:
  http://www.f...

Read more...

Revision history for this message
Jono Bacon (jonobacon) wrote :
Download full text (5.0 KiB)

Sorry for the delay, Duncan, I am still on vacation.

On 22 March 2012 15:22, Duncan McGreggor <email address hidden> wrote:
>>  * It looks like I needed to set my PYTHONPATH, is this the case?
>
> I was never able to duplicate the issues you had with importing python
> files from the working directory. I added a sys.path.insert(0, ".") to
> hopefully overcome that for you. If that works, you should be all set.
> If not, we'll have to do some trial and error to see what does work.
> There should be no need to set any environment variables for this to
> work, though.

Thanks, Duncan, I will test again soon.

>>  * I see you created AsyncAPI - does this literally just put these async orientated methods into a different class for the purposes of classification/naming
>
> Yup.
>
> The idea is that all the different types of API calls that are mixed
> in the Accomplishments class right now would be organized into their
> own class and then instantiated like AsyncAPI is (e.g., self.asyncapi
> = AsyncAPI()). This is simply to help with code organization,
> readability, etc.

Makes sense. :-)

>>  * It looks like each of the classes in service.py overload a set of methods in Twisted (startService, stopService etc)
>
> It doesn't overload. It simply implements the method defined by the
> interface. Each service can potentially do lots of difference things
> in its startService and stopService methods -- all different from each
> other, since each service is of a different type and has different
> responsibilities.
>
> This would be a good place to do any DBus main loop tweaks, if needed,
> for instance.
>
> (Btw, Python can't really do classic overloading, since a
> method/function can only be defined once in the same scope. However,
> you can achieve a mostly similar-seeming result with positional and
> named args and a dispatch mechanism. You could also override and then
> call the superclass's method, that also gets you something similar to
> overloading.)

So it looks like each of the different services are set up and each of
these are then used throughout the rest of the API (e.g. when we
run_scripts() this uses the ScriptRunner service). I guess the
startService/stopService pieces are then used to help them start and
stop in a twistd way (incorporating logging etc), right?

>> and then applicationFactory in app.py gets everything up and running. Is this right?
>
> Almost. applicationFactory creates an application object (with lots of
> child components) and assigns that to the variable name "application"
> in the .tac file. twistd loads the .tac file, looks for the
> "application" variable name, loads the object stored there, and fires
> of the chain of startService methods on each child object.

I saw mention of a .tac file in the twistd docs, but I didn't see it
in the code. Is there a file somewhere I should see?

> So it's twistd that gets everything up and running. applicationFactory
> just applies the configuration to various instantiations and then
> passes the application object back.

Gotcha. Btw, you showed me how to start a twistd service, how do I stop it?

Also, do you know if there is a good way to map twistd start/stop to
upstart ...

Read more...

Revision history for this message
Duncan McGreggor (oubiwann) wrote :
Download full text (6.4 KiB)

On Mon, Mar 26, 2012 at 2:01 AM, Jono Bacon <email address hidden> wrote:
> On 22 March 2012 15:22, Duncan McGreggor <email address hidden> wrote:
>>
>>>  * It looks like each of the classes in service.py overload a set of methods in Twisted (startService, stopService etc)
>>
>> It doesn't overload. It simply implements the method defined by the
>> interface. Each service can potentially do lots of difference things
>> in its startService and stopService methods -- all different from each
>> other, since each service is of a different type and has different
>> responsibilities.
>>
>> This would be a good place to do any DBus main loop tweaks, if needed,
>> for instance.
>>
>> (Btw, Python can't really do classic overloading, since a
>> method/function can only be defined once in the same scope. However,
>> you can achieve a mostly similar-seeming result with positional and
>> named args and a dispatch mechanism. You could also override and then
>> call the superclass's method, that also gets you something similar to
>> overloading.)
>
> So it looks like each of the different services are set up and each of
> these are then used throughout the rest of the API (e.g. when we
> run_scripts() this uses the ScriptRunner service). I guess the
> startService/stopService pieces are then used to help them start and
> stop in a twistd way (incorporating logging etc), right?

Yup.

>>> and then applicationFactory in app.py gets everything up and running. Is this right?
>>
>> Almost. applicationFactory creates an application object (with lots of
>> child components) and assigns that to the variable name "application"
>> in the .tac file. twistd loads the .tac file, looks for the
>> "application" variable name, loads the object stored there, and fires
>> of the chain of startService methods on each child object.
>
> I saw mention of a .tac file in the twistd docs, but I didn't see it
> in the code. Is there a file somewhere I should see?

Sorry, I renamed it to not use an extension, since it will be used as
a daemon in a long-running process. It's still a .tac file, though.
(it's in the bin dir)

>> So it's twistd that gets everything up and running. applicationFactory
>> just applies the configuration to various instantiations and then
>> passes the application object back.
>
> Gotcha. Btw, you showed me how to start a twistd service, how do I stop it?

So when run in non-daemonized mode (for development), it's just ^C.

When run in daemon mode, it writes std out to a log file
(configurable; see twistd --help) and the PID of the process to a .pid
file (configurable; see twistd --help). Sys V init scripts typically
generate a file or shell variable themselves then later cat the pid
file when running a kill against it. You'll do the same thing here,
except the pid file has already been created for you.

> Also, do you know if there is a good way to map twistd start/stop to
> upstart start/stop events?

I've never written upstart scripts, but twistd is used in all sorts of
init/rc scripts, similarly to how I outlined above.

Jereb (another core hacker/long-time Twisted guy) did a great write-up
here for plugins:
  https://bitbucket.org/jerub/twisted-plugin-exampl...

Read more...

Revision history for this message
Jono Bacon (jonobacon) wrote :

Thanks for your work on thus Duncan. I think I have my head around this now. I took a good look at it today and fixed the issue with trophy_received and I have just merged this in.

I might drop you a message every so often if I have further Twisted questions if that is OK. Thanks!

review: Approve
Revision history for this message
Duncan McGreggor (oubiwann) wrote :

On Sat, Mar 31, 2012 at 8:06 PM, Jono Bacon <email address hidden> wrote:
> Review: Approve
>
> Thanks for your work on thus Duncan. I think I have my head around this now. I took a good look at it today and fixed the issue with trophy_received and I have just merged this in.
>
> I might drop you a message every so often if I have further Twisted questions if that is OK.

You betcha!

> Thanks!

No prob, bro :-)

d

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'accomplishments/daemon/api.py'
2--- accomplishments/daemon/api.py 2012-03-18 16:56:29 +0000
3+++ accomplishments/daemon/api.py 2012-03-20 03:24:21 +0000
4@@ -12,7 +12,6 @@
5 import gobject
6 import gpgme
7 import json
8-import logging
9 import os
10 import pwd
11 import subprocess
12@@ -24,6 +23,7 @@
13 from twisted.internet import defer, reactor
14 from twisted.internet.protocol import ProcessProtocol
15 from twisted.python import filepath
16+from twisted.python import log
17
18 import xdg.BaseDirectory
19
20@@ -38,7 +38,7 @@
21 import accomplishments
22 from accomplishments import exceptions
23 from accomplishments.daemon import dbusapi
24-from accomplishments.util import get_data_file
25+from accomplishments.util import get_data_file, SubprocessReturnCodeProtocol
26
27
28 MATRIX_USERNAME = "openiduser155707"
29@@ -46,20 +46,229 @@
30 SCRIPT_DELAY = 900
31
32
33-class SubprocessReturnCodeProtocol(ProcessProtocol):
34- """
35- """
36- def connectionMade(self):
37- self.returnCodeDeferred = defer.Deferred()
38-
39- def processEnded(self, reason):
40- self.returnCodeDeferred.callback(reason.value.exitCode)
41-
42- def outReceived(self, data):
43- print data
44-
45- def errReceived(self, data):
46- print data
47+# XXX the source code needs to be updated to use Twisted async calls better:
48+# grep the source code for any *.asyncapi.* references, and if they return
49+# deferreds, adjust them to use callbacks
50+class AsyncAPI(object):
51+ """
52+ This class simply organizes all the Twisted calls into a single location
53+ for better readability and separation of concerns.
54+ """
55+ def __init__(self, parent):
56+ self.parent = parent
57+
58+ @staticmethod
59+ def run_a_subprocess(command):
60+ log.msg("Running subprocess command: " + str(command))
61+ pprotocol = SubprocessReturnCodeProtocol()
62+ reactor.spawnProcess(pprotocol, command[0], command, env=os.environ)
63+ return pprotocol.returnCodeDeferred
64+
65+ # XXX let's rewrite this to use deferreds explicitly
66+ @defer.inlineCallbacks
67+ def wait_until_a_sig_file_arrives(self):
68+ path, info = yield self.parent.sd.wait_for_signals(
69+ signal_ok="DownloadFinished",
70+ success_filter=lambda path,
71+ info: path.startswith(self.trophies_path)
72+ and path.endswith(".asc"))
73+ log.msg("Trophy signature recieved...")
74+ accomname = os.path.splitext(os.path.splitext(
75+ os.path.split(path)[1])[0])[0]
76+ data = self.parent.listAccomplishmentInfo(accomname)
77+ iconpath = os.path.join(
78+ self.parent.accomplishments_path,
79+ data[0]["application"],
80+ "trophyimages",
81+ data[0]["icon"])
82+
83+ item = os.path.split(path)[1][:-11]
84+ app = os.path.split(os.path.split(path)[0])[1]
85+ data = self.parent.listAccomplishmentInfo(item)
86+
87+ if self.parent.scriptrun_total == len(self.parent.scriptrun_results):
88+ self.parent.show_unlocked_accomplishments()
89+
90+ if self.parent.show_notifications == True and pynotify and (
91+ pynotify.is_initted() or pynotify.init("icon-summary-body")):
92+ self.parent.service.trophy_received("foo")
93+ trophy_icon_path = "file://%s" % os.path.realpath(
94+ os.path.join(
95+ os.path.split(__file__)[0],
96+ "trophy-accomplished.svg"))
97+ n = pynotify.Notification(
98+ "You have accomplished something!", data[0]["title"], iconpath)
99+ n.show()
100+
101+ self.wait_until_a_sig_file_arrives()
102+ #reload_trophy_corresponding_to_sig_file(path)
103+
104+ # XXX let's rewrite this to use deferreds explicitly
105+ @defer.inlineCallbacks
106+ def register_trophy_dir(self, trophydir):
107+ """
108+ Creates the Ubuntu One share for the trophydir and offers it to the
109+ server. Returns True if the folder was successfully shared, False if
110+ not.
111+ """
112+ timeid = str(time.time())
113+ log.msg("Registering Ubuntu One share directory: " + trophydir)
114+
115+ folder_list = yield self.parent.sd.get_folders()
116+ folder_is_synced = False
117+ for folder in folder_list:
118+ if folder["path"] == trophydir:
119+ folder_is_synced = True
120+ break
121+ if not folder_is_synced:
122+ # XXX let's breack this out into a separate sync'ing method
123+ log.msg(
124+ "...the '%s' folder is not synced with the Matrix" % trophydir)
125+ log.msg("...creating the share folder on Ubuntu One")
126+ self.parent.sd.create_folder(trophydir)
127+
128+ success_filter = lambda info: info["path"] == trophydir
129+ info = yield self.parent.sd.wait_for_signals(
130+ signal_ok='FolderCreated', success_filter=success_filter)
131+
132+ self.parent.sd.offer_share(
133+ trophydir, MATRIX_USERNAME, LOCAL_USERNAME + " Trophies Folder"
134+ + " (" + timeid + ")", "Modify")
135+ log.msg(
136+ "...share has been offered (" + trophydir + "" + ", "
137+ + MATRIX_USERNAME + ", " + LOCAL_USERNAME + ")")
138+ return
139+
140+ log.msg("...the '%s' folder is already synced" % trophydir)
141+ # XXX put the following logic into a folders (plural) sharing method
142+ log.msg("... now checking whether it's shared")
143+ shared_list = yield self.parent.sd.list_shared()
144+ folder_is_shared = False
145+ shared_to = []
146+ for share in shared_list:
147+ # XXX let's break this out into a separate share-tracking method
148+ if share["path"] == trophydir:
149+ log.msg("...the folder is already shared.")
150+ folder_is_shared = True
151+ shared_to.append("%s (%s)" % (
152+ share["other_visible_name"], share["other_username"]))
153+ if not folder_is_shared:
154+ # XXX let's break this out into a separate folder-sharing method
155+ log.msg("...the '%s' folder is not shared" % trophydir)
156+ self.parent.sd.offer_share(
157+ trophydir, MATRIX_USERNAME, LOCAL_USERNAME + " Trophies Folder"
158+ + " (" + timeid + ")", "Modify")
159+ log.msg("...share has been offered (" + trophydir + "" + ", "
160+ + MATRIX_USERNAME + ", " + LOCAL_USERNAME + ")")
161+ log.msg("...offered the share.")
162+ return
163+ else:
164+ log.msg("The folder is shared, with: %s" % ", ".join(
165+ shared_to))
166+ return
167+
168+ # XXX let's rewrite this to use deferreds explicitly
169+ @defer.inlineCallbacks
170+ def run_scripts_for_user(self, uid):
171+ log.msg("--- Starting Running Scripts ---")
172+ timestart = time.time()
173+ self.parent.service.scriptrunner_start()
174+
175+ # Is the user currently logged in and running a gnome session?
176+ # XXX use deferToThread
177+ username = pwd.getpwuid(uid).pw_name
178+ try:
179+ # XXX since we're using Twisted, let's use it here too and use the
180+ # deferred-returning call
181+ proc = subprocess.check_output(
182+ ["pgrep", "-u", username, "gnome-session"]).strip()
183+ except subprocess.CalledProcessError:
184+ # user does not have gnome-session running or isn't logged in at
185+ # all
186+ log.msg("No gnome-session process for user %s" % username)
187+ return
188+ # XXX this is a blocking call and can't be here if we want to take
189+ # advantage of deferreds; instead, rewrite this so that the blocking
190+ # call occurs in a separate thread (e.g., deferToThread)
191+ fp = open("/proc/%s/environ" % proc)
192+ try:
193+ envars = dict(
194+ [line.split("=", 1) for line in fp.read().split("\0")
195+ if line.strip()])
196+ except IOError:
197+ # user does not have gnome-session running or isn't logged in at
198+ # all
199+ log.msg("No gnome-session environment for user %s" % username)
200+ return
201+ fp.close()
202+
203+ # XXX use deferToThread
204+ os.seteuid(uid)
205+
206+ required_envars = ['DBUS_SESSION_BUS_ADDRESS']
207+ env = dict([kv for kv in envars.items() if kv[0] in required_envars])
208+ # XXX use deferToThread
209+ oldenviron = os.environ
210+ os.environ.update(env)
211+ # XXX note that for many of these deferredToThread changes, we can put
212+ # them all in a DeferredList and once they're all done and we have the
213+ # results for all of them, a callback can be fired to continue.
214+
215+ # XXX this next call, a DBus check, happens in the middle of this
216+ # method; it would be better if this check was done at a higher level,
217+ # for instance, where this class is initiated: if the daemon isn't
218+ # registered at the time of instantiation, simply abort then instead of
219+ # making all the way here and then aborting. (Note that moving this
220+ # check to that location will also eliminate an obvious circular
221+ # import.)
222+ if not dbusapi.daemon_is_registered():
223+ return
224+
225+ # XXX all parent calls should be refactored out of the AsyncAPI class
226+ # to keep the code cleaner and the logic more limited to one particular
227+ # task
228+ accoms = self.parent.listAllAvailableAccomplishmentsWithScripts()
229+ totalscripts = len(accoms)
230+ self.parent.scriptrun_total = totalscripts
231+ log.msg("Need to run (%d) scripts" % totalscripts)
232+
233+ scriptcount = 1
234+ for accom in accoms:
235+ msg = "%s/%s: %s" % (scriptcount, totalscripts, accom["_script"])
236+ log.msg(msg)
237+ exitcode = yield self.run_a_subprocess([accom["_script"]])
238+ if exitcode == 0:
239+ self.parent.scriptrun_results.append(
240+ str(accom["application"]) + "/"
241+ + str(accom["accomplishment"]))
242+ self.parent.accomplish(
243+ accom["application"], accom["accomplishment"])
244+ log.msg("...Accomplished")
245+ elif exitcode == 1:
246+ self.parent.scriptrun_results.append(None)
247+ log.msg("...Not Accomplished")
248+ elif exitcode == 2:
249+ self.parent.scriptrun_results.append(None)
250+ log.msg("....Error")
251+ elif exitcode == 4:
252+ self.parent.scriptrun_results.append(None)
253+ log.msg("...Could not get launchpad email")
254+ else:
255+ self.parent.scriptrun_results.append(None)
256+ log.msg("...Error code %d" % exitcode)
257+ scriptcount = scriptcount + 1
258+
259+ os.environ = oldenviron
260+
261+ # XXX eventually the code in this method will be rewritten using
262+ # deferreds; as such, we're going to have to be more clever regarding
263+ # timing things...
264+ timeend = time.time()
265+ timefinal = round((timeend - timestart), 2)
266+
267+ log.msg(
268+ "--- Completed Running Scripts in %.2f seconds---" % timefinal)
269+ self.parent.service.scriptrunner_finish()
270
271
272 class Accomplishments(object):
273@@ -82,9 +291,9 @@
274 self.scriptrun_results = []
275 self.depends = []
276 self.processing_unlocked = False
277+ self.asyncapi = AsyncAPI(self)
278
279 # create config / data dirs if they don't exist
280-
281 self.dir_config = os.path.join(
282 xdg.BaseDirectory.xdg_config_home, "accomplishments")
283 self.dir_data = os.path.join(
284@@ -101,73 +310,26 @@
285 if not os.path.exists(self.dir_cache):
286 os.makedirs(self.dir_cache)
287
288- # set up logging
289- logdir = os.path.join(self.dir_cache, "logs")
290-
291- if not os.path.exists(logdir):
292- os.makedirs(logdir)
293-
294- #self.logging = logging
295- logging.basicConfig(
296- filename=(os.path.join(logdir, 'daemon.log')), level=logging.INFO)
297-
298- now = datetime.datetime.now()
299- logging.info(
300+ log.msg(
301 "------------------- Ubuntu Accomplishments Daemon Log - %s "
302- "-------------------", str(now))
303+ "-------------------", str(datetime.datetime.now()))
304
305 self._loadConfigFile()
306
307- print "Accomplishments path: " + self.accomplishments_path
308- print "Scripts path: " + self.scripts_path
309- print "Trophies path: " + self.trophies_path
310+ log.msg("Accomplishments path: " + self.accomplishments_path)
311+ log.msg("Scripts path: " + self.scripts_path)
312+ log.msg("Trophies path: " + self.trophies_path)
313
314 self.show_notifications = show_notifications
315- logging.info("Connecting to Ubuntu One")
316+ log.msg("Connecting to Ubuntu One")
317 self.sd = SyncDaemonTool()
318
319- self.wait_until_a_sig_file_arrives()
320+ # XXX this wait-until thing should go away; it should be replaced by a
321+ # deferred-returning function that has a callback which fires off
322+ # generate_all_trophis and schedule_run_scripts...
323+ self.asyncapi.wait_until_a_sig_file_arrives()
324 self.generate_all_trophies()
325
326- reactor.callLater(5, self.run_scripts, False)
327-
328- @defer.inlineCallbacks
329- def wait_until_a_sig_file_arrives(self):
330- path, info = yield self.sd.wait_for_signals(
331- signal_ok="DownloadFinished",
332- success_filter=lambda path,
333- info: path.startswith(self.trophies_path)
334- and path.endswith(".asc"))
335- logging.info("Trophy signature recieved...")
336- accomname = os.path.splitext(os.path.splitext(
337- os.path.split(path)[1])[0])[0]
338- data = self.listAccomplishmentInfo(accomname)
339- iconpath = os.path.join(
340- self.accomplishments_path,
341- data[0]["application"],
342- "trophyimages",
343- data[0]["icon"])
344-
345- item = os.path.split(path)[1][:-11]
346- app = os.path.split(os.path.split(path)[0])[1]
347- data = self.listAccomplishmentInfo(item)
348-
349- if self.scriptrun_total == len(self.scriptrun_results):
350- self.show_unlocked_accomplishments()
351-
352- if self.show_notifications == True and pynotify and (
353- pynotify.is_initted() or pynotify.init("icon-summary-body")):
354- self.service.trophy_received("foo")
355- trophy_icon_path = "file://%s" % os.path.realpath(
356- os.path.join(
357- os.path.split(__file__)[0],
358- "trophy-accomplished.svg"))
359- n = pynotify.Notification(
360- "You have accomplished something!", data[0]["title"], iconpath)
361- n.show()
362-
363- self.wait_until_a_sig_file_arrives()
364- #reload_trophy_corresponding_to_sig_file(path)
365
366 def get_media_file(self, media_file_name):
367 media_filename = get_data_file('media', '%s' % (media_file_name,))
368@@ -210,14 +372,14 @@
369 self.depends = []
370
371 def _get_accomplishments_files_list(self):
372- logging.info("Looking for accomplishments files in "
373+ log.msg("Looking for accomplishments files in "
374 + self.accomplishments_path)
375 accom_files = os.path.join(self.accomplishments_path,
376 "*", "*.accomplishment")
377 return glob.glob(accom_files)
378
379 def _load_accomplishment_file(self, f):
380- logging.info("Loading accomplishments file: " + f)
381+ log.msg("Loading accomplishments file: " + f)
382 config = ConfigParser.RawConfigParser()
383 config.read(f)
384 data = dict(config._sections["accomplishment"])
385@@ -232,7 +394,7 @@
386 Returns True for valid or False for invalid (missing file, bad sig
387 etc).
388 """
389- logging.info("Validate trophy: " + str(filename))
390+ log.msg("Validate trophy: " + str(filename))
391
392 if os.path.exists(filename):
393 # the .asc signed file exists, so let's verify that it is correctly
394@@ -246,24 +408,24 @@
395 sig = c.verify(signed, None, plaintext)
396
397 if len(sig) != 1:
398- logging.info("...No Sig")
399+ log.msg("...No Sig")
400 return False
401
402 if sig[0].status is not None:
403- logging.info("...Bad Sig")
404+ log.msg("...Bad Sig")
405 return False
406 else:
407 result = {'timestamp': sig[0].timestamp, 'signer': sig[0].fpr}
408- logging.info("...Verified!")
409+ log.msg("...Verified!")
410 return True
411 else:
412- logging.info(".asc does not exist for this trophy")
413+ log.msg(".asc does not exist for this trophy")
414 return False
415
416- logging.info("Verifying trophy signature")
417+ log.msg("Verifying trophy signature")
418
419 def _load_trophy_file(self, f):
420- logging.info("Load trophy file: " + f)
421+ log.msg("Load trophy file: " + f)
422 config = ConfigParser.RawConfigParser()
423 config.read(f)
424 data = dict(config._sections["trophy"])
425@@ -272,7 +434,7 @@
426 return data
427
428 def listAllAccomplishments(self):
429- logging.info("List all accomplishments")
430+ log.msg("List all accomplishments")
431 fs = [self._load_accomplishment_file(f) for f in
432 self._get_accomplishments_files_list()]
433 return fs
434@@ -351,32 +513,32 @@
435 img = Image.composite(layer, reduced, layer)
436 img.save(filecore + "-locked" + filetype)
437 except Exception, (msg):
438- print msg
439+ log.msg(msg)
440
441 def verifyU1Account(self):
442 # check if this machine has an Ubuntu One account
443- logging.info("Check if this machine has an Ubuntu One account...")
444+ log.msg("Check if this machine has an Ubuntu One account...")
445 u1auth_response = auth.request(
446 url='https://one.ubuntu.com/api/account/')
447 u1email = None
448 if not isinstance(u1auth_response, basestring):
449 u1email = json.loads(u1auth_response[1])['email']
450 else:
451- logging.info("No Ubuntu One account is configured.")
452+ log.msg("No Ubuntu One account is configured.")
453
454 if u1email is None:
455- logging.info("...No.")
456- logging.info(u1auth_response)
457+ log.msg("...No.")
458+ log.msg(u1auth_response)
459 self.has_u1 = False
460 return False
461 else:
462- logging.info("...Yes.")
463+ log.msg("...Yes.")
464 self.has_u1 = True
465 return True
466
467 def getConfigValue(self, section, item):
468 """Return a configuration value from the .accomplishments file"""
469- logging.info(
470+ log.msg(
471 "Returning configuration values for: %s, %s", section, item)
472 homedir = os.getenv("HOME")
473 config = ConfigParser.RawConfigParser()
474@@ -395,7 +557,7 @@
475
476 def setConfigValue(self, section, item, value):
477 """Set a configuration value in the .accomplishments file"""
478- logging.info(
479+ log.msg(
480 "Set configuration file value in '%s': %s = %s", section, item,
481 value)
482 homedir = os.getenv("HOME")
483@@ -415,7 +577,7 @@
484 self._loadConfigFile()
485
486 def _writeConfigFile(self):
487- logging.info("Writing the configuration file")
488+ log.msg("Writing the configuration file")
489 homedir = os.getenv("HOME")
490 config = ConfigParser.RawConfigParser()
491 cfile = self.dir_config + "/.accomplishments"
492@@ -433,10 +595,10 @@
493
494 self.accomplishments_path = os.path.join(
495 self.accomplishments_path, "accomplishments")
496- logging.info("...done.")
497+ log.msg("...done.")
498
499 def accomplish(self, app, accomplishment_name):
500- logging.info(
501+ log.msg(
502 "Accomplishing something: %s, %s", app, accomplishment_name)
503 accom_file = os.path.join(self.accomplishments_path, app,
504 "%s.accomplishment" % accomplishment_name)
505@@ -481,9 +643,9 @@
506 cp.write(fp)
507 fp.close()
508
509- if not data["needs-signing"] or data["needs-signing"] == False:
510- self.trophy_received()
511- if self.show_notifications == True and pynotify and (
512+ if not data["needs-signing"] or data["needs-signing"] is False:
513+ self.service.trophy_received()
514+ if self.show_notifications is True and pynotify and (
515 pynotify.is_initted() or pynotify.init("icon-summary-body")):
516 trophy_icon_path = "file://%s" % os.path.realpath(
517 os.path.join(
518@@ -507,19 +669,19 @@
519 self.has_u1 = True
520
521 if config.read(cfile):
522- logging.info("Loading configuration file: " + cfile)
523+ log.msg("Loading configuration file: " + cfile)
524 if config.get('config', 'accompath'):
525 self.accomplishments_path = os.path.join(
526 config.get('config', 'accompath'), "accomplishments/")
527- logging.info(
528+ log.msg(
529 "...setting accomplishments path to: "
530 + self.accomplishments_path)
531 self.scripts_path = os.path.split(
532 os.path.split(self.accomplishments_path)[0])[0] + "/scripts"
533- logging.info(
534+ log.msg(
535 "...setting scripts path to: " + self.scripts_path)
536 if config.get('config', 'trophypath'):
537- logging.info(
538+ log.msg(
539 "...setting trophies path to: "
540 + config.get('config', 'trophypath'))
541 self.trophies_path = config.get('config', 'trophypath')
542@@ -529,84 +691,23 @@
543 self.has_verif = config.getboolean('config', 'has_verif')
544 else:
545 accompath = os.path.join(homedir, "accomplishments")
546- logging.info("Configuration file not found...creating it!")
547- print "Configuration file not found...creating it!"
548+ log.msg("Configuration file not found...creating it!")
549
550 self.has_verif = False
551 self.accomplishments_path = accompath
552- logging.info(
553+ log.msg(
554 "...setting accomplishments path to: "
555 + self.accomplishments_path)
556 self.trophies_path = os.path.join(self.dir_data, "trophies")
557- logging.info("...setting trophies path to: " + self.trophies_path)
558+ log.msg("...setting trophies path to: " + self.trophies_path)
559 self.scripts_path = os.path.join(accompath, "scripts")
560- logging.info("...setting scripts path to: " + self.scripts_path)
561+ log.msg("...setting scripts path to: " + self.scripts_path)
562
563 if not os.path.exists(self.trophies_path):
564 os.makedirs(self.trophies_path)
565
566 self._writeConfigFile()
567
568- @defer.inlineCallbacks
569- def registerTrophyDir(self, trophydir):
570- """
571- Creates the Ubuntu One share for the trophydir and offers it to the
572- server. Returns True if the folder was successfully shared, False if
573- not.
574- """
575- timeid = str(time.time())
576- logging.info("Registering Ubuntu One share directory: " + trophydir)
577-
578- folder_list = yield self.sd.get_folders()
579- folder_is_synced = False
580- for folder in folder_list:
581- if folder["path"] == trophydir:
582- folder_is_synced = True
583- break
584- if not folder_is_synced:
585- logging.info(
586- "...the '%s' folder is not synced with the Matrix" % trophydir)
587- logging.info("...creating the share folder on Ubuntu One")
588- self.sd.create_folder(trophydir)
589-
590- success_filter = lambda info: info["path"] == trophydir
591- info = yield self.sd.wait_for_signals(
592- signal_ok='FolderCreated', success_filter=success_filter)
593-
594- self.sd.offer_share(
595- trophydir, MATRIX_USERNAME, LOCAL_USERNAME + " Trophies Folder"
596- + " (" + timeid + ")", "Modify")
597- logging.info(
598- "...share has been offered (" + trophydir + "" + ", "
599- + MATRIX_USERNAME + ", " + LOCAL_USERNAME + ")")
600- return
601-
602- logging.info(
603- "...the '%s' folder is already synced... now checking whether "
604- "it's shared" % trophydir)
605- shared_list = yield self.sd.list_shared()
606- folder_is_shared = False
607- shared_to = []
608- for share in shared_list:
609- if share["path"] == trophydir:
610- logging.info("...the folder is already shared.")
611- folder_is_shared = True
612- shared_to.append("%s (%s)" % (
613- share["other_visible_name"], share["other_username"]))
614- if not folder_is_shared:
615- logging.info("...the '%s' folder is not shared" % trophydir)
616- self.sd.offer_share(
617- trophydir, MATRIX_USERNAME, LOCAL_USERNAME + " Trophies Folder"
618- + " (" + timeid + ")", "Modify")
619- logging.info("...share has been offered (" + trophydir + "" + ", "
620- + MATRIX_USERNAME + ", " + LOCAL_USERNAME + ")")
621- logging.info("...offered the share.")
622- return
623- else:
624- logging.info("The folder is shared, with: %s" % ", ".join(
625- shared_to))
626- return
627-
628 def getAllExtraInformationRequired(self):
629 """
630 Return a dictionary of all information required for the accomplishments
631@@ -664,7 +765,7 @@
632 else:
633 getdepends = False
634
635- logging.info("List all accomplishments and status")
636+ log.msg("List all accomplishments and status")
637 accomplishments_files = self._get_accomplishments_files_list()
638 things = {}
639 for accomplishment_file in accomplishments_files:
640@@ -748,7 +849,7 @@
641 return things.values()
642
643 def listAllAvailableAccomplishmentsWithScripts(self):
644- logging.info("List all accomplishments with scripts")
645+ log.msg("List all accomplishments with scripts")
646 available = [accom for accom in self.listAllAccomplishmentsAndStatus()
647 if not accom["accomplished"] and not accom["locked"]]
648 withscripts = []
649@@ -764,7 +865,7 @@
650 return withscripts
651
652 def listAccomplishmentInfo(self, accomplishment):
653- logging.info("Getting accomplishment info for " + accomplishment)
654+ log.msg("Getting accomplishment info for " + accomplishment)
655 search = "/" + accomplishment + ".accomplishment"
656 files = self._get_accomplishments_files_list()
657 match = None
658@@ -780,119 +881,23 @@
659 data.append(dict(config._sections["accomplishment"]))
660 return data
661
662- @defer.inlineCallbacks
663- def run_scripts_for_user(self, uid):
664- print "--- Starting Running Scripts ---"
665- logging.info("--- Starting Running Scripts ---")
666- timestart = time.time()
667- self.service.scriptrunner_start()
668-
669- # Is the user currently logged in and running a gnome session?
670- username = pwd.getpwuid(uid).pw_name
671- try:
672- proc = subprocess.check_output(
673- ["pgrep", "-u", username, "gnome-session"]).strip()
674- except subprocess.CalledProcessError:
675- # user does not have gnome-session running or isn't logged in at
676- # all
677- logging.info("No gnome-session process for user %s" % username)
678- return
679- fp = open("/proc/%s/environ" % proc)
680- try:
681- envars = dict(
682- [line.split("=", 1) for line in fp.read().split("\0")
683- if line.strip()])
684- except IOError:
685- # user does not have gnome-session running or isn't logged in at
686- # all
687- logging.info("No gnome-session environment for user %s" % username)
688- return
689- fp.close()
690-
691- os.seteuid(uid)
692-
693- required_envars = ['DBUS_SESSION_BUS_ADDRESS']
694- env = dict([kv for kv in envars.items() if kv[0] in required_envars])
695- oldenviron = os.environ
696- os.environ.update(env)
697- # XXX this DBUS check happens in the middle of this method; it would be
698- # better if this check was done at a higher level, for instance, where
699- # this class is initiated: if the daemon isn't registered at the time
700- # of instantiation, simply abort then instead of making all the way
701- # here and then aborting. (Note that moving this check to that location
702- # will also eliminate an obvious circular import.)
703- if not dbusapi.daemon_is_registered():
704- return
705-
706- accoms = self.listAllAvailableAccomplishmentsWithScripts()
707- totalscripts = len(accoms)
708- self.scriptrun_total = totalscripts
709- logging.info("Need to run (%d) scripts" % totalscripts)
710- print "Need to run (%d) scripts" % totalscripts
711-
712- scriptcount = 1
713- for accom in accoms:
714- msg = "%s/%s: %s" % (scriptcount, totalscripts, accom["_script"])
715- print msg
716- logging.info(msg)
717- exitcode = yield self.run_a_subprocess([accom["_script"]])
718- if exitcode == 0:
719- self.scriptrun_results.append(
720- str(accom["application"]) + "/"
721- + str(accom["accomplishment"]))
722- self.accomplish(accom["application"], accom["accomplishment"])
723- print "...Accomplished"
724- logging.info("...Accomplished")
725- elif exitcode == 1:
726- self.scriptrun_results.append(None)
727- print "...Not Accomplished"
728- logging.info("...Not Accomplished")
729- elif exitcode == 2:
730- self.scriptrun_results.append(None)
731- print "...Error"
732- logging.info("....Error")
733- else:
734- self.scriptrun_results.append(None)
735- print "...Other error code."
736- logging.info("...Other error code")
737- scriptcount = scriptcount + 1
738-
739- os.environ = oldenviron
740-
741- timeend = time.time()
742- timefinal = round((timeend - timestart), 2)
743-
744- print "--- Completed Running Scripts in %.2f seconds ---" % timefinal
745- logging.info(
746- "--- Completed Running Scripts in %.2f seconds---" % timefinal)
747- self.service.scriptrunner_finish()
748-
749- def run_a_subprocess(self, command):
750- logging.info("Running subprocess command: " + str(command))
751- pprotocol = SubprocessReturnCodeProtocol()
752- reactor.spawnProcess(pprotocol, command[0], command, env=os.environ)
753- return pprotocol.returnCodeDeferred
754-
755 def run_scripts_for_all_active_users(self):
756 for uid in [x.pw_uid for x in pwd.getpwall()
757 if x.pw_dir.startswith('/home/') and x.pw_shell != '/bin/false']:
758 os.seteuid(0)
759- self.run_scripts_for_user(uid)
760+ self.asyncapi.run_scripts_for_user(uid)
761
762 def run_scripts(self, run_by_client):
763 uid = os.getuid()
764 if uid == 0:
765- logging.info("Run scripts for all active users")
766+ log.msg("Run scripts for all active users")
767 self.run_scripts_for_all_active_users()
768 else:
769- logging.info("Run scripts for user")
770- self.run_scripts_for_user(uid)
771-
772- if run_by_client is False:
773- reactor.callLater(SCRIPT_DELAY, self.run_scripts, False)
774+ log.msg("Run scripts for user")
775+ self.asyncapi.run_scripts_for_user(uid)
776
777 def createExtraInformationFile(self, app, item, data):
778- logging.info(
779+ log.msg(
780 "Creating Extra Information file: %s, %s, %s", app, item, data)
781 extrainfodir = os.path.join(self.trophies_path, ".extrainformation/")
782
783@@ -906,7 +911,6 @@
784 f.write(data)
785 f.close()
786
787-
788 def getExtraInformation(self, app, item):
789 extrainfopath = os.path.join(self.trophies_path, ".extrainformation/")
790 authfile = os.path.join(extrainfopath, item)
791@@ -918,10 +922,3 @@
792 #print "No data."
793 final = [{item : False}]
794 return final
795-
796-
797- # XXX once the other reference to dbusapi is removed from this file, this
798- # will be the last one. It doesn't really belong here... we can set a whole
799- # slew of dbus api calls on this object when it is instantiated, thus
800- # alleviating us from the burden of a hack like this below
801- trophy_received = dbusapi.DBUSSignals.trophy_received
802
803=== modified file 'accomplishments/daemon/app.py'
804--- accomplishments/daemon/app.py 2012-03-18 16:56:29 +0000
805+++ accomplishments/daemon/app.py 2012-03-20 03:24:21 +0000
806@@ -1,13 +1,15 @@
807 from optparse import OptionParser
808
809-from twisted.internet import glib2reactor
810-glib2reactor.install()
811-from twisted.internet import reactor
812+from twisted.application.internet import TimerService
813+from twisted.application.service import Application
814
815 from accomplishments.daemon import dbusapi
816-
817-
818-if __name__ == "__main__":
819+from accomplishments.daemon import service
820+
821+
822+# XXX these won't work with twistd; we need to write a twistd plugin to support
823+# additional command line options.
824+def parse_options():
825 parser = OptionParser()
826 parser.set_defaults(suppress_notifications=False)
827 parser.add_option("--trophies-path", dest="trophies_path", default=None)
828@@ -16,8 +18,31 @@
829 parser.add_option("--scripts-path", dest="scripts_path", default=None)
830 parser.add_option("--suppress-notifications", action="store_true",
831 dest="suppress_notifications")
832- options, args = parser.parse_args()
833-
834- service = dbusapi.AccomplishmentsService(
835- show_notifications=not options.suppress_notifications)
836- reactor.run()
837+ return parser.parse_args()
838+
839+
840+def applicationFactory(app_name="", bus_name="", main_loop=None,
841+ session_bus=None, object_path="/", update_interval=3600,
842+ gpg_key=""):
843+ # create the application object
844+ application = Application(app_name)
845+ # create the top-level service object that will contain all others; it will
846+ # not shutdown until all child services have been terminated
847+ top_level_service = service.AccomplishmentsDaemonService(gpg_key)
848+ top_level_service.setServiceParent(application)
849+ # create the service that all DBus services will rely upon (this parent
850+ # service will wait until child DBus services are shutdown before it shuts
851+ # down
852+ dbus_service = service.DBusService(main_loop, session_bus)
853+ dbus_service.setServiceParent(top_level_service)
854+ # create a child dbus serivce
855+ dbus_export_service = dbusapi.AccomplishmentsDBusService(
856+ bus_name, session_bus, object_path=object_path,
857+ show_notifications=False)
858+ dbus_export_service.setServiceParent(dbus_service)
859+ # create a service that will run the scripts at a regular interval
860+ timer_service = service.ScriptRunnerService(
861+ update_interval, dbus_export_service.api)
862+ timer_service.setServiceParent(top_level_service)
863+
864+ return application
865
866=== modified file 'accomplishments/daemon/dbusapi.py'
867--- accomplishments/daemon/dbusapi.py 2012-03-18 16:56:29 +0000
868+++ accomplishments/daemon/dbusapi.py 2012-03-20 03:24:21 +0000
869@@ -3,11 +3,10 @@
870 # GObject-introspection-capable C thing so anyone can use it. But someone else
871 # needs to write that because I'm crap at Vala.
872 import dbus
873-import dbus.service
874-from dbus.mainloop.glib import DBusGMainLoop
875-
876-
877-DBusGMainLoop(set_as_default=True)
878+
879+from twisted.python import log
880+
881+from accomplishments.daemon import service
882
883
884 def daemon_is_registered():
885@@ -18,7 +17,7 @@
886 "org.ubuntu.accomplishments", "/")
887 return True
888 except dbus.exceptions.DBusException:
889- logging.info(
890+ log.msg(
891 "User %s does not have the accomplishments daemon "
892 "available" % username)
893 return False
894@@ -26,6 +25,17 @@
895
896 # XXX as a function, this needs to be renamed to a lower-case function name and
897 # not use the class naming convention of upper-case.
898+#
899+# Hrm, on second thought... this is:
900+# * a singleton object (SessionBus)
901+# * obtaining an object indicated by string constants
902+# * and then doing a lookup on these
903+# In other words, nothing changes ;-)
904+#
905+# As such, there's no need for this to be a function; instead, we can set our
906+# own module-level singleton, perhaps as part of the AccomplishmentsDBusService
907+# set up, since that object has access to all the configuration used here (bus
908+# name and bus path).
909 def Accomplishments():
910 """
911 """
912@@ -33,23 +43,14 @@
913 return dbus.Interface(obj, "org.ubuntu.accomplishments")
914
915
916-class DBUSSignals(object):
917- """
918- """
919- @staticmethod
920- @dbus.service.signal(dbus_interface='org.ubuntu.accomplishments')
921- def trophy_received(self, trophy):
922- t = "mytrophy"
923- return t
924-
925-
926-class AccomplishmentsService(dbus.service.Object):
927- """
928- """
929- def __init__(self, show_notifications=True):
930- bus_name = dbus.service.BusName(
931- 'org.ubuntu.accomplishments', bus=dbus.SessionBus())
932- dbus.service.Object.__init__(self, bus_name, '/')
933+class AccomplishmentsDBusService(service.DBusExportService):
934+ """
935+ """
936+ def __init__(self, bus_name, session_bus, object_path="/",
937+ show_notifications=True):
938+ super(AccomplishmentsDBusService, self).__init__(bus_name, session_bus)
939+ bus_name = dbus.service.BusName(bus_name, bus=session_bus)
940+ dbus.service.Object.__init__(self, bus_name, object_path)
941 self.show_notifications = show_notifications
942 # XXX until all the imports are cleaned up and the code is organized
943 # properly, we're doing the import here (to avoid circular imports).
944@@ -57,76 +58,83 @@
945 # this is not a subclass of dbus.service.Object *and* Accomplishments
946 # because doing that confuses everything, so we create our own
947 # private Accomplishments object and use it.
948- self.ad = api.Accomplishments(self, self.show_notifications)
949+ self.api = api.Accomplishments(self, self.show_notifications)
950
951 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
952 in_signature="", out_signature="aa{sv}")
953 def listAllAccomplishments(self):
954- return self.ad.listAllAccomplishments()
955+ return self.api.listAllAccomplishments()
956
957 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
958 in_signature="", out_signature="aa{sv}")
959 def listAllAccomplishmentsAndStatus(self):
960- return self.ad.listAllAccomplishmentsAndStatus()
961+ return self.api.listAllAccomplishmentsAndStatus()
962
963 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
964 in_signature="", out_signature="aa{sv}")
965 def listAllAvailableAccomplishmentsWithScripts(self):
966- return self.ad.listAllAvailableAccomplishmentsWithScripts()
967+ return self.api.listAllAvailableAccomplishmentsWithScripts()
968
969 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
970 in_signature="", out_signature="aa{sv}")
971 def getAllExtraInformationRequired(self):
972- return self.ad.getAllExtraInformationRequired()
973+ return self.api.getAllExtraInformationRequired()
974
975 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
976 in_signature="", out_signature="aa{sv}")
977 def listAccomplishmentInfo(self, accomplishment):
978- return self.ad.listAccomplishmentInfo(accomplishment)
979+ return self.api.listAccomplishmentInfo(accomplishment)
980
981 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
982 in_signature="b", out_signature="")
983 def run_scripts(self, run_by_client):
984- return self.ad.run_scripts(run_by_client)
985+ return self.api.run_scripts(run_by_client)
986
987 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
988 in_signature="", out_signature="aa{sv}")
989 def getExtraInformation(self, app, info):
990- return self.ad.getExtraInformation(app, info)
991+ return self.api.getExtraInformation(app, info)
992
993 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
994 in_signature="", out_signature="")
995 def createExtraInformationFile(self, app, item, data):
996- return self.ad.createExtraInformationFile(app, item, data)
997+ return self.api.createExtraInformationFile(app, item, data)
998
999 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
1000 in_signature="ss", out_signature="")
1001 def accomplish(self, app, accomplishment_name):
1002- trophy = self.ad.accomplish(app, accomplishment_name)
1003+ trophy = self.api.accomplish(app, accomplishment_name)
1004
1005 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
1006 in_signature="s", out_signature="b")
1007- def registerTrophyDir(self, trophydir):
1008- return self.ad.registerTrophyDir(trophydir)
1009+ def register_trophy_dir(self, trophydir):
1010+ return self.api.async.register_trophy_dir(trophydir)
1011
1012 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
1013 in_signature="vv", out_signature="v")
1014 def getConfigValue(self, section, item):
1015- return self.ad.getConfigValue(section, item)
1016+ return self.api.getConfigValue(section, item)
1017
1018 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
1019 in_signature="vvv", out_signature="")
1020 def setConfigValue(self, section, item, value):
1021- return self.ad.setConfigValue(section, item, value)
1022+ return self.api.setConfigValue(section, item, value)
1023
1024 @dbus.service.method(dbus_interface='org.ubuntu.accomplishments',
1025 in_signature="", out_signature="b")
1026 def verifyU1Account(self):
1027- return self.ad.verifyU1Account()
1028+ return self.api.verifyU1Account()
1029
1030+ # XXX this looks like an unintentional duplicate of the "other"
1031+ # trophy_received... I've moved them here together so that someone in the
1032+ # know (Jono?) can clarify and remove the one that's not needed
1033 @dbus.service.signal(dbus_interface='org.ubuntu.accomplishments')
1034 def trophy_received(self, trophy):
1035 pass
1036+ @dbus.service.signal(dbus_interface='org.ubuntu.accomplishments')
1037+ def trophy_received(trophy):
1038+ t = "mytrophy"
1039+ return t
1040
1041 @dbus.service.signal(dbus_interface='org.ubuntu.accomplishments')
1042 def scriptrunner_start(self):
1043
1044=== added file 'accomplishments/daemon/service.py'
1045--- accomplishments/daemon/service.py 1970-01-01 00:00:00 +0000
1046+++ accomplishments/daemon/service.py 2012-03-20 03:24:21 +0000
1047@@ -0,0 +1,80 @@
1048+from twisted.application.internet import TimerService
1049+from twisted.application.service import MultiService
1050+from twisted.application.service import Service
1051+from twisted.python import log
1052+
1053+import dbus.service
1054+
1055+from accomplishments import util
1056+
1057+
1058+class AccomplishmentsDaemonService(MultiService):
1059+ """
1060+ The top-level service that all other services should set as their parent.
1061+ """
1062+ def __init__(self, gpg_pub_key):
1063+ MultiService.__init__(self)
1064+ self.gpg_key = gpg_pub_key
1065+
1066+ def startService(self):
1067+ log.msg("Starting up accomplishments daemon ...")
1068+ util.import_gpg_key(self.gpg_key)
1069+ MultiService.startService(self)
1070+
1071+ def stopService(self):
1072+ log.msg("Shutting down accomplishments daemon ...")
1073+ return MultiService.stopService(self)
1074+
1075+
1076+class DBusService(MultiService):
1077+ """
1078+ A Twisted application service that tracks the DBus mainloop and the DBus
1079+ session bus.
1080+ """
1081+ def __init__(self, main_loop, session_bus):
1082+ MultiService.__init__(self)
1083+ self.main_loop = main_loop
1084+ self.session_bus = session_bus
1085+
1086+ def startService(self):
1087+ log.msg("Starting up DBus service ...")
1088+ return MultiService.startService(self)
1089+
1090+ def stopService(self):
1091+ log.msg("Shutting down DBus service ...")
1092+ return MultiService.stopService(self)
1093+
1094+
1095+class DBusExportService(Service, dbus.service.Object):
1096+ """
1097+ A base class that is both a Twisted application service as well as a means
1098+ for exporting custom objects across a given bus.
1099+ """
1100+ def __init__(self, bus_name, session_bus):
1101+ self.bus_name = bus_name
1102+ self.session_bus = session_bus
1103+
1104+ def startService(self):
1105+ log.msg("Starting up API exporter service ...")
1106+ return Service.startService(self)
1107+
1108+ def stopService(self):
1109+ log.msg("Shutting down API exporter service ...")
1110+ return Service.stopService(self)
1111+
1112+
1113+class ScriptRunnerService(TimerService):
1114+ """
1115+ A simple wrapper for the TimerService that runs the scripts at the given
1116+ intertal.
1117+ """
1118+ def __init__(self, interval, api):
1119+ TimerService.__init__(self, interval, api.run_scripts, False)
1120+
1121+ def startService(self):
1122+ log.msg("Starting up script runner service ...")
1123+ return TimerService.startService(self)
1124+
1125+ def stopService(self):
1126+ log.msg("Shutting down script runner service ...")
1127+ return TimerService.stopService(self)
1128
1129=== modified file 'accomplishments/gui/TrophyinfoWindow.py'
1130--- accomplishments/gui/TrophyinfoWindow.py 2012-03-12 16:53:31 +0000
1131+++ accomplishments/gui/TrophyinfoWindow.py 2012-03-20 03:24:21 +0000
1132@@ -233,7 +233,7 @@
1133 self.has_verif = True
1134 self.libaccom.setConfigValue("config", "has_verif", True)
1135
1136- res = self.libaccom.registerTrophyDir(trophydir)
1137+ res = self.libaccom.asyncapi.register_trophy_dir(trophydir)
1138
1139 if res == 1:
1140 self.u1_button.set_label("Successfully shared. Click here to continue...")
1141
1142=== modified file 'accomplishments/util/__init__.py'
1143--- accomplishments/util/__init__.py 2012-03-05 03:49:28 +0000
1144+++ accomplishments/util/__init__.py 2012-03-20 03:24:21 +0000
1145@@ -5,6 +5,11 @@
1146 import gettext
1147 from gettext import gettext as _
1148
1149+from twisted.internet import defer
1150+from twisted.internet import protocol
1151+from twisted.internet import reactor
1152+from twisted.python import log
1153+
1154 import accomplishments
1155 from accomplishments import config
1156 from accomplishments import exceptions
1157@@ -43,7 +48,7 @@
1158 logger.setLevel(logging.DEBUG)
1159 logger.debug('logging enabled')
1160 if opts.verbose > 1:
1161- lib_logger.setLevel(logging.DEBUG)
1162+ logger.setLevel(logging.DEBUG)
1163
1164
1165 def get_data_path():
1166@@ -86,3 +91,34 @@
1167 help=_("Clear your trophies collection"))
1168 (options, args) = parser.parse_args()
1169 return options
1170+
1171+
1172+class SubprocessReturnCodeProtocol(protocol.ProcessProtocol):
1173+ """
1174+ """
1175+ def __init__(self, command=""):
1176+ self.command = command
1177+
1178+ def connectionMade(self):
1179+ self.returnCodeDeferred = defer.Deferred()
1180+
1181+ def processEnded(self, reason):
1182+ self.returnCodeDeferred.callback(reason.value.exitCode)
1183+
1184+ def outReceived(self, data):
1185+ log.msg("Got process results: %s" % data)
1186+
1187+ def errReceived(self, data):
1188+ log.err("Got non-zero exit code for process: %s" % (
1189+ " ".join(self.command),))
1190+ log.msg(data)
1191+
1192+
1193+def import_gpg_key(pub_key):
1194+ """
1195+ """
1196+ cmd = ["gpg", "--import", pub_key]
1197+ gpg = SubprocessReturnCodeProtocol(cmd)
1198+ gpg.deferred = defer.Deferred()
1199+ process = reactor.spawnProcess(gpg, cmd[0], cmd, env=None)
1200+ return gpg.deferred
1201
1202=== renamed file 'bin/rundaemon.sh' => 'bin/daemon'
1203--- bin/rundaemon.sh 2012-03-05 03:51:41 +0000
1204+++ bin/daemon 2012-03-20 03:24:21 +0000
1205@@ -1,15 +1,28 @@
1206-#!/bin/bash
1207-
1208-#D="../../"
1209-export PYTHONPATH=$PYTHONPATH:.
1210-
1211-echo Importing the validation key...
1212-gpg --import ./data/daemon/validation-key.pub
1213-
1214-echo Starting the accomplishments daemon...
1215-echo "(this would be done by D-Bus activation in the real world)"
1216-
1217-python ./accomplishments/daemon/app.py
1218-DAEMON=$!
1219-
1220-
1221+#!/usr/bin/twistd
1222+"""
1223+Run the Accomplishments daemon!
1224+"""
1225+import sys
1226+
1227+from twisted.internet import glib2reactor
1228+glib2reactor.install()
1229+
1230+import dbus
1231+from dbus.mainloop.glib import DBusGMainLoop
1232+
1233+from accomplishments.daemon import app
1234+
1235+
1236+# PYTHONPATH mangling
1237+sys.path.insert(0, ".")
1238+
1239+dbus_loop = DBusGMainLoop(set_as_default=True)
1240+application = app.applicationFactory(
1241+ app_name="Ubuntu Accomplishments",
1242+ bus_name="org.ubuntu.accomplishments",
1243+ main_loop=dbus_loop,
1244+ session_bus=dbus.SessionBus(mainloop=dbus_loop),
1245+ object_path="/",
1246+ # Let's execute the timer service every 15 minutes
1247+ update_interval=15 * 60,
1248+ gpg_key="./data/daemon/validation-key.pub")

Subscribers

People subscribed via source and target branches