summaryrefslogtreecommitdiff
path: root/indra/viewer_components
diff options
context:
space:
mode:
authorGlenn Glazer <coyot@lindenlab.com>2016-07-05 08:06:03 -0700
committerGlenn Glazer <coyot@lindenlab.com>2016-07-05 08:06:03 -0700
commit924e80142fd3ef1454dab1e9a002e5be9e66db94 (patch)
tree00eee26d7eb661654452e4298c30ae2960ea9654 /indra/viewer_components
parent9bc49fb4bd94814482846106954e198e504d802a (diff)
SL-323: apply update code
Diffstat (limited to 'indra/viewer_components')
-rwxr-xr-xindra/viewer_components/manager/apply_update.py232
1 files changed, 232 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..1cf394e008
--- /dev/null
+++ b/indra/viewer_components/manager/apply_update.py
@@ -0,0 +1,232 @@
+#!/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 fnmatch
+import InstallerUserMessage as IUM
+import os
+import os.path
+import plistlib
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+
+LNX_REGEX = '*' + '.bz2'
+MAC_REGEX = '*' + '.dmg'
+MAC_APP_REGEX = '*' + '.app'
+WIN_REGEX = '*' + '.exe'
+
+INSTALL_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+
+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)
+ else:
+ 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)
+ 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 and "True" for Windows
+ #returns None on failure for all three
+ 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:
+ 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)
+ #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):
+ #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
+ 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"]
+ 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 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
+ # swap out old install directory
+ bundlename = os.path.basename(appdir)
+ #INSTALL_DIR is something like /Applications/Second Life Viewer.app/Contents/MacOS, need to jump up two levels
+ installed_test = os.path.dirname(INSTALL_DIR)
+ installed_test = os.path.dirname(installed_test)
+ if os.path.exists(installed_test):
+ silent_write(log_file_handle, "Updating %s" % installed_test)
+ swapped_out = os.path.join(tmpdir, INSTALL_DIR.lstrip('/'))
+ shutil.move(installed_test, swapped_out)
+ else:
+ silent_write(log_file_handle, "Installing %s" % installed_test)
+
+ # copy over the new bits
+ try:
+ shutil.copytree(appdir, installed_test, 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
+ with allow_errno(errno.ENOENT):
+ shutil.rmtree(STATE_DIR)
+
+ os.remove(installable)
+ return INSTALL_DIR
+
+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 True
+
+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('--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)
+
+ 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()