fix: centralize Windows exec handling and async migrations (#318)

* Add Windows exec foundation helpers

* server: centralize windows exec handling phase 2

* migrate cli daemon status to execCommand

* refactor: complete phase 4 async exec migrations

* test: cover windows exec helpers

* fix: use beforeEach for context.skip in test setup
This commit is contained in:
Mohamed Boudra
2026-04-13 16:20:59 +08:00
committed by GitHub
parent f0c59ec534
commit bf76915884
44 changed files with 3048 additions and 2781 deletions
@@ -1,5 +1,5 @@
import { spawnSync } from "node:child_process";
import { platform } from "node:os";
import { execCommand } from "@getpaseo/server";
export interface NodePathFromPidResult {
nodePath: string | null;
@@ -13,63 +13,37 @@ function normalizeError(error: unknown): string {
return String(error);
}
function resolveNodePathFromPidUnix(pid: number): NodePathFromPidResult {
const result = spawnSync("ps", ["-o", "comm=", "-p", String(pid)], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error) {
return { nodePath: null, error: `ps failed: ${normalizeError(result.error)}` };
async function resolveNodePathFromPidUnix(pid: number): Promise<NodePathFromPidResult> {
try {
const { stdout } = await execCommand("ps", ["-o", "comm=", "-p", String(pid)]);
const resolved = stdout.trim();
return resolved
? { nodePath: resolved }
: { nodePath: null, error: "ps returned an empty command path" };
} catch (error) {
return { nodePath: null, error: `ps failed: ${normalizeError(error)}` };
}
if ((result.status ?? 1) !== 0) {
const details = result.stderr?.trim();
return {
nodePath: null,
error: details ? `ps failed: ${details}` : `ps exited with code ${result.status ?? 1}`,
};
}
const resolved = result.stdout.trim();
return resolved
? { nodePath: resolved }
: { nodePath: null, error: "ps returned an empty command path" };
}
function runProcessProbe(
async function runProcessProbe(
command: string,
args: string[],
): {
): Promise<{
resolved: string | null;
error?: string;
} {
const result = spawnSync(command, args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
if (result.error) {
return { resolved: null, error: `${command} failed: ${normalizeError(result.error)}` };
}> {
try {
const { stdout } = await execCommand(command, args);
const resolved = stdout.trim();
return resolved
? { resolved }
: { resolved: null, error: `${command} returned no executable path` };
} catch (error) {
return { resolved: null, error: `${command} failed: ${normalizeError(error)}` };
}
if ((result.status ?? 1) !== 0) {
const details = result.stderr?.trim();
return {
resolved: null,
error: details
? `${command} failed: ${details}`
: `${command} exited with code ${result.status ?? 1}`,
};
}
const resolved = result.stdout.trim();
return resolved
? { resolved }
: { resolved: null, error: `${command} returned no executable path` };
}
function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult {
async function resolveNodePathFromPidWindows(pid: number): Promise<NodePathFromPidResult> {
const probes: Array<{
label: string;
command: string;
@@ -103,7 +77,7 @@ function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult {
const errors: string[] = [];
for (const probe of probes) {
const result = runProcessProbe(probe.command, probe.args);
const result = await runProcessProbe(probe.command, probe.args);
if (result.resolved) {
const resolved = probe.parseValue ? probe.parseValue(result.resolved) : result.resolved;
if (resolved) {
@@ -123,8 +97,8 @@ function resolveNodePathFromPidWindows(pid: number): NodePathFromPidResult {
};
}
export function resolveNodePathFromPid(pid: number): NodePathFromPidResult {
export async function resolveNodePathFromPid(pid: number): Promise<NodePathFromPidResult> {
return platform() === "win32"
? resolveNodePathFromPidWindows(pid)
: resolveNodePathFromPidUnix(pid);
? await resolveNodePathFromPidWindows(pid)
: await resolveNodePathFromPidUnix(pid);
}
+8 -9
View File
@@ -1,10 +1,11 @@
import type { Command } from "commander";
import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { promisify } from "node:util";
import { getOrCreateServerId, findExecutable, applyProviderEnv } from "@getpaseo/server";
const execFileAsync = promisify(execFile);
import {
getOrCreateServerId,
findExecutable,
applyProviderEnv,
execCommand,
} from "@getpaseo/server";
import { tryConnectToDaemon } from "../../utils/client.js";
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
import { resolveLocalDaemonState, resolveTcpHostFromListen } from "./local-daemon.js";
@@ -178,11 +179,9 @@ async function checkProviderBinary(
}
const env = applyProviderEnv(process.env);
try {
const { stdout } = await execFileAsync(binaryPath, ["--version"], {
encoding: "utf8",
const { stdout } = await execCommand(binaryPath, ["--version"], {
timeout: 5000,
env,
windowsHide: true,
});
return { path: binaryPath, version: stdout.trim() || null };
} catch {
@@ -224,7 +223,7 @@ export async function runStatusCommand(
if (!state.running) {
daemonNode = "-";
} else if (state.pidInfo?.pid) {
const fromPid = resolveNodePathFromPid(state.pidInfo.pid);
const fromPid = await resolveNodePathFromPid(state.pidInfo.pid);
daemonNode = fromPid.nodePath ?? `unknown (${fromPid.error ?? "could not resolve from PID"})`;
} else {
daemonNode = "unknown (no PID available)";