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