mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(parallels): stabilize Windows beta smoke transport
This commit is contained in:
parent
30109542d3
commit
0a4d0daa8c
5 changed files with 220 additions and 165 deletions
|
|
@ -16,7 +16,6 @@ export interface WindowsBackgroundPowerShellOptions {
|
|||
beforeLaunchAttempt?: () => void;
|
||||
completedLogDrainGraceMs?: number;
|
||||
label: string;
|
||||
logChunkBytes?: number;
|
||||
onLaunchRetry?: (message: string) => void;
|
||||
pollIntervalMs?: number;
|
||||
runCommand?: typeof run;
|
||||
|
|
@ -59,6 +58,7 @@ function throwIfFailed(label: string, result: CommandResult, check: boolean | un
|
|||
}
|
||||
|
||||
const POSIX_GUEST_SCRIPT_CLEANUP_TIMEOUT_MS = 30_000;
|
||||
const WINDOWS_BACKGROUND_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
function appendCommandResult(phases: PhaseRunner, result: CommandResult): void {
|
||||
phases.append(result.stdout);
|
||||
|
|
@ -88,36 +88,54 @@ export async function runWindowsBackgroundPowerShell(
|
|||
1,
|
||||
Math.floor(options.completedLogDrainGraceMs ?? 30_000),
|
||||
);
|
||||
const logChunkBytes = Math.max(1, Math.floor(options.logChunkBytes ?? 1024 * 1024));
|
||||
const pollIntervalMs = Math.max(1, Math.floor(options.pollIntervalMs ?? 5_000));
|
||||
const runCommand = options.runCommand ?? run;
|
||||
const safeLabel = options.label.replaceAll(/[^A-Za-z0-9_-]/g, "-");
|
||||
const nonce = `${safeLabel}-${randomUUID()}`;
|
||||
const fileBase = `openclaw-parallels-${nonce}`;
|
||||
const logLengthPrefix = `__OPENCLAW_LOG_LENGTH__:${nonce}:`;
|
||||
const logOffsetPrefix = `__OPENCLAW_LOG_OFFSET__:${nonce}:`;
|
||||
const guestRunDir = `openclaw-parallels\\${nonce}`;
|
||||
const windowsDonePath = `%WINDIR%\\Temp\\${guestRunDir}\\done`;
|
||||
const windowsLogPath = `%WINDIR%\\Temp\\${guestRunDir}\\run.log`;
|
||||
const backgroundExitPrefix = `__OPENCLAW_BACKGROUND_EXIT__:${nonce}:`;
|
||||
const backgroundDoneMarker = `__OPENCLAW_BACKGROUND_DONE__:${nonce}`;
|
||||
const pathsScript = `$base = Join-Path $env:TEMP ${psSingleQuote(fileBase)}
|
||||
$scriptPath = "$base.ps1"
|
||||
$logPath = "$base.log"
|
||||
$donePath = "$base.done"
|
||||
$exitPath = "$base.exit"
|
||||
$pidPath = "$base.pid"
|
||||
const deadline = Date.now() + options.timeoutMs;
|
||||
const pathsScript = `$runDir = Join-Path (Join-Path $env:WINDIR 'Temp\\openclaw-parallels') ${psSingleQuote(nonce)}
|
||||
$scriptPath = Join-Path $runDir 'run.ps1'
|
||||
$logPath = Join-Path $runDir 'run.log'
|
||||
$donePath = Join-Path $runDir 'done'
|
||||
$exitPath = Join-Path $runDir 'exit'
|
||||
$pidPath = Join-Path $runDir 'pid'
|
||||
function Write-OpenClawUtf8File([string]$Path, [string]$Value) {
|
||||
[System.IO.File]::WriteAllText($Path, $Value, [System.Text.UTF8Encoding]::new($false))
|
||||
}`;
|
||||
const payload = `$ErrorActionPreference = 'Stop'
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
${pathsScript}
|
||||
Write-OpenClawUtf8File $pidPath ([string]$PID)
|
||||
$script:OpenClawBackgroundLogBytes = 0
|
||||
function Add-OpenClawBackgroundLog {
|
||||
param([Parameter(ValueFromPipeline=$true)]$InputObject)
|
||||
process {
|
||||
$text = $InputObject | Out-String
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($text)
|
||||
$remaining = [int64]${WINDOWS_BACKGROUND_LOG_MAX_BYTES} - $script:OpenClawBackgroundLogBytes
|
||||
if ($remaining -le 0) {
|
||||
return
|
||||
}
|
||||
$count = [int][Math]::Min($remaining, $bytes.Length)
|
||||
$needsBoundaryNewline = $count -eq $remaining -and $count -gt 0 -and $bytes[$count - 1] -ne 10
|
||||
if ($needsBoundaryNewline) {
|
||||
$count--
|
||||
}
|
||||
$stream = [System.IO.File]::Open($logPath, [System.IO.FileMode]::Append, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
$stream.Write($bytes, 0, $bytes.Length)
|
||||
if ($count -gt 0) {
|
||||
$stream.Write($bytes, 0, $count)
|
||||
$script:OpenClawBackgroundLogBytes += $count
|
||||
}
|
||||
if ($needsBoundaryNewline) {
|
||||
$stream.WriteByte(10)
|
||||
$script:OpenClawBackgroundLogBytes++
|
||||
}
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
|
|
@ -134,25 +152,41 @@ ${options.script}
|
|||
} finally {
|
||||
Write-OpenClawUtf8File $donePath 'done'
|
||||
}`;
|
||||
const writeScript = runCommand(
|
||||
"prlctl",
|
||||
[
|
||||
"exec",
|
||||
options.vmName,
|
||||
"--current-user",
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
encodePowerShell(`${pathsScript}
|
||||
const writeArgs = [
|
||||
"exec",
|
||||
options.vmName,
|
||||
"--current-user",
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
encodePowerShell(`${pathsScript}
|
||||
New-Item -ItemType Directory -Path $runDir -Force | Out-Null
|
||||
& icacls.exe $runDir /inheritance:r /grant:r "\${env:USERNAME}:(OI)(CI)(F)" "SYSTEM:(OI)(CI)(F)" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "${safeLabel} background directory ACL setup failed" }
|
||||
Remove-Item -Path $scriptPath, $logPath, $donePath, $exitPath, $pidPath -Force -ErrorAction SilentlyContinue
|
||||
[System.IO.File]::WriteAllText($scriptPath, [Console]::In.ReadToEnd(), [System.Text.UTF8Encoding]::new($false))
|
||||
if (!(Test-Path $scriptPath)) { throw "${safeLabel} background script was not written" }`),
|
||||
],
|
||||
{ check: false, input: payload, timeoutMs: Math.min(options.timeoutMs, 120_000) },
|
||||
);
|
||||
];
|
||||
let writeScript = runCommand("prlctl", writeArgs, {
|
||||
check: false,
|
||||
input: payload,
|
||||
timeoutMs: timeoutBefore(deadline, 120_000),
|
||||
});
|
||||
appendOutput(append, writeScript);
|
||||
if (writeScript.status === 255) {
|
||||
options.onLaunchRetry?.(
|
||||
`${options.label} background script write retry after guest transport rc255`,
|
||||
);
|
||||
options.beforeLaunchAttempt?.();
|
||||
writeScript = runCommand("prlctl", writeArgs, {
|
||||
check: false,
|
||||
input: payload,
|
||||
timeoutMs: timeoutBefore(deadline, 120_000),
|
||||
});
|
||||
appendOutput(append, writeScript);
|
||||
}
|
||||
if (writeScript.status !== 0) {
|
||||
throw new Error(
|
||||
`${options.label} background script write failed with exit code ${writeScript.status}`,
|
||||
|
|
@ -161,7 +195,6 @@ if (!(Test-Path $scriptPath)) { throw "${safeLabel} background script was not wr
|
|||
|
||||
let doneSeen = false;
|
||||
try {
|
||||
const deadline = Date.now() + options.timeoutMs;
|
||||
let launched = false;
|
||||
let lastLaunchStatus = 0;
|
||||
for (let attempt = 1; attempt <= 5 && Date.now() < deadline; attempt++) {
|
||||
|
|
@ -178,11 +211,13 @@ if (!(Test-Path $scriptPath)) { throw "${safeLabel} background script was not wr
|
|||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
encodePowerShell(`${pathsScript}
|
||||
$process = Start-Process -FilePath powershell.exe -WindowStyle Hidden -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $scriptPath) -PassThru
|
||||
Write-OpenClawUtf8File $pidPath ([string]$process.Id)
|
||||
cmd.exe /d /s /c start "" /b powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$scriptPath" | Out-Null
|
||||
'started'`),
|
||||
],
|
||||
{ check: false, quiet: true, timeoutMs: timeoutBefore(deadline, 30_000) },
|
||||
// A busy Windows guest can leave one Parallels Tools session wedged.
|
||||
// Keep polls short so a single transport cancellation cannot consume
|
||||
// the entire install timeout while the detached process continues.
|
||||
{ check: false, quiet: true, timeoutMs: timeoutBefore(deadline, 8_000) },
|
||||
);
|
||||
appendOutput(append, launch);
|
||||
if (launch.status === 0 && launch.stdout.includes("started")) {
|
||||
|
|
@ -221,81 +256,64 @@ Write-OpenClawUtf8File $pidPath ([string]$process.Id)
|
|||
);
|
||||
}
|
||||
|
||||
let lastLogOffset = 0;
|
||||
let completedLogDrainDeadline = 0;
|
||||
const activeDeadline = () => (doneSeen ? completedLogDrainDeadline : deadline);
|
||||
let doneFileSeen = false;
|
||||
const activeDeadline = () => (doneFileSeen ? completedLogDrainDeadline : deadline);
|
||||
while (Date.now() < activeDeadline()) {
|
||||
const doneProbe = runCommand(
|
||||
"prlctl",
|
||||
[
|
||||
"exec",
|
||||
options.vmName,
|
||||
"cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`if exist "${windowsDonePath}" (echo done) else (echo wait)`,
|
||||
],
|
||||
{ check: false, quiet: true, timeoutMs: timeoutBefore(deadline, 5_000) },
|
||||
);
|
||||
appendOutput(append, doneProbe);
|
||||
if (doneProbe.stdout.split(/\r?\n/u).some((line) => line.trim() === "done")) {
|
||||
doneFileSeen = true;
|
||||
completedLogDrainDeadline ||= Date.now() + completedLogDrainGraceMs;
|
||||
} else {
|
||||
await sleep(pollIntervalMs);
|
||||
continue;
|
||||
}
|
||||
|
||||
const poll = runCommand(
|
||||
"prlctl",
|
||||
[
|
||||
"exec",
|
||||
options.vmName,
|
||||
"--current-user",
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
encodePowerShell(`${pathsScript}
|
||||
$offset = ${lastLogOffset}
|
||||
if (Test-Path $logPath) {
|
||||
$stream = [System.IO.File]::Open($logPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
|
||||
try {
|
||||
$length = $stream.Length
|
||||
${psSingleQuote(logLengthPrefix)} + $length
|
||||
if ($length -gt $offset) {
|
||||
[void]$stream.Seek($offset, [System.IO.SeekOrigin]::Begin)
|
||||
$count = [int][Math]::Min($length - $offset, ${logChunkBytes})
|
||||
$buffer = New-Object byte[] $count
|
||||
$read = $stream.Read($buffer, 0, $count)
|
||||
if ($read -gt 0) {
|
||||
$nextOffset = $offset + $read
|
||||
${psSingleQuote(logOffsetPrefix)} + $nextOffset
|
||||
[System.Text.Encoding]::UTF8.GetString($buffer, 0, $read)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
if (Test-Path $donePath) {
|
||||
$backgroundExit = if (Test-Path $exitPath) { (Get-Content -Path $exitPath -Raw).Trim() } else { '0' }
|
||||
${psSingleQuote(backgroundExitPrefix)} + $backgroundExit
|
||||
${psSingleQuote(backgroundDoneMarker)}
|
||||
if ($backgroundExit -ne '0') { exit 23 }
|
||||
exit 0
|
||||
}`),
|
||||
"cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`if exist "${windowsDonePath}" (type "%WINDIR%\\Temp\\${guestRunDir}\\run.log" & for /f "usebackq delims=" %A in ("%WINDIR%\\Temp\\${guestRunDir}\\exit") do @echo ${backgroundExitPrefix}%A & echo ${backgroundDoneMarker}) else (echo wait)`,
|
||||
],
|
||||
{ check: false, quiet: true, timeoutMs: timeoutBefore(deadline, 30_000) },
|
||||
{ check: false, quiet: true, timeoutMs: timeoutBefore(activeDeadline(), 30_000) },
|
||||
);
|
||||
appendOutput(append, poll);
|
||||
const offsetRaw = findControlValue(poll.stdout, logOffsetPrefix);
|
||||
if (offsetRaw) {
|
||||
lastLogOffset = Number(offsetRaw);
|
||||
}
|
||||
const lengthRaw = findControlValue(poll.stdout, logLengthPrefix);
|
||||
const logLength = lengthRaw ? Number(lengthRaw) : lastLogOffset;
|
||||
if (hasControlLine(poll.stdout, backgroundDoneMarker)) {
|
||||
doneSeen = true;
|
||||
completedLogDrainDeadline ||= Date.now() + completedLogDrainGraceMs;
|
||||
if (lastLogOffset < logLength) {
|
||||
await sleep(Math.min(pollIntervalMs, 100));
|
||||
continue;
|
||||
}
|
||||
const backgroundExit = findControlValue(poll.stdout, backgroundExitPrefix) ?? "0";
|
||||
if (backgroundExit !== "0" || (poll.status !== 0 && poll.status !== 124)) {
|
||||
throw new Error(`${options.label} failed`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await sleep(pollIntervalMs);
|
||||
await sleep(Math.min(pollIntervalMs, 100));
|
||||
}
|
||||
if (doneSeen) {
|
||||
throw new Error(`${options.label} completed but log drain timed out`);
|
||||
}
|
||||
throw new Error(`${options.label} timed out`);
|
||||
} finally {
|
||||
cleanupWindowsBackground(options.vmName, pathsScript, runCommand, {
|
||||
cleanupWindowsBackground(options.vmName, pathsScript, windowsLogPath, runCommand, {
|
||||
append,
|
||||
captureLog: !doneSeen,
|
||||
stopProcessTree: !doneSeen,
|
||||
});
|
||||
}
|
||||
|
|
@ -325,14 +343,13 @@ async function waitForWindowsBackgroundMaterialized(params: {
|
|||
[
|
||||
"exec",
|
||||
params.vmName,
|
||||
"--current-user",
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
encodePowerShell(`${params.pathsScript}
|
||||
if ((Test-Path $logPath) -or (Test-Path $donePath)) {
|
||||
if ((Test-Path $pidPath) -or (Test-Path $donePath)) {
|
||||
'materialized'
|
||||
}`),
|
||||
],
|
||||
|
|
@ -350,8 +367,13 @@ if ((Test-Path $logPath) -or (Test-Path $donePath)) {
|
|||
function cleanupWindowsBackground(
|
||||
vmName: string,
|
||||
pathsScript: string,
|
||||
windowsLogPath: string,
|
||||
runCommand: typeof run,
|
||||
options: { stopProcessTree: boolean },
|
||||
options: {
|
||||
append?: (chunk: string | Uint8Array) => void;
|
||||
captureLog: boolean;
|
||||
stopProcessTree: boolean;
|
||||
},
|
||||
): void {
|
||||
const stopProcessTree = options.stopProcessTree
|
||||
? `function Stop-OpenClawBackgroundProcessTree([int]$ProcessId) {
|
||||
|
|
@ -373,15 +395,45 @@ if (Test-Path $pidPath) {
|
|||
[
|
||||
"exec",
|
||||
vmName,
|
||||
"--current-user",
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
encodePowerShell(`${pathsScript}
|
||||
${stopProcessTree}
|
||||
Remove-Item -Path $scriptPath, $logPath, $donePath, $exitPath, $pidPath -Force -ErrorAction SilentlyContinue`),
|
||||
${stopProcessTree}`),
|
||||
],
|
||||
{ check: false, quiet: true, timeoutMs: 30_000 },
|
||||
);
|
||||
if (options.captureLog) {
|
||||
const log = runCommand(
|
||||
"prlctl",
|
||||
[
|
||||
"exec",
|
||||
vmName,
|
||||
"cmd.exe",
|
||||
"/d",
|
||||
"/s",
|
||||
"/c",
|
||||
`if exist "${windowsLogPath}" type "${windowsLogPath}"`,
|
||||
],
|
||||
{ check: false, quiet: true, timeoutMs: 30_000 },
|
||||
);
|
||||
appendOutput(options.append, log);
|
||||
}
|
||||
runCommand(
|
||||
"prlctl",
|
||||
[
|
||||
"exec",
|
||||
vmName,
|
||||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
encodePowerShell(`${pathsScript}
|
||||
Remove-Item -Path $scriptPath, $logPath, $donePath, $exitPath, $pidPath -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path $runDir -Recurse -Force -ErrorAction SilentlyContinue`),
|
||||
],
|
||||
{ check: false, quiet: true, timeoutMs: 30_000 },
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1163,6 +1163,7 @@ export class NpmUpdateSmoke {
|
|||
): Promise<void> {
|
||||
await runWindowsBackgroundPowerShell({
|
||||
append: (chunk) => ctx.append(chunk),
|
||||
beforeLaunchAttempt: () => ensureVmRunning(windowsVm),
|
||||
label: "Windows update",
|
||||
script,
|
||||
timeoutMs,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ interface WindowsSummary {
|
|||
};
|
||||
}
|
||||
|
||||
const WINDOWS_PACKAGE_INSTALL_TIMEOUT_SECONDS = 900;
|
||||
const WINDOWS_PACKAGE_INSTALL_TIMEOUT_MS = WINDOWS_PACKAGE_INSTALL_TIMEOUT_SECONDS * 1000;
|
||||
|
||||
const defaultOptions = (): WindowsOptions => ({
|
||||
hostIp: undefined,
|
||||
hostPort: 18426,
|
||||
|
|
@ -362,7 +365,9 @@ class WindowsSmoke extends SmokeRunController<WindowsOptions> {
|
|||
ensureGuestGit({ guest: this.guest, minGitZipPath: this.minGitZipPath, server: this.server }),
|
||||
);
|
||||
await this.phase("fresh.preflight", 120, () => this.logGuestPreflight(true));
|
||||
await this.phase("fresh.install-main", 420, () => this.installMain("openclaw-main-fresh.tgz"));
|
||||
await this.phase("fresh.install-main", WINDOWS_PACKAGE_INSTALL_TIMEOUT_SECONDS, () =>
|
||||
this.installMain("openclaw-main-fresh.tgz"),
|
||||
);
|
||||
this.status.freshVersion = await this.extractLastVersion("fresh.install-main");
|
||||
await this.phase("fresh.verify-main-version", 120, () => this.verifyTargetVersion());
|
||||
await this.phase("fresh.onboard-ref", 720, () => this.runRefOnboard());
|
||||
|
|
@ -381,8 +386,10 @@ class WindowsSmoke extends SmokeRunController<WindowsOptions> {
|
|||
);
|
||||
await this.phase("upgrade.preflight", 120, () => this.logGuestPreflight(false));
|
||||
if (this.options.targetPackageSpec || this.options.upgradeFromPackedMain) {
|
||||
await this.phase("upgrade.install-baseline-package", 420, () =>
|
||||
this.installMain("openclaw-main-upgrade.tgz"),
|
||||
await this.phase(
|
||||
"upgrade.install-baseline-package",
|
||||
WINDOWS_PACKAGE_INSTALL_TIMEOUT_SECONDS,
|
||||
() => this.installMain("openclaw-main-upgrade.tgz"),
|
||||
);
|
||||
this.status.latestInstalledVersion = await this.extractLastVersion(
|
||||
"upgrade.install-baseline-package",
|
||||
|
|
@ -391,7 +398,9 @@ class WindowsSmoke extends SmokeRunController<WindowsOptions> {
|
|||
this.verifyTargetVersion(),
|
||||
);
|
||||
} else {
|
||||
await this.phase("upgrade.install-baseline", 420, () => this.installLatestRelease());
|
||||
await this.phase("upgrade.install-baseline", WINDOWS_PACKAGE_INSTALL_TIMEOUT_SECONDS, () =>
|
||||
this.installLatestRelease(),
|
||||
);
|
||||
this.status.latestInstalledVersion = await this.extractLastVersion(
|
||||
"upgrade.install-baseline",
|
||||
);
|
||||
|
|
@ -538,33 +547,37 @@ ${cleanScript}`,
|
|||
);
|
||||
}
|
||||
|
||||
private installLatestRelease(): void {
|
||||
private installLatestRelease(): Promise<void> {
|
||||
const versionArg = this.installVersion ? ` -Tag ${psSingleQuote(this.installVersion)}` : "";
|
||||
this.guestPowerShell(
|
||||
return this.guestPowerShellBackground(
|
||||
"install-latest",
|
||||
`$ErrorActionPreference = 'Stop'
|
||||
$script = Invoke-RestMethod -Uri ${psSingleQuote(this.options.installUrl)} -TimeoutSec 120
|
||||
& ([scriptblock]::Create($script))${versionArg} -NoOnboard
|
||||
if ($LASTEXITCODE -ne 0) { throw "installer failed with exit code $LASTEXITCODE" }
|
||||
Invoke-OpenClaw --version
|
||||
if ($LASTEXITCODE -ne 0) { throw "openclaw --version failed with exit code $LASTEXITCODE" }`,
|
||||
{ timeoutMs: 420_000 },
|
||||
if ($LASTEXITCODE -ne 0) { throw "openclaw --version failed with exit code $LASTEXITCODE" }`,
|
||||
this.remainingPhaseTimeoutMs(WINDOWS_PACKAGE_INSTALL_TIMEOUT_MS) ??
|
||||
WINDOWS_PACKAGE_INSTALL_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
private installMain(tempName: string): void {
|
||||
private installMain(tempName: string): Promise<void> {
|
||||
if (!this.artifact || !this.server) {
|
||||
die("package artifact/server missing");
|
||||
}
|
||||
const tgzUrl = this.server.urlFor(this.artifact.path);
|
||||
this.guestPowerShell(
|
||||
return this.guestPowerShellBackground(
|
||||
`install-main-${tempName.replaceAll(/[^A-Za-z0-9_-]/g, "-")}`,
|
||||
`$ErrorActionPreference = 'Stop'
|
||||
$tgz = Join-Path $env:TEMP ${psSingleQuote(tempName)}
|
||||
curl.exe -fsSL --connect-timeout 10 --max-time 120 --retry 2 --retry-delay 2 ${psSingleQuote(tgzUrl)} -o $tgz
|
||||
npm.cmd install -g $tgz --no-fund --no-audit --loglevel=error
|
||||
if ($LASTEXITCODE -ne 0) { throw "npm install failed with exit code $LASTEXITCODE" }
|
||||
Invoke-OpenClaw --version
|
||||
if ($LASTEXITCODE -ne 0) { throw "openclaw --version failed with exit code $LASTEXITCODE" }`,
|
||||
{ timeoutMs: 420_000 },
|
||||
if ($LASTEXITCODE -ne 0) { throw "openclaw --version failed with exit code $LASTEXITCODE" }`,
|
||||
this.remainingPhaseTimeoutMs(WINDOWS_PACKAGE_INSTALL_TIMEOUT_MS) ??
|
||||
WINDOWS_PACKAGE_INSTALL_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -622,11 +635,14 @@ ${this.windowsPluginIsolationScript()}`,
|
|||
await runWindowsBackgroundPowerShell({
|
||||
append: (chunk) =>
|
||||
this.log(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")),
|
||||
beforeLaunchAttempt: () => this.waitForGuestReady(120),
|
||||
beforeLaunchAttempt: () => {
|
||||
ensureVmRunning(this.options.vmName, 120);
|
||||
this.waitForGuestReady(120);
|
||||
},
|
||||
label,
|
||||
onLaunchRetry: warn,
|
||||
script: `${windowsOpenClawResolver}\n${script}`,
|
||||
timeoutMs,
|
||||
timeoutMs: this.remainingPhaseTimeoutMs(timeoutMs) ?? timeoutMs,
|
||||
vmName: this.options.vmName,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,8 +82,6 @@ function decodePowerShellFromArgs(args: string[]): string {
|
|||
function extractWindowsBackgroundControlMarkers(decoded: string): {
|
||||
done: string;
|
||||
exitPrefix: string;
|
||||
lengthPrefix: string;
|
||||
offsetPrefix: string;
|
||||
} {
|
||||
const marker = (name: string, trailingColon: boolean): string => {
|
||||
const suffix = trailingColon ? ":" : "";
|
||||
|
|
@ -96,8 +94,6 @@ function extractWindowsBackgroundControlMarkers(decoded: string): {
|
|||
return {
|
||||
done: marker("__OPENCLAW_BACKGROUND_DONE__", false),
|
||||
exitPrefix: marker("__OPENCLAW_BACKGROUND_EXIT__", true),
|
||||
lengthPrefix: marker("__OPENCLAW_LOG_LENGTH__", true),
|
||||
offsetPrefix: marker("__OPENCLAW_LOG_OFFSET__", true),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -620,7 +616,7 @@ exit 1
|
|||
expect(transports).toContain("${options.label} timed out");
|
||||
});
|
||||
|
||||
it("cleans timed-out Windows background work and reads bounded log chunks", async () => {
|
||||
it("cleans timed-out Windows background work", async () => {
|
||||
const decodedCommands: string[] = [];
|
||||
const inputs: string[] = [];
|
||||
const fakeRun: typeof hostCommandRun = (_command, args, options) => {
|
||||
|
|
@ -629,16 +625,18 @@ exit 1
|
|||
if (options?.input) {
|
||||
inputs.push(String(options.input));
|
||||
}
|
||||
if (decoded.includes("Start-Process")) {
|
||||
if (decoded.includes('cmd.exe /d /s /c start "" /b powershell.exe')) {
|
||||
return { status: 0, stderr: "", stdout: "started\n" };
|
||||
}
|
||||
if (args.includes("cmd.exe")) {
|
||||
return { status: 0, stderr: "", stdout: "wait\n" };
|
||||
}
|
||||
return { status: 0, stderr: "", stdout: "" };
|
||||
};
|
||||
|
||||
await expect(
|
||||
runWindowsBackgroundPowerShell({
|
||||
label: "windows background timeout",
|
||||
logChunkBytes: 64,
|
||||
pollIntervalMs: 1,
|
||||
runCommand: fakeRun,
|
||||
script: "Start-Sleep -Seconds 60",
|
||||
|
|
@ -654,11 +652,9 @@ exit 1
|
|||
expect(commands).toContain("[System.Text.UTF8Encoding]::new($false)");
|
||||
expect(payloads).toContain("Write-OpenClawUtf8File $exitPath '0'");
|
||||
expect(payloads).toContain("Write-OpenClawUtf8File $donePath 'done'");
|
||||
expect(commands).toContain("Write-OpenClawUtf8File $pidPath ([string]$process.Id)");
|
||||
expect(commands).toContain("Start-Process -FilePath powershell.exe");
|
||||
expect(commands).toContain("-PassThru");
|
||||
expect(commands).toContain("[System.IO.File]::Open($logPath");
|
||||
expect(commands).toContain("[Math]::Min($length - $offset, 64)");
|
||||
expect(payloads).toContain("Write-OpenClawUtf8File $pidPath ([string]$PID)");
|
||||
expect(commands).toContain('cmd.exe /d /s /c start "" /b powershell.exe');
|
||||
expect(commands).toContain("icacls.exe $runDir /inheritance:r");
|
||||
expect(commands).toContain("Stop-OpenClawBackgroundProcessTree ([int]$backgroundPid)");
|
||||
expect(commands).toContain(
|
||||
'Get-CimInstance Win32_Process -Filter "ParentProcessId=$ProcessId"',
|
||||
|
|
@ -677,22 +673,11 @@ exit 1
|
|||
const fakeRun: typeof hostCommandRun = (_command, args) => {
|
||||
const decoded = decodePowerShellFromArgs(args);
|
||||
decodedCommands.push(decoded);
|
||||
if (decoded.includes("Start-Process")) {
|
||||
if (decoded.includes('cmd.exe /d /s /c start "" /b powershell.exe')) {
|
||||
return { status: 0, stderr: "", stdout: "started\n" };
|
||||
}
|
||||
if (decoded.includes("__OPENCLAW_LOG_LENGTH__")) {
|
||||
const markers = extractWindowsBackgroundControlMarkers(decoded);
|
||||
return {
|
||||
status: 0,
|
||||
stderr: "",
|
||||
stdout: [
|
||||
`${markers.lengthPrefix}128`,
|
||||
`${markers.offsetPrefix}128`,
|
||||
"__OPENCLAW_BACKGROUND_EXIT__:0",
|
||||
"__OPENCLAW_BACKGROUND_DONE__",
|
||||
"",
|
||||
].join("\n"),
|
||||
};
|
||||
if (args.includes("cmd.exe")) {
|
||||
return { status: 0, stderr: "", stdout: "done\n" };
|
||||
}
|
||||
return { status: 0, stderr: "", stdout: "" };
|
||||
};
|
||||
|
|
@ -700,11 +685,11 @@ exit 1
|
|||
await expect(
|
||||
runWindowsBackgroundPowerShell({
|
||||
label: "windows background marker smuggle",
|
||||
logChunkBytes: 128,
|
||||
pollIntervalMs: 1,
|
||||
runCommand: fakeRun,
|
||||
script: "Write-Output done",
|
||||
timeoutMs: 5,
|
||||
completedLogDrainGraceMs: 5,
|
||||
vmName: "Windows Test",
|
||||
}),
|
||||
).rejects.toThrow("windows background marker smuggle timed out");
|
||||
|
|
@ -721,34 +706,24 @@ exit 1
|
|||
const fakeRun: typeof hostCommandRun = (_command, args) => {
|
||||
const decoded = decodePowerShellFromArgs(args);
|
||||
decodedCommands.push(decoded);
|
||||
if (decoded.includes("Start-Process")) {
|
||||
if (decoded.includes('cmd.exe /d /s /c start "" /b powershell.exe')) {
|
||||
return { status: 0, stderr: "", stdout: "started\n" };
|
||||
}
|
||||
if (decoded.includes("__OPENCLAW_LOG_LENGTH__")) {
|
||||
const markers = extractWindowsBackgroundControlMarkers(decoded);
|
||||
pollCount += 1;
|
||||
return {
|
||||
status: 0,
|
||||
stderr: "",
|
||||
stdout:
|
||||
pollCount === 1
|
||||
? [
|
||||
`${markers.lengthPrefix}128`,
|
||||
`${markers.offsetPrefix}64`,
|
||||
"first chunk",
|
||||
`${markers.exitPrefix}0`,
|
||||
markers.done,
|
||||
"",
|
||||
].join("\n")
|
||||
: [
|
||||
`${markers.lengthPrefix}128`,
|
||||
`${markers.offsetPrefix}128`,
|
||||
"second chunk",
|
||||
`${markers.exitPrefix}0`,
|
||||
markers.done,
|
||||
"",
|
||||
].join("\n"),
|
||||
};
|
||||
if (args.includes("cmd.exe")) {
|
||||
const command = args.at(-1) ?? "";
|
||||
if (command.includes("type")) {
|
||||
pollCount += 1;
|
||||
const markers = extractWindowsBackgroundControlMarkers(command);
|
||||
return {
|
||||
status: 0,
|
||||
stderr: "",
|
||||
stdout: ["first chunk", `${markers.exitPrefix}0`, markers.done, ""].join("\n"),
|
||||
};
|
||||
}
|
||||
if (command.includes("if exist")) {
|
||||
return { status: 0, stderr: "", stdout: "done\n" };
|
||||
}
|
||||
return { status: 0, stderr: "", stdout: "" };
|
||||
}
|
||||
return { status: 0, stderr: "", stdout: "" };
|
||||
};
|
||||
|
|
@ -758,7 +733,6 @@ exit 1
|
|||
append: (chunk) => output.push(String(chunk)),
|
||||
completedLogDrainGraceMs: 1000,
|
||||
label: "windows background drain",
|
||||
logChunkBytes: 64,
|
||||
pollIntervalMs: 5000,
|
||||
runCommand: fakeRun,
|
||||
script: "Write-Output done",
|
||||
|
|
@ -767,9 +741,8 @@ exit 1
|
|||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(pollCount).toBe(2);
|
||||
expect(pollCount).toBe(1);
|
||||
expect(output.join("")).toContain("first chunk");
|
||||
expect(output.join("")).toContain("second chunk");
|
||||
expect(decodedCommands.join("\n")).not.toContain("Stop-OpenClawBackgroundProcessTree");
|
||||
expect(decodedCommands.join("\n")).toContain(
|
||||
"Remove-Item -Path $scriptPath, $logPath, $donePath, $exitPath, $pidPath",
|
||||
|
|
|
|||
|
|
@ -424,7 +424,9 @@ describe("Parallels smoke model selection", () => {
|
|||
expect(script).not.toContain("'openclaw-parallels-plugin-isolation.cjs'");
|
||||
expect(script).toContain("try {");
|
||||
expect(script).toContain("} finally {");
|
||||
expect(script).toContain("Remove-Item $isolationScriptPath -Force -ErrorAction SilentlyContinue");
|
||||
expect(script).toContain(
|
||||
"Remove-Item $isolationScriptPath -Force -ErrorAction SilentlyContinue",
|
||||
);
|
||||
expect(script).toContain("Remove-Item Env:OPENCLAW_PARALLELS_PLUGIN_ISOLATION");
|
||||
});
|
||||
|
||||
|
|
@ -1463,16 +1465,29 @@ if (isPrlctl) {
|
|||
|
||||
expect(script).toContain("guestPowerShellBackground");
|
||||
expect(script).toContain("runWindowsBackgroundPowerShell");
|
||||
expect(transports).toContain("Join-Path $env:TEMP");
|
||||
expect(transports).toContain("Join-Path (Join-Path $env:WINDIR 'Temp\\\\openclaw-parallels')");
|
||||
expect(transports).toContain("icacls.exe $runDir /inheritance:r");
|
||||
expect(transports).toContain("__OPENCLAW_BACKGROUND_DONE__");
|
||||
expect(transports).toContain("__OPENCLAW_BACKGROUND_EXIT__");
|
||||
expect(transports).toContain("__OPENCLAW_LOG_OFFSET__");
|
||||
expect(transports).toContain("poll.status !== 0 && poll.status !== 124");
|
||||
expect(transports).toContain("Start-Process -FilePath powershell.exe");
|
||||
expect(transports).toContain('cmd.exe /d /s /c start "" /b powershell.exe');
|
||||
expect(transports).toContain('if exist "${windowsDonePath}"');
|
||||
expect(transports).toContain('type "%WINDIR%\\\\Temp\\\\${guestRunDir}\\\\run.log"');
|
||||
expect(transports).toContain("WINDOWS_BACKGROUND_LOG_MAX_BYTES");
|
||||
expect(transports).toContain("Write-OpenClawUtf8File $pidPath ([string]$PID)");
|
||||
expect(transports).toContain('launch.stdout.includes("started")');
|
||||
expect(transports).toContain("waitForWindowsBackgroundMaterialized");
|
||||
});
|
||||
|
||||
it("runs Windows package installs through the detached done-file runner", () => {
|
||||
const script = readFileSync(TS_PATHS.windows, "utf8");
|
||||
|
||||
expect(script).toContain('guestPowerShellBackground(\n "install-latest"');
|
||||
expect(script).toContain("guestPowerShellBackground(\n `install-main-${");
|
||||
expect(script).not.toMatch(/private installMain\(tempName: string\): void/u);
|
||||
expect(script).not.toMatch(/private installLatestRelease\(\): void/u);
|
||||
});
|
||||
|
||||
it("paces ambiguous Windows background launch materialization probes", async () => {
|
||||
let calls = 0;
|
||||
const runCommand = vi.fn(() => {
|
||||
|
|
@ -2096,9 +2111,7 @@ setInterval(() => {}, 1000);
|
|||
);
|
||||
|
||||
expect(duplicateNpmUpdatePlatformResult.status).toBe(1);
|
||||
expect(duplicateNpmUpdatePlatformResult.stderr).toContain(
|
||||
"duplicate --platform entry: macos",
|
||||
);
|
||||
expect(duplicateNpmUpdatePlatformResult.stderr).toContain("duplicate --platform entry: macos");
|
||||
|
||||
expect(readFileSync(TS_PATHS.macos, "utf8")).toContain(
|
||||
'this.updateDevTimeoutSeconds = readPositiveIntEnv(\n "OPENCLAW_PARALLELS_MACOS_UPDATE_DEV_TIMEOUT_S"',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue