| Index: chrome_mac/Google Chrome.app/Contents/Versions/35.0.1916.114/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/install.py
|
| ===================================================================
|
| --- chrome_mac/Google Chrome.app/Contents/Versions/35.0.1916.114/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/install.py (revision 0)
|
| +++ chrome_mac/Google Chrome.app/Contents/Versions/35.0.1916.114/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/install.py (revision 0)
|
| @@ -0,0 +1,1446 @@
|
| +#!/usr/bin/python
|
| +# Copyright 2008 Google Inc. All rights reserved.
|
| +
|
| +"""This script will install Keystone in the correct context
|
| +(system-wide or per-user). It can also uninstall Keystone. is run by
|
| +KeystoneRegistration.framework.
|
| +
|
| +Example command lines for testing:
|
| +Install: install.py --install=/tmp/Keystone.tbz --root=/Users/fred
|
| +Uninstall: install.py --nuke --root=/Users/fred
|
| +
|
| +Example real command lines, for user and root install and uninstall:
|
| + install.py --install Keystone.tbz
|
| + install.py --nuke
|
| + sudo install.py --install Keystone.tbz
|
| + sudo install.py --nuke
|
| +
|
| +For a system-wide Keystone, the install root is "/". Run with --help
|
| +for a list of options. Use --no-launchdjobs to NOT start background
|
| +processes.
|
| +
|
| +Errors can happen if:
|
| + - we don't have write permission to install in the given root
|
| + - pieces of our install are missing
|
| +
|
| +On error, we print a simple message on stdout and our exit status is
|
| +non-zero. On success, we print nothing and exit with a status of 0.
|
| +"""
|
| +
|
| +import os
|
| +import re
|
| +import sys
|
| +import pwd
|
| +import stat
|
| +import glob
|
| +import getopt
|
| +import shutil
|
| +import platform
|
| +import fcntl
|
| +import traceback
|
| +
|
| +
|
| +# Allow us to force the installer to think we're on Tiger (10.4)
|
| +FORCE_TIGER = False
|
| +
|
| +# Allow us to adjust the agent launch interval (for testing).
|
| +# In seconds. Time is 1 hour minus a jitter factor.
|
| +AGENT_START_INTERVAL = 3523
|
| +
|
| +# Name of our "lockdown" ticket. If you change this name be sure to
|
| +# change it in other places in the code (grep is your friend)
|
| +LOCKDOWN_TICKET = 'com.google.Keystone.Lockdown'
|
| +
|
| +# Process that we consider a marker of a running user login session
|
| +USER_SESSION_PROCESSNAME = \
|
| + ' /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder'
|
| +
|
| +# URL for our Omaha server
|
| +OMAHA_SERVER_URL = 'https://tools.google.com/service/update2'
|
| +
|
| +
|
| +# Error codes. These need to be changed in sync with KSInstallErrors.h
|
| +# and (potentially) KSAdministration/KSInstallation
|
| +UNKNOWN_ERROR_CODE = -1
|
| +MASTER_DISABLE_ERROR_CODE = -2
|
| +# Error 1 reserved for generic errors from this script.
|
| +GENERIC_ERROR_CODE = 1
|
| +PACKAGE_ERROR_CODE = 2
|
| +UNSUPPORTED_OS_ERROR_CODE = 3
|
| +# These are internal and reported with a generic error message and their value
|
| +# in a nested error.
|
| +USAGE_ERROR_CODE = 64
|
| +BAD_ROOT_ERROR_CODE = 65
|
| +INSTALLED_VERSION_CHECK_ERROR_CODE = 66
|
| +PACKAGE_VERSION_CHECK_ERROR_CODE = 67
|
| +FILE_INSTALLATION_ERROR_CODE = 68
|
| +DAEMON_CONTROL_ERROR_CODE = 69
|
| +AGENT_CONTROL_ERROR_CODE = 70
|
| +AGENT_INSTALL_ERROR_CODE = 71
|
| +KSADMIN_MISSING_ERROR_CODE = 72
|
| +KEYSTONE_TICKET_ERROR_CODE = 73
|
| +
|
| +
|
| +class Error(Exception):
|
| + """Exception for Keystone install failure. Has methods for pretty printing
|
| + in a manner readable by our Obj-C wrapper code.
|
| + """
|
| +
|
| + def __init__(self, package, root, code, msg):
|
| + self.package = package
|
| + self.root = root
|
| + self.code = code
|
| + self.msg = msg
|
| +
|
| + def __str__(self):
|
| + return '%s (Package: %s Root: %s)' % (self.msg, self.package, self.root)
|
| +
|
| + def errorcode(self):
|
| + if self.code:
|
| + return self.code
|
| + else:
|
| + return UNKNOWN_ERROR_CODE
|
| +
|
| + def message(self):
|
| + return self.msg
|
| +
|
| +
|
| +def CheckOnePath(file, statmode, errorcode=UNKNOWN_ERROR_CODE):
|
| + """Sanity check a file or directory as requested. On failure throw
|
| + an exception."""
|
| + if os.path.exists(file):
|
| + st = os.stat(file)
|
| + if (st.st_mode & statmode) != 0:
|
| + return True
|
| + return False
|
| +
|
| +# -------------------------------------------------------------------------
|
| +
|
| +class KeystoneInstall(object):
|
| + """Worker object which does the heavy lifting of install or uninstall.
|
| + By default it assumes 10.5 (Leopard).
|
| +
|
| + Args:
|
| + package: The package to install (i.e. Keystone.tbz)
|
| + is_system: True if this is a system Keystone install
|
| + agent_job_uid: uid to start agent jobs as or None to use current euid
|
| + root: root directory for install. On System this would be "/";
|
| + else would be a user home directory (unless testing, in which case
|
| + the root can be anywhere).
|
| + launchd_setup: True if the installation should setup launchd job description
|
| + plists (and Tiger equivalents)
|
| + launchd_jobs: True if the installation should start/stop related jobs
|
| + self_destruct: True if uninstall is being triggered by a process the
|
| + uninstall is expected to kill
|
| +
|
| + Conventions:
|
| + All functions which return directory paths end in '/'
|
| + """
|
| +
|
| + def __init__(self, package, is_system, agent_job_uid, root,
|
| + launchd_setup, launchd_jobs, self_destruct):
|
| + self.package = package
|
| + self.is_system = is_system
|
| + self.agent_job_uid = agent_job_uid
|
| + if is_system:
|
| + assert agent_job_uid is not None, 'System install needs agent job uid'
|
| + self.root = root
|
| + if not self.root.endswith('/'):
|
| + self.root = self.root + '/'
|
| + self.launchd_setup = launchd_setup
|
| + self.launchd_jobs = launchd_jobs
|
| + self.self_destruct = self_destruct
|
| + self.cached_package_version = None
|
| + # Save/restore permissions
|
| + self.old_euid = None
|
| + self.old_egid = None
|
| + self.old_umask = None
|
| +
|
| + def RunCommand(self, cmd):
|
| + """Runs a command, returning return code and output.
|
| +
|
| + Returns:
|
| + Tuple of return value, stdout and stderr.
|
| + """
|
| + # We need to work in python 2.3 (OSX 10.4), 2.5 (10.5), and 2.6 (10.6)
|
| + if (sys.version_info[0] == 2) and (sys.version_info[1] <= 5):
|
| + # subprocess.communicate implemented the hard way
|
| + import errno
|
| + import popen2
|
| + import select
|
| + p = popen2.Popen3(cmd, True)
|
| + stdout = []
|
| + stderr = []
|
| + readable = [ p.fromchild, p.childerr ]
|
| + while not p.fromchild.closed or not p.childerr.closed:
|
| + try:
|
| + try_to_read = []
|
| + if not p.fromchild.closed:
|
| + try_to_read.append(p.fromchild)
|
| + if not p.childerr.closed:
|
| + try_to_read.append(p.childerr)
|
| + readable, ignored_w, ignored_x = select.select(try_to_read, [], [])
|
| + except select.error, e:
|
| + if e.args[0] == errno.EINTR:
|
| + continue
|
| + raise
|
| + if p.fromchild in readable:
|
| + out = os.read(p.fromchild.fileno(), 1024)
|
| + stdout.append(out)
|
| + if out == '':
|
| + p.fromchild.close()
|
| + if p.childerr in readable:
|
| + errout = os.read(p.childerr.fileno(), 1024)
|
| + stderr.append(errout)
|
| + if errout == '':
|
| + p.childerr.close()
|
| + result = p.wait()
|
| + return (os.WEXITSTATUS(result), ''.join(stdout), ''.join(stderr))
|
| + else:
|
| + # Just use subprocess, so much simpler
|
| + import subprocess
|
| + p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE,
|
| + stderr=subprocess.PIPE, close_fds=True)
|
| + (stdout, stderr) = p.communicate()
|
| + return (p.returncode, stdout, stderr)
|
| +
|
| + def _AgentProcessName(self):
|
| + """Return the process name of the agent."""
|
| + return 'GoogleSoftwareUpdateAgent'
|
| +
|
| + def _LibraryCachesDirPath(self):
|
| + """Return the Library/Caches subdirectory"""
|
| + return os.path.join(self.root, 'Library/Caches/')
|
| +
|
| + def _LibraryGoogleDirPath(self):
|
| + """Return the Library subdirectory that parents all our dirs"""
|
| + return os.path.join(self.root, 'Library/Google/')
|
| +
|
| + def _KeystoneDirPath(self):
|
| + """Return the subdirectory where Keystone.bundle is or will be.
|
| + Does not sanity check the directory."""
|
| + return os.path.join(self._LibraryGoogleDirPath(), 'GoogleSoftwareUpdate/')
|
| +
|
| + def _KeystoneBundlePath(self):
|
| + """Return the location of Keystone.bundle."""
|
| + return os.path.join(self._KeystoneDirPath(), 'GoogleSoftwareUpdate.bundle/')
|
| +
|
| + def _KeystoneTicketStorePath(self):
|
| + """Returns directory path of the Keystone ticket store."""
|
| + return os.path.join(self._KeystoneDirPath(), 'TicketStore')
|
| +
|
| + def _KsadminPath(self):
|
| + """Return a path to ksadmin which will exist only AFTER Keystone is
|
| + installed. Return None if it doesn't exist."""
|
| + ksadmin = os.path.join(self._KeystoneBundlePath(), 'Contents/MacOS/ksadmin')
|
| + if not os.path.exists(ksadmin):
|
| + return None
|
| + return ksadmin
|
| +
|
| + def _KeystoneResourcePath(self):
|
| + """Return the subdirectory where Keystone.bundle's resources should be."""
|
| + return os.path.join(self._KeystoneBundlePath(), 'Contents/Resources/')
|
| +
|
| + def _KeystoneAgentPath(self):
|
| + """Returns a path to installed KeystoneAgent.app."""
|
| + return os.path.join(self._KeystoneResourcePath(),
|
| + 'GoogleSoftwareUpdateAgent.app/')
|
| +
|
| + def _LaunchAgentConfigDir(self):
|
| + """Return the destination directory where launch agents should go."""
|
| + return os.path.join(self.root, 'Library/LaunchAgents/')
|
| +
|
| + def _LaunchDaemonConfigDir(self):
|
| + """Return the destination directory where launch daemons should go."""
|
| + return os.path.join(self.root, 'Library/LaunchDaemons/')
|
| +
|
| + def _AgentPlistFileName(self):
|
| + """Return the filename of the Keystone agent launchd plist or None."""
|
| + return 'com.google.keystone.agent.plist'
|
| +
|
| + def _DaemonPlistSourceFileName(self):
|
| + """Return the filename of the Keystone daemon launchd plist to install."""
|
| + return 'com.google.keystone.daemon.plist'
|
| +
|
| + def _DaemonPlistFileName(self):
|
| + """Return the filename of the post-install Keystone daemon launchd plist."""
|
| + return 'com.google.keystone.daemon.plist'
|
| +
|
| + def _IsMasterDisabled(self):
|
| + """Check for master disable MCX preference."""
|
| + # 'defaults' does not honor MCX, so read the file directly
|
| + if os.path.exists('/Library/Managed Preferences/com.google.Keystone.plist'):
|
| + cmd = ['/usr/bin/defaults', 'read',
|
| + '/Library/Managed Preferences/com.google.Keystone',
|
| + 'masterDisable']
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if result == 0 and out.strip() == '1':
|
| + return True
|
| + # Honor admin preferences too
|
| + if os.path.exists('/Library/Preferences/com.google.Keystone.plist'):
|
| + cmd = ['/usr/bin/defaults', 'read',
|
| + '/Library/Preferences/com.google.Keystone',
|
| + 'masterDisable']
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if result == 0 and out.strip() == '1':
|
| + return True
|
| + return False
|
| +
|
| + def _CFBundleVersionFromInfo(self, info):
|
| + """Given the content of an Info.plist return its CFBundleVersion."""
|
| + linelist = info.splitlines()
|
| + for i in range(len(linelist)):
|
| + if linelist[i].find('<key>CFBundleVersion</key>') != -1:
|
| + version = linelist[i+1].strip()
|
| + version = version.strip('<string>').strip('</string>')
|
| + if version:
|
| + return version
|
| + return None
|
| +
|
| + def InstalledKeystoneTicketVersion(self):
|
| + """Return the version the current Keystone ticket, or None if not installed.
|
| +
|
| + Invariant: we use the same a 4-digit version for the ticket as we use
|
| + for CFBundleVersion.
|
| + """
|
| + ksadmin_path = self._KsadminPath()
|
| + if not ksadmin_path or not os.path.exists(ksadmin_path):
|
| + return None
|
| + cmd = [ksadmin_path,
|
| + # store is specified explicitly so unit tests work
|
| + '--store', os.path.join(self._KeystoneTicketStorePath(),
|
| + 'Keystone.ticketstore'),
|
| + '--productid', 'com.google.Keystone',
|
| + '--print-tickets']
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if result:
|
| + return None
|
| + for line in out.splitlines():
|
| + if line.find('version=') != -1:
|
| + version = line.strip()
|
| + version = version.strip('version=')
|
| + if version:
|
| + return version
|
| + return None
|
| +
|
| + def InstalledKeystoneBundleVersion(self):
|
| + """Return the version of an installed Keystone bundle, or None if
|
| + not installed. Specifically, it returns the CFBundleVersion as a
|
| + string (e.g. "0.1.0.0").
|
| +
|
| + Invariant: we require a 4-digit version when building Keystone.bundle.
|
| + """
|
| + plist_path = os.path.join(self._KeystoneBundlePath(), 'Contents/Info.plist')
|
| + if not os.path.exists(plist_path):
|
| + return None
|
| + p = open(plist_path, 'r')
|
| + info = p.read()
|
| + p.close()
|
| + return self._CFBundleVersionFromInfo(info)
|
| +
|
| + def MyKeystoneBundleVersion(self):
|
| + """Return the version of our Keystone bundle which we might want to install.
|
| + Specifically, it returns the CFBundleVersion as a string (e.g. "0.1.0.0").
|
| +
|
| + Invariant: we require a 4-digit version when building Keystone.bundle.
|
| + """
|
| + if self.cached_package_version is None:
|
| + cmd = ['/usr/bin/tar', '-Oxjf',
|
| + self.package,
|
| + 'GoogleSoftwareUpdate.bundle/Contents/Info.plist']
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if result != 0:
|
| + raise Error(self.package, self.root, PACKAGE_VERSION_CHECK_ERROR_CODE,
|
| + 'Google Software Update installer unable to read package '
|
| + 'Info.plist: "%s"' % errout)
|
| + self.cached_package_version = self._CFBundleVersionFromInfo(out)
|
| + return self.cached_package_version
|
| +
|
| + def IsVersionGreaterThanVersion(self, a_version, b_version):
|
| + """Return True if a_version is greater than b_version.
|
| +
|
| + Invariant: we require a 4-digit version when building Keystone.bundle.
|
| + """
|
| + if a_version is None or b_version is None:
|
| + return True
|
| + else:
|
| + a_version = a_version.split('.')
|
| + b_version = b_version.split('.')
|
| + # Only correct for 4-digit versions, see invariants.
|
| + if len(a_version) != len(b_version):
|
| + return True
|
| + for a, b in zip(a_version, b_version):
|
| + if int(a) > int(b):
|
| + return True
|
| + elif int(a) < int(b):
|
| + return False
|
| + # If we get here, it's a complete match, so no.
|
| + return False
|
| +
|
| + def IsMyVersionGreaterThanInstalledVersion(self):
|
| + """Returns True if package Keystone version is greater than current install.
|
| +
|
| + Invariant: we require a 4-digit version when building Keystone.bundle.
|
| + """
|
| + my_version = self.MyKeystoneBundleVersion()
|
| + bundle_version = self.InstalledKeystoneBundleVersion()
|
| + if self.IsVersionGreaterThanVersion(my_version, bundle_version):
|
| + return True
|
| + ticket_version = self.InstalledKeystoneTicketVersion()
|
| + if self.IsVersionGreaterThanVersion(my_version, ticket_version):
|
| + return True
|
| + return False
|
| +
|
| + def _SetSystemInstallPermissions(self):
|
| + """Set permissions for system install, must pair with
|
| + _ClearSystemInstallPermissions(). Call before any filesystem access."""
|
| + assert (self.old_euid is None and self.old_egid is None and
|
| + self.old_umask is None), 'System permissions used reentrant'
|
| + self.old_euid = os.geteuid()
|
| + os.seteuid(0)
|
| + self.old_egid = os.getegid()
|
| + os.setegid(0)
|
| + self.old_umask = os.umask(022)
|
| +
|
| + def _ClearSystemInstallPermissions(self):
|
| + """Restore prior permissions after _SetSystemInstallPermissions()."""
|
| + assert (self.old_euid is not None and self.old_egid is not None and
|
| + self.old_umask is not None), 'System permissions cleared before set'
|
| + os.seteuid(self.old_euid)
|
| + self.old_euid = None
|
| + os.setegid(self.old_egid)
|
| + self.old_egid = None
|
| + os.umask(self.old_umask)
|
| + self.old_umask = None
|
| +
|
| + def _InstallPlist(self, source, dest_name, dest_dir):
|
| + """Install a copy of the plist from Resources to the dest_dir path using
|
| + dest_name. For system install, assumes you have already called
|
| + _SetSystemInstallPermissions().
|
| + """
|
| + try:
|
| + pf = open(os.path.join(self._KeystoneResourcePath(), source), 'r')
|
| + content = pf.read()
|
| + pf.close()
|
| + except IOError, e:
|
| + raise Error(self.package, self.root, PACKAGE_ERROR_CODE,
|
| + 'Google Software Update installer failed to read resource '
|
| + 'launchd plist "%s": %s' % (source, str(e)))
|
| + # This line is key. We can't have a tilde in a launchd script;
|
| + # we need an absolute path. So we replace a known token, like this:
|
| + # cat src.plist | 's/INSTALL_ROOT/self.root/g' > dest.plist
|
| + content = content.replace('${INSTALL_ROOT}', self.root)
|
| + content = content.replace(self.root + '/', self.root) # doubleslash remove
|
| + # Make sure launchd can distinguish between user and system Agents.
|
| + # This is a no-op for the daemon.
|
| + if self.is_system:
|
| + content = content.replace('${INSTALL_TYPE}', 'system')
|
| + else:
|
| + content = content.replace('${INSTALL_TYPE}', 'user')
|
| + # Allow start interval to be configured.
|
| + content = content.replace('${START_INTERVAL}', str(AGENT_START_INTERVAL))
|
| + try:
|
| + # Write to temp file then move in place (safe save)
|
| + target_file = os.path.join(dest_dir, dest_name)
|
| + target_tmp_file = target_file + '.tmp'
|
| + pf = open(target_tmp_file, 'w')
|
| + pf.write(content)
|
| + pf.close()
|
| + os.rename(target_tmp_file, target_file)
|
| + except IOError, e:
|
| + raise Error(self.package, self.root, FILE_INSTALLATION_ERROR_CODE,
|
| + 'Google Software Update installer failed to install launchd '
|
| + 'plist "%s": %s' % (os.path.join(dest_dir, dest_name),
|
| + str(e)))
|
| +
|
| + def _RemoveOldDaemonPlists(self):
|
| + """Remove older daemon plists installed using older names.
|
| + Assumes _SetSystemInstallPermissions() has been called."""
|
| + # In general we have nothing to do
|
| + pass
|
| +
|
| + def _InstallAgentLoginItem(self):
|
| + """Setup the agent login item (vs. launchd job).
|
| + Assumes _SetSystemInstallPermissions() has been called."""
|
| + pass
|
| +
|
| + def _RemoveAgentLoginItem(self):
|
| + """Remove the agent login item (vs. launchd job).
|
| + Assumes _SetSystemInstallPermissions() has been called.
|
| +
|
| + Note: We use this code on both Tiger and Leopard to handle the OS upgrade
|
| + case.
|
| + """
|
| + if self.is_system:
|
| + domain = '/Library/Preferences/loginwindow'
|
| + else:
|
| + domain = 'loginwindow'
|
| + (result, alaout, errout) = self.RunCommand(['/usr/bin/defaults', 'read',
|
| + domain, 'AutoLaunchedApplicationDictionary'])
|
| + # Ignoring result
|
| + if len(alaout.strip()) == 0:
|
| + alaout = '()'
|
| + # One line per loginitem to help us match
|
| + alaout = re.compile('[\n]+').sub('', alaout)
|
| + # handles case where we are the only item
|
| + alaout = alaout.replace('(', '(\n')
|
| + alaout = alaout.replace('}', '}\n')
|
| + needed_removal = False
|
| + for line in alaout.splitlines():
|
| + if line.find('/Library/Google/GoogleSoftwareUpdate/'
|
| + 'GoogleSoftwareUpdate.bundle/Contents/'
|
| + 'Resources/GoogleSoftwareUpdateAgent.app') != -1:
|
| + alaout = alaout.replace(line, '')
|
| + needed_removal = True
|
| + alaout = alaout.replace('\n', '')
|
| + # make sure it's a well-formed list
|
| + alaout = alaout.replace('(,', '(')
|
| + if needed_removal:
|
| + (result, out, errout) = self.RunCommand(['/usr/bin/defaults', 'write',
|
| + domain, 'AutoLaunchedApplicationDictionary', alaout])
|
| + # Ignore result, if we messed up the parse just move on.
|
| +
|
| + def _ChangeDaemonRunStatus(self, start, ignore_failure):
|
| + """Start or stop the daemon using launchd."""
|
| + assert self.is_system, 'Daemon start on non-system install'
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + if start:
|
| + action = 'load'
|
| + else:
|
| + action = 'unload'
|
| + job_path = os.path.join(self._LaunchDaemonConfigDir(),
|
| + self._DaemonPlistFileName())
|
| + # Workaround for incorrect Tiger installation
|
| + if os.path.exists(os.path.join(self._LaunchDaemonConfigDir(),
|
| + self._DaemonPlistSourceFileName())):
|
| + job_path = os.path.join(self._LaunchDaemonConfigDir(),
|
| + self._DaemonPlistSourceFileName())
|
| + (result, out, errout) = self.RunCommand(['/bin/launchctl', action,
|
| + job_path])
|
| + if not ignore_failure and result != 0:
|
| + raise Error(self.package, self.root, DAEMON_CONTROL_ERROR_CODE,
|
| + 'Google Software Update installer failed to %s daemon '
|
| + '(%d): %s' % (action, result, errout))
|
| + finally:
|
| + self._ClearSystemInstallPermissions()
|
| +
|
| + def _ChangeAgentRunStatus(self, start, ignore_failure):
|
| + """Start or stop the agent using launchd."""
|
| + if self._AgentPlistFileName() is None:
|
| + return
|
| + if start:
|
| + action = 'load'
|
| + search_process_name = USER_SESSION_PROCESSNAME
|
| + else:
|
| + action = 'unload'
|
| + search_process_name = self._AgentProcessName()
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + # System installation needs to use bsexec to hit all the running agents
|
| + (result, psout, pserr) = self.RunCommand(['/bin/ps', 'auxwww'])
|
| + if result != 0: # Internal problem so don't use ignore_failure
|
| + raise Error(self.package, self.root, UNKNOWN_ERROR_CODE,
|
| + 'Google Software Update installer could not run '
|
| + '/bin/ps: %s' % pserr)
|
| + for psline in psout.splitlines():
|
| + if psline.find(search_process_name) != -1:
|
| + username = psline.split()[0]
|
| + uid = pwd.getpwnam(username)[2]
|
| + # Must be root to bsexec.
|
| + # Must bsexec to (pid) to get in local user's context.
|
| + # Must become local user to have right process owner.
|
| + # Must unset SUDO_COMMAND to keep launchctl happy.
|
| + # Order is important.
|
| + agent_plist_path = os.path.join(self._LaunchAgentConfigDir(),
|
| + self._AgentPlistFileName())
|
| + (result, out, errout) = self.RunCommand([
|
| + '/bin/launchctl', 'bsexec', psline.split()[1],
|
| + '/usr/bin/sudo', '-u', username, '/bin/bash', '-c',
|
| + 'unset SUDO_COMMAND ; /bin/launchctl %s -S Aqua "%s"' % (
|
| + action,
|
| + os.path.join(self._LaunchAgentConfigDir(),
|
| + self._AgentPlistFileName()))])
|
| + # Although we're running for every user, only treat the requested
|
| + # user as an error
|
| + if not ignore_failure and result != 0 and uid == self.agent_job_uid:
|
| + raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
|
| + 'Google Software Update installer failed to %s agent for '
|
| + 'uid %d from plist "%s" (%d): %s' %
|
| + (action, self.agent_job_uid, agent_plist_path, result,
|
| + errout))
|
| + finally:
|
| + self._ClearSystemInstallPermissions()
|
| + else:
|
| + # Non-system variant requires basic launchctl commands
|
| + agent_plist_path = os.path.join(self._LaunchAgentConfigDir(),
|
| + self._AgentPlistFileName())
|
| + (result, out, errout) = self.RunCommand(['/bin/launchctl', action,
|
| + '-S', 'Aqua', agent_plist_path])
|
| + if not ignore_failure and result != 0:
|
| + raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
|
| + 'Google Software Update installer failed to %s agent from '
|
| + 'plist "%s" (%d): %s' %
|
| + (action, agent_plist_path, result, errout))
|
| +
|
| + def _ClearQuarantine(self, path):
|
| + """Remove LaunchServices quarantine attributes from a file hierarchy."""
|
| + # /usr/bin/xattr* are implemented in Python, and there's much magic
|
| + # around which of /usr/bin/xattr and the multiple /usr/bin/xattr-2.?
|
| + # actually execute. I suspect at least some users have /usr/bin/python
|
| + # linked to a "real" copy or otherwise replaced, so we're going to
|
| + # try a bunch of different options.
|
| + # Implement it ourself
|
| + try:
|
| + import xattr
|
| + for (root, dirs, files) in os.walk(path):
|
| + for name in files:
|
| + attrs = xattr.xattr(os.path.join(path, name))
|
| + try:
|
| + del attrs['com.apple.quarantine']
|
| + except KeyError:
|
| + pass
|
| + return # Success
|
| + except:
|
| + pass
|
| + # Use specific version by name in case /usr/bin/python isn't the Apple magic
|
| + # that selects the right copy of xattr. xattr-2.6 present on 10.6 and 10.7.
|
| + if os.path.exists('/usr/bin/xattr-2.6'):
|
| + (result, out, errout) = self.RunCommand(['/usr/bin/xattr-2.6', '-dr',
|
| + 'com.apple.quarantine', path])
|
| + if result == 0:
|
| + return
|
| + # Fall back to /usr/bin/xattr. On Leopard it doesn't support '-r' so
|
| + # recurse using find. Ignore the result, this is our last attempt.
|
| + self.RunCommand(['/usr/bin/find', '-x', path, '-exec', '/usr/bin/xattr',
|
| + '-d', 'com.apple.quarantine', '{}'])
|
| +
|
| + def Install(self):
|
| + """Perform a complete install operation, including safe upgrade"""
|
| + # Unload any current processes but ignore failures
|
| + if self.launchd_setup and self.launchd_jobs:
|
| + self._ChangeAgentRunStatus(False, True)
|
| + if self.is_system:
|
| + self._ChangeDaemonRunStatus(False, True)
|
| + # Install new files
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + # Make and protect base directories (always safe during upgrade)
|
| + if not os.path.isdir(self._KeystoneDirPath()):
|
| + os.makedirs(self._KeystoneDirPath())
|
| + if self.is_system:
|
| + os.chown(self._KeystoneDirPath(), 0, 0)
|
| + os.chmod(self._KeystoneDirPath(), 0755)
|
| + os.chown(self._LibraryGoogleDirPath(), 0, 0)
|
| + os.chmod(self._LibraryGoogleDirPath(), 0755)
|
| + # Unpack Keystone bundle. In an upgrade we want to try to restore
|
| + # to the old binary if install encounters a problem. Options flag names
|
| + # chosen to be compatible with both 10.4 and 10.6 (both BSD tar, but
|
| + # very different versions).
|
| + saved_bundle_path = self._KeystoneBundlePath().rstrip('/') + '.old'
|
| + if os.path.exists(self._KeystoneBundlePath()):
|
| + if os.path.isdir(saved_bundle_path):
|
| + shutil.rmtree(saved_bundle_path)
|
| + elif os.path.exists(saved_bundle_path):
|
| + os.unlink(saved_bundle_path)
|
| + os.rename(self._KeystoneBundlePath(), saved_bundle_path)
|
| + cmd = ['/usr/bin/tar', 'xjf', self.package, '--no-same-owner',
|
| + '-C', self._KeystoneDirPath()]
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if result != 0:
|
| + try:
|
| + if os.path.exists(saved_bundle_path):
|
| + os.rename(saved_bundle_path, self._KeystoneBundlePath())
|
| + finally:
|
| + raise Error(self.package, self.root, FILE_INSTALLATION_ERROR_CODE,
|
| + 'Google Software Update installer unable to unpack '
|
| + 'package: "%s"' % errout)
|
| + if os.path.exists(saved_bundle_path):
|
| + shutil.rmtree(saved_bundle_path)
|
| + # Clear quarantine on the new bundle. Failure is ignored, user will
|
| + # be prompted if quarantine is not cleared, but we will still operate
|
| + # correctly.
|
| + self._ClearQuarantine(self._KeystoneBundlePath())
|
| + # Create Keystone ticket store. On a system install start by checking
|
| + # ticket store permissions. Bad permissions on the store could be the
|
| + # result of a prior install or an attempt to poison the store.
|
| + if self.is_system and os.path.exists(self._KeystoneTicketStorePath()):
|
| + s = os.lstat(self._KeystoneTicketStorePath())
|
| + if (s[stat.ST_UID] == 0 and
|
| + (s[stat.ST_GID] == 0 or s[stat.ST_GID] == 80)):
|
| + pass
|
| + else:
|
| + if os.path.isdir(self._KeystoneTicketStorePath()):
|
| + shutil.rmtree(self._KeystoneTicketStorePath())
|
| + else:
|
| + os.unlink(self._KeystoneTicketStorePath())
|
| + # Now create and protect ticket store
|
| + if not os.path.isdir(self._KeystoneTicketStorePath()):
|
| + os.makedirs(self._KeystoneTicketStorePath())
|
| + if self.is_system:
|
| + os.chown(self._KeystoneTicketStorePath(), 0, 0)
|
| + os.chmod(self._KeystoneTicketStorePath(), 0755)
|
| + # Create/update Keystone ticket
|
| + ksadmin_path = self._KsadminPath()
|
| + if not ksadmin_path or not os.path.exists(ksadmin_path):
|
| + raise Error(self.package, self.root, KSADMIN_MISSING_ERROR_CODE,
|
| + 'Google Software Update installer ksadmin not available')
|
| + cmd = [ksadmin_path,
|
| + # store is specified explicitly so unit tests work
|
| + '--store', os.path.join(self._KeystoneTicketStorePath(),
|
| + 'Keystone.ticketstore'),
|
| + '--register',
|
| + '--productid', 'com.google.Keystone',
|
| + '--version', self.MyKeystoneBundleVersion(),
|
| + '--xcpath', self._KeystoneBundlePath(),
|
| + '--url', OMAHA_SERVER_URL,
|
| + '--preserve-tttoken']
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if result != 0:
|
| + raise Error(self.package, self.root, KEYSTONE_TICKET_ERROR_CODE,
|
| + 'Google Software Update installer Keystone ticket install failed '
|
| + '(%d): %s' % (result, errout))
|
| + # launchd config if requested
|
| + if self.launchd_setup:
|
| + # Daemon first (safer if upgrade fails)
|
| + if self.is_system:
|
| + if not os.path.isdir(self._LaunchDaemonConfigDir()):
|
| + os.makedirs(self._LaunchDaemonConfigDir())
|
| + # Again set permissions only if we created it, but if we did use
|
| + # standard permission from a default OS install.
|
| + os.chown(self._LaunchDaemonConfigDir(), 0, 0)
|
| + os.chmod(self._LaunchDaemonConfigDir(), 0755)
|
| + self._InstallPlist(self._DaemonPlistSourceFileName(),
|
| + self._DaemonPlistFileName(),
|
| + self._LaunchDaemonConfigDir())
|
| + # Remove daemon under older names
|
| + self._RemoveOldDaemonPlists()
|
| + # Agent launchd
|
| + if self._AgentPlistFileName() is not None:
|
| + if not os.path.isdir(self._LaunchAgentConfigDir()):
|
| + os.makedirs(self._LaunchAgentConfigDir())
|
| + # /Library/LaunchAgents is a OS directory, use permissions from
|
| + # default OS install, but only if we created it.
|
| + if self.is_system:
|
| + os.chown(self._LaunchAgentConfigDir(), 0, 0)
|
| + os.chmod(self._LaunchAgentConfigDir(), 0755)
|
| + self._InstallPlist(self._AgentPlistFileName(),
|
| + self._AgentPlistFileName(),
|
| + self._LaunchAgentConfigDir())
|
| + # Agent login item remove/restore. Removal prior to add
|
| + # so that removal happens in Tiger -> Leopard upgrade case and
|
| + # we do not duplicate entries.
|
| + self._RemoveAgentLoginItem()
|
| + self._InstallAgentLoginItem()
|
| + finally:
|
| + if self.is_system:
|
| + self._ClearSystemInstallPermissions()
|
| + # If requested, start our jobs, failures treated as errors.
|
| + if self.launchd_setup and self.launchd_jobs:
|
| + if self.is_system:
|
| + self._ChangeDaemonRunStatus(True, False)
|
| + self._ChangeAgentRunStatus(True, False)
|
| +
|
| + def LockdownKeystone(self):
|
| + """Prevent Keystone from ever self-uninstalling.
|
| +
|
| + This is necessary for a System Keystone used for Trusted Tester support.
|
| + We do this by installing (and never uninstalling) a system ticket.
|
| + """
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + ksadmin_path = self._KsadminPath()
|
| + if not ksadmin_path:
|
| + raise Error(self.package, self.root, KSADMIN_MISSING_ERROR_CODE,
|
| + 'Google Software Update installer ksadmin not available')
|
| + cmd = [ksadmin_path,
|
| + # store is specified explicitly so unit tests work
|
| + '--store', os.path.join(self._KeystoneTicketStorePath(),
|
| + 'Keystone.ticketstore'),
|
| + '--register',
|
| + '--productid', LOCKDOWN_TICKET,
|
| + '--version', '1.0',
|
| + '--xcpath', '/',
|
| + '--url', OMAHA_SERVER_URL]
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if result != 0:
|
| + raise Error(self.package, self.root, KEYSTONE_TICKET_ERROR_CODE,
|
| + 'Google Software Update installer Keystone ticket install '
|
| + 'failed (%d): %s' % (result, errout))
|
| + finally:
|
| + if self.is_system:
|
| + self._ClearSystemInstallPermissions()
|
| +
|
| + def Uninstall(self):
|
| + """Perform a complete uninstall (uninstall leaves tickets in place)"""
|
| + # On uninstall if we are not in self-destruct stop all processes but
|
| + # ignore failure (may not be running). On a non-self destruct case we do
|
| + # this first since it avoids race conditions on caches and pref writes
|
| + if not self.self_destruct and self.launchd_setup and self.launchd_jobs:
|
| + self._ChangeAgentRunStatus(False, True)
|
| + if self.is_system:
|
| + self._ChangeDaemonRunStatus(False, True)
|
| + # Perform file removals. In self-destruct case the processes may still
|
| + # be running.
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + # Remove plist files unless blocked
|
| + if self.launchd_setup:
|
| + # In self-destruct mode we still need these plists for launchctl
|
| + if not self.self_destruct:
|
| + if self._AgentPlistFileName() is not None:
|
| + agent_plist = os.path.join(self._LaunchAgentConfigDir(),
|
| + self._AgentPlistFileName())
|
| + if os.path.exists(agent_plist):
|
| + os.unlink(agent_plist)
|
| + daemon_plist = os.path.join(self._LaunchDaemonConfigDir(),
|
| + self._DaemonPlistFileName())
|
| + if os.path.exists(daemon_plist):
|
| + os.unlink(daemon_plist)
|
| + # Remove daemon under older names as well
|
| + self._RemoveOldDaemonPlists()
|
| + # Self-destruct or not, we can remove login item (Tiger)
|
| + self._RemoveAgentLoginItem()
|
| + # Unregister Keystone ticket (if installed at all).
|
| + if os.path.exists(self._KeystoneBundlePath()):
|
| + ksadmin_path = self._KsadminPath()
|
| + if not ksadmin_path or not os.path.exists(ksadmin_path):
|
| + raise Error(self.package, self.root, KSADMIN_MISSING_ERROR_CODE,
|
| + 'Google Software Update installer ksadmin not available')
|
| + cmd = [ksadmin_path,
|
| + # store is specified explicitly so unit tests work
|
| + '--store', os.path.join(self._KeystoneTicketStorePath(),
|
| + 'Keystone.ticketstore'),
|
| + '--delete', '--productid', 'com.google.Keystone']
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if result != 0 and errout.find('No ticket to delete') == -1:
|
| + raise Error(self.package, self.root, KEYSTONE_TICKET_ERROR_CODE,
|
| + 'Google Software Update installer Keystone ticket uninstall '
|
| + 'failed (%d): %s' % (result, errout))
|
| + # Remove the Keystone bundle
|
| + if os.path.exists(self._KeystoneBundlePath()):
|
| + shutil.rmtree(self._KeystoneBundlePath())
|
| + # Clean up caches. Race condition here if self-destructing, but unlikely
|
| + # and we'll just leak a cache dir.
|
| + if os.path.exists(self._LibraryCachesDirPath()):
|
| + caches = glob.glob(os.path.join(self._LibraryCachesDirPath(),
|
| + 'com.google.Keystone.*'))
|
| + caches.extend(glob.glob(os.path.join(self._LibraryCachesDirPath(),
|
| + 'com.google.UpdateEngine.*')))
|
| + caches.extend(glob.glob(os.path.join(self._LibraryCachesDirPath(),
|
| + 'UpdateEngine-Temp')))
|
| + for cache_item in caches:
|
| + if os.path.isdir(cache_item):
|
| + shutil.rmtree(cache_item, True) # Ignore cache deletion errors
|
| + else:
|
| + try:
|
| + os.unlink(cache_item)
|
| + except OSError:
|
| + pass
|
| + # Clean up preferences, this prevents old installations from propagating
|
| + # dates (like uninstall embargo time) forward in a complete uninstall/
|
| + # reinstall scenario. Again, race condition here for self-destruct case
|
| + # but the risk is minor and only leaks a pref file.
|
| + if self.is_system:
|
| + agent_pref_path = os.path.join(pwd.getpwuid(self.agent_job_uid)[5],
|
| + 'Library/Preferences/'
|
| + 'com.google.Keystone.Agent.plist')
|
| + else:
|
| + agent_pref_path = os.path.expanduser('~/Library/Preferences/'
|
| + 'com.google.Keystone.Agent.plist')
|
| + if os.path.exists(agent_pref_path):
|
| + os.unlink(agent_pref_path)
|
| + finally:
|
| + if self.is_system:
|
| + self._ClearSystemInstallPermissions()
|
| + # Remove receipts
|
| + self.RemoveReceipts()
|
| + # With all other files removed, cleanup processes and job control files in
|
| + # the self-destruct case. This will presumably kill our parent, so after
|
| + # this no one is listening for our errors. We do it as late as possible.
|
| + if self.self_destruct:
|
| + if self.launchd_setup and self.launchd_jobs:
|
| + self._ChangeAgentRunStatus(False, True)
|
| + if self.is_system:
|
| + self._ChangeDaemonRunStatus(False, True)
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + # We needed these plists to stop the agent and daemon. No one is
|
| + # listening to errors, but failure only leaves a stale launchctl file
|
| + # (actual program files removed above)
|
| + if self._AgentPlistFileName() is not None:
|
| + agent_plist = os.path.join(self._LaunchAgentConfigDir(),
|
| + self._AgentPlistFileName())
|
| + if os.path.exists(agent_plist):
|
| + os.unlink(agent_plist)
|
| + daemon_plist = os.path.join(self._LaunchDaemonConfigDir(),
|
| + self._DaemonPlistFileName())
|
| + if os.path.exists(daemon_plist):
|
| + os.unlink(daemon_plist)
|
| + # Remove daemon under older names as well
|
| + self._RemoveOldDaemonPlists()
|
| + finally:
|
| + if self.is_system:
|
| + self._ClearSystemInstallPermissions()
|
| +
|
| + def Nuke(self):
|
| + """Perform an uninstall and remove all files (including tickets)"""
|
| + # Uninstall
|
| + self.Uninstall()
|
| + # Nuke what's left
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + # Remove whole Keystone tree
|
| + if os.path.exists(self._KeystoneDirPath()):
|
| + shutil.rmtree(self._KeystoneDirPath())
|
| + finally:
|
| + if self.is_system:
|
| + self._ClearSystemInstallPermissions()
|
| +
|
| + def RemoveReceipts(self):
|
| + """Remove receipts from Apple's package database, allowing downgrade or
|
| + reinstall."""
|
| + # Only works on system installs
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + # In theory we should only handle old-style receipts on older OS
|
| + # versions. However, we don't know the upgrade history of the machine.
|
| + # So we try all variants.
|
| + if os.path.isdir('/Library/Receipts/Keystone.pkg'):
|
| + shutil.rmtree('/Library/Receipts/Keystone.pkg', True)
|
| + if os.path.exists('/Library/Receipts/Keystone.pkg'):
|
| + try:
|
| + os.unlink('/Library/Receipts/Keystone.pkg')
|
| + except OSError:
|
| + pass
|
| + if os.path.isdir('/Library/Receipts/UninstallKeystone.pkg'):
|
| + shutil.rmtree('/Library/Receipts/UninstallKeystone.pkg', True)
|
| + if os.path.exists('/Library/Receipts/UninstallKeystone.pkg'):
|
| + try:
|
| + os.unlink('/Library/Receipts/UninstallKeystone.pkg')
|
| + except OSError:
|
| + pass
|
| + if os.path.isdir('/Library/Receipts/NukeKeystone.pkg'):
|
| + shutil.rmtree('/Library/Receipts/NukeKeystone.pkg', True)
|
| + if os.path.exists('/Library/Receipts/NukeKeystone.pkg'):
|
| + try:
|
| + os.unlink('/Library/Receipts/NukeKeystone.pkg')
|
| + except OSError:
|
| + pass
|
| + # pkgutil where appropriate (ignoring results)
|
| + if os.path.exists('/usr/sbin/pkgutil'):
|
| + self.RunCommand(['/usr/sbin/pkgutil', '--forget',
|
| + 'com.google.pkg.Keystone'])
|
| + self.RunCommand(['/usr/sbin/pkgutil', '--forget',
|
| + 'com.google.pkg.UninstallKeystone'])
|
| + self.RunCommand(['/usr/sbin/pkgutil', '--forget',
|
| + 'com.google.pkg.NukeKeystone'])
|
| + finally:
|
| + self._ClearSystemInstallPermissions()
|
| +
|
| + def FixupProducts(self):
|
| + """Attempt to repair any products might be broken."""
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + # Remove the (original) Google Updater manifest files. Stale manifest
|
| + # caches prevent Updater from checking for updates and downloading its
|
| + # auto-uninstall package. Stomp those files everywhere we can.
|
| + try:
|
| + os.unlink(os.path.expanduser('~/Library/Application Support/'
|
| + 'Google/SoftwareUpdates/manifest.xml'))
|
| + except OSError:
|
| + pass
|
| + if self.agent_job_uid is not None:
|
| + try:
|
| + os.unlink(os.path.join(pwd.getpwuid(self.agent_job_uid)[5],
|
| + 'Library/Application Support/'
|
| + 'Google/SoftwareUpdates/manifest.xml'))
|
| + except OSError:
|
| + pass
|
| + try:
|
| + os.unlink(os.path.join(self.root, 'Library/Application Support/'
|
| + 'Google/SoftwareUpdates/manifest.xml'))
|
| + except OSError:
|
| + pass
|
| + try:
|
| + os.unlink('/Library/Caches/Google/SoftwareUpdates/manifest.xml')
|
| + except OSError:
|
| + pass
|
| +
|
| + # Other repairs require ksadmin
|
| + ksadmin_path = self._KsadminPath()
|
| + if ksadmin_path and os.path.exists(ksadmin_path):
|
| +
|
| + # Fix various Talk plugin problems
|
| + if self.is_system:
|
| + (result, out, errout) = self.RunCommand([ksadmin_path, '--productid',
|
| + 'com.google.talkplugin', '-p'])
|
| +
|
| + # Google Talk Plugin 1.0.15.1351 can have its existence checker
|
| + # pointing to a deleted directory. Fix up the xc so it'll update
|
| + # next time.
|
| + if out.find('1.0.15.1351') != -1:
|
| + # Fix the ticket by reregistering it.
|
| + # We can only get here if 1.0.15.1351 is the current version, so
|
| + # it's safe to use that version.
|
| + (result, out, errout) = self.RunCommand([ksadmin_path, '--register',
|
| + '--productid', 'com.google.talkplugin',
|
| + '--xcpath',
|
| + '/Library/Internet Plug-Ins/googletalkbrowserplugin.plugin',
|
| + '--version', '1.0.15.1351',
|
| + '--url', OMAHA_SERVER_URL])
|
| +
|
| + # Repair tickets presumed lost in the 1.x to 2.x Talk upgrade.
|
| + if ((out.find('productID=com.google.talkplugin') == -1) and
|
| + os.path.exists(
|
| + '/Library/Internet Plug-Ins/googletalkbrowserplugin.plugin')):
|
| + # Register Talk again, using a unique version number that will be
|
| + # updated.
|
| + (result, out, errout) = self.RunCommand([ksadmin_path, '--register',
|
| + '--productid', 'com.google.talkplugin',
|
| + '--xcpath',
|
| + '/Library/Internet Plug-Ins/googletalkbrowserplugin.plugin',
|
| + '--version', '0.1.0.1234',
|
| + '--url', OMAHA_SERVER_URL])
|
| +
|
| + finally:
|
| + if self.is_system:
|
| + self._ClearSystemInstallPermissions()
|
| +
|
| +# -------------------------------------------------------------------------
|
| +
|
| +class KeystoneInstallTiger(KeystoneInstall):
|
| +
|
| + """Like KeystoneInstall, but overrides a few methods to support 10.4"""
|
| +
|
| + def _AgentPlistFileName(self):
|
| + return None
|
| +
|
| + def _DaemonPlistSourceFileName(self):
|
| + return 'com.google.keystone.daemon4.plist'
|
| +
|
| + def _RemoveOldDaemonPlists(self):
|
| + # Older installers installed this under the wrong name
|
| + daemon_plist = os.path.join(self._LaunchDaemonConfigDir(),
|
| + self._DaemonPlistSourceFileName())
|
| + if os.path.exists(daemon_plist):
|
| + os.unlink(daemon_plist)
|
| +
|
| + def _InstallAgentLoginItem(self):
|
| + # This will write to the Library domain as root/wheel, which is OK because
|
| + # permissions on /Library/Preferences still allow admin group to modify
|
| + if self.is_system:
|
| + domain = '/Library/Preferences/loginwindow'
|
| + else:
|
| + domain = 'loginwindow'
|
| + (result, out, errout) = self.RunCommand(
|
| + ['/usr/bin/defaults', 'write', domain,
|
| + 'AutoLaunchedApplicationDictionary', '-array-add',
|
| + '{Hide = 1; Path = "%s"; }' % self._KeystoneAgentPath()])
|
| + if result == 0:
|
| + return
|
| + # An empty AutoLaunchedApplicationDictionary is an empty string,
|
| + # not an empty array, in which case -array-add chokes. There is
|
| + # no easy way to do a typeof(AutoLaunchedApplicationDictionary)
|
| + # for a plist. Our solution is to catch the error and try a
|
| + # different way.
|
| + (result, out, errout) = self.RunCommand(
|
| + ['/usr/bin/defaults', 'write', domain,
|
| + 'AutoLaunchedApplicationDictionary', '-array',
|
| + '{Hide = 1; Path = "%s"; }' % self._KeystoneAgentPath()])
|
| + if result != 0:
|
| + raise Error(self.package, self.root, AGENT_INSTALL_ERROR_CODE,
|
| + 'Google Software Update installer Keystone agent login item '
|
| + 'in domain "%s" failed (%d): %s' % (domain, result, errout))
|
| +
|
| + def _ChangeAgentRunStatus(self, start, ignore_failure):
|
| + """Start the agent as a normal (non-launchd) process on Tiger."""
|
| + if self.is_system:
|
| + self._SetSystemInstallPermissions()
|
| + try:
|
| + # Start
|
| + if start:
|
| + if self.is_system:
|
| + # Tiger 'sudo' has problems with numeric uid so use username (man
|
| + # page wrong)
|
| + username = pwd.getpwuid(self.agent_job_uid)[0]
|
| + (result, out, errout) = self.RunCommand(['/usr/bin/sudo',
|
| + '-u', username,
|
| + '/usr/bin/open',
|
| + self._KeystoneAgentPath()])
|
| + if not ignore_failure and result != 0:
|
| + raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
|
| + 'Google Software Update installer failed to start '
|
| + 'system agent for uid %d (%d): %s' %
|
| + (self.agent_job_uid, result, errout))
|
| + else:
|
| + (result, out, errout) = self.RunCommand(['/usr/bin/open',
|
| + self._KeystoneAgentPath()])
|
| + if not ignore_failure and result != 0:
|
| + raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
|
| + 'Google Software Update installer failed to start '
|
| + 'user agent (%d): %s' % (result, errout))
|
| + # Stop
|
| + else:
|
| + if self.is_system:
|
| + cmd = ['/usr/bin/killall', '-u', str(self.agent_job_uid),
|
| + self._AgentProcessName()]
|
| + else:
|
| + cmd = ['/usr/bin/killall', self._AgentProcessName()]
|
| + (result, out, errout) = self.RunCommand(cmd)
|
| + if (not ignore_failure and result != 0 and
|
| + out.find('No matching processes') == -1):
|
| + raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
|
| + 'Google Software Update installer failed to kill '
|
| + 'agent (%d): %s' % (result, errout))
|
| + finally:
|
| + if self.is_system:
|
| + self._ClearSystemInstallPermissions()
|
| +
|
| + def _ClearQuarantine(self, path):
|
| + """Remove LaunchServices quarantine attributes from a file hierarchy."""
|
| + # Tiger does not implement quarantine (http://support.apple.com/kb/HT3662)
|
| + return
|
| +
|
| +
|
| +# -------------------------------------------------------------------------
|
| +
|
| +class Keystone(object):
|
| +
|
| + """Top-level interface for Keystone install and uninstall.
|
| +
|
| + Attributes:
|
| + install_class: KeystoneInstall subclass to use for installation
|
| + installer: KeystoneInstall instance (system or user)
|
| + """
|
| +
|
| + def __init__(self, package, root, launchd_setup, start_jobs, self_destruct):
|
| + # Sanity
|
| + if package:
|
| + package = os.path.abspath(os.path.expanduser(package))
|
| + if not CheckOnePath(package, stat.S_IRUSR):
|
| + raise Error(package, root, PACKAGE_ERROR_CODE,
|
| + 'Google Software Update installer missing or unreadable '
|
| + 'installation package.')
|
| + if root:
|
| + expanded_root = os.path.abspath(os.path.expanduser(root))
|
| + assert (expanded_root and
|
| + len(expanded_root) > 0), 'Root is empty after expansion.'
|
| + # Force user-supplied root to pre-exist, this was a side effect of
|
| + # prior versions of the code and the tests assume its part of the contract
|
| + if not CheckOnePath(expanded_root, stat.S_IWUSR):
|
| + raise Error(package, root, BAD_ROOT_ERROR_CODE,
|
| + 'Google Software Update installer installation location '
|
| + 'missing or unwritable.')
|
| + root = expanded_root
|
| +
|
| + # Setup installer instances
|
| + self.install_class = KeystoneInstall
|
| + if self._IsTiger():
|
| + self.install_class = KeystoneInstallTiger
|
| + if self._IsPrivilegedInstall():
|
| + # Install using privileges on behalf of other user (for agent start)
|
| + install_uid = self._LocalUserUID()
|
| + if root is not None:
|
| + self.installer = self.install_class(package, True, install_uid, root,
|
| + launchd_setup, start_jobs,
|
| + self_destruct)
|
| + else:
|
| + self.installer = self.install_class(package, True, install_uid,
|
| + self._DefaultRootForUID(0),
|
| + launchd_setup, start_jobs,
|
| + self_destruct)
|
| + else:
|
| + # Non-system install, no attempt at privilege changes
|
| + if root is not None:
|
| + self.installer = self.install_class(package, False, None, root,
|
| + launchd_setup, start_jobs,
|
| + self_destruct)
|
| + else:
|
| + self.installer = self.install_class(package, False, None,
|
| + self._DefaultRootForUID(
|
| + self._LocalUserUID()),
|
| + launchd_setup, start_jobs,
|
| + self_destruct)
|
| +
|
| + def _LocalUserUID(self):
|
| + """Return the UID of the local (non-root) user who initiated this
|
| + install/uninstall. If we can't figure it out, default to the user
|
| + on conosle. We don't want to default to console user in case a
|
| + FUS happens in the middle of install or uninstall."""
|
| + uid = os.geteuid()
|
| + if uid != 0:
|
| + return uid
|
| + else:
|
| + return os.stat('/dev/console')[stat.ST_UID]
|
| +
|
| + def _IsLeopardOrLater(self):
|
| + """Return True if we're on 10.5 or later; else return False."""
|
| + global FORCE_TIGER
|
| + if FORCE_TIGER:
|
| + return False
|
| + # Ouch! platform.mac_ver() returns strange results.
|
| + # ('10.7', ('', '', ''), 'i386') - 10.7, python2.7
|
| + # ('10.7.0', ('', '', ''), 'i386') - 10.7, python2.5 or python2.6
|
| + # ('10.6.7', ('', '', ''), 'i386') - 10.6, python2.5 or python2.6
|
| + # ('10.5.1', ('', '', ''), 'i386') - 10.5, python2.4 or python2.5
|
| + # ('', ('', '', ''), '') - 10.4, python2.3 (also 2.4)
|
| + (vers, ignored1, ignored2) = platform.mac_ver()
|
| + splits = vers.split('.')
|
| + # Try to break down a proper version number
|
| + if ((len(splits) == 2) or (len(splits) == 3)) and (splits[1] >= '5'):
|
| + return True
|
| + # Tiger is rare these days, so unless we're on 2.3 build of Python
|
| + # assume we must be newer.
|
| + if (((sys.version_info[0] == 2) and (sys.version_info[1] == 3)) or
|
| + ((sys.version_info[0] == 2) and (sys.version_info[1] == 4) and
|
| + (vers == ''))):
|
| + return False
|
| + else:
|
| + return True
|
| +
|
| + def _IsTiger(self):
|
| + """Return the boolean opposite of IsLeopardOrLater()."""
|
| + if self._IsLeopardOrLater():
|
| + return False
|
| + else:
|
| + return True
|
| +
|
| + def _IsPrivilegedInstall(self):
|
| + """Return True if this is a privileged (root) install."""
|
| + if os.geteuid() == 0:
|
| + return True
|
| + else:
|
| + return False
|
| +
|
| + def _DefaultRootForUID(self, uid):
|
| + """For the given UID, return the default install root for Keystone (where
|
| + is is, or where it should be, installed)."""
|
| + if uid == 0:
|
| + return '/'
|
| + else:
|
| + return pwd.getpwuid(uid)[5]
|
| +
|
| + def _ShouldInstall(self):
|
| + """Return True if we should on install.
|
| +
|
| + Possible reasons for punting (returning False):
|
| + 1) This is a System Keystone install and the installed System
|
| + Keystone has a smaller version.
|
| + 2) This is a User Keystone and there is a System Keystone
|
| + installed (of any version).
|
| + 3) This is a User Keystone and the installed User Keystone has a
|
| + smaller version.
|
| + """
|
| + if self._IsPrivilegedInstall():
|
| + if self.installer.IsMyVersionGreaterThanInstalledVersion():
|
| + return True
|
| + else:
|
| + return False
|
| + else:
|
| + # User install, need to check if system install exists
|
| + system_checker = self.install_class(None, False, None,
|
| + self._DefaultRootForUID(0),
|
| + False, False, False)
|
| + if system_checker.InstalledKeystoneBundleVersion() != None:
|
| + return False
|
| + # Check just user version
|
| + if self.installer.IsMyVersionGreaterThanInstalledVersion():
|
| + return True
|
| + else:
|
| + return False
|
| +
|
| + def Install(self, force, lockdown):
|
| + """Public install interface.
|
| +
|
| + force: If True, no version check is performed.
|
| + lockdown: if True, install a special ticket to lock down Keystone
|
| + and prevent uninstall. This will happen even if an install
|
| + of Keystone itself is not needed.
|
| + """
|
| + if self.installer._IsMasterDisabled():
|
| + raise Error(None, None, MASTER_DISABLE_ERROR_CODE,
|
| + 'Google Software Update installer failed. An administrator has '
|
| + 'disabled Google Software Update.')
|
| + if force or self._ShouldInstall():
|
| + self.installer.Install()
|
| + # possibly lockdown even if we don't need to install
|
| + if lockdown:
|
| + self.installer.LockdownKeystone()
|
| +
|
| + def Uninstall(self):
|
| + """Uninstall, which has the effect of preparing this machine for a new
|
| + install. Although similar, it is NOT as comprehensive as a nuke.
|
| + """
|
| + self.installer.Uninstall()
|
| +
|
| + def Nuke(self):
|
| + """Public nuke interface. Typically only used for testing."""
|
| + self.installer.Nuke()
|
| +
|
| + def RemoveReceipts(self):
|
| + """Public receipt removal interface. Used by uninstall, and to allow
|
| + downgraades of system installations."""
|
| + self.installer.RemoveReceipts()
|
| +
|
| + def FixupProducts(self):
|
| + """Attempt to repair any products might have broken tickets."""
|
| + self.installer.FixupProducts()
|
| +
|
| +# -------------------------------------------------------------------------
|
| +
|
| +def PrintUse():
|
| + print 'Use: '
|
| + print ' [--install PKG] Install keystone using PKG as the source.'
|
| + print ' [--root ROOT] Use ROOT as the dest for an install. Optional.'
|
| + print ' [--uninstall] Remove Keystone program files but do NOT delete '
|
| + print ' the ticket store.'
|
| + print ' [--nuke] Remove Keystone and all tickets.'
|
| + print ' [--remove-receipts] Remove Keystone package receipts, allowing for '
|
| + print ' downgrade (system install only)'
|
| + print ' [--no-launchd] Do NOT touch Keystone launchd plists or jobs,'
|
| + print ' for both install and uninstall. For test.'
|
| + print ' [--no-launchdjobs] Do NOT start/stop jobs, but do change launchd'
|
| + print ' plist files,for both install and uninstall.'
|
| + print ' For test.'
|
| + print ' [--self-destruct] Use if uninstall is triggered by process that '
|
| + print ' will be killed by uninstall.'
|
| + print ' [--force] Force an install no matter what. For test.'
|
| + print ' [--forcetiger] Pretend we are on Tiger (MacOSX 10.4). For test.'
|
| + print ' [--failcode] Fake an error with that code occurred. For test.'
|
| + print ' [--lockdown] Prevent Keystone from ever uninstalling itself.'
|
| + print ' [--interval N] Change agent plist to wake up every N seconds.'
|
| + print ' [--help] This message.'
|
| +
|
| +
|
| +def main():
|
| + os.environ.clear()
|
| + os.environ['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/libexec'
|
| +
|
| + # Make sure AuthorizationExecuteWithPrivileges() is happy
|
| + if os.getuid() and os.geteuid() == 0:
|
| + os.setuid(os.geteuid())
|
| +
|
| + try:
|
| + opts, args = getopt.getopt(sys.argv[1:], 'i:r:XunNfI:h',
|
| + ['install=', 'root=', 'nuke', 'uninstall',
|
| + 'no-launchd', 'no-launchdjobs', 'force',
|
| + 'interval=', 'help',
|
| + # Long-only
|
| + 'remove-receipts', 'self-destruct',
|
| + 'forcetiger', 'failcode=', 'lockdown'])
|
| + except getopt.GetoptError:
|
| + print 'Bad options.'
|
| + PrintUse()
|
| + sys.exit(USAGE_ERROR_CODE)
|
| +
|
| + root = None
|
| + package = None
|
| + nuke = False
|
| + uninstall = False
|
| + remove_receipts = False
|
| + launchd_setup = True
|
| + fail_code = 0
|
| + start_jobs = True
|
| + self_destruct = False
|
| + force = False
|
| + lockdown = False # If true, prevent uninstall by adding a "lockdown" ticket
|
| +
|
| + for opt, val in opts:
|
| + if opt in ('-h', '--help'):
|
| + PrintUse()
|
| + sys.exit(USAGE_ERROR_CODE)
|
| +
|
| + if opt in ['-i', '--install']:
|
| + package = val
|
| + if opt in ['-r', '--root']:
|
| + root = val
|
| + if opt in ['-X', '--nuke']:
|
| + nuke = True
|
| + if opt in ['-u', '--uninstall']:
|
| + uninstall = True
|
| + if opt in ['-n', '--no-launchd']:
|
| + launchd_setup = False
|
| + if opt in ['-N', '--no-launchdjobs']:
|
| + start_jobs = False
|
| + if opt in ['-f', '--force']:
|
| + force = True
|
| + if opt in ['-I', '--interval']:
|
| + global AGENT_START_INTERVAL
|
| + AGENT_START_INTERVAL = int(val)
|
| + if opt == '--remove-receipts':
|
| + remove_receipts = True
|
| + if opt == '--self-destruct':
|
| + self_destruct = True
|
| + if opt == '--forcetiger':
|
| + global FORCE_TIGER
|
| + FORCE_TIGER = True
|
| + if opt == '--failcode':
|
| + fail_code = int(val)
|
| + if opt == '--lockdown':
|
| + lockdown = True
|
| +
|
| + if package is None and not nuke and not uninstall and not remove_receipts:
|
| + print 'Must specify package path, uninstall, nuke, or remove-receipts.'
|
| + PrintUse()
|
| + sys.exit(USAGE_ERROR_CODE)
|
| + try:
|
| + (vers, ignored1, ignored2) = platform.mac_ver()
|
| + splits = vers.split('.')
|
| + if (len(splits) == 3) and (int(splits[1]) < 4):
|
| + print 'Requires Mac OS 10.4 or later.'
|
| + sys.exit(UNSUPPORTED_OS_ERROR_CODE)
|
| + except:
|
| + # 10.3 throws an exception for platform.mac_ver()
|
| + print 'Requires Mac OS 10.4 or later.'
|
| + sys.exit(UNSUPPORTED_OS_ERROR_CODE)
|
| +
|
| + # Lock file to make sure only one Keystone install at once. We want to
|
| + # share this lock amongst all users on the machine.
|
| + lockfilename = '/tmp/.keystone_install_lock'
|
| + oldmask = os.umask(0000)
|
| + lockfile = os.open(lockfilename, os.O_CREAT | os.O_RDONLY | os.O_NOFOLLOW,
|
| + 0444)
|
| + os.umask(oldmask)
|
| + # Lock, callers that cannot wait are expected to kill us.
|
| + fcntl.flock(lockfile, fcntl.LOCK_EX)
|
| +
|
| + try:
|
| + try:
|
| + # Simulate a failure
|
| + if fail_code != 0:
|
| + raise Error(None, None, fail_code,
|
| + 'Google Software Update installer simulated failure %d' % fail_code)
|
| + # Do the install
|
| + k = Keystone(package, root, launchd_setup, start_jobs, self_destruct)
|
| + # Ordered by level of cleanup applied
|
| + if nuke:
|
| + k.Nuke()
|
| + elif uninstall:
|
| + k.Uninstall()
|
| + elif remove_receipts:
|
| + k.RemoveReceipts()
|
| + else:
|
| + k.Install(force, lockdown)
|
| + k.FixupProducts()
|
| + except Error, e:
|
| + # To conform to previous contract on this tool (see headerdoc)
|
| + print e.message()
|
| + # We want the backtrace on stderr, but we need to control the exit code
|
| + # so dump manually
|
| + traceback.print_exc(file=sys.stderr)
|
| + # exit with the right error code
|
| + sys.exit(e.errorcode())
|
| + finally:
|
| + os.close(lockfile) # Lock file left around on purpose
|
| +
|
| +if __name__ == '__main__':
|
| + main()
|
|
|
| Property changes on: chrome_mac/Google Chrome.app/Contents/Versions/35.0.1916.114/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/install.py
|
| ___________________________________________________________________
|
| Added: svn:executable
|
| + *
|
| Added: svn:eol-style
|
| + LF
|
|
|
|
|