Merge lp:~oubiwann/ubuntu-accomplishments-system/946850-twisted-app into lp:~jonobacon/ubuntu-accomplishments-system/trophyinfo
- 946850-twisted-app
- Merge into trophyinfo
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jono Bacon | Approve | ||
Review via email: mp+98325@code.launchpad.net |
Commit message
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.
Duncan McGreggor (oubiwann) wrote : | # |
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/
* 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
Duncan McGreggor (oubiwann) wrote : | # |
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/
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://
Jono Bacon (jonobacon) wrote : | # |
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/
>
> 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/
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 ...
Duncan McGreggor (oubiwann) wrote : | # |
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/
> 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:/
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!
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
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") |
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.