SLS-323: integrate update manager with lanucher, various fixes, CMake changes

master
Glenn Glazer 2016-07-11 11:24:45 -07:00
parent bb19a1e9cc
commit 03bcad6111
6 changed files with 582 additions and 24 deletions

View File

@ -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

View File

@ -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()

View File

@ -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
#make a copy by value, not by reference
command = list(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")
#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)

View File

@ -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")

View File

@ -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)

View File

@ -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()