fix: merge login shell PATH for gateway exec
This commit is contained in:
34
src/infra/shell-env.path.test.ts
Normal file
34
src/infra/shell-env.path.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user