OLD | NEW |
(Empty) | |
| 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() |
OLD | NEW |