Gracefully handle Omaha 4 update install timeout on macOS (#36914)

This commit is contained in:
Michael Herrmann
2026-06-02 19:50:59 +02:00
committed by GitHub
parent b66aed6cf1
commit af3d0067dc
3 changed files with 70 additions and 7 deletions
+19 -2
View File
@@ -1,5 +1,5 @@
diff --git a/chrome/updater/mac/.install.sh b/chrome/updater/mac/.install.sh
index 3b1874d09fca0cf88e361927dfea7c4273790030..c98a50dab0f753aff3a604b43d1659b9eb78abd0 100755
index 3b1874d09fca0cf88e361927dfea7c4273790030..67479bdad6fbd5e95231e0ecc0dae081801f606e 100755
--- a/chrome/updater/mac/.install.sh
+++ b/chrome/updater/mac/.install.sh
@@ -322,7 +322,7 @@ main() {
@@ -20,11 +20,28 @@ index 3b1874d09fca0cf88e361927dfea7c4273790030..c98a50dab0f753aff3a604b43d1659b9
note "update_app = ${update_app}"
# Make sure that it's an absolute path.
@@ -463,7 +463,52 @@ main() {
@@ -455,15 +455,68 @@ main() {
# directory unusable even for an instant.
if [[ -n "${update_versioned_dir}" ]]; then
note "rsyncing versioned directory"
- if ! rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \
+ if ! perl -e 'alarm shift; exec @ARGV' "${RSYNC_TIMEOUT:-600}" rsync ${RSYNC_FLAGS} --delete-before "${update_versioned_dir}/" \
"${new_versioned_dir}"; then
+ local rsync_status=${PIPESTATUS[0]}
+ # The assignment we just made set PIPESTATUS[0] to 0. Restore it. The
+ # leading `!` keeps `set -e` from terminating the script.
+ ! (exit "${rsync_status}")
err "rsync of versioned directory failed, status ${PIPESTATUS[0]}"
# If the rsync of a new-layout versioned directory failed, remove it.
# The incomplete version would break code signature validation.
note "cleaning up new_versioned_dir"
rm -rf "${new_versioned_dir}"
- exit 12
+ if [[ ${rsync_status} -eq 142 ]]; then
+ # rsync timed out.
+ exit 80
+ fi
+ # Try various remedies; Report in exit code what would have worked.
+ local mkdir_stderr
+ if ! mkdir_stderr="$(mkdir -p "${new_versioned_dir}" 2>&1 1>/dev/null)"
+24 -1
View File
@@ -27,6 +27,25 @@ description = 'Omit perms, link and dir times when non-root'
pattern = 'RSYNC_FLAGS="--ignore-times --links --perms --recursive --times"'
replace = 'RSYNC_FLAGS="--ignore-times --links $(if [[ ${EUID} -eq 0 ]]; then echo "--perms --recursive --times"; else echo "--no-perms --executability --chmod=u=rwX,go=rX --recursive"; fi)"'
# install_from_archive.mm executes .install.sh with a timeout of 15 minutes. The
# versioned-dir rsync normally takes only a few seconds. But there was one
# reported case where it took 16 minutes. (It seems likely that there was some
# interference; either the computer went to sleep or the OS was blocking on a
# prompt.) This case bricked the browser because install_from_archive.mm then
# interrupted the later rsyncing of the app directory mid-execution.
# The following substitution runs rsync with its own, lower timeout. This gives
# the script a chance to put the browser back into a usable state when the
# parent timeout would have likely been exceeded.
[[substitution]]
description = 'Cap versioned-dir rsync at 10 minutes, capture status'
re_pattern = '''if ! rsync (.*? "\$\{update_versioned_dir\}/" .*?); then'''
re_flags = ['DOTALL']
replace = '''if ! perl -e 'alarm shift; exec @ARGV' "${RSYNC_TIMEOUT:-600}" rsync \1; then
local rsync_status=${PIPESTATUS[0]}
# The assignment we just made set PIPESTATUS[0] to 0. Restore it. The
# leading `!` keeps `set -e` from terminating the script.
! (exit "${rsync_status}")'''
[[substitution]]
description = '''Try various fixes for exit code 12 failures.
@@ -40,7 +59,11 @@ We start our custom codes at 70 in order to not clash with upstream's, even in
case they add new ones beyond the current maximum 16.
'''
pattern = 'exit 12'
replace = '''# Try various remedies; Report in exit code what would have worked.
replace = '''if [[ ${rsync_status} -eq 142 ]]; then
# rsync timed out.
exit 80
fi
# Try various remedies; Report in exit code what would have worked.
local mkdir_stderr
if ! mkdir_stderr="$(mkdir -p "${new_versioned_dir}" 2>&1 1>/dev/null)"
then
+25 -2
View File
@@ -14,6 +14,7 @@ from stat import S_IXUSR
from subprocess import run, DEVNULL, Popen, PIPE, STDOUT
from tempfile import TemporaryDirectory
from threading import Thread
from time import sleep
import plistlib
import re
@@ -200,6 +201,19 @@ class InstallShPatchTest(unittest.TestCase):
},
expected_exit_code=expected_exit_code)
def test_rsync_timeout(self):
def rsync(_):
sleep(2)
return 0, ""
app_dir = join(self.temp_dir.name, f"{PRODUCT_NAME}.app")
self._make_app(app_dir, CURRENT_VERSION)
self._run_install_sh(app_dir,
commands={'rsync': rsync},
expected_exit_code=80,
env={"RSYNC_TIMEOUT": "1"})
def _prepare_dmg_dir(self):
dmg_dir = join(self.temp_dir.name, "dmg")
mkdir(dmg_dir)
@@ -265,15 +279,17 @@ class InstallShPatchTest(unittest.TestCase):
installed_app_dir,
is_root=False,
commands=None,
expected_exit_code=0):
expected_exit_code=0,
env=None):
commands = commands or {}
env = env.copy() if env is not None else {}
for name in commands:
wrapper_path = join(self.bin_dir, name)
with open(wrapper_path, "w") as f:
f.write(COMMAND_WRAPPER)
chmod(wrapper_path, stat(wrapper_path).st_mode | S_IXUSR)
prompt_r, prompt_w = pipe()
env = {"PROMPT_FD": str(prompt_w)}
env["PROMPT_FD"] = str(prompt_w)
if is_root:
env["EUID"] = "0"
proc = Popen([
@@ -304,14 +320,21 @@ class InstallShPatchTest(unittest.TestCase):
for line in prompts:
name, *args = shlex.split(line)
exit_code, stderr = commands[name](args)
try:
proc.stdin.write(f"{exit_code}\n{stderr}\n")
proc.stdin.flush()
except BrokenPipeError:
break
proc.wait(timeout=30)
finally:
# Reap the subprocess if wait() timed out or the loop raised.
if proc.poll() is None:
proc.kill()
proc.wait()
try:
proc.stdin.close()
except BrokenPipeError:
pass
drain_thread.join(timeout=5)
output = "".join(output_lines)
self.assertEqual(