From: robk-tahoe Date: Thu, 25 Sep 2008 23:32:35 +0000 (-0700) Subject: gui/macapp: slew of code cleanup; unmount filesystems on quit X-Git-Url: https://git.rkrishnan.org/?a=commitdiff_plain;h=183dd587d5b74b0cc361d2826037b874f970006f;p=tahoe-lafs%2Ftahoe-lafs.git gui/macapp: slew of code cleanup; unmount filesystems on quit a handful of code cleanup, renaming and refactoring. basically consolidating 'application logic' (mount/unmount fs) into the 'MacGuiApp' class (the wx.App) and cleaning up various scoping things around that. renamed all references to 'app' to refer more clearly to the 'AppContainer' or to the guiapp. globally renamed basedir -> nodedir also made the guiapp keep a note of each filesystem it mounts, and unmount them upon 'quit' so as to cleanup the user's environment before the tahoe node vanishes from out underneath the orphaned tahoe fuse processes --- diff --git a/src/allmydata/gui/macapp.py b/src/allmydata/gui/macapp.py index 382f326e..70949187 100644 --- a/src/allmydata/gui/macapp.py +++ b/src/allmydata/gui/macapp.py @@ -2,7 +2,7 @@ import operator import os import stat -import subprocess +from subprocess import Popen, PIPE import sys import thread import threading @@ -34,20 +34,25 @@ fi ''' def run_macapp(): - basedir = os.path.expanduser('~/.tahoe') - if not os.path.isdir(basedir): + nodedir = os.path.expanduser('~/.tahoe') + if not os.path.isdir(nodedir): app_supp = os.path.expanduser('~/Library/Application Support/Allmydata Tahoe/') if not os.path.isdir(app_supp): os.makedirs(app_supp) - os.symlink(app_supp, basedir) + os.symlink(app_supp, nodedir) - app = App(basedir) - return app.run() + app_cont = AppContainer(nodedir) + return app_cont.run() class MacGuiClient(client.Client): - def __init__(self, basedir, app): - self.app = app - client.Client.__init__(self, basedir) + """ + This is a subclass of the tahoe 'client' node, which hooks into the + client's 'notice something went wrong' mechanism, to display the fact + in a manner sensible for a wx gui app + """ + def __init__(self, nodedir, app_cont): + self.app_cont = app_cont + client.Client.__init__(self, nodedir) def _service_startup_failed(self, failure): wx.CallAfter(self.wx_abort, failure) @@ -56,40 +61,46 @@ class MacGuiClient(client.Client): def wx_abort(self, failure): wx.MessageBox(failure.getTraceback(), 'Fatal Error in Node startup') - self.app.guiapp.ExitMainLoop() - -class App(object): - def __init__(self, basedir): - self.basedir = basedir + self.app_cont.guiapp.ExitMainLoop() + +class AppContainer(object): + """ + This is the 'container' for the mac app, which takes care of initial + configuration concerns - e.g. running the confwiz before going any further - + of launching the reactor, and within it the tahoe node, on a separate thread, + and then lastly of launching the actual wx gui app and waiting for it to exit. + """ + def __init__(self, nodedir): + self.nodedir = nodedir def files_exist(self, file_list): - extant_conf = [ os.path.exists(os.path.join(self.basedir, f)) for f in file_list ] + extant_conf = [ os.path.exists(os.path.join(self.nodedir, f)) for f in file_list ] return reduce(operator.__and__, extant_conf) def is_config_incomplete(self): necessary_conf_files = ['introducer.furl', 'private/root_dir.cap'] need_config = not self.files_exist(necessary_conf_files) if need_config: - print 'some config is missing from basedir (%s): %s' % (self.basedir, necessary_conf_files) + print 'some config is missing from nodedir (%s): %s' % (self.nodedir, necessary_conf_files) return need_config def run(self): # handle initial config - if not os.path.exists(os.path.join(self.basedir, 'webport')): - f = file(os.path.join(self.basedir, 'webport'), 'wb') + if not os.path.exists(os.path.join(self.nodedir, 'webport')): + f = file(os.path.join(self.nodedir, 'webport'), 'wb') f.write('8123') f.close() if self.is_config_incomplete(): - app = ConfWizApp(DEFAULT_SERVER_URL, open_welcome_page=True) - app.MainLoop() + confwiz = ConfWizApp(DEFAULT_SERVER_URL, open_welcome_page=True) + confwiz.MainLoop() if self.is_config_incomplete(): print 'config still incomplete; confwiz cancelled, exiting' return 1 # set up twisted logging. this will become part of the node rsn. - logdir = os.path.join(self.basedir, 'logs') + logdir = os.path.join(self.nodedir, 'logs') if not os.path.exists(logdir): os.makedirs(logdir) lf = logfile.LogFile('tahoesvc.log', logdir) @@ -99,17 +110,20 @@ class App(object): self.maybe_install_tahoe_script() # actually start up the node and the ui - os.chdir(self.basedir) + os.chdir(self.nodedir) + # start the reactor thread up, launch the tahoe node therein self.start_reactor() try: - self.guiapp = MacGuiApp(app=self) + # launch the actual gui on the wx event loop, wait for it to quit + self.guiapp = MacGuiApp(app_cont=self) self.guiapp.MainLoop() log.msg('gui mainloop exited') except: log.err() + # shutdown the reactor, hence tahoe node, before exiting self.stop_reactor() return 0 @@ -120,8 +134,8 @@ class App(object): def launch_reactor(self): # run the node itself - #c = client.Client(self.basedir) - c = MacGuiClient(self.basedir, self) + #c = client.Client(self.nodedir) + c = MacGuiClient(self.nodedir, app_cont=self) reactor.callLater(0, c.startService) # after reactor startup reactor.run(installSignalHandlers=False) self.reactor_shutdown.set() @@ -133,18 +147,6 @@ class App(object): self.reactor_shutdown.wait() log.msg('reactor shut down') - def webopen(self, alias=None): - log.msg('webopen: %r' % (alias,)) - if alias is None: - alias = 'tahoe' - root_uri = get_aliases(self.basedir).get(alias) - if root_uri: - nodeurl = file(os.path.join(self.basedir, 'node.url'), 'rb').read().strip() - if nodeurl[-1] != "/": - nodeurl += "/" - url = nodeurl + "uri/%s/" % urllib.quote(root_uri) - webbrowser.open(url) - def maybe_install_tahoe_script(self): path_candidates = ['/usr/local/bin', '~/bin', '~/Library/bin'] env_path = map(os.path.expanduser, os.environ['PATH'].split(':')) @@ -253,7 +255,7 @@ class SplashPanel(wx.Panel): class MountFrame(wx.Frame): - def __init__(self, app): + def __init__(self, guiapp): wx.Frame.__init__(self, None, -1, 'Allmydata Mount Filesystem') self.SetSizeHints(100, 100, 600, 800) @@ -262,7 +264,7 @@ class MountFrame(wx.Frame): background = wx.Panel(self, -1) background.parent = self - self.mount_panel = MountPanel(background, self.on_close, app) + self.mount_panel = MountPanel(background, self.on_close, guiapp) sizer = wx.BoxSizer(wx.VERTICAL) background_sizer = wx.BoxSizer(wx.VERTICAL) background_sizer.Add(self.mount_panel, 1, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 26) @@ -276,69 +278,17 @@ class MountFrame(wx.Frame): def on_close(self, event): self.Show(False) - -def check_mount(proc): - message = [ 'pid: %s' % (proc.pid,), - 'ret: %s' % (proc.returncode,), - 'stdout:\n%s' % (proc.stdout.read(),), - 'stderr:\n%s' % (proc.stderr.read(),), - ] - log.msg('\n'.join(['spawned process:'] + message)) - -def mount_filesystem(basedir, alias_name, mountpoint, display_name=None, timeout=None): - log.msg('mount_filesystem(%r,%r,%r,%r)' % (basedir, alias_name, mountpoint, display_name)) - log.msg('sys.exec = %r' % (sys.executable,)) - if not sys.executable.endswith('Allmydata.app/Contents/MacOS/python'): - log.msg("can't find allmydata.app: sys.executable = %r" % (sys.executable,)) - wx.MessageBox("Can't determine location of Allmydata.app") - return False - bin_path = sys.executable[:-6] + 'Allmydata' - log.msg('%r exists: %r' % (bin_path, os.path.exists(bin_path),)) - - foptions = [] - foptions.append('-olocal') # required to display in Finder on leopard - - if display_name is None: - display_name = alias_name - foptions.append('-ovolname=%s' % (display_name,)) - - if timeout is None: - timeout = DEFAULT_FUSE_TIMEOUT - if timeout: - foptions.append('-odaemon_timeout=%d' % (timeout,)) - - icns_path = os.path.join(basedir, 'private', alias_name+'.icns') - if not os.path.exists(icns_path): - icns_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), - '../Resources/allmydata.icns')) - log.msg('set icns_path=%s' % (icns_path,)) - log.msg('icns_path exists: %s' % os.path.exists(icns_path)) - if os.path.exists(icns_path): - foptions.append('-ovolicon=%s' % (icns_path,)) - - command = [bin_path, 'fuse', '--alias', alias_name] + foptions + [mountpoint] - log.msg('spawning command %r' % (command,)) - proc = subprocess.Popen(command, - cwd=basedir, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - log.msg('spawned process, pid %s' % (proc.pid,)) - wx.FutureCall(4096, check_mount, proc) - return True - class MountPanel(wx.Panel): - def __init__(self, parent, on_close, app): + def __init__(self, parent, on_close, guiapp): wx.Panel.__init__(self, parent, -1) self.parent = parent - self.app = app + self.guiapp = guiapp self.sizer = wx.BoxSizer(wx.VERTICAL) - self.aliases = get_aliases(self.app.basedir) - self.label = wx.StaticText(self, -1, 'Allmydata Mount Filesystem') self.mnt_label = wx.StaticText(self, -1, 'Mount') - self.alias_choice = wx.Choice(self, -1, (120, 64), choices=self.aliases.keys()) + self.alias_choice = wx.Choice(self, -1, (120, 64), choices=self.guiapp.aliases.keys()) root_dir = self.alias_choice.FindString('tahoe') if root_dir != -1: self.alias_choice.SetSelection(root_dir) @@ -363,10 +313,6 @@ class MountPanel(wx.Panel): self.SetSizer(self.sizer) self.SetAutoLayout(True) - #def on_choice(self, event): - #choice = event.GetString() - #log.msg('chose dir: %s' % (choice,)) - def on_mount(self, event): mountpoint = str(self.mountpoint.GetValue()) if not os.path.isdir(mountpoint): @@ -385,65 +331,9 @@ class MountPanel(wx.Panel): def do_mount(self, alias_name, mountpoint): log.msg('do_mount(%r, %r)' % (alias_name, mountpoint)) - # XXX this needs a referential cleanup - timeout = self.app.config['daemon-timeout'] - ok = mount_filesystem(self.app.basedir, alias_name, mountpoint, timeout=timeout) - if ok and self.app.config['auto-open']: - wx.FutureCall(2048, open_finder, mountpoint) + self.guiapp.mount_filesystem(alias_name, mountpoint) self.parent.parent.Show(False) - def old_do_mount(self, alias_name, mountpoint): - log.msg('do_mount(%r, %r)' % (alias_name, mountpoint)) - log.msg('sys.exec = %r' % (sys.executable,)) - if not sys.executable.endswith('Allmydata.app/Contents/MacOS/python'): - log.msg("can't find allmydata.app: sys.executable = %r" % (sys.executable,)) - wx.MessageBox("Can't determine location of Allmydata.app") - self.parent.parent.Show(False) - return - bin_path = sys.executable[:-6] + 'Allmydata' - log.msg('%r exists: %r' % (bin_path, os.path.exists(bin_path),)) - - foptions = [] - foptions.append('-ovolname=%s' % (alias_name,)) - foptions.append('-olocal') - - timeout = DEFAULT_FUSE_TIMEOUT - # [ ] TODO: make this configurable - if timeout: - foptions.append('-odaemon_timeout=%d' % (timeout,)) - - icns_path = os.path.join(self.app.basedir, 'private', alias_name+'.icns') - if not os.path.exists(icns_path): - icns_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), - '../Resources/allmydata.icns')) - log.msg('set icns_path=%s' % (icns_path,)) - log.msg('icns_path exists: %s' % os.path.exists(icns_path)) - if os.path.exists(icns_path): - foptions.append('-ovolicon=%s' % (icns_path,)) - - command = [bin_path, 'fuse', '--alias', alias_name] + foptions + [mountpoint] - log.msg('spawning command %r' % (command,)) - proc = subprocess.Popen(command, - cwd=self.app.basedir, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - log.msg('spawned process, pid %s' % (proc.pid,)) - wx.FutureCall(4096, self.check_mount, proc) - self.parent.parent.Show(False) - - def check_mount(self, proc): - message = [ 'pid: %s' % (proc.pid,), - 'ret: %s' % (proc.returncode,), - 'stdout:\n%s' % (proc.stdout.read(),), - 'stderr:\n%s' % (proc.stderr.read(),), - ] - log.msg('\n'.join(['spawned process:'] + message)) - -def open_finder(path): - proc = subprocess.Popen(['/usr/bin/open', path], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - class MacGuiApp(wx.App): config = { 'auto-mount': True, @@ -452,15 +342,18 @@ class MacGuiApp(wx.App): 'daemon-timeout': DEFAULT_FUSE_TIMEOUT, } - def __init__(self, app): - self.app = app + def __init__(self, app_cont): + self.app_cont = app_cont + self.nodedir = app_cont.nodedir self.load_config() + self.mounted_filesystems = {} + self.aliases = get_aliases(self.nodedir) wx.App.__init__(self) - # XXX should have a table of all mounted filesystems, so that can unmount on quit + ## load up setting from gui.conf dir def load_config(self): log.msg('load_config') - confdir = os.path.join(self.app.basedir, 'gui.conf') + confdir = os.path.join(self.nodedir, 'gui.conf') config = {} config.update(self.config) for k in self.config: @@ -476,8 +369,8 @@ class MacGuiApp(wx.App): val = int(val) config[k] = val self.config = config - #log.msg('loaded config: %r' % (self.config,)) # XXX + ## GUI wx init def OnInit(self): log.msg('OnInit') try: @@ -490,10 +383,7 @@ class MacGuiApp(wx.App): wx.FutureCall(4096, self.on_timer, None) - # XXX clean up this ref - self.app.config = self.config - - self.mount_frame = MountFrame(self.app) + self.mount_frame = MountFrame(guiapp=self) self.setup_dock_icon() menubar = self.setup_app_menu(self.frame) @@ -504,21 +394,12 @@ class MacGuiApp(wx.App): DisplayTraceback('exception on startup') sys.exit() + ## WX menu and event handling + def on_timer(self, event): self.frame.Show(False) self.perhaps_automount() - def perhaps_automount(self): - if self.config['auto-mount']: - mountpoint = os.path.expanduser('~/.tahoe/mnt/__auto__') - if not os.path.isdir(mountpoint): - os.makedirs(mountpoint) - timeout = self.config['daemon-timeout'] - mount_filesystem(self.app.basedir, 'tahoe', mountpoint, 'Allmydata', timeout) - if self.config['auto-open']: - assert os.path.isdir(mountpoint) - wx.FutureCall(2048, open_finder, mountpoint) - def setup_dock_icon(self): self.tbicon = wx.TaskBarIcon() #self.tbicon.SetIcon(amdicon.getIcon(), "Allmydata") @@ -529,17 +410,13 @@ class MacGuiApp(wx.App): file_menu = wx.Menu() if self.config['show-webopen']: webopen_menu = wx.Menu() - # XXX should promote to mac app inst var - aliases = get_aliases(self.app.basedir) self.webopen_menu_ids = {} - for alias in aliases: + for alias in self.aliases: mid = wx.NewId() self.webopen_menu_ids[mid] = alias item = webopen_menu.Append(mid, alias) frame.Bind(wx.EVT_MENU, self.on_webopen, item) - #log.msg('menu ids: %r' % (self.webopen_menu_ids,)) file_menu.AppendMenu(WEBOPEN_ID, 'Open Web UI', webopen_menu) - self.aliases = get_aliases(self.app.basedir) item = file_menu.Append(ACCOUNT_PAGE_ID, text='Open Account Page') frame.Bind(wx.EVT_MENU, self.on_account_page, item) item = file_menu.Append(MOUNT_ID, text='Mount Filesystem') @@ -568,13 +445,13 @@ class MacGuiApp(wx.App): self.frame.Show(True) def on_quit(self, event): - #XXX unmount mounted (automounted!) filesystems + self.unmount_filesystems() self.ExitMainLoop() def on_webopen(self, event): alias = self.webopen_menu_ids.get(event.GetId()) #log.msg('on_webopen() alias=%r' % (alias,)) - self.app.webopen(alias) + self.webopen(alias) def on_account_page(self, event): webbrowser.open(DEFAULT_SERVER_URL + ACCOUNT_PAGE) @@ -582,3 +459,109 @@ class MacGuiApp(wx.App): def on_mount(self, event): self.mount_frame.Show(True) + ## Gui App methods + + def perhaps_automount(self): + if self.config['auto-mount']: + mountpoint = os.path.join(self.nodedir, 'mnt/__auto__') + if not os.path.isdir(mountpoint): + os.makedirs(mountpoint) + self.mount_filesystem(self.nodedir, 'tahoe', mountpoint, 'Allmydata') + + def webopen(self, alias=None): + log.msg('webopen: %r' % (alias,)) + if alias is None: + alias = 'tahoe' + root_uri = self.aliases.get(alias) + if root_uri: + nodeurl = file(os.path.join(self.nodedir, 'node.url'), 'rb').read().strip() + if nodeurl[-1] != "/": + nodeurl += "/" + url = nodeurl + "uri/%s/" % urllib.quote(root_uri) + webbrowser.open(url) + + def mount_filesystem(self, alias_name, mountpoint, display_name=None): + log.msg('mount_filesystem(%r,%r,%r)' % (alias_name, mountpoint, display_name)) + log.msg('sys.exec = %r' % (sys.executable,)) + + # first determine if we can find the 'tahoe' binary (i.e. contents of .app) + if not sys.executable.endswith('Allmydata.app/Contents/MacOS/python'): + log.msg("can't find allmydata.app: sys.executable = %r" % (sys.executable,)) + wx.MessageBox("Can't determine location of Allmydata.app") + return False + bin_path = sys.executable[:-6] + 'Allmydata' + log.msg('%r exists: %r' % (bin_path, os.path.exists(bin_path),)) + + # check mountpoint exists + if not os.path.exists(mountpoint): + log.msg('mountpoint %r does not exist' % (mountpoint,)) + return False + + # figure out options for fuse_main + foptions = [] + foptions.append('-olocal') # required to display in Finder on leopard + foptions.append('-ofstypename=allmydata') # shown in 'get info' + if display_name is None: + display_name = alias_name + foptions.append('-ovolname=%s' % (display_name,)) + timeout = self.config['daemon-timeout'] + foptions.append('-odaemon_timeout=%d' % (timeout,)) + icns_path = os.path.join(self.nodedir, 'private/icons', alias_name+'.icns') + log.msg('icns_path %r exists: %s' % (icns_path, os.path.exists(icns_path))) + if not os.path.exists(icns_path): + icns_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), + '../Resources/allmydata.icns')) + log.msg('set icns_path=%s' % (icns_path,)) + if os.path.exists(icns_path): + foptions.append('-ovolicon=%s' % (icns_path,)) + + + # actually launch tahoe fuse + command = [bin_path, 'fuse', '--alias', alias_name] + foptions + [mountpoint] + #log.msg('spawning command %r' % (command,)) + #proc = Popen(command, cwd=self.nodedir, stdout=PIPE, stderr=PIPE) + #log.msg('spawned process, pid %s' % (proc.pid,)) + self.async_run_cmd(command) + + # log the outcome, record the fact that we mounted this fs + #wx.FutureCall(4096, self.check_proc, proc, 'fuse') + self.mounted_filesystems[display_name] = mountpoint + + # open finder, if configured to do so + if self.config['auto-open']: + wx.FutureCall(2048, self.sync_run_cmd, ['/usr/bin/open', mountpoint]) + return True + + def unmount_filesystems(self): + # the user may've already unmounted some, but we should ensure that + # anything the gui mounted gets shut down, since they all depend on + # the tahoe node, which is going away + for name, mountpoint in self.mounted_filesystems.items(): + log.msg('unmounting %r (%s)' % (name, mountpoint)) + self.sync_run_cmd(['/sbin/umount', mountpoint]) + + def sync_run_cmd(self, argv): + log.msg('synchronously running command: %r' % (argv,)) + proc = Popen(argv, cwd=self.nodedir, stdout=PIPE, stderr=PIPE) + proc.wait() + self.check_proc(proc) + + def async_run_cmd(self, argv): + log.msg('asynchronously running command: %r' % (argv,)) + proc = Popen(argv, cwd=self.nodedir, stdout=PIPE, stderr=PIPE) + log.msg('spawned process, pid: %s' % (proc.pid,)) + wx.FutureCall(4096, self.check_proc, proc, 'async fuse process:') + + def check_proc(self, proc, description=None): + message = [] + if description is not None: + message.append(description) + message.append('pid: %s retcode: %s' % (proc.pid, proc.returncode,)) + stdout = proc.stdout.read() + if stdout: + message.append('\nstdout:\n%s' % (stdout,)) + stderr = proc.stderr.read() + if stderr: + message.append('\nstdout:\n%s' % (stderr,)) + log.msg(' '.join(message)) +