Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 ae7b316623 fix(ssh): resolve ET ssh wrapper without a host OpenSSH binary
Unit tests stub execFileSync and only found ssh via /usr/bin/ssh, so the
PATH wrapper was skipped on images without that path. Prefer an absolute
findExecutable result, then command -v, then the well-known locations.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-13 07:49:20 +00:00
2 changed files with 22 additions and 8 deletions

No files matched your search

+14 -7
View File
@@ -832,13 +832,19 @@ main();
try {
// Resolve the real OpenSSH binary to an absolute path BEFORE we
// prepend the wrapper directory to PATH. Embedding a bare `ssh`
// name would recurse into the wrapper forever.
let realSsh = process.platform === "win32"
? (findExecutable("ssh") || "")
: "";
// name would recurse into the wrapper forever. findExecutable
// returns that bare name off Windows, so only an absolute result
// is usable; otherwise fall back to command -v and well-known paths.
let realSsh = "";
try {
const found = typeof findExecutable === "function" ? findExecutable("ssh") : "";
if (found && path.isAbsolute(String(found))) realSsh = String(found);
} catch {
// Fall through to command -v / well-known paths.
}
if (!realSsh || !path.isAbsolute(realSsh)) {
try {
const resolved = execFileSync(
const lookup = execFileSync(
process.platform === "win32" ? "where" : "sh",
process.platform === "win32" ? ["ssh"] : ["-c", "command -v ssh"],
{
@@ -847,8 +853,9 @@ main();
windowsHide: true,
env: process.env,
},
).trim().split(/\r?\n/)[0];
if (resolved) realSsh = resolved;
);
const resolved = String(lookup ?? "").trim().split(/\r?\n/).find(Boolean) || "";
if (resolved && path.isAbsolute(resolved)) realSsh = resolved;
} catch {
// Fall through to common absolute locations.
}
@@ -26,6 +26,13 @@ function makeApi(t, overrides = {}) {
getTempFilePath: (name) => path.join(base, name),
getTempDir: () => base,
};
// Absolute stand-in for OpenSSH. prepareEtSshEnvironment must be able to
// inject the PATH wrapper without the host image having /usr/bin/ssh, and
// without the execFileSync stub (used to skip Windows icacls) answering
// `command -v ssh`.
const fakeSsh = path.join(base, "openssh", process.platform === "win32" ? "ssh.exe" : "ssh");
fs.mkdirSync(path.dirname(fakeSsh), { recursive: true });
fs.writeFileSync(fakeSsh, "#!/bin/sh\nexit 0\n");
const osMock = {
homedir: () => fakeHome,
userInfo: () => ({ username: "tester" }),
@@ -53,7 +60,7 @@ function makeApi(t, overrides = {}) {
trackSessionIdlePrompt: () => {},
createPtyOutputBuffer: overrides.createPtyOutputBuffer
|| (() => ({ bufferData() {}, flush() {}, flushPaced() {} })),
findExecutable: () => "ssh",
findExecutable: (name) => (name === "ssh" ? fakeSsh : name),
bundledEtClient,
});
return { api, base, sessions };