




























@@ -1,6 +1,6 @@
11import { run } from "./host-command.ts";
22import type { PhaseRunner } from "./phase-runner.ts";
3-import { encodePowerShell } from "./powershell.ts";
3+import { encodePowerShell, psSingleQuote } from "./powershell.ts";
44import type { CommandResult } from "./types.ts";
5566export interface GuestExecOptions {
@@ -9,6 +9,253 @@ export interface GuestExecOptions {
99timeoutMs?: number;
1010}
111112+export interface WindowsBackgroundPowerShellOptions {
13+append?: (chunk: string | Uint8Array) => void;
14+beforeLaunchAttempt?: () => void;
15+label: string;
16+onLaunchRetry?: (message: string) => void;
17+script: string;
18+timeoutMs: number;
19+vmName: string;
20+}
21+22+function appendOutput(
23+append: ((chunk: string | Uint8Array) => void) | undefined,
24+result: CommandResult,
25+): void {
26+if (result.stdout) {
27+append?.(result.stdout);
28+}
29+if (result.stderr) {
30+append?.(result.stderr);
31+}
32+}
33+34+function timeoutBefore(deadline: number, fallbackMs: number): number {
35+return Math.min(fallbackMs, Math.max(1_000, deadline - Date.now()));
36+}
37+38+function sleep(ms: number): Promise<void> {
39+return new Promise((resolve) => setTimeout(resolve, ms));
40+}
41+42+function throwIfFailed(label: string, result: CommandResult, check: boolean | undefined): void {
43+if (check === false || result.status === 0) {
44+return;
45+}
46+throw new Error(`${label} failed with exit code ${result.status}`);
47+}
48+49+export async function runWindowsBackgroundPowerShell(
50+options: WindowsBackgroundPowerShellOptions,
51+): Promise<void> {
52+const append = options.append;
53+const safeLabel = options.label.replaceAll(/[^A-Za-z0-9_-]/g, "-");
54+const nonce = `${safeLabel}-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
55+const fileBase = `openclaw-parallels-${nonce}`;
56+const pathsScript = `$base = Join-Path $env:TEMP ${psSingleQuote(fileBase)}
57+$scriptPath = "$base.ps1"
58+$logPath = "$base.log"
59+$donePath = "$base.done"
60+$exitPath = "$base.exit"`;
61+const payload = `$ErrorActionPreference = 'Stop'
62+$PSNativeCommandUseErrorActionPreference = $false
63+${pathsScript}
64+try {
65+ & {
66+${options.script}
67+ } *>&1 | ForEach-Object { $_ | Out-String | Add-Content -Path $logPath -Encoding UTF8 }
68+ Set-Content -Path $exitPath -Value '0' -Encoding UTF8
69+} catch {
70+ $_ | Out-String | Add-Content -Path $logPath -Encoding UTF8
71+ Set-Content -Path $exitPath -Value '1' -Encoding UTF8
72+} finally {
73+ Set-Content -Path $donePath -Value 'done' -Encoding UTF8
74+}`;
75+const writeScript = run(
76+"prlctl",
77+[
78+"exec",
79+options.vmName,
80+"--current-user",
81+"powershell.exe",
82+"-NoProfile",
83+"-ExecutionPolicy",
84+"Bypass",
85+"-EncodedCommand",
86+encodePowerShell(`${pathsScript}
87+Remove-Item -Path $scriptPath, $logPath, $donePath, $exitPath -Force -ErrorAction SilentlyContinue
88+[System.IO.File]::WriteAllText($scriptPath, [Console]::In.ReadToEnd(), [System.Text.UTF8Encoding]::new($false))
89+if (!(Test-Path $scriptPath)) { throw "${safeLabel} background script was not written" }`),
90+],
91+{ check: false, input: payload, timeoutMs: Math.min(options.timeoutMs, 120_000) },
92+);
93+appendOutput(append, writeScript);
94+if (writeScript.status !== 0) {
95+throw new Error(
96+`${options.label} background script write failed with exit code ${writeScript.status}`,
97+);
98+}
99+100+const deadline = Date.now() + options.timeoutMs;
101+let launched = false;
102+let lastLaunchStatus = 0;
103+for (let attempt = 1; attempt <= 5 && Date.now() < deadline; attempt++) {
104+options.beforeLaunchAttempt?.();
105+const launch = run(
106+"prlctl",
107+[
108+"exec",
109+options.vmName,
110+"--current-user",
111+"powershell.exe",
112+"-NoProfile",
113+"-ExecutionPolicy",
114+"Bypass",
115+"-EncodedCommand",
116+encodePowerShell(`${pathsScript}
117+Start-Process -FilePath powershell.exe -WindowStyle Hidden -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $scriptPath)
118+'started'`),
119+],
120+{ check: false, quiet: true, timeoutMs: timeoutBefore(deadline, 30_000) },
121+);
122+appendOutput(append, launch);
123+if (launch.status === 0 && launch.stdout.includes("started")) {
124+launched = true;
125+break;
126+}
127+lastLaunchStatus = launch.status;
128+if (launch.status === 0 || launch.status === 124) {
129+const materialized = waitForWindowsBackgroundMaterialized({
130+ append,
131+ deadline,
132+ pathsScript,
133+vmName: options.vmName,
134+});
135+if (materialized) {
136+launched = true;
137+break;
138+}
139+options.onLaunchRetry?.(
140+`${options.label} launch retry ${attempt}: background log/done file did not materialize`,
141+);
142+continue;
143+}
144+if (launch.stdout.includes("restoring") || launch.stderr.includes("restoring")) {
145+options.onLaunchRetry?.(`${options.label} launch retry ${attempt}: VM is still restoring`);
146+await sleep(5_000);
147+continue;
148+}
149+throw new Error(`${options.label} background launch failed with exit code ${launch.status}`);
150+}
151+if (!launched) {
152+throw new Error(`${options.label} background launch failed with exit code ${lastLaunchStatus}`);
153+}
154+155+let lastLogOffset = 0;
156+while (Date.now() < deadline) {
157+const poll = run(
158+"prlctl",
159+[
160+"exec",
161+options.vmName,
162+"--current-user",
163+"powershell.exe",
164+"-NoProfile",
165+"-ExecutionPolicy",
166+"Bypass",
167+"-EncodedCommand",
168+encodePowerShell(`${pathsScript}
169+$offset = ${lastLogOffset}
170+if (Test-Path $logPath) {
171+ $bytes = [System.IO.File]::ReadAllBytes($logPath)
172+ if ($bytes.Length -gt $offset) {
173+ "__OPENCLAW_LOG_OFFSET__:$($bytes.Length)"
174+ [System.Text.Encoding]::UTF8.GetString($bytes, $offset, $bytes.Length - $offset)
175+ }
176+}
177+if (Test-Path $donePath) {
178+ $backgroundExit = if (Test-Path $exitPath) { (Get-Content -Path $exitPath -Raw).Trim() } else { '0' }
179+ "__OPENCLAW_BACKGROUND_EXIT__:$backgroundExit"
180+ '__OPENCLAW_BACKGROUND_DONE__'
181+ if ($backgroundExit -ne '0') { exit 23 }
182+ exit 0
183+}`),
184+],
185+{ check: false, quiet: true, timeoutMs: timeoutBefore(deadline, 30_000) },
186+);
187+appendOutput(append, poll);
188+const offsetMatch = poll.stdout.match(/__OPENCLAW_LOG_OFFSET__:(\d+)/);
189+if (offsetMatch) {
190+lastLogOffset = Number(offsetMatch[1]);
191+}
192+if (poll.stdout.includes("__OPENCLAW_BACKGROUND_DONE__")) {
193+const exitMatch = poll.stdout.match(/__OPENCLAW_BACKGROUND_EXIT__:(\S+)/);
194+const backgroundExit = exitMatch?.[1] ?? "0";
195+if (backgroundExit !== "0" || (poll.status !== 0 && poll.status !== 124)) {
196+throw new Error(`${options.label} failed`);
197+}
198+cleanupWindowsBackground(options.vmName, pathsScript);
199+return;
200+}
201+await sleep(5_000);
202+}
203+throw new Error(`${options.label} timed out`);
204+}
205+206+function waitForWindowsBackgroundMaterialized(params: {
207+append?: (chunk: string | Uint8Array) => void;
208+deadline: number;
209+pathsScript: string;
210+vmName: string;
211+}): boolean {
212+const materializeDeadline = Math.min(Date.now() + 45_000, params.deadline);
213+while (Date.now() < materializeDeadline) {
214+const result = run(
215+"prlctl",
216+[
217+"exec",
218+params.vmName,
219+"--current-user",
220+"powershell.exe",
221+"-NoProfile",
222+"-ExecutionPolicy",
223+"Bypass",
224+"-EncodedCommand",
225+encodePowerShell(`${params.pathsScript}
226+if ((Test-Path $logPath) -or (Test-Path $donePath)) {
227+ 'materialized'
228+}`),
229+],
230+{ check: false, quiet: true, timeoutMs: timeoutBefore(materializeDeadline, 15_000) },
231+);
232+appendOutput(params.append, result);
233+if (result.stdout.includes("materialized")) {
234+return true;
235+}
236+}
237+return false;
238+}
239+240+function cleanupWindowsBackground(vmName: string, pathsScript: string): void {
241+run(
242+"prlctl",
243+[
244+"exec",
245+vmName,
246+"--current-user",
247+"powershell.exe",
248+"-NoProfile",
249+"-ExecutionPolicy",
250+"Bypass",
251+"-EncodedCommand",
252+encodePowerShell(`${pathsScript}
253+Remove-Item -Path $scriptPath, $logPath, $donePath, $exitPath -Force -ErrorAction SilentlyContinue`),
254+],
255+{ check: false, quiet: true, timeoutMs: 30_000 },
256+);
257+}
258+12259export class LinuxGuest {
13260constructor(
14261private vmName: string,
@@ -17,13 +264,14 @@ export class LinuxGuest {
1726418265exec(args: string[], options: GuestExecOptions = {}): string {
19266const result = run("prlctl", ["exec", this.vmName, "/usr/bin/env", "HOME=/root", ...args], {
20-check: options.check,
267+check: false,
21268input: options.input,
22269quiet: true,
23270timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
24271});
25272this.phases.append(result.stdout);
26273this.phases.append(result.stderr);
274+throwIfFailed("Linux guest command", result, options.check);
27275return result.stdout.trim();
28276}
29277@@ -91,13 +339,14 @@ export class MacosGuest {
91339]
92340 : ["exec", this.input.vmName, "--current-user", "/usr/bin/env", ...envArgs, ...args];
93341const result = run("prlctl", transportArgs, {
94-check: options.check,
342+check: false,
95343input: options.input,
96344quiet: true,
97345timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
98346});
99347this.phases.append(result.stdout);
100348this.phases.append(result.stderr);
349+throwIfFailed("macOS guest command", result, options.check);
101350return result;
102351}
103352@@ -124,13 +373,14 @@ export class WindowsGuest {
124373125374run(args: string[], options: GuestExecOptions = {}): CommandResult {
126375const result = run("prlctl", ["exec", this.vmName, "--current-user", ...args], {
127-check: options.check,
376+check: false,
128377input: options.input,
129378quiet: true,
130379timeoutMs: this.phases.remainingTimeoutMs(options.timeoutMs),
131380});
132381this.phases.append(result.stdout);
133382this.phases.append(result.stderr);
383+throwIfFailed("Windows guest command", result, options.check);
134384return result;
135385}
136386此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。