Merge lp:~nataliabidart/ubuntu-sso-client/run-stuff-from-mainloop into lp:ubuntu-sso-client
- run-stuff-from-mainloop
- Merge into trunk
Proposed by
Natalia Bidart
Status: | Merged |
---|---|
Approved by: | Natalia Bidart |
Approved revision: | 862 |
Merged at revision: | 852 |
Proposed branch: | lp:~nataliabidart/ubuntu-sso-client/run-stuff-from-mainloop |
Merge into: | lp:ubuntu-sso-client |
Diff against target: |
602 lines (+523/-4) 10 files modified
setup.py (+2/-0) ubuntu_sso/logger.py (+1/-1) ubuntu_sso/utils/__init__.py (+3/-3) ubuntu_sso/utils/runner/__init__.py (+99/-0) ubuntu_sso/utils/runner/glib.py (+75/-0) ubuntu_sso/utils/runner/qt.py (+59/-0) ubuntu_sso/utils/runner/tests/__init__.py (+17/-0) ubuntu_sso/utils/runner/tests/test_qt.py (+93/-0) ubuntu_sso/utils/runner/tests/test_runner.py (+81/-0) ubuntu_sso/utils/runner/tx.py (+93/-0) |
To merge this branch: | bzr merge lp:~nataliabidart/ubuntu-sso-client/run-stuff-from-mainloop |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Manuel de la Peña (community) | Approve | ||
Roberto Alsina (community) | Approve | ||
Review via email: mp+89956@code.launchpad.net |
Commit message
- Provide a helper to spawn programs from the main loop that is being used by the SSO Service (LP: #920949).
Description of the change
The spawnner for twisted is provided so we can have a valid test suite running inside trial, it shouldn't be used IRL.
To post a comment you must log in.
- 861. By Natalia Bidart
-
Merged trunk in.
- 862. By Natalia Bidart
-
Handle programs that already have the EXE_EXT.
Revision history for this message
Manuel de la Peña (mandel) wrote : | # |
Nice work, I have tried to be evil in the review and I could't!
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'setup.py' |
2 | --- setup.py 2012-02-08 02:34:29 +0000 |
3 | +++ setup.py 2012-02-08 18:36:30 +0000 |
4 | @@ -391,6 +391,8 @@ |
5 | 'ubuntu_sso.qt.ui', |
6 | 'ubuntu_sso.utils', |
7 | 'ubuntu_sso.utils.tests', |
8 | + 'ubuntu_sso.utils.runner', |
9 | + 'ubuntu_sso.utils.runner.tests', |
10 | 'ubuntu_sso.utils.webclient', |
11 | 'ubuntu_sso.utils.webclient.tests', |
12 | 'ubuntu_sso.xdg_base_directory', |
13 | |
14 | === modified file 'ubuntu_sso/logger.py' |
15 | --- ubuntu_sso/logger.py 2012-02-06 19:57:46 +0000 |
16 | +++ ubuntu_sso/logger.py 2012-02-08 18:36:30 +0000 |
17 | @@ -35,7 +35,7 @@ |
18 | if not os.path.exists(unicode_path(LOGFOLDER)): |
19 | os.makedirs(unicode_path(LOGFOLDER)) |
20 | |
21 | -if os.environ.get('DEBUG'): |
22 | +if os.environ.get('U1_DEBUG'): |
23 | LOG_LEVEL = logging.DEBUG |
24 | else: |
25 | # Only log this level and above |
26 | |
27 | === modified file 'ubuntu_sso/utils/__init__.py' |
28 | --- ubuntu_sso/utils/__init__.py 2012-02-07 14:45:29 +0000 |
29 | +++ ubuntu_sso/utils/__init__.py 2012-02-08 18:36:30 +0000 |
30 | @@ -1,8 +1,6 @@ |
31 | # -*- coding: utf-8 -*- |
32 | - |
33 | -# Author: Alejandro J. Cura <alecu@canonical.com> |
34 | # |
35 | -# Copyright 2010, 2011 Canonical Ltd. |
36 | +# Copyright 2010-2012 Canonical Ltd. |
37 | # |
38 | # This program is free software: you can redistribute it and/or modify it |
39 | # under the terms of the GNU General Public License version 3, as published |
40 | @@ -26,6 +24,8 @@ |
41 | from urlparse import urlparse |
42 | |
43 | from ubuntu_sso.logger import setup_logging |
44 | + |
45 | + |
46 | logger = setup_logging("ubuntu_sso.utils") |
47 | |
48 | |
49 | |
50 | === added directory 'ubuntu_sso/utils/runner' |
51 | === added file 'ubuntu_sso/utils/runner/__init__.py' |
52 | --- ubuntu_sso/utils/runner/__init__.py 1970-01-01 00:00:00 +0000 |
53 | +++ ubuntu_sso/utils/runner/__init__.py 2012-02-08 18:36:30 +0000 |
54 | @@ -0,0 +1,99 @@ |
55 | +# -*- coding: utf-8 -*- |
56 | +# |
57 | +# Copyright 2012 Canonical Ltd. |
58 | +# |
59 | +# This program is free software: you can redistribute it and/or modify it |
60 | +# under the terms of the GNU General Public License version 3, as published |
61 | +# by the Free Software Foundation. |
62 | +# |
63 | +# This program is distributed in the hope that it will be useful, but |
64 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
65 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
66 | +# PURPOSE. See the GNU General Public License for more details. |
67 | +# |
68 | +# You should have received a copy of the GNU General Public License along |
69 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
70 | + |
71 | +"""Utility to spawn another program from a mainloop.""" |
72 | + |
73 | +import sys |
74 | + |
75 | +from twisted.internet import defer |
76 | + |
77 | +from ubuntu_sso.logger import setup_logging |
78 | + |
79 | + |
80 | +logger = setup_logging("ubuntu_sso.utils.runner") |
81 | + |
82 | + |
83 | +class SpawnError(Exception): |
84 | + """Generic error when spawning processes.""" |
85 | + |
86 | + |
87 | +class FailedToStartError(SpawnError): |
88 | + """The process could not be spawned.""" |
89 | + |
90 | + |
91 | +def is_qt4_main_loop_installed(): |
92 | + """Check if the Qt4 main loop is installed.""" |
93 | + result = False |
94 | + try: |
95 | + from PyQt4.QtCore import QCoreApplication |
96 | + result = QCoreApplication.instance() is not None |
97 | + except ImportError: |
98 | + pass |
99 | + |
100 | + return result |
101 | + |
102 | + |
103 | +def is_twisted_reactor_installed(): |
104 | + """Check if the Twisted reactor is installed.""" |
105 | + result = 'twisted.internet.reactor' in sys.modules |
106 | + return result |
107 | + |
108 | + |
109 | +def spawn_program(args): |
110 | + """Spawn the program specified by 'args'. |
111 | + |
112 | + - 'args' should be a sequence of program arguments, the program to execute |
113 | + is normally the first item in 'args'. |
114 | + |
115 | + Return a deferred that will be fired when the execution of 'args' finishes, |
116 | + passing as the deferred result code the program return code. |
117 | + |
118 | + On error, the returned deferred will be errback'd. |
119 | + |
120 | + """ |
121 | + logger.debug('spawn_program: requested to spawn %r.', repr(args)) |
122 | + d = defer.Deferred() |
123 | + |
124 | + if is_twisted_reactor_installed(): |
125 | + from ubuntu_sso.utils.runner import tx |
126 | + source = tx |
127 | + elif 'PyQt4' in sys.modules and is_qt4_main_loop_installed(): |
128 | + from ubuntu_sso.utils.runner import qt |
129 | + source = qt |
130 | + else: |
131 | + from ubuntu_sso.utils.runner import glib |
132 | + source = glib |
133 | + |
134 | + logger.debug('Spawn source is %r.', source) |
135 | + |
136 | + def reply_handler(status): |
137 | + """Callback the returned deferred.""" |
138 | + logger.debug('The program %r finished with status %r.', args, status) |
139 | + d.callback(status) |
140 | + |
141 | + def error_handler(msg, failed_to_start=False): |
142 | + """Errback the returned deferred.""" |
143 | + if failed_to_start: |
144 | + msg = 'Process %r could not be started (%r).' % (args, msg) |
145 | + exc = FailedToStartError(msg) |
146 | + else: |
147 | + exc = SpawnError('Unspecified error (%r).' % msg) |
148 | + |
149 | + logger.error('The program %r could not be run: %r', args, exc) |
150 | + d.errback(exc) |
151 | + |
152 | + source.spawn_program(args, reply_handler, error_handler) |
153 | + return d |
154 | |
155 | === added file 'ubuntu_sso/utils/runner/glib.py' |
156 | --- ubuntu_sso/utils/runner/glib.py 1970-01-01 00:00:00 +0000 |
157 | +++ ubuntu_sso/utils/runner/glib.py 2012-02-08 18:36:30 +0000 |
158 | @@ -0,0 +1,75 @@ |
159 | +# -*- coding: utf-8 -*- |
160 | +# |
161 | +# Copyright 2012 Canonical Ltd. |
162 | +# |
163 | +# This program is free software: you can redistribute it and/or modify it |
164 | +# under the terms of the GNU General Public License version 3, as published |
165 | +# by the Free Software Foundation. |
166 | +# |
167 | +# This program is distributed in the hope that it will be useful, but |
168 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
169 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
170 | +# PURPOSE. See the GNU General Public License for more details. |
171 | +# |
172 | +# You should have received a copy of the GNU General Public License along |
173 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
174 | + |
175 | +"""Utility to spawn another program from a GLib mainloop.""" |
176 | + |
177 | +# pylint: disable=E0611,F0401 |
178 | +try: |
179 | + from shlex import quote |
180 | +except ImportError: |
181 | + from pipes import quote |
182 | + |
183 | +from gi.repository import GLib |
184 | +# pylint: enable=E0611,F0401 |
185 | + |
186 | +from ubuntu_sso.logger import setup_logging |
187 | + |
188 | +logger = setup_logging("ubuntu_sso.utils.runner.glib") |
189 | + |
190 | + |
191 | +NO_SUCH_FILE_OR_DIR = '(No such file or directory)' |
192 | + |
193 | + |
194 | +def spawn_program(args, reply_handler, error_handler): |
195 | + """Spawn the program specified by 'args' using the GLib mainloop. |
196 | + |
197 | + When the program finishes, 'reply_handler' will be called with a single |
198 | + argument that will be the porgram status code. |
199 | + |
200 | + If there is an error, error_handler will be called with an instance of |
201 | + SpawnError. |
202 | + |
203 | + """ |
204 | + |
205 | + def child_watch(pid, status): |
206 | + """Handle child termination.""" |
207 | + # pylint: disable=E1103 |
208 | + GLib.spawn_close_pid(pid) |
209 | + # pylint: enable=E1103 |
210 | + reply_handler(status) |
211 | + |
212 | + def handle_error(gerror): |
213 | + """Handle error when spawning the process.""" |
214 | + failed_to_start = NO_SUCH_FILE_OR_DIR in gerror.message |
215 | + msg = 'GError is: code %r, message %r' % (gerror.code, gerror.message) |
216 | + error_handler(msg=msg, failed_to_start=failed_to_start) |
217 | + |
218 | + # escape arguments |
219 | + args = [quote(a) for a in args] |
220 | + |
221 | + flags = GLib.SpawnFlags.DO_NOT_REAP_CHILD | \ |
222 | + GLib.SpawnFlags.SEARCH_PATH | \ |
223 | + GLib.SpawnFlags.STDOUT_TO_DEV_NULL | \ |
224 | + GLib.SpawnFlags.STDERR_TO_DEV_NULL |
225 | + pid = None |
226 | + try: |
227 | + pid, _, _, _ = GLib.spawn_async(argv=args, flags=flags) |
228 | + except GLib.GError, e: |
229 | + handle_error(e) |
230 | + else: |
231 | + logger.debug('Spawning the program %r with the glib mainloop ' |
232 | + '(returned pid is %r).', args, pid) |
233 | + GLib.child_watch_add(pid, child_watch) |
234 | |
235 | === added file 'ubuntu_sso/utils/runner/qt.py' |
236 | --- ubuntu_sso/utils/runner/qt.py 1970-01-01 00:00:00 +0000 |
237 | +++ ubuntu_sso/utils/runner/qt.py 2012-02-08 18:36:30 +0000 |
238 | @@ -0,0 +1,59 @@ |
239 | +# -*- coding: utf-8 -*- |
240 | +# |
241 | +# Copyright 2012 Canonical Ltd. |
242 | +# |
243 | +# This program is free software: you can redistribute it and/or modify it |
244 | +# under the terms of the GNU General Public License version 3, as published |
245 | +# by the Free Software Foundation. |
246 | +# |
247 | +# This program is distributed in the hope that it will be useful, but |
248 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
249 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
250 | +# PURPOSE. See the GNU General Public License for more details. |
251 | +# |
252 | +# You should have received a copy of the GNU General Public License along |
253 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
254 | + |
255 | +"""Utility to spawn another program from a plain Qt mainloop.""" |
256 | + |
257 | +from PyQt4 import QtCore |
258 | + |
259 | +from ubuntu_sso.logger import setup_logging |
260 | + |
261 | + |
262 | +logger = setup_logging("ubuntu_sso.utils.runner.qt") |
263 | + |
264 | + |
265 | +def spawn_program(args, reply_handler, error_handler): |
266 | + """Spawn the program specified by 'args' using the Qt mainloop. |
267 | + |
268 | + When the program finishes, 'reply_handler' will be called with a single |
269 | + argument that will be the porgram status code. |
270 | + |
271 | + If there is an error, error_handler will be called with an instance of |
272 | + SpawnError. |
273 | + |
274 | + """ |
275 | + |
276 | + process = QtCore.QProcess() |
277 | + |
278 | + def print_pid(): |
279 | + """Add a debug log message.""" |
280 | + pid = process.pid() |
281 | + logger.debug('Spawning the program %r with the qt mainloop ' |
282 | + '(returned pid is %r).', args, pid) |
283 | + |
284 | + def handle_error(process_error): |
285 | + """Handle error when spawning the process.""" |
286 | + failed_to_start = process_error == QtCore.QProcess.FailedToStart |
287 | + msg = 'ProcessError is %r' % process_error |
288 | + error_handler(msg=msg, failed_to_start=failed_to_start) |
289 | + |
290 | + process.started.connect(print_pid) |
291 | + process.finished.connect(reply_handler) |
292 | + process.error.connect(handle_error) |
293 | + |
294 | + args = list(args) |
295 | + program = args[0] |
296 | + argv = args[1:] |
297 | + process.start(program, argv) |
298 | |
299 | === added directory 'ubuntu_sso/utils/runner/tests' |
300 | === added file 'ubuntu_sso/utils/runner/tests/__init__.py' |
301 | --- ubuntu_sso/utils/runner/tests/__init__.py 1970-01-01 00:00:00 +0000 |
302 | +++ ubuntu_sso/utils/runner/tests/__init__.py 2012-02-08 18:36:30 +0000 |
303 | @@ -0,0 +1,17 @@ |
304 | +# -*- coding: utf-8 -*- |
305 | +# |
306 | +# Copyright 2012 Canonical Ltd. |
307 | +# |
308 | +# This program is free software: you can redistribute it and/or modify it |
309 | +# under the terms of the GNU General Public License version 3, as published |
310 | +# by the Free Software Foundation. |
311 | +# |
312 | +# This program is distributed in the hope that it will be useful, but |
313 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
314 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
315 | +# PURPOSE. See the GNU General Public License for more details. |
316 | +# |
317 | +# You should have received a copy of the GNU General Public License along |
318 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
319 | + |
320 | +"""Tests for the program runner.""" |
321 | |
322 | === added file 'ubuntu_sso/utils/runner/tests/test_qt.py' |
323 | --- ubuntu_sso/utils/runner/tests/test_qt.py 1970-01-01 00:00:00 +0000 |
324 | +++ ubuntu_sso/utils/runner/tests/test_qt.py 2012-02-08 18:36:30 +0000 |
325 | @@ -0,0 +1,93 @@ |
326 | +# -*- coding: utf-8 -*- |
327 | +# |
328 | +# Copyright 2012 Canonical Ltd. |
329 | +# |
330 | +# This program is free software: you can redistribute it and/or modify it |
331 | +# under the terms of the GNU General Public License version 3, as published |
332 | +# by the Free Software Foundation. |
333 | +# |
334 | +# This program is distributed in the hope that it will be useful, but |
335 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
336 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
337 | +# PURPOSE. See the GNU General Public License for more details. |
338 | +# |
339 | +# You should have received a copy of the GNU General Public License along |
340 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
341 | + |
342 | +"""Tests for the qt runner helper module.""" |
343 | + |
344 | +import subprocess |
345 | + |
346 | +from PyQt4 import QtCore |
347 | +from twisted.internet import defer |
348 | + |
349 | +from ubuntu_sso.utils import runner |
350 | +from ubuntu_sso.utils.runner.tests.test_runner import SpawnProgramTestCase |
351 | + |
352 | + |
353 | +class FakedSignal(object): |
354 | + """Fake a Qt signal.""" |
355 | + |
356 | + def __init__(self, name): |
357 | + self.name = name |
358 | + self._handlers = [] |
359 | + |
360 | + def connect(self, handler): |
361 | + """Connect 'handler' with this signal.""" |
362 | + self._handlers.append(handler) |
363 | + |
364 | + def emit(self, *args, **kwargs): |
365 | + """Emit this signal.""" |
366 | + for handler in self._handlers: |
367 | + handler(*args, **kwargs) |
368 | + |
369 | + |
370 | +class FakedProcess(object): |
371 | + """Fake a Qt Process.""" |
372 | + |
373 | + _pid = 123456 |
374 | + _error = None |
375 | + _status_code = 0 |
376 | + |
377 | + FailedToStart = 0 |
378 | + |
379 | + def __init__(self): |
380 | + self.pid = lambda: self._pid |
381 | + self.started = FakedSignal('started') |
382 | + self.finished = FakedSignal('finished') |
383 | + self.error = FakedSignal('error') |
384 | + |
385 | + def start(self, program, arguments): |
386 | + """Start this process.""" |
387 | + if self._error is None: |
388 | + self.started.emit() |
389 | + |
390 | + args = (program,) + tuple(arguments) |
391 | + try: |
392 | + subprocess.call(args) |
393 | + except OSError, e: |
394 | + if e.errno == 2: |
395 | + self.error.emit(self.FailedToStart) |
396 | + else: |
397 | + self.error.emit(e) |
398 | + except Exception, e: |
399 | + self.error.emit(e) |
400 | + else: |
401 | + self.finished.emit(self._status_code) |
402 | + else: |
403 | + self.error.emit(self._error) |
404 | + |
405 | + |
406 | +class QtSpawnProgramTestCase(SpawnProgramTestCase): |
407 | + """The test suite for the spawn_program method (using Qt).""" |
408 | + |
409 | + use_reactor = False |
410 | + |
411 | + @defer.inlineCallbacks |
412 | + def setUp(self): |
413 | + yield super(QtSpawnProgramTestCase, self).setUp() |
414 | + # Since we can't mix plan qt runner and the qt4reactor, we patch |
415 | + # QProcess and fake the conditions so the qt runner is chosen |
416 | + self.patch(QtCore, 'QProcess', FakedProcess) |
417 | + self.patch(runner, 'is_twisted_reactor_installed', lambda: False) |
418 | + self.patch(runner, 'is_qt4_main_loop_installed', lambda: True) |
419 | |
420 | === added file 'ubuntu_sso/utils/runner/tests/test_runner.py' |
421 | --- ubuntu_sso/utils/runner/tests/test_runner.py 1970-01-01 00:00:00 +0000 |
422 | +++ ubuntu_sso/utils/runner/tests/test_runner.py 2012-02-08 18:36:30 +0000 |
423 | @@ -0,0 +1,81 @@ |
424 | +# -*- coding: utf-8 -*- |
425 | +# |
426 | +# Copyright 2012 Canonical Ltd. |
427 | +# |
428 | +# This program is free software: you can redistribute it and/or modify it |
429 | +# under the terms of the GNU General Public License version 3, as published |
430 | +# by the Free Software Foundation. |
431 | +# |
432 | +# This program is distributed in the hope that it will be useful, but |
433 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
434 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
435 | +# PURPOSE. See the GNU General Public License for more details. |
436 | +# |
437 | +# You should have received a copy of the GNU General Public License along |
438 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
439 | + |
440 | +"""Tests for the runner helper module.""" |
441 | + |
442 | +import os |
443 | + |
444 | +from twisted.internet import defer |
445 | + |
446 | +from ubuntu_sso.tests import TestCase |
447 | +from ubuntu_sso.utils import runner |
448 | + |
449 | + |
450 | +TEST_ME_DIR = 'test-me' |
451 | + |
452 | + |
453 | +class SpawnProgramTestCase(TestCase): |
454 | + """The test suite for the spawn_program method.""" |
455 | + |
456 | + timeout = 3 |
457 | + args = ('python', '-c', 'import os; os.system("mkdir %s")' % TEST_ME_DIR) |
458 | + |
459 | + @defer.inlineCallbacks |
460 | + def setUp(self): |
461 | + yield super(SpawnProgramTestCase, self).setUp() |
462 | + assert not os.path.exists(TEST_ME_DIR) |
463 | + self.addCleanup(lambda: os.path.exists(TEST_ME_DIR) and |
464 | + os.rmdir(TEST_ME_DIR)) |
465 | + |
466 | + def spawn_fn(self, args): |
467 | + """The target function to test.""" |
468 | + return runner.spawn_program(args) |
469 | + |
470 | + def assert_command_was_run(self): |
471 | + """The spawnned commnad was correctly run.""" |
472 | + self.assertTrue(os.path.exists(TEST_ME_DIR)) |
473 | + self.assertTrue(os.path.isdir(TEST_ME_DIR)) |
474 | + |
475 | + @defer.inlineCallbacks |
476 | + def test_program_is_spawned(self): |
477 | + """The program is actually spawned.""" |
478 | + yield self.spawn_fn(self.args) |
479 | + self.assert_command_was_run() |
480 | + |
481 | + @defer.inlineCallbacks |
482 | + def test_program_is_spawned_returned_code_non_zero(self): |
483 | + """The program is actually spawned.""" |
484 | + status = yield self.spawn_fn(self.args) |
485 | + self.assertEqual(status, 0) |
486 | + |
487 | + @defer.inlineCallbacks |
488 | + def test_failed_to_start(self): |
489 | + """FailedToStartError is raised if the program does not start.""" |
490 | + no_such_program = './myexecutablethatdoesnotexist' |
491 | + assert not os.path.exists(no_such_program) |
492 | + |
493 | + d = self.spawn_fn((no_such_program,)) |
494 | + exc = yield self.assertFailure(d, runner.FailedToStartError) |
495 | + |
496 | + self.assertIn(no_such_program, exc.message) |
497 | + |
498 | + @defer.inlineCallbacks |
499 | + def test_other_error(self): |
500 | + """SpawnError is raised if the program does not start.""" |
501 | + d = self.spawn_fn((None,)) |
502 | + exc = yield self.assertFailure(d, runner.SpawnError) |
503 | + |
504 | + self.assertIn('Unspecified error', exc.message) |
505 | |
506 | === added file 'ubuntu_sso/utils/runner/tx.py' |
507 | --- ubuntu_sso/utils/runner/tx.py 1970-01-01 00:00:00 +0000 |
508 | +++ ubuntu_sso/utils/runner/tx.py 2012-02-08 18:36:30 +0000 |
509 | @@ -0,0 +1,93 @@ |
510 | +# -*- coding: utf-8 -*- |
511 | +# |
512 | +# Copyright 2012 Canonical Ltd. |
513 | +# |
514 | +# This program is free software: you can redistribute it and/or modify it |
515 | +# under the terms of the GNU General Public License version 3, as published |
516 | +# by the Free Software Foundation. |
517 | +# |
518 | +# This program is distributed in the hope that it will be useful, but |
519 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
520 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
521 | +# PURPOSE. See the GNU General Public License for more details. |
522 | +# |
523 | +# You should have received a copy of the GNU General Public License along |
524 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
525 | + |
526 | +"""Utility to spawn another program from a mainloop.""" |
527 | + |
528 | +import os |
529 | +import sys |
530 | + |
531 | +from twisted.internet import utils |
532 | + |
533 | +from ubuntu_sso.logger import setup_logging |
534 | + |
535 | + |
536 | +logger = setup_logging("ubuntu_sso.utils.runner.tx") |
537 | + |
538 | +NO_SUCH_FILE_OR_DIR = 'OSError: [Errno 2] No such file or directory' |
539 | + |
540 | + |
541 | +EXE_EXT = '' |
542 | +if sys.platform == 'win32': |
543 | + EXE_EXT = '.exe' |
544 | + |
545 | + |
546 | +def spawn_program(args, reply_handler, error_handler): |
547 | + """Spawn the program specified by 'args' using the twisted reactor. |
548 | + |
549 | + When the program finishes, 'reply_handler' will be called with a single |
550 | + argument that will be the porgram status code. |
551 | + |
552 | + If there is an error, error_handler will be called with an instance of |
553 | + SpawnError. |
554 | + |
555 | + """ |
556 | + |
557 | + def child_watch((stdout, stderr, exit_code)): |
558 | + """Handle child termination.""" |
559 | + if stdout: |
560 | + logger.debug('Returned stdout is (exit code was %r): %r', |
561 | + exit_code, stdout) |
562 | + if stderr: |
563 | + logger.warning('Returned stderr is (exit code was %r): %r', |
564 | + exit_code, stderr) |
565 | + |
566 | + if OSError.__name__ in stderr: |
567 | + failed_to_start = NO_SUCH_FILE_OR_DIR in stderr |
568 | + error_handler(msg=stderr, failed_to_start=failed_to_start) |
569 | + else: |
570 | + reply_handler(exit_code) |
571 | + |
572 | + def handle_error(failure): |
573 | + """Handle error when spawning the process.""" |
574 | + error_handler(msg=failure.getErrorMessage()) |
575 | + |
576 | + args = list(args) |
577 | + program = args[0] |
578 | + argv = args[1:] |
579 | + |
580 | + if program and not os.access(program, os.X_OK): |
581 | + # handle searching the executable in the PATH, since |
582 | + # twisted will not solve that for us :-/ |
583 | + paths = os.environ['PATH'].split(os.pathsep) |
584 | + for path in paths: |
585 | + target = os.path.join(path, program) |
586 | + if not target.endswith(EXE_EXT): |
587 | + target += EXE_EXT |
588 | + if os.access(target, os.X_OK): |
589 | + program = target |
590 | + break |
591 | + |
592 | + try: |
593 | + d = utils.getProcessOutputAndValue(program, argv, env=os.environ) |
594 | + except OSError, e: |
595 | + error_handler(msg=e, failed_to_start=True) |
596 | + except Exception, e: |
597 | + error_handler(msg=e, failed_to_start=False) |
598 | + else: |
599 | + logger.debug('Spawning the program %r with the twisted reactor ' |
600 | + '(returned deferred is %r).', repr(args), d) |
601 | + d.addCallback(child_watch) |
602 | + d.addErrback(handle_error) |
+1 I like it!