SLS-323: integrate update manager with lanucher, various fixes, CMake changes
parent
bb19a1e9cc
commit
03bcad6111
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue