Gracefully handle Omaha 4 update install timeout on macOS (#36914)
This commit is contained in:
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user