]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/gui/macapp.py
96061129c38155e1e21bfb9a2884253b4a1b20a3
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / gui / macapp.py
1
2 import operator
3 import os
4 import stat
5 from subprocess import Popen, PIPE
6 import sys
7 import thread
8 import threading
9 import traceback
10 import urllib
11 import webbrowser
12
13 import wx
14 from twisted.internet import reactor
15 from twisted.python import log, logfile
16
17 import allmydata
18 from allmydata import client
19 from allmydata.gui.confwiz import ConfWizApp, ACCOUNT_PAGE, DEFAULT_SERVER_URL
20 from allmydata.scripts.common import get_aliases
21 import amdicon
22 import amdlogo
23
24 DEFAULT_FUSE_TIMEOUT = 300
25
26 TRY_TO_INSTALL_TAHOE_SCRIPT = True
27 TAHOE_SCRIPT = '''#!/bin/bash
28 if [ "x${*}x" == "xx" ]
29 then
30     %(exe)s --help
31 else
32     %(exe)s "${@}"
33 fi
34 '''
35
36 def run_macapp():
37     nodedir = os.path.expanduser('~/.tahoe')
38     if not os.path.isdir(nodedir):
39         app_supp = os.path.expanduser('~/Library/Application Support/Allmydata Tahoe/')
40         if not os.path.isdir(app_supp):
41             os.makedirs(app_supp)
42         os.symlink(app_supp, nodedir)
43
44     app_cont = AppContainer(nodedir)
45     return app_cont.run()
46
47 class MacGuiClient(client.Client):
48     """
49     This is a subclass of the tahoe 'client' node, which hooks into the
50     client's 'notice something went wrong' mechanism, to display the fact
51     in a manner sensible for a wx gui app
52     """
53     def __init__(self, nodedir, app_cont):
54         self.app_cont = app_cont
55         client.Client.__init__(self, nodedir)
56
57     def _service_startup_failed(self, failure):
58         wx.CallAfter(self.wx_abort, failure)
59         log.msg('node service startup failed')
60         log.err(failure)
61
62     def wx_abort(self, failure):
63         wx.MessageBox(failure.getTraceback(), 'Fatal Error in Node startup')
64         self.app_cont.guiapp.ExitMainLoop()
65
66 class AppContainer(object):
67     """
68     This is the 'container' for the mac app, which takes care of initial
69     configuration concerns - e.g. running the confwiz before going any further -
70     of launching the reactor, and within it the tahoe node, on a separate thread,
71     and then lastly of launching the actual wx gui app and waiting for it to exit.
72     """
73     def __init__(self, nodedir):
74         self.nodedir = nodedir
75
76     def files_exist(self, file_list):
77         extant_conf = [ os.path.exists(os.path.join(self.nodedir, f)) for f in file_list ]
78         return reduce(operator.__and__, extant_conf)
79
80     def is_config_incomplete(self):
81         necessary_conf_files = ['introducer.furl', 'private/root_dir.cap']
82         need_config = not self.files_exist(necessary_conf_files)
83         if need_config:
84             print 'some config is missing from nodedir (%s): %s' % (self.nodedir, necessary_conf_files)
85         return need_config
86
87     def run(self):
88         # handle initial config
89         if not os.path.exists(os.path.join(self.nodedir, 'webport')):
90             f = file(os.path.join(self.nodedir, 'webport'), 'wb')
91             f.write('3456')
92             f.close()
93
94         if self.is_config_incomplete():
95             confwiz = ConfWizApp(DEFAULT_SERVER_URL, open_welcome_page=True)
96             confwiz.MainLoop()
97
98         if self.is_config_incomplete():
99             print 'config still incomplete; confwiz cancelled, exiting'
100             return 1
101
102         # set up twisted logging. this will become part of the node rsn.
103         logdir = os.path.join(self.nodedir, 'logs')
104         if not os.path.exists(logdir):
105             os.makedirs(logdir)
106         lf = logfile.LogFile('tahoesvc.log', logdir)
107         log.startLogging(lf)
108
109         if TRY_TO_INSTALL_TAHOE_SCRIPT:
110             self.maybe_install_tahoe_script()
111
112         # actually start up the node and the ui
113         os.chdir(self.nodedir)
114
115         # start the reactor thread up, launch the tahoe node therein
116         self.start_reactor()
117
118         try:
119             # launch the actual gui on the wx event loop, wait for it to quit
120             self.guiapp = MacGuiApp(app_cont=self)
121             self.guiapp.MainLoop()
122             log.msg('gui mainloop exited')
123         except:
124             log.err()
125
126         # shutdown the reactor, hence tahoe node, before exiting
127         self.stop_reactor()
128
129         return 0
130
131     def start_reactor(self):
132         self.reactor_shutdown = threading.Event()
133         thread.start_new_thread(self.launch_reactor, ())
134
135     def launch_reactor(self):
136         # run the node itself
137         #c = client.Client(self.nodedir)
138         c = MacGuiClient(self.nodedir, app_cont=self)
139         reactor.callLater(0, c.startService) # after reactor startup
140         reactor.run(installSignalHandlers=False)
141         self.reactor_shutdown.set()
142
143     def stop_reactor(self):
144         # trigger reactor shutdown, and block waiting on it
145         reactor.callFromThread(reactor.stop)
146         log.msg('waiting for reactor shutdown')
147         self.reactor_shutdown.wait()
148         log.msg('reactor shut down')
149
150     def maybe_install_tahoe_script(self):
151         path_candidates = ['/usr/local/bin', '~/bin', '~/Library/bin']
152         env_path = map(os.path.expanduser, os.environ['PATH'].split(':'))
153         if not sys.executable.endswith('/python'):
154             print 'not installing tahoe script: unexpected sys.exe "%s"' % (sys.executable,)
155             return
156         for path_candidate in map(os.path.expanduser, env_path):
157             tahoe_path = path_candidate + '/tahoe'
158             if os.path.exists(tahoe_path):
159                 print 'not installing "tahoe": it already exists at "%s"' % (tahoe_path,)
160                 return
161         for path_candidate in map(os.path.expanduser, path_candidates):
162             if path_candidate not in env_path:
163                 print path_candidate, 'not in', env_path
164                 continue
165             tahoe_path = path_candidate + '/tahoe'
166             try:
167                 print 'trying to install "%s"' % (tahoe_path,)
168                 bin_path = (sys.executable[:-6] + 'Allmydata')
169                 script = TAHOE_SCRIPT % { 'exe': bin_path }
170                 f = file(tahoe_path, 'wb')
171                 f.write(script)
172                 f.close()
173                 mode = stat.S_IRUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH
174                 os.chmod(tahoe_path, mode)
175                 print 'installed "%s"' % (tahoe_path,)
176                 return
177             except:
178                 print 'unable to write %s' % (tahoe_path,)
179                 traceback.print_exc()
180         else:
181             print 'no remaining candidate paths for installation of tahoe script'
182
183
184 def DisplayTraceback(message):
185     xc = traceback.format_exception(*sys.exc_info())
186     wx.MessageBox(u"%s\n (%s)"%(message,''.join(xc)), 'Error')
187
188 WEBOPEN_ID = wx.NewId()
189 ACCOUNT_PAGE_ID = wx.NewId()
190 MOUNT_ID = wx.NewId()
191
192 class SplashFrame(wx.Frame):
193     def __init__(self):
194         no_resz = wx.DEFAULT_FRAME_STYLE & ~ (wx.MINIMIZE_BOX|wx.RESIZE_BORDER|wx.MAXIMIZE_BOX)
195         wx.Frame.__init__(self, None, -1, 'Allmydata', style=no_resz)
196
197         self.SetSizeHints(100, 100, 600, 800)
198         self.SetIcon(amdicon.getIcon())
199         self.Bind(wx.EVT_CLOSE, self.on_close)
200
201         background = wx.Panel(self, -1)
202         background.parent = self
203         self.login_panel = SplashPanel(background, self.on_close)
204         sizer = wx.BoxSizer(wx.VERTICAL)
205         background_sizer = wx.BoxSizer(wx.VERTICAL)
206         background_sizer.Add(self.login_panel, 1, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 26)
207         background.SetSizer(background_sizer)
208         sizer.Add(background, 0, wx.EXPAND | wx.ALL, 0)
209         self.SetSizer(sizer)
210         self.SetAutoLayout(True)
211         self.Fit()
212         self.Layout()
213
214     def on_close(self, event):
215         self.Show(False)
216
217 class SplashPanel(wx.Panel):
218     def __init__(self, parent, on_close):
219         wx.Panel.__init__(self, parent, -1)
220         self.parent = parent
221
222         hbox = wx.BoxSizer(wx.HORIZONTAL)
223         vbox = wx.BoxSizer(wx.VERTICAL)
224         self.sizer = wx.BoxSizer(wx.VERTICAL)
225
226         self.icon = wx.StaticBitmap(self, -1, amdlogo.getBitmap())
227         self.label = wx.StaticText(self, -1, 'Allmydata')
228         bigfont = self.label.GetFont()
229         bigfont.SetPointSize(26)
230         smlfont = self.label.GetFont()
231         smlfont.SetPointSize(10)
232         self.label.SetFont(bigfont)
233         ver = "Version 3.0 (%s)" % (allmydata.__version__,)
234         self.ver_label = wx.StaticText(self, -1, ver)
235         self.ver_label.SetFont(smlfont)
236         copy = u"Copyright \N{COPYRIGHT SIGN} 2004-2008 Allmydata Inc.,"
237         self.copy_label = wx.StaticText(self, -1, copy)
238         self.copy_label.SetFont(smlfont)
239         self.res_label = wx.StaticText(self, -1, "All Rights Reserved.")
240         self.res_label.SetFont(smlfont)
241         ##self.ok = wx.Button(self, -1, 'Ok')
242         ##self.Bind(wx.EVT_BUTTON, on_close, self.ok)
243         hbox.Add(self.icon, 0, wx.CENTER | wx.ALL, 2)
244         vbox.Add(self.label, 0, wx.CENTER | wx.ALL, 2)
245         vbox.Add(self.ver_label, 0, wx.CENTER | wx.ALL, 2)
246         hbox.Add(vbox)
247         self.sizer.Add(hbox)
248         self.sizer.Add(wx.Size(8,8), 1, wx.EXPAND | wx.ALL, 2)
249         self.sizer.Add(self.copy_label, 0, wx.CENTER | wx.ALL, 2)
250         self.sizer.Add(self.res_label, 0, wx.CENTER | wx.ALL, 2)
251         #self.sizer.Add(wx.Size(42,42), 1, wx.EXPAND | wx.ALL, 2)
252         ##self.sizer.Add(self.ok, 0, wx.CENTER | wx.ALL, 2)
253         self.SetSizer(self.sizer)
254         self.SetAutoLayout(True)
255
256
257 class MountFrame(wx.Frame):
258     def __init__(self, guiapp):
259         wx.Frame.__init__(self, None, -1, 'Allmydata Mount Filesystem')
260
261         self.SetSizeHints(100, 100, 600, 800)
262         self.SetIcon(amdicon.getIcon())
263         self.Bind(wx.EVT_CLOSE, self.on_close)
264
265         background = wx.Panel(self, -1)
266         background.parent = self
267         self.mount_panel = MountPanel(background, self.on_close, guiapp)
268         sizer = wx.BoxSizer(wx.VERTICAL)
269         background_sizer = wx.BoxSizer(wx.VERTICAL)
270         background_sizer.Add(self.mount_panel, 1, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 26)
271         background.SetSizer(background_sizer)
272         sizer.Add(background, 0, wx.EXPAND | wx.ALL, 0)
273         self.SetSizer(sizer)
274         self.SetAutoLayout(True)
275         self.Fit()
276         self.Layout()
277
278     def on_close(self, event):
279         self.Show(False)
280
281 class MountPanel(wx.Panel):
282     def __init__(self, parent, on_close, guiapp):
283         wx.Panel.__init__(self, parent, -1)
284         self.parent = parent
285         self.guiapp = guiapp
286
287         self.sizer = wx.BoxSizer(wx.VERTICAL)
288
289         self.label = wx.StaticText(self, -1, 'Allmydata Mount Filesystem')
290         self.mnt_label = wx.StaticText(self, -1, 'Mount')
291         self.alias_choice = wx.Choice(self, -1, (120, 64), choices=self.guiapp.aliases.keys())
292         root_dir = self.alias_choice.FindString('tahoe')
293         if root_dir != -1:
294             self.alias_choice.SetSelection(root_dir)
295         self.at_label = wx.StaticText(self, -1, 'at')
296         self.mountpoint = wx.TextCtrl(self, -1, 'choose a mount dir', size=(256,22))
297         self.mnt_browse = wx.Button(self, -1, 'Browse')
298         mount_sizer = wx.BoxSizer(wx.HORIZONTAL)
299         mount_sizer.Add(self.mnt_label, 0, wx.ALL, 4)
300         mount_sizer.Add(self.alias_choice, 0, wx.ALL, 4)
301         mount_sizer.Add(self.at_label, 0, wx.ALL, 4)
302         mount_sizer.Add(self.mountpoint, 0, wx.ALL, 4)
303         mount_sizer.Add(self.mnt_browse, 0, wx.ALL, 4)
304         self.mount = wx.Button(self, -1, 'Mount')
305         self.Bind(wx.EVT_BUTTON, self.on_mount, self.mount)
306         #self.Bind(wx.EVT_CHOICE, self.on_choice, self.alias_choice)
307         self.Bind(wx.EVT_BUTTON, self.on_mnt_browse, self.mnt_browse)
308         self.sizer.Add(self.label, 0, wx.CENTER | wx.ALL, 2)
309         self.sizer.Add(wx.Size(28,28), 1, wx.EXPAND | wx.ALL, 2)
310         self.sizer.Add(mount_sizer, 0, wx.EXPAND | wx.ALL, 0)
311         self.sizer.Add(wx.Size(28,28), 1, wx.EXPAND | wx.ALL, 2)
312         self.sizer.Add(self.mount, 0, wx.CENTER | wx.ALL, 2)
313         self.SetSizer(self.sizer)
314         self.SetAutoLayout(True)
315
316     def on_mount(self, event):
317         mountpoint = str(self.mountpoint.GetValue())
318         if not os.path.isdir(mountpoint):
319             wx.MessageBox(u'"%s" is not a directory' % (mountpoint,))
320         else:
321             alias_name = self.alias_choice.GetStringSelection()
322             self.do_mount(alias_name, mountpoint)
323
324     def on_mnt_browse(self, event):
325         dlg = wx.DirDialog(self, "Choose a Mountpoint Directory:",
326                            style=wx.DD_DEFAULT_STYLE|wx.DD_NEW_DIR_BUTTON)
327         if dlg.ShowModal() == wx.ID_OK:
328             mountpoint = dlg.GetPath()
329             self.mountpoint.SetValue(mountpoint)
330         dlg.Destroy()
331
332     def do_mount(self, alias_name, mountpoint):
333         log.msg('do_mount(%r, %r)' % (alias_name, mountpoint))
334         self.guiapp.mount_filesystem(alias_name, mountpoint)
335         self.parent.parent.Show(False)
336
337 class MacGuiApp(wx.App):
338     config = {
339         'auto-mount': True,
340         'auto-open': True,
341         'show-webopen': False,
342         'daemon-timeout': DEFAULT_FUSE_TIMEOUT,
343         }
344
345     def __init__(self, app_cont):
346         self.app_cont = app_cont
347         self.nodedir = app_cont.nodedir
348         self.load_config()
349         self.mounted_filesystems = {}
350         self.aliases = get_aliases(self.nodedir)
351         wx.App.__init__(self)
352
353     ## load up setting from gui.conf dir
354     def load_config(self):
355         log.msg('load_config')
356         confdir = os.path.join(self.nodedir, 'gui.conf')
357         config = {}
358         config.update(self.config)
359         for k in self.config:
360             f = os.path.join(confdir, k)
361             if os.path.exists(f):
362                 val = file(f, 'rb').read().strip()
363                 if type(self.config[k]) == bool:
364                     if val.lower() in ['y', 'yes', 'true', 'on', '1']:
365                         val = True
366                     else:
367                         val = False
368                 elif type(self.config[k]) == int:
369                     val = int(val)
370                 config[k] = val
371         self.config = config
372
373     ## GUI wx init
374     def OnInit(self):
375         log.msg('OnInit')
376         try:
377             self.frame = SplashFrame()
378             self.frame.CenterOnScreen()
379             self.frame.Show(True)
380             self.SetTopWindow(self.frame)
381
382             # self.load_config()
383
384             wx.FutureCall(4096, self.on_timer, None)
385
386             self.mount_frame = MountFrame(guiapp=self)
387
388             self.setup_dock_icon()
389             menubar = self.setup_app_menu(self.frame)
390             self.frame.SetMenuBar(menubar)
391
392             return True
393         except:
394             DisplayTraceback('exception on startup')
395             sys.exit()
396
397     ## WX menu and event handling
398
399     def on_timer(self, event):
400         self.frame.Show(False)
401         self.perhaps_automount()
402
403     def setup_dock_icon(self):
404         self.tbicon = wx.TaskBarIcon()
405         #self.tbicon.SetIcon(amdicon.getIcon(), "Allmydata")
406         wx.EVT_TASKBAR_RIGHT_UP(self.tbicon, self.on_dock_menu)
407
408     def setup_app_menu(self, frame):
409         menubar = wx.MenuBar()
410         file_menu = wx.Menu()
411         if self.config['show-webopen']:
412             webopen_menu = wx.Menu()
413             self.webopen_menu_ids = {}
414             for alias in self.aliases:
415                 mid = wx.NewId()
416                 self.webopen_menu_ids[mid] = alias
417                 item = webopen_menu.Append(mid, alias)
418                 frame.Bind(wx.EVT_MENU, self.on_webopen, item)
419             file_menu.AppendMenu(WEBOPEN_ID, 'Open Web UI', webopen_menu)
420         item = file_menu.Append(ACCOUNT_PAGE_ID, text='Open Account Page')
421         frame.Bind(wx.EVT_MENU, self.on_account_page, item)
422         item = file_menu.Append(MOUNT_ID, text='Mount Filesystem')
423         frame.Bind(wx.EVT_MENU, self.on_mount, item)
424         item = file_menu.Append(wx.ID_ABOUT, text='About')
425         frame.Bind(wx.EVT_MENU, self.on_about, item)
426         item = file_menu.Append(wx.ID_EXIT, text='Quit')
427         frame.Bind(wx.EVT_MENU, self.on_quit, item)
428         menubar.Append(file_menu, 'File')
429         return menubar
430
431     def on_dock_menu(self, event):
432         dock_menu = wx.Menu()
433         item = dock_menu.Append(wx.NewId(), text='About')
434         self.tbicon.Bind(wx.EVT_MENU, self.on_about, item)
435         if self.config['show-webopen']:
436             item = dock_menu.Append(WEBOPEN_ID, text='Open Web Root')
437             self.tbicon.Bind(wx.EVT_MENU, self.on_webopen, item)
438         item = dock_menu.Append(ACCOUNT_PAGE_ID, text='Open Account Page')
439         self.tbicon.Bind(wx.EVT_MENU, self.on_account_page, item)
440         item = dock_menu.Append(MOUNT_ID, text='Mount Filesystem')
441         self.tbicon.Bind(wx.EVT_MENU, self.on_mount, item)
442         self.tbicon.PopupMenu(dock_menu)
443
444     def on_about(self, event):
445         self.frame.Show(True)
446
447     def on_quit(self, event):
448         self.unmount_filesystems()
449         self.ExitMainLoop()
450
451     def on_webopen(self, event):
452         alias = self.webopen_menu_ids.get(event.GetId())
453         #log.msg('on_webopen() alias=%r' % (alias,))
454         self.webopen(alias)
455
456     def on_account_page(self, event):
457         webbrowser.open(DEFAULT_SERVER_URL + ACCOUNT_PAGE)
458
459     def on_mount(self, event):
460         self.mount_frame.Show(True)
461
462     ## Gui App methods
463
464     def perhaps_automount(self):
465         if self.config['auto-mount']:
466             mountpoint = os.path.join(self.nodedir, 'mnt/__auto__')
467             if not os.path.isdir(mountpoint):
468                 os.makedirs(mountpoint)
469             self.mount_filesystem('tahoe', mountpoint, 'Allmydata')
470
471     def webopen(self, alias=None):
472         log.msg('webopen: %r' % (alias,))
473         if alias is None:
474             alias = 'tahoe'
475         root_uri = self.aliases.get(alias)
476         if root_uri:
477             nodeurl = file(os.path.join(self.nodedir, 'node.url'), 'rb').read().strip()
478             if nodeurl[-1] != "/":
479                 nodeurl += "/"
480             url = nodeurl + "uri/%s/" % urllib.quote(root_uri)
481             webbrowser.open(url)
482
483     def mount_filesystem(self, alias_name, mountpoint, display_name=None):
484         log.msg('mount_filesystem(%r,%r,%r)' % (alias_name, mountpoint, display_name))
485         log.msg('sys.exec = %r' % (sys.executable,))
486
487         # first determine if we can find the 'tahoe' binary (i.e. contents of .app)
488         if not sys.executable.endswith('Allmydata.app/Contents/MacOS/python'):
489             log.msg("can't find allmydata.app: sys.executable = %r" % (sys.executable,))
490             wx.MessageBox("Can't determine location of Allmydata.app")
491             return False
492         bin_path = sys.executable[:-6] + 'Allmydata'
493         log.msg('%r exists: %r' % (bin_path, os.path.exists(bin_path),))
494
495         # check mountpoint exists
496         if not os.path.exists(mountpoint):
497             log.msg('mountpoint %r does not exist' % (mountpoint,))
498             return False
499
500         # figure out options for fuse_main
501         foptions = []
502         foptions.append('-olocal') # required to display in Finder on leopard
503         #foptions.append('-ofstypename=allmydata') # shown in 'get info'
504         if display_name is None:
505             display_name = alias_name
506         foptions.append('-ovolname=%s' % (display_name,))
507         timeout = self.config['daemon-timeout']
508         foptions.append('-odaemon_timeout=%d' % (timeout,))
509         icns_path = os.path.join(self.nodedir, 'private/icons', alias_name+'.icns')
510         log.msg('icns_path %r exists: %s' % (icns_path, os.path.exists(icns_path)))
511         if not os.path.exists(icns_path):
512             icns_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable),
513                                                       '../Resources/allmydata.icns'))
514             log.msg('set icns_path=%s' % (icns_path,))
515         if os.path.exists(icns_path):
516             foptions.append('-ovolicon=%s' % (icns_path,))
517
518
519         # actually launch tahoe fuse
520         command = [bin_path, 'fuse', '--alias', alias_name] + foptions + [mountpoint]
521         #log.msg('spawning command %r' % (command,))
522         #proc = Popen(command, cwd=self.nodedir, stdout=PIPE, stderr=PIPE)
523         #log.msg('spawned process, pid %s' % (proc.pid,))
524         self.async_run_cmd(command)
525
526         # log the outcome, record the fact that we mounted this fs
527         #wx.FutureCall(4096, self.check_proc, proc, 'fuse')
528         self.mounted_filesystems[display_name] = mountpoint
529
530         # open finder, if configured to do so
531         if self.config['auto-open']:
532             wx.FutureCall(4096, self.sync_run_cmd, ['/usr/bin/open', mountpoint])
533         return True
534
535     def unmount_filesystems(self):
536         # the user may've already unmounted some, but we should ensure that
537         # anything the gui mounted gets shut down, since they all depend on
538         # the tahoe node, which is going away
539         for name, mountpoint in self.mounted_filesystems.items():
540             log.msg('unmounting %r (%s)' % (name, mountpoint))
541             self.sync_run_cmd(['/sbin/umount', mountpoint])
542
543     def sync_run_cmd(self, argv):
544         log.msg('synchronously running command: %r' % (argv,))
545         proc = Popen(argv, cwd=self.nodedir, stdout=PIPE, stderr=PIPE)
546         proc.wait()
547         self.check_proc(proc)
548
549     def async_run_cmd(self, argv):
550         log.msg('asynchronously running command: %r' % (argv,))
551         proc = Popen(argv, cwd=self.nodedir, stdout=PIPE, stderr=PIPE)
552         log.msg('spawned process, pid: %s' % (proc.pid,))
553         wx.FutureCall(4096, self.check_proc, proc, 'async fuse process:')
554
555     def check_proc(self, proc, description=None):
556         message = []
557         if description is not None:
558             message.append(description)
559         message.append('pid: %s  retcode: %s' % (proc.pid, proc.returncode,))
560         stdout = proc.stdout.read()
561         if stdout:
562             message.append('\nstdout:\n%s' % (stdout,))
563         stderr = proc.stderr.read()
564         if stderr:
565             message.append('\nstdout:\n%s' % (stderr,))
566         log.msg(' '.join(message))
567