fix: merge login shell PATH for gateway exec

This commit is contained in:
Peter Steinberger
2026-01-20 14:03:59 +00:00
parent a0180f364d
commit e45228ac37
7 changed files with 221 additions and 12 deletions

View File

@@ -0,0 +1,34 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { getShellPathFromLoginShell, resetShellPathCacheForTests } from "./shell-env.js";
describe("getShellPathFromLoginShell", () => {
afterEach(() => resetShellPathCacheForTests());
it("returns PATH from login shell env", () => {
if (process.platform === "win32") return;
const exec = vi
.fn()
.mockReturnValue(Buffer.from("PATH=/custom/bin\0HOME=/home/user\0", "utf-8"));
const result = getShellPathFromLoginShell({ env: { SHELL: "/bin/sh" }, exec });
expect(result).toBe("/custom/bin");
});
it("caches the value", () => {
if (process.platform === "win32") return;
const exec = vi.fn().mockReturnValue(Buffer.from("PATH=/custom/bin\0", "utf-8"));
const env = { SHELL: "/bin/sh" } as NodeJS.ProcessEnv;
expect(getShellPathFromLoginShell({ env, exec })).toBe("/custom/bin");
expect(getShellPathFromLoginShell({ env, exec })).toBe("/custom/bin");
expect(exec).toHaveBeenCalledTimes(1);
});
it("returns null on exec failure", () => {
if (process.platform === "win32") return;
const exec = vi.fn(() => {
throw new Error("boom");
});
const result = getShellPathFromLoginShell({ env: { SHELL: "/bin/sh" }, exec });
expect(result).toBeNull();
});
});

View File

@@ -5,12 +5,28 @@ import { isTruthyEnvValue } from "./env.js";
const DEFAULT_TIMEOUT_MS = 15_000;
const DEFAULT_MAX_BUFFER_BYTES = 2 * 1024 * 1024;
let lastAppliedKeys: string[] = [];
let cachedShellPath: string | null | undefined;
function resolveShell(env: NodeJS.ProcessEnv): string {
const shell = env.SHELL?.trim();
return shell && shell.length > 0 ? shell : "/bin/sh";
}
function parseShellEnv(stdout: Buffer): Map<string, string> {
const shellEnv = new Map<string, string>();
const parts = stdout.toString("utf8").split("\0");
for (const part of parts) {
if (!part) continue;
const eq = part.indexOf("=");
if (eq <= 0) continue;
const key = part.slice(0, eq);
const value = part.slice(eq + 1);
if (!key) continue;
shellEnv.set(key, value);
}
return shellEnv;
}
export type ShellEnvFallbackResult =
| { ok: true; applied: string[]; skippedReason?: never }
| { ok: true; applied: []; skippedReason: "already-has-keys" | "disabled" }
@@ -63,17 +79,7 @@ export function loadShellEnvFallback(opts: ShellEnvFallbackOptions): ShellEnvFal
return { ok: false, error: msg, applied: [] };
}
const shellEnv = new Map<string, string>();
const parts = stdout.toString("utf8").split("\0");
for (const part of parts) {
if (!part) continue;
const eq = part.indexOf("=");
if (eq <= 0) continue;
const key = part.slice(0, eq);
const value = part.slice(eq + 1);
if (!key) continue;
shellEnv.set(key, value);
}
const shellEnv = parseShellEnv(stdout);
const applied: string[] = [];
for (const key of opts.expectedKeys) {
@@ -104,6 +110,48 @@ export function resolveShellEnvFallbackTimeoutMs(env: NodeJS.ProcessEnv): number
return Math.max(0, parsed);
}
export function getShellPathFromLoginShell(opts: {
env: NodeJS.ProcessEnv;
timeoutMs?: number;
exec?: typeof execFileSync;
}): string | null {
if (cachedShellPath !== undefined) return cachedShellPath;
if (process.platform === "win32") {
cachedShellPath = null;
return cachedShellPath;
}
const exec = opts.exec ?? execFileSync;
const timeoutMs =
typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs)
? Math.max(0, opts.timeoutMs)
: DEFAULT_TIMEOUT_MS;
const shell = resolveShell(opts.env);
let stdout: Buffer;
try {
stdout = exec(shell, ["-l", "-c", "env -0"], {
encoding: "buffer",
timeout: timeoutMs,
maxBuffer: DEFAULT_MAX_BUFFER_BYTES,
env: opts.env,
stdio: ["ignore", "pipe", "pipe"],
});
} catch {
cachedShellPath = null;
return cachedShellPath;
}
const shellEnv = parseShellEnv(stdout);
const shellPath = shellEnv.get("PATH")?.trim();
cachedShellPath = shellPath && shellPath.length > 0 ? shellPath : null;
return cachedShellPath;
}
export function resetShellPathCacheForTests(): void {
cachedShellPath = undefined;
}
export function getShellEnvAppliedKeys(): string[] {
return [...lastAppliedKeys];
}