summaryrefslogtreecommitdiff
path: root/indra/viewer_components/manager/apply_update.py
diff options
context:
space:
mode:
authorGlenn Glazer <coyot@lindenlab.com>2016-07-05 13:44:43 -0700
committerGlenn Glazer <coyot@lindenlab.com>2016-07-05 13:44:43 -0700
commitbb19a1e9cc2d11f1db89a92eb17f98641736cd1e (patch)
tree0fe4bdd71321277e1747bfb09083c8c4f5036a49 /indra/viewer_components/manager/apply_update.py
parent7e120a61d1ce670b2bab8ab6b1a814ed4af0dc12 (diff)
SL-323: apply update code, v2
Diffstat (limited to 'indra/viewer_components/manager/apply_update.py')
-rwxr-xr-xindra/viewer_components/manager/apply_update.py254
1 files changed, 254 insertions, 0 deletions
diff --git a/indra/viewer_components/manager/apply_update.py b/indra/viewer_components/manager/apply_update.py
new file mode 100755
index 0000000000..362d57c94e
--- /dev/null
+++ b/indra/viewer_components/manager/apply_update.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2016, Linden Research, Inc.
+#
+# The following source code is PROPRIETARY AND CONFIDENTIAL. Use of
+# this source code is governed by the Linden Lab Source Code Disclosure
+# Agreement ("Agreement") previously entered between you and Linden
+# Lab. By accessing, using, copying, modifying or distributing this
+# software, you acknowledge that you have been informed of your
+# obligations under the Agreement and agree to abide by those obligations.
+#
+# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
+# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
+# COMPLETENESS OR PERFORMANCE.
+# $LicenseInfo:firstyear=2016&license=viewerlgpl$
+# Copyright (c) 2016, Linden Research, Inc.
+# $/LicenseInfo$
+
+"""
+@file apply_update.py
+@author coyot
+@date 2016-06-28
+"""
+
+"""
+Applies an already downloaded update.
+"""
+
+import argparse
+import errno
+import fnmatch
+import InstallerUserMessage as IUM
+import os
+import os.path
+import plistlib
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+#fnmatch expressions
+LNX_REGEX = '*' + '.bz2'
+MAC_REGEX = '*' + '.dmg'
+MAC_APP_REGEX = '*' + '.app'
+WIN_REGEX = '*' + '.exe'
+
+#which install the updater is run from
+INSTALL_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+
+#whether the update is to the INSTALL_DIR or not. Most of the time this is the case.
+IN_PLACE = True
+
+BUNDLE_IDENTIFIER = "com.secondlife.indra.viewer"
+# Magic OS directory name that causes Cocoa viewer to crash on OS X 10.7.5
+# (see MAINT-3331)
+STATE_DIR = os.path.join(os.environ["HOME"], "Library", "Saved Application State",
+ BUNDLE_IDENTIFIER + ".savedState")
+
+def silent_write(log_file_handle, text):
+ #if we have a log file, write. If not, do nothing.
+ if (log_file_handle):
+ #prepend text for easy grepping
+ log_file_handle.write("APPLY UPDATE: " + text + "\n")
+
+def get_filename(download_dir = None):
+ #given a directory that supposedly has the download, find the installable
+ for filename in os.listdir(download_dir):
+ if (fnmatch.fnmatch(filename, LNX_REGEX)
+ or fnmatch.fnmatch(filename, MAC_REGEX)
+ or fnmatch.fnmatch(filename, WIN_REGEX)):
+ return os.path.join(download_dir, filename)
+ #someone gave us a bad directory
+ return None
+
+def try_dismount(log_file_handle = None, installable = None, tmpdir = None):
+ #best effort cleanup try to dismount the dmg file if we have mounted one
+ #the French judge gave it a 5.8
+ try:
+ command = ["df", os.path.join(tmpdir, "Second Life Installer")]
+ output = subprocess.check_output(command)
+ #first word of second line of df output is the device name
+ mnt_dev = output.split('\n')[1].split()[0]
+ command = ["hdiutil", "detach", "-force", mnt_dev]
+ output = subprocess.check_output(command)
+ silent_write(log_file_handle, "hdiutil detach succeeded")
+ silent_write(log_file_handle, output)
+ except Exception, e:
+ silent_write(log_file_handle, "Could not detach dmg file %s. Error messages: %s" % (installable, e.message))
+
+def apply_update(download_dir = None, platform_key = None, log_file_handle = None):
+ #for lnx and mac, returns path to newly installed viewer
+ #for win, return the name of the executable
+ #returns None on failure for all three
+ #throws an exception if it can't find an installable at all
+
+ installable = get_filename(download_dir)
+ if not installable:
+ #could not find download
+ raise ValueError("Could not find installable in " + download_dir)
+
+ if platform_key == 'lnx':
+ installed = apply_linux_update(installable, log_file_handle)
+ elif platform_key == 'mac':
+ installed = apply_mac_update(installable, log_file_handle)
+ elif platform_key == 'win':
+ installed = apply_windows_update(installable, log_file_handle)
+ else:
+ #wtf?
+ raise ValueError("Unknown Platform: " + platform_key)
+
+ if not installed:
+ #only mark the download as done when everything is done
+ done_filename = os.path.join(os.path.dirname(installable), ".done")
+ open(done_filename, 'w+').close()
+
+ return installed
+
+def apply_linux_update(installable = None, log_file_handle = None):
+ try:
+ #untar to tmpdir
+ tmpdir = tempfile.mkdtemp()
+ tar = tarfile.open(name = installable, mode="r:bz2")
+ tar.extractall(path = tmpdir)
+ if IN_PLACE:
+ #rename current install dir
+ shutil.move(INSTALL_DIR,install_dir + ".bak")
+ #mv new to current
+ shutil.move(tmpdir, INSTALL_DIR)
+ #delete tarball on success
+ os.remove(installable)
+ except Exception, e:
+ silent_write(log_file_handle, "Update failed due to " + repr(e))
+ return None
+ return INSTALL_DIR
+
+def apply_mac_update(installable = None, log_file_handle = None):
+ #INSTALL_DIR is something like /Applications/Second Life Viewer.app/Contents/MacOS, need to jump up two levels for the install base
+ install_base = os.path.dirname(INSTALL_DIR)
+ install_base = os.path.dirname(install_base)
+
+ #verify dmg file
+ try:
+ output = subprocess.check_output(["hdiutil", "verify", installable], stderr=subprocess.STDOUT)
+ silent_write(log_file_handle, "dmg verification succeeded")
+ silent_write(log_file_handle, output)
+ except Exception, e:
+ silent_write(log_file_handle, "Could not verify dmg file %s. Error messages: %s" % (installable, e.message))
+ return None
+ #make temp dir and mount & attach dmg
+ tmpdir = tempfile.mkdtemp()
+ try:
+ output = subprocess.check_output(["hdiutil", "attach", installable, "-mountroot", tmpdir])
+ silent_write(log_file_handle, "hdiutil attach succeeded")
+ silent_write(log_file_handle, output)
+ except Exception, e:
+ silent_write(log_file_handle, "Could not attach dmg file %s. Error messages: %s" % (installable, e.message))
+ return None
+ #verify plist
+ mounted_appdir = None
+ for top_dir in os.listdir(tmpdir):
+ for appdir in os.listdir(os.path.join(tmpdir, top_dir)):
+ appdir = os.path.join(os.path.join(tmpdir, top_dir), appdir)
+ if fnmatch.fnmatch(appdir, MAC_APP_REGEX):
+ try:
+ plist = os.path.join(appdir, "Contents", "Info.plist")
+ CFBundleIdentifier = plistlib.readPlist(plist)["CFBundleIdentifier"]
+ mounted_appdir = appdir
+ except:
+ #there is no except for this try because there are multiple directories that legimately don't have what we are looking for
+ pass
+ if not mounted_appdir:
+ silent_write(log_file_handle, "Could not find app bundle in dmg %s." % (installable,))
+ return None
+ if CFBundleIdentifier != BUNDLE_IDENTIFIER:
+ silent_write(log_file_handle, "Wrong or null bundle identifier for dmg %s. Bundle identifier: %s" % (installable, CFBundleIdentifier))
+ try_dismount(log_file_handle, installable, tmpdir)
+ return None
+ #do the install, finally
+ if IN_PLACE:
+ # swap out old install directory
+ bundlename = os.path.basename(mounted_appdir)
+ silent_write(log_file_handle, "Updating %s" % bundlename)
+ swapped_out = os.path.join(tmpdir, INSTALL_DIR.lstrip('/'))
+ shutil.move(install_base, swapped_out)
+ else:
+ silent_write(log_file_handle, "Installing %s" % install_base)
+
+ # copy over the new bits
+ try:
+ shutil.copytree(mounted_appdir, install_base, symlinks=True)
+ retcode = 0
+ except Exception, e:
+ # try to restore previous viewer
+ if os.path.exists(swapped_out):
+ silent_write(log_file_handle, "Install of %s failed, rolling back to previous viewer." % installable)
+ shutil.move(swapped_out, installed_test)
+ retcode = 1
+ finally:
+ try_dismount(log_file_handle, installable, tmpdir)
+ if retcode:
+ return None
+
+ #see MAINT-3331
+ try:
+ shutil.rmtree(STATE_DIR)
+ except Exception, e:
+ #if we fail to delete something that isn't there, that's okay
+ if e[0] == errno.ENOENT:
+ pass
+ else:
+ raise e
+
+ os.remove(installable)
+ return install_base
+
+def apply_windows_update(installable = None, log_file_handle = None):
+ #the windows install is just running the NSIS installer executable
+ #from VMP's perspective, it is a black box
+ try:
+ output = subprocess.check_output(installable, stderr=subprocess.STDOUT)
+ silent_write(log_file_handle, "Install of %s succeeded." % installable)
+ silent_write(log_file_handle, output)
+ except subprocess.CalledProcessError, cpe:
+ silent_write(log_file_handle, "%s failed with return code %s. Error messages: %s." %
+ (cpe.cmd, cpe.returncode, cpe.message))
+ return None
+ return installable
+
+def main():
+ parser = argparse.ArgumentParser("Apply Downloaded Update")
+ parser.add_argument('--dir', dest = 'download_dir', help = 'directory to find installable', required = True)
+ parser.add_argument('--pkey', dest = 'platform_key', help =' OS: lnx|mac|win', required = True)
+ parser.add_argument('--in_place', action = 'store_false', help = 'This upgrade is for a different channel', default = True)
+ parser.add_argument('--log_file', dest = 'log_file', default = None, help = 'file to write messages to')
+ args = parser.parse_args()
+
+ if args.log_file:
+ try:
+ f = open(args.log_file,'w+')
+ except:
+ print "%s could not be found or opened" % args.log_file
+ sys.exit(1)
+
+ IN_PLACE = args.in_place
+ result = apply_update(download_dir = args.download_dir, platform_key = args.platform_key, log_file_handle = f)
+ if not result:
+ sys.exit("Update failed")
+ else:
+ sys.exit(0)
+
+
+if __name__ == "__main__":
+ main()