mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:19:05 +08:00
On Windows, spawnService ran uvx through cmd.exe (shell: true), so the `<` in the `agent-client-protocol<0.11` version constraint was parsed as input redirection and agent-server exited immediately with "The system cannot find the file specified." Resolve the command to its absolute path with where.exe and spawn it directly, with no shell, so argument metacharacters stay literal. npm is unaffected: it is already wrapped in cmd.exe by buildNpmScriptCommand before it reaches spawnService.
120 lines
3.3 KiB
JavaScript
120 lines
3.3 KiB
JavaScript
import { spawnSync } from "node:child_process";
|
|
import process from "node:process";
|
|
|
|
/**
|
|
* Return true while Node still considers the child process active.
|
|
*
|
|
* Do not use ChildProcess#killed for cleanup decisions. In Node, `killed`
|
|
* only means a signal was sent successfully; it does not mean the process has
|
|
* exited. That distinction matters for dev launchers because uvx/npm
|
|
* wrappers can receive SIGTERM while their long-running child process keeps
|
|
* serving on the original port.
|
|
*/
|
|
export function isProcessRunning(proc) {
|
|
return proc.exitCode === null && proc.signalCode === null;
|
|
}
|
|
|
|
/**
|
|
* Add spawn options needed for process-tree cleanup.
|
|
*
|
|
* On POSIX, `detached: true` makes the spawned service the leader of a new
|
|
* process group. Later we can signal `-pid` to terminate that whole group,
|
|
* including wrapper chains like:
|
|
*
|
|
* launcher -> uvx -> python agent-server
|
|
* launcher -> npm -> sh -> Vite
|
|
*
|
|
* Windows does not support POSIX process groups, so callers fall back to
|
|
* signaling the direct child process there.
|
|
*/
|
|
export function getProcessTreeSpawnOptions(options = {}) {
|
|
return {
|
|
...options,
|
|
detached: process.platform !== "win32",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolve a service command to a directly spawnable target on Windows.
|
|
*
|
|
* Services spawn without a shell so argument values reach the child verbatim.
|
|
* Spawning `uvx` via cmd.exe instead makes it parse the args: a constraint like
|
|
* `agent-client-protocol<0.11` is read as `<` input redirection and the spawn
|
|
* dies with "The system cannot find the file specified." Resolving to an
|
|
* absolute path lets callers spawn it shell-free.
|
|
*
|
|
* Returns `command` unchanged off Windows, when already a path, or if the lookup
|
|
* fails.
|
|
*/
|
|
export function resolveWindowsCommand(
|
|
command,
|
|
platform = process.platform,
|
|
lookup = whereCommandLookup,
|
|
) {
|
|
if (platform !== "win32") {
|
|
return command;
|
|
}
|
|
if (command.includes("/") || command.includes("\\")) {
|
|
return command;
|
|
}
|
|
return lookup(command) || command;
|
|
}
|
|
|
|
function whereCommandLookup(command) {
|
|
const result = spawnSync("where.exe", [command], { encoding: "utf8" });
|
|
if (result.status !== 0 || !result.stdout) {
|
|
return null;
|
|
}
|
|
return result.stdout.split(/\r?\n/).find(Boolean)?.trim() || null;
|
|
}
|
|
|
|
/**
|
|
* Signal the whole spawned service tree when possible.
|
|
*
|
|
* POSIX `process.kill(-pid, signal)` targets the process group whose id is
|
|
* `pid`; this only works because services are spawned with
|
|
* `getProcessTreeSpawnOptions()`. Without the negative pid, shutdown would
|
|
* often stop only the wrapper process and leave the actual server child
|
|
* listening on its port.
|
|
*/
|
|
export function signalProcessTree(proc, signal) {
|
|
if (!isProcessRunning(proc)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
if (process.platform === "win32" || !proc.pid) {
|
|
proc.kill(signal);
|
|
} else {
|
|
process.kill(-proc.pid, signal);
|
|
}
|
|
return true;
|
|
} catch (err) {
|
|
if (err?.code === "ESRCH") {
|
|
return false;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export function createShutdownHookRegistry(onError) {
|
|
const hooks = new Set();
|
|
|
|
return {
|
|
add(hook) {
|
|
hooks.add(hook);
|
|
return () => hooks.delete(hook);
|
|
},
|
|
|
|
run() {
|
|
for (const hook of hooks) {
|
|
try {
|
|
hook();
|
|
} catch (err) {
|
|
onError?.(err);
|
|
}
|
|
}
|
|
},
|
|
};
|
|
}
|