diff options
| author | Glenn Glazer <coyot@lindenlab.com> | 2016-07-11 11:24:45 -0700 | 
|---|---|---|
| committer | Glenn Glazer <coyot@lindenlab.com> | 2016-07-11 11:24:45 -0700 | 
| commit | 03bcad61115b7128b3b1a5ede3cd7f2bf309fd39 (patch) | |
| tree | 74bf526f169b14cedfa08b5276db766ea60ddd02 | |
| parent | bb19a1e9cc2d11f1db89a92eb17f98641736cd1e (diff) | |
SLS-323: integrate update manager with lanucher, various fixes, CMake changes
| -rw-r--r-- | indra/newview/CMakeLists.txt | 33 | ||||
| -rw-r--r-- | indra/viewer_components/manager/InstallerUserMessage.py | 14 | ||||
| -rwxr-xr-x | indra/viewer_components/manager/SL_Launcher | 59 | ||||
| -rwxr-xr-x | indra/viewer_components/manager/apply_update.py | 29 | ||||
| -rwxr-xr-x | indra/viewer_components/manager/download_update.py | 16 | ||||
| -rwxr-xr-x | indra/viewer_components/manager/update_manager.py | 455 | 
6 files changed, 582 insertions, 24 deletions
| diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 767a280beb..9ee7b656c0 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -1774,9 +1774,42 @@ if (WINDOWS)         --distpath ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}               ${CMAKE_SOURCE_DIR}/viewer_components/manager/SL_Launcher      COMMENT "Performing pyinstaller compile of SL_Launcher" +     +    add_custom_command( +    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/apply_update.exe +    COMMAND ${PYTHON_DIRECTORY}/Scripts/pyinstaller.exe +    ARGS +       --onefile  +       --log-level WARN +       --distpath ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}  +            ${CMAKE_SOURCE_DIR}/viewer_components/manager/apply_update.py +    COMMENT "Performing pyinstaller compile of updater" +     +    add_custom_command( +    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/download_update.exe +    COMMAND ${PYTHON_DIRECTORY}/Scripts/pyinstaller.exe +    ARGS +       --onefile  +       --log-level WARN +       --distpath ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}  +            ${CMAKE_SOURCE_DIR}/viewer_components/manager/download_update.py +    COMMENT "Performing pyinstaller compile of update downloader" +     +    add_custom_command( +    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/update_manager.exe +    COMMAND ${PYTHON_DIRECTORY}/Scripts/pyinstaller.exe +    ARGS +       --onefile  +       --log-level WARN +       --distpath ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}  +            ${CMAKE_SOURCE_DIR}/viewer_components/manager/update_manager.py +    COMMENT "Performing pyinstaller compile of update manager"  )  add_custom_target(compile_w_viewer_launcher ALL DEPENDS ${CMAKE_CFG_INTDIR}/SL_Launcher.exe) +add_custom_target(compile_w_viewer_launcher ALL DEPENDS ${CMAKE_CFG_INTDIR}/apply_update.exe) +add_custom_target(compile_w_viewer_launcher ALL DEPENDS ${CMAKE_CFG_INTDIR}/download_update.exe) +add_custom_target(compile_w_viewer_launcher ALL DEPENDS ${CMAKE_CFG_INTDIR}/update_manager.exe)      add_custom_command(        OUTPUT  ${CMAKE_CFG_INTDIR}/copy_touched.bat diff --git a/indra/viewer_components/manager/InstallerUserMessage.py b/indra/viewer_components/manager/InstallerUserMessage.py index 2ec71df030..f66af81d06 100644 --- a/indra/viewer_components/manager/InstallerUserMessage.py +++ b/indra/viewer_components/manager/InstallerUserMessage.py @@ -280,13 +280,6 @@ if __name__ == "__main__":      print frame3.choice.get()      sys.stdout.flush() -    #trinary choice test.  User destroys window when they select. -    frame3a = InstallerUserMessage(text = "Something in the way she knows....", title = "Beatles Quotes for 200", icon_name="head-sl-logo.gif") -    frame3a.trinary_choice_message(message = "And all I have to do is think of her.",  -        one = "Don't want to leave her now", two = 'You know I believe and how', three = 'John is Dead') -    print frame3a.choice.get() -    sys.stdout.flush() -      #progress bar      queue = Queue.Queue()      thread = ThreadedClient(queue) @@ -297,3 +290,10 @@ if __name__ == "__main__":      frame4.progress_bar(message = "You're asking me will my love grow", size = 100, pb_queue = queue)      print "frame defined"      frame4.mainloop() + +    #trinary choice test.  User destroys window when they select. +    frame3a = InstallerUserMessage(text = "Something in the way she knows....", title = "Beatles Quotes for 200", icon_name="head-sl-logo.gif") +    frame3a.trinary_choice_message(message = "And all I have to do is think of her.",  +        one = "Don't want to leave her now", two = 'You know I believe and how', three = 'John is Dead') +    print frame3a.choice.get() +    sys.stdout.flush() diff --git a/indra/viewer_components/manager/SL_Launcher b/indra/viewer_components/manager/SL_Launcher index 6eaccc8b13..ecf88a1105 100755 --- a/indra/viewer_components/manager/SL_Launcher +++ b/indra/viewer_components/manager/SL_Launcher @@ -18,10 +18,19 @@  # Copyright (c) 2013, Linden Research, Inc.  import argparse +import InstallerUserMessage  import os  import sys  import subprocess -import InstallerUserMessage +import update_manager + +def after_frame(my_message, timeout = 10000): +   #pop up a InstallerUserMessage.basic_message that kills itself after timeout milliseconds +   #note that this blocks the caller for the duration of timeout +   frame = InstallerUserMessage(title = "Second Life Installer", icon_name="head-sl-logo.gif") +   #this is done before basic_message so that we aren't blocked by mainloop() +   frame.after(timout, lambda: frame._delete_window) +   frame.basic_message(message = my_message)  cwd = os.path.dirname(os.path.realpath(__file__)) @@ -40,21 +49,47 @@ elif sys.platform.startswith("linux"):  else:     #SL doesn't run on VMS or punch cards     sys.exit("Unsupported platform") +    +#check for an update +#TODO -#print "COYOT: executable name ", executable_name -#print "COYOT: path ", os.path.dirname(os.path.abspath(sys.argv[0])) - +#find the viewer to be lauched  viewer_binary = os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])),executable_name)   parser = argparse.ArgumentParser() -#parser.add_argument('--f', action='store_const', const=42)  args = parser.parse_known_args(sys.argv)  args_list_to_pass = args[1][1:] -args_list_to_pass.insert(0,viewer_binary) -print "COYOT: arrrrrghs to pass", args_list_to_pass - -#to prove we are launching from the script, launch a Tkinter window first -frame2 = InstallerUserMessage(title = "Second Life") -frame2.basic_message(message = viewer_binary, icon_name="head-sl-logo.gif") +#make a copy by value, not by reference +command = list(args_list_to_pass) -#viewer_process = subprocess.Popen(args_list_to_pass) +(success, state, condition) = update_manager.update_manager() +# From update_manager: +#  (False, 'setup', None): error occurred before we knew what the update was (e.g., in setup or parsing) +#  (False, 'download', version): we failed to download the new version +#  (False, 'apply', version): we failed to apply the new version +#  (True, None, None): No update found +#  (True, 'in place', True): update applied in place +#  (True, 'in place', path_to_new_launcher): Update applied by a new install to a new location +#  (True, 'background', True): background download initiated +#These boil down three cases: +#  Success is False, then pop up a message and launch the current viewer +#  No update, update succeeded in place in foreground, or background update started: silently launch the current viewer channel +#  Updated succeed to a different channel, launch that viewer and exit +if not success: +   msg = 'Update failed in the %s process.  Please check logs.  Viewer will launch starting momentarily.' +   after_frame(msg) +   command.insert(0,viewer_binary) +   viewer_process = subprocess.Popen(command) +   #at the moment, we just exit here.  Later, the crash monitor will be launched at this point +elif (success == True and  +      (state == None  +       or (state ==  'background' and condition == True) +       or (state == 'in_place' and condition == True))): +   command.insert(0,viewer_binary) +   viewer_process = subprocess.Popen(command) +   #at the moment, we just exit here.  Later, the crash monitor will be launched at this point +else: +   #'condition' is the path to the new launcher. +   command.insert(0,condition) +   viewer_process = subprocess.Popen(command) +   sys.exit(0) diff --git a/indra/viewer_components/manager/apply_update.py b/indra/viewer_components/manager/apply_update.py index 362d57c94e..643e4ad2bc 100755 --- a/indra/viewer_components/manager/apply_update.py +++ b/indra/viewer_components/manager/apply_update.py @@ -33,12 +33,15 @@ import InstallerUserMessage as IUM  import os  import os.path  import plistlib +import re  import shutil  import subprocess  import sys  import tarfile  import tempfile +#Module level variables +  #fnmatch expressions  LNX_REGEX = '*' + '.bz2'  MAC_REGEX = '*' + '.dmg' @@ -65,6 +68,9 @@ def silent_write(log_file_handle, text):  def get_filename(download_dir = None):      #given a directory that supposedly has the download, find the installable +    #if you are on platform X and you give the updater a directory with an installable   +    #for platform Y, you are either trying something fancy or get what you deserve +    #or both      for filename in os.listdir(download_dir):          if (fnmatch.fnmatch(filename, LNX_REGEX)             or fnmatch.fnmatch(filename, MAC_REGEX)  @@ -77,10 +83,14 @@ 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: +        #use the df command to find the device name +        #Filesystem   512-blocks   Used Available Capacity iused  ifree %iused  Mounted on +        #/dev/disk1s2    2047936 643280   1404656    32%   80408 175582   31%   /private/tmp/mnt/Second Life Installer          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] +        #do the dismount          command = ["hdiutil", "detach", "-force", mnt_dev]          output = subprocess.check_output(command)          silent_write(log_file_handle, "hdiutil detach succeeded") @@ -88,17 +98,20 @@ def try_dismount(log_file_handle = None, installable = None, tmpdir = None):      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): +def apply_update(download_dir = None, platform_key = None, log_file_handle = None, in_place = True):      #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 +    IN_PLACE = in_place +          installable = get_filename(download_dir)      if not installable: -        #could not find download +        #could not find the download          raise ValueError("Could not find installable in " + download_dir) +    #apply update using the platform specific tools      if platform_key == 'lnx':          installed = apply_linux_update(installable, log_file_handle)      elif platform_key == 'mac': @@ -225,7 +238,17 @@ def apply_windows_update(installable = None, log_file_handle = None):          silent_write(log_file_handle, "%s failed with return code %s. Error messages: %s." %                        (cpe.cmd, cpe.returncode, cpe.message))          return None -    return installable +    #Due to the black box nature of the install, we have to derive the application path from the +    #name of the installable.  This is essentially reverse-engineering app_name()/app_name_oneword() +    #in viewer_manifest.py +    #the format of the filename is:  Second_Life_{Project Name}_A-B-C-XXXXXX_i686_Setup.exe +    #which deploys to C:\Program Files (x86)\SecondLifeProjectName\ +    #so we want all but the last four phrases and tack on Viewer if there is no project +    if re.search('Project', installable): +        winstall = os.path.join("C:\\Program Files (x86)\\", "".join(installable.split("_")[:-3])) +    else: +        winstall = os.path.join("C:\\Program Files (x86)\\", "".join(installable.split("_")[:-3])+"Viewer") +    return winstall  def main():      parser = argparse.ArgumentParser("Apply Downloaded Update") diff --git a/indra/viewer_components/manager/download_update.py b/indra/viewer_components/manager/download_update.py index cd4e6680b0..23f784c6c1 100755 --- a/indra/viewer_components/manager/download_update.py +++ b/indra/viewer_components/manager/download_update.py @@ -42,9 +42,10 @@ def download_update(url = None, download_dir = None, size = None, progressbar =      #download_dir to download to      #total size (for progressbar) of download      #progressbar: whether to display one (not used for background downloads) -    #chunk_size is in bytes +    #chunk_size is in bytes, amount to download at once      queue = Queue.Queue() +    #the url split provides the basename of the filename      filename = os.path.join(download_dir, url.split('/')[-1])      req = requests.get(url, stream=True)      down_thread = ThreadedDownload(req, filename, chunk_size, progressbar, queue) @@ -60,6 +61,11 @@ def download_update(url = None, download_dir = None, size = None, progressbar =  class ThreadedDownload(threading.Thread):      def __init__(self, req, filename, chunk_size, progressbar, in_queue): +        #req is a python request object +        #target filename to download to +        #chunk_size is in bytes, amount to download at once +        #progressbar: whether to display one (not used for background downloads) +        #in_queue mediates communication between this thread and the progressbar          threading.Thread.__init__(self)          self.req = req          self.filename = filename @@ -69,13 +75,19 @@ class ThreadedDownload(threading.Thread):      def run(self):          with open(self.filename, 'wb') as fd: +            #keep downloading until we run out of chunks, then download the last bit              for chunk in self.req.iter_content(self.chunk_size):                  fd.write(chunk)                  if self.progressbar: -                    self.in_queue.put(len(chunk))                        +                    #this will increment the progress bar by len(chunk)/size units +                    self.in_queue.put(len(chunk))   +            #signal value saying to the progress bar that it is done and can destroy itself +            #if len(chunk) is ever -1, we get to file a bug against Python              self.in_queue.put(-1)  def main(): +    #main method is for standalone use such as support and QA +    #VMP will import this module and run download_update directly      parser = argparse.ArgumentParser("Download URI to directory")      parser.add_argument('--url', dest='url', help='URL of file to be downloaded', required=True)      parser.add_argument('--dir', dest='download_dir', help='directory to be downloaded to', required=True) diff --git a/indra/viewer_components/manager/update_manager.py b/indra/viewer_components/manager/update_manager.py new file mode 100755 index 0000000000..ec6df17a6c --- /dev/null +++ b/indra/viewer_components/manager/update_manager.py @@ -0,0 +1,455 @@ +#!/usr/bin/env python + +# $LicenseInfo:firstyear=2016&license=internal$ +#  +# 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$ +# Copyright (c) 2013, Linden Research, Inc. + +""" +@file   update_manager.py +@author coyot +@date   2016-05-16 +""" + +from llbase import llrest +from llbase import llsd +from urlparse import urljoin + +import apply_update +import download_update +import errno +import fnmatch +import hashlib +import InstallerUserMessage +import json +import os +import platform +import re +import shutil +import subprocess +import sys +import tempfile +import thread +import urllib + +def silent_write(log_file_handle, text): +    #if we have a log file, write.  If not, do nothing. +    #this is so we don't have to keep trapping for an exception with a None handle +    #oh and because it is best effort, it is also a holey_write ;) +    if (log_file_handle): +        #prepend text for easy grepping +        log_file_handle.write("UPDATE MANAGER: " + text + "\n") + +def after_frame(my_message, timeout = 10000): +    #pop up a InstallerUserMessage.basic_message that kills itself after timeout milliseconds +    #note that this blocks the caller for the duration of timeout +    frame = InstallerUserMessage(title = "Second Life Installer", icon_name="head-sl-logo.gif") +    #this is done before basic_message so that we aren't blocked by mainloop() +    frame.after(timout, lambda: frame._delete_window) +    frame.basic_message(message = my_message) + +def convert_version_file_style(version): +    #converts a version string a.b.c.d to a_b_c_d as used in downloaded filenames +    #re will throw a TypeError if it gets None, just return that. +    try: +        pattern = re.compile('\.') +        return pattern.sub('_', version) +    except TypeError, te: +        return None + +def get_platform_key(): +    #this is the name that is inserted into the VVM URI +    #and carried forward through the rest of the updater to determine +    #platform specific actions as appropriate +    platform_dict = {'Darwin':'mac', 'Linux':'lnx', 'Windows':'win'} +    platform_uname = platform.system() +    try: +        return platform_dict[platform_uname] +    except KeyError: +        return None + +def get_summary(platform_name, launcher_path): +    #get the contents of the summary.json file. +    #for linux and windows, this file is in the same directory as the script +    #for mac, the script is in ../Contents/MacOS/ and the file is in ../Contents/Resources/ +    script_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) +    if (platform_name == 'mac'): +        summary_dir = os.path.abspath(os.path.join(script_dir, "../Resources")) +    else: +        summary_dir = script_dir +    summary_file = os.path.join(summary_dir,"summary.json") +    with open(summary_file) as summary_handle: +        return json.load(summary_handle) + +def get_parent_path(platform_name): +    #find the parent of the logs and user_settings directories +    if (platform_name == 'mac'): +        settings_dir = os.path.join(os.path.expanduser('~'),'Library','Application Support','SecondLife') +    elif (platform_name == 'lnx'):  +        settings_dir = os.path.join(os.path.expanduser('~'),'.secondlife') +    #using list format of join is important here because the Windows pathsep in a string escapes the next char +    elif (platform_name == 'win'): +        settings_dir = os.path.join(os.path.expanduser('~'),'AppData','Roaming','SecondLife') +    else: +        settings_dir = None +    return settings_dir + +def make_download_dir(parent_dir, new_version): +    #make a canonical download dir if it does not already exist +    #format: ../user_settings/downloads/1.2.3.456789 +    #we do this so that multiple viewers on the same host can update separately +    #this also functions as a getter +    try: +        download_dir = os.path.join(parent_dir, "downloads", new_version) +        os.makedirs(download_dir) +    except OSError, hell: +        #Directory already exists, that's okay.  Other OSErrors are not okay. +        if hell[0] == errno.EEXIST:   +            pass +        else: +            raise hell +    return download_dir + +def check_for_completed_download(download_dir): +    #there will be two files on completion, the download and a marker file called "".done"" +    #for optional upgrades, there may also be a .skip file to skip this particular upgrade  +    #or .next to install on next run +    completed = None +    marker_regex = '*' + '.done' +    skip_regex = '*' + '.skip' +    next_regex = '*' + '.next' +    for filename in os.listdir(download_dir): +        if fnmatch.fnmatch(filename, marker_regex): +            completed = 'done' +        elif fnmatch.fnmatch(filename, skip_regex): +            completed = 'skip' +        elif fnmatch.fnmatch(filename, next_regex): +            #so we don't skip infinitely +            os.remove(filename) +            completed = 'next' +    if not completed: +        #cleanup +        shutil.rmtree(download_dir) +    return completed   + +def get_settings(log_file_handle, parent_dir): +    #return the settings file parsed into a dict +    try: +        settings_file = os.path.abspath(os.path.join(parent_dir,'user_settings','settings.xml')) +        settings = llsd.parse((open(settings_file)).read()) +    except llsd.LLSDParseError as lpe: +        silent_write(log_file_handle, "Could not parse settings file %s" % lpe) +        return None +    return settings + +def get_log_file_handle(parent_dir): +    #return a write handle on the log file +    #plus log rotation and not dying on failure +    log_file = os.path.join(parent_dir, 'update_manager.log') +    old_file = log_file + '.old' +    #if someone's log files are present but not writable, they've screwed up their install. +    if os.access(log_file, os.W_OK): +        if os.access(old_file, os.W_OK): +            os.unlink(old_file) +        os.rename(log_file, old_file) +    elif not os.path.exists(log_file): +        #reimplement TOUCH(1) in Python +        #perms default to 644 which is fine +        open(log_file, 'w+').close() +    try: +        f = open(log_file,'w+') +    except Exception as e: +        #we don't have a log file to write to, make a best effort and sally onward +        print "Could not open update manager log file %s" % log_file +        f = None +    return f + +def make_VVM_UUID_hash(platform_key): +    #NOTE: There is no python library support for a persistent machine specific UUID +    #      AND all three platforms do this a different way, so exec'ing out is really the best we can do +    #Lastly, this is a best effort service.  If we fail, we should still carry on with the update  +    uuid = None +    if (platform_key == 'lnx'): +        uuid = subprocess.check_output(['/usr/bin/hostid']).rstrip() +    elif (platform_key == 'mac'): +        #this is absurdly baroque +        #/usr/sbin/system_profiler SPHardwareDataType | fgrep 'Serial' | awk '{print $NF}' +        uuid = subprocess.check_output(["/usr/sbin/system_profiler", "SPHardwareDataType"]) +        #findall[0] does the grep for the value we are looking for: "Serial Number (system): XXXXXXXX" +        #split(:)[1] gets us the XXXXXXX part +        #lstrip shaves off the leading space that was after the colon +        uuid = re.split(":", re.findall('Serial Number \(system\): \S*', uuid)[0])[1].lstrip() +    elif (platform_key == 'win'): +        # wmic csproduct get UUID | grep -v UUID +        uuid = subprocess.check_output(['wmic','csproduct','get','UUID']) +        #outputs in two rows: +        #UUID +        #XXXXXXX-XXXX... +        uuid = re.split('\n',uuid)[1].rstrip() +    if uuid is not None: +        return hashlib.md5(uuid).hexdigest() +    else: +        #fake it +        return hashlib.md5(str(uuid.uuid1())).hexdigest() + +def query_vvm(log_file_handle, platform_key, settings, summary_dict): +    result_data = None +    #URI template /update/v1.1/channelname/version/platformkey/platformversion/willing-to-test/uniqueid +    #https://wiki.lindenlab.com/wiki/Viewer_Version_Manager_REST_API#Viewer_Update_Query +    base_URI = 'https://update.secondlife.com/update/' +    channelname = summary_dict['Channel'] +    #this is kind of a mess because the settings value a) in a map and b) is both the cohort and the version +    version = summary_dict['Version'] +    platform_version = platform.release() +    #this will always return something usable, error handling in method +    hashed_UUID = make_VVM_UUID_hash(platform_key) +    #note that this will not normally be in a settings.xml file and is only here for test builds. +    #for test builds, add this key to the ../user_settings/settings.xml +    """ +        <key>test</key> +        <map> +        <key>Comment</key> +            <string>Tell update manager you aren't willing to test.</string> +        <key>Type</key> +            <string>String</string> +        <key>Value</key> +            <integer>testno</integer> +        </map> +    </map> +    """ +    try: +        test_ok = settings['test']['Value'] +    except KeyError as ke: +        #normal case, no testing key +        test_ok = 'testok' +    UUID = make_VVM_UUID_hash(platform_key) +    #because urljoin can't be arsed to take multiple elements +    query_string =  '/v1.0/' + channelname + '/' + version + '/' + platform_key + '/' + platform_version + '/' + test_ok + '/' + UUID +    VVMService = llrest.SimpleRESTService(name='VVM', baseurl=base_URI) +    try: +        result_data = VVMService.get(query_string) +    except RESTError as re: +        silent_write.write(log_file_handle, "Failed to query VVM using %s failed as %s" % (urljoin(base_URI,query_string, re))) +        return None +    return result_data + +def download(url = None, version = None, download_dir = None, size = 0, background = False): +    download_tries = 0 +    download_success = False +    #for background execution +    path_to_downloader = os.path.join(os.path.dirname(os.path.realpath(__file__)), "download_update.py") +    #three strikes and you're out +    while download_tries < 3 and not download_success: +        #323: Check for a partial update of the required update; in either event, display an alert that a download is required, initiate the download, and then install and launch +        if download_tries == 0: +            after_frame(message = "Downloading new version " + version + " Please wait.") +        else: +            after_frame(message = "Trying again to download new version " + version + " Please wait.") +        if not background: +            try: +                download_update.download_update(url = url, download_dir = download_dir, size = size, progressbar = True) +                download_success = True +            except: +                download_tries += 1     +                silent_write(log_file_handle, "Failed to download new version " + version + ". Trying again.") +        else: +            try: +                #Python does not have a facility to multithread a method, so we make the method a standalone +                #and subprocess that +                subprocess.call(path_to_downloader, "--url = %s --dir = %s --pb --size= %s" % (url, download_dir, size)) +                download_success = True +            except: +                download_tries += 1 +                silent_write(log_file_handle, "Failed to download new version " + version + ". Trying again.") +    if not download_success: +        silent_write(log_file_handle, "Failed to download new version " + version) +        after_frame(message = "Failed to download new version " + version + " Please check connectivity.") +        return False +    return True + +def install(platform_key = None, download_dir = None, log_file_handle = None, in_place = None, downloaded = None): +    #user said no to this one +    if downloaded != 'skip': +        after_frame(message = "New version downloaded.  Installing now, please wait.") +        success = apply_update.apply_update(download_dir, platform_key, log_file_handle, in_place) +        if success: +            silent_write(log_file_handle, "successfully updated to " + version) +            shutil.rmtree(download_dir) +            #this is either True for in place or the path to the new install for not in place +            return success +        else: +            after_frame(message = "Failed to apply " + version) +            silent_write(log_file_handle, "Failed to update viewer to " + version) +            return False +         +def download_and_install(downloaded = None, url = None, version = None, download_dir = None, size = None, platform_key = None, log_file_handle = None, in_place = None): +    #extracted to a method because we do it twice in update_manager() and this makes the logic clearer +    if not downloaded: +        #do the download, exit if we fail +        if not download(url = url, version = version, download_dir = download_dir, size = size):  +            return (False, 'download', version)   +    #do the install +    path_to_new_launcher = install(platform_key = platform_key, download_dir = download_dir,  +                                   log_file_handle = log_file_handle, in_place = in_place, downloaded = downloaded) +    if path_to_new_launcher: +        #if we succeed, propagate the success type upwards +        if in_place: +            return (True, 'in place', True) +        else: +            return (True, 'in place', path_to_new_launcher) +    else: +        #propagate failure +        return (False, 'apply', version)     +             +def update_manager(): +    #comments that begin with '323:' are steps taken from the algorithm in the description of SL-323.  +    #  Note that in the interest of efficiency, such as determining download success once at the top +    #  The code does follow precisely the same order as the algorithm. +    #return values rather than exit codes.  All of them are to communicate with launcher +    #we print just before we return so that __main__ outputs something - returns are swallowed +    #  (False, 'setup', None): error occurred before we knew what the update was (e.g., in setup or parsing) +    #  (False, 'download', version): we failed to download the new version +    #  (False, 'apply', version): we failed to apply the new version +    #  (True, None, None): No update found +    #  (True, 'in place, True): update applied in place +    #  (True, 'in place', path_to_new_launcher): Update applied by a new install to a new location +    #  (True, 'background', True): background download initiated + +    #setup and getting initial parameters +    platform_key = get_platform_key() +    parent_dir = get_parent_path(platform_key) +    log_file_handle = get_log_file_handle(parent_dir) + +    #check to see if user has install rights +    #get the owner of the install and the current user +    script_owner_id = os.stat(os.path.realpath(__file__)).st_uid +    user_id = os.geteuid() +    #if we are on lnx or mac, we can pretty print the IDs as names using the pwd module +    #win does not provide this support and Python will throw an ImportError there, so just use raw IDs +    if script_owner_id != user_id: +        if platform_key != 'win': +            import pwd +            script_owner_name = pwd.getpwuid(script_owner_id)[0] +            username = pwd.getpwuid(user_id)[0] +        else: +            username = user_id +            script_owner_name = script_owner_id +        silent_write(log_file_handle, "Upgrade notification attempted by userid " + username)     +        frame = InstallerUserMessage(title = "Second Life Installer", icon_name="head-sl-logo.gif") +        frame.binary_choice_message(message = "Second Life was installed by userid " + script_owner_name  +            + ".  Do you have privileges to install?", true = "Yes", false = 'No') +        if not frame.choice.get(): +            silent_write(log_file_handle, "Upgrade attempt declined by userid " + username) +            after_frame(message = "Please find a system admin to upgrade Second Life") +            print "Update manager exited with (%s, %s, %s)" % (False, 'setup', None) +            return (False, 'setup', None) + +    settings = get_settings(log_file_handle, parent_dir) +    if settings is None: +        silent_write(log_file_handle, "Failed to load viewer settings") +        print "Update manager exited with (%s, %s, %s)" % (False, 'setup', None) +        return (False, 'setup', None) + +    #323: If a complete download of that update is found, check the update preference: +    #settings['UpdaterServiceSetting'] = 0 is manual install +    """ +    <key>UpdaterServiceSetting</key> +        <map> +        <key>Comment</key> +            <string>Configure updater service.</string> +        <key>Type</key> +            <string>U32</string> +        <key>Value</key> +            <string>0</string> +        </map> +    """ +    try: +        install_automatically = settings['UpdaterServiceSetting']['Value'] +    #because, for some godforsaken reason, we delete the setting rather than changing the value +    except KeyError: +        install_automatically = 1 + +    #get channel and version +    try: +        summary_dict = get_summary(platform_key, os.path.abspath(os.path.realpath(__file__))) +    except: +        silent_write(log_file_handle, "Could not obtain channel and version, exiting.") +        print "Update manager exited with (%s, %s, %s)" % (False, 'setup', None) +        return (False, 'setup', None)         + +    #323: On launch, the Viewer Manager should query the Viewer Version Manager update api. +    result_data = query_vvm(log_file_handle, platform_key, settings, summary_dict) +    #nothing to do or error +    if not result_data: +        silent_write.write(og_file_handle, "No update found.") +        print "Update manager exited with (%s, %s, %s)" % (True, None, None) +        return (True, None, None) + +    #get download directory, if there are perm issues or similar problems, give up +    try: +        download_dir = make_download_dir(parent_dir, result_data['version']) +    except Exception, e: +        print "Update manager exited with (%s, %s, %s)" % (False, 'setup', None) +        return (False, 'setup', None) +     +    #if the channel name of the response is the same as the channel we are launched from, the update is "in place" +    #and launcher will launch the viewer in this install location.  Otherwise, it will launch the Launcher from  +    #the new location and kill itself. +    in_place = (summary_dict['Channel'] == result_data['channel']) +     +    #determine if we've tried this download before +    downloaded = check_for_completed_download(download_dir) + +    #323: If the response indicates that there is a required update:  +    if result_data['required'] or (not result_data['required'] and install_automatically): +        #323: Check for a completed download of the required update; if found, display an alert, install the required update, and launch the newly installed viewer. +        #323: If [optional download and] Install Automatically: display an alert, install the update and launch updated viewer. +        return download_and_install(downloaded = downloaded, url = result_data['url'], version = result_data['version'], download_dir = download_dir,  +                                    size = result_data['size'], platform_key = platform_key, log_file_handle = log_file_handle, in_place = in_place) +    else: +        #323: If the update response indicates that there is an optional update:  +        #323: Check to see if the optional update has already been downloaded. +        #323: If a complete download of that update is found, check the update preference:  +        #note: automatic install handled above as the steps are the same as required upgrades +        #323: If Install Manually: display a message with the update information and ask the user whether or not to install the update with three choices: +        #323: Skip this update: create a marker that subsequent launches should not prompt for this update as long as it is optional,  +        #     but leave the download in place so that if it becomes required it will be there. +        #323: Install next time: create a marker that skips the prompt and installs on the next launch +        #323: Install and launch now: do it. +        if downloaded is not None and downloaded != 'skip': +            frame = InstallerUserMessage(title = "Second Life Installer", icon_name="head-sl-logo.gif") +            #The choices are reordered slightly to encourage immediate install and slightly discourage skipping +            frame.trinary_message(message = "Please make a selection",  +                one = "Install new version now.", two = 'Install the next time the viewer is launched.', three = 'Skip this update.') +            choice = frame.choice.get() +            if choice == 1: +                return download_and_install(downloaded = downloaded, url = result_data['url'], version = result_data['version'], download_dir = download_dir,  +                                    size = result_data['size'], platform_key = platform_key, log_file_handle = log_file_handle, in_place = in_place) +            elif choice == 2: +                tempfile.mkstmp(suffix = ".next", dir = download_dir) +                return (True, None, None) +            else: +                tempfile.mkstmp(suffix = ".skip", dir = download_dir) +                return (True, None, None) +        else: +            #multithread a download +            download(url = result_data['url'], version = result_data['version'], download_dir = download_dir, size = result_data['size'], background = True) +            print "Update manager exited with (%s, %s, %s)" % (True, 'background', True) +            return (True, 'background', True)                   + + +if __name__ == '__main__': +    #there is no argument parsing or other main() work to be done +    update_manager() | 
