mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(server): preserve provider environment during availability probes
This commit is contained in:
@@ -65,11 +65,15 @@ export async function probeExecutable(
|
||||
executablePath: string,
|
||||
options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS,
|
||||
): Promise<boolean> {
|
||||
const { probeTimeoutMs: timeoutMs = PROBE_TIMEOUT_MS, env } =
|
||||
typeof options === "number" ? { probeTimeoutMs: options } : options;
|
||||
const {
|
||||
probeTimeoutMs: timeoutMs = PROBE_TIMEOUT_MS,
|
||||
env,
|
||||
envMode,
|
||||
} = typeof options === "number" ? { probeTimeoutMs: options } : options;
|
||||
try {
|
||||
await execCommand(executablePath, ["--version"], {
|
||||
baseEnv: env,
|
||||
envMode,
|
||||
timeout: timeoutMs,
|
||||
killSignal: "SIGKILL",
|
||||
maxBuffer: 64 * 1024,
|
||||
@@ -140,14 +144,19 @@ export async function findExecutableWithoutProbing(
|
||||
export interface ExecutableResolutionOptions {
|
||||
probeTimeoutMs?: number;
|
||||
env?: ProcessEnvRecord;
|
||||
/** Preserve an environment already finalized by the caller's launch policy. */
|
||||
envMode?: "external" | "internal";
|
||||
}
|
||||
|
||||
export async function findExecutable(
|
||||
name: string,
|
||||
options: number | ExecutableResolutionOptions = PROBE_TIMEOUT_MS,
|
||||
): Promise<string | null> {
|
||||
const { probeTimeoutMs = PROBE_TIMEOUT_MS, env } =
|
||||
typeof options === "number" ? { probeTimeoutMs: options } : options;
|
||||
const {
|
||||
probeTimeoutMs = PROBE_TIMEOUT_MS,
|
||||
env,
|
||||
envMode,
|
||||
} = typeof options === "number" ? { probeTimeoutMs: options } : options;
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
@@ -157,19 +166,19 @@ export async function findExecutable(
|
||||
return windowsExecutableResolution.find(trimmed, {
|
||||
enumeratePathCandidates: (command) => enumerateCandidates(command, env),
|
||||
probeExecutable: (command, timeout) =>
|
||||
probeExecutable(command, { probeTimeoutMs: timeout, env }),
|
||||
probeExecutable(command, { probeTimeoutMs: timeout, env, envMode }),
|
||||
exists: existsSync,
|
||||
probeTimeoutMs,
|
||||
});
|
||||
}
|
||||
|
||||
if (hasPathSeparator(trimmed)) {
|
||||
return (await probeExecutable(trimmed, { probeTimeoutMs, env })) ? trimmed : null;
|
||||
return (await probeExecutable(trimmed, { probeTimeoutMs, env, envMode })) ? trimmed : null;
|
||||
}
|
||||
|
||||
const candidates = await enumerateCandidates(trimmed, env);
|
||||
for (const candidate of candidates) {
|
||||
if (await probeExecutable(candidate, { probeTimeoutMs, env })) {
|
||||
if (await probeExecutable(candidate, { probeTimeoutMs, env, envMode })) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
@@ -189,6 +189,71 @@ describe("resolveProviderLaunch", () => {
|
||||
});
|
||||
|
||||
describe("checkProviderLaunchAvailable", () => {
|
||||
test.each([
|
||||
{ lookup: "absolute", configured: true },
|
||||
{ lookup: "PATH", configured: true },
|
||||
{ lookup: "default", configured: true },
|
||||
{ lookup: "absolute", configured: false },
|
||||
{ lookup: "PATH", configured: false },
|
||||
{ lookup: "default", configured: false },
|
||||
])(
|
||||
"passes finalized provider environment to the $lookup probe (configured=$configured)",
|
||||
async ({ lookup, configured }) => {
|
||||
const dir = makeTempDir();
|
||||
const observed = path.join(dir, "probe.json");
|
||||
const script = path.join(dir, "probe.cjs");
|
||||
writeFileSync(
|
||||
script,
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(observed)}, JSON.stringify({
|
||||
args: process.argv.slice(2),
|
||||
mode: process.env.ELECTRON_RUN_AS_NODE ?? null,
|
||||
inherited: process.env.PASEO_NODE_ENV ?? null,
|
||||
parent: process.env.CLAUDECODE ?? null,
|
||||
ordinary: process.env.CUSTOM
|
||||
}));`,
|
||||
);
|
||||
const command = process.platform === "win32" ? "provider.cmd" : "provider";
|
||||
const executable = path.join(dir, command);
|
||||
writeFileSync(
|
||||
executable,
|
||||
process.platform === "win32"
|
||||
? `@echo off\r\n"${process.execPath}" "${script}" %*\r\n`
|
||||
: `#!/bin/sh\nexec '${process.execPath.replaceAll("'", "'\\''")}' '${script.replaceAll("'", "'\\''")}' "$@"\n`,
|
||||
);
|
||||
chmodSync(executable, 0o755);
|
||||
const env = createProviderEnv({
|
||||
baseEnv: {
|
||||
...process.env,
|
||||
ELECTRON_RUN_AS_NODE: "inherited",
|
||||
PASEO_NODE_ENV: "production",
|
||||
CLAUDECODE: "1",
|
||||
},
|
||||
runtimeSettings: { env: configured ? { ELECTRON_RUN_AS_NODE: "1" } : {} },
|
||||
overlays: [{ CUSTOM: "session", PASEO_NODE_ENV: "development" }],
|
||||
});
|
||||
env.PATH = `${dir}${path.delimiter}${env.PATH ?? ""}`;
|
||||
const launch = await resolveProviderLaunch({
|
||||
commandConfig:
|
||||
lookup === "default"
|
||||
? undefined
|
||||
: { mode: "replace", argv: [lookup === "absolute" ? executable : command] },
|
||||
defaultBinary: command,
|
||||
});
|
||||
|
||||
await expect(checkProviderLaunchAvailable({ ...launch, env }, { command })).resolves.toEqual({
|
||||
available: true,
|
||||
resolvedPath: executable,
|
||||
});
|
||||
expect(JSON.parse(readFileSync(observed, "utf8"))).toEqual({
|
||||
args: ["--version"],
|
||||
mode: configured ? "1" : null,
|
||||
inherited: null,
|
||||
parent: null,
|
||||
ordinary: "session",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test("reports available with a resolved path", async () => {
|
||||
const binDir = makeTempDir();
|
||||
const binary = createExecutable(binDir, "default-provider");
|
||||
|
||||
@@ -44,7 +44,8 @@ export interface ResolvedProviderLaunch {
|
||||
command: string;
|
||||
args: string[];
|
||||
source: ProviderLaunchSource;
|
||||
env?: ProcessEnvRecord;
|
||||
/** Finalized provider environment from createProviderEnv(), ready for child execution. */
|
||||
env?: ExternalProcessEnv;
|
||||
}
|
||||
|
||||
export interface ProviderLaunchAvailability {
|
||||
@@ -67,7 +68,8 @@ function normalizeLaunchDefault(
|
||||
}
|
||||
|
||||
async function resolveLaunchPath(command: string, env?: ProcessEnvRecord): Promise<string | null> {
|
||||
const found = await findExecutable(command, { env });
|
||||
// launch.env is finalized by the provider builder, including explicit runtime controls.
|
||||
const found = await findExecutable(command, { env, envMode: env ? "internal" : "external" });
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
@@ -79,10 +81,11 @@ async function resolveLaunchPath(command: string, env?: ProcessEnvRecord): Promi
|
||||
|
||||
async function resolveDefaultLaunchPath(
|
||||
defaultBinary: ProviderLaunchDefault,
|
||||
env?: ProcessEnvRecord,
|
||||
): Promise<string | null> {
|
||||
return defaultBinary.resolvePath
|
||||
? await defaultBinary.resolvePath()
|
||||
: await resolveLaunchPath(defaultBinary.command);
|
||||
: await resolveLaunchPath(defaultBinary.command, env);
|
||||
}
|
||||
|
||||
export interface ResolveProviderLaunchOptions {
|
||||
@@ -121,7 +124,7 @@ export async function checkProviderLaunchAvailable(
|
||||
): Promise<ProviderLaunchAvailability> {
|
||||
const resolvedPath =
|
||||
defaultBinary && launch.source !== "override"
|
||||
? await resolveDefaultLaunchPath(defaultBinary)
|
||||
? await resolveDefaultLaunchPath(defaultBinary, launch.env)
|
||||
: await resolveLaunchPath(launch.command, launch.env);
|
||||
return {
|
||||
available: resolvedPath !== null,
|
||||
|
||||
Reference in New Issue
Block a user