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
This commit is contained in:
robk-tahoe 2008-09-25 16:32:35 -07:00
parent 9b10f46374
commit 183dd587d5

View File

@ -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()
self.app_cont.guiapp.ExitMainLoop()
class App(object):
def __init__(self, basedir):
self.basedir = basedir
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))