Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(141)

Side by Side Diff: chrome_mac/Google Chrome.app/Contents/Versions/32.0.1700.19/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/install.py

Issue 85333005: Update reference builds to Chrome 32.0.1700.19 (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/deps/reference_builds/
Patch Set: Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
Property Changes:
Added: svn:executable
+ *
OLDNEW
(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()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698