From fd353ee5d772385d107cbaaef9e7be683c62ef32 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 13 Aug 2026 13:14:22 +0800 Subject: [PATCH] fix(parallels): recover migration-refused gateway startup --- scripts/e2e/parallels/npm-update-scripts.ts | 126 ++++++++++++++---- .../parallels-npm-update-smoke.test.ts | 43 +++++- 2 files changed, 137 insertions(+), 32 deletions(-) diff --git a/scripts/e2e/parallels/npm-update-scripts.ts b/scripts/e2e/parallels/npm-update-scripts.ts index e5e0ba9aad01..9f0f9427b282 100644 --- a/scripts/e2e/parallels/npm-update-scripts.ts +++ b/scripts/e2e/parallels/npm-update-scripts.ts @@ -26,6 +26,8 @@ interface NpmUpdateScriptInput { } const windowsStalePostSwapImportRegex = String.raw`node_modules\\openclaw\\dist\\[^\\]+-[A-Za-z0-9_-]+\.js`; +const startupMigrationRestartPrefix = + "OpenClaw plugin migration inputs changed during startup convergence;"; const macosGuestPath = "/opt/homebrew/bin:/opt/homebrew/opt/node/bin:/usr/local/bin:/usr/local/sbin:/opt/homebrew/sbin:/usr/bin:/bin:/usr/sbin:/sbin"; const macosOpenClawCommand = '"$OPENCLAW_BIN"'; @@ -155,25 +157,65 @@ $updateExit = $script:OpenClawUpdateExit $updateOutput`; } -function windowsGatewayReadyScript(): string { - return `function Wait-OpenClawGateway { +function windowsGatewayReadyScript(input: NpmUpdateScriptInput): string { + return `$gatewayLogRoot = Join-Path ([System.IO.Path]::GetTempPath()) 'openclaw-parallels-windows-gateway' +$gatewayLaunch = 0 +$gatewayRestartCount = 0 +function Start-OpenClawGateway { + $script:gatewayLaunch += 1 + $script:gatewayLogPath = "$gatewayLogRoot-$($script:gatewayLaunch).log" + Remove-Item $script:gatewayLogPath -Force -ErrorAction SilentlyContinue + $gatewayCommand = Resolve-OpenClawCommand + $gatewayCommandPath = $gatewayCommand.Path.Replace("'", "''") + $gatewayInvocation = if ($gatewayCommand.Kind -eq 'node') { + "& node.exe '$gatewayCommandPath' gateway run --bind loopback --port 18789 --force" + } else { + "& '$gatewayCommandPath' gateway run --bind loopback --port 18789 --force" + } + $gatewayScript = "\`$ErrorActionPreference = 'Continue'\`n$gatewayInvocation *>> \`$env:OPENCLAW_PARALLELS_GATEWAY_LOG\`nexit \`$LASTEXITCODE" + $gatewayEncodedScript = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($gatewayScript)) + $gatewayPowerShell = (Get-Process -Id $PID).Path + Invoke-WithScopedEnv @{ + OPENCLAW_HOME = $env:USERPROFILE + OPENCLAW_STATE_DIR = (Join-Path $env:USERPROFILE '.openclaw') + OPENCLAW_CONFIG_PATH = (Join-Path $env:USERPROFILE '.openclaw\\openclaw.json') + OPENCLAW_PARALLELS_GATEWAY_LOG = $script:gatewayLogPath + ${input.auth.apiKeyEnv} = ${psSingleQuote(input.auth.apiKeyValue)} + } { + $script:gatewayProcess = Start-Process -FilePath $gatewayPowerShell -ArgumentList @('-NoProfile', '-NonInteractive', '-EncodedCommand', $gatewayEncodedScript) -WindowStyle Hidden -PassThru + } +} +function Write-CurrentGatewayLog { + if (Test-Path $script:gatewayLogPath) { + Get-Content $script:gatewayLogPath -ErrorAction SilentlyContinue | Out-Host + } +} +function Test-CurrentGatewayStartupMigrationRefusal { + if (-not (Test-Path $script:gatewayLogPath)) { return $false } + return Select-String -Path $script:gatewayLogPath -SimpleMatch ${psSingleQuote(startupMigrationRestartPrefix)} -Quiet +} +function Wait-OpenClawGateway { $deadline = (Get-Date).AddSeconds(180) - $attempt = 0 while ((Get-Date) -lt $deadline) { Invoke-OpenClaw gateway status --deep --require-rpc --timeout 15000 if ($LASTEXITCODE -eq 0) { return } - $attempt += 1 - if ($attempt -eq 4) { - Invoke-OpenClaw gateway start *>&1 | Out-Host + if ($script:gatewayProcess.HasExited) { + $script:gatewayProcess.WaitForExit() + if ($script:gatewayRestartCount -eq 0 -and (Test-CurrentGatewayStartupMigrationRefusal)) { + $script:gatewayRestartCount = 1 + Write-Host 'gateway exited after startup migration convergence refusal; restarting once' + Start-OpenClawGateway + continue + } + Write-CurrentGatewayLog + throw "gateway exited before becoming ready after update with code $($script:gatewayProcess.ExitCode)" } Start-Sleep -Seconds 5 } + Write-CurrentGatewayLog throw "gateway did not become ready after update" } -Invoke-OpenClaw gateway restart *>&1 | Out-Host -if ($LASTEXITCODE -ne 0) { - "gateway restart exited with code $LASTEXITCODE; probing readiness before failing" | Out-Host -} +Start-OpenClawGateway Wait-OpenClawGateway`; } @@ -259,27 +301,44 @@ stop_openclaw_gateway_processes() { fi fi } +gateway_log=/tmp/openclaw-parallels-macos-gateway.log +rm -f "$gateway_log" +touch "$gateway_log" +gateway_pid= +gateway_launch_log_offset=0 +gateway_restart_count=0 start_openclaw_gateway() { stop_openclaw_gateway_processes - rm -f /tmp/openclaw-parallels-macos-gateway.log + gateway_launch_log_offset="$(wc -c <"$gateway_log" 2>/dev/null | tr -d '[:space:]' || echo 0)" trap '' HUP - with_provider_api_key /usr/bin/env OPENCLAW_HOME="$HOME" OPENCLAW_STATE_DIR="$HOME/.openclaw" OPENCLAW_CONFIG_PATH="$HOME/.openclaw/openclaw.json" "$OPENCLAW_BIN" gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-macos-gateway.log 2>&1 >"$gateway_log" 2>&1 /dev/null; then + if wait "$gateway_pid"; then gateway_exit_status=0; else gateway_exit_status=$?; fi + if [ "$gateway_exit_status" -le 128 ] && [ "$gateway_restart_count" -eq 0 ]; then + if tail -c +"$((gateway_launch_log_offset + 1))" "$gateway_log" 2>/dev/null | grep -F -- ${shellQuote(startupMigrationRestartPrefix)} >/dev/null; then + gateway_restart_count=1 + echo "gateway exited after startup migration convergence refusal; restarting once" + start_openclaw_gateway + continue + fi + fi + print_log_tail "$gateway_log" >&2 + echo "gateway exited before becoming ready after update (exit $gateway_exit_status)" >&2 + if [ "$gateway_exit_status" -eq 0 ]; then exit 1; fi + exit "$gateway_exit_status" fi sleep 2 done - print_log_tail /tmp/openclaw-parallels-macos-gateway.log >&2 + print_log_tail "$gateway_log" >&2 echo "gateway did not become ready after update" >&2 exit 1 } @@ -368,7 +427,7 @@ if ($updateExit -ne 0) { Write-Host "openclaw update returned a stale post-swap module import; continuing to post-update health checks" } ${windowsVersionCheck(input.expectedNeedle)} -${windowsGatewayReadyScript()} +${windowsGatewayReadyScript(input)} ${windowsAssertAgentOkScript(input)}`; } @@ -403,27 +462,44 @@ stop_openclaw_gateway_processes() { OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 OPENCLAW_ALLOW_ROOT=1 openclaw gateway stop || true pkill -f 'openclaw.*gateway' >/dev/null 2>&1 || true } +gateway_log=/tmp/openclaw-parallels-linux-gateway.log +rm -f "$gateway_log" +touch "$gateway_log" +gateway_pid= +gateway_launch_log_offset=0 +gateway_restart_count=0 start_openclaw_gateway() { pkill -f "openclaw gateway run" >/dev/null 2>&1 || true - rm -f /tmp/openclaw-parallels-linux-gateway.log + gateway_launch_log_offset="$(wc -c <"$gateway_log" 2>/dev/null | tr -d '[:space:]' || echo 0)" with_provider_api_key setsid sh -lc ${shellQuote( - "exec env OPENCLAW_HOME=/root OPENCLAW_STATE_DIR=/root/.openclaw OPENCLAW_CONFIG_PATH=/root/.openclaw/openclaw.json OPENCLAW_DISABLE_BONJOUR=1 OPENCLAW_ALLOW_ROOT=1 openclaw gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-linux-gateway.log 2>&1", + "exec env OPENCLAW_HOME=/root OPENCLAW_STATE_DIR=/root/.openclaw OPENCLAW_CONFIG_PATH=/root/.openclaw/openclaw.json OPENCLAW_DISABLE_BONJOUR=1 OPENCLAW_ALLOW_ROOT=1 openclaw gateway run --bind loopback --port 18789 --force >>/tmp/openclaw-parallels-linux-gateway.log 2>&1", )} >/dev/null 2>&1 < /dev/null & + gateway_pid=$! } wait_for_gateway() { deadline=$((SECONDS + 240)) - attempt=0 while [ "$SECONDS" -lt "$deadline" ]; do if openclaw gateway status --deep --require-rpc --timeout 15000; then return fi - attempt=$((attempt + 1)) - if [ "$attempt" -eq 4 ]; then - start_openclaw_gateway + if ! kill -0 "$gateway_pid" 2>/dev/null; then + if wait "$gateway_pid"; then gateway_exit_status=0; else gateway_exit_status=$?; fi + if [ "$gateway_exit_status" -le 128 ] && [ "$gateway_restart_count" -eq 0 ]; then + if tail -c +"$((gateway_launch_log_offset + 1))" "$gateway_log" 2>/dev/null | grep -F -- ${shellQuote(startupMigrationRestartPrefix)} >/dev/null; then + gateway_restart_count=1 + echo "gateway exited after startup migration convergence refusal; restarting once" + start_openclaw_gateway + continue + fi + fi + print_log_tail "$gateway_log" >&2 + echo "gateway exited before becoming ready after update (exit $gateway_exit_status)" >&2 + if [ "$gateway_exit_status" -eq 0 ]; then exit 1; fi + exit "$gateway_exit_status" fi sleep 2 done - print_log_tail /tmp/openclaw-parallels-linux-gateway.log >&2 + print_log_tail "$gateway_log" >&2 echo "gateway did not become ready after update" >&2 exit 1 } diff --git a/test/scripts/parallels-npm-update-smoke.test.ts b/test/scripts/parallels-npm-update-smoke.test.ts index f08549a7bd53..f083a07120fb 100644 --- a/test/scripts/parallels-npm-update-smoke.test.ts +++ b/test/scripts/parallels-npm-update-smoke.test.ts @@ -371,7 +371,7 @@ exit 1 expect(windowsUpdateScript(input)).toContain(`NPM_CONFIG_REGISTRY = '${registry}'`); }); - it("relaunches POSIX gateways after a transient post-update startup failure", () => { + it("restarts POSIX gateways only after an exact current-launch migration refusal", () => { const input = { auth: TEST_AUTH, expectedNeedle: "2026.7.2-beta.5", @@ -379,11 +379,41 @@ exit 1 }; for (const script of [macosUpdateScript(input), linuxUpdateScript(input)]) { - expect(script).toContain("attempt=$((attempt + 1))"); - expect(script).toContain('if [ "$attempt" -eq 4 ]; then\n start_openclaw_gateway'); + expect(script).toContain( + "OpenClaw plugin migration inputs changed during startup convergence;", + ); + expect(script).toContain("gateway_launch_log_offset="); + expect(script).toContain("gateway_pid=$!"); + expect(script).toContain('if ! kill -0 "$gateway_pid" 2>/dev/null; then'); + expect(script).toContain('tail -c +"$((gateway_launch_log_offset + 1))" "$gateway_log"'); + expect(script).toContain( + 'if [ "$gateway_exit_status" -le 128 ] && [ "$gateway_restart_count" -eq 0 ]; then', + ); + expect(script).toContain("gateway_restart_count=1"); + expect(script).not.toContain('if [ "$attempt" -eq 4 ]'); } }); + it("restarts the Windows gateway only after its current launch exits with the exact refusal", () => { + const script = windowsUpdateScript({ + auth: TEST_AUTH, + expectedNeedle: "2026.7.2-beta.5", + updateTarget: "2026.7.2-beta.5", + }); + + expect(script).toContain( + "OpenClaw plugin migration inputs changed during startup convergence;", + ); + expect(script).toContain("$script:gatewayProcess.HasExited"); + expect(script).toContain("$script:gatewayProcess.WaitForExit()"); + expect(script).toContain("$script:gatewayRestartCount -eq 0"); + expect(script).toContain("Test-CurrentGatewayStartupMigrationRefusal"); + expect(script).toContain("Select-String -Path $script:gatewayLogPath -SimpleMatch"); + expect(script).toContain("$script:gatewayRestartCount = 1"); + expect(script).not.toContain("$attempt -eq 4"); + expect(script).not.toContain("Invoke-OpenClaw gateway restart"); + }); + it("keeps POSIX provider secrets out of executable command lines", () => { const input = { auth: TEST_AUTH, @@ -515,8 +545,7 @@ exit 1 expect(scripts).toContain("print_log_tail()"); expect(scripts).toContain("OPENCLAW_PARALLELS_NPM_UPDATE_LOG_TAIL_BYTES"); expect(scripts).toContain('print_log_tail "$output_file"'); - expect(scripts).toContain("print_log_tail /tmp/openclaw-parallels-macos-gateway.log >&2"); - expect(scripts).toContain("print_log_tail /tmp/openclaw-parallels-linux-gateway.log >&2"); + expect(scripts).toContain('print_log_tail "$gateway_log" >&2'); expect(scripts).not.toContain('cat "$output_file"'); expect(scripts).not.toContain("cat /tmp/openclaw-parallels-"); }); @@ -1222,14 +1251,14 @@ exit 7 "Invoke-WithScopedEnv @{ OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS", ); const versionIndex = script.indexOf("Invoke-OpenClaw --version", scopedIndex); - const restartIndex = script.indexOf("Invoke-OpenClaw gateway restart"); + const startIndex = script.indexOf("\nStart-OpenClawGateway\n", updateIndex); const agentIndex = script.indexOf("Invoke-OpenClaw agent --local"); expect(updateIndex).toBeGreaterThanOrEqual(0); expect(scopedIndex).toBeGreaterThanOrEqual(0); expect(updateIndex).toBeGreaterThan(scopedIndex); expect(versionIndex).toBeGreaterThan(updateIndex); - expect(restartIndex).toBeGreaterThan(updateIndex); + expect(startIndex).toBeGreaterThan(updateIndex); expect(agentIndex).toBeGreaterThan(updateIndex); expect(script).not.toContain("OPENCLAW_DISABLE_BUNDLED_PLUGINS"); });