diff options
Diffstat (limited to 'indra/viewer_components')
28 files changed, 3399 insertions, 0 deletions
diff --git a/indra/viewer_components/Resources/README b/indra/viewer_components/Resources/README new file mode 100644 index 0000000000..e1b35730d4 --- /dev/null +++ b/indra/viewer_components/Resources/README @@ -0,0 +1,9 @@ +This directory only exists as a place for the summary.json file to exist when the unit tests are run on a Mac, where the file goes to a sibling directory of the scripts dir. In Linux and Windows, the JSON file goes into the same directory as the script. + +See: + +test_get_summary.py +update_manager.get_summary() + +for more details +- coyot 201606.02 diff --git a/indra/viewer_components/Resources/summary.json b/indra/viewer_components/Resources/summary.json new file mode 100644 index 0000000000..b78859d427 --- /dev/null +++ b/indra/viewer_components/Resources/summary.json @@ -0,0 +1 @@ +{"Type":"viewer","Version":"4.0.5.315117","Channel":"Second Life Release"} diff --git a/indra/viewer_components/manager/InstallerError.py b/indra/viewer_components/manager/InstallerError.py new file mode 100644 index 0000000000..3b199ea231 --- /dev/null +++ b/indra/viewer_components/manager/InstallerError.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +"""\ +@file InstallerError.py +@author coyot +@date 2016-05-16 +@brief custom exception class for VMP + +$LicenseInfo:firstyear=2016&license=viewerlgpl$ +Second Life Viewer Source Code +Copyright (C) 2016, Linden Research, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; +version 2.1 of the License only. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA +$/LicenseInfo$ +""" + +""" +usage: + +>>> import InstallerError +>>> import os +>>> try: +... os.mkdir('/tmp') +... except OSError, oe: +... ie = InstallerError.InstallerError(oe, "foo") +... raise ie + +Traceback (most recent call last): + File "<stdin>", line 5, in <module> +InstallerError.InstallerError: [Errno [Errno 17] File exists: '/tmp'] foo +""" + +class InstallerError(OSError): + def __init___(self, message): + Exception.__init__(self, message) diff --git a/indra/viewer_components/manager/InstallerUserMessage.py b/indra/viewer_components/manager/InstallerUserMessage.py new file mode 100644 index 0000000000..f66af81d06 --- /dev/null +++ b/indra/viewer_components/manager/InstallerUserMessage.py @@ -0,0 +1,299 @@ +#!/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:firstyear=2013&license=viewerlgpl$ +# Copyright (c) 2013, Linden Research, Inc. +# $/LicenseInfo$ + +""" +@file InstallerUserMessage.py +@author coyot +@date 2016-05-16 +""" + +""" +This does everything the old updater/scripts/darwin/messageframe.py script did and some more bits. +Pushed up the manager directory to be multiplatform. +""" + +import os +import Queue +import threading +import time +import Tkinter as tk +import ttk + + +class InstallerUserMessage(tk.Tk): + #Goals for this class: + # Provide a uniform look and feel + # Provide an easy to use convenience class for other scripts + # Provide windows that automatically disappear when done (for differing notions of done) + # Provide a progress bar that isn't a glorified spinner, but based on download progress + #Non-goals: + # No claim to threadsafety is made or warranted. Your mileage may vary. + # Please consult a doctor if you experience thread pain. + + #Linden standard green color, from Marketing + linden_green = "#487A7B" + + def __init__(self, text="", title="", width=500, height=200, icon_name = None, icon_path = None): + tk.Tk.__init__(self) + self.grid() + self.title(title) + self.choice = tk.BooleanVar() + self.config(background = 'black') + # background="..." doesn't work on MacOS for radiobuttons or progress bars + # http://tinyurl.com/tkmacbuttons + ttk.Style().configure('Linden.TLabel', foreground=InstallerUserMessage.linden_green, background='black') + ttk.Style().configure('Linden.TButton', foreground=InstallerUserMessage.linden_green, background='black') + ttk.Style().configure("black.Horizontal.TProgressbar", foreground=InstallerUserMessage.linden_green, background='black') + + #This bit of configuration centers the window on the screen + # The constants below are to adjust for typical overhead from the + # frame borders. + self.xp = (self.winfo_screenwidth() / 2) - (width / 2) - 8 + self.yp = (self.winfo_screenheight() / 2) - (height / 2) - 20 + self.geometry('{0}x{1}+{2}+{3}'.format(width, height, self.xp, self.yp)) + + #find a few things + self.script_dir = os.path.dirname(os.path.realpath(__file__)) + self.icon_dir = os.path.abspath(os.path.join(self.script_dir, 'icons')) + + #finds the icon and creates the widget + self.find_icon(icon_path, icon_name) + + #defines what to do when window is closed + self.protocol("WM_DELETE_WINDOW", self._delete_window) + + def _delete_window(self): + #capture and discard all destroy events before the choice is set + if not ((self.choice == None) or (self.choice == "")): + try: + self.destroy() + except: + #tk may try to destroy the same object twice + pass + + def set_colors(self, widget): + # #487A7B is "Linden Green" + widget.config(foreground = InstallerUserMessage.linden_green) + widget.config(background='black') + + def find_icon(self, icon_path = None, icon_name = None): + #we do this in each message, let's do it just once instead. + if not icon_path: + icon_path = self.icon_dir + icon_path = os.path.join(icon_path, icon_name) + if os.path.exists(icon_path): + icon = tk.PhotoImage(file=icon_path) + self.image_label = tk.Label(image = icon) + self.image_label.image = icon + else: + #default to text if image not available + self.image_label = tk.Label(text = "Second Life") + + def auto_resize(self, row_count = 0, column_count = 0, heavy_row = None, heavy_column = None): + #auto resize window to fit all rows and columns + #"heavy" gets extra weight + for x in range(column_count): + if x == heavy_column: + self.columnconfigure(x, weight = 2) + else: + self.columnconfigure(x, weight=1) + + for y in range(row_count): + if y == heavy_row: + self.rowconfigure(y, weight = 2) + else: + self.rowconfigure(x, weight=1) + + def basic_message(self, message): + #message: text to be displayed + #icon_path: directory holding the icon, defaults to icons subdir of script dir + #icon_name: filename of icon to be displayed + self.choice.set(True) + self.text_label = tk.Label(text = message) + self.set_colors(self.text_label) + self.set_colors(self.image_label) + #pad, direction and weight are all experimentally derived by retrying various values + self.image_label.grid(row = 1, column = 1, sticky = 'W') + self.text_label.grid(row = 1, column = 2, sticky = 'W', padx =100) + self.auto_resize(row_count = 1, column_count = 2) + self.mainloop() + + def binary_choice_message(self, message, true = 'Yes', false = 'No'): + #true: first option, returns True + #false: second option, returns False + #usage is kind of opaque and relies on this object persisting after the window destruction to pass back choice + #usage: + # frame = InstallerUserMessage.InstallerUserMessage( ... ) + # frame = frame.binary_choice_message( ... ) + # (wait for user to click) + # value = frame.choice.get() + + self.text_label = tk.Label(text = message) + #command registers the callback to the method named. We want the frame to go away once clicked. + #button 1 returns True/1, button 2 returns False/0 + self.button_one = ttk.Radiobutton(text = true, variable = self.choice, value = True, + command = self._delete_window, style = 'Linden.TButton') + self.button_two = ttk.Radiobutton(text = false, variable = self.choice, value = False, + command = self._delete_window, style = 'Linden.TButton') + self.set_colors(self.text_label) + self.set_colors(self.image_label) + #pad, direction and weight are all experimentally derived by retrying various values + self.image_label.grid(row = 1, column = 1, rowspan = 3, sticky = 'W') + self.text_label.grid(row = 1, column = 2, rowspan = 3) + self.button_one.grid(row = 1, column = 3, sticky = 'W', pady = 40) + self.button_two.grid(row = 2, column = 3, sticky = 'W', pady = 0) + self.auto_resize(row_count = 2, column_count = 3, heavy_column = 3) + #self.button_two.deselect() + self.update() + self.mainloop() + + def trinary_choice_message(self, message, one = 1, two = 2, three = 3): + #one: first option, returns 1 + #two: second option, returns 2 + #three: third option, returns 3 + #usage is kind of opaque and relies on this object persisting after the window destruction to pass back choice + #usage: + # frame = InstallerUserMessage.InstallerUserMessage( ... ) + # frame = frame.binary_choice_message( ... ) + # (wait for user to click) + # value = frame.choice.get() + + self.text_label = tk.Label(text = message) + #command registers the callback to the method named. We want the frame to go away once clicked. + self.button_one = ttk.Radiobutton(text = one, variable = self.choice, value = 1, + command = self._delete_window, style = 'Linden.TButton') + self.button_two = ttk.Radiobutton(text = two, variable = self.choice, value = 2, + command = self._delete_window, style = 'Linden.TButton') + self.button_three = ttk.Radiobutton(text = three, variable = self.choice, value = 3, + command = self._delete_window, style = 'Linden.TButton') + self.set_colors(self.text_label) + self.set_colors(self.image_label) + #pad, direction and weight are all experimentally derived by retrying various values + self.image_label.grid(row = 1, column = 1, rowspan = 4, sticky = 'W') + self.text_label.grid(row = 1, column = 2, rowspan = 4, padx = 5) + self.button_one.grid(row = 1, column = 3, sticky = 'W', pady = 5) + self.button_two.grid(row = 2, column = 3, sticky = 'W', pady = 5) + self.button_three.grid(row = 3, column = 3, sticky = 'W', pady = 5) + self.auto_resize(row_count = 3, column_count = 3, heavy_column = 3) + #self.button_two.deselect() + self.update() + self.mainloop() + + def progress_bar(self, message = None, size = 0, interval = 100, pb_queue = None): + #Best effort attempt at a real progress bar + # This is what Tk calls "determinate mode" rather than "indeterminate mode" + #size: denominator of percent complete + #interval: frequency, in ms, of how often to poll the file for progress + #pb_queue: queue object used to send updates to the bar + self.text_label = tk.Label(text = message) + self.set_colors(self.text_label) + self.set_colors(self.image_label) + self.image_label.grid(row = 1, column = 1, sticky = 'NSEW') + self.text_label.grid(row = 2, column = 1, sticky = 'NSEW') + self.progress = ttk.Progressbar(self, style = 'black.Horizontal.TProgressbar', orient="horizontal", length=100, mode="determinate") + self.progress.grid(row = 3, column = 1, sticky = 'NSEW') + self.value = 0 + self.progress["maximum"] = size + self.auto_resize(row_count = 1, column_count = 3) + self.queue = pb_queue + self.check_scheduler() + + def check_scheduler(self): + if self.value < self.progress["maximum"]: + self.check_queue() + self.after(100, self.check_scheduler) + + def check_queue(self): + while self.queue.qsize(): + try: + msg = float(self.queue.get(0)) + #custom signal, time to tear down + if msg == -1: + self.choice.set(True) + self.destroy() + else: + self.progress.step(msg) + self.value = msg + except Queue.Empty: + #nothing to do + return + +class ThreadedClient(threading.Thread): + #for test only, not part of the functional code + def __init__(self, queue): + threading.Thread.__init__(self) + self.queue = queue + + def run(self): + for x in range(1, 90, 10): + time.sleep(1) + print "run " + str(x) + self.queue.put(10) + #tkk progress bars wrap at exactly 100 percent, look full at 99% + print "leftovers" + self.queue.put(9) + time.sleep(5) + # -1 is a custom signal to the progress_bar to quit + self.queue.put(-1) + +if __name__ == "__main__": + #When run as a script, just test the InstallUserMessage. + #To proceed with the test, close the first window, select on the second. The third will close by itself. + import sys + import tempfile + + def set_and_check(frame, value): + print "value: " + str(value) + frame.progress.step(value) + if frame.progress["value"] < frame.progress["maximum"]: + print "In Progress" + else: + print "Over now" + + #basic message window test + frame2 = InstallerUserMessage(text = "Something in the way she moves....", title = "Beatles Quotes for 100", icon_name="head-sl-logo.gif") + frame2.basic_message(message = "...attracts me like no other.") + print "Destroyed!" + sys.stdout.flush() + + #binary choice test. User destroys window when they select. + frame3 = InstallerUserMessage(text = "Something in the way she knows....", title = "Beatles Quotes for 200", icon_name="head-sl-logo.gif") + frame3.binary_choice_message(message = "And all I have to do is think of her.", + true = "Don't want to leave her now", false = 'You know I believe and how') + print frame3.choice.get() + sys.stdout.flush() + + #progress bar + queue = Queue.Queue() + thread = ThreadedClient(queue) + thread.start() + print "thread started" + + frame4 = InstallerUserMessage(text = "Something in the way she knows....", title = "Beatles Quotes for 300", icon_name="head-sl-logo.gif") + 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 new file mode 100755 index 0000000000..0403e01cec --- /dev/null +++ b/indra/viewer_components/manager/SL_Launcher @@ -0,0 +1,194 @@ +#!/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. + +import os +import sys + +#module globals +log_file_handle = None +cwd = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.join(cwd, 'llbase')) + +import argparse +import collections +import InstallerUserMessage +#NOTA BENE: +# For POSIX platforms, llsd.py will be imported from the same directory. +# For Windows, llsd.py will be compiled into the executable by pyinstaller +from llbase import llsd +import platform +import subprocess +import update_manager + + +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("SL LAUNCHER: " + text + "\n") + +def get_cmd_line(): + platform_name = platform.system() + #find the parent of the logs and user_settings directories + if (platform_name == 'Darwin'): + settings_file = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'Resources/app_settings/cmd_line.xml') + elif (platform_name == 'Linux'): + settings_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'app_settings/cmd_line.xml') + #using list format of join is important here because the Windows pathsep in a string escapes the next char + elif (platform_name == 'Windows'): + settings_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'app_settings/cmd_line.xml') + else: + settings_file = None + + try: + cmd_line = llsd.parse((open(settings_file)).read()) + except: + silent_write(log_file_handle, "Could not parse settings file %s" % settings_file) + cmd_line = None + + return cmd_line + +def get_settings(): + #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 capture_vmp_args(arg_list = None, cmd_line = None): + #expected input format: arg_list = ['--set', 'foo', 'bar', '-X', '-Y', 'qux'] + #take a copy of the viewer parameters that are of interest to VMP. + #the regex for a parameter is --<param> {opt1} {opt2} + cli_overrides = {} + cmd_line = get_cmd_line() + + vmp_params = {'--channel':'channel', '--settings':'settings', '--update-service':'update-service', '--set':'set'} + #the settings set with --set. All such settings have only one argument. + vmp_setters = ('UpdaterMaximumBandwidth', 'UpdaterServiceCheckPeriod', 'UpdaterServicePath', 'UpdaterServiceSetting', 'UpdaterServiceURL', 'UpdaterWillingToTest') + + #Here turn the list into a queue, popping off the left as we go. Note that deque() makes a copy by value, not by reference + #Because of the complexity introduced by the uncertainty of how many options a parameter can take, this is far less complicated code than the more + #pythonic (x,y) = <some generator> since we will sometimes have (x), sometimes (x,y) and sometimes (x,y,z) + #also, because the pop is destructive, we prevent ourselves from iterating back over list elements that iterator methods would peek ahead at + vmp_queue = collections.deque(arg_list) + while (len(vmp_queue)): + param = vmp_queue.popleft() + #if it is not one of ours, pop through args until we get to the next parameter + if param in vmp_params.keys(): + if param == '--set': + setting_name = vmp_queue.popleft() + setting_value = vmp_queue.popleft() + if setting_name in vmp_setters: + cli_overrides[vmp_params[param]] = (setting_name, setting_value) + else: + #find out how many args this parameter has + no_dashes = vmp_params[param] + count = cmd_line[no_dashes]['count'] + param_args = [] + if count > 0: + for argh in range(0,count): + param_args.append(vmp_queue.popleft()) + #the parameter name is the key, the (possibly empty) list of args is the value + cli_overrides[vmp_params[param]] = param_args + print "cli override param %s vmp_param %s args %s count %s" % (param, vmp_params[param], param_args, count) + + #to prevent KeyErrors on missing keys, set the remainder to None + for key in vmp_params: + if key != '--set': + try: + cli_overrides[key] + except KeyError: + cli_overrides[key] = None + else: + cli_overrides["--set"] = {} + for arg in vmp_setters: + try: + cli_overrides[key][arg] + except KeyError: + cli_overrides[key][arg] = None + return cli_overrides + +#main entry point +#this and a few other update manager methods really should be refactored into a util lib +parent_dir = update_manager.get_parent_path(update_manager.get_platform_key()) +log_file_handle = update_manager.get_log_file_handle(parent_dir) + +executable_name = "" +if sys.platform.startswith('darwin'): + executable_name = "Second Life" +elif sys.platform.startswith("win") or sys.platform.startswith("cyg"): + if os.path.isfile(os.path.join(cwd,"SecondLifeViewer.exe")): + executable_name = "SecondLifeViewer.exe" + elif os.path.isfile(os.path.join(cwd,"SecondLifeTest.exe")): + executable_name = "SecondLifeTest.exe" + else: + sys.exit("Can't find Windows viewer binary") +elif sys.platform.startswith("linux"): + executable_name = "secondlife" +else: + #SL doesn't run on VMS or punch cards + sys.exit("Unsupported platform") + +#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() +args = parser.parse_known_args(sys.argv) +#args[1] looks like ['./SL_Launcher', '--set', 'foo', 'bar', '-X', '-Y', 'qux'], dump the progname +args_list_to_pass = args[1][1:] +vmp_args = capture_vmp_args(args_list_to_pass) +#make a copy by value, not by reference +command = list(args_list_to_pass) + +(success, state, condition) = update_manager.update_manager(vmp_args) +# 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.' % state + update_manager.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 new file mode 100755 index 0000000000..643e4ad2bc --- /dev/null +++ b/indra/viewer_components/manager/apply_update.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python + +# Copyright (c) 2016, Linden Research, Inc. +# +# The following source code is PROPRIETARY AND CONFIDENTIAL. Use of +# this source code is governed by the Linden Lab Source Code Disclosure +# Agreement ("Agreement") previously entered between you and Linden +# Lab. By accessing, using, copying, modifying or distributing this +# software, you acknowledge that you have been informed of your +# obligations under the Agreement and agree to abide by those obligations. +# +# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO +# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, +# COMPLETENESS OR PERFORMANCE. +# $LicenseInfo:firstyear=2016&license=viewerlgpl$ +# Copyright (c) 2016, Linden Research, Inc. +# $/LicenseInfo$ + +""" +@file apply_update.py +@author coyot +@date 2016-06-28 +""" + +""" +Applies an already downloaded update. +""" + +import argparse +import errno +import fnmatch +import InstallerUserMessage as IUM +import os +import os.path +import plistlib +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile + +#Module level variables + +#fnmatch expressions +LNX_REGEX = '*' + '.bz2' +MAC_REGEX = '*' + '.dmg' +MAC_APP_REGEX = '*' + '.app' +WIN_REGEX = '*' + '.exe' + +#which install the updater is run from +INSTALL_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) + +#whether the update is to the INSTALL_DIR or not. Most of the time this is the case. +IN_PLACE = True + +BUNDLE_IDENTIFIER = "com.secondlife.indra.viewer" +# Magic OS directory name that causes Cocoa viewer to crash on OS X 10.7.5 +# (see MAINT-3331) +STATE_DIR = os.path.join(os.environ["HOME"], "Library", "Saved Application State", + BUNDLE_IDENTIFIER + ".savedState") + +def silent_write(log_file_handle, text): + #if we have a log file, write. If not, do nothing. + if (log_file_handle): + #prepend text for easy grepping + log_file_handle.write("APPLY UPDATE: " + text + "\n") + +def get_filename(download_dir = None): + #given a directory that supposedly has the download, find the installable + #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) + or fnmatch.fnmatch(filename, WIN_REGEX)): + return os.path.join(download_dir, filename) + #someone gave us a bad directory + return None + +def try_dismount(log_file_handle = None, installable = None, tmpdir = None): + #best effort cleanup try to dismount the dmg file if we have mounted one + #the French judge gave it a 5.8 + try: + #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") + silent_write(log_file_handle, output) + except Exception, e: + silent_write(log_file_handle, "Could not detach dmg file %s. Error messages: %s" % (installable, e.message)) + +def apply_update(download_dir = None, platform_key = None, log_file_handle = None, 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 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': + installed = apply_mac_update(installable, log_file_handle) + elif platform_key == 'win': + installed = apply_windows_update(installable, log_file_handle) + else: + #wtf? + raise ValueError("Unknown Platform: " + platform_key) + + if not installed: + #only mark the download as done when everything is done + done_filename = os.path.join(os.path.dirname(installable), ".done") + open(done_filename, 'w+').close() + + return installed + +def apply_linux_update(installable = None, log_file_handle = None): + try: + #untar to tmpdir + tmpdir = tempfile.mkdtemp() + tar = tarfile.open(name = installable, mode="r:bz2") + tar.extractall(path = tmpdir) + if IN_PLACE: + #rename current install dir + shutil.move(INSTALL_DIR,install_dir + ".bak") + #mv new to current + shutil.move(tmpdir, INSTALL_DIR) + #delete tarball on success + os.remove(installable) + except Exception, e: + silent_write(log_file_handle, "Update failed due to " + repr(e)) + return None + return INSTALL_DIR + +def apply_mac_update(installable = None, log_file_handle = None): + #INSTALL_DIR is something like /Applications/Second Life Viewer.app/Contents/MacOS, need to jump up two levels for the install base + install_base = os.path.dirname(INSTALL_DIR) + install_base = os.path.dirname(install_base) + + #verify dmg file + try: + output = subprocess.check_output(["hdiutil", "verify", installable], stderr=subprocess.STDOUT) + silent_write(log_file_handle, "dmg verification succeeded") + silent_write(log_file_handle, output) + except Exception, e: + silent_write(log_file_handle, "Could not verify dmg file %s. Error messages: %s" % (installable, e.message)) + return None + #make temp dir and mount & attach dmg + tmpdir = tempfile.mkdtemp() + try: + output = subprocess.check_output(["hdiutil", "attach", installable, "-mountroot", tmpdir]) + silent_write(log_file_handle, "hdiutil attach succeeded") + silent_write(log_file_handle, output) + except Exception, e: + silent_write(log_file_handle, "Could not attach dmg file %s. Error messages: %s" % (installable, e.message)) + return None + #verify plist + mounted_appdir = None + for top_dir in os.listdir(tmpdir): + for appdir in os.listdir(os.path.join(tmpdir, top_dir)): + appdir = os.path.join(os.path.join(tmpdir, top_dir), appdir) + if fnmatch.fnmatch(appdir, MAC_APP_REGEX): + try: + plist = os.path.join(appdir, "Contents", "Info.plist") + CFBundleIdentifier = plistlib.readPlist(plist)["CFBundleIdentifier"] + mounted_appdir = appdir + except: + #there is no except for this try because there are multiple directories that legimately don't have what we are looking for + pass + if not mounted_appdir: + silent_write(log_file_handle, "Could not find app bundle in dmg %s." % (installable,)) + return None + if CFBundleIdentifier != BUNDLE_IDENTIFIER: + silent_write(log_file_handle, "Wrong or null bundle identifier for dmg %s. Bundle identifier: %s" % (installable, CFBundleIdentifier)) + try_dismount(log_file_handle, installable, tmpdir) + return None + #do the install, finally + if IN_PLACE: + # swap out old install directory + bundlename = os.path.basename(mounted_appdir) + silent_write(log_file_handle, "Updating %s" % bundlename) + swapped_out = os.path.join(tmpdir, INSTALL_DIR.lstrip('/')) + shutil.move(install_base, swapped_out) + else: + silent_write(log_file_handle, "Installing %s" % install_base) + + # copy over the new bits + try: + shutil.copytree(mounted_appdir, install_base, symlinks=True) + retcode = 0 + except Exception, e: + # try to restore previous viewer + if os.path.exists(swapped_out): + silent_write(log_file_handle, "Install of %s failed, rolling back to previous viewer." % installable) + shutil.move(swapped_out, installed_test) + retcode = 1 + finally: + try_dismount(log_file_handle, installable, tmpdir) + if retcode: + return None + + #see MAINT-3331 + try: + shutil.rmtree(STATE_DIR) + except Exception, e: + #if we fail to delete something that isn't there, that's okay + if e[0] == errno.ENOENT: + pass + else: + raise e + + os.remove(installable) + return install_base + +def apply_windows_update(installable = None, log_file_handle = None): + #the windows install is just running the NSIS installer executable + #from VMP's perspective, it is a black box + try: + output = subprocess.check_output(installable, stderr=subprocess.STDOUT) + silent_write(log_file_handle, "Install of %s succeeded." % installable) + silent_write(log_file_handle, output) + except subprocess.CalledProcessError, cpe: + silent_write(log_file_handle, "%s failed with return code %s. Error messages: %s." % + (cpe.cmd, cpe.returncode, cpe.message)) + return None + #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") + parser.add_argument('--dir', dest = 'download_dir', help = 'directory to find installable', required = True) + parser.add_argument('--pkey', dest = 'platform_key', help =' OS: lnx|mac|win', required = True) + parser.add_argument('--in_place', action = 'store_false', help = 'This upgrade is for a different channel', default = True) + parser.add_argument('--log_file', dest = 'log_file', default = None, help = 'file to write messages to') + args = parser.parse_args() + + if args.log_file: + try: + f = open(args.log_file,'w+') + except: + print "%s could not be found or opened" % args.log_file + sys.exit(1) + + IN_PLACE = args.in_place + result = apply_update(download_dir = args.download_dir, platform_key = args.platform_key, log_file_handle = f) + if not result: + sys.exit("Update failed") + else: + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/indra/viewer_components/manager/download_update.py b/indra/viewer_components/manager/download_update.py new file mode 100755 index 0000000000..23f784c6c1 --- /dev/null +++ b/indra/viewer_components/manager/download_update.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +# Copyright (c) 2016, Linden Research, Inc. +# +# The following source code is PROPRIETARY AND CONFIDENTIAL. Use of +# this source code is governed by the Linden Lab Source Code Disclosure +# Agreement ("Agreement") previously entered between you and Linden +# Lab. By accessing, using, copying, modifying or distributing this +# software, you acknowledge that you have been informed of your +# obligations under the Agreement and agree to abide by those obligations. +# +# ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO +# WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY, +# COMPLETENESS OR PERFORMANCE. +# $LicenseInfo:firstyear=2016&license=viewerlgpl$ +# Copyright (c) 2016, Linden Research, Inc. +# $/LicenseInfo$ + +""" +@file download_update.py +@author coyot +@date 2016-06-23 +""" + +""" +Performs a download of an update. In a separate script from update_manager so that we can +call it with subprocess. +""" + +import argparse +import InstallerUserMessage as IUM +import os +import Queue +import requests +import threading + +#module default +CHUNK_SIZE = 1024 + +def download_update(url = None, download_dir = None, size = None, progressbar = False, chunk_size = CHUNK_SIZE): + #url to download from + #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, 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) + down_thread.start() + + if progressbar: + frame = IUM.InstallerUserMessage(title = "Second Life Downloader", icon_name="head-sl-logo.gif") + frame.progress_bar(message = "Download Progress", size = size, pb_queue = queue) + frame.mainloop() + else: + #nothing for the main thread to do + down_thread.join() + +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 + self.chunk_size = int(chunk_size) + self.progressbar = progressbar + self.in_queue = in_queue + + 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: + #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) + parser.add_argument('--pb', dest='progressbar', help='whether or not to show a progressbar', action="store_true", default = False) + parser.add_argument('--size', dest='size', help='size of download for progressbar') + parser.add_argument('--chunk_size', dest='chunk_size', default=CHUNK_SIZE, help='max portion size of download to be loaded in memory in bytes.') + args = parser.parse_args() + + download_update(url = args.url, download_dir = args.download_dir, size = args.size, progressbar = args.progressbar, chunk_size = args.chunk_size) + + +if __name__ == "__main__": + main() diff --git a/indra/viewer_components/manager/icons/SL_Logo.gif b/indra/viewer_components/manager/icons/SL_Logo.gif Binary files differnew file mode 100644 index 0000000000..c24d6b08cb --- /dev/null +++ b/indra/viewer_components/manager/icons/SL_Logo.gif diff --git a/indra/viewer_components/manager/icons/SL_Logo.png b/indra/viewer_components/manager/icons/SL_Logo.png Binary files differnew file mode 100644 index 0000000000..5e376c72f9 --- /dev/null +++ b/indra/viewer_components/manager/icons/SL_Logo.png diff --git a/indra/viewer_components/manager/icons/head-sl-logo.gif b/indra/viewer_components/manager/icons/head-sl-logo.gif Binary files differnew file mode 100644 index 0000000000..d635348dcc --- /dev/null +++ b/indra/viewer_components/manager/icons/head-sl-logo.gif diff --git a/indra/viewer_components/manager/icons/head-sl-logo.png b/indra/viewer_components/manager/icons/head-sl-logo.png Binary files differnew file mode 100644 index 0000000000..5c214e96d1 --- /dev/null +++ b/indra/viewer_components/manager/icons/head-sl-logo.png diff --git a/indra/viewer_components/manager/tests/data/settings.xml b/indra/viewer_components/manager/tests/data/settings.xml new file mode 100644 index 0000000000..07e420dcb3 --- /dev/null +++ b/indra/viewer_components/manager/tests/data/settings.xml @@ -0,0 +1,1184 @@ +<llsd> + <map> + <key>AllowMultipleViewers</key> + <map> + <key>Comment</key> + <string>Allow multiple viewers.</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>1</integer> + </map> + <key>AllowTapTapHoldRun</key> + <map> + <key>Comment</key> + <string>Tapping a direction key twice and holding it down makes avatar run</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>AppearanceCameraMovement</key> + <map> + <key>Comment</key> + <string>When entering appearance editing mode, camera zooms in on currently selected portion of avatar</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>AudioLevelMedia</key> + <map> + <key>Comment</key> + <string>Audio level of Quicktime movies</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.699999988079071044921875</real> + </map> + <key>AudioLevelMic</key> + <map> + <key>Comment</key> + <string>Audio level of microphone input</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.1749999970197677612304688</real> + </map> + <key>AudioLevelMusic</key> + <map> + <key>Comment</key> + <string>Audio level of streaming music</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0</real> + </map> + <key>AudioLevelSFX</key> + <map> + <key>Comment</key> + <string>Audio level of in-world sound effects</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.699999988079071044921875</real> + </map> + <key>AudioLevelVoice</key> + <map> + <key>Comment</key> + <string>Audio level of voice chat</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>1</real> + </map> + <key>AudioStreamingMedia</key> + <map> + <key>Comment</key> + <string>Enable streaming</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>AvatarAxisDeadZone0</key> + <map> + <key>Comment</key> + <string>Avatar axis 0 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.1000000014901161193847656</real> + </map> + <key>AvatarAxisDeadZone1</key> + <map> + <key>Comment</key> + <string>Avatar axis 1 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.1000000014901161193847656</real> + </map> + <key>AvatarAxisDeadZone2</key> + <map> + <key>Comment</key> + <string>Avatar axis 2 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.1000000014901161193847656</real> + </map> + <key>AvatarAxisDeadZone3</key> + <map> + <key>Comment</key> + <string>Avatar axis 3 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>1</real> + </map> + <key>AvatarAxisDeadZone4</key> + <map> + <key>Comment</key> + <string>Avatar axis 4 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.01999999955296516418457031</real> + </map> + <key>AvatarAxisDeadZone5</key> + <map> + <key>Comment</key> + <string>Avatar axis 5 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>AvatarAxisScale3</key> + <map> + <key>Comment</key> + <string>Avatar axis 3 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0</real> + </map> + <key>AvatarAxisScale4</key> + <map> + <key>Comment</key> + <string>Avatar axis 4 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>2</real> + </map> + <key>AvatarAxisScale5</key> + <map> + <key>Comment</key> + <string>Avatar axis 5 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>2</real> + </map> + <key>AvatarFeathering</key> + <map> + <key>Comment</key> + <string>Avatar feathering (less is softer)</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>6</real> + </map> + <key>AvatarFileName</key> + <map> + <key>Comment</key> + <string>Alternative avatar file name</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>avatar_lad_tentacles.xml</string> + </map> + <key>BuildAxisDeadZone0</key> + <map> + <key>Comment</key> + <string>Build axis 0 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>BuildAxisDeadZone1</key> + <map> + <key>Comment</key> + <string>Build axis 1 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>BuildAxisDeadZone2</key> + <map> + <key>Comment</key> + <string>Build axis 2 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>BuildAxisDeadZone3</key> + <map> + <key>Comment</key> + <string>Build axis 3 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>BuildAxisDeadZone4</key> + <map> + <key>Comment</key> + <string>Build axis 4 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>BuildAxisDeadZone5</key> + <map> + <key>Comment</key> + <string>Build axis 5 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>BuildAxisScale0</key> + <map> + <key>Comment</key> + <string>Build axis 0 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>6</real> + </map> + <key>BuildAxisScale1</key> + <map> + <key>Comment</key> + <string>Build axis 1 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>6</real> + </map> + <key>BuildAxisScale2</key> + <map> + <key>Comment</key> + <string>Build axis 2 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>6</real> + </map> + <key>BuildAxisScale3</key> + <map> + <key>Comment</key> + <string>Build axis 3 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>6</real> + </map> + <key>BuildAxisScale4</key> + <map> + <key>Comment</key> + <string>Build axis 4 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>6</real> + </map> + <key>BuildAxisScale5</key> + <map> + <key>Comment</key> + <string>Build axis 5 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>6</real> + </map> + <key>BuildFeathering</key> + <map> + <key>Comment</key> + <string>Build feathering (less is softer)</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>12</real> + </map> + <key>BulkChangeEveryoneCopy</key> + <map> + <key>Comment</key> + <string>Bulk changed objects can be copied by everyone</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>BulkChangeNextOwnerCopy</key> + <map> + <key>Comment</key> + <string>Bulk changed objects can be copied by next owner</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>BulkChangeNextOwnerModify</key> + <map> + <key>Comment</key> + <string>Bulk changed objects can be modified by next owner</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>BulkChangeShareWithGroup</key> + <map> + <key>Comment</key> + <string>Bulk changed objects are shared with the currently active group</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>CacheValidateCounter</key> + <map> + <key>Comment</key> + <string>Used to distribute cache validation</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>122</integer> + </map> + <key>CameraPosOnLogout</key> + <map> + <key>Comment</key> + <string>Camera position when last logged out (global coordinates)</string> + <key>Type</key> + <string>Vector3D</string> + <key>Value</key> + <array> + <real>288290.4477181434631347656</real> + <real>275988.5277819633483886719</real> + <real>49.10921102762222290039062</real> + </array> + </map> + <key>ClickToWalk</key> + <map> + <key>Comment</key> + <string>Click in world to walk to location</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>ConversationSortOrder</key> + <map> + <key>Comment</key> + <string>Specifies sort key for conversations</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>CurrentGrid</key> + <map> + <key>Comment</key> + <string>Currently Selected Grid</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>util.agni.lindenlab.com</string> + </map> + <key>Cursor3D</key> + <map> + <key>Comment</key> + <string>Treat Joystick values as absolute positions (not deltas).</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>FirstLoginThisInstall</key> + <map> + <key>Comment</key> + <string>Specifies that you have not logged in with the viewer since you performed a clean install</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>FirstRunThisInstall</key> + <map> + <key>Comment</key> + <string>Specifies that you have not run the viewer since you performed a clean install</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>FlycamAxisDeadZone0</key> + <map> + <key>Comment</key> + <string>Flycam axis 0 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>FlycamAxisDeadZone1</key> + <map> + <key>Comment</key> + <string>Flycam axis 1 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>FlycamAxisDeadZone2</key> + <map> + <key>Comment</key> + <string>Flycam axis 2 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>FlycamAxisDeadZone3</key> + <map> + <key>Comment</key> + <string>Flycam axis 3 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>FlycamAxisDeadZone4</key> + <map> + <key>Comment</key> + <string>Flycam axis 4 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>FlycamAxisDeadZone5</key> + <map> + <key>Comment</key> + <string>Flycam axis 5 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.009999999776482582092285156</real> + </map> + <key>FlycamAxisDeadZone6</key> + <map> + <key>Comment</key> + <string>Flycam axis 6 dead zone.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>1</real> + </map> + <key>FlycamAxisScale0</key> + <map> + <key>Comment</key> + <string>Flycam axis 0 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>42</real> + </map> + <key>FlycamAxisScale1</key> + <map> + <key>Comment</key> + <string>Flycam axis 1 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>40</real> + </map> + <key>FlycamAxisScale2</key> + <map> + <key>Comment</key> + <string>Flycam axis 2 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>40</real> + </map> + <key>FlycamAxisScale3</key> + <map> + <key>Comment</key> + <string>Flycam axis 3 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0</real> + </map> + <key>FlycamAxisScale4</key> + <map> + <key>Comment</key> + <string>Flycam axis 4 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>2</real> + </map> + <key>FlycamAxisScale5</key> + <map> + <key>Comment</key> + <string>Flycam axis 5 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>3</real> + </map> + <key>FlycamAxisScale6</key> + <map> + <key>Comment</key> + <string>Flycam axis 6 scaler.</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0</real> + </map> + <key>FlycamFeathering</key> + <map> + <key>Comment</key> + <string>Flycam feathering (less is softer)</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>5</real> + </map> + <key>FocusPosOnLogout</key> + <map> + <key>Comment</key> + <string>Camera focus point when last logged out (global coordinates)</string> + <key>Type</key> + <string>Vector3D</string> + <key>Value</key> + <array> + <real>288287.8830481640761718154</real> + <real>275991.5973855691263452172</real> + <real>47.96361158013021963597566</real> + </array> + </map> + <key>ForceShowGrid</key> + <map> + <key>Comment</key> + <string>Always show grid dropdown on login screen</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>1</integer> + </map> + <key>HttpProxyType</key> + <map> + <key>Comment</key> + <string>Proxy type to use for HTTP operations</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>None</string> + </map> + <key>JoystickInitialized</key> + <map> + <key>Comment</key> + <string>Whether or not a joystick has been detected and initiailized.</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>UnknownDevice</string> + </map> + <key>LSLFindCaseInsensitivity</key> + <map> + <key>Comment</key> + <string>Use case insensitivity when searching in LSL editor</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>1</integer> + </map> + <key>LastFeatureVersion</key> + <map> + <key>Comment</key> + <string>[DO NOT MODIFY] Feature Table Version number for tracking rendering system changes</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>37</integer> + </map> + <key>LastGPUString</key> + <map> + <key>Comment</key> + <string>[DO NOT MODIFY] previous GPU id string for tracking hardware changes</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>NVIDIA Corporation NVIDIA GeForce GT 750M OpenGL Engine</string> + </map> + <key>LastPrefTab</key> + <map> + <key>Comment</key> + <string>Last selected tab in preferences window</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>1</integer> + </map> + <key>LastRunVersion</key> + <map> + <key>Comment</key> + <string>Version number of last instance of the viewer that you ran</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>Second Life Project Bento 5.0.0.315657</string> + </map> + <key>LocalCacheVersion</key> + <map> + <key>Comment</key> + <string>Version number of cache</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>7</integer> + </map> + <key>LoginLocation</key> + <map> + <key>Comment</key> + <string>Default Login location ('last', 'home') preference</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>home</string> + </map> + <key>MapScale</key> + <map> + <key>Comment</key> + <string>World map zoom level (pixels per region)</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>256</real> + </map> + <key>MaxJointsPerMeshObject</key> + <map> + <key>Comment</key> + <string>Maximum joints per rigged mesh object</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <real>51</real> + </map> + <key>MediaEnablePopups</key> + <map> + <key>Comment</key> + <string>If true, enable targeted links and javascript in media to open new media browser windows without a prompt.</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>MediaShowOnOthers</key> + <map> + <key>Comment</key> + <string>Whether or not to show media on other avatars</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>1</integer> + </map> + <key>MigrateCacheDirectory</key> + <map> + <key>Comment</key> + <string>Check for old version of disk cache to migrate to current location</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>NavBarShowParcelProperties</key> + <map> + <key>Comment</key> + <string>Show parcel property icons in navigation bar</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>NextLoginLocation</key> + <map> + <key>Comment</key> + <string>Location to log into for this session - set from command line or the login panel, cleared following a successfull login.</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>home</string> + </map> + <key>NotificationConferenceIMOptions</key> + <map> + <key>Comment</key> + <string> + Specifies how the UI responds to Conference IM Notifications. + Allowed values: [openconversations,toast,flash,noaction] + </string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>none</string> + </map> + <key>NotificationFriendIMOptions</key> + <map> + <key>Comment</key> + <string> + Specifies how the UI responds to Friend IM Notifications. + Allowed values: [openconversations,toast,flash,noaction] + </string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>openconversations</string> + </map> + <key>NotificationGroupChatOptions</key> + <map> + <key>Comment</key> + <string> + Specifies how the UI responds to Group Chat Notifications. + Allowed values: [openconversations,toast,flash,noaction] + </string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>none</string> + </map> + <key>NotificationNearbyChatOptions</key> + <map> + <key>Comment</key> + <string> + Specifies how the UI responds to Nearby Chat Notifications. + Allowed values: [openconversations,toast,flash,noaction] + </string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>none</string> + </map> + <key>NotificationNonFriendIMOptions</key> + <map> + <key>Comment</key> + <string> + Specifies how the UI responds to Non Friend IM Notifications. + Allowed values: [openconversations,toast,flash,noaction] + </string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>openconversations</string> + </map> + <key>NumSessions</key> + <map> + <key>Comment</key> + <string>Number of successful logins to Second Life</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>1674</integer> + </map> + <key>PlayTypingAnim</key> + <map> + <key>Comment</key> + <string>Your avatar plays the typing animation whenever you type in the chat bar</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>PoolSizeExpCache</key> + <map> + <key>Comment</key> + <string>Coroutine Pool size for ExpCache</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>5</integer> + </map> + <key>PreferredBrowserBehavior</key> + <map> + <key>Comment</key> + <string>Use system browser for any links (0), use builtin browser for SL links and system one for others (1) or use builtin browser only (2).</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <string>0</string> + </map> + <key>PreferredMaturity</key> + <map> + <key>Comment</key> + <string>Setting for the user's preferred maturity level (consts in indra_constants.h)</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>42</integer> + </map> + <key>PresetGraphicActive</key> + <map> + <key>Comment</key> + <string>Name of currently selected preference</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>Default</string> + </map> + <key>ProbeHardwareOnStartup</key> + <map> + <key>Comment</key> + <string>Query current hardware configuration on application startup</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>QAMode</key> + <map> + <key>Comment</key> + <string>Enable Testing Features.</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>RenderAnisotropic</key> + <map> + <key>Comment</key> + <string>Render textures using anisotropic filtering</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>RenderAvatarCloth</key> + <map> + <key>Comment</key> + <string>Controls if avatars use wavy cloth</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>RenderAvatarLODFactor</key> + <map> + <key>Comment</key> + <string>Controls level of detail of avatars (multiplier for current screen area when calculated level of detail)</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>1</real> + </map> + <key>RenderFSAASamples</key> + <map> + <key>Comment</key> + <string>Number of samples to use for FSAA (0 = no AA).</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>2</integer> + </map> + <key>RenderFarClip</key> + <map> + <key>Comment</key> + <string>Distance of far clip plane from camera (meters)</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>128</real> + </map> + <key>RenderMaxPartCount</key> + <map> + <key>Comment</key> + <string>Maximum number of particles to display on screen</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <real>2048</real> + </map> + <key>RenderQualityPerformance</key> + <map> + <key>Comment</key> + <string>Which graphics settings you've chosen</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <real>4</real> + </map> + <key>RenderReflectionDetail</key> + <map> + <key>Comment</key> + <string>Detail of reflection render pass.</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>RenderTerrainLODFactor</key> + <map> + <key>Comment</key> + <string>Controls level of detail of terrain (multiplier for current screen area when calculated level of detail)</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>2</real> + </map> + <key>RenderVBOEnable</key> + <map> + <key>Comment</key> + <string>Use GL Vertex Buffer Objects</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>RenderVolumeLODFactor</key> + <map> + <key>Comment</key> + <string>Controls level of detail of primitives (multiplier for current screen area when calculated level of detail)</string> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>1.125</real> + </map> + <key>ShowAdvancedGraphicsSettings</key> + <map> + <key>Comment</key> + <string>Show advanced graphics settings</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>1</integer> + </map> + <key>ShowBanLines</key> + <map> + <key>Comment</key> + <string>Show in-world ban/access borders</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>ShowStartLocation</key> + <map> + <key>Comment</key> + <string>Display starting location menu on login screen</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>SkeletonFileName</key> + <map> + <key>Comment</key> + <string>Alternative skeleton file name</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>avatar_skeleton_tentacles.xml</string> + </map> + <key>SkyPresetName</key> + <map> + <key>Comment</key> + <string>Sky preset to use. May be superseded by region settings or by a day cycle (see DayCycleName).</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>Sunset</string> + </map> + <key>SnapshotConfigURL</key> + <map> + <key>Comment</key> + <string>URL to fetch Snapshot Sharing configuration data from.</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>http://photos.apps.avatarsunited.com/viewer_config</string> + </map> + <key>SnapshotFormat</key> + <map> + <key>Comment</key> + <string>Save snapshots in this format (0 = PNG, 1 = JPEG, 2 = BMP)</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>1</integer> + </map> + <key>SnapshotQuality</key> + <map> + <key>Comment</key> + <string>Quality setting of postcard JPEGs (0 = worst, 100 = best)</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>100</integer> + </map> + <key>SpellCheck</key> + <map> + <key>Comment</key> + <string>Enable spellchecking on line and text editors</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <integer>0</integer> + </map> + <key>SpellCheckDictionary</key> + <map> + <key>Comment</key> + <string>Current primary and secondary dictionaries used for spell checking</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>English (United States)</string> + </map> + <key>TextureMemory</key> + <map> + <key>Comment</key> + <string>Amount of memory to use for textures in MB (0 = autodetect)</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>256</integer> + </map> + <key>UseDayCycle</key> + <map> + <key>Comment</key> + <string>Whether to use use a day cycle or a fixed sky.</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>UseDebugMenus</key> + <map> + <key>Comment</key> + <string>Turns on "Debug" menu</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>UseEnvironmentFromRegion</key> + <map> + <key>Comment</key> + <string>Choose whether to use the region's environment settings, or override them with the local settings.</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>0</boolean> + </map> + <key>VFSOldSize</key> + <map> + <key>Comment</key> + <string>[DO NOT MODIFY] Controls resizing of local file cache</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>102</integer> + </map> + <key>VFSSalt</key> + <map> + <key>Comment</key> + <string>[DO NOT MODIFY] Controls local file caching behavior</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>260093998</integer> + </map> + <key>VersionChannelName</key> + <map> + <key>Comment</key> + <string>Version information generated by running the viewer</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>Second Life Release</string> + </map> + <key>VertexShaderEnable</key> + <map> + <key>Comment</key> + <string>Enable/disable all GLSL shaders (debug)</string> + <key>Type</key> + <string>Boolean</string> + <key>Value</key> + <boolean>1</boolean> + </map> + <key>VoiceInputAudioDevice</key> + <map> + <key>Comment</key> + <string>Audio input device to use for voice</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>C-Media USB Audio Device</string> + </map> + <key>VoiceOutputAudioDevice</key> + <map> + <key>Comment</key> + <string>Audio output device to use for voice</string> + <key>Type</key> + <string>String</string> + <key>Value</key> + <string>C-Media USB Audio Device</string> + </map> + <key>WLSkyDetail</key> + <map> + <key>Comment</key> + <string>Controls vertex detail on the WindLight sky. Lower numbers will give better performance and uglier skies.</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>48</integer> + </map> + <key>WebProfileFloaterRect</key> + <map> + <key>Comment</key> + <string>Web profile floater dimensions</string> + <key>Type</key> + <string>Rect</string> + <key>Value</key> + <array> + <integer>1189</integer> + <integer>957</integer> + <integer>1674</integer> + <integer>277</integer> + </array> + </map> + <key>WindowHeight</key> + <map> + <key>Comment</key> + <string>SL viewer window height</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>1000</integer> + </map> + <key>WindowWidth</key> + <map> + <key>Comment</key> + <string>SL viewer window width</string> + <key>Type</key> + <string>U32</string> + <key>Value</key> + <integer>1626</integer> + </map> + <key>WindowX</key> + <map> + <key>Comment</key> + <string>X coordinate of upper left corner of SL viewer window, relative to upper left corner of primary display (pixels)</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>50</integer> + </map> + <key>WindowY</key> + <map> + <key>Comment</key> + <string>Y coordinate of upper left corner of SL viewer window, relative to upper left corner of primary display (pixels)</string> + <key>Type</key> + <string>S32</string> + <key>Value</key> + <integer>50</integer> + </map> + </map> +</llsd> diff --git a/indra/viewer_components/manager/tests/summary.json b/indra/viewer_components/manager/tests/summary.json new file mode 100644 index 0000000000..b78859d427 --- /dev/null +++ b/indra/viewer_components/manager/tests/summary.json @@ -0,0 +1 @@ +{"Type":"viewer","Version":"4.0.5.315117","Channel":"Second Life Release"} diff --git a/indra/viewer_components/manager/tests/test_InstallerError.py b/indra/viewer_components/manager/tests/test_InstallerError.py new file mode 100644 index 0000000000..d722208b7f --- /dev/null +++ b/indra/viewer_components/manager/tests/test_InstallerError.py @@ -0,0 +1,39 @@ +#!/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$ + +""" +@file test_InstallerError.py +@author coyot +@date 2016-06-01 +""" + +from nose.tools import assert_equal + +import InstallerError +import os + +def test_InstallerError(): + try: + #try to make our own homedir, this will fail on all three platforms + homedir = os.path.abspath(os.path.expanduser('~')) + os.mkdir(homedir) + except OSError, oe: + ie = InstallerError.InstallerError(oe, "Installer failed to create a homedir that already exists.") + + assert_equal( str(ie), + "[Errno [Errno 17] File exists: '%s'] Installer failed to create a homedir that already exists." % homedir) diff --git a/indra/viewer_components/manager/tests/test_check_for_completed_download.py b/indra/viewer_components/manager/tests/test_check_for_completed_download.py new file mode 100644 index 0000000000..388bc900e9 --- /dev/null +++ b/indra/viewer_components/manager/tests/test_check_for_completed_download.py @@ -0,0 +1,53 @@ +#!/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$ + +""" +@file test_check_for_completed_download.py +@author coyot +@date 2016-06-03 +""" + +from nose.tools import * +from nose import with_setup + +import os +import shutil +import tempfile +import update_manager +import with_setup_args + +def check_for_completed_download_setup(): + tmpdir1 = tempfile.mkdtemp(prefix = 'test1') + tmpdir2 = tempfile.mkdtemp(prefix = 'test2') + tempfile.mkstemp(suffix = '.done', dir = tmpdir1) + + return [tmpdir1,tmpdir2], {} + +def check_for_completed_download_teardown(tmpdir1,tmpdir2): + shutil.rmtree(tmpdir1, ignore_errors = True) + shutil.rmtree(tmpdir2, ignore_errors = True) + +@with_setup_args.with_setup_args(check_for_completed_download_setup, check_for_completed_download_teardown) +def test_completed_check_for_completed_download(tmpdir1,tmpdir2): + assert_equal(update_manager.check_for_completed_download(tmpdir1), 'done'), "Failed to find completion marker" + +@with_setup_args.with_setup_args(check_for_completed_download_setup, check_for_completed_download_teardown) +def test_incomplete_check_for_completed_download(tmpdir1,tmpdir2): + #should return False + incomplete = not update_manager.check_for_completed_download(tmpdir2) + assert incomplete, "False positive, should not mark complete without a marker"
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/test_convert_version_file_style.py b/indra/viewer_components/manager/tests/test_convert_version_file_style.py new file mode 100644 index 0000000000..d700f91b84 --- /dev/null +++ b/indra/viewer_components/manager/tests/test_convert_version_file_style.py @@ -0,0 +1,55 @@ +#!/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$ + +""" +@file test_convert_version_file_style.py +@author coyot +@date 2016-06-01 +""" + +from nose.tools import assert_equal + +import update_manager + +def test_normal_form(): + version = '1.2.3.456789' + golden = '1_2_3_456789' + converted = update_manager.convert_version_file_style(version) + + assert_equal(golden, converted) + +def test_short_form(): + version = '1.23' + golden = '1_23' + converted = update_manager.convert_version_file_style(version) + + assert_equal(golden, converted) + +def test_idempotent(): + version = '123' + golden = '123' + converted = update_manager.convert_version_file_style(version) + + assert_equal(golden, converted) + +def test_none(): + version = None + golden = None + converted = update_manager.convert_version_file_style(version) + + assert_equal(golden, converted)
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/test_get_filename.py b/indra/viewer_components/manager/tests/test_get_filename.py new file mode 100644 index 0000000000..95771d75dc --- /dev/null +++ b/indra/viewer_components/manager/tests/test_get_filename.py @@ -0,0 +1,60 @@ +#!/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$ + +""" +@file test_get_filename.py +@author coyot +@date 2016-06-30 +""" + +from nose.tools import * +from nose import with_setup + +import os +import shutil +import tempfile +import apply_update +import with_setup_args + +def get_filename_setup(): + tmpdir1 = tempfile.mkdtemp(prefix = 'lnx') + tmpdir2 = tempfile.mkdtemp(prefix = 'mac') + tmpdir3 = tempfile.mkdtemp(prefix = 'win') + tmpdir4 = tempfile.mkdtemp(prefix = 'bad') + tempfile.mkstemp(suffix = '.bz2', dir = tmpdir1) + tempfile.mkstemp(suffix = '.dmg', dir = tmpdir2) + tempfile.mkstemp(suffix = '.exe', dir = tmpdir3) + + return [tmpdir1,tmpdir2, tmpdir3, tmpdir4], {} + +def get_filename_teardown(tmpdir1,tmpdir2, tmpdir3, tmpdir4): + shutil.rmtree(tmpdir1, ignore_errors = True) + shutil.rmtree(tmpdir2, ignore_errors = True) + shutil.rmtree(tmpdir3, ignore_errors = True) + shutil.rmtree(tmpdir4, ignore_errors = True) + +@with_setup_args.with_setup_args(get_filename_setup, get_filename_teardown) +def test_get_filename(tmpdir1, tmpdir2, tmpdir3, tmpdir4): + assert_is_not_none(apply_update.get_filename(tmpdir1)), "Failed to find installable" + assert_is_not_none(apply_update.get_filename(tmpdir2)), "Failed to find installable" + assert_is_not_none(apply_update.get_filename(tmpdir3)), "Failed to find installable" + +@with_setup_args.with_setup_args(get_filename_setup, get_filename_teardown) +def test_missing_get_filename(tmpdir1, tmpdir2, tmpdir3, tmpdir4): + not_found = not apply_update.get_filename(tmpdir4) + assert not_found, "False positive, should not find an installable in an empty dir"
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/test_get_log_file_handle.py b/indra/viewer_components/manager/tests/test_get_log_file_handle.py new file mode 100644 index 0000000000..c5b3c89550 --- /dev/null +++ b/indra/viewer_components/manager/tests/test_get_log_file_handle.py @@ -0,0 +1,63 @@ +#!/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$ + +""" +@file test_get_log_file_handle.py +@author coyot +@date 2016-06-08 +""" + +from nose.tools import * + +import os +import shutil +import tempfile +import update_manager +import with_setup_args + +def get_log_file_handle_setup(): + tmpdir1 = tempfile.mkdtemp(prefix = 'test1') + tmpdir2 = tempfile.mkdtemp(prefix = 'test2') + log_file_path = os.path.abspath(os.path.join(tmpdir1,"update_manager.log")) + #not using tempfile because we want a particular filename + open(log_file_path, 'w+').close + + return [tmpdir1,tmpdir2,log_file_path], {} + +def get_log_file_handle_teardown(tmpdir1,tmpdir2,log_file_path): + shutil.rmtree(tmpdir1, ignore_errors = True) + shutil.rmtree(tmpdir2, ignore_errors = True) + +@with_setup_args.with_setup_args(get_log_file_handle_setup, get_log_file_handle_teardown) +def test_existing_get_log_file_handle(tmpdir1,tmpdir2,log_file_path): + handle = update_manager.get_log_file_handle(tmpdir1) + if not handle: + print "Failed to find existing log file" + assert False + elif not os.path.exists(os.path.abspath(log_file_path+".old")): + print "Failed to rotate update manager log" + assert False + assert True + +@with_setup_args.with_setup_args(get_log_file_handle_setup, get_log_file_handle_teardown) +def test_missing_get_log_file_handle(tmpdir1,tmpdir2,log_file_path): + handle = update_manager.get_log_file_handle(tmpdir2) + if not os.path.exists(log_file_path): + print "Failed to touch new log file" + assert False + assert True
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/test_get_parent_path.py b/indra/viewer_components/manager/tests/test_get_parent_path.py new file mode 100644 index 0000000000..3cfd72310e --- /dev/null +++ b/indra/viewer_components/manager/tests/test_get_parent_path.py @@ -0,0 +1,79 @@ +#!/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$ + +""" +@file test_get_parent_path.py +@author coyot +@date 2016-06-02 +""" + +from nose.tools import * +from nose import with_setup + +import os +import shutil +import update_manager +import with_setup_args + +def get_parent_path_setup(): + key = update_manager.get_platform_key() + try: + if key == 'mac': + settings_dir = os.path.join(os.path.expanduser('~'),'Library','Application Support','SecondLife') + elif key == 'lnx': + settings_dir = os.path.join(os.path.expanduser('~'),'.secondlife') + elif key == 'win': + settings_dir = os.path.join(os.path.expanduser('~'),'AppData','Roaming','SecondLife') + else: + raise Exception("Invalid Platform Key") + + #preserve existing settings dir if any + if os.path.exists(settings_dir): + old_dir = settings_dir + ".tmp" + if os.path.exists(old_dir): + shutil.rmtree(old_dir, ignore_errors = True) + os.rename(settings_dir, old_dir) + os.makedirs(settings_dir) + except Exception, e: + print "get_parent_path_setup failed due to: %s" % str(e) + assert False + + #this is we don't have to rediscover settings_dir for test and teardown + return [settings_dir], {} + +def get_parent_path_teardown(settings_dir): + try: + shutil.rmtree(settings_dir, ignore_errors = True) + #restore previous settings dir if any + old_dir = settings_dir + ".tmp" + if os.path.exists(old_dir): + os.rename(old_dir, settings_dir) + except: + #cleanup is best effort + pass + +@with_setup_args.with_setup_args(get_parent_path_setup, get_parent_path_teardown) +def test_get_parent_path(settings_dir): + key = update_manager.get_platform_key() + got_settings_dir = update_manager.get_parent_path(key) + + assert settings_dir, "test_get_parent_path failed to obtain parent path" + + assert_equal(settings_dir, got_settings_dir) + +
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/test_get_platform_key.py b/indra/viewer_components/manager/tests/test_get_platform_key.py new file mode 100644 index 0000000000..37c570532c --- /dev/null +++ b/indra/viewer_components/manager/tests/test_get_platform_key.py @@ -0,0 +1,40 @@ +#!/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$ + +""" +@file test_get_platform_key.py +@author coyot +@date 2016-06-01 +""" + +from nose.tools import assert_equal + +import platform +import update_manager + +def test_get_platform_key(): + key = update_manager.get_platform_key() + if key == 'mac': + assert_equal(platform.system(),'Darwin') + elif key == 'lnx': + assert_equal(platform.system(),'Linux') + elif key == 'win': + assert_equal(platform.system(),'Windows') + else: + assert_equal(key, None) +
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/test_get_settings.py b/indra/viewer_components/manager/tests/test_get_settings.py new file mode 100644 index 0000000000..46b779cbd5 --- /dev/null +++ b/indra/viewer_components/manager/tests/test_get_settings.py @@ -0,0 +1,82 @@ +#!/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$ + +""" +@file test_get_settings.py +@author coyot +@date 2016-06-03 +""" + +from nose.tools import * +from nose import with_setup + +import os +import shutil +import update_manager +import with_setup_args + +def get_settings_setup(): + try: + key = update_manager.get_platform_key() + settings_dir = os.path.join(update_manager.get_parent_path(key), "user_settings") + print settings_dir + + #preserve existing settings dir if any + if os.path.exists(settings_dir): + old_dir = settings_dir + ".tmp" + if os.path.exists(old_dir): + shutil.rmtree(old_dir, ignore_errors = True) + os.rename(settings_dir, old_dir) + os.makedirs(settings_dir) + + #the data subdir of the tests dir that this script is in + data_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") + #the test settings file + settings_file = os.path.join(data_dir, "settings.xml") + shutil.copyfile(settings_file, os.path.join(settings_dir, "settings.xml")) + + except Exception, e: + print "get_settings_setup failed due to: %s" % str(e) + assert False + + #this is we don't have to rediscover settings_dir for test and teardown + return [settings_dir], {} + +def get_settings_teardown(settings_dir): + try: + shutil.rmtree(settings_dir, ignore_errors = True) + #restore previous settings dir if any + old_dir = settings_dir + ".tmp" + if os.path.exists(old_dir): + os.rename(old_dir, settings_dir) + except: + #cleanup is best effort + pass + +@with_setup_args.with_setup_args(get_settings_setup, get_settings_teardown) +def test_get_settings(settings_dir): + key = update_manager.get_platform_key() + parent = update_manager.get_parent_path(key) + log_file = update_manager.get_log_file_handle(parent) + got_settings = update_manager.get_settings(log_file, parent) + + assert got_settings, "test_get_settings failed to find a settings.xml file" + + #test one key just to make sure it parsed + assert_equal(got_settings['CurrentGrid']['Value'], 'util.agni.lindenlab.com') + diff --git a/indra/viewer_components/manager/tests/test_make_VVM_UUID_hash.py b/indra/viewer_components/manager/tests/test_make_VVM_UUID_hash.py new file mode 100644 index 0000000000..513502a6ca --- /dev/null +++ b/indra/viewer_components/manager/tests/test_make_VVM_UUID_hash.py @@ -0,0 +1,42 @@ +#!/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$ + +""" +@file test_make_VVM_UUID_hash.py +@author coyot +@date 2016-06-03 +""" + +from nose.tools import * + +import update_manager + +def test_make_VVM_UUID_hash(): + #because the method returns different results on different hosts + #it is not easy to unit test it reliably. + #About the best we can do is check for the exception from subprocess + key = update_manager.get_platform_key() + try: + UUID_hash = update_manager.make_VVM_UUID_hash(key) + except Exception, e: + print "Test failed due to: %s" % str(e) + assert False + + #make_UUID_hash returned None + assert UUID_hash, "make_UUID_hash failed to make a hash." + diff --git a/indra/viewer_components/manager/tests/test_make_download_dir.py b/indra/viewer_components/manager/tests/test_make_download_dir.py new file mode 100644 index 0000000000..5198a6de05 --- /dev/null +++ b/indra/viewer_components/manager/tests/test_make_download_dir.py @@ -0,0 +1,42 @@ +#!/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$ + +""" +@file test_make_download_dir.py +@author coyot +@date 2016-06-03 +""" + +from nose.tools import * + +import update_manager + +def test_make_download_dir(): + key = update_manager.get_platform_key() + path = update_manager.get_parent_path(key) + version = '1.2.3.456789' + try: + download_dir = update_manager.make_download_dir(path, version) + except OSError, e: + print "make_download_dir failed to eat OSError %s" % str(e) + assert False + except Exception, e: + print "make_download_dir raised an unexpected exception %s" % str(e) + assert False + + assert download_dir, "make_download_dir returned None for path %s and version %s" % (path, version)
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/test_query_vvm.py b/indra/viewer_components/manager/tests/test_query_vvm.py new file mode 100644 index 0000000000..40a0f7b215 --- /dev/null +++ b/indra/viewer_components/manager/tests/test_query_vvm.py @@ -0,0 +1,67 @@ +#!/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$ + +""" +@file test_query_vvm.py +@author coyot +@date 2016-06-08 +""" + +from nose.tools import * + +import os +import re +import shutil +import tempfile +import update_manager +import with_setup_args + +def query_vvm_setup(): + tmpdir1 = tempfile.mkdtemp(prefix = 'test1') + handle = update_manager.get_log_file_handle(tmpdir1) + + return [tmpdir1,handle], {} + +def query_vvm_teardown(tmpdir1, handle): + shutil.rmtree(tmpdir1, ignore_errors = True) + +@with_setup_args.with_setup_args(query_vvm_setup, query_vvm_teardown) +def test_query_vvm(tmpdir1, handle): + key = update_manager.get_platform_key() + parent = update_manager.get_parent_path(key) + settings = update_manager.get_settings(handle, parent) + launcher_path = os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__)))) + summary = update_manager.get_summary(key, launcher_path) + + #for unit testing purposes, just testing a value from results. If no update, then None and it falls through + #for formal QA see: + # https://docs.google.com/document/d/1WNjOPdKlq0j_7s7gdNe_3QlyGnQDa3bFNvtyVM6Hx8M/edit + # https://wiki.lindenlab.com/wiki/Login_Test#Test_Viewer_Updater + #for test plans on all cases, as it requires setting up a fake VVM service + + try: + results = update_manager.query_vvm(handle, key, settings, summary) + except Exception, e: + print "query_vvm threw unexpected exception %s" % str(e) + assert False + + if results: + pattern = re.compile('Second Life') + assert pattern.search(results['channel']), "Bad results returned %s" % str(results) + + assert True diff --git a/indra/viewer_components/manager/tests/test_silent_write.py b/indra/viewer_components/manager/tests/test_silent_write.py new file mode 100644 index 0000000000..a55665799f --- /dev/null +++ b/indra/viewer_components/manager/tests/test_silent_write.py @@ -0,0 +1,43 @@ +#!/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$ + +""" +@file test_silent_write.py +@author coyot +@date 2016-06-02 +""" + +from nose.tools import * + +import tempfile +import update_manager + +def test_silent_write_to_file(): + test_log = tempfile.TemporaryFile() + try: + update_manager.silent_write(test_log, "This is a test.") + except Exception, e: + print "Test failed due to: %s" % str(e) + assert False + +def test_silent_write_to_null(): + try: + update_manager.silent_write(None, "This is a test.") + except Exception, e: + print "Test failed due to: %s" % str(e) + assert False
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/test_summary.py b/indra/viewer_components/manager/tests/test_summary.py new file mode 100644 index 0000000000..b318012b54 --- /dev/null +++ b/indra/viewer_components/manager/tests/test_summary.py @@ -0,0 +1,39 @@ +#!/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$ + +""" +@file test_summary.py +@author coyot +@date 2016-06-02 +""" + +from nose.tools import * + +import os.path +import tempfile +import update_manager + +def test_get_summary(): + key = update_manager.get_platform_key() + #launcher is one dir above tests + launcher_path = os.path.dirname(os.path.dirname(os.path.abspath(os.path.realpath(__file__)))) + summary_json = update_manager.get_summary(key, launcher_path) + + #we aren't testing the JSON library, one key pair is enough + #so we will use the one pair that is actually a constant + assert_equal(summary_json['Type'],'viewer')
\ No newline at end of file diff --git a/indra/viewer_components/manager/tests/with_setup_args.py b/indra/viewer_components/manager/tests/with_setup_args.py new file mode 100644 index 0000000000..38350ab8c4 --- /dev/null +++ b/indra/viewer_components/manager/tests/with_setup_args.py @@ -0,0 +1,66 @@ +#!/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$ +# +# Taken from: https://gist.github.com/garyvdm/392ae20c673c7ee58d76 + +""" +@file with_setup_args.py +@author garyvdm +@date 2016-06-02 +""" + + +def with_setup_args(setup, teardown=None): + """Decorator to add setup and/or teardown methods to a test function:: + @with_setup_args(setup, teardown) + def test_something(): + " ... " + The setup function should return (args, kwargs) which will be passed to + test function, and teardown function. + Note that `with_setup_args` is useful *only* for test functions, not for test + methods or inside of TestCase subclasses. + """ + def decorate(func): + args = [] + kwargs = {} + + def test_wrapped(): + func(*args, **kwargs) + + test_wrapped.__name__ = func.__name__ + + def setup_wrapped(): + a, k = setup() + args.extend(a) + kwargs.update(k) + if hasattr(func, 'setup'): + func.setup() + test_wrapped.setup = setup_wrapped + + if teardown: + def teardown_wrapped(): + if hasattr(func, 'teardown'): + func.teardown() + teardown(*args, **kwargs) + + test_wrapped.teardown = teardown_wrapped + else: + if hasattr(func, 'teardown'): + test_wrapped.teardown = func.teardown() + return test_wrapped + return decorate
\ No newline at end of file diff --git a/indra/viewer_components/manager/update_manager.py b/indra/viewer_components/manager/update_manager.py new file mode 100755 index 0000000000..ff0f69a1b1 --- /dev/null +++ b/indra/viewer_components/manager/update_manager.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python + +"""\ +@file update_manager.py +@author coyot +@date 2016-05-16 +@brief executes viewer update checking and manages downloading and applying of updates + +$LicenseInfo:firstyear=2016&license=viewerlgpl$ +Second Life Viewer Source Code +Copyright (C) 2016, Linden Research, Inc. + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; +version 2.1 of the License only. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA +$/LicenseInfo$ +""" + +from llbase import llrest +from llbase.llrest import RESTError +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.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(timeout, 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 + print str(parent_dir) + try: + settings_file = os.path.abspath(os.path.join(parent_dir,'user_settings','settings.xml')) + print "Settings file: " + str(settings_file) + 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 = None, platform_key = None, settings = None, summary_dict = None, UpdaterServiceURL = None, UpdaterWillingToTest = None): + 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 + if UpdaterServiceURL: + baseURI = UpdaterServiceURL + else: + base_URI = 'https://update.secondlife.com/update/' + channelname = summary_dict['Channel'] + #this is kind of a mess because the settings value is 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> + """ + if UpdaterWillingToTest is not None: + if UpdaterWillingToTest: + test_ok = 'testok' + else: + test_ok = 'testno' + else: + try: + test_ok = settings['test']['Value'] + except KeyError: + #normal case, no testing key + test_ok = 'testok' + UUID = make_VVM_UUID_hash(platform_key) + print repr(channelname) + print repr(version) + print repr(platform_key) + print repr(platform_version) + print repr(test_ok) + print repr(UUID) + #because urljoin can't be arsed to take multiple elements + #channelname is a list because although it can only be one word, it is a kind of argument and viewer args can take multiple keywords. + query_string = '/v1.0/' + channelname[0] + '/' + 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(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, chunk_size = 1024): + 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, chunk_size = chunk_size) + 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 --chunk_size = %s" % (url, download_dir, size, chunk_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, chunk_size = 1024): + #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, chunk_size = chunk_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(cli_overrides = None): + #cli_overrides is a dict where the keys are specific parameters of interest and the values are the arguments to + #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) + settings = None + print "cli_overrides: " + str(cli_overrides) + + #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) + + if cli_overrides is not None: + if '--settings' in cli_overrides.keys(): + if cli_overrides['--settings'] is not None: + settings = get_settings(log_file_handle, cli_overrides['--settings']) + else: + 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 + """ssh://hg@bitbucket.org/lindenlab/viewer-release-maint-6585 + <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> + """ + if cli_overrides is not None: + if '--set' in cli_overrides.keys(): + if 'UpdaterServiceSetting' in cli_overrides['--set'].keys(): + install_automatically = cli_overrides['--set']['UpdaterServiceSetting'] + else: + 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 + + #use default chunk size if none is given + if cli_overrides is not None: + if '--set' in cli_overrides.keys(): + if 'UpdaterMaximumBandwidth' in cli_overrides['--set'].keys(): + chunk_size = cli_overrides['--set']['UpdaterMaximumBandwidth'] + else: + chunk_size = 1024 + + #get channel and version + try: + summary_dict = get_summary(platform_key, os.path.abspath(os.path.realpath(__file__))) + if cli_overrides is not None: + if 'channel' in cli_overrides.keys(): + summary_dict['Channel'] = cli_overrides['channel'] + except Exception, e: + silent_write(log_file_handle, "Could not obtain channel and version, exiting.") + silent_write(log_file_handle, e.message) + 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. + if cli_overrides is not None: + if '--update-service' in cli_overrides.keys(): + UpdaterServiceURL = cli_overrides['--update-service'] + else: + #tells query_vvm to use the default + UpdaterServiceURL = None + result_data = query_vvm(log_file_handle, platform_key, settings, summary_dict, UpdaterServiceURL) + #nothing to do or error + if not result_data: + silent_write(log_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, chunk_size = chunk_size) + 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, chunk_size = chunk_size) + 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() |