OLD | NEW |
1 #!/bin/bash -p | 1 #!/bin/bash -p |
2 | 2 |
3 # Copyright (c) 2009 The Chromium Authors. All rights reserved. | 3 # Copyright (c) 2009 The Chromium Authors. All rights reserved. |
4 # Use of this source code is governed by a BSD-style license that can be | 4 # Use of this source code is governed by a BSD-style license that can be |
5 # found in the LICENSE file. | 5 # found in the LICENSE file. |
6 | 6 |
7 # usage: keystone_install.sh update_dmg_mount_point | 7 # usage: keystone_install.sh update_dmg_mount_point |
8 # | 8 # |
9 # Called by the Keystone system to update the installed application with a new | 9 # Called by the Keystone system to update the installed application with a new |
10 # version from a disk image. | 10 # version from a disk image. |
11 # | 11 # |
12 # Environment variables: | 12 # Environment variables: |
13 # GOOGLE_CHROME_UPDATER_DEBUG | 13 # GOOGLE_CHROME_UPDATER_DEBUG |
14 # When set to a non-empty value, additional information about this script's | 14 # When set to a non-empty value, additional information about this script's |
15 # actions will be logged to stderr. The same debugging information will | 15 # actions will be logged to stderr. The same debugging information will |
16 # also be enabled when "Library/Google/Google Chrome Updater Debug" in the | 16 # also be enabled when "Library/Google/Google Chrome Updater Debug" in the |
17 # root directory or in ${HOME} exists. | 17 # root directory or in ${HOME} exists. |
18 # | 18 # |
19 # Exit codes: | 19 # Exit codes: |
20 # 0 Happiness | 20 # 0 Happiness |
21 # 1 Unknown failure | 21 # 1 Unknown failure |
22 # 2 Basic sanity check source failure (e.g. no app on disk image) | 22 # 2 Basic sanity check source failure (e.g. no app on disk image) |
23 # 3 Basic sanity check destination failure (e.g. ticket points to nothing) | 23 # 3 Basic sanity check destination failure (e.g. ticket points to nothing) |
24 # 4 Update driven by user ticket when a system ticket is also present | 24 # 4 Update driven by user ticket when a system ticket is also present |
25 # 5 Could not prepare existing installed version to receive update | 25 # 5 Could not prepare existing installed version to receive update |
26 # 6 rsync failed (could not assure presence of Versions directory) | 26 # 6 Patch sanity check failure |
27 # 7 rsync failed (could not copy new versioned directory to Versions) | 27 # 7 rsync failed (could not copy new versioned directory to Versions) |
28 # 8 rsync failed (could not update outer .app bundle) | 28 # 8 rsync failed (could not update outer .app bundle) |
29 # 9 Could not get the version, update URL, or channel after update | 29 # 9 Could not get the version, update URL, or channel after update |
30 # 10 Updated application does not have the version number from the update | 30 # 10 Updated application does not have the version number from the update |
31 # 11 ksadmin failure | 31 # 11 ksadmin failure |
| 32 # 12 dirpatcher failed for versioned directory |
| 33 # 13 dirpatcher failed for outer .app bundle |
| 34 # |
| 35 # The following exit codes are not used by this script, but can be used to |
| 36 # convey special meaning to Keystone: |
| 37 # 66 (unused) success, request reboot |
| 38 # 77 (unused) try installation again later |
32 | 39 |
33 set -eu | 40 set -eu |
34 | 41 |
35 # http://b/2290916: Keystone runs the installation with a restrictive PATH | 42 # http://b/2290916: Keystone runs the installation with a restrictive PATH |
36 # that only includes the directory containing ksadmin, /bin, and /usr/bin. It | 43 # that only includes the directory containing ksadmin, /bin, and /usr/bin. It |
37 # does not include /sbin or /usr/sbin. This script uses lsof, which is in | 44 # does not include /sbin or /usr/sbin. This script uses lsof, which is in |
38 # /usr/sbin, and it's conceivable that it might want to use other tools in an | 45 # /usr/sbin, and it's conceivable that it might want to use other tools in an |
39 # sbin directory. Adjust the path accordingly. | 46 # sbin directory. Adjust the path accordingly. |
40 export PATH="${PATH}:/sbin:/usr/sbin" | 47 export PATH="${PATH}:/sbin:/usr/sbin" |
41 | 48 |
(...skipping 25 matching lines...) Expand all Loading... |
67 } | 74 } |
68 | 75 |
69 note() { | 76 note() { |
70 local message="${1}" | 77 local message="${1}" |
71 | 78 |
72 if [[ -n "${GOOGLE_CHROME_UPDATER_DEBUG}" ]]; then | 79 if [[ -n "${GOOGLE_CHROME_UPDATER_DEBUG}" ]]; then |
73 err "${message}" | 80 err "${message}" |
74 fi | 81 fi |
75 } | 82 } |
76 | 83 |
| 84 declare g_temp_dir |
| 85 cleanup() { |
| 86 local status=${?} |
| 87 |
| 88 trap - EXIT |
| 89 trap '' HUP INT QUIT TERM |
| 90 |
| 91 if [[ ${status} -ge 128 ]]; then |
| 92 err "Caught signal $((${status} - 128))" |
| 93 fi |
| 94 |
| 95 if [[ -n "${g_temp_dir}" ]]; then |
| 96 rm -rf "${g_temp_dir}" |
| 97 fi |
| 98 |
| 99 exit ${status} |
| 100 } |
| 101 |
| 102 ensure_temp_dir() { |
| 103 if [[ -z "${g_temp_dir}" ]]; then |
| 104 # Choose a template that won't be a dot directory. Make it safe by |
| 105 # removing leading hyphens, too. |
| 106 local template="${ME}" |
| 107 if [[ "${template}" =~ ^[-.]+(.*)$ ]]; then |
| 108 template="${BASH_REMATCH[1]}" |
| 109 fi |
| 110 if [[ -z "${template}" ]]; then |
| 111 template="keystone_install" |
| 112 fi |
| 113 |
| 114 g_temp_dir="$(mktemp -d -t "${template}")" |
| 115 note "g_temp_dir = ${g_temp_dir}" |
| 116 fi |
| 117 } |
| 118 |
77 # Returns 0 (true) if |symlink| exists, is a symbolic link, and appears | 119 # Returns 0 (true) if |symlink| exists, is a symbolic link, and appears |
78 # writable on the basis of its POSIX permissions. This is used to determine | 120 # writable on the basis of its POSIX permissions. This is used to determine |
79 # writability like test's -w primary, but -w resolves symbolic links and this | 121 # writability like test's -w primary, but -w resolves symbolic links and this |
80 # function does not. | 122 # function does not. |
81 is_writable_symlink() { | 123 is_writable_symlink() { |
82 local symlink="${1}" | 124 local symlink="${1}" |
83 | 125 |
84 local link_mode | 126 local link_mode |
85 link_mode="$(stat -f %Sp "${symlink}" 2> /dev/null || true)" | 127 link_mode="$(stat -f %Sp "${symlink}" 2> /dev/null || true)" |
86 if [[ -z "${link_mode}" ]] || [[ "${link_mode:0:1}" != "l" ]]; then | 128 if [[ -z "${link_mode}" ]] || [[ "${link_mode:0:1}" != "l" ]]; then |
(...skipping 90 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
177 | 219 |
178 (ln -fhs "${target}" "${temp_link}" && \ | 220 (ln -fhs "${target}" "${temp_link}" && \ |
179 chmod -h 755 "${temp_link}" && \ | 221 chmod -h 755 "${temp_link}" && \ |
180 mv -f "${temp_link}" "${symlink_dir}/") || true | 222 mv -f "${temp_link}" "${symlink_dir}/") || true |
181 rm -rf "${temp_link_dir}" | 223 rm -rf "${temp_link_dir}" |
182 fi | 224 fi |
183 | 225 |
184 return 0 | 226 return 0 |
185 } | 227 } |
186 | 228 |
| 229 # ensure_writable_symlinks_recursive calls ensure_writable_symlink for every |
| 230 # symbolic link in |directory|, recursivley. |
| 231 # |
| 232 # In some very weird and rare cases, it is possible to wind up with a user |
| 233 # installation that contains symbolic links that the user does not have write |
| 234 # permission over. More on how that might happen later. |
| 235 # |
| 236 # If a weird and rare case like this is observed, rsync will exit with an |
| 237 # error when attempting to update the times on these symbolic links. rsync |
| 238 # may not be intelligent enough to try creating a new symbolic link in these |
| 239 # cases, but this script can be. |
| 240 # |
| 241 # The problem occurs when an administrative user first drag-installs the |
| 242 # application to /Applications, resulting in the program's user being set to |
| 243 # the user's own ID. If, subsequently, a .pkg package is installed over that, |
| 244 # the existing directory ownership will be preserved, but file ownership will |
| 245 # be changed to whateer is specified by the package, typically root. This |
| 246 # applies to symbolic links as well. On a subsequent update, rsync will be |
| 247 # able to copy the new files into place, because the user still has permission |
| 248 # to write to the directories. If the symbolic link targets are not changing, |
| 249 # though, rsync will not replace them, and they will remain owned by root. |
| 250 # The user will not have permission to update the time on the symbolic links, |
| 251 # resulting in an rsync error. |
| 252 ensure_writable_symlinks_recursive() { |
| 253 local directory="${1}" |
| 254 |
| 255 # This fix-up is not necessary when running as root, because root will |
| 256 # always be able to write everything needed. |
| 257 if [[ ${EUID} -eq 0 ]]; then |
| 258 return 0 |
| 259 fi |
| 260 |
| 261 # This step isn't critical. |
| 262 local set_e= |
| 263 if [[ "${-}" =~ e ]]; then |
| 264 set_e="y" |
| 265 set +e |
| 266 fi |
| 267 |
| 268 # Use find -print0 with read -d $'\0' to handle even the weirdest paths. |
| 269 local symlink |
| 270 while IFS= read -r -d $'\0' symlink; do |
| 271 ensure_writable_symlink "${symlink}" |
| 272 done < <(find "${directory}" -type l -print0) |
| 273 |
| 274 # Go back to how things were. |
| 275 if [[ -n "${set_e}" ]]; then |
| 276 set -e |
| 277 fi |
| 278 } |
| 279 |
187 # Prints the version of ksadmin, as reported by ksadmin --ksadmin-version, to | 280 # Prints the version of ksadmin, as reported by ksadmin --ksadmin-version, to |
188 # stdout. This function operates with "static" variables: it will only check | 281 # stdout. This function operates with "static" variables: it will only check |
189 # the ksadmin version once per script run. If ksadmin is old enough to not | 282 # the ksadmin version once per script run. If ksadmin is old enough to not |
190 # support --ksadmin-version, or another error occurs, this function prints an | 283 # support --ksadmin-version, or another error occurs, this function prints an |
191 # empty string. | 284 # empty string. |
192 g_checked_ksadmin_version= | 285 g_checked_ksadmin_version= |
193 g_ksadmin_version= | 286 g_ksadmin_version= |
194 ksadmin_version() { | 287 ksadmin_version() { |
195 if [[ -z "${g_checked_ksadmin_version}" ]]; then | 288 if [[ -z "${g_checked_ksadmin_version}" ]]; then |
196 g_checked_ksadmin_version="y" | 289 g_checked_ksadmin_version="y" |
197 g_ksadmin_version="$(ksadmin --ksadmin-version || true)" | 290 g_ksadmin_version="$(ksadmin --ksadmin-version || true)" |
198 fi | 291 fi |
199 echo "${g_ksadmin_version}" | 292 echo "${g_ksadmin_version}" |
200 return 0 | 293 return 0 |
201 } | 294 } |
202 | 295 |
203 # Compares the installed ksadmin version against a supplied version number, | 296 # Compares the installed ksadmin version against a supplied version number, |
204 # |check_version|, and returns 0 (true) if the installed Keystone version is | 297 # |check_version|, and returns 0 (true) if the installed Keystone version is |
205 # greater than or equal to |check_version| according to a piece-wise | 298 # greater than or equal to |check_version| according to a piece-wise |
206 # comparison. Returns 1 (false) if the installed Keystone version number | 299 # comparison. Returns 1 (false) if the installed Keystone version number |
207 # cannot be determined or if |check_version| is greater than the installed | 300 # cannot be determined or if |check_version| is greater than the installed |
208 # Keystone version. |check_version| should be a string of the form | 301 # Keystone version. |check_version| should be a string of the form |
209 # "major.minor.micro.build". Returns 1 (false) if either |check_version| or | 302 # "major.minor.micro.build". Returns 1 (false) if either |check_version| or |
210 # the Keystone version do not match this format. | 303 # the Keystone version do not match this format. |
211 readonly VER_RE="^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)\$" | 304 readonly KSADMIN_VERSION_RE="^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)\$" |
212 is_ksadmin_version_ge() { | 305 is_ksadmin_version_ge() { |
213 local check_version="${1}" | 306 local check_version="${1}" |
214 | 307 |
215 if ! [[ "${check_version}" =~ ${VER_RE} ]]; then | 308 if ! [[ "${check_version}" =~ ${KSADMIN_VERSION_RE} ]]; then |
216 return 1 | 309 return 1 |
217 fi | 310 fi |
218 | 311 |
219 local check_components=("${BASH_REMATCH[1]}" | 312 local check_components=("${BASH_REMATCH[1]}" |
220 "${BASH_REMATCH[2]}" | 313 "${BASH_REMATCH[2]}" |
221 "${BASH_REMATCH[3]}" | 314 "${BASH_REMATCH[3]}" |
222 "${BASH_REMATCH[4]}") | 315 "${BASH_REMATCH[4]}") |
223 | 316 |
224 local ksadmin_version | 317 local ksadmin_version |
225 ksadmin_version="$(ksadmin_version)" | 318 ksadmin_version="$(ksadmin_version)" |
226 | 319 |
227 if ! [[ "${ksadmin_version}" =~ ${VER_RE} ]]; then | 320 if ! [[ "${ksadmin_version}" =~ ${KSADMIN_VERSION_RE} ]]; then |
228 return 1 | 321 return 1 |
229 fi | 322 fi |
230 | 323 |
231 local ksadmin_components=("${BASH_REMATCH[1]}" | 324 local ksadmin_components=("${BASH_REMATCH[1]}" |
232 "${BASH_REMATCH[2]}" | 325 "${BASH_REMATCH[2]}" |
233 "${BASH_REMATCH[3]}" | 326 "${BASH_REMATCH[3]}" |
234 "${BASH_REMATCH[4]}") | 327 "${BASH_REMATCH[4]}") |
235 | 328 |
236 local i | 329 local i |
237 for i in 0 1 2 3; do | 330 for i in 0 1 2 3; do |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
287 usage() { | 380 usage() { |
288 echo "usage: ${ME} update_dmg_mount_point" >& 2 | 381 echo "usage: ${ME} update_dmg_mount_point" >& 2 |
289 } | 382 } |
290 | 383 |
291 main() { | 384 main() { |
292 local update_dmg_mount_point="${1}" | 385 local update_dmg_mount_point="${1}" |
293 | 386 |
294 # Early steps are critical. Don't continue past any failure. | 387 # Early steps are critical. Don't continue past any failure. |
295 set -e | 388 set -e |
296 | 389 |
| 390 trap cleanup EXIT HUP INT QUIT TERM |
| 391 |
297 readonly PRODUCT_NAME="Google Chrome" | 392 readonly PRODUCT_NAME="Google Chrome" |
298 readonly APP_DIR="${PRODUCT_NAME}.app" | 393 readonly APP_DIR="${PRODUCT_NAME}.app" |
299 readonly FRAMEWORK_NAME="${PRODUCT_NAME} Framework" | 394 readonly FRAMEWORK_NAME="${PRODUCT_NAME} Framework" |
300 readonly FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework" | 395 readonly FRAMEWORK_DIR="${FRAMEWORK_NAME}.framework" |
| 396 readonly PATCH_DIR=".patch" |
301 readonly CONTENTS_DIR="Contents" | 397 readonly CONTENTS_DIR="Contents" |
302 readonly APP_PLIST="${CONTENTS_DIR}/Info" | 398 readonly APP_PLIST="${CONTENTS_DIR}/Info" |
303 readonly VERSIONS_DIR="${CONTENTS_DIR}/Versions" | 399 readonly VERSIONS_DIR="${CONTENTS_DIR}/Versions" |
304 readonly UNROOTED_BRAND_PLIST="Library/Google/Google Chrome Brand" | 400 readonly UNROOTED_BRAND_PLIST="Library/Google/Google Chrome Brand" |
305 readonly UNROOTED_DEBUG_FILE="Library/Google/Google Chrome Updater Debug" | 401 readonly UNROOTED_DEBUG_FILE="Library/Google/Google Chrome Updater Debug" |
306 | 402 |
307 readonly APP_VERSION_KEY="CFBundleShortVersionString" | 403 readonly APP_VERSION_KEY="CFBundleShortVersionString" |
308 readonly KS_VERSION_KEY="KSVersion" | 404 readonly KS_VERSION_KEY="KSVersion" |
309 readonly KS_PRODUCT_KEY="KSProductID" | 405 readonly KS_PRODUCT_KEY="KSProductID" |
310 readonly KS_URL_KEY="KSUpdateURL" | 406 readonly KS_URL_KEY="KSUpdateURL" |
(...skipping 30 matching lines...) Expand all Loading... |
341 note "checking update" | 437 note "checking update" |
342 | 438 |
343 if [[ -z "${update_dmg_mount_point}" ]] || | 439 if [[ -z "${update_dmg_mount_point}" ]] || |
344 [[ "${update_dmg_mount_point:0:1}" != "/" ]] || | 440 [[ "${update_dmg_mount_point:0:1}" != "/" ]] || |
345 ! [[ -d "${update_dmg_mount_point}" ]]; then | 441 ! [[ -d "${update_dmg_mount_point}" ]]; then |
346 err "update_dmg_mount_point must be an absolute path to a directory" | 442 err "update_dmg_mount_point must be an absolute path to a directory" |
347 usage | 443 usage |
348 exit 2 | 444 exit 2 |
349 fi | 445 fi |
350 | 446 |
351 # The update to install. | 447 local patch_dir="${update_dmg_mount_point}/${PATCH_DIR}" |
352 local update_app="${update_dmg_mount_point}/${APP_DIR}" | 448 if [[ "${patch_dir:0:1}" != "/" ]]; then |
353 note "update_app = ${update_app}" | 449 note "patch_dir = ${patch_dir}" |
354 | 450 err "patch_dir must be an absolute path" |
355 # Make sure that there's something to copy from, and that it's an absolute | |
356 # path. | |
357 if [[ "${update_app:0:1}" != "/" ]] || | |
358 ! [[ -d "${update_app}" ]]; then | |
359 err "update_app must be an absolute path to a directory" | |
360 exit 2 | 451 exit 2 |
361 fi | 452 fi |
362 | 453 |
363 # Get some information about the update. | 454 # Figure out if this is an ordinary installation disk image being used as a |
364 note "reading update values" | 455 # full update, or a patch. A patch will have a .patch directory at the root |
| 456 # of the disk image containing information about the update, tools to apply |
| 457 # it, and the update contents. |
| 458 local is_patch= |
| 459 local dirpatcher= |
| 460 if [[ -d "${patch_dir}" ]]; then |
| 461 # patch_dir exists and is a directory - this is a patch update. |
| 462 is_patch="y" |
| 463 dirpatcher="${patch_dir}/dirpatcher.sh" |
| 464 if ! [[ -x "${dirpatcher}" ]]; then |
| 465 err "couldn't locate dirpatcher" |
| 466 exit 6 |
| 467 fi |
| 468 elif [[ -e "${patch_dir}" ]]; then |
| 469 # patch_dir exists, but is not a directory - what's that mean? |
| 470 note "patch_dir = ${patch_dir}" |
| 471 err "patch_dir must be a directory" |
| 472 exit 2 |
| 473 else |
| 474 # patch_dir does not exist - this is a full "installer." |
| 475 patch_dir= |
| 476 fi |
| 477 note "patch_dir = ${patch_dir}" |
| 478 note "is_patch = ${is_patch}" |
| 479 note "dirpatcher = ${dirpatcher}" |
365 | 480 |
366 local update_app_plist="${update_app}/${APP_PLIST}" | 481 # The update to install. |
367 note "update_app_plist = ${update_app_plist}" | 482 |
368 local update_version_app | 483 # update_app is the path to the new version of the .app. It will only be |
369 if ! update_version_app="$(defaults read "${update_app_plist}" \ | 484 # set at this point for a non-patch update. It is not yet set for a patch |
370 "${APP_VERSION_KEY}")" || | 485 # update because no such directory exists yet; it will be set later when |
371 [[ -z "${update_version_app}" ]]; then | 486 # dirpatcher creates it. |
372 err "couldn't determine update_version_app" | 487 local update_app= |
373 exit 2 | 488 |
| 489 # update_version_app_old, patch_app_dir, and patch_versioned_dir will only |
| 490 # be set for patch updates. |
| 491 local update_version_app_old= |
| 492 local patch_app_dir= |
| 493 local patch_versioned_dir= |
| 494 |
| 495 local update_version_app update_version_ks product_id |
| 496 if [[ -z "${is_patch}" ]]; then |
| 497 update_app="${update_dmg_mount_point}/${APP_DIR}" |
| 498 note "update_app = ${update_app}" |
| 499 |
| 500 # Make sure that there's something to copy from, and that it's an absolute |
| 501 # path. |
| 502 if [[ "${update_app:0:1}" != "/" ]] || |
| 503 ! [[ -d "${update_app}" ]]; then |
| 504 err "update_app must be an absolute path to a directory" |
| 505 exit 2 |
| 506 fi |
| 507 |
| 508 # Get some information about the update. |
| 509 note "reading update values" |
| 510 |
| 511 local update_app_plist="${update_app}/${APP_PLIST}" |
| 512 note "update_app_plist = ${update_app_plist}" |
| 513 if ! update_version_app="$(defaults read "${update_app_plist}" \ |
| 514 "${APP_VERSION_KEY}")" || |
| 515 [[ -z "${update_version_app}" ]]; then |
| 516 err "couldn't determine update_version_app" |
| 517 exit 2 |
| 518 fi |
| 519 note "update_version_app = ${update_version_app}" |
| 520 |
| 521 local update_ks_plist="${update_app_plist}" |
| 522 note "update_ks_plist = ${update_ks_plist}" |
| 523 if ! update_version_ks="$(defaults read "${update_ks_plist}" \ |
| 524 "${KS_VERSION_KEY}")" || |
| 525 [[ -z "${update_version_ks}" ]]; then |
| 526 err "couldn't determine update_version_ks" |
| 527 exit 2 |
| 528 fi |
| 529 note "update_version_ks = ${update_version_ks}" |
| 530 |
| 531 if ! product_id="$(defaults read "${update_ks_plist}" \ |
| 532 "${KS_PRODUCT_KEY}")" || |
| 533 [[ -z "${product_id}" ]]; then |
| 534 err "couldn't determine product_id" |
| 535 exit 2 |
| 536 fi |
| 537 note "product_id = ${product_id}" |
| 538 else # [[ -n "${is_patch}" ]] |
| 539 # Get some information about the update. |
| 540 note "reading update values" |
| 541 |
| 542 if ! update_version_app_old=$(<"${patch_dir}/old_app_version") || |
| 543 [[ -z "${update_version_app_old}" ]]; then |
| 544 err "couldn't determine update_version_app_old" |
| 545 exit 2 |
| 546 fi |
| 547 note "update_version_app_old = ${update_version_app_old}" |
| 548 |
| 549 if ! update_version_app=$(<"${patch_dir}/new_app_version") || |
| 550 [[ -z "${update_version_app}" ]]; then |
| 551 err "couldn't determine update_version_app" |
| 552 exit 2 |
| 553 fi |
| 554 note "update_version_app = ${update_version_app}" |
| 555 |
| 556 if ! update_version_ks=$(<"${patch_dir}/new_ks_version") || |
| 557 [[ -z "${update_version_ks}" ]]; then |
| 558 err "couldn't determine update_version_ks" |
| 559 exit 2 |
| 560 fi |
| 561 note "update_version_ks = ${update_version_ks}" |
| 562 |
| 563 if ! product_id=$(<"${patch_dir}/ks_product") || |
| 564 [[ -z "${product_id}" ]]; then |
| 565 err "couldn't determine product_id" |
| 566 exit 2 |
| 567 fi |
| 568 note "product_id = ${product_id}" |
| 569 |
| 570 patch_app_dir="${patch_dir}/application.dirpatch" |
| 571 if ! [[ -d "${patch_app_dir}" ]]; then |
| 572 err "couldn't locate patch_app_dir" |
| 573 exit 6 |
| 574 fi |
| 575 note "patch_app_dir = ${patch_app_dir}" |
| 576 |
| 577 patch_versioned_dir=\ |
| 578 "${patch_dir}/version_${update_version_app_old}_${update_version_app}.dirpatch" |
| 579 if ! [[ -d "${patch_versioned_dir}" ]]; then |
| 580 err "couldn't locate patch_versioned_dir" |
| 581 exit 6 |
| 582 fi |
| 583 note "patch_versioned_dir = ${patch_versioned_dir}" |
374 fi | 584 fi |
375 note "update_version_app = ${update_version_app}" | |
376 | |
377 local update_ks_plist="${update_app_plist}" | |
378 note "update_ks_plist = ${update_ks_plist}" | |
379 local update_version_ks | |
380 if ! update_version_ks="$(defaults read "${update_ks_plist}" \ | |
381 "${KS_VERSION_KEY}")" || | |
382 [[ -z "${update_version_ks}" ]]; then | |
383 err "couldn't determine update_version_ks" | |
384 exit 2 | |
385 fi | |
386 note "update_version_ks = ${update_version_ks}" | |
387 | |
388 local product_id | |
389 if ! product_id="$(defaults read "${update_ks_plist}" \ | |
390 "${KS_PRODUCT_KEY}")" || | |
391 [[ -z "${product_id}" ]]; then | |
392 err "couldn't determine product_id" | |
393 exit 2 | |
394 fi | |
395 note "product_id = ${product_id}" | |
396 | 585 |
397 # ksadmin is required. Keystone should have set a ${PATH} that includes it. | 586 # ksadmin is required. Keystone should have set a ${PATH} that includes it. |
398 # Check that here, so that more useful feedback can be offered in the | 587 # Check that here, so that more useful feedback can be offered in the |
399 # unlikely event that ksadmin is missing. | 588 # unlikely event that ksadmin is missing. |
400 note "checking Keystone" | 589 note "checking Keystone" |
401 | 590 |
402 local ksadmin_path | 591 local ksadmin_path |
403 if ! ksadmin_path="$(type -p ksadmin)" || [[ -z "${ksadmin_path}" ]]; then | 592 if ! ksadmin_path="$(type -p ksadmin)" || [[ -z "${ksadmin_path}" ]]; then |
404 err "couldn't locate ksadmin_path" | 593 err "couldn't locate ksadmin_path" |
405 exit 3 | 594 exit 3 |
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
472 | 661 |
473 local installed_app_plist="${installed_app}/${APP_PLIST}" | 662 local installed_app_plist="${installed_app}/${APP_PLIST}" |
474 note "installed_app_plist = ${installed_app_plist}" | 663 note "installed_app_plist = ${installed_app_plist}" |
475 local installed_app_plist_path="${installed_app_plist}.plist" | 664 local installed_app_plist_path="${installed_app_plist}.plist" |
476 note "installed_app_plist_path = ${installed_app_plist_path}" | 665 note "installed_app_plist_path = ${installed_app_plist_path}" |
477 local old_version_app | 666 local old_version_app |
478 old_version_app="$(defaults read "${installed_app_plist}" \ | 667 old_version_app="$(defaults read "${installed_app_plist}" \ |
479 "${APP_VERSION_KEY}" || true)" | 668 "${APP_VERSION_KEY}" || true)" |
480 note "old_version_app = ${old_version_app}" | 669 note "old_version_app = ${old_version_app}" |
481 | 670 |
| 671 # old_version_app is not required, because it won't be present in skeleton |
| 672 # bootstrap installations, which just have an empty .app directory. Only |
| 673 # require it when doing a patch update, and use it to validate that the |
| 674 # patch applies to the old installed version. By definition, skeleton |
| 675 # bootstraps can't be installed with patch udpates. They require the full |
| 676 # application on the disk image. |
| 677 if [[ -n "${is_patch}" ]]; then |
| 678 if [[ -z "${old_version_app}" ]]; then |
| 679 err "old_version_app required for patch" |
| 680 exit 6 |
| 681 elif [[ "${old_version_app}" != "${update_version_app_old}" ]]; then |
| 682 err "this patch does not apply to the installed version" |
| 683 exit 6 |
| 684 fi |
| 685 fi |
| 686 |
482 local installed_versions_dir="${installed_app}/${VERSIONS_DIR}" | 687 local installed_versions_dir="${installed_app}/${VERSIONS_DIR}" |
483 note "installed_versions_dir = ${installed_versions_dir}" | 688 note "installed_versions_dir = ${installed_versions_dir}" |
484 | 689 |
485 # If the installed application is incredibly old, old_versioned_dir may not | 690 # If the installed application is incredibly old, old_versioned_dir may not |
486 # exist. | 691 # exist. |
487 local old_versioned_dir | 692 local old_versioned_dir |
488 if [[ -n "${old_version_app}" ]]; then | 693 if [[ -n "${old_version_app}" ]]; then |
489 old_versioned_dir="${installed_versions_dir}/${old_version_app}" | 694 old_versioned_dir="${installed_versions_dir}/${old_version_app}" |
490 fi | 695 fi |
491 note "old_versioned_dir = ${old_versioned_dir}" | 696 note "old_versioned_dir = ${old_versioned_dir}" |
492 | 697 |
493 # Collect the installed application's brand code, it will be used later. It | 698 # Collect the installed application's brand code, it will be used later. It |
494 # is not an error for the installed application to not have a brand code. | 699 # is not an error for the installed application to not have a brand code. |
495 local old_ks_plist="${installed_app_plist}" | 700 local old_ks_plist="${installed_app_plist}" |
496 note "old_ks_plist = ${old_ks_plist}" | 701 note "old_ks_plist = ${old_ks_plist}" |
497 local old_brand | 702 local old_brand |
498 old_brand="$(defaults read "${old_ks_plist}" \ | 703 old_brand="$(defaults read "${old_ks_plist}" \ |
499 "${KS_BRAND_KEY}" 2> /dev/null || | 704 "${KS_BRAND_KEY}" 2> /dev/null || |
500 true)" | 705 true)" |
501 note "old_brand = ${old_brand}" | 706 note "old_brand = ${old_brand}" |
502 | 707 |
| 708 ensure_writable_symlinks_recursive "${installed_app}" |
| 709 |
| 710 # By copying to ${installed_app}, the existing application name will be |
| 711 # preserved, if the user has renamed the application on disk. Respecting |
| 712 # the user's changes is friendly. |
| 713 |
| 714 # Make sure that ${installed_versions_dir} exists, so that it can receive |
| 715 # the versioned directory. It may not exist if updating from an older |
| 716 # version that did not use the versioned layout on disk. Later, during the |
| 717 # rsync to copy the applciation directory, the mode bits and timestamp on |
| 718 # ${installed_versions_dir} will be set to conform to whatever is present in |
| 719 # the update. |
| 720 # |
| 721 # ${installed_app} is guaranteed to exist at this point, but |
| 722 # ${installed_app}/${CONTENTS_DIR} may not if things are severely broken or |
| 723 # if this update is actually an initial installation from a Keystone |
| 724 # skeleton bootstrap. The mkdir creates ${installed_app}/${CONTENTS_DIR} if |
| 725 # it doesn't exist; its mode bits will be fixed up in a subsequent rsync. |
| 726 note "creating installed_versions_dir" |
| 727 if ! mkdir -p "${installed_versions_dir}"; then |
| 728 err "mkdir of installed_versions_dir failed" |
| 729 exit 5 |
| 730 fi |
| 731 |
| 732 local new_versioned_dir |
| 733 new_versioned_dir="${installed_versions_dir}/${update_version_app}" |
| 734 note "new_versioned_dir = ${new_versioned_dir}" |
| 735 |
| 736 # If there's an entry at ${new_versioned_dir} but it's not a directory |
| 737 # (or it's a symbolic link, whether or not it points to a directory), rsync |
| 738 # won't get rid of it. It's never correct to have a non-directory in place |
| 739 # of the versioned directory, so toss out whatever's there. Don't treat |
| 740 # this as a critical step: if removal fails, operation can still proceed to |
| 741 # to the dirpatcher or rsync, which will likely fail. |
| 742 if [[ -e "${new_versioned_dir}" ]] && |
| 743 ([[ -L "${new_versioned_dir}" ]] || |
| 744 ! [[ -d "${new_versioned_dir}" ]]); then |
| 745 note "removing non-directory in place of versioned directory" |
| 746 rm -f "${new_versioned_dir}" 2> /dev/null || true |
| 747 fi |
| 748 |
| 749 local update_versioned_dir |
| 750 if [[ -z "${is_patch}" ]]; then |
| 751 update_versioned_dir="${update_app}/${VERSIONS_DIR}/${update_version_app}" |
| 752 note "update_versioned_dir = ${update_versioned_dir}" |
| 753 else # [[ -n "${is_patch}" ]] |
| 754 # dirpatcher won't patch into a directory that already exists. Doing so |
| 755 # would be a bad idea, anyway. If ${new_versioned_dir} already exists, |
| 756 # it may be something left over from a previous failed or incomplete |
| 757 # update attempt, or it may be the live versioned directory if this is a |
| 758 # same-version update intended only to change channels. Since there's no |
| 759 # way to tell, this case is handled by having dirpatcher produce the new |
| 760 # versioned directory in a temporary location and then having rsync copy |
| 761 # it into place as an ${update_versioned_dir}, the same as in a non-patch |
| 762 # update. If ${new_versioned_dir} doesn't exist, dirpatcher can place the |
| 763 # new versioned directory at that location directly. |
| 764 local versioned_dir_target |
| 765 if ! [[ -e "${new_versioned_dir}" ]]; then |
| 766 versioned_dir_target="${new_versioned_dir}" |
| 767 note "versioned_dir_target = ${versioned_dir_target}" |
| 768 else |
| 769 ensure_temp_dir |
| 770 versioned_dir_target="${g_temp_dir}/${update_version_app}" |
| 771 note "versioned_dir_target = ${versioned_dir_target}" |
| 772 update_versioned_dir="${versioned_dir_target}" |
| 773 note "update_versioned_dir = ${update_versioned_dir}" |
| 774 fi |
| 775 |
| 776 note "dirpatching versioned directory" |
| 777 if ! "${dirpatcher}" "${old_versioned_dir}" \ |
| 778 "${patch_versioned_dir}" \ |
| 779 "${versioned_dir_target}"; then |
| 780 err "dirpatcher of versioned directory failed, status ${PIPESTATUS[0]}" |
| 781 exit 12 |
| 782 fi |
| 783 fi |
| 784 |
| 785 # Copy the versioned directory. The new versioned directory should have a |
| 786 # different name than any existing one, so this won't harm anything already |
| 787 # present in ${installed_versions_dir}, including the versioned directory |
| 788 # being used by any running processes. If this step is interrupted, there |
| 789 # will be an incomplete versioned directory left behind, but it won't |
| 790 # won't interfere with anything, and it will be replaced or removed during a |
| 791 # future update attempt. |
| 792 # |
| 793 # In certain cases, same-version updates are distributed to move users |
| 794 # between channels; when this happens, the contents of the versioned |
| 795 # directories are identical and rsync will not render the versioned |
| 796 # directory unusable even for an instant. |
| 797 # |
| 798 # ${update_versioned_dir} may be empty during a patch update (${is_patch}) |
| 799 # if the dirpatcher above was able to write it into place directly. In |
| 800 # that event, dirpatcher guarantees that ${new_versioned_dir} is already in |
| 801 # place. |
| 802 if [[ -n "${update_versioned_dir}" ]]; then |
| 803 note "rsyncing versioned directory" |
| 804 if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \ |
| 805 "${new_versioned_dir}"; then |
| 806 err "rsync of versioned directory failed, status ${PIPESTATUS[0]}" |
| 807 exit 7 |
| 808 fi |
| 809 fi |
| 810 |
| 811 if [[ -n "${is_patch}" ]]; then |
| 812 # If the versioned directory was prepared in a temporary directory and |
| 813 # then rsynced into place, remove the temporary copy now that it's no |
| 814 # longer needed. |
| 815 if [[ -n "${update_versioned_dir}" ]]; then |
| 816 rm -rf "${update_versioned_dir}" 2> /dev/null || true |
| 817 update_versioned_dir= |
| 818 note "update_versioned_dir = ${update_versioned_dir}" |
| 819 fi |
| 820 |
| 821 # Prepare ${update_app}. This always needs to be done in a temporary |
| 822 # location because dirpatcher won't write to a directory that already |
| 823 # exists, and ${installed_app} needs to be used as input to dirpatcher |
| 824 # in any event. The new application will be rsynced into place once |
| 825 # dirpatcher creates it. |
| 826 ensure_temp_dir |
| 827 update_app="${g_temp_dir}/${APP_DIR}" |
| 828 note "update_app = ${update_app}" |
| 829 |
| 830 note "dirpatching app directory" |
| 831 if ! "${dirpatcher}" "${installed_app}" \ |
| 832 "${patch_app_dir}" \ |
| 833 "${update_app}"; then |
| 834 err "dirpatcher of app directory failed, status ${PIPESTATUS[0]}" |
| 835 exit 13 |
| 836 fi |
| 837 fi |
| 838 |
503 # See if the timestamp of what's currently on disk is newer than the | 839 # See if the timestamp of what's currently on disk is newer than the |
504 # update's outer .app's timestamp. rsync will copy the update's timestamp | 840 # update's outer .app's timestamp. rsync will copy the update's timestamp |
505 # over, but if that timestamp isn't as recent as what's already on disk, the | 841 # over, but if that timestamp isn't as recent as what's already on disk, the |
506 # .app will need to be touched. | 842 # .app will need to be touched. |
507 local needs_touch= | 843 local needs_touch= |
508 if [[ "${installed_app}" -nt "${update_app}" ]]; then | 844 if [[ "${installed_app}" -nt "${update_app}" ]]; then |
509 needs_touch="y" | 845 needs_touch="y" |
510 fi | 846 fi |
511 note "needs_touch = ${needs_touch}" | 847 note "needs_touch = ${needs_touch}" |
512 | 848 |
513 # In some very weird and rare cases, it is possible to wind up with a user | |
514 # installation that contains symbolic links that the user does not have | |
515 # write permission over. More on how that might happen later. | |
516 # | |
517 # If a weird and rare case like this is observed, rsync will exit with an | |
518 # error when attempting to update the times on these symbolic links. rsync | |
519 # may not be intelligent enough to try creating a new symbolic link in these | |
520 # cases, but this script can be. | |
521 # | |
522 # This fix-up is not necessary when running as root, because root will | |
523 # always be able to write everything needed. | |
524 # | |
525 # The problem occurs when an administrative user first drag-installs the | |
526 # application to /Applications, resulting in the program's user being set to | |
527 # the user's own ID. If, subsequently, a .pkg package is installed over | |
528 # that, the existing directory ownership will be preserved, but file | |
529 # ownership will be changed to whateer is specified by the package, | |
530 # typically root. This applies to symbolic links as well. On a subsequent | |
531 # update, rsync will be able to copy the new files into place, because the | |
532 # user still has permission to write to the directories. If the symbolic | |
533 # link targets are not changing, though, rsync will not replace them, and | |
534 # they will remain owned by root. The user will not have permission to | |
535 # update the time on the symbolic links, resulting in an rsync error. | |
536 if [[ ${EUID} -ne 0 ]]; then | |
537 # This step isn't critical. | |
538 set +e | |
539 note "fixing installed symbolic links" | |
540 | |
541 # Only consider symbolic links in ${update_app}. If there are any other | |
542 # links in ${installed_app} not present in ${update_app}, rsync will | |
543 # delete them as needed later. Use find -print0 with read -d $'\0' to | |
544 # handle even the weirdest paths. | |
545 local update_link | |
546 while IFS= read -r -d $'\0' update_link; do | |
547 # ${update_link} is relative to ${update_app}. Prepending | |
548 # ${installed_app} looks for the same link already on disk. | |
549 local installed_link="${installed_app}/${update_link}" | |
550 note "ensure_writable_symlink ${installed_link}" | |
551 ensure_writable_symlink "${installed_link}" | |
552 done < <(cd "${update_app}" && find . -type l -print0) | |
553 | |
554 # Go back to how things were. | |
555 set -e | |
556 fi | |
557 | |
558 # By copying to ${installed_app}, the existing application name will be | |
559 # preserved, if the user has renamed the application on disk. Respecting | |
560 # the user's changes is friendly. | |
561 | |
562 # Make sure that ${installed_versions_dir} exists, so that it can receive | |
563 # the versioned directory. It may not exist if updating from an older | |
564 # version that did not use the versioned layout on disk. An rsync that | |
565 # excludes all contents is used to bring the permissions over from | |
566 # ${update_versions_dir}, otherwise, this directory would be the only one in | |
567 # the entire update exempt from getting its permissions copied over. A | |
568 # simple mkdir wouldn't copy mode bits. This is done even if | |
569 # ${installed_versions_dir} already does exist to ensure that the mode bits | |
570 # come from the update. | |
571 # | |
572 # ${installed_app} is guaranteed to exist at this point, but | |
573 # ${installed_app}/${CONTENTS_DIR} may not if things are severely broken or | |
574 # if this update is actually an initial installation from a Keystone | |
575 # skeleton bootstrap. The mkdir creates ${installed_app}/${CONTENTS_DIR} if | |
576 # it doesn't exist; its mode bits will be fixed up in a subsequent rsync. | |
577 note "creating CONTENTS_DIR" | |
578 if ! mkdir -p "${installed_app}/${CONTENTS_DIR}"; then | |
579 err "mkdir of CONTENTS_DIR failed" | |
580 exit 5 | |
581 fi | |
582 | |
583 local update_versions_dir="${update_app}/${VERSIONS_DIR}" | |
584 note "update_versions_dir = ${update_versions_dir}" | |
585 | |
586 note "rsyncing VERSIONS_DIR" | |
587 if ! rsync ${RSYNC_FLAGS} --exclude "*" "${update_versions_dir}/" \ | |
588 "${installed_versions_dir}"; then | |
589 err "rsync of VERSIONS_DIR failed, status ${PIPESTATUS[0]}" | |
590 exit 6 | |
591 fi | |
592 | |
593 # Copy the versioned directory. The new versioned directory should have a | |
594 # different name than any existing one, so this won't harm anything already | |
595 # present in ${installed_versions_dir}, including the versioned directory | |
596 # being used by any running processes. If this step is interrupted, there | |
597 # will be an incomplete versioned directory left behind, but it won't | |
598 # won't interfere with anything, and it will be replaced or removed during a | |
599 # future update attempt. Note that in certain cases, same-version updates | |
600 # are distributed to move users between channels; when this happens, the | |
601 # contents of the versioned directories are identical and rsync will not | |
602 # render the versioned directory unusable even for an instant. | |
603 local update_versioned_dir new_versioned_dir | |
604 update_versioned_dir="${update_versions_dir}/${update_version_app}" | |
605 note "update_versioned_dir = ${update_versioned_dir}" | |
606 new_versioned_dir="${installed_versions_dir}/${update_version_app}" | |
607 note "new_versioned_dir = ${new_versioned_dir}" | |
608 | |
609 note "rsyncing versioned directory" | |
610 if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \ | |
611 "${new_versioned_dir}"; then | |
612 err "rsync of versioned directory failed, status ${PIPESTATUS[0]}" | |
613 exit 7 | |
614 fi | |
615 | |
616 # Copy the unversioned files into place, leaving everything in | 849 # Copy the unversioned files into place, leaving everything in |
617 # ${installed_versions_dir} alone. If this step is interrupted, the | 850 # ${installed_versions_dir} alone. If this step is interrupted, the |
618 # application will at least remain in a usable state, although it may not | 851 # application will at least remain in a usable state, although it may not |
619 # pass signature validation. Depending on when this step is interrupted, | 852 # pass signature validation. Depending on when this step is interrupted, |
620 # the application will either launch the old or the new version. The | 853 # the application will either launch the old or the new version. The |
621 # critical point is when the main executable is replaced. There isn't very | 854 # critical point is when the main executable is replaced. There isn't very |
622 # much to copy in this step, because most of the application is in the | 855 # much to copy in this step, because most of the application is in the |
623 # versioned directory. This step only accounts for around 50 files, most of | 856 # versioned directory. This step only accounts for around 50 files, most of |
624 # which are small localized InfoPlist.strings files. | 857 # which are small localized InfoPlist.strings files. Note that |
| 858 # ${VERSIONS_DIR} is included to copy its mode bits and timestamp, but its |
| 859 # contents are excluded, having already been installed above. |
625 note "rsyncing app directory" | 860 note "rsyncing app directory" |
626 if ! rsync ${RSYNC_FLAGS} --delete-after --exclude "/${VERSIONS_DIR}" \ | 861 if ! rsync ${RSYNC_FLAGS} --delete-after --exclude "/${VERSIONS_DIR}/*" \ |
627 "${update_app}/" "${installed_app}"; then | 862 "${update_app}/" "${installed_app}"; then |
628 err "rsync of app directory failed, status ${PIPESTATUS[0]}" | 863 err "rsync of app directory failed, status ${PIPESTATUS[0]}" |
629 exit 8 | 864 exit 8 |
630 fi | 865 fi |
631 | 866 |
632 note "rsyncs complete" | 867 note "rsyncs complete" |
633 | 868 |
| 869 if [[ -n "${is_patch}" ]]; then |
| 870 # update_app has been rsynced into place and is no longer needed. |
| 871 rm -rf "${update_app}" 2> /dev/null || true |
| 872 update_app= |
| 873 note "update_app = ${update_app}" |
| 874 fi |
| 875 |
| 876 if [[ -n "${g_temp_dir}" ]]; then |
| 877 # The temporary directory, if any, is no longer needed. |
| 878 rm -rf "${g_temp_dir}" 2> /dev/null || true |
| 879 g_temp_dir= |
| 880 note "g_temp_dir = ${g_temp_dir}" |
| 881 fi |
| 882 |
634 # If necessary, touch the outermost .app so that it appears to the outside | 883 # If necessary, touch the outermost .app so that it appears to the outside |
635 # world that something was done to the bundle. This will cause | 884 # world that something was done to the bundle. This will cause |
636 # LaunchServices to invalidate the information it has cached about the | 885 # LaunchServices to invalidate the information it has cached about the |
637 # bundle even if lsregister does not run. This is not done if rsync already | 886 # bundle even if lsregister does not run. This is not done if rsync already |
638 # updated the timestamp to something newer than what had been on disk. This | 887 # updated the timestamp to something newer than what had been on disk. This |
639 # is not considered a critical step, and if it fails, this script will not | 888 # is not considered a critical step, and if it fails, this script will not |
640 # exit. | 889 # exit. |
641 if [[ -n "${needs_touch}" ]]; then | 890 if [[ -n "${needs_touch}" ]]; then |
642 touch -cf "${installed_app}" || true | 891 touch -cf "${installed_app}" || true |
643 fi | 892 fi |
(...skipping 316 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
960 # On 10.6, xattr supports -r for recursive operation. | 1209 # On 10.6, xattr supports -r for recursive operation. |
961 xattr -d -r "${QUARANTINE_ATTR}" "${installed_app}" 2> /dev/null | 1210 xattr -d -r "${QUARANTINE_ATTR}" "${installed_app}" 2> /dev/null |
962 else | 1211 else |
963 # On earlier systems, xattr doesn't support -r, so run xattr via find. | 1212 # On earlier systems, xattr doesn't support -r, so run xattr via find. |
964 find "${installed_app}" -exec xattr -d "${QUARANTINE_ATTR}" {} + \ | 1213 find "${installed_app}" -exec xattr -d "${QUARANTINE_ATTR}" {} + \ |
965 2> /dev/null | 1214 2> /dev/null |
966 fi | 1215 fi |
967 | 1216 |
968 # Great success! | 1217 # Great success! |
969 note "done!" | 1218 note "done!" |
| 1219 |
| 1220 trap - EXIT |
| 1221 |
970 return 0 | 1222 return 0 |
971 } | 1223 } |
972 | 1224 |
973 # Check "less than" instead of "not equal to" in case Keystone ever changes to | 1225 # Check "less than" instead of "not equal to" in case Keystone ever changes to |
974 # pass more arguments. | 1226 # pass more arguments. |
975 if [[ ${#} -lt 1 ]]; then | 1227 if [[ ${#} -lt 1 ]]; then |
976 usage | 1228 usage |
977 exit 2 | 1229 exit 2 |
978 fi | 1230 fi |
979 | 1231 |
980 main "${@}" | 1232 main "${@}" |
981 exit ${?} | 1233 exit ${?} |
OLD | NEW |