summaryrefslogtreecommitdiffstats
path: root/chrome/installer/mac/keystone_install.sh
blob: cbbcad8c7bfa9a2f5e9f0f5bfc445dd6ffc55309 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
#!/bin/bash -p

# 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.

# usage: keystone_install.sh update_dmg_mount_point
#
# Called by the Keystone system to update the installed application with a new
# version from a disk image.
#
# Environment variables:
# GOOGLE_CHROME_UPDATER_DEBUG
#   When set to a non-empty value, additional information about this script's
#   actions will be logged to stderr.  The same debugging information will
#   also be enabled when "Library/Google/Google Chrome Updater Debug" in the
#   root directory or in ${HOME} exists.
#
# Exit codes:
#  0  Happiness
#  1  Unknown failure
#  2  Basic sanity check source failure (e.g. no app on disk image)
#  3  Basic sanity check destination failure (e.g. ticket points to nothing)
#  4  Update driven by user ticket when a system ticket is also present
#  5  Could not prepare existing installed version to receive update
#  6  Patch sanity check failure
#  7  rsync failed (could not copy new versioned directory to Versions)
#  8  rsync failed (could not update outer .app bundle)
#  9  Could not get the version, update URL, or channel after update
# 10  Updated application does not have the version number from the update
# 11  ksadmin failure
# 12  dirpatcher failed for versioned directory
# 13  dirpatcher failed for outer .app bundle
#
# The following exit codes are not used by this script, but can be used to
# convey special meaning to Keystone:
# 66  (unused) success, request reboot
# 77  (unused) try installation again later

set -eu

# http://b/2290916: Keystone runs the installation with a restrictive PATH
# that only includes the directory containing ksadmin, /bin, and /usr/bin.  It
# does not include /sbin or /usr/sbin.  This script uses lsof, which is in
# /usr/sbin, and it's conceivable that it might want to use other tools in an
# sbin directory.  Adjust the path accordingly.
export PATH="${PATH}:/sbin:/usr/sbin"

# Environment sanitization.  Clear environment variables that might impact the
# interpreter's operation.  The |bash -p| invocation on the #! line takes the
# bite out of BASH_ENV, ENV, and SHELLOPTS (among other features), but
# clearing them here ensures that they won't impact any shell scripts used as
# utility programs. SHELLOPTS is read-only and can't be unset, only
# unexported.
unset BASH_ENV CDPATH ENV GLOBIGNORE IFS POSIXLY_CORRECT
export -n SHELLOPTS

set -o pipefail
shopt -s nullglob

ME="$(basename "${0}")"
readonly ME

declare GOOGLE_CHROME_UPDATER_DEBUG
err() {
  local error="${1}"

  local id=
  if [[ -n "${GOOGLE_CHROME_UPDATER_DEBUG}" ]]; then
    id=": ${$} $(date "+%Y-%m-%d %H:%M:%S %z")"
  fi

  echo "${ME}${id}: ${error}" >& 2
}

note() {
  local message="${1}"

  if [[ -n "${GOOGLE_CHROME_UPDATER_DEBUG}" ]]; then
    err "${message}"
  fi
}

declare g_temp_dir
cleanup() {
  local status=${?}

  trap - EXIT
  trap '' HUP INT QUIT TERM

  if [[ ${status} -ge 128 ]]; then
    err "Caught signal $((${status} - 128))"
  fi

  if [[ -n "${g_temp_dir}" ]]; then
    rm -rf "${g_temp_dir}"
  fi

  exit ${status}
}

ensure_temp_dir() {
  if [[ -z "${g_temp_dir}" ]]; then
    # Choose a template that won't be a dot directory.  Make it safe by
    # removing leading hyphens, too.
    local template="${ME}"
    if [[ "${template}" =~ ^[-.]+(.*)$ ]]; then
      template="${BASH_REMATCH[1]}"
    fi
    if [[ -z "${template}" ]]; then
      template="keystone_install"
    fi

    g_temp_dir="$(mktemp -d -t "${template}")"
    note "g_temp_dir = ${g_temp_dir}"
  fi
}

# Returns 0 (true) if |symlink| exists, is a symbolic link, and appears
# writable on the basis of its POSIX permissions.  This is used to determine
# writability like test's -w primary, but -w resolves symbolic links and this
# function does not.
is_writable_symlink() {
  local symlink="${1}"

  local link_mode
  link_mode="$(stat -f %Sp "${symlink}" 2> /dev/null || true)"
  if [[ -z "${link_mode}" ]] || [[ "${link_mode:0:1}" != "l" ]]; then
    return 1
  fi

  local link_user link_group
  link_user="$(stat -f %u "${symlink}" 2> /dev/null || true)"
  link_group="$(stat -f %g "${symlink}" 2> /dev/null || true)"
  if [[ -z "${link_user}" ]] || [[ -z "${link_group}" ]]; then
    return 1
  fi

  # If the users match, check the owner-write bit.
  if [[ ${EUID} -eq "${link_user}" ]]; then
    if [[ "${link_mode:2:1}" = "w" ]]; then
      return 0
    fi
    return 1
  fi

  # If the file's group matches any of the groups that this process is a
  # member of, check the group-write bit.
  local group_match=
  local group
  for group in "${GROUPS[@]}"; do
    if [[ "${group}" -eq "${link_group}" ]]; then
      group_match="y"
      break
    fi
  done
  if [[ -n "${group_match}" ]]; then
    if [[ "${link_mode:5:1}" = "w" ]]; then
      return 0
    fi
    return 1
  fi

  # Check the other-write bit.
  if [[ "${link_mode:8:1}" = "w" ]]; then
    return 0
  fi

  return 1
}

# If |symlink| exists and is a symbolic link, but is not writable according to
# is_writable_symlink, this function attempts to replace it with a new
# writable symbolic link.  If |symlink| does not exist, is not a symbolic
# link, or is already writable, this function does nothing.  This function
# always returns 0 (true).
ensure_writable_symlink() {
  local symlink="${1}"

  if [[ -L "${symlink}" ]] && ! is_writable_symlink "${symlink}"; then
    # If ${symlink} refers to a directory, doing this naively might result in
    # the new link being placed in that directory, instead of replacing the
    # existing link.  ln -fhs is supposed to handle this case, but it does so
    # by unlinking (removing) the existing symbolic link before creating a new
    # one.  That leaves a small window during which the symbolic link is not
    # present on disk at all.
    #
    # To avoid that possibility, a new symbolic link is created in a temporary
    # location and then swapped into place with mv.  An extra temporary
    # directory is used to convince mv to replace the symbolic link: again, if
    # the existing link refers to a directory, "mv newlink oldlink" will
    # actually leave oldlink alone and place newlink into the directory.
    # "mv newlink dirname(oldlink)" works as expected, but in order to replace
    # oldlink, newlink must have the same basename, hence the temporary
    # directory.

    local target
    target="$(readlink "${symlink}" 2> /dev/null || true)"
    if [[ -z "${target}" ]]; then
      return 0
    fi

    # Error handling strategy: if anything fails, such as the mktemp, ln,
    # chmod, or mv, ignore the failure and return 0 (success), leaving the
    # existing state with the non-writable symbolic link intact.  Failures
    # in this function will be difficult to understand and diagnose, and a
    # non-writable symbolic link is not necessarily fatal.  If something else
    # requires a writable symbolic link, allowing it to fail when a symbolic
    # link is not writable is easier to understand than bailing out of the
    # script on failure here.

    local symlink_dir temp_link_dir temp_link
    symlink_dir="$(dirname "${symlink}")"
    temp_link_dir="$(mktemp -d "${symlink_dir}/.symlink_temp.XXXXXX" || true)"
    if [[ -z "${temp_link_dir}" ]]; then
      return 0
    fi
    temp_link="${temp_link_dir}/$(basename "${symlink}")"

    (ln -fhs "${target}" "${temp_link}" && \
        chmod -h 755 "${temp_link}" && \
        mv -f "${temp_link}" "${symlink_dir}/") || true
    rm -rf "${temp_link_dir}"
  fi

  return 0
}

# ensure_writable_symlinks_recursive calls ensure_writable_symlink for every
# symbolic link in |directory|, recursivley.
#
# In some very weird and rare cases, it is possible to wind up with a user
# installation that contains symbolic links that the user does not have write
# permission over.  More on how that might happen later.
#
# If a weird and rare case like this is observed, rsync will exit with an
# error when attempting to update the times on these symbolic links.  rsync
# may not be intelligent enough to try creating a new symbolic link in these
# cases, but this script can be.
#
# The problem occurs when an administrative user first drag-installs the
# application to /Applications, resulting in the program's user being set to
# the user's own ID.  If, subsequently, a .pkg package is installed over that,
# the existing directory ownership will be preserved, but file ownership will
# be changed to whateer is specified by the package, typically root.  This
# applies to symbolic links as well.  On a subsequent update, rsync will be
# able to copy the new files into place, because the user still has permission
# to write to the directories.  If the symbolic link targets are not changing,
# though, rsync will not replace them, and they will remain owned by root.
# The user will not have permission to update the time on the symbolic links,
# resulting in an rsync error.
ensure_writable_symlinks_recursive() {
  local directory="${1}"

  # This fix-up is not necessary when running as root, because root will
  # always be able to write everything needed.
  if [[ ${EUID} -eq 0 ]]; then
    return 0
  fi

  # This step isn't critical.
  local set_e=
  if [[ "${-}" =~ e ]]; then
    set_e="y"
    set +e
  fi

  # Use find -print0 with read -d $'\0' to handle even the weirdest paths.
  local symlink
  while IFS= read -r -d $'\0' symlink; do
    ensure_writable_symlink "${symlink}"
  done < <(find "${directory}" -type l -print0)

  # Go back to how things were.
  if [[ -n "${set_e}" ]]; then
    set -e
  fi
}

# Prints the version of ksadmin, as reported by ksadmin --ksadmin-version, to
# stdout.  This function operates with "static" variables: it will only check
# the ksadmin version once per script run.  If ksadmin is old enough to not
# support --ksadmin-version, or another error occurs, this function prints an
# empty string.
g_checked_ksadmin_version=
g_ksadmin_version=
ksadmin_version() {
  if [[ -z "${g_checked_ksadmin_version}" ]]; then
    g_checked_ksadmin_version="y"
    g_ksadmin_version="$(ksadmin --ksadmin-version || true)"
  fi
  echo "${g_ksadmin_version}"
  return 0
}

# Compares the installed ksadmin version against a supplied version number,
# |check_version|, and returns 0 (true) if the installed Keystone version is
# greater than or equal to |check_version| according to a piece-wise
# comparison.  Returns 1 (false) if the installed Keystone version number
# cannot be determined or if |check_version| is greater than the installed
# Keystone version.  |check_version| should be a string of the form
# "major.minor.micro.build".  Returns 1 (false) if either |check_version| or
# the Keystone version do not match this format.
readonly KSADMIN_VERSION_RE="^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)\$"
is_ksadmin_version_ge() {
  local check_version="${1}"

  if ! [[ "${check_version}" =~ ${KSADMIN_VERSION_RE} ]]; then
    return 1
  fi

  local check_components=("${BASH_REMATCH[1]}"
                          "${BASH_REMATCH[2]}"
                          "${BASH_REMATCH[3]}"
                          "${BASH_REMATCH[4]}")

  local ksadmin_version
  ksadmin_version="$(ksadmin_version)"

  if ! [[ "${ksadmin_version}" =~ ${KSADMIN_VERSION_RE} ]]; then
    return 1
  fi

  local ksadmin_components=("${BASH_REMATCH[1]}"
                            "${BASH_REMATCH[2]}"
                            "${BASH_REMATCH[3]}"
                            "${BASH_REMATCH[4]}")

  local i
  for i in 0 1 2 3; do
    local check_component="${check_components[${i}]}"
    local ksadmin_component="${ksadmin_components[${i}]}"

    if [[ ${ksadmin_component} -lt ${check_component} ]]; then
      # ksadmin_version is less than check_version
      return 1
    fi
    if [[ ${ksadmin_component} -gt ${check_component} ]]; then
      # ksadmin_version is greater than check_version
      return 0
    fi
  done

  # The version numbers are equal.
  return 0
}

# Returns 0 (true) if ksadmin supports --tag.
ksadmin_supports_tag() {
  local ksadmin_version

  ksadmin_version="$(ksadmin_version)"
  if [[ -n "${ksadmin_version}" ]]; then
    # A ksadmin that recognizes --ksadmin-version and provides a version
    # number is new enough to recognize --tag.
    return 0
  fi

  return 1
}

# Returns 0 (true) if ksadmin supports --tag-path and --tag-key.
ksadmin_supports_tagpath_tagkey() {
  # --tag-path and --tag-key were introduced in Keystone 1.0.7.1306.
  is_ksadmin_version_ge 1.0.7.1306

  # The return value of is_ksadmin_version_ge is used as this function's
  # return value.
}

# Returns 0 (true) if ksadmin supports --brand-path and --brand-key.
ksadmin_supports_brandpath_brandkey() {
  # --brand-path and --brand-key were introduced in Keystone 1.0.8.1620.
  is_ksadmin_version_ge 1.0.8.1620

  # The return value of is_ksadmin_version_ge is used as this function's
  # return value.
}

usage() {
  echo "usage: ${ME} update_dmg_mount_point" >& 2
}

main() {
  local update_dmg_mount_point="${1}"

  # Early steps are critical.  Don't continue past any failure.
  set -e

  trap cleanup EXIT HUP INT QUIT TERM

  readonly PRODUCT_NAME="Google Chrome"
  readonly APP_DIR="${PRODUCT_NAME}.app"
  readonly FRAMEWORK_NAME="${PRODUCT_NAME} Framework"
  readonly FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework"
  readonly PATCH_DIR=".patch"
  readonly CONTENTS_DIR="Contents"
  readonly APP_PLIST="${CONTENTS_DIR}/Info"
  readonly VERSIONS_DIR="${CONTENTS_DIR}/Versions"
  readonly UNROOTED_BRAND_PLIST="Library/Google/Google Chrome Brand"
  readonly UNROOTED_DEBUG_FILE="Library/Google/Google Chrome Updater Debug"

  readonly APP_VERSION_KEY="CFBundleShortVersionString"
  readonly KS_VERSION_KEY="KSVersion"
  readonly KS_PRODUCT_KEY="KSProductID"
  readonly KS_URL_KEY="KSUpdateURL"
  readonly KS_CHANNEL_KEY="KSChannelID"
  readonly KS_BRAND_KEY="KSBrandID"

  readonly QUARANTINE_ATTR="com.apple.quarantine"

  # 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.
  #  -I, --ignore-times  don't skip files that match in size and mod-time
  #  -l, --links         copy symlinks as symlinks
  #  -r, --recursive     recurse into directories
  #  -p, --perms         preserve permissions
  #  -t, --times         preserve times
  readonly RSYNC_FLAGS="-Ilprt"

  # It's difficult to get GOOGLE_CHROME_UPDATER_DEBUG set in the environment
  # when this script is called from Keystone.  If a "debug file" exists in
  # either the root directory or the home directory of the user who owns the
  # ticket, turn on verbosity.  This may aid debugging.
  if [[ -e "/${UNROOTED_DEBUG_FILE}" ]] ||
     [[ -e ~/"${UNROOTED_DEBUG_FILE}" ]]; then
    export GOOGLE_CHROME_UPDATER_DEBUG="y"
  fi

  note "update_dmg_mount_point = ${update_dmg_mount_point}"

  # The argument should be the disk image path.  Make sure it exists and that
  # it's an absolute path.
  note "checking update"

  if [[ -z "${update_dmg_mount_point}" ]] ||
     [[ "${update_dmg_mount_point:0:1}" != "/" ]] ||
     ! [[ -d "${update_dmg_mount_point}" ]]; then
    err "update_dmg_mount_point must be an absolute path to a directory"
    usage
    exit 2
  fi

  local patch_dir="${update_dmg_mount_point}/${PATCH_DIR}"
  if [[ "${patch_dir:0:1}" != "/" ]]; then
    note "patch_dir = ${patch_dir}"
    err "patch_dir must be an absolute path"
    exit 2
  fi

  # Figure out if this is an ordinary installation disk image being used as a
  # full update, or a patch.  A patch will have a .patch directory at the root
  # of the disk image containing information about the update, tools to apply
  # it, and the update contents.
  local is_patch=
  local dirpatcher=
  if [[ -d "${patch_dir}" ]]; then
    # patch_dir exists and is a directory - this is a patch update.
    is_patch="y"
    dirpatcher="${patch_dir}/dirpatcher.sh"
    if ! [[ -x "${dirpatcher}" ]]; then
      err "couldn't locate dirpatcher"
      exit 6
    fi
  elif [[ -e "${patch_dir}" ]]; then
    # patch_dir exists, but is not a directory - what's that mean?
    note "patch_dir = ${patch_dir}"
    err "patch_dir must be a directory"
    exit 2
  else
    # patch_dir does not exist - this is a full "installer."
    patch_dir=
  fi
  note "patch_dir = ${patch_dir}"
  note "is_patch = ${is_patch}"
  note "dirpatcher = ${dirpatcher}"

  # The update to install.

  # update_app is the path to the new version of the .app.  It will only be
  # set at this point for a non-patch update.  It is not yet set for a patch
  # update because no such directory exists yet; it will be set later when
  # dirpatcher creates it.
  local update_app=

  # update_version_app_old, patch_app_dir, and patch_versioned_dir will only
  # be set for patch updates.
  local update_version_app_old=
  local patch_app_dir=
  local patch_versioned_dir=

  local update_version_app update_version_ks product_id
  if [[ -z "${is_patch}" ]]; then
    update_app="${update_dmg_mount_point}/${APP_DIR}"
    note "update_app = ${update_app}"

    # Make sure that there's something to copy from, and that it's an absolute
    # path.
    if [[ "${update_app:0:1}" != "/" ]] ||
       ! [[ -d "${update_app}" ]]; then
      err "update_app must be an absolute path to a directory"
      exit 2
    fi

    # Get some information about the update.
    note "reading update values"

    local update_app_plist="${update_app}/${APP_PLIST}"
    note "update_app_plist = ${update_app_plist}"
    if ! update_version_app="$(defaults read "${update_app_plist}" \
                                             "${APP_VERSION_KEY}")" ||
       [[ -z "${update_version_app}" ]]; then
      err "couldn't determine update_version_app"
      exit 2
    fi
    note "update_version_app = ${update_version_app}"

    local update_ks_plist="${update_app_plist}"
    note "update_ks_plist = ${update_ks_plist}"
    if ! update_version_ks="$(defaults read "${update_ks_plist}" \
                                            "${KS_VERSION_KEY}")" ||
       [[ -z "${update_version_ks}" ]]; then
      err "couldn't determine update_version_ks"
      exit 2
    fi
    note "update_version_ks = ${update_version_ks}"

    if ! product_id="$(defaults read "${update_ks_plist}" \
                                     "${KS_PRODUCT_KEY}")" ||
       [[ -z "${product_id}" ]]; then
      err "couldn't determine product_id"
      exit 2
    fi
    note "product_id = ${product_id}"
  else  # [[ -n "${is_patch}" ]]
    # Get some information about the update.
    note "reading update values"

    if ! update_version_app_old=$(<"${patch_dir}/old_app_version") ||
       [[ -z "${update_version_app_old}" ]]; then
      err "couldn't determine update_version_app_old"
      exit 2
    fi
    note "update_version_app_old = ${update_version_app_old}"

    if ! update_version_app=$(<"${patch_dir}/new_app_version") ||
       [[ -z "${update_version_app}" ]]; then
      err "couldn't determine update_version_app"
      exit 2
    fi
    note "update_version_app = ${update_version_app}"

    if ! update_version_ks=$(<"${patch_dir}/new_ks_version") ||
       [[ -z "${update_version_ks}" ]]; then
      err "couldn't determine update_version_ks"
      exit 2
    fi
    note "update_version_ks = ${update_version_ks}"

    if ! product_id=$(<"${patch_dir}/ks_product") ||
       [[ -z "${product_id}" ]]; then
      err "couldn't determine product_id"
      exit 2
    fi
    note "product_id = ${product_id}"

    patch_app_dir="${patch_dir}/application.dirpatch"
    if ! [[ -d "${patch_app_dir}" ]]; then
      err "couldn't locate patch_app_dir"
      exit 6
    fi
    note "patch_app_dir = ${patch_app_dir}"

    patch_versioned_dir=\
"${patch_dir}/version_${update_version_app_old}_${update_version_app}.dirpatch"
    if ! [[ -d "${patch_versioned_dir}" ]]; then
      err "couldn't locate patch_versioned_dir"
      exit 6
    fi
    note "patch_versioned_dir = ${patch_versioned_dir}"
  fi

  # ksadmin is required. Keystone should have set a ${PATH} that includes it.
  # Check that here, so that more useful feedback can be offered in the
  # unlikely event that ksadmin is missing.
  note "checking Keystone"

  local ksadmin_path
  if ! ksadmin_path="$(type -p ksadmin)" || [[ -z "${ksadmin_path}" ]]; then
    err "couldn't locate ksadmin_path"
    exit 3
  fi
  note "ksadmin_path = ${ksadmin_path}"

  # Call ksadmin_version once to prime the global state.  This is needed
  # because subsequent calls to ksadmin_version that occur in $(...)
  # expansions will not affect the global state (although they can read from
  # the already-initialized global state) and thus will cause a new ksadmin
  # --ksadmin-version process to run for each check unless the globals have
  # been properly initialized beforehand.
  ksadmin_version >& /dev/null || true
  local ksadmin_version_string
  ksadmin_version_string="$(ksadmin_version 2> /dev/null || true)"
  note "ksadmin_version_string = ${ksadmin_version_string}"

  # Figure out where to install.
  local installed_app
  if ! installed_app="$(ksadmin -pP "${product_id}" | sed -Ene \
      "s%^[[:space:]]+xc=<KSPathExistenceChecker:.* path=(/.+)>\$%\\1%p")" ||
      [[ -z "${installed_app}" ]]; then
    err "couldn't locate installed_app"
    exit 3
  fi
  note "installed_app = ${installed_app}"

  if [[ "${installed_app:0:1}" != "/" ]] ||
     ! [[ -d "${installed_app}" ]]; then
    err "installed_app must be an absolute path to a directory"
    exit 3
  fi

  # If this script is running as root, it's being driven by a system ticket.
  # Otherwise, it's being driven by a user ticket.
  local system_ticket=
  if [[ ${EUID} -eq 0 ]]; then
    system_ticket="y"
  fi
  note "system_ticket = ${system_ticket}"

  # If this script is being driven by a user ticket, but a system ticket is
  # also present, there's a potential for the two to collide.  Both ticket
  # types might be present if another user on the system promoted the ticket
  # to system: the other user could not have removed this user's user ticket.
  # Handle that case here by deleting the user ticket and exiting early with
  # a discrete exit code.
  #
  # Current versions of ksadmin will exit 1 (false) when asked to print tickets
  # and given a specific product ID to print.  Older versions of ksadmin would
  # exit 0 (true), but those same versions did not support -S (meaning to check
  # the system ticket store) and would exit 1 (false) with this invocation due
  # to not understanding the question.  Therefore, the usage here will only
  # delete the existing user ticket when running as non-root with access to a
  # sufficiently recent ksadmin.  Older ksadmins are tolerated: the update will
  # likely fail for another reason and the user ticket will hang around until
  # something is eventually able to remove it.
  if [[ -z "${system_ticket}" ]] &&
     ksadmin -S --print-tickets -P "${product_id}" >& /dev/null; then
    ksadmin --delete -P "${product_id}" || true
    err "can't update on a user ticket when a system ticket is also present"
    exit 4
  fi

  # Figure out what the existing installed application is using for its
  # versioned directory.  This will be used later, to avoid removing the
  # existing installed version's versioned directory in case anything is still
  # using it.
  note "reading install values"

  local installed_app_plist="${installed_app}/${APP_PLIST}"
  note "installed_app_plist = ${installed_app_plist}"
  local installed_app_plist_path="${installed_app_plist}.plist"
  note "installed_app_plist_path = ${installed_app_plist_path}"
  local old_version_app
  old_version_app="$(defaults read "${installed_app_plist}" \
                                   "${APP_VERSION_KEY}" || true)"
  note "old_version_app = ${old_version_app}"

  # old_version_app is not required, because it won't be present in skeleton
  # bootstrap installations, which just have an empty .app directory.  Only
  # require it when doing a patch update, and use it to validate that the
  # patch applies to the old installed version.  By definition, skeleton
  # bootstraps can't be installed with patch udpates.  They require the full
  # application on the disk image.
  if [[ -n "${is_patch}" ]]; then
    if [[ -z "${old_version_app}" ]]; then
      err "old_version_app required for patch"
      exit 6
    elif [[ "${old_version_app}" != "${update_version_app_old}" ]]; then
      err "this patch does not apply to the installed version"
      exit 6
    fi
  fi

  local installed_versions_dir="${installed_app}/${VERSIONS_DIR}"
  note "installed_versions_dir = ${installed_versions_dir}"

  # If the installed application is incredibly old, old_versioned_dir may not
  # exist.
  local old_versioned_dir
  if [[ -n "${old_version_app}" ]]; then
    old_versioned_dir="${installed_versions_dir}/${old_version_app}"
  fi
  note "old_versioned_dir = ${old_versioned_dir}"

  # Collect the installed application's brand code, it will be used later.  It
  # is not an error for the installed application to not have a brand code.
  local old_ks_plist="${installed_app_plist}"
  note "old_ks_plist = ${old_ks_plist}"
  local old_brand
  old_brand="$(defaults read "${old_ks_plist}" \
                             "${KS_BRAND_KEY}" 2> /dev/null ||
               true)"
  note "old_brand = ${old_brand}"

  ensure_writable_symlinks_recursive "${installed_app}"

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

  # Make sure that ${installed_versions_dir} 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.  Later, during the
  # rsync to copy the applciation directory, the mode bits and timestamp on
  # ${installed_versions_dir} will be set to conform to whatever is present in
  # the update.
  #
  # ${installed_app} is guaranteed to exist at this point, but
  # ${installed_app}/${CONTENTS_DIR} may not if things are severely broken or
  # if this update is actually an initial installation from a Keystone
  # skeleton bootstrap.  The mkdir creates ${installed_app}/${CONTENTS_DIR} if
  # it doesn't exist; its mode bits will be fixed up in a subsequent rsync.
  note "creating installed_versions_dir"
  if ! mkdir -p "${installed_versions_dir}"; then
    err "mkdir of installed_versions_dir failed"
    exit 5
  fi

  local new_versioned_dir
  new_versioned_dir="${installed_versions_dir}/${update_version_app}"
  note "new_versioned_dir = ${new_versioned_dir}"

  # If there's an entry at ${new_versioned_dir} but it's not a directory
  # (or it's a symbolic link, whether or not it points to a directory), rsync
  # won't get rid of it.  It's never correct to have a non-directory in place
  # of the versioned directory, so toss out whatever's there.  Don't treat
  # this as a critical step: if removal fails, operation can still proceed to
  # to the dirpatcher or rsync, which will likely fail.
  if [[ -e "${new_versioned_dir}" ]] &&
     ([[ -L "${new_versioned_dir}" ]] ||
      ! [[ -d "${new_versioned_dir}" ]]); then
    note "removing non-directory in place of versioned directory"
    rm -f "${new_versioned_dir}" 2> /dev/null || true
  fi

  local update_versioned_dir
  if [[ -z "${is_patch}" ]]; then
    update_versioned_dir="${update_app}/${VERSIONS_DIR}/${update_version_app}"
    note "update_versioned_dir = ${update_versioned_dir}"
  else  # [[ -n "${is_patch}" ]]
    # dirpatcher won't patch into a directory that already exists.  Doing so
    # would be a bad idea, anyway.  If ${new_versioned_dir} already exists,
    # it may be something left over from a previous failed or incomplete
    # update attempt, or it may be the live versioned directory if this is a
    # same-version update intended only to change channels.  Since there's no
    # way to tell, this case is handled by having dirpatcher produce the new
    # versioned directory in a temporary location and then having rsync copy
    # it into place as an ${update_versioned_dir}, the same as in a non-patch
    # update.  If ${new_versioned_dir} doesn't exist, dirpatcher can place the
    # new versioned directory at that location directly.
    local versioned_dir_target
    if ! [[ -e "${new_versioned_dir}" ]]; then
      versioned_dir_target="${new_versioned_dir}"
      note "versioned_dir_target = ${versioned_dir_target}"
    else
      ensure_temp_dir
      versioned_dir_target="${g_temp_dir}/${update_version_app}"
      note "versioned_dir_target = ${versioned_dir_target}"
      update_versioned_dir="${versioned_dir_target}"
      note "update_versioned_dir = ${update_versioned_dir}"
    fi

    note "dirpatching versioned directory"
    if ! "${dirpatcher}" "${old_versioned_dir}" \
                         "${patch_versioned_dir}" \
                         "${versioned_dir_target}"; then
      err "dirpatcher of versioned directory failed, status ${PIPESTATUS[0]}"
      exit 12
    fi
  fi

  # Copy the versioned directory.  The new versioned directory should have a
  # different name than any existing one, so this won't harm anything already
  # present in ${installed_versions_dir}, 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
  # won't interfere with anything, and it will be replaced or removed during a
  # future update attempt.
  #
  # In certain cases, same-version updates are distributed to move users
  # between channels; when this happens, the contents of the versioned
  # directories are identical and rsync will not render the versioned
  # directory unusable even for an instant.
  #
  # ${update_versioned_dir} may be empty during a patch update (${is_patch})
  # if the dirpatcher above was able to write it into place directly.  In
  # that event, dirpatcher guarantees that ${new_versioned_dir} is already in
  # place.
  if [[ -n "${update_versioned_dir}" ]]; then
    note "rsyncing versioned directory"
    if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \
                                              "${new_versioned_dir}"; then
      err "rsync of versioned directory failed, status ${PIPESTATUS[0]}"
      exit 7
    fi
  fi

  if [[ -n "${is_patch}" ]]; then
    # If the versioned directory was prepared in a temporary directory and
    # then rsynced into place, remove the temporary copy now that it's no
    # longer needed.
    if [[ -n "${update_versioned_dir}" ]]; then
      rm -rf "${update_versioned_dir}" 2> /dev/null || true
      update_versioned_dir=
      note "update_versioned_dir = ${update_versioned_dir}"
    fi

    # Prepare ${update_app}.  This always needs to be done in a temporary
    # location because dirpatcher won't write to a directory that already
    # exists, and ${installed_app} needs to be used as input to dirpatcher
    # in any event.  The new application will be rsynced into place once
    # dirpatcher creates it.
    ensure_temp_dir
    update_app="${g_temp_dir}/${APP_DIR}"
    note "update_app = ${update_app}"

    note "dirpatching app directory"
    if ! "${dirpatcher}" "${installed_app}" \
                         "${patch_app_dir}" \
                         "${update_app}"; then
      err "dirpatcher of app directory failed, status ${PIPESTATUS[0]}"
      exit 13
    fi
  fi

  # 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.
  local needs_touch=
  if [[ "${installed_app}" -nt "${update_app}" ]]; then
    needs_touch="y"
  fi
  note "needs_touch = ${needs_touch}"

  # Copy the unversioned files into place, leaving everything in
  # ${installed_versions_dir} 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.  Note that
  # ${VERSIONS_DIR} is included to copy its mode bits and timestamp, but its
  # contents are excluded, having already been installed above.
  note "rsyncing app directory"
  if ! rsync ${RSYNC_FLAGS} --delete-after --exclude "/${VERSIONS_DIR}/*" \
       "${update_app}/" "${installed_app}"; then
    err "rsync of app directory failed, status ${PIPESTATUS[0]}"
    exit 8
  fi

  note "rsyncs complete"

  if [[ -n "${is_patch}" ]]; then
    # update_app has been rsynced into place and is no longer needed.
    rm -rf "${update_app}" 2> /dev/null || true
    update_app=
    note "update_app = ${update_app}"
  fi

  if [[ -n "${g_temp_dir}" ]]; then
    # The temporary directory, if any, is no longer needed.
    rm -rf "${g_temp_dir}" 2> /dev/null || true
    g_temp_dir=
    note "g_temp_dir = ${g_temp_dir}"
  fi

  # 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 "${installed_app}" || true
  fi

  # Read the new values, such as the version.
  note "reading new values"

  local new_version_app
  if ! new_version_app="$(defaults read "${installed_app_plist}" \
                                        "${APP_VERSION_KEY}")" ||
     [[ -z "${new_version_app}" ]]; then
    err "couldn't determine new_version_app"
    exit 9
  fi
  note "new_version_app = ${new_version_app}"

  local new_versioned_dir="${installed_versions_dir}/${new_version_app}"
  note "new_versioned_dir = ${new_versioned_dir}"

  local new_ks_plist="${installed_app_plist}"
  note "new_ks_plist = ${new_ks_plist}"

  local new_version_ks
  if ! new_version_ks="$(defaults read "${new_ks_plist}" \
                                       "${KS_VERSION_KEY}")" ||
     [[ -z "${new_version_ks}" ]]; then
    err "couldn't determine new_version_ks"
    exit 9
  fi
  note "new_version_ks = ${new_version_ks}"

  local update_url
  if ! update_url="$(defaults read "${new_ks_plist}" "${KS_URL_KEY}")" ||
     [[ -z "${update_url}" ]]; then
    err "couldn't determine update_url"
    exit 9
  fi
  note "update_url = ${update_url}"

  # The channel ID is optional.  Suppress stderr to prevent Keystone from
  # seeing possible error output.
  local channel
  channel="$(defaults read "${new_ks_plist}" "${KS_CHANNEL_KEY}" 2> /dev/null ||
             true)"
  note "channel = ${channel}"

  # 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}" != "${update_version_ks}" ]]; then
    err "new_version_ks and update_version_ks do not match"
    exit 10
  fi

  # Notify LaunchServices.  This is not considered a critical step, and
  # lsregister's exit codes shouldn't be confused with this script's own.
  # Redirect stdout to /dev/null to suppress the useless "ThrottleProcessIO:
  # throttling disk i/o" messages that lsregister might print.
  note "notifying LaunchServices"
  local cs_fwk="/System/Library/Frameworks/CoreServices.framework"
  local ls_fwk="${cs_fwk}/Frameworks/LaunchServices.framework"
  local lsregister="${ls_fwk}/Support/lsregister"
  note "cs_fwk = ${cs_fwk}"
  note "ls_fwk = ${ls_fwk}"
  note "lsregister = ${lsregister}"
  "${lsregister}" "${installed_app}" > /dev/null || true

  # The brand information is stored differently depending on whether this is
  # running for a system or user ticket.
  note "handling brand code"

  local set_brand_file_access=
  local brand_plist
  if [[ -n "${system_ticket}" ]]; then
    # System ticket.
    set_brand_file_access="y"
    brand_plist="/${UNROOTED_BRAND_PLIST}"
  else
    # User ticket.
    brand_plist=~/"${UNROOTED_BRAND_PLIST}"
  fi
  local brand_plist_path="${brand_plist}.plist"
  note "set_brand_file_access = ${set_brand_file_access}"
  note "brand_plist = ${brand_plist}"
  note "brand_plist_path = ${brand_plist_path}"

  # If the user manually updated their copy of Chrome, there might be new
  # brand information in the app bundle, and that needs to be copied out into
  # the file Keystone looks at.
  if [[ -n "${old_brand}" ]]; then
    local brand_dir
    brand_dir="$(dirname "${brand_plist_path}")"
    note "brand_dir = ${brand_dir}"
    if ! mkdir -p "${brand_dir}"; then
      err "couldn't mkdir brand_dir, continuing"
    else
      if ! defaults write "${brand_plist}" "${KS_BRAND_KEY}" \
                          -string "${old_brand}"; then
        err "couldn't write brand_plist, continuing"
      elif [[ -n "${set_brand_file_access}" ]]; then
        if ! chown "root:wheel" "${brand_plist_path}"; then
          err "couldn't chown brand_plist_path, continuing"
        else
          if ! chmod 644 "${brand_plist_path}"; then
            err "couldn't chmod brand_plist_path, continuing"
          fi
        fi
      fi
    fi
  fi

  # Confirm that the brand file exists.  It's optional.
  local ksadmin_brand_plist_path="${brand_plist_path}"
  local ksadmin_brand_key="${KS_BRAND_KEY}"
  if [[ ! -f "${ksadmin_brand_plist_path}" ]]; then
    # Clear any branding information.
    ksadmin_brand_plist_path=
    ksadmin_brand_key=
  fi
  note "ksadmin_brand_plist_path = ${ksadmin_brand_plist_path}"
  note "ksadmin_brand_key = ${ksadmin_brand_key}"

  note "notifying Keystone"

  local ksadmin_args=(
    --register
    -P "${product_id}"
    --version "${new_version_ks}"
    --xcpath "${installed_app}"
    --url "${update_url}"
  )

  if ksadmin_supports_tag; then
    ksadmin_args+=(
      --tag "${channel}"
    )
  fi

  if ksadmin_supports_tagpath_tagkey; then
    ksadmin_args+=(
      --tag-path "${installed_app_plist_path}"
      --tag-key "${KS_CHANNEL_KEY}"
    )
  fi

  if ksadmin_supports_brandpath_brandkey; then
    ksadmin_args+=(
      --brand-path "${ksadmin_brand_plist_path}"
      --brand-key "${ksadmin_brand_key}"
    )
  fi

  note "ksadmin_args = ${ksadmin_args[*]}"

  if ! ksadmin "${ksadmin_args[@]}"; then
    err "ksadmin failed"
    exit 11
  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.
  note "cleaning up old versioned directories"

  local versioned_dir
  for versioned_dir in "${installed_versions_dir}/"*; do
    note "versioned_dir = ${versioned_dir}"
    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.
      note "versioned_dir is new_versioned_dir or old_versioned_dir, skipping"
      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.
    local ps_string="${versioned_dir}/"
    note "ps_string = ${ps_string}"

    # 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.
    local lsof_file="${versioned_dir}/${FRAMEWORK_DIR}/${FRAMEWORK_NAME}"
    note "lsof_file = ${lsof_file}"

    # 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.
      note "versioned_dir doesn't appear to be in use, removing"
      rm -rf "${versioned_dir}"
    else
      note "versioned_dir is in use, skipping"
    fi
  done

  # If this script is being driven by a user Keystone ticket, it is not
  # running as root.  If the application is installed somewhere under
  # /Applications, try to make it writable by all admin users.  This will
  # allow other admin users to update the application from their own user
  # Keystone instances.
  #
  # If the script is being driven by a user Keystone ticket (not running as
  # root) and 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 on the system are running, so err on the side of safety and
  # don't make it group-writable.
  #
  # If this script is being driven by a system ticket (running as root), it's
  # future updates can be expected to be applied the same way, so admin-
  # writability is not a concern.  Set the entire thing to be owned by root
  # in that case, regardless of where it's installed, and drop any group and
  # other write permission.
  #
  # If this script is running as a user that is not a member of the admin
  # group, the chgrp operation will not succeed.  Tolerate that case, because
  # it's better than the alternative, which is to make the application
  # world-writable.
  note "setting permissions"

  local chmod_mode="a+rX,u+w,go-w"
  if [[ -z "${system_ticket}" ]]; then
    if [[ "${installed_app:0:14}" = "/Applications/" ]] &&
       chgrp -Rh admin "${installed_app}" 2> /dev/null; then
      chmod_mode="a+rX,ug+w,o-w"
    fi
  else
    chown -Rh root:wheel "${installed_app}" 2> /dev/null
  fi

  note "chmod_mode = ${chmod_mode}"
  chmod -R "${chmod_mode}" "${installed_app}" 2> /dev/null

  # On the Mac, or at least on HFS+, symbolic link permissions are significant,
  # but chmod -R and -h can't be used together.  Do another pass to fix the
  # permissions on any symbolic links.
  find "${installed_app}" -type l -exec chmod -h "${chmod_mode}" {} + \
      2> /dev/null

  # Host OS version check, to be able to take advantage of features on newer
  # systems and fall back to slow ways of doing things on older systems.
  local os_version
  os_version="$(sw_vers -productVersion)"
  note "os_version = ${os_version}"

  local os_major=0
  local os_minor=0
  if [[ "${os_version}" =~ ^([0-9]+)\.([0-9]+) ]]; then
    os_major="${BASH_REMATCH[1]}"
    os_minor="${BASH_REMATCH[2]}"
  fi
  note "os_major = ${os_major}"
  note "os_minor = ${os_minor}"

  # If an update is triggered from within the application itself, the update
  # process inherits the quarantine bit (LSFileQuarantineEnabled).  Any files
  # or directories created during the update will be quarantined in that case,
  # which may cause Launch Services to display quarantine UI.  That's bad,
  # especially if it happens when the outer .app launches a quarantined inner
  # helper.  If the application is already on the system and is being updated,
  # then it can be assumed that it should not be quarantined.  Use xattr to
  # drop the quarantine attribute.
  #
  # TODO(mark): Instead of letting the quarantine attribute be set and then
  # dropping it here, figure out a way to get the update process to run
  # without LSFileQuarantineEnabled even when triggering an update from within
  # the application.
  note "lifting quarantine"

  if [[ ${os_major} -gt 10 ]] ||
     ([[ ${os_major} -eq 10 ]] && [[ ${os_minor} -ge 6 ]]); then
    # On 10.6, xattr supports -r for recursive operation.
    xattr -d -r "${QUARANTINE_ATTR}" "${installed_app}" 2> /dev/null
  else
    # On earlier systems, xattr doesn't support -r, so run xattr via find.
    find "${installed_app}" -exec xattr -d "${QUARANTINE_ATTR}" {} + \
        2> /dev/null
  fi

  # Great success!
  note "done!"

  trap - EXIT

  return 0
}

# Check "less than" instead of "not equal to" in case Keystone ever changes to
# pass more arguments.
if [[ ${#} -lt 1 ]]; then
  usage
  exit 2
fi

main "${@}"
exit ${?}