5 from subprocess import Popen, PIPE
14 from twisted.internet import reactor
15 from twisted.python import log, logfile
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
24 DEFAULT_FUSE_TIMEOUT = 300
26 TRY_TO_INSTALL_TAHOE_SCRIPT = True
27 TAHOE_SCRIPT = '''#!/bin/bash
28 if [ "x${*}x" == "xx" ]
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):
42 os.symlink(app_supp, nodedir)
44 app_cont = AppContainer(nodedir)
47 class MacGuiClient(client.Client):
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
53 def __init__(self, nodedir, app_cont):
54 self.app_cont = app_cont
55 client.Client.__init__(self, nodedir)
57 def _service_startup_failed(self, failure):
58 wx.CallAfter(self.wx_abort, failure)
59 log.msg('node service startup failed')
62 def wx_abort(self, failure):
63 wx.MessageBox(failure.getTraceback(), 'Fatal Error in Node startup')
64 self.app_cont.guiapp.ExitMainLoop()
66 class AppContainer(object):
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.
73 def __init__(self, nodedir):
74 self.nodedir = nodedir
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)
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)
84 print 'some config is missing from nodedir (%s): %s' % (self.nodedir, necessary_conf_files)
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')
94 if self.is_config_incomplete():
95 confwiz = ConfWizApp(DEFAULT_SERVER_URL, open_welcome_page=True)
98 if self.is_config_incomplete():
99 print 'config still incomplete; confwiz cancelled, exiting'
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):
106 lf = logfile.LogFile('tahoesvc.log', logdir)
109 if TRY_TO_INSTALL_TAHOE_SCRIPT:
110 self.maybe_install_tahoe_script()
112 # actually start up the node and the ui
113 os.chdir(self.nodedir)
115 # start the reactor thread up, launch the tahoe node therein
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')
126 # shutdown the reactor, hence tahoe node, before exiting
131 def start_reactor(self):
132 self.reactor_shutdown = threading.Event()
133 thread.start_new_thread(self.launch_reactor, ())
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()
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')
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,)
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,)
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
165 tahoe_path = path_candidate + '/tahoe'
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')
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,)
178 print 'unable to write %s' % (tahoe_path,)
179 traceback.print_exc()
181 print 'no remaining candidate paths for installation of tahoe script'
184 def DisplayTraceback(message):
185 xc = traceback.format_exception(*sys.exc_info())
186 wx.MessageBox(u"%s\n (%s)"%(message,''.join(xc)), 'Error')
188 WEBOPEN_ID = wx.NewId()
189 ACCOUNT_PAGE_ID = wx.NewId()
190 MOUNT_ID = wx.NewId()
192 class SplashFrame(wx.Frame):
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)
197 self.SetSizeHints(100, 100, 600, 800)
198 self.SetIcon(amdicon.getIcon())
199 self.Bind(wx.EVT_CLOSE, self.on_close)
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)
210 self.SetAutoLayout(True)
214 def on_close(self, event):
217 class SplashPanel(wx.Panel):
218 def __init__(self, parent, on_close):
219 wx.Panel.__init__(self, parent, -1)
222 hbox = wx.BoxSizer(wx.HORIZONTAL)
223 vbox = wx.BoxSizer(wx.VERTICAL)
224 self.sizer = wx.BoxSizer(wx.VERTICAL)
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)
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)
257 class MountFrame(wx.Frame):
258 def __init__(self, guiapp):
259 wx.Frame.__init__(self, None, -1, 'Allmydata Mount Filesystem')
261 self.SetSizeHints(100, 100, 600, 800)
262 self.SetIcon(amdicon.getIcon())
263 self.Bind(wx.EVT_CLOSE, self.on_close)
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)
274 self.SetAutoLayout(True)
278 def on_close(self, event):
281 class MountPanel(wx.Panel):
282 def __init__(self, parent, on_close, guiapp):
283 wx.Panel.__init__(self, parent, -1)
287 self.sizer = wx.BoxSizer(wx.VERTICAL)
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')
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)
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,))
321 alias_name = self.alias_choice.GetStringSelection()
322 self.do_mount(alias_name, mountpoint)
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)
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)
337 class MacGuiApp(wx.App):
341 'show-webopen': False,
342 'daemon-timeout': DEFAULT_FUSE_TIMEOUT,
345 def __init__(self, app_cont):
346 self.app_cont = app_cont
347 self.nodedir = app_cont.nodedir
349 self.mounted_filesystems = {}
350 self.aliases = get_aliases(self.nodedir)
351 wx.App.__init__(self)
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')
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']:
368 elif type(self.config[k]) == int:
377 self.frame = SplashFrame()
378 self.frame.CenterOnScreen()
379 self.frame.Show(True)
380 self.SetTopWindow(self.frame)
384 wx.FutureCall(4096, self.on_timer, None)
386 self.mount_frame = MountFrame(guiapp=self)
388 self.setup_dock_icon()
389 menubar = self.setup_app_menu(self.frame)
390 self.frame.SetMenuBar(menubar)
394 DisplayTraceback('exception on startup')
397 ## WX menu and event handling
399 def on_timer(self, event):
400 self.frame.Show(False)
401 self.perhaps_automount()
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)
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:
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')
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)
444 def on_about(self, event):
445 self.frame.Show(True)
447 def on_quit(self, event):
448 self.unmount_filesystems()
451 def on_webopen(self, event):
452 alias = self.webopen_menu_ids.get(event.GetId())
453 #log.msg('on_webopen() alias=%r' % (alias,))
456 def on_account_page(self, event):
457 webbrowser.open(DEFAULT_SERVER_URL + ACCOUNT_PAGE)
459 def on_mount(self, event):
460 self.mount_frame.Show(True)
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')
471 def webopen(self, alias=None):
472 log.msg('webopen: %r' % (alias,))
475 root_uri = self.aliases.get(alias)
477 nodeurl = file(os.path.join(self.nodedir, 'node.url'), 'rb').read().strip()
478 if nodeurl[-1] != "/":
480 url = nodeurl + "uri/%s/" % urllib.quote(root_uri)
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,))
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")
492 bin_path = sys.executable[:-6] + 'Allmydata'
493 log.msg('%r exists: %r' % (bin_path, os.path.exists(bin_path),))
495 # check mountpoint exists
496 if not os.path.exists(mountpoint):
497 log.msg('mountpoint %r does not exist' % (mountpoint,))
500 # figure out options for fuse_main
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,))
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)
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
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])
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])
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)
547 self.check_proc(proc)
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:')
555 def check_proc(self, proc, description=None):
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()
562 message.append('\nstdout:\n%s' % (stdout,))
563 stderr = proc.stderr.read()
565 message.append('\nstdout:\n%s' % (stderr,))
566 log.msg(' '.join(message))