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

Side by Side Diff: remoting/host/installer/mac/Keystone/GoogleSoftwareUpdate.pkg/Contents/Resources/install.py

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

Powered by Google App Engine
This is Rietveld 408576698