shornby

Google Keystone Utility for Mac OS X Jun 2013.py

Jun 14th, 2013
362
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 60.25 KB | None | 0 0
  1. #!/usr/bin/python
  2. # Copyright 2008 Google Inc.  All rights reserved.
  3.  
  4. """This script will install Keystone in the correct context
  5. (system-wide or per-user).  It can also uninstall Keystone.  is run by
  6. KeystoneRegistration.framework.
  7.  
  8. Example command lines for testing:
  9. Install:    install.py --install=/tmp/Keystone.tbz --root=/Users/fred
  10. Uninstall:  install.py --nuke --root=/Users/fred
  11.  
  12. Example real command lines, for user and root install and uninstall:
  13.  install.py --install Keystone.tbz
  14.  install.py --nuke
  15.  sudo install.py --install Keystone.tbz
  16.  sudo install.py --nuke
  17.  
  18. For a system-wide Keystone, the install root is "/".  Run with --help
  19. for a list of options.  Use --no-launchdjobs to NOT start background
  20. processes.
  21.  
  22. Errors can happen if:
  23. - we don't have write permission to install in the given root
  24. - pieces of our install are missing
  25.  
  26. On error, we print a simple message on stdout and our exit status is
  27. non-zero.  On success, we print nothing and exit with a status of 0.
  28. """
  29.  
  30. import os
  31. import re
  32. import sys
  33. import pwd
  34. import stat
  35. import glob
  36. import getopt
  37. import shutil
  38. import platform
  39. import fcntl
  40. import traceback
  41.  
  42.  
  43. # Allow us to force the installer to think we're on Tiger (10.4)
  44. FORCE_TIGER = False
  45.  
  46. # Allow us to adjust the agent launch interval (for testing).
  47. # In seconds.  Time is 1 hour minus a jitter factor.
  48. AGENT_START_INTERVAL = 3523
  49.  
  50. # Name of our "lockdown" ticket.  If you change this name be sure to
  51. # change it in other places in the code (grep is your friend)
  52. LOCKDOWN_TICKET = 'com.google.Keystone.Lockdown'
  53.  
  54. # Process that we consider a marker of a running user login session
  55. USER_SESSION_PROCESSNAME = \
  56.     ' /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder'
  57.  
  58. # URL for our Omaha server
  59. OMAHA_SERVER_URL = 'https://tools.google.com/service/update2'
  60.  
  61.  
  62. # Error codes. These need to be changed in sync with KSInstallErrors.h
  63. # and (potentially) KSAdministration/KSInstallation
  64. UNKNOWN_ERROR_CODE = -1
  65. MASTER_DISABLE_ERROR_CODE = -2
  66. # Error 1 reserved for generic errors from this script.
  67. GENERIC_ERROR_CODE = 1
  68. PACKAGE_ERROR_CODE = 2
  69. UNSUPPORTED_OS_ERROR_CODE = 3
  70. # These are internal and reported with a generic error message and their value
  71. # in a nested error.
  72. USAGE_ERROR_CODE = 64
  73. BAD_ROOT_ERROR_CODE = 65
  74. INSTALLED_VERSION_CHECK_ERROR_CODE = 66
  75. PACKAGE_VERSION_CHECK_ERROR_CODE = 67
  76. FILE_INSTALLATION_ERROR_CODE = 68
  77. DAEMON_CONTROL_ERROR_CODE = 69
  78. AGENT_CONTROL_ERROR_CODE = 70
  79. AGENT_INSTALL_ERROR_CODE = 71
  80. KSADMIN_MISSING_ERROR_CODE = 72
  81. KEYSTONE_TICKET_ERROR_CODE = 73
  82.  
  83.  
  84. class Error(Exception):
  85.   """Exception for Keystone install failure. Has methods for pretty printing
  86.  in a manner readable by our Obj-C wrapper code.
  87.  """
  88.  
  89.   def __init__(self, package, root, code, msg):
  90.     self.package = package
  91.     self.root = root
  92.     self.code = code
  93.     self.msg = msg
  94.  
  95.   def __str__(self):
  96.     return '%s (Package: %s Root: %s)' % (self.msg, self.package, self.root)
  97.  
  98.   def errorcode(self):
  99.     if self.code:
  100.       return self.code
  101.     else:
  102.       return UNKNOWN_ERROR_CODE
  103.  
  104.   def message(self):
  105.     return self.msg
  106.  
  107.  
  108. def CheckOnePath(file, statmode, errorcode=UNKNOWN_ERROR_CODE):
  109.   """Sanity check a file or directory as requested.  On failure throw
  110.  an exception."""
  111.   if os.path.exists(file):
  112.     st = os.stat(file)
  113.     if (st.st_mode & statmode) != 0:
  114.       return True
  115.   return False
  116.  
  117. # -------------------------------------------------------------------------
  118.  
  119. class KeystoneInstall(object):
  120.   """Worker object which does the heavy lifting of install or uninstall.
  121.  By default it assumes 10.5 (Leopard).
  122.  
  123.  Args:
  124.    package: The package to install (i.e. Keystone.tbz)
  125.    is_system: True if this is a system Keystone install
  126.    agent_job_uid: uid to start agent jobs as or None to use current euid
  127.    root: root directory for install.  On System this would be "/";
  128.          else would be a user home directory (unless testing, in which case
  129.          the root can be anywhere).
  130.    launchd_setup: True if the installation should setup launchd job description
  131.                   plists (and Tiger equivalents)
  132.    launchd_jobs: True if the installation should start/stop related jobs
  133.    self_destruct: True if uninstall is being triggered by a process the
  134.                   uninstall is expected to kill
  135.  
  136.  Conventions:
  137.    All functions which return directory paths end in '/'
  138.  """
  139.  
  140.   def __init__(self, package, is_system, agent_job_uid, root,
  141.                launchd_setup, launchd_jobs, self_destruct):
  142.     self.package = package
  143.     self.is_system = is_system
  144.     self.agent_job_uid = agent_job_uid
  145.     if is_system:
  146.       assert agent_job_uid is not None, 'System install needs agent job uid'
  147.     self.root = root
  148.     if not self.root.endswith('/'):
  149.       self.root = self.root + '/'
  150.     self.launchd_setup = launchd_setup
  151.     self.launchd_jobs = launchd_jobs
  152.     self.self_destruct = self_destruct
  153.     self.cached_package_version = None
  154.     # Save/restore permissions
  155.     self.old_euid = None
  156.     self.old_egid = None
  157.     self.old_umask = None
  158.  
  159.   def RunCommand(self, cmd):
  160.     """Runs a command, returning return code and output.
  161.  
  162.    Returns:
  163.      Tuple of return value, stdout and stderr.
  164.    """
  165.     # We need to work in python 2.3 (OSX 10.4), 2.5 (10.5), and 2.6 (10.6)
  166.     if (sys.version_info[0] == 2) and (sys.version_info[1] <= 5):
  167.       # subprocess.communicate implemented the hard way
  168.       import errno
  169.       import popen2
  170.       import select
  171.       p = popen2.Popen3(cmd, True)
  172.       stdout = []
  173.       stderr = []
  174.       readable = [ p.fromchild, p.childerr ]
  175.       while not p.fromchild.closed or not p.childerr.closed:
  176.         try:
  177.           try_to_read = []
  178.           if not p.fromchild.closed:
  179.             try_to_read.append(p.fromchild)
  180.           if not p.childerr.closed:
  181.             try_to_read.append(p.childerr)
  182.           readable, ignored_w, ignored_x = select.select(try_to_read, [], [])
  183.         except select.error, e:
  184.           if e.args[0] == errno.EINTR:
  185.             continue
  186.           raise
  187.         if p.fromchild in readable:
  188.           out = os.read(p.fromchild.fileno(), 1024)
  189.           stdout.append(out)
  190.           if out == '':
  191.             p.fromchild.close()
  192.         if p.childerr in readable:
  193.           errout = os.read(p.childerr.fileno(), 1024)
  194.           stderr.append(errout)
  195.           if errout == '':
  196.             p.childerr.close()
  197.       result = p.wait()
  198.       return (os.WEXITSTATUS(result), ''.join(stdout), ''.join(stderr))
  199.     else:
  200.       # Just use subprocess, so much simpler
  201.       import subprocess
  202.       p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE,
  203.                            stderr=subprocess.PIPE, close_fds=True)
  204.       (stdout, stderr) = p.communicate()
  205.       return (p.returncode, stdout, stderr)
  206.  
  207.   def _AgentProcessName(self):
  208.     """Return the process name of the agent."""
  209.     return 'GoogleSoftwareUpdateAgent'
  210.  
  211.   def _LibraryCachesDirPath(self):
  212.     """Return the Library/Caches subdirectory"""
  213.     return os.path.join(self.root, 'Library/Caches/')
  214.  
  215.   def _LibraryGoogleDirPath(self):
  216.     """Return the Library subdirectory that parents all our dirs"""
  217.     return os.path.join(self.root, 'Library/Google/')
  218.  
  219.   def _KeystoneDirPath(self):
  220.     """Return the subdirectory where Keystone.bundle is or will be.
  221.    Does not sanity check the directory."""
  222.     return os.path.join(self._LibraryGoogleDirPath(), 'GoogleSoftwareUpdate/')
  223.  
  224.   def _KeystoneBundlePath(self):
  225.     """Return the location of Keystone.bundle."""
  226.     return os.path.join(self._KeystoneDirPath(), 'GoogleSoftwareUpdate.bundle/')
  227.  
  228.   def _KeystoneTicketStorePath(self):
  229.     """Returns directory path of the Keystone ticket store."""
  230.     return os.path.join(self._KeystoneDirPath(), 'TicketStore')
  231.  
  232.   def _KsadminPath(self):
  233.     """Return a path to ksadmin which will exist only AFTER Keystone is
  234.    installed.  Return None if it doesn't exist."""
  235.     ksadmin = os.path.join(self._KeystoneBundlePath(), 'Contents/MacOS/ksadmin')
  236.     if not os.path.exists(ksadmin):
  237.       return None
  238.     return ksadmin
  239.  
  240.   def _KeystoneResourcePath(self):
  241.     """Return the subdirectory where Keystone.bundle's resources should be."""
  242.     return os.path.join(self._KeystoneBundlePath(), 'Contents/Resources/')
  243.  
  244.   def _KeystoneAgentPath(self):
  245.     """Returns a path to installed KeystoneAgent.app."""
  246.     return os.path.join(self._KeystoneResourcePath(),
  247.                         'GoogleSoftwareUpdateAgent.app/')
  248.  
  249.   def _LaunchAgentConfigDir(self):
  250.     """Return the destination directory where launch agents should go."""
  251.     return os.path.join(self.root, 'Library/LaunchAgents/')
  252.  
  253.   def _LaunchDaemonConfigDir(self):
  254.     """Return the destination directory where launch daemons should go."""
  255.     return os.path.join(self.root, 'Library/LaunchDaemons/')
  256.  
  257.   def _AgentPlistFileName(self):
  258.     """Return the filename of the Keystone agent launchd plist or None."""
  259.     return 'com.google.keystone.agent.plist'
  260.  
  261.   def _DaemonPlistSourceFileName(self):
  262.     """Return the filename of the Keystone daemon launchd plist to install."""
  263.     return 'com.google.keystone.daemon.plist'
  264.  
  265.   def _DaemonPlistFileName(self):
  266.     """Return the filename of the post-install Keystone daemon launchd plist."""
  267.     return 'com.google.keystone.daemon.plist'
  268.  
  269.   def _IsMasterDisabled(self):
  270.     """Check for master disable MCX preference."""
  271.     # 'defaults' does not honor MCX, so read the file directly
  272.     if os.path.exists('/Library/Managed Preferences/com.google.Keystone.plist'):
  273.       cmd = ['/usr/bin/defaults', 'read',
  274.              '/Library/Managed Preferences/com.google.Keystone',
  275.              'masterDisable']
  276.       (result, out, errout) = self.RunCommand(cmd)
  277.       if result == 0 and out.strip() == '1':
  278.         return True
  279.     # Honor admin preferences too
  280.     if os.path.exists('/Library/Preferences/com.google.Keystone.plist'):
  281.       cmd = ['/usr/bin/defaults', 'read',
  282.              '/Library/Preferences/com.google.Keystone',
  283.              'masterDisable']
  284.       (result, out, errout) = self.RunCommand(cmd)
  285.       if result == 0 and out.strip() == '1':
  286.         return True
  287.     return False
  288.  
  289.   def _CFBundleVersionFromInfo(self, info):
  290.     """Given the content of an Info.plist return its CFBundleVersion."""
  291.     linelist = info.splitlines()
  292.     for i in range(len(linelist)):
  293.       if linelist[i].find('<key>CFBundleVersion</key>') != -1:
  294.         version = linelist[i+1].strip()
  295.         version = version.strip('<string>').strip('</string>')
  296.         if version:
  297.           return version
  298.     return None
  299.  
  300.   def InstalledKeystoneTicketVersion(self):
  301.     """Return the version the current Keystone ticket, or None if not installed.
  302.  
  303.    Invariant: we use the same a 4-digit version for the ticket as we use
  304.    for CFBundleVersion.
  305.    """
  306.     ksadmin_path = self._KsadminPath()
  307.     if not ksadmin_path or not os.path.exists(ksadmin_path):
  308.       return None
  309.     cmd = [ksadmin_path,
  310.            # store is specified explicitly so unit tests work
  311.            '--store', os.path.join(self._KeystoneTicketStorePath(),
  312.                                    'Keystone.ticketstore'),
  313.            '--productid', 'com.google.Keystone',
  314.            '--print-tickets']
  315.     (result, out, errout) = self.RunCommand(cmd)
  316.     if result:
  317.       return None
  318.     for line in out.splitlines():
  319.       if line.find('version=') != -1:
  320.         version = line.strip()
  321.         version = version.strip('version=')
  322.         if version:
  323.           return version
  324.     return None
  325.  
  326.   def InstalledKeystoneBundleVersion(self):
  327.     """Return the version of an installed Keystone bundle, or None if
  328.    not installed.  Specifically, it returns the CFBundleVersion as a
  329.    string (e.g. "0.1.0.0").
  330.  
  331.    Invariant: we require a 4-digit version when building Keystone.bundle.
  332.    """
  333.     plist_path = os.path.join(self._KeystoneBundlePath(), 'Contents/Info.plist')
  334.     if not os.path.exists(plist_path):
  335.       return None
  336.     p = open(plist_path, 'r')
  337.     info = p.read()
  338.     p.close()
  339.     return self._CFBundleVersionFromInfo(info)
  340.  
  341.   def MyKeystoneBundleVersion(self):
  342.     """Return the version of our Keystone bundle which we might want to install.
  343.    Specifically, it returns the CFBundleVersion as a string (e.g. "0.1.0.0").
  344.  
  345.    Invariant: we require a 4-digit version when building Keystone.bundle.
  346.    """
  347.     if self.cached_package_version is None:
  348.       cmd = ['/usr/bin/tar', '-Oxjf',
  349.              self.package,
  350.              'GoogleSoftwareUpdate.bundle/Contents/Info.plist']
  351.       (result, out, errout) = self.RunCommand(cmd)
  352.       if result != 0:
  353.         raise Error(self.package, self.root, PACKAGE_VERSION_CHECK_ERROR_CODE,
  354.                     'Google Software Update installer unable to read package '
  355.                     'Info.plist: "%s"' % errout)
  356.       self.cached_package_version = self._CFBundleVersionFromInfo(out)
  357.     return self.cached_package_version
  358.  
  359.   def IsVersionGreaterThanVersion(self, a_version, b_version):
  360.     """Return True if a_version is greater than b_version.
  361.  
  362.    Invariant: we require a 4-digit version when building Keystone.bundle.
  363.    """
  364.     if a_version is None or b_version is None:
  365.       return True
  366.     else:
  367.       a_version = a_version.split('.')
  368.       b_version = b_version.split('.')
  369.     # Only correct for 4-digit versions, see invariants.
  370.     if len(a_version) != len(b_version):
  371.       return True
  372.     for a, b in zip(a_version, b_version):
  373.       if int(a) > int(b):
  374.         return True
  375.       elif int(a) < int(b):
  376.         return False
  377.     # If we get here, it's a complete match, so no.
  378.     return False
  379.  
  380.   def IsMyVersionGreaterThanInstalledVersion(self):
  381.     """Returns True if package Keystone version is greater than current install.
  382.  
  383.    Invariant: we require a 4-digit version when building Keystone.bundle.
  384.    """
  385.     my_version = self.MyKeystoneBundleVersion()
  386.     bundle_version = self.InstalledKeystoneBundleVersion()
  387.     if self.IsVersionGreaterThanVersion(my_version, bundle_version):
  388.       return True
  389.     ticket_version = self.InstalledKeystoneTicketVersion()
  390.     if self.IsVersionGreaterThanVersion(my_version, ticket_version):
  391.       return True
  392.     return False
  393.  
  394.   def _SetSystemInstallPermissions(self):
  395.     """Set permissions for system install, must pair with
  396.    _ClearSystemInstallPermissions(). Call before any filesystem access."""
  397.     assert (self.old_euid is None and self.old_egid is None and
  398.             self.old_umask is None), 'System permissions used reentrant'
  399.     self.old_euid = os.geteuid()
  400.     os.seteuid(0)
  401.     self.old_egid = os.getegid()
  402.     os.setegid(0)
  403.     self.old_umask = os.umask(022)
  404.  
  405.   def _ClearSystemInstallPermissions(self):
  406.     """Restore prior permissions after _SetSystemInstallPermissions()."""
  407.     assert (self.old_euid is not None and self.old_egid is not None and
  408.             self.old_umask is not None), 'System permissions cleared before set'
  409.     os.seteuid(self.old_euid)
  410.     self.old_euid = None
  411.     os.setegid(self.old_egid)
  412.     self.old_egid = None
  413.     os.umask(self.old_umask)
  414.     self.old_umask = None
  415.  
  416.   def _InstallPlist(self, source, dest_name, dest_dir):
  417.     """Install a copy of the plist from Resources to the dest_dir path using
  418.    dest_name. For system install, assumes you have already called
  419.    _SetSystemInstallPermissions().
  420.    """
  421.     try:
  422.       pf = open(os.path.join(self._KeystoneResourcePath(), source), 'r')
  423.       content = pf.read()
  424.       pf.close()
  425.     except IOError, e:
  426.       raise Error(self.package, self.root, PACKAGE_ERROR_CODE,
  427.                   'Google Software Update installer failed to read resource '
  428.                   'launchd plist "%s": %s' % (source, str(e)))
  429.     # This line is key.  We can't have a tilde in a launchd script;
  430.     # we need an absolute path.  So we replace a known token, like this:
  431.     #    cat src.plist | 's/INSTALL_ROOT/self.root/g' > dest.plist
  432.     content = content.replace('${INSTALL_ROOT}', self.root)
  433.     content = content.replace(self.root + '/', self.root)  # doubleslash remove
  434.     # Make sure launchd can distinguish between user and system Agents.
  435.     # This is a no-op for the daemon.
  436.     if self.is_system:
  437.       content = content.replace('${INSTALL_TYPE}', 'system')
  438.     else:
  439.       content = content.replace('${INSTALL_TYPE}', 'user')
  440.     # Allow start interval to be configured.
  441.     content = content.replace('${START_INTERVAL}', str(AGENT_START_INTERVAL))
  442.     try:
  443.       # Write to temp file then move in place (safe save)
  444.       target_file = os.path.join(dest_dir, dest_name)
  445.       target_tmp_file = target_file + '.tmp'
  446.       pf = open(target_tmp_file, 'w')
  447.       pf.write(content)
  448.       pf.close()
  449.       os.rename(target_tmp_file, target_file)
  450.     except IOError, e:
  451.       raise Error(self.package, self.root, FILE_INSTALLATION_ERROR_CODE,
  452.                   'Google Software Update installer failed to install launchd '
  453.                   'plist "%s": %s' % (os.path.join(dest_dir, dest_name),
  454.                                       str(e)))
  455.  
  456.   def _RemoveOldDaemonPlists(self):
  457.     """Remove older daemon plists installed using older names.
  458.       Assumes _SetSystemInstallPermissions() has been called."""
  459.     # In general we have nothing to do
  460.     pass
  461.  
  462.   def _InstallAgentLoginItem(self):
  463.     """Setup the agent login item (vs. launchd job).
  464.    Assumes _SetSystemInstallPermissions() has been called."""
  465.     pass
  466.  
  467.   def _RemoveAgentLoginItem(self):
  468.     """Remove the agent login item (vs. launchd job).
  469.    Assumes _SetSystemInstallPermissions() has been called.
  470.  
  471.    Note: We use this code on both Tiger and Leopard to handle the OS upgrade
  472.    case.
  473.    """
  474.     if self.is_system:
  475.       domain = '/Library/Preferences/loginwindow'
  476.     else:
  477.       domain = 'loginwindow'
  478.     (result, alaout, errout) = self.RunCommand(['/usr/bin/defaults', 'read',
  479.         domain, 'AutoLaunchedApplicationDictionary'])
  480.     # Ignoring result
  481.     if len(alaout.strip()) == 0:
  482.       alaout = '()'
  483.     # One line per loginitem to help us match
  484.     alaout = re.compile('[\n]+').sub('', alaout)
  485.     # handles case where we are the only item
  486.     alaout = alaout.replace('(', '(\n')
  487.     alaout = alaout.replace('}', '}\n')
  488.     needed_removal = False
  489.     for line in alaout.splitlines():
  490.       if line.find('/Library/Google/GoogleSoftwareUpdate/'
  491.                    'GoogleSoftwareUpdate.bundle/Contents/'
  492.                    'Resources/GoogleSoftwareUpdateAgent.app') != -1:
  493.         alaout = alaout.replace(line, '')
  494.         needed_removal = True
  495.     alaout = alaout.replace('\n', '')
  496.     # make sure it's a well-formed list
  497.     alaout = alaout.replace('(,', '(')
  498.     if needed_removal:
  499.       (result, out, errout) = self.RunCommand(['/usr/bin/defaults', 'write',
  500.           domain, 'AutoLaunchedApplicationDictionary', alaout])
  501.       # Ignore result, if we messed up the parse just move on.
  502.  
  503.   def _ChangeDaemonRunStatus(self, start, ignore_failure):
  504.     """Start or stop the daemon using launchd."""
  505.     assert self.is_system, 'Daemon start on non-system install'
  506.     self._SetSystemInstallPermissions()
  507.     try:
  508.       if start:
  509.         action = 'load'
  510.       else:
  511.         action = 'unload'
  512.       job_path = os.path.join(self._LaunchDaemonConfigDir(),
  513.                               self._DaemonPlistFileName())
  514.       # Workaround for incorrect Tiger installation
  515.       if os.path.exists(os.path.join(self._LaunchDaemonConfigDir(),
  516.                                      self._DaemonPlistSourceFileName())):
  517.         job_path = os.path.join(self._LaunchDaemonConfigDir(),
  518.                                 self._DaemonPlistSourceFileName())
  519.       (result, out, errout) = self.RunCommand(['/bin/launchctl', action,
  520.                                                job_path])
  521.       if not ignore_failure and result != 0:
  522.         raise Error(self.package, self.root, DAEMON_CONTROL_ERROR_CODE,
  523.                     'Google Software Update installer failed to %s daemon '
  524.                     '(%d): %s' % (action, result, errout))
  525.     finally:
  526.       self._ClearSystemInstallPermissions()
  527.  
  528.   def _ChangeAgentRunStatus(self, start, ignore_failure):
  529.     """Start or stop the agent using launchd."""
  530.     if self._AgentPlistFileName() is None:
  531.       return
  532.     if start:
  533.       action = 'load'
  534.       search_process_name = USER_SESSION_PROCESSNAME
  535.     else:
  536.       action = 'unload'
  537.       search_process_name = self._AgentProcessName()
  538.     if self.is_system:
  539.       self._SetSystemInstallPermissions()
  540.       try:
  541.         # System installation needs to use bsexec to hit all the running agents
  542.         (result, psout, pserr) = self.RunCommand(['/bin/ps', 'auxwww'])
  543.         if result != 0:  # Internal problem so don't use ignore_failure
  544.           raise Error(self.package, self.root, UNKNOWN_ERROR_CODE,
  545.                       'Google Software Update installer could not run '
  546.                       '/bin/ps: %s' % pserr)
  547.         for psline in psout.splitlines():
  548.           if psline.find(search_process_name) != -1:
  549.             username = psline.split()[0]
  550.             uid = pwd.getpwnam(username)[2]
  551.             # Must be root to bsexec.
  552.             # Must bsexec to (pid) to get in local user's context.
  553.             # Must become local user to have right process owner.
  554.             # Must unset SUDO_COMMAND to keep launchctl happy.
  555.             # Order is important.
  556.             agent_plist_path = os.path.join(self._LaunchAgentConfigDir(),
  557.                                             self._AgentPlistFileName())
  558.             (result, out, errout) = self.RunCommand([
  559.                 '/bin/launchctl', 'bsexec', psline.split()[1],
  560.                 '/usr/bin/sudo', '-u', username, '/bin/bash', '-c',
  561.                 'unset SUDO_COMMAND ; /bin/launchctl %s -S Aqua "%s"' % (
  562.                     action,
  563.                     os.path.join(self._LaunchAgentConfigDir(),
  564.                                  self._AgentPlistFileName()))])
  565.             # Although we're running for every user, only treat the requested
  566.             # user as an error
  567.             if not ignore_failure and result != 0 and uid == self.agent_job_uid:
  568.               raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
  569.                   'Google Software Update installer failed to %s agent for '
  570.                   'uid %d from plist "%s" (%d): %s' %
  571.                   (action, self.agent_job_uid, agent_plist_path, result,
  572.                    errout))
  573.       finally:
  574.         self._ClearSystemInstallPermissions()
  575.     else:
  576.       # Non-system variant requires basic launchctl commands
  577.       agent_plist_path = os.path.join(self._LaunchAgentConfigDir(),
  578.                                       self._AgentPlistFileName())
  579.       (result, out, errout) = self.RunCommand(['/bin/launchctl', action,
  580.                                                '-S', 'Aqua', agent_plist_path])
  581.       if not ignore_failure and result != 0:
  582.         raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
  583.                     'Google Software Update installer failed to %s agent from '
  584.                     'plist "%s" (%d): %s' %
  585.                     (action, agent_plist_path, result, errout))
  586.  
  587.   def _ClearQuarantine(self, path):
  588.     """Remove LaunchServices quarantine attributes from a file hierarchy."""
  589.     # /usr/bin/xattr* are implemented in Python, and there's much magic
  590.     # around which of /usr/bin/xattr and the multiple /usr/bin/xattr-2.?
  591.     # actually execute. I suspect at least some users have /usr/bin/python
  592.     # linked to a "real" copy or otherwise replaced, so we're going to
  593.     # try a bunch of different options.
  594.     # Implement it ourself
  595.     try:
  596.       import xattr
  597.       for (root, dirs, files) in os.walk(path):
  598.         for name in files:
  599.           attrs = xattr.xattr(os.path.join(path, name))
  600.           try:
  601.             del attrs['com.apple.quarantine']
  602.           except KeyError:
  603.             pass
  604.       return  # Success
  605.     except:
  606.       pass
  607.     # Use specific version by name in case /usr/bin/python isn't the Apple magic
  608.     # that selects the right copy of xattr. xattr-2.6 present on 10.6 and 10.7.
  609.     if os.path.exists('/usr/bin/xattr-2.6'):
  610.       (result, out, errout) = self.RunCommand(['/usr/bin/xattr-2.6', '-dr',
  611.                                                'com.apple.quarantine', path])
  612.       if result == 0:
  613.         return
  614.     # Fall back to /usr/bin/xattr. On Leopard it doesn't support '-r' so
  615.     # recurse using find. Ignore the result, this is our last attempt.
  616.     self.RunCommand(['/usr/bin/find', '-x', path, '-exec', '/usr/bin/xattr',
  617.                      '-d', 'com.apple.quarantine', '{}'])
  618.  
  619.   def Install(self):
  620.     """Perform a complete install operation, including safe upgrade"""
  621.     # Unload any current processes but ignore failures
  622.     if self.launchd_setup and self.launchd_jobs:
  623.       self._ChangeAgentRunStatus(False, True)
  624.       if self.is_system:
  625.         self._ChangeDaemonRunStatus(False, True)
  626.     # Install new files
  627.     if self.is_system:
  628.       self._SetSystemInstallPermissions()
  629.     try:
  630.       # Make and protect base directories (always safe during upgrade)
  631.       if not os.path.isdir(self._KeystoneDirPath()):
  632.         os.makedirs(self._KeystoneDirPath())
  633.       if self.is_system:
  634.         os.chown(self._KeystoneDirPath(), 0, 0)
  635.         os.chmod(self._KeystoneDirPath(), 0755)
  636.         os.chown(self._LibraryGoogleDirPath(), 0, 0)
  637.         os.chmod(self._LibraryGoogleDirPath(), 0755)
  638.       # Unpack Keystone bundle. In an upgrade we want to try to restore
  639.       # to the old binary if install encounters a problem. Options flag names
  640.       # chosen to be compatible with both 10.4 and 10.6 (both BSD tar, but
  641.       # very different versions).
  642.       saved_bundle_path = self._KeystoneBundlePath().rstrip('/') + '.old'
  643.       if os.path.exists(self._KeystoneBundlePath()):
  644.         if os.path.isdir(saved_bundle_path):
  645.           shutil.rmtree(saved_bundle_path)
  646.         elif os.path.exists(saved_bundle_path):
  647.           os.unlink(saved_bundle_path)
  648.         os.rename(self._KeystoneBundlePath(), saved_bundle_path)
  649.       cmd = ['/usr/bin/tar', 'xjf', self.package, '--no-same-owner',
  650.              '-C', self._KeystoneDirPath()]
  651.       (result, out, errout) = self.RunCommand(cmd)
  652.       if result != 0:
  653.         try:
  654.           if os.path.exists(saved_bundle_path):
  655.             os.rename(saved_bundle_path, self._KeystoneBundlePath())
  656.         finally:
  657.           raise Error(self.package, self.root, FILE_INSTALLATION_ERROR_CODE,
  658.                       'Google Software Update installer unable to unpack '
  659.                       'package: "%s"' % errout)
  660.       if os.path.exists(saved_bundle_path):
  661.         shutil.rmtree(saved_bundle_path)
  662.       # Clear quarantine on the new bundle. Failure is ignored, user will
  663.       # be prompted if quarantine is not cleared, but we will still operate
  664.       # correctly.
  665.       self._ClearQuarantine(self._KeystoneBundlePath())
  666.       # Create Keystone ticket store. On a system install start by checking
  667.       # ticket store permissions. Bad permissions on the store could be the
  668.       # result of a prior install or an attempt to poison the store.
  669.       if self.is_system and os.path.exists(self._KeystoneTicketStorePath()):
  670.         s = os.lstat(self._KeystoneTicketStorePath())
  671.         if (s[stat.ST_UID] == 0 and
  672.             (s[stat.ST_GID] == 0 or s[stat.ST_GID] == 80)):
  673.           pass
  674.         else:
  675.           if os.path.isdir(self._KeystoneTicketStorePath()):
  676.             shutil.rmtree(self._KeystoneTicketStorePath())
  677.           else:
  678.             os.unlink(self._KeystoneTicketStorePath())
  679.       # Now create and protect ticket store
  680.       if not os.path.isdir(self._KeystoneTicketStorePath()):
  681.         os.makedirs(self._KeystoneTicketStorePath())
  682.       if self.is_system:
  683.         os.chown(self._KeystoneTicketStorePath(), 0, 0)
  684.         os.chmod(self._KeystoneTicketStorePath(), 0755)
  685.       # Create/update Keystone ticket
  686.       ksadmin_path = self._KsadminPath()
  687.       if not ksadmin_path or not os.path.exists(ksadmin_path):
  688.         raise Error(self.package, self.root, KSADMIN_MISSING_ERROR_CODE,
  689.                     'Google Software Update installer ksadmin not available')
  690.       cmd = [ksadmin_path,
  691.              # store is specified explicitly so unit tests work
  692.              '--store', os.path.join(self._KeystoneTicketStorePath(),
  693.                                      'Keystone.ticketstore'),
  694.              '--register',
  695.              '--productid', 'com.google.Keystone',
  696.              '--version', self.MyKeystoneBundleVersion(),
  697.              '--xcpath', self._KeystoneBundlePath(),
  698.              '--url', OMAHA_SERVER_URL,
  699.              '--preserve-tttoken']
  700.       (result, out, errout) = self.RunCommand(cmd)
  701.       if result != 0:
  702.         raise Error(self.package, self.root, KEYSTONE_TICKET_ERROR_CODE,
  703.             'Google Software Update installer Keystone ticket install failed '
  704.             '(%d): %s' % (result, errout))
  705.       # launchd config if requested
  706.       if self.launchd_setup:
  707.         # Daemon first (safer if upgrade fails)
  708.         if self.is_system:
  709.           if not os.path.isdir(self._LaunchDaemonConfigDir()):
  710.             os.makedirs(self._LaunchDaemonConfigDir())
  711.             # Again set permissions only if we created it, but if we did use
  712.             # standard permission from a default OS install.
  713.             os.chown(self._LaunchDaemonConfigDir(), 0, 0)
  714.             os.chmod(self._LaunchDaemonConfigDir(), 0755)
  715.           self._InstallPlist(self._DaemonPlistSourceFileName(),
  716.                              self._DaemonPlistFileName(),
  717.                              self._LaunchDaemonConfigDir())
  718.           # Remove daemon under older names
  719.           self._RemoveOldDaemonPlists()
  720.         # Agent launchd
  721.         if self._AgentPlistFileName() is not None:
  722.           if not os.path.isdir(self._LaunchAgentConfigDir()):
  723.             os.makedirs(self._LaunchAgentConfigDir())
  724.             # /Library/LaunchAgents is a OS directory, use permissions from
  725.             # default OS install, but only if we created it.
  726.             if self.is_system:
  727.               os.chown(self._LaunchAgentConfigDir(), 0, 0)
  728.               os.chmod(self._LaunchAgentConfigDir(), 0755)
  729.           self._InstallPlist(self._AgentPlistFileName(),
  730.                              self._AgentPlistFileName(),
  731.                              self._LaunchAgentConfigDir())
  732.         # Agent login item remove/restore. Removal prior to add
  733.         # so that removal happens in Tiger -> Leopard upgrade case and
  734.         # we do not duplicate entries.
  735.         self._RemoveAgentLoginItem()
  736.         self._InstallAgentLoginItem()
  737.     finally:
  738.       if self.is_system:
  739.         self._ClearSystemInstallPermissions()
  740.     # If requested, start our jobs, failures treated as errors.
  741.     if self.launchd_setup and self.launchd_jobs:
  742.       if self.is_system:
  743.         self._ChangeDaemonRunStatus(True, False)
  744.       self._ChangeAgentRunStatus(True, False)
  745.  
  746.   def LockdownKeystone(self):
  747.     """Prevent Keystone from ever self-uninstalling.
  748.  
  749.    This is necessary for a System Keystone used for Trusted Tester support.
  750.    We do this by installing (and never uninstalling) a system ticket.
  751.    """
  752.     if self.is_system:
  753.       self._SetSystemInstallPermissions()
  754.     try:
  755.       ksadmin_path = self._KsadminPath()
  756.       if not ksadmin_path:
  757.         raise Error(self.package, self.root, KSADMIN_MISSING_ERROR_CODE,
  758.                     'Google Software Update installer ksadmin not available')
  759.       cmd = [ksadmin_path,
  760.              # store is specified explicitly so unit tests work
  761.              '--store', os.path.join(self._KeystoneTicketStorePath(),
  762.                                      'Keystone.ticketstore'),
  763.              '--register',
  764.              '--productid', LOCKDOWN_TICKET,
  765.              '--version', '1.0',
  766.              '--xcpath', '/',
  767.              '--url', OMAHA_SERVER_URL]
  768.       (result, out, errout) = self.RunCommand(cmd)
  769.       if result != 0:
  770.         raise Error(self.package, self.root, KEYSTONE_TICKET_ERROR_CODE,
  771.             'Google Software Update installer Keystone ticket install '
  772.             'failed (%d): %s' % (result, errout))
  773.     finally:
  774.       if self.is_system:
  775.         self._ClearSystemInstallPermissions()
  776.  
  777.   def Uninstall(self):
  778.     """Perform a complete uninstall (uninstall leaves tickets in place)"""
  779.     # On uninstall if we are not in self-destruct stop all processes but
  780.     # ignore failure (may not be running). On a non-self destruct case we do
  781.     # this first since it avoids race conditions on caches and pref writes
  782.     if not self.self_destruct and self.launchd_setup and self.launchd_jobs:
  783.       self._ChangeAgentRunStatus(False, True)
  784.       if self.is_system:
  785.         self._ChangeDaemonRunStatus(False, True)
  786.     # Perform file removals. In self-destruct case the processes may still
  787.     # be running.
  788.     if self.is_system:
  789.       self._SetSystemInstallPermissions()
  790.     try:
  791.       # Remove plist files unless blocked
  792.       if self.launchd_setup:
  793.         # In self-destruct mode we still need these plists for launchctl
  794.         if not self.self_destruct:
  795.           if self._AgentPlistFileName() is not None:
  796.             agent_plist = os.path.join(self._LaunchAgentConfigDir(),
  797.                                        self._AgentPlistFileName())
  798.             if os.path.exists(agent_plist):
  799.               os.unlink(agent_plist)
  800.           daemon_plist = os.path.join(self._LaunchDaemonConfigDir(),
  801.                                       self._DaemonPlistFileName())
  802.           if os.path.exists(daemon_plist):
  803.             os.unlink(daemon_plist)
  804.           # Remove daemon under older names as well
  805.           self._RemoveOldDaemonPlists()
  806.         # Self-destruct or not, we can remove login item (Tiger)
  807.         self._RemoveAgentLoginItem()
  808.       # Unregister Keystone ticket (if installed at all).
  809.       if os.path.exists(self._KeystoneBundlePath()):
  810.         ksadmin_path = self._KsadminPath()
  811.         if not ksadmin_path or not os.path.exists(ksadmin_path):
  812.           raise Error(self.package, self.root, KSADMIN_MISSING_ERROR_CODE,
  813.                       'Google Software Update installer ksadmin not available')
  814.         cmd = [ksadmin_path,
  815.                # store is specified explicitly so unit tests work
  816.                '--store', os.path.join(self._KeystoneTicketStorePath(),
  817.                                        'Keystone.ticketstore'),
  818.                '--delete', '--productid', 'com.google.Keystone']
  819.         (result, out, errout) = self.RunCommand(cmd)
  820.         if result != 0 and errout.find('No ticket to delete') == -1:
  821.           raise Error(self.package, self.root, KEYSTONE_TICKET_ERROR_CODE,
  822.               'Google Software Update installer Keystone ticket uninstall '
  823.               'failed (%d): %s' % (result, errout))
  824.       # Remove the Keystone bundle
  825.       if os.path.exists(self._KeystoneBundlePath()):
  826.         shutil.rmtree(self._KeystoneBundlePath())
  827.       # Clean up caches. Race condition here if self-destructing, but unlikely
  828.       # and we'll just leak a cache dir.
  829.       if os.path.exists(self._LibraryCachesDirPath()):
  830.         caches = glob.glob(os.path.join(self._LibraryCachesDirPath(),
  831.                                         'com.google.Keystone.*'))
  832.         caches.extend(glob.glob(os.path.join(self._LibraryCachesDirPath(),
  833.                                         'com.google.UpdateEngine.*')))
  834.         caches.extend(glob.glob(os.path.join(self._LibraryCachesDirPath(),
  835.                                         'UpdateEngine-Temp')))
  836.         for cache_item in caches:
  837.           if os.path.isdir(cache_item):
  838.             shutil.rmtree(cache_item, True)  # Ignore cache deletion errors
  839.           else:
  840.             try:
  841.               os.unlink(cache_item)
  842.             except OSError:
  843.               pass
  844.       # Clean up preferences, this prevents old installations from propagating
  845.       # dates (like uninstall embargo time) forward in a complete uninstall/
  846.       # reinstall scenario. Again, race condition here for self-destruct case
  847.       # but the risk is minor and only leaks a pref file.
  848.       if self.is_system:
  849.         agent_pref_path = os.path.join(pwd.getpwuid(self.agent_job_uid)[5],
  850.                                        'Library/Preferences/'
  851.                                        'com.google.Keystone.Agent.plist')
  852.       else:
  853.         agent_pref_path = os.path.expanduser('~/Library/Preferences/'
  854.                                              'com.google.Keystone.Agent.plist')
  855.       if os.path.exists(agent_pref_path):
  856.         os.unlink(agent_pref_path)
  857.     finally:
  858.       if self.is_system:
  859.         self._ClearSystemInstallPermissions()
  860.     # Remove receipts
  861.     self.RemoveReceipts()
  862.     # With all other files removed, cleanup processes and job control files in
  863.     # the self-destruct case. This will presumably kill our parent, so after
  864.     # this no one is listening for our errors. We do it as late as possible.
  865.     if self.self_destruct:
  866.       if self.launchd_setup and self.launchd_jobs:
  867.         self._ChangeAgentRunStatus(False, True)
  868.         if self.is_system:
  869.           self._ChangeDaemonRunStatus(False, True)
  870.       if self.is_system:
  871.         self._SetSystemInstallPermissions()
  872.       try:
  873.         # We needed these plists to stop the agent and daemon. No one is
  874.         # listening to errors, but failure only leaves a stale launchctl file
  875.         # (actual program files removed above)
  876.         if self._AgentPlistFileName() is not None:
  877.           agent_plist = os.path.join(self._LaunchAgentConfigDir(),
  878.                                      self._AgentPlistFileName())
  879.           if os.path.exists(agent_plist):
  880.             os.unlink(agent_plist)
  881.         daemon_plist = os.path.join(self._LaunchDaemonConfigDir(),
  882.                                     self._DaemonPlistFileName())
  883.         if os.path.exists(daemon_plist):
  884.           os.unlink(daemon_plist)
  885.         # Remove daemon under older names as well
  886.         self._RemoveOldDaemonPlists()
  887.       finally:
  888.         if self.is_system:
  889.           self._ClearSystemInstallPermissions()
  890.  
  891.   def Nuke(self):
  892.     """Perform an uninstall and remove all files (including tickets)"""
  893.     # Uninstall
  894.     self.Uninstall()
  895.     # Nuke what's left
  896.     if self.is_system:
  897.       self._SetSystemInstallPermissions()
  898.     try:
  899.       # Remove whole Keystone tree
  900.       if os.path.exists(self._KeystoneDirPath()):
  901.         shutil.rmtree(self._KeystoneDirPath())
  902.     finally:
  903.       if self.is_system:
  904.         self._ClearSystemInstallPermissions()
  905.  
  906.   def RemoveReceipts(self):
  907.     """Remove receipts from Apple's package database, allowing downgrade or
  908.    reinstall."""
  909.     # Only works on system installs
  910.     if self.is_system:
  911.       self._SetSystemInstallPermissions()
  912.       try:
  913.         # In theory we should only handle old-style receipts on older OS
  914.         # versions. However, we don't know the upgrade history of the machine.
  915.         # So we try all variants.
  916.         if os.path.isdir('/Library/Receipts/Keystone.pkg'):
  917.           shutil.rmtree('/Library/Receipts/Keystone.pkg', True)
  918.         if os.path.exists('/Library/Receipts/Keystone.pkg'):
  919.           try:
  920.             os.unlink('/Library/Receipts/Keystone.pkg')
  921.           except OSError:
  922.             pass
  923.         if os.path.isdir('/Library/Receipts/UninstallKeystone.pkg'):
  924.           shutil.rmtree('/Library/Receipts/UninstallKeystone.pkg', True)
  925.         if os.path.exists('/Library/Receipts/UninstallKeystone.pkg'):
  926.           try:
  927.             os.unlink('/Library/Receipts/UninstallKeystone.pkg')
  928.           except OSError:
  929.             pass
  930.         if os.path.isdir('/Library/Receipts/NukeKeystone.pkg'):
  931.           shutil.rmtree('/Library/Receipts/NukeKeystone.pkg', True)
  932.         if os.path.exists('/Library/Receipts/NukeKeystone.pkg'):
  933.           try:
  934.             os.unlink('/Library/Receipts/NukeKeystone.pkg')
  935.           except OSError:
  936.             pass
  937.         # pkgutil where appropriate (ignoring results)
  938.         if os.path.exists('/usr/sbin/pkgutil'):
  939.           self.RunCommand(['/usr/sbin/pkgutil', '--forget',
  940.                            'com.google.pkg.Keystone'])
  941.           self.RunCommand(['/usr/sbin/pkgutil', '--forget',
  942.                            'com.google.pkg.UninstallKeystone'])
  943.           self.RunCommand(['/usr/sbin/pkgutil', '--forget',
  944.                            'com.google.pkg.NukeKeystone'])
  945.       finally:
  946.         self._ClearSystemInstallPermissions()
  947.  
  948.   def FixupProducts(self):
  949.     """Attempt to repair any products might be broken."""
  950.     if self.is_system:
  951.       self._SetSystemInstallPermissions()
  952.     try:
  953.       # Remove the (original) Google Updater manifest files.  Stale manifest
  954.       # caches prevent Updater from checking for updates and downloading its
  955.       # auto-uninstall package. Stomp those files everywhere we can.
  956.       try:
  957.         os.unlink(os.path.expanduser('~/Library/Application Support/'
  958.                                      'Google/SoftwareUpdates/manifest.xml'))
  959.       except OSError:
  960.         pass
  961.       if self.agent_job_uid is not None:
  962.         try:
  963.           os.unlink(os.path.join(pwd.getpwuid(self.agent_job_uid)[5],
  964.                                  'Library/Application Support/'
  965.                                  'Google/SoftwareUpdates/manifest.xml'))
  966.         except OSError:
  967.           pass
  968.       try:
  969.         os.unlink(os.path.join(self.root, 'Library/Application Support/'
  970.                                'Google/SoftwareUpdates/manifest.xml'))
  971.       except OSError:
  972.         pass
  973.       try:
  974.         os.unlink('/Library/Caches/Google/SoftwareUpdates/manifest.xml')
  975.       except OSError:
  976.         pass
  977.  
  978.       # Other repairs require ksadmin
  979.       ksadmin_path = self._KsadminPath()
  980.       if ksadmin_path and os.path.exists(ksadmin_path):
  981.  
  982.         # Fix various Talk plugin problems
  983.         if self.is_system:
  984.           (result, out, errout) = self.RunCommand([ksadmin_path, '--productid',
  985.                                       'com.google.talkplugin', '-p'])
  986.  
  987.           # Google Talk Plugin 1.0.15.1351 can have its existence checker
  988.           # pointing to a deleted directory.  Fix up the xc so it'll update
  989.           # next time.
  990.           if out.find('1.0.15.1351') != -1:
  991.             # Fix the ticket by reregistering it.
  992.             # We can only get here if 1.0.15.1351 is the current version, so
  993.             # it's safe to use that version.
  994.             (result, out, errout) = self.RunCommand([ksadmin_path, '--register',
  995.                 '--productid', 'com.google.talkplugin',
  996.                 '--xcpath',
  997.                 '/Library/Internet Plug-Ins/googletalkbrowserplugin.plugin',
  998.                 '--version', '1.0.15.1351',
  999.                 '--url', OMAHA_SERVER_URL])
  1000.  
  1001.           # Repair tickets presumed lost in the 1.x to 2.x Talk upgrade.
  1002.           if ((out.find('productID=com.google.talkplugin') == -1) and
  1003.               os.path.exists(
  1004.                   '/Library/Internet Plug-Ins/googletalkbrowserplugin.plugin')):
  1005.             # Register Talk again, using a unique version number that will be
  1006.             # updated.
  1007.             (result, out, errout) = self.RunCommand([ksadmin_path, '--register',
  1008.                 '--productid', 'com.google.talkplugin',
  1009.                 '--xcpath',
  1010.                 '/Library/Internet Plug-Ins/googletalkbrowserplugin.plugin',
  1011.                 '--version', '0.1.0.1234',
  1012.                 '--url', OMAHA_SERVER_URL])
  1013.  
  1014.     finally:
  1015.       if self.is_system:
  1016.         self._ClearSystemInstallPermissions()
  1017.  
  1018. # -------------------------------------------------------------------------
  1019.  
  1020. class KeystoneInstallTiger(KeystoneInstall):
  1021.  
  1022.   """Like KeystoneInstall, but overrides a few methods to support 10.4"""
  1023.  
  1024.   def _AgentPlistFileName(self):
  1025.     return None
  1026.  
  1027.   def _DaemonPlistSourceFileName(self):
  1028.     return 'com.google.keystone.daemon4.plist'
  1029.  
  1030.   def _RemoveOldDaemonPlists(self):
  1031.     # Older installers installed this under the wrong name
  1032.     daemon_plist = os.path.join(self._LaunchDaemonConfigDir(),
  1033.                                 self._DaemonPlistSourceFileName())
  1034.     if os.path.exists(daemon_plist):
  1035.       os.unlink(daemon_plist)
  1036.  
  1037.   def _InstallAgentLoginItem(self):
  1038.     # This will write to the Library domain as root/wheel, which is OK because
  1039.     # permissions on /Library/Preferences still allow admin group to modify
  1040.     if self.is_system:
  1041.       domain = '/Library/Preferences/loginwindow'
  1042.     else:
  1043.       domain = 'loginwindow'
  1044.     (result, out, errout) = self.RunCommand(
  1045.         ['/usr/bin/defaults', 'write', domain,
  1046.          'AutoLaunchedApplicationDictionary', '-array-add',
  1047.          '{Hide = 1; Path = "%s"; }' % self._KeystoneAgentPath()])
  1048.     if result == 0:
  1049.       return
  1050.     # An empty AutoLaunchedApplicationDictionary is an empty string,
  1051.     # not an empty array, in which case -array-add chokes.  There is
  1052.     # no easy way to do a typeof(AutoLaunchedApplicationDictionary)
  1053.     # for a plist. Our solution is to catch the error and try a
  1054.     # different way.
  1055.     (result, out, errout) = self.RunCommand(
  1056.         ['/usr/bin/defaults', 'write', domain,
  1057.          'AutoLaunchedApplicationDictionary', '-array',
  1058.          '{Hide = 1; Path = "%s"; }' % self._KeystoneAgentPath()])
  1059.     if result != 0:
  1060.       raise Error(self.package, self.root, AGENT_INSTALL_ERROR_CODE,
  1061.                   'Google Software Update installer Keystone agent login item '
  1062.                   'in domain "%s" failed (%d): %s' % (domain, result, errout))
  1063.  
  1064.   def _ChangeAgentRunStatus(self, start, ignore_failure):
  1065.     """Start the agent as a normal (non-launchd) process on Tiger."""
  1066.     if self.is_system:
  1067.       self._SetSystemInstallPermissions()
  1068.     try:
  1069.       # Start
  1070.       if start:
  1071.         if self.is_system:
  1072.           # Tiger 'sudo' has problems with numeric uid so use username (man
  1073.           # page wrong)
  1074.           username = pwd.getpwuid(self.agent_job_uid)[0]
  1075.           (result, out, errout) = self.RunCommand(['/usr/bin/sudo',
  1076.                                                    '-u', username,
  1077.                                                    '/usr/bin/open',
  1078.                                                    self._KeystoneAgentPath()])
  1079.           if not ignore_failure and result != 0:
  1080.             raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
  1081.                         'Google Software Update installer failed to start '
  1082.                         'system agent for uid %d (%d): %s' %
  1083.                         (self.agent_job_uid, result, errout))
  1084.         else:
  1085.           (result, out, errout) = self.RunCommand(['/usr/bin/open',
  1086.                                                    self._KeystoneAgentPath()])
  1087.           if not ignore_failure and result != 0:
  1088.             raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
  1089.                         'Google Software Update installer failed to start '
  1090.                         'user agent (%d): %s' % (result, errout))
  1091.       # Stop
  1092.       else:
  1093.         if self.is_system:
  1094.           cmd = ['/usr/bin/killall', '-u', str(self.agent_job_uid),
  1095.                  self._AgentProcessName()]
  1096.         else:
  1097.           cmd = ['/usr/bin/killall', self._AgentProcessName()]
  1098.         (result, out, errout) = self.RunCommand(cmd)
  1099.         if (not ignore_failure and result != 0 and
  1100.             out.find('No matching processes') == -1):
  1101.           raise Error(self.package, self.root, AGENT_CONTROL_ERROR_CODE,
  1102.                       'Google Software Update installer failed to kill '
  1103.                       'agent (%d): %s' % (result, errout))
  1104.     finally:
  1105.       if self.is_system:
  1106.         self._ClearSystemInstallPermissions()
  1107.  
  1108.   def _ClearQuarantine(self, path):
  1109.     """Remove LaunchServices quarantine attributes from a file hierarchy."""
  1110.     # Tiger does not implement quarantine (http://support.apple.com/kb/HT3662)
  1111.     return
  1112.  
  1113.  
  1114. # -------------------------------------------------------------------------
  1115.  
  1116. class Keystone(object):
  1117.  
  1118.   """Top-level interface for Keystone install and uninstall.
  1119.  
  1120.  Attributes:
  1121.    install_class: KeystoneInstall subclass to use for installation
  1122.    installer: KeystoneInstall instance (system or user)
  1123.  """
  1124.  
  1125.   def __init__(self, package, root, launchd_setup, start_jobs, self_destruct):
  1126.     # Sanity
  1127.     if package:
  1128.       package = os.path.abspath(os.path.expanduser(package))
  1129.       if not CheckOnePath(package, stat.S_IRUSR):
  1130.         raise Error(package, root, PACKAGE_ERROR_CODE,
  1131.                     'Google Software Update installer missing or unreadable '
  1132.                     'installation package.')
  1133.     if root:
  1134.       expanded_root = os.path.abspath(os.path.expanduser(root))
  1135.       assert (expanded_root and
  1136.               len(expanded_root) > 0), 'Root is empty after expansion.'
  1137.       # Force user-supplied root to pre-exist, this was a side effect of
  1138.       # prior versions of the code and the tests assume its part of the contract
  1139.       if not CheckOnePath(expanded_root, stat.S_IWUSR):
  1140.         raise Error(package, root, BAD_ROOT_ERROR_CODE,
  1141.                     'Google Software Update installer installation location '
  1142.                     'missing or unwritable.')
  1143.       root = expanded_root
  1144.  
  1145.     # Setup installer instances
  1146.     self.install_class = KeystoneInstall
  1147.     if self._IsTiger():
  1148.       self.install_class = KeystoneInstallTiger
  1149.     if self._IsPrivilegedInstall():
  1150.       # Install using privileges on behalf of other user (for agent start)
  1151.       install_uid = self._LocalUserUID()
  1152.       if root is not None:
  1153.         self.installer = self.install_class(package, True, install_uid, root,
  1154.                                             launchd_setup, start_jobs,
  1155.                                             self_destruct)
  1156.       else:
  1157.         self.installer = self.install_class(package, True, install_uid,
  1158.                                             self._DefaultRootForUID(0),
  1159.                                             launchd_setup, start_jobs,
  1160.                                             self_destruct)
  1161.     else:
  1162.       # Non-system install, no attempt at privilege changes
  1163.       if root is not None:
  1164.         self.installer = self.install_class(package, False, None, root,
  1165.                                             launchd_setup, start_jobs,
  1166.                                             self_destruct)
  1167.       else:
  1168.         self.installer = self.install_class(package, False, None,
  1169.                                             self._DefaultRootForUID(
  1170.                                                 self._LocalUserUID()),
  1171.                                             launchd_setup, start_jobs,
  1172.                                             self_destruct)
  1173.  
  1174.   def _LocalUserUID(self):
  1175.     """Return the UID of the local (non-root) user who initiated this
  1176.    install/uninstall.  If we can't figure it out, default to the user
  1177.    on conosle.  We don't want to default to console user in case a
  1178.    FUS happens in the middle of install or uninstall."""
  1179.     uid = os.geteuid()
  1180.     if uid != 0:
  1181.       return uid
  1182.     else:
  1183.       return os.stat('/dev/console')[stat.ST_UID]
  1184.  
  1185.   def _IsLeopardOrLater(self):
  1186.     """Return True if we're on 10.5 or later; else return False."""
  1187.     global FORCE_TIGER
  1188.     if FORCE_TIGER:
  1189.       return False
  1190.     # Ouch!  platform.mac_ver() returns strange results.
  1191.     # ('10.7', ('', '', ''), 'i386')        - 10.7, python2.7
  1192.     # ('10.7.0', ('', '', ''), 'i386')      - 10.7, python2.5 or python2.6
  1193.     # ('10.6.7', ('', '', ''), 'i386')      - 10.6, python2.5 or python2.6
  1194.     # ('10.5.1', ('', '', ''), 'i386')      - 10.5, python2.4 or python2.5
  1195.     # ('', ('', '', ''), '')                - 10.4, python2.3 (also 2.4)
  1196.     (vers, ignored1, ignored2) = platform.mac_ver()
  1197.     splits = vers.split('.')
  1198.     # Try to break down a proper version number
  1199.     if ((len(splits) == 2) or (len(splits) == 3)) and (splits[1] >= '5'):
  1200.       return True
  1201.     # Tiger is rare these days, so unless we're on 2.3 build of Python
  1202.     # assume we must be newer.
  1203.     if (((sys.version_info[0] == 2) and (sys.version_info[1] == 3)) or
  1204.         ((sys.version_info[0] == 2) and (sys.version_info[1] == 4) and
  1205.          (vers == ''))):
  1206.       return False
  1207.     else:
  1208.       return True
  1209.  
  1210.   def _IsTiger(self):
  1211.     """Return the boolean opposite of IsLeopardOrLater()."""
  1212.     if self._IsLeopardOrLater():
  1213.       return False
  1214.     else:
  1215.       return True
  1216.  
  1217.   def _IsPrivilegedInstall(self):
  1218.     """Return True if this is a privileged (root) install."""
  1219.     if os.geteuid() == 0:
  1220.       return True
  1221.     else:
  1222.       return False
  1223.  
  1224.   def _DefaultRootForUID(self, uid):
  1225.     """For the given UID, return the default install root for Keystone (where
  1226.    is is, or where it should be, installed)."""
  1227.     if uid == 0:
  1228.       return '/'
  1229.     else:
  1230.       return pwd.getpwuid(uid)[5]
  1231.  
  1232.   def _ShouldInstall(self):
  1233.     """Return True if we should on install.
  1234.  
  1235.    Possible reasons for punting (returning False):
  1236.    1) This is a System Keystone install and the installed System
  1237.       Keystone has a smaller version.
  1238.    2) This is a User Keystone and there is a System Keystone
  1239.       installed (of any version).
  1240.    3) This is a User Keystone and the installed User Keystone has a
  1241.       smaller version.
  1242.    """
  1243.     if self._IsPrivilegedInstall():
  1244.       if self.installer.IsMyVersionGreaterThanInstalledVersion():
  1245.         return True
  1246.       else:
  1247.         return False
  1248.     else:
  1249.       # User install, need to check if system install exists
  1250.       system_checker = self.install_class(None, False, None,
  1251.                                           self._DefaultRootForUID(0),
  1252.                                           False, False, False)
  1253.       if system_checker.InstalledKeystoneBundleVersion() != None:
  1254.         return False
  1255.       # Check just user version
  1256.       if self.installer.IsMyVersionGreaterThanInstalledVersion():
  1257.         return True
  1258.       else:
  1259.         return False
  1260.  
  1261.   def Install(self, force, lockdown):
  1262.     """Public install interface.
  1263.  
  1264.      force: If True, no version check is performed.
  1265.      lockdown: if True, install a special ticket to lock down Keystone
  1266.                and prevent uninstall.  This will happen even if an install
  1267.                of Keystone itself is not needed.
  1268.    """
  1269.     if self.installer._IsMasterDisabled():
  1270.       raise Error(None, None, MASTER_DISABLE_ERROR_CODE,
  1271.           'Google Software Update installer failed. An administrator has '
  1272.           'disabled Google Software Update.')
  1273.     if force or self._ShouldInstall():
  1274.       self.installer.Install()
  1275.     # possibly lockdown even if we don't need to install
  1276.     if lockdown:
  1277.       self.installer.LockdownKeystone()
  1278.  
  1279.   def Uninstall(self):
  1280.     """Uninstall, which has the effect of preparing this machine for a new
  1281.    install. Although similar, it is NOT as comprehensive as a nuke.
  1282.    """
  1283.     self.installer.Uninstall()
  1284.  
  1285.   def Nuke(self):
  1286.     """Public nuke interface. Typically only used for testing."""
  1287.     self.installer.Nuke()
  1288.  
  1289.   def RemoveReceipts(self):
  1290.     """Public receipt removal interface. Used by uninstall, and to allow
  1291.    downgraades of system installations."""
  1292.     self.installer.RemoveReceipts()
  1293.  
  1294.   def FixupProducts(self):
  1295.     """Attempt to repair any products might have broken tickets."""
  1296.     self.installer.FixupProducts()
  1297.  
  1298. # -------------------------------------------------------------------------
  1299.  
  1300. def PrintUse():
  1301.   print 'Use: '
  1302.   print ' [--install PKG]     Install keystone using PKG as the source.'
  1303.   print ' [--root ROOT]       Use ROOT as the dest for an install. Optional.'
  1304.   print ' [--uninstall]       Remove Keystone program files but do NOT delete '
  1305.   print '                       the ticket store.'
  1306.   print ' [--nuke]            Remove Keystone and all tickets.'
  1307.   print ' [--remove-receipts] Remove Keystone package receipts, allowing for '
  1308.   print '                       downgrade (system install only)'
  1309.   print ' [--no-launchd]      Do NOT touch Keystone launchd plists or jobs,'
  1310.   print '                       for both install and uninstall. For test.'
  1311.   print ' [--no-launchdjobs]  Do NOT start/stop jobs, but do change launchd'
  1312.   print '                       plist files,for both install and uninstall.'
  1313.   print '                       For test.'
  1314.   print ' [--self-destruct]   Use if uninstall is triggered by process that '
  1315.   print '                       will be killed by uninstall.'
  1316.   print ' [--force]           Force an install no matter what. For test.'
  1317.   print ' [--forcetiger]      Pretend we are on Tiger (MacOSX 10.4). For test.'
  1318.   print ' [--failcode]        Fake an error with that code occurred. For test.'
  1319.   print ' [--lockdown]        Prevent Keystone from ever uninstalling itself.'
  1320.   print ' [--interval N]      Change agent plist to wake up every N seconds.'
  1321.   print ' [--help]            This message.'
  1322.  
  1323.  
  1324. def main():
  1325.   os.environ.clear()
  1326.   os.environ['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/libexec'
  1327.  
  1328.   # Make sure AuthorizationExecuteWithPrivileges() is happy
  1329.   if os.getuid() and os.geteuid() == 0:
  1330.     os.setuid(os.geteuid())
  1331.  
  1332.   try:
  1333.     opts, args = getopt.getopt(sys.argv[1:], 'i:r:XunNfI:h',
  1334.                                ['install=', 'root=', 'nuke', 'uninstall',
  1335.                                 'no-launchd', 'no-launchdjobs', 'force',
  1336.                                 'interval=', 'help',
  1337.                                 # Long-only
  1338.                                 'remove-receipts', 'self-destruct',
  1339.                                 'forcetiger', 'failcode=', 'lockdown'])
  1340.   except getopt.GetoptError:
  1341.     print 'Bad options.'
  1342.     PrintUse()
  1343.     sys.exit(USAGE_ERROR_CODE)
  1344.  
  1345.   root = None
  1346.   package = None
  1347.   nuke = False
  1348.   uninstall = False
  1349.   remove_receipts = False
  1350.   launchd_setup = True
  1351.   fail_code = 0
  1352.   start_jobs = True
  1353.   self_destruct = False
  1354.   force = False
  1355.   lockdown = False  # If true, prevent uninstall by adding a "lockdown" ticket
  1356.  
  1357.   for opt, val in opts:
  1358.     if opt in ('-h', '--help'):
  1359.       PrintUse()
  1360.       sys.exit(USAGE_ERROR_CODE)
  1361.  
  1362.     if opt in ['-i', '--install']:
  1363.       package = val
  1364.     if opt in ['-r', '--root']:
  1365.       root = val
  1366.     if opt in ['-X', '--nuke']:
  1367.       nuke = True
  1368.     if opt in ['-u', '--uninstall']:
  1369.       uninstall = True
  1370.     if opt in ['-n', '--no-launchd']:
  1371.       launchd_setup = False
  1372.     if opt in ['-N', '--no-launchdjobs']:
  1373.       start_jobs = False
  1374.     if opt in ['-f', '--force']:
  1375.       force = True
  1376.     if opt in ['-I', '--interval']:
  1377.       global AGENT_START_INTERVAL
  1378.       AGENT_START_INTERVAL = int(val)
  1379.     if opt == '--remove-receipts':
  1380.       remove_receipts = True
  1381.     if opt == '--self-destruct':
  1382.       self_destruct = True
  1383.     if opt == '--forcetiger':
  1384.       global FORCE_TIGER
  1385.       FORCE_TIGER = True
  1386.     if opt == '--failcode':
  1387.       fail_code = int(val)
  1388.     if opt == '--lockdown':
  1389.       lockdown = True
  1390.  
  1391.   if package is None and not nuke and not uninstall and not remove_receipts:
  1392.     print 'Must specify package path, uninstall, nuke, or remove-receipts.'
  1393.     PrintUse()
  1394.     sys.exit(USAGE_ERROR_CODE)
  1395.   try:
  1396.     (vers, ignored1, ignored2) = platform.mac_ver()
  1397.     splits = vers.split('.')
  1398.     if (len(splits) == 3) and (int(splits[1]) < 4):
  1399.       print 'Requires Mac OS 10.4 or later.'
  1400.       sys.exit(UNSUPPORTED_OS_ERROR_CODE)
  1401.   except:
  1402.     # 10.3 throws an exception for platform.mac_ver()
  1403.     print 'Requires Mac OS 10.4 or later.'
  1404.     sys.exit(UNSUPPORTED_OS_ERROR_CODE)
  1405.  
  1406.   # Lock file to make sure only one Keystone install at once. We want to
  1407.   # share this lock amongst all users on the machine.
  1408.   lockfilename = '/tmp/.keystone_install_lock'
  1409.   oldmask = os.umask(0000)
  1410.   lockfile = os.open(lockfilename, os.O_CREAT | os.O_RDONLY | os.O_NOFOLLOW,
  1411.                      0444)
  1412.   os.umask(oldmask)
  1413.   # Lock, callers that cannot wait are expected to kill us.
  1414.   fcntl.flock(lockfile, fcntl.LOCK_EX)
  1415.  
  1416.   try:
  1417.     try:
  1418.       # Simulate a failure
  1419.       if fail_code != 0:
  1420.         raise Error(None, None, fail_code,
  1421.             'Google Software Update installer simulated failure %d' % fail_code)
  1422.       # Do the install
  1423.       k = Keystone(package, root, launchd_setup, start_jobs, self_destruct)
  1424.       # Ordered by level of cleanup applied
  1425.       if nuke:
  1426.         k.Nuke()
  1427.       elif uninstall:
  1428.         k.Uninstall()
  1429.       elif remove_receipts:
  1430.         k.RemoveReceipts()
  1431.       else:
  1432.         k.Install(force, lockdown)
  1433.         k.FixupProducts()
  1434.     except Error, e:
  1435.       # To conform to previous contract on this tool (see headerdoc)
  1436.       print e.message()
  1437.       # We want the backtrace on stderr, but we need to control the exit code
  1438.       # so dump manually
  1439.       traceback.print_exc(file=sys.stderr)
  1440.       # exit with the right error code
  1441.       sys.exit(e.errorcode())
  1442.   finally:
  1443.     os.close(lockfile)  # Lock file left around on purpose
  1444.  
  1445. if __name__ == '__main__':
  1446.   main()
Add Comment
Please, Sign In to add comment