perf(infra): cache login shell env probes

This commit is contained in:
Vincent Koc
2026-04-13 16:12:17 +01:00
parent 8820547a07
commit 418cb55cb9
2 changed files with 98 additions and 1 deletions

View File

@@ -187,6 +187,40 @@ describe("shell env fallback", () => {
expect(exec2).not.toHaveBeenCalled();
});
it("reuses the cached login-shell env probe across repeated fallback reads", () => {
resetShellPathCacheForTests();
const env: NodeJS.ProcessEnv = {};
const exec = vi.fn(() =>
Buffer.from("OPENAI_API_KEY=from-shell\0ANTHROPIC_API_KEY=from-shell-anthropic\0"),
);
expect(
loadShellEnvFallback({
enabled: true,
env,
expectedKeys: ["OPENAI_API_KEY"],
exec: exec as unknown as Parameters<typeof loadShellEnvFallback>[0]["exec"],
}),
).toEqual({
ok: true,
applied: ["OPENAI_API_KEY"],
});
expect(
loadShellEnvFallback({
enabled: true,
env,
expectedKeys: ["ANTHROPIC_API_KEY"],
exec: exec as unknown as Parameters<typeof loadShellEnvFallback>[0]["exec"],
}),
).toEqual({
ok: true,
applied: ["ANTHROPIC_API_KEY"],
});
expect(exec).toHaveBeenCalledTimes(1);
});
it("tracks last applied keys across success, skip, and failure paths", () => {
const successEnv: NodeJS.ProcessEnv = {};
const successExec = vi.fn(() =>

View File

@@ -12,6 +12,9 @@ const DEFAULT_SHELL = "/bin/sh";
let lastAppliedKeys: string[] = [];
let cachedShellPath: string | null | undefined;
let cachedEtcShells: Set<string> | null | undefined;
let nextExecCacheId = 1;
const loginShellEnvProbeCache = new Map<string, Array<[string, string]>>();
const execCacheIds = new WeakMap<object, number>();
function resolveShellExecEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const execEnv = sanitizeHostExecEnv({ baseEnv: env });
@@ -111,6 +114,52 @@ function parseShellEnv(stdout: Buffer): Map<string, string> {
return shellEnv;
}
function resolveExecCacheId(exec: typeof execFileSync | undefined): string {
if (!exec) {
return "default";
}
const key = exec as object;
let id = execCacheIds.get(key);
if (!id) {
id = nextExecCacheId;
nextExecCacheId += 1;
execCacheIds.set(key, id);
}
return `exec:${id}`;
}
function createLoginShellEnvCacheKey(params: {
shell: string;
timeoutMs: number;
exec?: typeof execFileSync;
execEnv: NodeJS.ProcessEnv;
}): string {
const startupEnvEntries = Object.entries(params.execEnv)
.filter(([key]) => {
if (
key === "HOME" ||
key === "PATH" ||
key === "TERM" ||
key === "LANG" ||
key === "LC_ALL" ||
key === "LC_CTYPE" ||
key === "USER" ||
key === "LOGNAME" ||
key === "TMPDIR"
) {
return true;
}
return key.startsWith("XDG_") || key.startsWith("OPENCLAW_");
})
.toSorted(([left], [right]) => left.localeCompare(right));
return JSON.stringify([
params.shell,
params.timeoutMs,
resolveExecCacheId(params.exec),
startupEnvEntries,
]);
}
type LoginShellEnvProbeResult =
| { ok: true; shellEnv: Map<string, string> }
| { ok: false; error: string };
@@ -124,10 +173,22 @@ function probeLoginShellEnv(params: {
const timeoutMs = resolveTimeoutMs(params.timeoutMs);
const shell = resolveShell(params.env);
const execEnv = resolveShellExecEnv(params.env);
const cacheKey = createLoginShellEnvCacheKey({
shell,
timeoutMs,
exec: params.exec,
execEnv,
});
const cached = loginShellEnvProbeCache.get(cacheKey);
if (cached) {
return { ok: true, shellEnv: new Map(cached) };
}
try {
const stdout = execLoginShellEnvZero({ shell, env: execEnv, exec, timeoutMs });
return { ok: true, shellEnv: parseShellEnv(stdout) };
const shellEnv = parseShellEnv(stdout);
loginShellEnvProbeCache.set(cacheKey, [...shellEnv.entries()]);
return { ok: true, shellEnv };
} catch (err) {
return { ok: false, error: formatErrorMessage(err) };
}
@@ -242,6 +303,8 @@ export function getShellPathFromLoginShell(opts: {
export function resetShellPathCacheForTests(): void {
cachedShellPath = undefined;
cachedEtcShells = undefined;
loginShellEnvProbeCache.clear();
nextExecCacheId = 1;
}
export function getShellEnvAppliedKeys(): string[] {