From 183dd587d5b74b0cc361d2826037b874f970006f Mon Sep 17 00:00:00 2001
From: robk-tahoe <robk-tahoe@allmydata.com>
Date: Thu, 25 Sep 2008 16:32:35 -0700
Subject: [PATCH] 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
---
 src/allmydata/gui/macapp.py | 347 +++++++++++++++++-------------------
 1 file changed, 165 insertions(+), 182 deletions(-)

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))
+
-- 
2.45.2