From 840cb864a3b41ccff310077eff487c3fa1d6b805 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Sat, 5 Jan 2013 09:17:51 -0500 Subject: MAINT-2155: replace embedded mac-updater.app with a Python script. Remove mac-updater subtree from viewer source, along with the update_install bash script that invoked it. Remove all mention of mac-updater in CMakeLists.txt files and in viewer_manifest.py. Change Mac update_install bash script references in viewer_manifest.py and in llupdaterservice.cpp (which invokes it) to new Python update_install.py. Add update_install.py, messageframe.py (which puts up some Tkinter UI) and janitor.py (cloned from vita, it's exactly what we need here). --- .../updater/scripts/darwin/janitor.py | 133 ++++++++ .../updater/scripts/darwin/messageframe.py | 66 ++++ .../updater/scripts/darwin/update_install | 10 - .../updater/scripts/darwin/update_install.py | 336 +++++++++++++++++++++ 4 files changed, 535 insertions(+), 10 deletions(-) create mode 100644 indra/viewer_components/updater/scripts/darwin/janitor.py create mode 100644 indra/viewer_components/updater/scripts/darwin/messageframe.py delete mode 100644 indra/viewer_components/updater/scripts/darwin/update_install create mode 100755 indra/viewer_components/updater/scripts/darwin/update_install.py (limited to 'indra/viewer_components/updater/scripts/darwin') diff --git a/indra/viewer_components/updater/scripts/darwin/janitor.py b/indra/viewer_components/updater/scripts/darwin/janitor.py new file mode 100644 index 0000000000..cdf33df731 --- /dev/null +++ b/indra/viewer_components/updater/scripts/darwin/janitor.py @@ -0,0 +1,133 @@ +#!/usr/bin/python +"""\ +@file janitor.py +@author Nat Goodspeed +@date 2011-09-14 +@brief Janitor class to clean up arbitrary resources + +2013-01-04 cloned from vita because it's exactly what update_install.py needs. + +$LicenseInfo:firstyear=2011&license=viewerlgpl$ +Copyright (c) 2011, Linden Research, Inc. +$/LicenseInfo$ +""" + +import sys +import functools +import itertools + +class Janitor(object): + """ + Usage: + + Basic: + self.janitor = Janitor(sys.stdout) # report cleanup actions on stdout + ... + self.janitor.later(os.remove, some_temp_file) + self.janitor.later(os.remove, some_other_file) + ... + self.janitor.cleanup() # perform cleanup actions + + Context Manager: + with Janitor() as janitor: # clean up quietly + ... + janitor.later(shutil.rmtree, some_temp_directory) + ... + # exiting 'with' block performs cleanup + + Test Class: + class TestMySoftware(unittest.TestCase, Janitor): + def __init__(self): + Janitor.__init__(self) # quiet cleanup + ... + + def setUp(self): + ... + self.later(os.rename, saved_file, original_location) + ... + + def tearDown(self): + Janitor.tearDown(self) # calls cleanup() + ... + # Or, if you have no other tearDown() logic for + # TestMySoftware, you can omit the TestMySoftware.tearDown() + # def entirely and let it inherit Janitor.tearDown(). + """ + def __init__(self, stream=None): + """ + If you pass stream= (e.g.) sys.stdout or sys.stderr, Janitor will + report its cleanup operations as it performs them. If you don't, it + will perform them quietly -- unless one or more of the actions throws + an exception, in which case you'll get output on stderr. + """ + self.stream = stream + self.cleanups = [] + + def later(self, func, *args, **kwds): + """ + Pass the callable you want to call at cleanup() time, plus any + positional or keyword args you want to pass it. + """ + # Get a name string for 'func' + try: + # A free function has a __name__ + name = func.__name__ + except AttributeError: + try: + # A class object (even builtin objects like ints!) support + # __class__.__name__ + name = func.__class__.__name__ + except AttributeError: + # Shrug! Just use repr() to get a string describing this func. + name = repr(func) + # Construct a description of this operation in Python syntax from + # args, kwds. + desc = "%s(%s)" % \ + (name, ", ".join(itertools.chain((repr(a) for a in args), + ("%s=%r" % (k, v) for (k, v) in kwds.iteritems())))) + # Use functools.partial() to bind passed args and keywords to the + # passed func so we get a nullary callable that does what caller + # wants. + bound = functools.partial(func, *args, **kwds) + self.cleanups.append((desc, bound)) + + def cleanup(self): + """ + Perform all the actions saved with later() calls. + """ + # Typically one allocates resource A, then allocates resource B that + # depends on it. In such a scenario it's appropriate to delete B + # before A -- so perform cleanup actions in reverse order. (This is + # the same strategy used by atexit().) + while self.cleanups: + # Until our list is empty, pop the last pair. + desc, bound = self.cleanups.pop(-1) + + # If requested, report the action. + if self.stream is not None: + print >>self.stream, desc + + try: + # Call the bound callable + bound() + except Exception, err: + # This is cleanup. Report the problem but continue. + print >>(self.stream or sys.stderr), "Calling %s\nraised %s: %s" % \ + (desc, err.__class__.__name__, err) + + def tearDown(self): + """ + If a unittest.TestCase subclass (or a nose test class) adds Janitor as + one of its base classes, and has no other tearDown() logic, let it + inherit Janitor.tearDown(). + """ + self.cleanup() + + def __enter__(self): + return self + + def __exit__(self, type, value, tb): + # Perform cleanup no matter how we exit this 'with' statement + self.cleanup() + # Propagate any exception from the 'with' statement, don't swallow it + return False diff --git a/indra/viewer_components/updater/scripts/darwin/messageframe.py b/indra/viewer_components/updater/scripts/darwin/messageframe.py new file mode 100644 index 0000000000..8f58848882 --- /dev/null +++ b/indra/viewer_components/updater/scripts/darwin/messageframe.py @@ -0,0 +1,66 @@ +#!/usr/bin/python +"""\ +@file messageframe.py +@author Nat Goodspeed +@date 2013-01-03 +@brief Define MessageFrame class for popping up messages from a command-line + script. + +$LicenseInfo:firstyear=2013&license=viewerlgpl$ +Copyright (c) 2013, Linden Research, Inc. +$/LicenseInfo$ +""" + +import Tkinter as tk +import os + +# Tricky way to obtain the filename of the main script (default title string) +import __main__ + +# This class is intended for displaying messages from a command-line script. +# Getting the base class right took a bit of trial and error. +# If you derive from tk.Frame, the destroy() method doesn't actually close it. +# If you derive from tk.Toplevel, it pops up a separate Tk frame too. destroy() +# closes this frame, but not that one. +# Deriving from tk.Tk appears to do the right thing. +class MessageFrame(tk.Tk): + def __init__(self, text="", title=os.path.splitext(os.path.basename(__main__.__file__))[0], + width=320, height=120): + tk.Tk.__init__(self) + self.grid() + self.title(title) + self.var = tk.StringVar() + self.var.set(text) + self.msg = tk.Label(self, textvariable=self.var) + self.msg.grid() + # from http://stackoverflow.com/questions/3352918/how-to-center-a-window-on-the-screen-in-tkinter : + self.update_idletasks() + + # The constants below are to adjust for typical overhead from the + # frame borders. + xp = (self.winfo_screenwidth() / 2) - (width / 2) - 8 + yp = (self.winfo_screenheight() / 2) - (height / 2) - 20 + self.geometry('{0}x{1}+{2}+{3}'.format(width, height, xp, yp)) + self.update() + + def set(self, text): + self.var.set(text) + self.update() + +if __name__ == "__main__": + # When run as a script, just test the MessageFrame. + import sys + import time + + frame = MessageFrame("something in the way she moves....") + time.sleep(3) + frame.set("smaller") + time.sleep(3) + frame.set("""this has +several +lines""") + time.sleep(3) + frame.destroy() + print "Destroyed!" + sys.stdout.flush() + time.sleep(3) diff --git a/indra/viewer_components/updater/scripts/darwin/update_install b/indra/viewer_components/updater/scripts/darwin/update_install deleted file mode 100644 index e7f36dc5a3..0000000000 --- a/indra/viewer_components/updater/scripts/darwin/update_install +++ /dev/null @@ -1,10 +0,0 @@ -#! /bin/bash - -# -# The first argument contains the path to the installer app. The second a path -# to a marker file which should be created if the installer fails.q -# - -cd "$(dirname "$0")" -(../Resources/mac-updater.app/Contents/MacOS/mac-updater -dmg "$1" -name "Second Life Viewer"; if [ $? -ne 0 ]; then echo $3 >> "$2"; fi;) & -exit 0 diff --git a/indra/viewer_components/updater/scripts/darwin/update_install.py b/indra/viewer_components/updater/scripts/darwin/update_install.py new file mode 100755 index 0000000000..e8b96e6123 --- /dev/null +++ b/indra/viewer_components/updater/scripts/darwin/update_install.py @@ -0,0 +1,336 @@ +#!/usr/bin/python +"""\ +@file update_install.py +@author Nat Goodspeed +@date 2012-12-20 +@brief Update the containing Second Life application bundle to the version in + the specified disk image file. + + This Python implementation is derived from the previous mac-updater + application, a funky mix of C++, classic C and Objective-C. + +$LicenseInfo:firstyear=2012&license=viewerlgpl$ +Copyright (c) 2012, Linden Research, Inc. +$/LicenseInfo$ +""" + +import os +import sys +import cgitb +import errno +import glob +import plistlib +import re +import shutil +import subprocess +import tempfile +import time +from janitor import Janitor +from messageframe import MessageFrame +import Tkinter, tkMessageBox + +TITLE = "SecondLife Updater" +# Magic bundle identifier used by all Second Life viewer bundles +BUNDLE_IDENTIFIER = "com.secondlife.indra.viewer" + +# Global handle to the MessageFrame so we can update message +FRAME = None +# Global handle to logfile, once it's open +LOGF = None + +# **************************************************************************** +# Logging and messaging +# +# This script is normally run implicitly by the old viewer to update to the +# new viewer. Its UI consists of a MessageFrame and possibly a Tk error box. +# Log details to updater.log -- especially uncaught exceptions! +# **************************************************************************** +def log(message): + """write message only to LOGF (also called by status() and fail())""" + # If we don't even have LOGF open yet, at least write to Console log + logf = LOGF or sys.stderr + logf.writelines((time.strftime("%Y-%m-%dT%H:%M:%SZ ", time.gmtime()), message, '\n')) + logf.flush() + +def status(message): + """display and log normal progress message""" + log(message) + + global FRAME + if not FRAME: + FRAME = MessageFrame(message, TITLE) + else: + FRAME.set(message) + +def fail(message): + """log message, produce error box, then terminate with nonzero rc""" + log(message) + + # If we haven't yet called status() (we don't yet have a FRAME), perform a + # bit of trickery to bypass the spurious "main window" that Tkinter would + # otherwise pop up if the first call is showerror(). + if not FRAME: + root = Tkinter.Tk() + root.withdraw() + + # If we do have a LOGF available, mention it in the error box. + if LOGF: + message = "%s\n(Updater log in %s)" % (message, LOGF.name) + + # We explicitly specify the WARNING icon because, at least on the Tkinter + # bundled with the system-default Python 2.7 on Mac OS X 10.7.4, the + # ERROR, QUESTION and INFO icons are all the silly Tk rocket ship. At + # least WARNING has an exclamation in a yellow triangle, even though + # overlaid by a smaller image of the rocket ship. + tkMessageBox.showerror(TITLE, +"""An error occurred while updating Second Life: +%s +Please download the latest viewer from www.secondlife.com.""" % message, + icon=tkMessageBox.WARNING) + sys.exit(1) + +def exception(err): + """call fail() with an exception instance""" + fail("%s exception: %s" % (err.__class__.__name__, str(err))) + +def excepthook(type, value, traceback): + """ + Store this hook function into sys.excepthook until we have a logfile. + """ + # At least in older Python versions, it could be tricky to produce a + # string from 'type' and 'value'. For instance, an OSError exception would + # pass type=OSError and value=some_tuple. Empirically, this funky + # expression seems to work. + exception(type(*value)) +sys.excepthook = excepthook + +class ExceptHook(object): + """ + Store an instance of this class into sys.excepthook once we have a logfile + open. + """ + def __init__(self, logfile): + # There's no magic to the cgitb.enable() function -- it merely stores + # an instance of cgitb.Hook into sys.excepthook, passing enable()'s + # params into Hook.__init__(). Sadly, enable() doesn't forward all its + # params using (*args, **kwds) syntax -- another story. But the point + # is that all the goodness is in the cgitb.Hook class. Capture an + # instance. + self.hook = cgitb.Hook(file=logfile, format="text") + + def __call__(self, type, value, traceback): + # produce nice text traceback to logfile + self.hook(type, value, traceback) + # Now display an error box. + excepthook(type, value, traceback) + +def write_marker(markerfile, markertext): + log("writing %r to %s" % (markertext, markerfile)) + try: + with open(markerfile, "w") as markerf: + markerf.write(markertext) + except IOError, err: + # write_marker() is invoked by fail(), and fail() is invoked by other + # error-handling functions. If we try to invoke any of those, we'll + # get infinite recursion. If for any reason we can't write markerfile, + # try to log it -- otherwise shrug. + log("%s exception: %s" % (err.__class__.__name__, err)) + +# **************************************************************************** +# Main script logic +# **************************************************************************** +def main(dmgfile, markerfile, markertext, appdir=None): + # Should we fail, we're supposed to write 'markertext' to 'markerfile'. + # Wrap the fail() function so we do that. + global fail + oldfail = fail + def fail(message): + write_marker(markerfile, markertext) + oldfail(message) + + try: + # Starting with the Cocoafied viewer, we'll find viewer logs in + # ~/Library/Application Support/$CFBundleIdentifier/logs rather than in + # ~/Library/Application Support/SecondLife/logs as before. This could be + # obnoxious -- but we Happen To Know that markerfile is a path specified + # within the viewer's logs directory. Use that. + logsdir = os.path.dirname(markerfile) + + # Move the old updater.log file out of the way + logname = os.path.join(logsdir, "updater.log") + try: + os.rename(logname, logname + ".old") + except OSError, err: + # Nonexistence is okay. Anything else, not so much. + if err.errno != errno.EEXIST: + raise + + # Open new updater.log. + global LOGF + LOGF = open(logname, "w") + + # Now that LOGF is in fact open for business, use it to log any further + # uncaught exceptions. + sys.excepthook = ExceptHook(LOGF) + + # log how this script was invoked + log(' '.join(repr(arg) for arg in sys.argv)) + + # prepare for other cleanup + with Janitor(LOGF) as janitor: + + # Hopefully caller explicitly stated the viewer bundle to update. + # But if not, try to derive it from our own pathname. (The only + # trouble with that is that the old viewer might copy this script + # to a temp dir before running.) + if not appdir: + # Somewhat peculiarly, this script is currently packaged in + # Appname.app/Contents/MacOS with the viewer executable. But even if we + # decide to move it to Appname.app/Contents/Resources, we'll still find + # Appname.app two levels up from dirname(__file__). + appdir = os.path.abspath(os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir)) + if not appdir.endswith(".app"): + fail(appdir + " is not an application directory") + + # We need to install into appdir's parent directory -- can we? + installdir = os.path.abspath(os.path.join(appdir, os.pardir)) + if not os.access(installdir, os.W_OK): + fail("Can't modify " + installdir) + + # invent a temporary directory + tempdir = tempfile.mkdtemp() + log("created " + tempdir) + # clean it up when we leave + janitor.later(shutil.rmtree, tempdir) + + status("Mounting image...") + + mntdir = os.path.join(tempdir, "mnt") + log("mkdir " + mntdir) + os.mkdir(mntdir) + command = ["hdiutil", "attach", dmgfile, "-mountpoint", mntdir] + log(' '.join(command)) + # Instantiating subprocess.Popen launches a child process with the + # specified command line. stdout=PIPE passes a pipe to its stdout. + hdiutil = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=LOGF) + # Popen.communicate() reads that pipe until the child process + # terminates, returning (stdout, stderr) output. Select just stdout. + hdiutil_out = hdiutil.communicate()[0] + if hdiutil.returncode != 0: + fail("Couldn't mount " + dmgfile) + # hdiutil should report the devnode. Find that. + found = re.search(r"/dev/[^ ]*\b", hdiutil_out) + if not found: + # If we don't spot the devnode, log it and continue -- we only + # use it to detach it. Don't fail the whole update if we can't + # clean up properly. + log("Couldn't spot devnode in hdiutil output:\n" + hdiutil_out) + else: + # If we do spot the devnode, detach it when done. + janitor.later(subprocess.call, ["hdiutil", "detach", found.group(0)], + stdout=LOGF, stderr=subprocess.STDOUT) + + status("Searching for app bundle...") + + for candidate in glob.glob(os.path.join(mntdir, "*.app")): + log("Considering " + candidate) + try: + # By convention, a valid Mac app bundle has a + # Contents/Info.plist file containing at least + # CFBundleIdentifier. + CFBundleIdentifier = \ + plistlib.readPlist(os.path.join(candidate, "Contents", + "Info.plist"))["CFBundleIdentifier"] + except Exception, err: + # might be IOError, xml.parsers.expat.ExpatError, KeyError + # Any of these means it's not a valid app bundle. Instead + # of aborting, just skip this candidate and continue. + log("%s not a valid app bundle: %s: %s" % + (candidate, err.__class__.__name__, err)) + continue + + if CFBundleIdentifier == BUNDLE_IDENTIFIER: + break + + log("unrecognized CFBundleIdentifier: " + CFBundleIdentifier) + + else: + fail("Could not find Second Life viewer in " + dmgfile) + + # Here 'candidate' is the new viewer to install + log("Found " + candidate) + status("Preparing to copy files...") + + # move old viewer to temp location in case copy from .dmg fails + aside = os.path.join(tempdir, os.path.basename(appdir)) + log("mv %r %r" % (appdir, aside)) + # Use shutil.move() instead of os.rename(). move() first tries + # os.rename(), but falls back to shutil.copytree() if the dest is + # on a different filesystem. + shutil.move(appdir, aside) + + status("Copying files...") + + # shutil.copytree()'s target must not already exist. But we just + # moved appdir out of the way. + log("cp -p %r %r" % (candidate, appdir)) + try: + # The viewer app bundle does include internal symlinks. Keep them + # as symlinks. + shutil.copytree(candidate, appdir, symlinks=True) + except Exception, err: + # copy failed -- try to restore previous viewer before crumping + type, value, traceback = sys.exc_info() + log("exception response: mv %r %r" % (aside, appdir)) + shutil.move(aside, appdir) + # let our previously-set sys.excepthook handle this + raise type, value, traceback + + status("Clearing cache...") + + # We don't know whether the previous viewer was old-style or + # new-style (Cocoa). Clear both kinds of caches. + for cachesubdir in "SecondLife", BUNDLE_IDENTIFIER: + wildcard = "~/Library/Caches/%s/*" % cachesubdir + log("rm " + wildcard) + for f in glob.glob(os.path.expanduser(wildcard)): + # Don't try to remove subdirs this way + if os.path.isfile(f): + try: + os.remove(f) + except Exception, err: + log("%s removing %s: %s" % (err.__class__.__name__, f, err)) + + status("Cleaning up...") + + log("touch " + appdir) + os.utime(appdir, None) # set to current time + + command = ["open", appdir] + log(' '.join(command)) + subprocess.check_call(command, stdout=LOGF, stderr=subprocess.STDOUT) + + except Exception, err: + # Because we carefully set sys.excepthook -- and even modify it to log + # the problem once we have our log file open -- you might think we + # could just let exceptions propagate. But when we do that, on + # exception in this block, we FIRST restore the no-side-effects fail() + # and THEN implicitly call sys.excepthook(), which calls the (no-side- + # effects) fail(). Explicitly call sys.excepthook() BEFORE restoring + # fail(). Only then do we get the enriched fail() behavior. + sys.excepthook(*sys.exc_info()) + + finally: + # When we leave main() -- for whatever reason -- reset fail() the way + # it was before, because the bound markerfile, markertext params + # passed to this main() call are no longer applicable. + fail = oldfail + +if __name__ == "__main__": + # We expect this script to be invoked with: + # - the pathname to the .dmg we intend to install; + # - the pathname to an update-error marker file to create on failure; + # - the content to write into the marker file; + # - optionally, the pathname of the Second Life viewer to update. + main(*sys.argv[1:]) -- cgit v1.2.3 From c3542b5e46c6599a4f3003b7a598072c9898412b Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 8 Jan 2013 14:50:34 -0500 Subject: MAINT-2155: when permitting missing old logfile, check for ENOENT instead of errno.EEXIST. Sigh. --- indra/viewer_components/updater/scripts/darwin/update_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra/viewer_components/updater/scripts/darwin') diff --git a/indra/viewer_components/updater/scripts/darwin/update_install.py b/indra/viewer_components/updater/scripts/darwin/update_install.py index e8b96e6123..87b931fbeb 100755 --- a/indra/viewer_components/updater/scripts/darwin/update_install.py +++ b/indra/viewer_components/updater/scripts/darwin/update_install.py @@ -162,7 +162,7 @@ def main(dmgfile, markerfile, markertext, appdir=None): os.rename(logname, logname + ".old") except OSError, err: # Nonexistence is okay. Anything else, not so much. - if err.errno != errno.EEXIST: + if err.errno != errno.ENOENT: raise # Open new updater.log. -- cgit v1.2.3 From 4ca04bd89c3ed2f876ad5780200cdaba5ef469ac Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 8 Jan 2013 22:24:07 -0500 Subject: MAINT-2155: Match window title of mac-updater.app's message window --- indra/viewer_components/updater/scripts/darwin/update_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra/viewer_components/updater/scripts/darwin') diff --git a/indra/viewer_components/updater/scripts/darwin/update_install.py b/indra/viewer_components/updater/scripts/darwin/update_install.py index 87b931fbeb..3402f90a2a 100755 --- a/indra/viewer_components/updater/scripts/darwin/update_install.py +++ b/indra/viewer_components/updater/scripts/darwin/update_install.py @@ -29,7 +29,7 @@ from janitor import Janitor from messageframe import MessageFrame import Tkinter, tkMessageBox -TITLE = "SecondLife Updater" +TITLE = "Second Life Viewer Updater" # Magic bundle identifier used by all Second Life viewer bundles BUNDLE_IDENTIFIER = "com.secondlife.indra.viewer" -- cgit v1.2.3 From f76e7cce5f68df3a2fa7874dcb6b0b1cd60d4fa3 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 22 Feb 2013 21:06:38 -0500 Subject: MAINT-2328: Mac updater should not clear (part of) viewer cache. Remove the offending code. --- .../updater/scripts/darwin/update_install.py | 15 --------------- 1 file changed, 15 deletions(-) (limited to 'indra/viewer_components/updater/scripts/darwin') diff --git a/indra/viewer_components/updater/scripts/darwin/update_install.py b/indra/viewer_components/updater/scripts/darwin/update_install.py index 3402f90a2a..25efc2b0bc 100755 --- a/indra/viewer_components/updater/scripts/darwin/update_install.py +++ b/indra/viewer_components/updater/scripts/darwin/update_install.py @@ -287,21 +287,6 @@ def main(dmgfile, markerfile, markertext, appdir=None): # let our previously-set sys.excepthook handle this raise type, value, traceback - status("Clearing cache...") - - # We don't know whether the previous viewer was old-style or - # new-style (Cocoa). Clear both kinds of caches. - for cachesubdir in "SecondLife", BUNDLE_IDENTIFIER: - wildcard = "~/Library/Caches/%s/*" % cachesubdir - log("rm " + wildcard) - for f in glob.glob(os.path.expanduser(wildcard)): - # Don't try to remove subdirs this way - if os.path.isfile(f): - try: - os.remove(f) - except Exception, err: - log("%s removing %s: %s" % (err.__class__.__name__, f, err)) - status("Cleaning up...") log("touch " + appdir) -- cgit v1.2.3 From fbb9bf89c045e96da00bfbc7f407758c87894e92 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 25 Feb 2013 16:26:40 -0500 Subject: MAINT-2155, DRTVWR-278: Make Mac updater honor app bundle name in .dmg. If you run "/Applications/Second Life Beta Materials.app", and the version manager directs you to update to a .dmg containing a release candidate whose app bundle name is "Second Life Viewer.app", update_install.py used to copy the contents of "Second Life Viewer.app" into "/Applications/Second Life Beta Materials.app". Changed it so that if the application bundle name differs, we leave the running app bundle alone, installing instead to the app bundle name from the .dmg. --- .../updater/scripts/darwin/update_install.py | 98 +++++++++++++++++----- 1 file changed, 75 insertions(+), 23 deletions(-) (limited to 'indra/viewer_components/updater/scripts/darwin') diff --git a/indra/viewer_components/updater/scripts/darwin/update_install.py b/indra/viewer_components/updater/scripts/darwin/update_install.py index 25efc2b0bc..2fc6fcdb29 100755 --- a/indra/viewer_components/updater/scripts/darwin/update_install.py +++ b/indra/viewer_components/updater/scripts/darwin/update_install.py @@ -139,7 +139,7 @@ def write_marker(markerfile, markertext): # **************************************************************************** # Main script logic # **************************************************************************** -def main(dmgfile, markerfile, markertext, appdir=None): +def main(dmgfile, markerfile, markertext): # Should we fail, we're supposed to write 'markertext' to 'markerfile'. # Wrap the fail() function so we do that. global fail @@ -179,18 +179,19 @@ def main(dmgfile, markerfile, markertext, appdir=None): # prepare for other cleanup with Janitor(LOGF) as janitor: - # Hopefully caller explicitly stated the viewer bundle to update. - # But if not, try to derive it from our own pathname. (The only - # trouble with that is that the old viewer might copy this script - # to a temp dir before running.) - if not appdir: - # Somewhat peculiarly, this script is currently packaged in - # Appname.app/Contents/MacOS with the viewer executable. But even if we - # decide to move it to Appname.app/Contents/Resources, we'll still find - # Appname.app two levels up from dirname(__file__). - appdir = os.path.abspath(os.path.join(os.path.dirname(__file__), - os.pardir, os.pardir)) + # Try to derive the name of the running viewer app bundle from our + # own pathname. (Hopefully the old viewer won't copy this script + # to a temp dir before running!) + # Somewhat peculiarly, this script is currently packaged in + # Appname.app/Contents/MacOS with the viewer executable. But even + # if we decide to move it to Appname.app/Contents/Resources, we'll + # still find Appname.app two levels up from dirname(__file__). + appdir = os.path.abspath(os.path.join(os.path.dirname(__file__), + os.pardir, os.pardir)) if not appdir.endswith(".app"): + # This can happen if either this script has been copied before + # being executed, or if it's in an unexpected place in the app + # bundle. fail(appdir + " is not an application directory") # We need to install into appdir's parent directory -- can we? @@ -260,15 +261,66 @@ def main(dmgfile, markerfile, markertext, appdir=None): # Here 'candidate' is the new viewer to install log("Found " + candidate) + + # This logic was changed to make Mac updates behave more like + # Windows. Most of the time, the user doesn't change the name of + # the app bundle on our .dmg installer (e.g. "Second Life Beta + # Viewer.app"). Most of the time, the version manager directs a + # given viewer to update to another .dmg containing an app bundle + # with THE SAME name. In that case, everything behaves as usual. + + # The case that was changed is when the version manager offers (or + # mandates) an update to a .dmg containing a different app bundle + # name. This can happen, for instance, to a user who's downloaded + # a "project beta" viewer, and the project subsequently publishes + # a Release Candidate viewer. Say the project beta's app bundle + # name is something like "Second Life Beta Neato.app". Anyone + # launching that viewer will be offered an update to the + # corresponding Release Candidate viewer -- which will be built as + # a release viewer, with app bundle name "Second Life Viewer.app". + + # On Windows, we run the NSIS installer, which will update/replace + # the embedded install directory name, e.g. Second Life Viewer. + # But the Mac installer used to locate the app bundle name in the + # mounted .dmg file, then ignore that name, copying its contents + # into the app bundle directory of the running viewer. That is, + # we'd install the Release Candidate from the .dmg's "Second + # Life.app" into "/Applications/Second Life Beta Neato.app". This + # is undesired behavior. + + # Instead, having found the app bundle name on the mounted .dmg, + # we try to install that app bundle name into the parent directory + # of the running app bundle. + + # Are we installing a different app bundle name? If so, call it + # out, both in the log and for the user -- this is an odd case. + # (Presumably they've already agreed to a similar notification in + # the viewer before the viewer launched this script, but still.) + bundlename = os.path.basename(candidate) + if os.path.basename(appdir) == bundlename: + # updating the running app bundle, which we KNOW exists + appexists = True + else: + # installing some other app bundle + newapp = os.path.join(installdir, bundlename) + appexists = os.path.exists(newapp) + message = "Note: %s %s %s" % \ + (appdir, "updating" if appexists else "installing new", newapp) + status(message) + # okay, we have no further need of the name of the running app + # bundle. + appdir = newapp + status("Preparing to copy files...") - # move old viewer to temp location in case copy from .dmg fails - aside = os.path.join(tempdir, os.path.basename(appdir)) - log("mv %r %r" % (appdir, aside)) - # Use shutil.move() instead of os.rename(). move() first tries - # os.rename(), but falls back to shutil.copytree() if the dest is - # on a different filesystem. - shutil.move(appdir, aside) + if appexists: + # move old viewer to temp location in case copy from .dmg fails + aside = os.path.join(tempdir, os.path.basename(appdir)) + log("mv %r %r" % (appdir, aside)) + # Use shutil.move() instead of os.rename(). move() first tries + # os.rename(), but falls back to shutil.copytree() if the dest is + # on a different filesystem. + shutil.move(appdir, aside) status("Copying files...") @@ -282,8 +334,9 @@ def main(dmgfile, markerfile, markertext, appdir=None): except Exception, err: # copy failed -- try to restore previous viewer before crumping type, value, traceback = sys.exc_info() - log("exception response: mv %r %r" % (aside, appdir)) - shutil.move(aside, appdir) + if appexists: + log("exception response: mv %r %r" % (aside, appdir)) + shutil.move(aside, appdir) # let our previously-set sys.excepthook handle this raise type, value, traceback @@ -316,6 +369,5 @@ if __name__ == "__main__": # We expect this script to be invoked with: # - the pathname to the .dmg we intend to install; # - the pathname to an update-error marker file to create on failure; - # - the content to write into the marker file; - # - optionally, the pathname of the Second Life viewer to update. + # - the content to write into the marker file. main(*sys.argv[1:]) -- cgit v1.2.3