From 4e259b04613c14825c02274616e843f72bc619bd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 23 Apr 2026 07:03:17 +0100 Subject: [PATCH] fix: harden parallels update smoke --- scripts/e2e/parallels-macos-smoke.sh | 46 ++++++++++++++++++++---- src/infra/package-dist-inventory.test.ts | 5 ++- src/infra/package-dist-inventory.ts | 15 +++++++- src/infra/update-global.ts | 3 ++ 4 files changed, 60 insertions(+), 9 deletions(-) diff --git a/scripts/e2e/parallels-macos-smoke.sh b/scripts/e2e/parallels-macos-smoke.sh index 4648e0a326e..b7702b9e612 100644 --- a/scripts/e2e/parallels-macos-smoke.sh +++ b/scripts/e2e/parallels-macos-smoke.sh @@ -48,7 +48,7 @@ BUILD_LOCK_DIR="${TMPDIR:-/tmp}/openclaw-parallels-build.lock" TIMEOUT_INSTALL_SITE_S=420 TIMEOUT_INSTALL_TGZ_S=420 TIMEOUT_INSTALL_REGISTRY_S=420 -TIMEOUT_UPDATE_DEV_S="${OPENCLAW_PARALLELS_MACOS_UPDATE_DEV_TIMEOUT_S:-600}" +TIMEOUT_UPDATE_DEV_S="${OPENCLAW_PARALLELS_MACOS_UPDATE_DEV_TIMEOUT_S:-1200}" TIMEOUT_VERIFY_S=60 TIMEOUT_ONBOARD_S=180 TIMEOUT_GATEWAY_S=180 @@ -796,6 +796,31 @@ guest_current_user_tail_file() { guest_current_user_exec /usr/bin/tail -n "$lines" "$file_path" } +guest_current_user_kill_process_tree() { + local pid="$1" + [[ "$pid" =~ ^[0-9]+$ ]] || return 0 + guest_current_user_sh "$(cat </dev/null || true); do + kill_tree "\$child" + done + /bin/kill -TERM "\$target" 2>/dev/null || true +} +kill_tree $(shell_quote "$pid") +/bin/sleep 2 +kill_tree_force() { + local target="\$1" child + for child in \$(/usr/bin/pgrep -P "\$target" 2>/dev/null || true); do + kill_tree_force "\$child" + done + /bin/kill -KILL "\$target" 2>/dev/null || true +} +kill_tree_force $(shell_quote "$pid") +EOF +)" >/dev/null 2>&1 || true +} + latest_guest_npm_debug_log_path() { local guest_home="$1" guest_current_user_sh "$(cat < $(shell_quote "$log_path") 2>&1 < /dev/null &) >/dev/null 2>&1" + write_runner_cmd+="(/bin/bash $(shell_quote "$runner_path") > $(shell_quote "$log_path") 2>&1 < /dev/null & printf '%s\n' \"\$!\" > $(shell_quote "$runner_pid_path")) >/dev/null 2>&1" guest_current_user_sh "$write_runner_cmd" guest_home="$(resolve_guest_current_user_home)" guest_log_state_path="$(mktemp "${TMPDIR:-/tmp}/openclaw-guest-log-state.XXXXXX")" @@ -936,7 +962,7 @@ print(matches[-1]) PY )" || rc="" if [[ "$rc" =~ ^-?[0-9]+$ ]]; then - guest_current_user_exec /bin/rm -f "$done_path" "$runner_path" >/dev/null 2>&1 || true + guest_current_user_exec /bin/rm -f "$done_path" "$runner_path" "$runner_pid_path" >/dev/null 2>&1 || true stream_guest_file_delta "$log_path" "$guest_log_state_path" "" if [[ -n "$latest_npm_log_path" ]]; then stream_guest_file_delta "$latest_npm_log_path" "$latest_npm_log_state_path" "npm-debug: " @@ -957,7 +983,7 @@ PY done_rc="$(guest_current_user_exec /bin/cat "$done_path" 2>/dev/null | tr -d '\r\n' || true)" if [[ "$done_rc" =~ ^-?[0-9]+$ ]]; then rc="$done_rc" - guest_current_user_exec /bin/rm -f "$done_path" "$runner_path" >/dev/null 2>&1 || true + guest_current_user_exec /bin/rm -f "$done_path" "$runner_path" "$runner_pid_path" >/dev/null 2>&1 || true stream_guest_file_delta "$log_path" "$guest_log_state_path" "" if [[ -n "$latest_npm_log_path" ]]; then stream_guest_file_delta "$latest_npm_log_path" "$latest_npm_log_state_path" "npm-debug: " @@ -968,7 +994,7 @@ PY fi rc="$(guest_runner_rc_from_log "$log_path" 2>/dev/null || true)" if [[ "$rc" =~ ^-?[0-9]+$ ]]; then - guest_current_user_exec /bin/rm -f "$done_path" "$runner_path" >/dev/null 2>&1 || true + guest_current_user_exec /bin/rm -f "$done_path" "$runner_path" "$runner_pid_path" >/dev/null 2>&1 || true stream_guest_file_delta "$log_path" "$guest_log_state_path" "" if [[ -n "$latest_npm_log_path" ]]; then stream_guest_file_delta "$latest_npm_log_path" "$latest_npm_log_state_path" "npm-debug: " @@ -979,6 +1005,12 @@ PY fi sleep 2 done + runner_pid="$(guest_current_user_exec /bin/cat "$runner_pid_path" 2>/dev/null | tr -d '\r\n' || true)" + if [[ "$runner_pid" =~ ^[0-9]+$ ]]; then + warn "terminating timed-out guest runner pid $runner_pid" + guest_current_user_kill_process_tree "$runner_pid" + fi + guest_current_user_exec /bin/rm -f "$done_path" "$runner_path" "$runner_pid_path" >/dev/null 2>&1 || true rm -f "$guest_log_state_path" "$latest_npm_log_state_path" "$npm_state_path" warn "guest script timed out after ${timeout_s}s" guest_current_user_tail_file "$log_path" 120 >&2 || true diff --git a/src/infra/package-dist-inventory.test.ts b/src/infra/package-dist-inventory.test.ts index 00c811ea812..02eae79c1e2 100644 --- a/src/infra/package-dist-inventory.test.ts +++ b/src/infra/package-dist-inventory.test.ts @@ -18,6 +18,7 @@ describe("package dist inventory", () => { await expect(writePackageDistInventory(packageRoot)).resolves.toEqual([ "dist/current-BR6xv1a1.js", + "dist/extensions/qa-channel/runtime-api.js", ]); await expect(collectPackageDistInventoryErrors(packageRoot)).resolves.toEqual([]); @@ -134,7 +135,9 @@ describe("package dist inventory", () => { ); await fs.writeFile(omittedMap, "{}", "utf8"); - await expect(writePackageDistInventory(packageRoot)).resolves.toEqual([]); + await expect(writePackageDistInventory(packageRoot)).resolves.toEqual([ + "dist/extensions/qa-channel/runtime-api.js", + ]); }); }); it("fails closed when the inventory is missing", async () => { diff --git a/src/infra/package-dist-inventory.ts b/src/infra/package-dist-inventory.ts index 723c11a0e83..479d8a0abc7 100644 --- a/src/infra/package-dist-inventory.ts +++ b/src/infra/package-dist-inventory.ts @@ -1,7 +1,9 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { NPM_UPDATE_COMPAT_SIDECAR_PATHS } from "./npm-update-compat-sidecars.js"; export const PACKAGE_DIST_INVENTORY_RELATIVE_PATH = "dist/postinstall-inventory.json"; +const LEGACY_VERIFIER_COMPAT_INVENTORY_PATHS = ["dist/extensions/qa-channel/runtime-api.js"]; const LEGACY_QA_LAB_DIR = ["qa", "lab"].join("-"); const OMITTED_QA_EXTENSION_PREFIXES = [ "dist/extensions/qa-channel/", @@ -46,6 +48,9 @@ function isPackagedDistPath(relativePath: string): boolean { if (relativePath === "dist/plugin-sdk/.tsbuildinfo") { return false; } + if (LEGACY_VERIFIER_COMPAT_INVENTORY_PATHS.includes(relativePath)) { + return true; + } if ( OMITTED_PRIVATE_QA_PLUGIN_SDK_PREFIXES.some((prefix) => relativePath.startsWith(prefix)) || OMITTED_PRIVATE_QA_PLUGIN_SDK_FILES.has(relativePath) || @@ -106,7 +111,12 @@ export async function collectPackageDistInventory(packageRoot: string): Promise< } export async function writePackageDistInventory(packageRoot: string): Promise { - const inventory = await collectPackageDistInventory(packageRoot); + const inventory = [ + ...new Set([ + ...(await collectPackageDistInventory(packageRoot)), + ...LEGACY_VERIFIER_COMPAT_INVENTORY_PATHS, + ]), + ].toSorted((left, right) => left.localeCompare(right)); const inventoryPath = path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH); await fs.mkdir(path.dirname(inventoryPath), { recursive: true }); await fs.writeFile(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`, "utf8"); @@ -151,6 +161,9 @@ export async function collectPackageDistInventoryErrors(packageRoot: string): Pr for (const relativePath of expectedFiles) { if (!actualSet.has(relativePath)) { + if (NPM_UPDATE_COMPAT_SIDECAR_PATHS.has(relativePath)) { + continue; + } errors.push(`missing packaged dist file ${relativePath}`); } } diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index 8dd409034ad..5c1f479499c 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -239,6 +239,9 @@ async function collectInstalledPathErrors(params: { ? actualSet.has(relativePath) : await pathExists(path.join(params.packageRoot, relativePath)); if (!exists) { + if (NPM_UPDATE_COMPAT_SIDECAR_PATHS.has(relativePath)) { + continue; + } errors.push(params.missingMessage(relativePath)); } }