Rixstep
 About | ACP | Buy | Industry Watch | Learning Curve | News | Products | Search | Substack
Home » Learning Curve » ACP Guru

Google Chrome 4.0.223.8

We're just sayin'.


Get It

Try It

It's more complete than before but the welcome dialog with the options probably won't float your boat. And with the Spotify-style prompts to do you a favour and save your passwords - same thing.

The focus rectangles are ugly. Cookies are on by default and there's no setting to turn off JavaScript. Great design, boys 'n' girls.

The scroll bars don't know how to turn off - they just get longer and longer even though there's nothing to scroll. How they do this with good classes like Cocoa is a mystery. The prefs window is incorrectly sized so text runs off the right and can't be read.

The download has almost more languages than exist and get this: every file in the 600+ file download has com.apple.FinderInfo. That's quite the feat. Apple's new NIB strip escapes these people as well.

It's 17 MB to download. The total footprint after removing fourteen million localisations but before further cleaning is 42,518,779 bytes. Almost all executable images are i386-only. [Five are both ppc and i386.]

Safety tip: copy the bundle out and dismount the DMG so the dotted keystone script doesn't get a chance to run.

#!/bin/bash

# Copyright (c) 2009 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Called by the Keystone system to update the installed application with a new
# version from a disk image.

# Return values:
# 0  Happiness
# 1  Unknown failure
# 2  Basic sanity check destination failure (e.g. ticket points to nothing)
# 3  (currently unused) indicates a problem with the existing installed copy
# 4  rsync failed (could not assure presence of Versions directory)
# 5  rsync failed (could not copy new versioned directory to Versions)
# 6  rsync failed (could not update outer .app bundle)
# 7  Could not get the version, update URL, or channel after update
# 8  Updated application does not have the version number from the update
# 9  ksadmin failure
# 10 Basic sanity check source failure (e.g. no app on disk image)

set -e

# The argument should be the disk image path.  Make sure it exists.
if [ $# -lt 1 ] || [ ! -d "${1}" ]; then
  exit 10
fi

# Who we are.
PRODUCT_NAME="Google Chrome"
APP_DIR="${PRODUCT_NAME}.app"
FRAMEWORK_NAME="${PRODUCT_NAME} Framework"
FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework"
SRC="${1}/${APP_DIR}"

# Make sure that there's something to copy from, and that it's an absolute
# path.
if [ -z "${SRC}" ] || [ "${SRC:0:1}" != "/" ] || [ ! -d "${SRC}" ] ; then
  exit 10
fi

# Figure out where we're going.  Determine the application version to be
# installed, use that to locate the framework, and then look inside the
# framework for the Keystone product ID.
APP_VERSION_KEY="CFBundleShortVersionString"
UPD_VERSION_APP=$(defaults read "${SRC}/Contents/Info" "${APP_VERSION_KEY}" ||
                  exit 10)
UPD_KS_PLIST="${SRC}/Contents/Versions/${UPD_VERSION_APP}/${FRAMEWORK_DIR}/Resources/Info"
KS_VERSION_KEY="KSVersion"
UPD_VERSION_KS=$(defaults read "${UPD_KS_PLIST}" "${KS_VERSION_KEY}" || exit 10)
PRODUCT_ID=$(defaults read "${UPD_KS_PLIST}" KSProductID || exit 10)
if [ -z "${UPD_VERSION_KS}" ] || [ -z "${PRODUCT_ID}" ] ; then
  exit 2
fi
DEST=$(ksadmin -pP "${PRODUCT_ID}" |
       sed -Ene \
           's%^[[:space:]]+xc=$%\1%p')

# More sanity checking.
if [ -z "${DEST}" ] || [ ! -d "${DEST}" ]; then
  exit 2
fi

# Figure out what the existing version is using for its versioned directory.
# This will be used later, to avoid removing the currently-installed version's
# versioned directory in case anything is still using it.
OLD_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" ||
                  true)
OLD_VERSIONED_DIR="${DEST}/Contents/Versions/${OLD_VERSION_APP}"

# Don't use rsync -a, because -a expands to -rlptgoD.  -g and -o copy owners
# and groups, respectively, from the source, and that is undesirable in this
# case.  -D copies devices and special files; copying devices only works
# when running as root, so for consistency between privileged and unprivileged
# operation, this option is omitted as well.
#  -c, --checksum              skip based on checksum, not mod-time & size
#  -l, --links                 copy symlinks as symlinks
#  -r, --recursive             recurse into directories
#  -p, --perms                 preserve permissions
#  -t, --times                 preserve times
RSYNC_FLAGS="-clprt"

# By copying to ${DEST}, the existing application name will be preserved, even
# if the user has renamed the application on disk.  Respecting the user's
# changes is friendly.

# Make sure that the Versions directory exists, so that it can receive the
# versioned directory.  It may not exist if updating from an older version
# that did not use the versioned layout on disk.  An rsync that excludes all
# contents is used to bring the permissions over from the update's Versions
# directory, otherwise, this directory would be the only one in the entire
# update exempt from getting its permissions copied over.  A simple mkdir
# wouldn't copy mode bits.  This is done even if ${DEST}/Contents/Versions
# already does exist to ensure that the mode bits come from the udpate.
rsync ${RSYNC_FLAGS} --exclude "*" "${SRC}/Contents/Versions/" \
                                   "${DEST}/Contents/Versions" || exit 4

# Copy the versioned directory.  The new versioned directory will have a
# different name than any existing one, so this won't harm anything already
# present in Contents/Versions, including the versioned directory being used
# by any running processes.  If this step is interrupted, there will be an
# incomplete versioned directory left behind, but it won't interfere with
# anything, and it will be replaced or removed during a future update attempt.
NEW_VERSIONED_DIR="${DEST}/Contents/Versions/${UPD_VERSION_APP}"
rsync ${RSYNC_FLAGS} --delete-before \
    "${SRC}/Contents/Versions/${UPD_VERSION_APP}/" \
    "${NEW_VERSIONED_DIR}" || exit 5

# See if the timestamp of what's currently on disk is newer than the update's
# outer .app's timestamp.  rsync will copy the update's timestamp over, but
# if that timestamp isn't as recent as what's already on disk, the .app will
# need to be touched.
NEEDS_TOUCH=
if [ "${DEST}" -nt "${SRC}" ] ; then
  NEEDS_TOUCH=1
fi

# Copy the unversioned files into place, leaving everything in
# Contents/Versions alone.  If this step is interrupted, the application will
# at least remain in a usable state, although it may not pass signature
# validation.  Depending on when this step is interrupted, the application
# will either launch the old or the new version.  The critical point is when
# the main executable is replaced.  There isn't very much to copy in this step,
# because most of the application is in the versioned directory.  This step
# only accounts for around 50 files, most of which are small localized
# InfoPlist.strings files.
rsync ${RSYNC_FLAGS} --delete-after --exclude /Contents/Versions \
    "${SRC}/" "${DEST}" || exit 6

# If necessary, touch the outermost .app so that it appears to the outside
# world that something was done to the bundle.  This will cause LaunchServices
# to invalidate the information it has cached about the bundle even if
# lsregister does not run.  This is not done if rsync already updated the
# timestamp to something newer than what had been on disk.  This is not
# considered a critical step, and if it fails, this script will not exit.
if [ -n "${NEEDS_TOUCH}" ] ; then
  touch -cf "${DEST}" || true
fi

# Read the new values (e.g. version).  Get the installed application version
# to get the path to the framework, where the Keystone keys are stored.
NEW_VERSION_APP=$(defaults read "${DEST}/Contents/Info" "${APP_VERSION_KEY}" ||
                  exit 7)
NEW_KS_PLIST="${DEST}/Contents/Versions/${NEW_VERSION_APP}/${FRAMEWORK_DIR}/Resources/Info"
NEW_VERSION_KS=$(defaults read "${NEW_KS_PLIST}" "${KS_VERSION_KEY}" || exit 7)
URL=$(defaults read "${NEW_KS_PLIST}" KSUpdateURL || exit 7)
# The channel ID is optional.  Suppress stderr to prevent Keystone from seeing
# possible error output.
CHANNEL_ID=$(defaults read "${NEW_KS_PLIST}" KSChannelID 2>/dev/null || true)

# Make sure that the update was successful by comparing the version found in
# the update with the version now on disk.
if [ "${NEW_VERSION_KS}" != "${UPD_VERSION_KS}" ]; then
  exit 8
fi

# Notify LaunchServices.  This is not considered a critical step, and
# lsregister's exit codes shouldn't be confused with this script's own.
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister "${DEST}" || true

# Notify Keystone.
KSADMIN_VERSION=$(ksadmin --ksadmin-version || true)
if [ -n "${KSADMIN_VERSION}" ] ; then
  # If ksadmin recognizes --ksadmin-version, it will recognize --tag.
  ksadmin --register \
          -P "${PRODUCT_ID}" \
          --version "${NEW_VERSION_KS}" \
          --xcpath "${DEST}" \
          --url "${URL}" \
          --tag "${CHANNEL_ID}" || exit 9
else
  # Older versions of ksadmin don't recognize --tag.  The application will
  # set the tag when it runs.
  ksadmin --register \
          -P "${PRODUCT_ID}" \
          --version "${NEW_VERSION_KS}" \
          --xcpath "${DEST}" \
          --url "${URL}" || exit 9
fi

# The remaining steps are not considered critical.
set +e

# Try to clean up old versions that are not in use.  The strategy is to keep
# the versioned directory corresponding to the update just applied
# (obviously) and the version that was just replaced, and to use ps and lsof
# to see if it looks like any processes are currently using any other old
# directories.  Directories not in use are removed.  Old versioned directories
# that are in use are left alone so as to not interfere with running
# processes.  These directories can be cleaned up by this script on future
# updates.
#
# To determine which directories are in use, both ps and lsof are used.  Each
# approach has limitations.
#
# The ps check looks for processes within the verisoned directory.  Only
# helper processes, such as renderers, are within the versioned directory.
# Browser processes are not, so the ps check will not find them, and will
# assume that a versioned directory is not in use if a browser is open without
# any windows.  The ps mechanism can also only detect processes running on the
# system that is performing the update.  If network shares are involved, all
# bets are off.
#
# The lsof check looks to see what processes have the framework dylib open.
# Browser processes will have their versioned framework dylib open, so this
# check is able to catch browsers even if there are no associated helper
# processes.  Like the ps check, the lsof check is limited to processes on
# the system that is performing the update.  Finally, unless running as root,
# the lsof check can only find processes running as the effective user
# performing the update.
#
# These limitations are motiviations to additionally preserve the versioned
# directory corresponding to the version that was just replaced.

# Set the nullglob option.  This causes a glob pattern that doesn't match
# any files to expand to an empty string, instead of expanding to the glob
# pattern itself.  This means that if /path/* doesn't match anything, it will
# expand to "" instead of, literally, "/path/*".  The glob used in the loop
# below is not expected to expand to nothing, but nullglob will prevent the
# loop from trying to remove nonexistent directories by weird names with
# funny characters in them.
shopt -s nullglob

for versioned_dir in "${DEST}/Contents/Versions/"* ; do
  if [ "${versioned_dir}" = "${NEW_VERSIONED_DIR}" ] || \
     [ "${versioned_dir}" = "${OLD_VERSIONED_DIR}" ] ; then
    # This is the versioned directory corresponding to the update that was
    # just applied or the version that was previously in use.  Leave it alone.
    continue
  fi

  # Look for any processes whose executables are within this versioned
  # directory.  They'll be helper processes, such as renderers.  Their
  # existence indicates that this versioned directory is currently in use.
  PS_STRING="${versioned_dir}/"

  # Look for any processes using the framework dylib.  This will catch
  # browser processes where the ps check will not, but it is limited to
  # processes running as the effective user.
  LSOF_FILE="${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}"

  # ps -e displays all users' processes, -ww causes ps to not truncate lines,
  # -o comm instructs it to only print the command name, and the = tells it to
  # not print a header line.
  # The cut invocation filters the ps output to only have at most the number
  # of characters in ${PS_STRING}.  This is done so that grep can look for an
  # exact match.
  # grep -F tells grep to look for lines that are exact matches (not regular
  # expressions), -q tells it to not print any output and just indicate
  # matches by exit status, and -x tells it that the entire line must match
  # ${PS_STRING} exactly, as opposed to matching a substring.  A match
  # causes grep to exit zero (true).
  #
  # lsof will exit nonzero if ${LSOF_FILE} does not exist or is open by any
  # process.  If the file exists and is open, it will exit zero (true).
  if (! ps -ewwo comm= | \
        cut -c "1-${#PS_STRING}" | \
        grep -Fqx "${PS_STRING}") &&
     (! lsof "${LSOF_FILE}" >& /dev/null) ; then
    # It doesn't look like anything is using this versioned directory.  Get rid
    # of it.
    rm -rf "${versioned_dir}"
  fi
done

# If this script is not running as root (indicating an update driven by user
# Keystone) and the application is installed somewhere under /Applications,
# try to make it writeable by all admin users.  This will allow other admin
# users to update the application from their own user Keystone instances.
#
# If this script is running as root, it's driven by system Keystone, and
# future updates can be expected to be applied the same way, so
# admin-writeability is not a concern.
#
# If the application is not installed under /Applications, it might not be in
# a system-wide location, and it probably won't be something that other users
# are running, so err on the side of safety and don't make it group-writeable.
#
# If this script is running as a user that is not a member of the admin group,
# this operation will not succeed.  Tolerate that case, because it's better
# than the alternative, which is to make the applicaiton world-writeable.
if [ ${EUID} -ne 0 ] && [ "${DEST:0:14}" = "/Applications/" ] ; then
  (chgrp -Rfh admin "${DEST}" && chmod -Rfh g+w "${DEST}") >& /dev/null
fi

# Great success!
exit 0

The monster's Google Chrome Framework - i386-only for a walloping 36,232,688 bytes. Here's the linkedit info.

Google Chrome Framework:
@executable_path/../Versions/4.0.223.8/Google Chrome Framework.framework/Google Chrome Framework (compatibility version 223.8.0, current version 223.8.0)
/System/Library/Frameworks/AppKit.framework/Versions/C/AppKit (compatibility version 45.0.0, current version 949.0.0)
/System/Library/Frameworks/Carbon.framework/Versions/A/Carbon (compatibility version 2.0.0, current version 136.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 476.0.0)
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 677.12.0)
/System/Library/Frameworks/Security.framework/Versions/A/Security (compatibility version 1.0.0, current version 31122.0.0)
/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration (compatibility version 1.0.0, current version 204.0.0)
/System/Library/Frameworks/SecurityInterface.framework/Versions/A/SecurityInterface (compatibility version 1.0.0, current version 32532.0.0)
/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore (compatibility version 1.2.0, current version 1.5.0)
/System/Library/Frameworks/AudioToolbox.framework/Versions/A/AudioToolbox (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
/usr/lib/libcrypto.0.9.7.dylib (compatibility version 0.9.7, current version 0.9.7)
/usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 7.4.0)
/usr/lib/libgcc_s.1.dylib (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 111.0.0)
/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices (compatibility version 1.0.0, current version 32.0.0)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 227.0.0)
/System/Library/Frameworks/ApplicationServices.framework/Versions/A/ApplicationServices (compatibility version 1.0.0, current version 34.0.0)

There's a file called Keystone.tbz in there. Oh so cute. It in turn contains GoogleSoftwareUpdate.bundle. And that bundle in turn contains GoogleSoftwareUpdateAgent.app. 'Russian doll architecture'. Also found is CheckForUpdatesNow.command.

#!/bin/bash
#
# CheckForUpdatesNow.command
# Keystone
#

# Determine directory where this script is running from
script_dir=$(dirname $(echo $0 | sed -e "s,^\([^/]\),$(pwd)/\1,"))

agent="$script_dir"/GoogleSoftwareUpdateAgent.app/Contents/MacOS/GoogleSoftwareUpdateAgent

if [ ! -x "$agent" ]; then
    echo "Can't figure out how to update now."
    exit 1
fi

"$agent" -runMode oneshot

Here are two files to make you relax. A browser with daemons? Perhaps not Google's but an ordinary innocent one?

com.google.keystone.daemon.plist
com.google.keystone.daemon4.plist

Here's com.google.keystone.agent.plist. This isn't in an OS X compatible format.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>Label</key>
   <string>com.google.keystone.${INSTALL_TYPE}.agent</string>
   <key>LimitLoadToSessionType</key>
   <string>Aqua</string>
   <key>ProgramArguments</key>
   <array>
     <string>${INSTALL_ROOT}/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/Resources/GoogleSoftwareUpdateAgent.app/Contents/MacOS/GoogleSoftwareUpdateAgent</string>
     <string>-runMode</string>
     <string>ifneeded</string>
   </array>
   <key>RunAtLoad</key>
   <true/>
   <key>StartInterval</key>
   <integer>${START_INTERVAL}</integer>
   <key>StandardErrorPath</key>
   <string>/dev/null</string>
   <key>StandardOutPath</key>
   <string>/dev/null</string>
</dict>
</plist>

com.google.keystone.daemon.plist.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>KeepAlive</key>
   <false/>
   <key>Label</key>
   <string>com.google.keystone.daemon</string>
   <key>MachServices</key>
   <dict>
      <key>com.google.Keystone.Daemon</key>
      <true/>
   </dict>
   <key>ProgramArguments</key>
   <array>
      <string>${INSTALL_ROOT}/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/MacOS/GoogleSoftwareUpdateDaemon</string>
      <string>-onDemand</string>
      <string>YES</string>
   </array>
   <key>RunAtLoad</key>
   <false/>
   <key>StandardErrorPath</key>
   <string>/dev/null</string>
   <key>StandardOutPath</key>
   <string>/dev/null</string>
   <key>UserName</key>
   <string>root</string>
</dict>
</plist>

com.google.keystone.daemon4.plist.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>Label</key>
   <string>com.google.keystone.daemon</string>
   <key>MachServices</key>
   <dict>
      <key>com.google.Keystone.Daemon</key>
      <dict>
         <key>HideUntilCheckIn</key>
         <true/>
      </dict>
   </dict>
   <key>OnDemand</key>
   <false/>
   <key>Program</key>
   <string>${INSTALL_ROOT}/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/MacOS/GoogleSoftwareUpdateDaemon</string>
   <key>RunAtLoad</key>
   <true/>
   <key>StandardErrorPath</key>
   <string>/dev/null</string>
   <key>StandardOutPath</key>
   <string>/dev/null</string>
</dict>
</plist>

The 'hidden' Keystone archive has 76 files, about a dozen languages, 1,058,118 bytes. It's all hidden on your install inside a TBZ. Just so you get an idea of the extent: 26 of those 76 files are directories. And there's a truckload of executable binaries as well.

5 items, 856452 bytes, 1688 blocks, 0 bytes in extended attributes.

Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/Frameworks/KeystoneCommon.framework/Versions/A/KeystoneCommon
Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/Frameworks/UpdateEngine.framework/Versions/A/UpdateEngine
Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/MacOS/GoogleSoftwareUpdateDaemon
Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/MacOS/ksadmin
Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/Resources/GoogleSoftwareUpdateAgent.app/Contents/MacOS/GoogleSoftwareUpdateAgent

Here's the keystone install script. It's in the same directory as the TBZ.

#!/usr/bin/python
# Copyright 2008 Google Inc.  All rights reserved.

"""This script will install Keystone in the correct context
(system-wide or per-user).  It can also uninstall Keystone.  is run by
KeystoneRegistration.framework.

Example command lines for testing:
Install:    install.py --install=/tmp/Keystone.tbz --root=/Users/fred
Uninstall:  install.py --nuke --root=/Users/fred

Example real command lines, for user and root install and uninstall:
  install.py --install Keystone.tbz
  install.py --nuke
  sudo install.py --install Keystone.tbz
  sudo install.py --nuke

For a system-wide Keystone, the install root is "/".  Run with --help
for a list of options.  Use --no-processes to NOT start background
processes (e.g. launchd item).

Errors can happen if:
- we don't have write permission to install in the given root
- pieces of our install are missing

On error, we print an message on stdout and our exit status is
non-zero.  On success, we print nothing and exit with a status of 0.
"""

import os
import re
import sys
import pwd
import stat
import glob
import MacOS
import fcntl
import getopt
import signal
import shutil
import platform
from popen2 import Popen4
from posix import umask

# Allow us to force the installer to think we're on Tiger (10.4)
gForceTiger = False

# Allow us to adjust the agent launch interval (for testing).
# In seconds.
gAgentStartInterval = 8651

# Name of our "lockdown" ticket.  If you change this name be sure to
# change it in other places in the code (grep is your friend)
LOCKDOWN_TICKET = 'com.google.Keystone.Lockdown'

class Failure(Exception):
  """Generic exception for Keystone install failure."""

  def __init__(self, package, root, error):
    self.package = package
    self.root = root
    self.error = error

  def __str__(self):
    return 'File %s, root %s, Error %s' % (self.package, self.root,
                                           self.error)


def CheckOnePath(file, statmode):
  """Sanity check a file or directory as requested.  On failure throw
  an exception."""
  if os.path.exists(file):
    st = os.stat(file)
    if (st.st_mode & statmode) != 0:
      return
  raise Failure(file, "None", "Bad access for " + file)


# -------------------------------------------------------------------------

class KeystoneInstall(object):

  """Worker object which does the heavy lifting of install or uninstall.
  By default it assumes 10.5 (Leopard).

  Attributes:
   keystone: owning Keystone object
   uid: the relevant uid for our install/uninstall.
     0 is System Keystone; else a UID.
   root: root directory for install.  On System this would be "/";
     else would be a user home directory (unless testing, in which case
     the root can be anywhere).
   myBundleVersion: a cached value of the Keystone bundle version in "package".

   Conventions:
   All functions which return directory paths end in '/'
     """

  def __init__(self, keystone, uid, root):
    self.keystone = keystone
    self.uid = uid
    self.root = root
    if not self.root.endswith('/'):
      self.root = self.root + '/'
    self.myBundleVersion = None

  def KeystoneDir(self):
    """Return the subdirectory where Keystone.bundle is or will be.
    Does not sanity check the directory."""
    return self.root + 'Library/Google/GoogleSoftwareUpdate/'

  def KeystoneBundle(self):
    """Return the location of Keystone.bundle."""
    return self.KeystoneDir() + 'GoogleSoftwareUpdate.bundle/'

  def GetKsadmin(self):
    """Return a path to ksadmin which will exist only AFTER Keystone is
    installed.  Return None if it doesn't exist."""
    ksadmin = self.KeystoneBundle() + 'Contents/MacOS/ksadmin'
    if not os.path.exists(ksadmin):
      return None
    return ksadmin

  def InstalledKeystoneBundleVersion(self):
    """Return the version of an installed Keystone bundle, or None if
    not installed.  Specifically, it returns the CFBundleVersion as a
    string (e.g. "0.1.0.0").  Invariant: we require a 4-digit version
    when building Keystone.bundle."""
    plist = self.KeystoneBundle() + 'Contents/Info.plist'
    if not os.path.exists(plist):
      return None
    cmds = [ '/usr/bin/defaults', 'read',
             self.KeystoneBundle() + 'Contents/Info',
             'CFBundleVersion' ]
    (stdin_ignored, stdoutfile, stderrfile) = os.popen3(cmds)
    stdout = stdoutfile.read().strip()
    return stdout

  def MyKeystoneBundleVersion(self):
    """Return the version of our Keystone bundle which we might want to install.
    Specifically, it returns the CFBundleVersion as a string (e.g. "0.1.0.0").
    Invariant: we require a 4-digit version when building Keystone.bundle."""
    if self.myBundleVersion == None:
      cmds = ['/usr/bin/tar', '-Oxjf',
              self.keystone.package,
              'GoogleSoftwareUpdate.bundle/Contents/Info.plist']
      (stdin_ignored, stdoutfile, stderrfile) = os.popen3(cmds)
      stdout = stdoutfile.read()
      # walking by index instead of implicit iterator so we can easily
      # "get next"
      linelist = stdout.splitlines()
      for i in range(len(linelist)):
        if linelist[i].find('<key>CFBundleVersion</key>') != -1:
          version = linelist[i+1].strip()
          version = version.strip('<string>').strip('</string>')
          self.myBundleVersion = version
          break
    return self.myBundleVersion

  def IsMyVersionGreaterThanInstalledVersion(self):
    """Return True if my Keystone version is greater than the installed version.
    Else return False.  Like above, assumes a 4-digit version."""
    myversion = self.MyKeystoneBundleVersion()
    insversion = self.InstalledKeystoneBundleVersion()
    if ((insversion == None) or (myversion == None)):
      return True
    else:
      myversion = myversion.split('.')
      insversion = insversion.split('.')
    if len(myversion) != len(insversion):
      return True
    for my, ins in zip(myversion, insversion):
      if my > ins:
        return True
      elif my < ins:
        return False
    # If we get here, it's a complete match, so no.
    return False

  def KeystoneResources(self):
    """Return the subdirectory where Keystone.bundle's resources should be.
    Does not sanity check the directory."""
    return self.KeystoneBundle() + 'Contents/Resources/'

  def KeystoneAgentPath(self):
    """Returns a path to KeystoneAgent.app. Does not sanity check the
    directory."""
    return (self.KeystoneDir() + 'GoogleSoftwareUpdate.bundle/Contents/'
            + 'Resources/GoogleSoftwareUpdateAgent.app')

  def MakeDirectories(self, doLaunchdPlists):
    """Make directories for our package if needed.  Note conditional on
    doLaunchdPlists."""
    dirs = [ self.KeystoneDir() ]
    if doLaunchdPlists:
      dirs.append(self.LaunchAgentConfigDir())
      if self.uid == 0:
        dirs.append(self.LaunchDaemonConfigDir())
    umask(022)
    for d in dirs:
      p = Popen4(['/bin/mkdir', '-p', d])
      result = p.wait()
      if os.WEXITSTATUS(result) != 0:
        raise Failure(self.keystone.package, self.root, 'mkdir -p')

  def InstallPackage(self):
    "Extract self.keystone.package into self.root."
    d = self.KeystoneDir()
    cmds = ['/usr/bin/tar', 'xjf', self.keystone.package, '--no-same-owner',
            '--directory', d]
    p = Popen4(cmds)
    result = p.wait()
    # runoutput = p.fromchild.read()
    if os.WEXITSTATUS(result) != 0:
      raise Failure(self.keystone.package, self.root, 'extract command')

  def DeleteDirectoryAndContents(self, d):
    """Delete |dir| and all it's contents with rm -rf."""
    d = self.KeystoneBundle()
    CheckOnePath(self.root, stat.S_IWUSR)
    cmds = ['/bin/rm', '-rf', d]
    p = Popen4(cmds)
    p.wait()

  # TODO: add a unit test for this
  def UninstallPackage(self):
    """Remove our package (opposite of ExtractPackage()) Note we
    delete the bundle, NOT the $ROOT/Library/Google/GoogleSoftwareUpdate
    directory, so that all tickets aren't blown away on
    upgrade/install.  DO uninstall our own ticket."""
    cmds = [self.GetKsadmin(), '--delete', '--productid',
            'com.google.Keystone'];
    if (self.uid != 0) and (os.geteuid() == 0):
      # We are promoting; be sure to delete the ticket as the right user.
      self.RunCommandAsUID(cmds, -1, self.uid)
    else:
      p = Popen4(cmds)
      p.wait() # rtn ignored
    self.DeleteDirectoryAndContents(self.KeystoneBundle())

  def DeleteCache(self):
    """Deletes any cached download files."""
    cachedirs = glob.glob(self.root + 'Library/Caches/com.google.Keystone.*')
    for dir in cachedirs:
      shutil.rmtree(dir, True)

  def FullUninstallOfDirectories(self):
    """*DOES* uninstall as much as possible (including ticket files)."""
    self.DeleteDirectoryAndContents(self.KeystoneDir())
    ksdir = self.KeystoneDir()
    CheckOnePath(self.root, stat.S_IWUSR)
    cmds = ['/bin/rm', '-rf', ksdir]
    p = Popen4(cmds)
    p.wait()

  def GetKeystoneTicketURL(self):
    """Return the URL for Keystone's ticket, possibly from a defaults file."""
    cmds = [ '/usr/bin/defaults', 'read',
             'com.google.KeystoneInstall', 'URL' ]
    (stdin_ignored, stdoutfile, stderrfile) = os.popen3(cmds)
    stdout = stdoutfile.read().strip()
    if len(stdout) > 0:
      return stdout
    else:
      return 'https://tools.google.com/service/update2'

  def MakeTicketForKeystone(self):
    """Install a ticket for Keystone itself."""
    ksadmin = self.GetKsadmin()
    if ksadmin == None:
      raise Failure(self.keystone.package, self.root,
                    "Can't use ksadmin if not installed")
    # may not exist yet...
    p = Popen4(['/bin/mkdir', '-p', self.KeystoneDir() + 'TicketStore'])
    p.wait()
    # Finally, register.
    url = self.GetKeystoneTicketURL()
    cmds = [ksadmin,
            # store is specified explicitly so unit tests work
            '--store', self.KeystoneDir() + 'TicketStore/Keystone.ticketstore',
            '--register',
            '--productid', 'com.google.Keystone',
            '--version', self.InstalledKeystoneBundleVersion(),
            '--xcpath', ksadmin,
            '--url', url,
            '--preserve-tttoken']
    p = Popen4(cmds)
    p.wait() # rtn ignored

  def LockdownKeystone(self):
    """Prevent Keystone from ever self-uninstalling.

    This is necessary for a System Keystone used for Trusted Tester support.
    We do this by installing (and never uninstalling) a system ticket.
    """
    ksadmin = self.GetKsadmin()
    if ksadmin == None:
      raise Failure(self.keystone.package, self.root,
                    "Can't use ksadmin if not installed")
    url = self.GetKeystoneTicketURL()
    cmds = [ksadmin,
            # store is specified explicitly so unit tests work
            '--store', self.KeystoneDir() + 'TicketStore/Keystone.ticketstore',
            '--register',
            '--productid', LOCKDOWN_TICKET,
            '--version', '1.0',
            '--xcpath', '/',
            '--url', url]
    p = Popen4(cmds)
    p.wait() # rtn ignored

  def LaunchAgentConfigDir(self):
    """Return the destination directory where launch agents should go."""
    return self.root + '/Library/LaunchAgents/'

  def LaunchDaemonConfigDir(self):
    """Return the destination directory where launch daemons should go.
    Only used on a root install."""
    return self.root + '/Library/LaunchDaemons/'

  def InstalledPlistsForRootInstall(self):
    """Return a list of plists which are supposed to be installed
    (destination paths) if we are a root install.  If we are not a root install,
    return an empty list.  Does NOT check they actually exist.  Called by
    both the 10.4 and 10.5 versions of self.InstalledPlists()"""
    plists = []
    if self.uid == 0:
      plists.append(self.LaunchDaemonConfigDir() +
                    'com.google.keystone.daemon.plist')
    return plists

  def InstalledPlists(self):
    """Return a list of plists which are supposed to be installed
    (destination paths).  Does NOT check they actually exist.
    10.5 version."""
    plists = [ self.LaunchAgentConfigDir() + 'com.google.keystone.agent.plist' ]
    plists.extend(self.InstalledPlistsForRootInstall())
    return plists

  def Plists(self):
    """Return an array of all launchd plists we care about.  These are
    not full paths.  These values are used as a SOURCE pathname (not
    fully qualified) for plists to install.  On 10.4, return only the
    daemon, and use the 10.4 daemon script."""
    # trim all except the last path component
    plists = map(lambda x: x.split('/')[-1], self.InstalledPlists())
    # On 10.4 use the always-running launchd config for the daemon
    if not self.keystone.IsLeopardOrLater():
      plists = map(lambda x: x.replace('.daemon.', '.daemon4.'), plists)
    return plists

  def RunCommand(self, cmds, rtn):
    """Run a command with args.  If rtn is not -1 and it doesn't match
    the proc return code, throw an exception.  Throws away output."""
    p = Popen4(cmds)
    result = p.wait()
    if (rtn != -1) and (os.WEXITSTATUS(result) != rtn):
      raise Failure(self.keystone.package, self.root,
                                    'Command failed.  cmd=' +
                                    str(cmds) + ' rtn=' +
                                    str(result))

  def StartProcessesForCurrentContext(self):
    """Start running processes (e.g. launchd item).
    If root, we start the daemon, then start the agent as the console user.
    If not, we just start the agent (as ourself)."""
    for plist in self.InstalledPlists():
      if not os.path.exists(plist):
        raise Failure(self.keystone.package, self.root, "didn't find plist")
    self.ChangeProcessRunState(True, 0)

  def StopProcessesForCurrentContext(self):
    """Stop some running processes; opposite of
    StartProcessesForCurrentContext().  If root, we stop the daemon as
    ourself, then stop the agent as the console user.  If not root, we
    just stop the agent (as ourself).  Ignores agents running in other
    contexts."""
    # We don't care if this works (e.g. uninstall before an install),
    # so -1 on rtn.
    self.ChangeProcessRunState(False, -1)

  def DoCommandOnAgentProcess(self, cmd, pid):
    """Stop one agent process specified by |pid| with |cmd|
    (e.g.'load', 'unload').
    10.5 version; agent is launchd job."""
    cmds = ['/bin/launchctl', 'bsexec', str(pid),
            '/bin/launchctl', cmd, '-S', 'Aqua',
            self.LaunchAgentConfigDir() + 'com.google.keystone.agent.plist']
    self.RunCommand(cmds, -1)

  def StopAllAgentProcesses(self):
    """Stop the agent running in any context (e.g. multi-user login).
    Not a good idea on upgrade, since we can't easily restart them."""
    if self.uid == 0:
      (stdin_ignored, stdoutfile, stderrfile) = os.popen3(['/bin/ps', 'auxwww'])
      stdout = stdoutfile.read()
      for s in stdout.splitlines():
        if s.endswith(' /Library/Google/GoogleSoftwareUpdate/' +
                      'GoogleSoftwareUpdate.bundle/Contents/' +
                      'Resources/GoogleSoftwareUpdateAgent.app/' +
                      'Contents/MacOS/GoogleSoftwareUpdateAgent'):
          words = s.split()
          pid = words[1]
          self.DoCommandOnAgentProcess('unload', pid)

  def InstallPlists(self):
    """Install plist files needed to running processes."""
    for plist, dest in zip(self.Plists(), self.InstalledPlists()):
      rsdir = self.KeystoneResources()
      rsdirfile = rsdir + plist
      try:
        f = open(rsdirfile, 'r')
      except IOError:
        raise Failure(file, self.root, "Bad access for " + rsdirfile)
      data = f.read()
      f.close()
      # This line is key.  We can't have a tilde in a launchd script;
      # we need an absolute path.  So we replace a known token, like this:
      #    cat src.plist | 's/INSTALL_ROOT/self.root/g' > dest.plist
      data = data.replace('${INSTALL_ROOT}', self.root)
      # Make sure launchd can distinguish between user and system Agents.
      # This is a no-op for the daemon.
      installType = 'user'
      if self.uid == 0:
        installType = 'root'
      data = data.replace('${INSTALL_TYPE}', installType)
      # Allow start interval to be configured.
      data = data.replace('${START_INTERVAL}', str(gAgentStartInterval))
      try:
        f = open(dest, 'w')
        f.write(data)
        f.close()
      except IOError:
        raise Failure(file, self.root, "Bad access for " + dest)

  def UninstallPlists(self):
    """Remove plists needed to run processes."""
    for plist in self.InstalledPlists():
      if os.path.exists(plist):
        os.unlink(plist)

  def DeleteReceipts(self):
    """Remove pkg receipts to help "clean out" an install."""
    if self.uid == 0:
      self.RunCommand(['/bin/rm', '-rf', '/Library/Receipts/Keystone.pkg'], -1)
      self.RunCommand(['/bin/rm', '-rf',
                       '/Library/Receipts/UninstallKeystone.pkg'], -1)

  def ChangeProcessRunState(self, doload, rtn):
    """Load or unload the correct processes if root or non-root.  If
    |doload| is True, load things (e.g. 'launchctl load').  Else,
    unload things (e.g. 'launchctl unload').  See
    StartProcessesForCurrentContext() and
    StopProcessesForCurrentContext() for more details.  For both 10.4
    and 10.5."""
    if self.uid == 0:
      self.ChangeDaemonRunState(doload, rtn)
      self.ChangeAgentRunStateRootInstall(doload, rtn)
    else:
      self.ChangeAgentRunStateUserInstall(doload, rtn)

  def ChangeDaemonRunState(self, doload, rtn):
    """For both 10.4 and 10.5."""
    if doload:
      cmd = 'load'
    else:
      cmd = 'unload'
    self.RunCommand(['/bin/launchctl', cmd,
                     self.LaunchDaemonConfigDir() +
                     'com.google.keystone.daemon.plist'], rtn)

  def RunCommandAsUID(self, cmdLine, rtn, uid):
    """Like self.RunCommand(), but do it as user id |uid|."""
    pid = os.fork()
    if pid == 0:
      os.setuid(uid)
      self.RunCommand(cmdLine, rtn)
      sys.exit(0)
    else:
      os.waitpid(pid, 0)

  def ChangeAgentRunStateRootInstallLeopard(self, doload, rtn):
    """Change the run state of the agent for a root (system) install.
    10.5 version."""
    procToLookFor = (' /System/Library/CoreServices/Finder.app/' +
                     'Contents/MacOS/Finder -')
    if doload:
      cmd = 'load'
    else:
      cmd = 'unload'
    # # launchd agent as user
    # self.RunCommandAsUID(['/bin/launchctl', cmd, '-S', 'Aqua',
    #                       (self.LaunchAgentConfigDir() +
    #                       'com.google.keystone.agent.plist') ],
    #                      rtn,
    #                      self.keystone.LocalUserUID())
    (stdin_ignored, stdoutfile, stderrfile) = os.popen3(['/bin/ps', 'auxwww'])
    stdout = stdoutfile.read()
    for s in stdout.splitlines():
      if ((s.find(procToLookFor) >= 0) and
          (s.split()[0] == self.keystone.LocalUsername())):
        pid = s.split()[1]
        # Must be root to bsexec.
        # Must bsexec to (pid) to get in local user's context.
        # Must become local user to have right process owner.
        # Must unset SUDO_COMMAND to keep launchctl happy.
        # Order is important.
        cmds = ['/bin/launchctl', 'bsexec', str(pid),
                '/usr/bin/sudo', '-u', self.keystone.LocalUsername(),
                '/bin/bash', '-c',
                'unset SUDO_COMMAND ; /bin/launchctl ' + cmd + ' -S Aqua ' +
                '"' + self.LaunchAgentConfigDir() +
                'com.google.keystone.agent.plist' + '"']
        self.RunCommand(cmds, rtn)
        return

  def ChangeAgentRunStateRootInstallTiger(self, doload, rtn):
    """10.4 version.  System-wide login item."""
    self.ChangeLoginItemRunState(doload, '/Library/Preferences/loginwindow')

  def ChangeAgentRunStateRootInstall(self, doload, rtn):
    """On 10.5, we also uninstall the 10.4 way in case we upgraded OSs."""
    self.ChangeAgentRunStateRootInstallLeopard(doload, rtn)
    if not doload:
      self.ChangeAgentRunStateRootInstallTiger(doload, rtn)

  def ChangeAgentRunStateUserInstallLeopard(self, doload, rtn):
    """Change the run state of the agent for a user install.  10.5
    version."""
    if doload:
      cmd = 'load'
    else:
      cmd = 'unload'
    fullCommandLine = ['/bin/launchctl', cmd, '-S', 'Aqua',
                       (self.LaunchAgentConfigDir() +
                        'com.google.keystone.agent.plist') ]
    if (not doload) and (os.geteuid() == 0):
      # We get here on promote (install as root, so euid==0, but we need
      # to uninstall the user Keystone).
      self.RunCommandAsUID(fullCommandLine, rtn, self.keystone.LocalUserUID())
    else:
      self.RunCommand(fullCommandLine, rtn)

  def ChangeAgentRunStateUserInstallTiger(self, doload, rtn):
    """10.4 version; login item."""
    self.ChangeLoginItemRunState(doload, 'loginwindow')

  def ChangeAgentRunStateUserInstall(self, doload, rtn):
    """On 10.5, we also uninstall the 10.4 way in case we upgraded OSs."""
    self.ChangeAgentRunStateUserInstallLeopard(doload, rtn)
    if not doload:
      self.ChangeAgentRunStateUserInstallTiger(doload, rtn)

  def AddAndStartLoginItem(self, domain):
    """Add the agent to as a login item to the specified |domain|.  Used
    for both root and user.  Since the 10.5 equivilent, 'launchctl
    load', will run the process now, we do the same thing.
    Intended for 10.4.  Added to the 10.5 base class to keep consistency
    with RemoveAndKillLoginItem()."""
    try:
      self.RunCommand(['/usr/bin/defaults', 'write', domain,
                       'AutoLaunchedApplicationDictionary',  '-array-add',
                       ('{Hide = 1; Path = \"' +
                        self.KeystoneAgentPath() + '\"; }')], 0)
    except Failure:
      # An empty AutoLaunchedApplicationDictionary is an empty string,
      # not an empty array, in which case -array-add chokes.  There is
      # no easy way to do a typeof(AutoLaunchedApplicationDictionary)
      # for a plist.  Our solution is to catch the error and try a
      # different way.
      self.RunCommand(['/usr/bin/defaults', 'write', domain,
                       'AutoLaunchedApplicationDictionary',  '-array',
                       ('{Hide = 1; Path = \"' +
                        self.KeystoneAgentPath() + '\"; }')], 0)
    if self.uid == 0:
      self.RunCommand(['/usr/bin/sudo', '-u',
                       str(self.keystone.LocalUsername()),
                       '/usr/bin/open', self.KeystoneAgentPath()], 0)
    else:
      self.RunCommand(['/usr/bin/open', self.KeystoneAgentPath()], 0)

  def RemoveAndKillLoginItem(self, domain):
    """Remove a login item in the specified |domain|.  Used for both
    root and user.  Since the 10.5 equivilent, 'launchctl unload',
    will kill the process, we do the same thing.  Intended for 10.4,
    but possibly used on 10.5 to cleanup."""
    aladir = 'AutoLaunchedApplicationDictionary'
    (stdin_ignored, stdoutfile, stderrfile) = os.popen3(['/usr/bin/defaults',
                                                         'read',
                                                         domain,
                                                         aladir])
    stdout = stdoutfile.read()
    if len(stdout.strip()) == 0:
      stdout = '()'

    # One line per loginitem to help us match
    stdout = re.compile('[\n]+').sub('', stdout)
    # handles case where we are the only item
    stdout = stdout.replace('(', '(\n')
    stdout = stdout.replace('}', '}\n')
    for line in stdout.splitlines():
      if line.find('/Library/Google/GoogleSoftwareUpdate/' +
                   'GoogleSoftwareUpdate.bundle/Contents/' +
                   'Resources/GoogleSoftwareUpdateAgent.app') != -1:
        stdout = stdout.replace(line, '')
    stdout = stdout.replace('\n', '')
    # help make sure it's a well-formed list
    stdout = stdout.replace('(,', '(')

    try:
      self.RunCommand(['/usr/bin/defaults', 'write', domain,
                       'AutoLaunchedApplicationDictionary', stdout], 0)
    except Failure, inst:
      # if we messed up the parse, log and move on.
      print inst

    # Now kill it
    lun = self.keystone.LocalUsername()
    (stdin_ignored, stdoutfile, stderrfile) = os.popen3(['/bin/ps',
                                                         'auxwww', '-U',
                                                         lun])
    for s in stdoutfile.readlines():
      pn1 = ('/Library/Google/GoogleSoftwareUpdate/' +
             'GoogleSoftwareUpdate.bundle/Contents/' +
             'Resources/GoogleSoftwareUpdateAgent.app/' +
             'Contents/MacOS/GoogleSoftwareUpdateAgent -psn')
      if s.find(pn1) != -1:
        words = s.split()
        pid = words[1]
        os.kill(int(pid), signal.SIGTERM)
        return

  def ChangeLoginItemRunState(self, doload, domain):
    """Change (add or remove) the login item for |domain| based on the
      value of |doload|.  Also start or kill the relevant item (in the
      current context) to mirror 10.5 launchctl behavior.  Although
      this is mainly for 10.4, it is also used on 10.5 to help cleanup
      (e.g. after an upgrade to 10.5)."""
    if doload:
      self.AddAndStartLoginItem(domain)
    else:
      self.RemoveAndKillLoginItem(domain)

# -------------------------------------------------------------------------

class KeystoneInstallTiger(KeystoneInstall):

  """Like KeystoneInstall, but overrides a few methods to support 10.4
  (Tiger)."""

  def __init__(self, keystone, uid, root):
    KeystoneInstall.__init__(self, keystone, uid, root)
    pass

  def InstalledPlists(self):
    """Return a list of plists which are supposed to be installed
    (destination paths).  Does NOT check they actually exist.
    10.4 override (no agent)."""
    plists = []
    plists.extend(self.InstalledPlistsForRootInstall())
    return plists

  def ChangeAgentRunStateRootInstall(self, doload, rtn):
    """Only do Tiger version."""
    self.ChangeAgentRunStateRootInstallTiger(doload, rtn)

  def ChangeAgentRunStateUserInstall(self, doload, rtn):
    """Only do Tiger version."""
    self.ChangeAgentRunStateUserInstallTiger(doload, rtn)

# -------------------------------------------------------------------------

class Keystone(object):

  """Top-level interface for Keystone install and uninstall.

  Attributes:
    package: name of the package to install (e.g. Keystone.tbz)
    root: root directory for install (e.g. "/", "/Users/frankie")
    doLaunchdPlists: boolean stating if we should install plist files
    doProcLaunch: boolean stating if we should launch/stop processes
    doForce: if True, force an install no matter what versions may say.
      Don't reference directly; use MyKeystoneBundleVersion().
    allInstallers: a list of all installers.  Used when uninstalling,
      stopping, or removing stuff.  If root, includes both,
      with the root installer first.
      Else only includes the user installer.
    currentInstaller: if a root install, the root installer.
      Else the user installer.
   """

  def __init__(self, package, systemRoot, userRoot, doLaunchdPlists,
               doProcLaunch, doForce):
    self.package = package
    self.doLaunchdPlists = doLaunchdPlists
    self.doProcLaunch = doProcLaunch
    self.doForce = doForce
    if userRoot == None:
      userRoot = self.RootForUID(self.LocalUserUID())
    if self.IsTiger():
      self.rootInstaller = KeystoneInstallTiger(self, 0, systemRoot)
      self.userInstaller = KeystoneInstallTiger(self, self.LocalUserUID(),
                                                userRoot)
    else:
      self.rootInstaller = KeystoneInstall(self, 0, systemRoot)
      self.userInstaller = KeystoneInstall(self, self.LocalUserUID(), userRoot)
    if self.IsRootInstall():
      self.allInstallers = [ self.rootInstaller, self.userInstaller ]
    else:
      self.allInstallers = [ self.userInstaller ]
    self.currentInstaller = self.allInstallers[0]

  def IsLeopardOrLater(self):
    """Ouch!  platform.mac_ver() returns
    ('10.5.1', ('', '', ''), 'i386')       10.5, python2.4 or python2.5
    ('', ('', '', ''), '')                 10.4, python2.3
    <unknown on 10.4, python2.4>
    Return True if we're on 10.5; else return False."""
    global gForceTiger
    if gForceTiger:
      return False
    (vers, dontcare1, dontcare2) = platform.mac_ver()
    splits = vers.split('.')
    if (len(splits) == 3) and (splits[1] >= '5'):
      return True
    return False

  def IsTiger(self):
    """Return the boolean opposite of IsLeopardOrLater()."""
    if self.IsLeopardOrLater():
      return False
    else:
      return True

  def IsRootInstall(self):
    """Return True if this is a root install.  On root install we do
    some special things (e.g. we have a daemon)."""
    uid = os.geteuid()
    if uid == 0:
      return True
    else:
      return False

  def LocalUserUID(self):
    """Return the UID of the local (non-root) user who initiated this
    install/uninstall.  If we can't figure it out, default to the user
    on conosle.  We don't want to default to console user in case a
    FUS happens in the middle of install or uninstall."""
    uid = os.geteuid()
    if uid != 0:
      return uid
    else:
      return os.stat('/dev/console')[stat.ST_UID]

  def LocalUsername(self):
    """Return the username of the local user."""
    uid = self.LocalUserUID()
    p = pwd.getpwuid(uid)
    return p[0]

  def RootForUID(self, uid):
    """For the given UID, return the install root for Keystone (where
    is is, or where it should be, installed)."""
    if uid == 0:
      return '/'
    else:
      return pwd.getpwuid(uid)[5]

  def ShouldInstall(self):
    """Return True if we should on install.  Possible reasons for
    punting (returning False):
    1) This is a System Keystone install and the installed System
    Keystone has a smaller version.
    2) This is a User Keystone and there is a System Keystone
    installed (of any version).
    3) This is a User Keystone and the installed User Keystone has a
    smaller version.
    4) We are told to force an install (--force cmd line option)
    """
    if self.doForce:
      return True
    if self.IsRootInstall():
      if self.rootInstaller.IsMyVersionGreaterThanInstalledVersion():
        return True
      else:
        return False
    else:
      # User install; check for any root presence
      if self.rootInstaller.InstalledKeystoneBundleVersion() != None:
        return False
      # There is no root install so just compare with existing user install
      elif self.userInstaller.IsMyVersionGreaterThanInstalledVersion():
        return True
      else:
        return False

  def Install(self, lockdown):
    """Public install interface.

      lockdown: if True, install a special ticket to lock down Keystone
        and prevent uninstall.  This will happen even if an install
        of Keystone itself is not needed.
    """
    CheckOnePath(self.package, stat.S_IRUSR)
    if self.ShouldInstall():
      self.Uninstall()
      self.currentInstaller.MakeDirectories(self.doLaunchdPlists)
      self.currentInstaller.InstallPackage()
      self.currentInstaller.MakeTicketForKeystone()
      if self.doLaunchdPlists:
        # Uninstall will also Stop/UninstallPlists if desired
        self.currentInstaller.InstallPlists()
        if self.doProcLaunch:
          self.currentInstaller.StartProcessesForCurrentContext()
    # possibly lockdown even if we don't need to install
    if lockdown:
      self.currentInstaller.LockdownKeystone()

  def Nuke(self):
    """Public nuke interface.  Likely never called explicitly
    other than testing."""
    self.Uninstall()
    for i in self.allInstallers:
      i.FullUninstallOfDirectories()  # DOES nuke all tickets
      i.StopAllAgentProcesses()
      i.DeleteReceipts()

  def Uninstall(self):
    """Prepare this machine for an install.  Although similar, it is
    NOT as comprehensive as a nuke.  Stops and removes
    components with all relevate installers (e.g. if root, do both; if
    user, do only user.)"""
    for i in self.allInstallers:
      if self.doProcLaunch:
        i.StopProcessesForCurrentContext()
      if self.doLaunchdPlists:
        i.UninstallPlists()
      i.UninstallPackage()  # does not delete all tickets; only our own
      i.DeleteCache()

# -------------------------------------------------------------------------

def PrintUse():
  print 'Use: '
  print ' [--install PKG]    Install keystone using PKG as the source.'
  print ' [--root ROOT]      Use ROOT as the dest for an install.  Optional.'
  print ' [--nuke]           Nuke Keystone and tickets.'
  print ' [--uninstall]      Like nuke but do NOT delete the ticket store.'
  print '                    Only supported for a user install.'
  print ' [--no-launchd]     Do NOT touch Keystone launchd plists or jobs,'
  print '                     for both install and uninstall.  For testing.'
  print ' [--no-launchdjobs] Do NOT touch jobs, but do do launchd plist files,'
  print '                     for both install and uninstall.  For testing.'
  print ' [--force]          Force an install no matter what.  For testing.'
  print ' [--forcetiger]     Pretend we are on Tiger (MacOSX 10.4).  For testing.'
  print ' [--lockdown]       Prevent Keystone from ever uninstalling itself.'
  print ' [--interval N]     Change agent plist to wake up every N sec '
  print ' [--help]           This message'


def main():
  os.environ.clear()
  os.environ['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin:/usr/libexec'

  # Make sure AuthorizationExecuteWithPrivileges() is happy
  if os.getuid() and os.geteuid() == 0:
    os.setuid(os.geteuid())

  try:
    opts, args = getopt.getopt(sys.argv[1:], "i:r:XunNhfI:",
                               ["install=", "root=", "nuke", "uninstall",
                                "no-launchd", "no-launchdjobs", "help",
                                "force", "forcetiger", "lockdown", "interval="])
  except getopt.GetoptError:
    print 'Bad options.'
    PrintUse()
    sys.exit(1)

  systemRoot = '/'
  userRoot = None
  package = None
  nuke = False
  uninstall = False
  doLaunchdPlists = True
  doProcLaunch = True
  doForce = False
  lockdown = False  # If true, prevent uninstall by adding a "lockdown" ticket

  for opt, val in opts:
    if opt in ('-i', '--install'):
      package = val
    if opt in ('-r', '--root'):
      userRoot = val
    if opt in ('-X', '--nuke'):
      nuke = True
    if opt in ('-u', '--uninstall'):
      uninstall = True
    if opt in ('-n', '--no-launchd'):
      doLaunchdPlists = False
    if opt in ('-N', '--no-launchdjobs'):
      doProcLaunch = False
    if opt in ('-f', '--force'):
      doForce = True
    if opt in ('-T', '--forcetiger'):
      global gForceTiger
      gForceTiger = True
    if opt in ('--lockdown',):
      lockdown = True
    if opt in ('-I', '--interval'):
      global gAgentStartInterval
      gAgentStartInterval = int(val)
    if opt in ('-h', '--help'):
      PrintUse()
      sys.exit(0)

  if (package == None) and (not nuke) and (not uninstall):
    print 'Must specify package name or nuke'
    PrintUse()
    sys.exit(1)
  try:
    (vers, dontcare1, dontcare2) = platform.mac_ver()
    splits = vers.split('.')
    if (len(splits) == 3) and (int(splits[1]) < 4):
      print 'Requires MacOS10.4 or later'
      sys.exit(1)
  except MacOS.Error:
    # 10.3 throws an exception for platform.mac_ver()
    print 'Requires MacOS10.4 or later'
    sys.exit(1)

  # lock file to make sure only one of these runs at a time
  lockfilename = '/tmp/.keystone_install_lock'

  # Make sure that root and user can share the same lockfile
  oldmask = os.umask(0000)
  # os.O_EXLOCK is 32, but isn't defined on 10.4 (python2.3)
  lockfile = os.open(lockfilename, os.O_CREAT | os.O_RDWR | 32, 0666)
  # restore umask for other files we create
  os.umask(oldmask)

  exitcode = 0
  try:
    k = Keystone(package, systemRoot, userRoot,
                 doLaunchdPlists, doProcLaunch, doForce)
    if uninstall:
      k.Uninstall()
    elif nuke:
      k.Nuke()
    else:
      k.Install(lockdown)
  except Failure, inst:
    print inst
    exitcode = 1

  os.close(lockfile)
  # lock file left around on purpose (or locking not happy)

  sys.exit(exitcode)

if __name__ == "__main__":
  main()

Postmortem

Tracker used. Considering what was found, it's a mystery how punters manage with all this junk getting spread around their disks all the time. They never learn. [Anyone want to clean and repair a punter hard drive? No way!]

Here's some of the monster do-do Google Chrome left behind.

22 items, 9928174 bytes, 19432 blocks, 920 bytes in extended attributes.

~/Library/Application Support/Google/Chrome
~/Library/Application Support/Google/Chrome/Consent To Send Stats
~/Library/Application Support/Google/Chrome/Default
~/Library/Application Support/Google/Chrome/Default/Archived History
~/Library/Application Support/Google/Chrome/Default/Bookmarks
~/Library/Application Support/Google/Chrome/Default/Cached Theme Images
~/Library/Application Support/Google/Chrome/Default/Cookies
~/Library/Application Support/Google/Chrome/Default/Current Session
~/Library/Application Support/Google/Chrome/Default/Current Tabs
~/Library/Application Support/Google/Chrome/Default/Extension Cookies
~/Library/Application Support/Google/Chrome/Default/History
~/Library/Application Support/Google/Chrome/Default/History Index 2009-10
~/Library/Application Support/Google/Chrome/Default/Login Data
~/Library/Application Support/Google/Chrome/Default/Preferences
~/Library/Application Support/Google/Chrome/Default/Thumbnails
~/Library/Application Support/Google/Chrome/Default/Visited Links
~/Library/Application Support/Google/Chrome/Default/Web Data
~/Library/Application Support/Google/Chrome/Dictionaries
~/Library/Application Support/Google/Chrome/First Run
~/Library/Application Support/Google/Chrome/Local State
~/Library/Application Support/Google/Chrome/Safe Browsing Bloom
~/Library/Application Support/Google/Chrome/Safe Browsing Bloom Filter 2
34 items, 7939238 bytes, 15600 blocks, 1564 bytes in extended attributes.

~/Library/Caches/Google/Chrome
~/Library/Caches/Google/Chrome/Default
~/Library/Caches/Google/Chrome/Default/Cache
~/Library/Caches/Google/Chrome/Default/Cache/data_0
~/Library/Caches/Google/Chrome/Default/Cache/data_1
~/Library/Caches/Google/Chrome/Default/Cache/data_2
~/Library/Caches/Google/Chrome/Default/Cache/data_3
~/Library/Caches/Google/Chrome/Default/Cache/f_000001
~/Library/Caches/Google/Chrome/Default/Cache/f_000002
~/Library/Caches/Google/Chrome/Default/Cache/f_000003
~/Library/Caches/Google/Chrome/Default/Cache/f_000004
~/Library/Caches/Google/Chrome/Default/Cache/f_000005
~/Library/Caches/Google/Chrome/Default/Cache/f_000006
~/Library/Caches/Google/Chrome/Default/Cache/f_000007
~/Library/Caches/Google/Chrome/Default/Cache/f_000008
~/Library/Caches/Google/Chrome/Default/Cache/f_000009
~/Library/Caches/Google/Chrome/Default/Cache/f_00000a
~/Library/Caches/Google/Chrome/Default/Cache/f_00000b
~/Library/Caches/Google/Chrome/Default/Cache/f_00000c
~/Library/Caches/Google/Chrome/Default/Cache/f_00000d
~/Library/Caches/Google/Chrome/Default/Cache/f_00000e
~/Library/Caches/Google/Chrome/Default/Cache/f_00000f
~/Library/Caches/Google/Chrome/Default/Cache/f_000010
~/Library/Caches/Google/Chrome/Default/Cache/f_000011
~/Library/Caches/Google/Chrome/Default/Cache/f_000012
~/Library/Caches/Google/Chrome/Default/Cache/f_000013
~/Library/Caches/Google/Chrome/Default/Cache/f_000014
~/Library/Caches/Google/Chrome/Default/Cache/f_000015
~/Library/Caches/Google/Chrome/Default/Cache/f_000016
~/Library/Caches/Google/Chrome/Default/Cache/f_000017
~/Library/Caches/Google/Chrome/Default/Cache/f_000018
~/Library/Caches/Google/Chrome/Default/Cache/f_000019
~/Library/Caches/Google/Chrome/Default/Cache/f_00001a
~/Library/Caches/Google/Chrome/Default/Cache/index
3 items, 204 bytes, 0 blocks, 149 bytes in extended attributes.

~/Library/Google/GoogleSoftwareUpdate
~/Library/Google/GoogleSoftwareUpdate/Actives
~/Library/Google/GoogleSoftwareUpdate/Actives/com.google.Chrome
1 item, 65 bytes, 8 blocks, 46 bytes in extended attributes.

~/Library/Preferences/com.google.Keystone.Agent.plist
18 items, 2270 bytes, 48 blocks, 1080 bytes in extended attributes.

~/Library/Preferences/Macromedia/Flash Player
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M/core.videoegg.com
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M/core.videoegg.com/#com
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M/core.videoegg.com/#com/videoegg
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M/core.videoegg.com/#com/videoegg/Demo.sol
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M/core.videoegg.com/#com/videoegg/Tearsheet.sol
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M/core.videoegg.com/#com/videoegg/Twig.sol
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M/core.videoegg.com/#ve
~/Library/Preferences/Macromedia/Flash Player/#SharedObjects/YVLA3S9M/core.videoegg.com/#ve/admanager.sol
~/Library/Preferences/Macromedia/Flash Player/macromedia.com
~/Library/Preferences/Macromedia/Flash Player/macromedia.com/support
~/Library/Preferences/Macromedia/Flash Player/macromedia.com/support/flashplayer
~/Library/Preferences/Macromedia/Flash Player/macromedia.com/support/flashplayer/sys
~/Library/Preferences/Macromedia/Flash Player/macromedia.com/support/flashplayer/sys/#core.videoegg.com
~/Library/Preferences/Macromedia/Flash Player/macromedia.com/support/flashplayer/sys/#core.videoegg.com/settings.sol
~/Library/Preferences/Macromedia/Flash Player/macromedia.com/support/flashplayer/sys/settings.sol

Here's its quarantine XA that it puts on literally everything.

0000;4ae0cc20;Google Chrome Helper;|com.google.Chrome.helper

Here's the file it left in 'preferences'.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>checkInterval</key>
   <real>18000</real>
</dict>
</plist>

Here's the hunt for ppc binaries. Note how far down these suckers are buried.

$ find . -type f -exec file {} \; | grep ppc
./Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/KeystoneRegistration (for architecture ppc):   Mach-O dynamically linked shared library ppc
./Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/Frameworks/KeystoneCommon.framework/Versions/A/KeystoneCommon (for architecture ppc):   Mach-O dynamically linked shared library ppc
./Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/Frameworks/UpdateEngine.framework/Versions/A/UpdateEngine (for architecture ppc):   Mach-O dynamically linked shared library ppc
./Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/MacOS/GoogleSoftwareUpdateDaemon (for architecture ppc):   Mach-O executable ppc
./Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/MacOS/ksadmin (for architecture ppc):   Mach-O executable ppc
./Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/Resources/GoogleSoftwareUpdateAgent.app/Contents/MacOS/GoogleSoftwareUpdateAgent (for architecture ppc):   Mach-O executable ppc

Here's the path to KeystoneCommon.

Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/Frameworks/KeystoneCommon.framework/Versions/A/KeystoneCommon

Here's the path to ksadmin - more on this basterd after the jump.

Google Chrome.app/Contents/Versions/4.0.223.8/Google Chrome Framework.framework/Frameworks/KeystoneRegistration.framework/Resources/GoogleSoftwareUpdate.bundle/Contents/MacOS/ksadmin

ksadmin is very scary. This is embedded within.

Usage: ksadmin [options]
  --store,-s FILE     Use FILE instead of the default ticket store.
                      Default means the system-wide if running as root, or
                      a per-user one if running as non-root
                      (either a per-user or system-wide one)
  --delete,-d         Delete a ticket as specified by option --productid
  --register,-r       Register a new ticket as specified by options.
                      Register requires 4 ticket options: -P, -v, -u, and an --xcsomething
  --productid,-P id   ProductID; can be a GUID or a BundleID
  --version,-v VERS   You can also specify -P id and -v VERS to update an existing ticket's version
  --xcpath,-x PATH    Specify an existence checker that checks for the existence
                      of the given path
  --xclsbundle,-X BID Specify an existence checker that asks LaunchServices whether an
                      application exists with the given Bundle ID
  --xcquery, -q query Specify an existence checker that checks whether the given Spotlight
                      query returns any results
  --url,-u URL        You can also specify -P id and -u URL to udpate an existing ticket's url
  --preserve-tttoken,-t  Preserve the trusted tester token from an existing ticket
                      when registering a new ticket or changing an existing one.
  --tttoken,-T TOKEN  Set the trusted tester token with a special register (requires only -P)
  --print-tickets,-p  Print all tickets and exit
  --list,-l           List the available updates, but don't install any
  --verbose,-V        Print activities verbosely
  --help,-h           Print this message

More strings from ksadmin. Note the source path at 0000000000003df4.

0000000000000758 __LINKEDIT
00000000000007fc /usr/lib/dyld
000000000000088c /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
00000000000008ec @executable_path/../Frameworks/KeystoneCommon.framework/Versions/A/KeystoneCommon
0000000000000958 @executable_path/../Frameworks/UpdateEngine.framework/Versions/A/UpdateEngine
00000000000009c0 /usr/lib/libgcc_s.1.dylib
00000000000009f4 /usr/lib/libSystem.B.dylib
0000000000000a28 /usr/lib/libobjc.A.dylib
0000000000000a5c /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
0000000000002a2c __dyld_make_delayed_module_initializer_calls
0000000000002a5c __dyld_mod_term_funcs
0000000000002a72 release
0000000000002a7a init
0000000000002a7f NSAutoreleasePool
0000000000002a91 KSAdminApp
0000000000002a9c @"KSTicketStore"
0000000000002ab4 @"NSString"
0000000000002ac0 @"NSURL"
0000000000002bbe store_
0000000000002bc5 argc_
0000000000002bcb argv_
0000000000002bd1 registerTicket_
0000000000002be1 deleteTicket_
0000000000002bef productID_
0000000000002bfa version_
0000000000002c03 xcpath_
0000000000002c0b xclsbundle_
0000000000002c17 xcquery_
0000000000002c20 url_
0000000000002c25 printTickets_
0000000000002c33 installUpdates_
0000000000002c43 listUpdates_
0000000000002c50 trustedTesterToken_
0000000000002c64 preserveToken_
0000000000002c73 commandRunnerForEngine:
0000000000002c8c engine:finished:wasSuccess:wantsReboot:
0000000000002cb4 engine:shouldUpdateProducts:
0000000000002cd4 engine:shouldPrefetchProducts:
0000000000002cf3 checkForUpdates
0000000000002d03 printTickets
0000000000002d10 allTickets
0000000000002d1b updateTicketURL
0000000000002d2b addTrustedTesterToken
0000000000002d41 updateTicketVersion
0000000000002d55 deleteTicket
0000000000002d62 createNewTicket
0000000000002d72 printUsage
0000000000002d7d printUsageWithError:
0000000000002d92 shortOptionsList:count:
0000000000002daa parseCommandLineOptions
0000000000002dc6 dealloc
0000000000002dce initWithArgc:argv:
0000000000002de1 verbose_
0000000000002dea filterAllowsMessage:level:
0000000000002e05 initWithVerbose:
0000000000002e16 commandRunner
0000000000002e24 stopAndReset
0000000000002e31 runUntilDate:
0000000000002e3f currentRunLoop
0000000000002e4e dateWithTimeIntervalSinceNow:
0000000000002e6c isUpdating
0000000000002e77 updateAllProducts
0000000000002e89 updateProductWithProductID:
0000000000002ea8 engineWithTicketStore:delegate:
0000000000002ec8 configureForKeystone
0000000000002ee0 handleFailureInFunction:file:lineNumber:description:
0000000000002f15 stringWithCString:
0000000000002f28 currentHandler
0000000000002f37 nextObject
0000000000002f42 objectEnumerator
0000000000002f53 count
0000000000002f59 copy
0000000000002f5e tickets
0000000000002f66 trueChecker
0000000000002f72 serverURL
0000000000002f7c existenceChecker
0000000000002f8d path
0000000000002f92 description
0000000000002f9e deleteTicket:
0000000000002fac logFuncError:msg:
0000000000002fbe storeTicket:
0000000000002fcc ticketWithProductID:version:existenceChecker:serverURL:trustedTesterToken:
0000000000003017 trustedTesterToken
000000000000302a ticketForProductID:
000000000000303e checkerWithQuery:
0000000000003050 checkerWithBundleID:
0000000000003065 checkerWithPath:
0000000000003076 appendString:
0000000000003084 appendFormat:
0000000000003092 string
0000000000003099 setFilter:
00000000000030a4 autorelease
00000000000030b0 setFormatter:
00000000000030be keystoneTicketStorePath
00000000000030d6 initWithString:
00000000000030e6 initWithUTF8String:
00000000000030fa initWithPath:
0000000000003108 stringWithUTF8String:
000000000000311e alloc
0000000000003124 UTF8String
000000000000312f logFuncInfo:msg:
0000000000003140 sharedLogger
000000000000314e retainCount
000000000000315a retain
0000000000003161 respondsToSelector:
0000000000003175 conformsToProtocol:
0000000000003189 isMemberOfClass:
000000000000319a isKindOfClass:
00000000000031a9 isProxy
00000000000031b4 performSelector:withObject:withObject:
00000000000031db performSelector:withObject:
00000000000031f7 performSelector:
0000000000003208 zone
000000000000320d self
0000000000003212 class
0000000000003218 superclass
0000000000003223 hash
0000000000003228 isEqual:
0000000000003234 NSObject
000000000000323d KSAdminAppFilter
000000000000324e GTMLogFilter
000000000000325c GTMLogger
0000000000003266 KSTicketStore
0000000000003274 NSString
000000000000327d NSURL
0000000000003283 NSMutableString
0000000000003293 KSPathExistenceChecker
00000000000032ac KSLaunchServicesExistenceChecker
00000000000032cd KSSpotlightExistenceChecker
00000000000032e9 KSTicket
00000000000032f2 KSExistenceChecker
0000000000003305 NSAssertionHandler
0000000000003318 KSUpdateEngine
0000000000003327 NSDate
000000000000332e NSRunLoop
0000000000003338 KSTaskCommandRunner
000000000000334c store
0000000000003352 register
000000000000335b delete
0000000000003362 productid
000000000000336c version
0000000000003374 xcpath
000000000000337b xclsbundle
0000000000003386 xcquery
0000000000003392 print-tickets
00000000000033a0 list
00000000000033a5 install
00000000000033ad verbose
00000000000033b5 preserve-tttoken
00000000000033c6 tttoken
00000000000033ce help
00000000000033d3 -[KSAdminApp run]
00000000000033e8 Can't register and delete a ticket at the same time.
000000000000341e install=%d, list=%d
0000000000003434 -[KSAdminApp parseCommandLineOptions]
000000000000345c Cannot specify multiple ticket stores
0000000000003482 Error creating ticket store
000000000000349f Using ticket store: %@.
00000000000034bc ERROR: %s
0000000000003b47 -[KSAdminApp createNewTicket]
0000000000003b65 Creating a new ticket.
0000000000003b7c Can't save ticket store (permissions problem?)
0000000000003bac  Can't create ticket; missing required argument.
0000000000003bde -[KSAdminApp deleteTicket]
0000000000003bf9 Deleting a ticket.
0000000000003c0c  Can't delete ticket; missing Product ID.
0000000000003c37 Deleting ticket: %s
0000000000003c4b From store: %@
0000000000003c5a No ticket to delete
0000000000003c70 -[KSAdminApp updateTicketVersion]
0000000000003c92 Updating URL for a ticket.
0000000000003cb0 Can't find ticket to update for product %@
0000000000003cdc -[KSAdminApp addTrustedTesterToken]
0000000000003d00 Adding trusted tester token.
0000000000003d1d file:///dev/null
0000000000003d30 -[KSAdminApp updateTicketURL]
0000000000003d50 Updating version for a ticket.
0000000000003d6f No ticket for %s
0000000000003d81 No tickets
0000000000003d8c -[KSAdminApp checkForUpdates]
0000000000003daa -[KSAdminApp checkForUpdates]
0000000000003dc8 No Keystone store set in checkForUpdates
0000000000003df4 /Volumes/BuildData/PulseData/data/recipes/290658576/base/branches/MacKeystone_release_branch/googlemac/Keystone/Cmd/KSAdminApp.m
0000000000003e75 Updating products with %@
0000000000003e8f Updating product with ID %@
0000000000003eab Updating all products
0000000000003ec1 Done checking for updates.
0000000000003edc Available updates: %s
0000000000003ef4 Finished updating (ok=%d, reboot=%d) %s
0000000000003f20 @(#)PROGRAM:ksadmin  PROJECT:Keystone-1.0.3.679

We're just sayin'.

About | ACP | Buy | Industry Watch | Learning Curve | News | Products | Search | Substack
Copyright © Rixstep. All rights reserved.