fix(parallels): recover migration-refused gateway startup

This commit is contained in:
Vincent Koc
2026-08-13 13:14:22 +08:00
committed by GitHub
parent c6530738ee
commit fd353ee5d7
2 changed files with 137 additions and 32 deletions
+101 -25
View File
@@ -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 </dev/null &
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 >>"$gateway_log" 2>&1 </dev/null &
gateway_pid=$!
sleep 1
}
wait_for_gateway() {
deadline=$((SECONDS + 240))
attempt=0
while [ "$SECONDS" -lt "$deadline" ]; do
if "$OPENCLAW_BIN" 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-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
}
@@ -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");
});