mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-31 15:45:23 +00:00
fix: start the macOS local terminal when the shell or spawn helper is broken (Support#1418)
This commit is contained in:
1 parent
679b61da9b
commit
18a594a92d
3 files changed
+132
-23
No files matched your search
@@ -1,4 +1,16 @@
|
|||||||
function resolveLocalShell(platform, requestedShell, env = process.env) {
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
function isUsableShell(file, exists) {
|
||||||
|
return !!file && (!path.isAbsolute(file) || exists(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLocalShell(
|
||||||
|
platform,
|
||||||
|
requestedShell,
|
||||||
|
env = process.env,
|
||||||
|
exists = fs.existsSync,
|
||||||
|
) {
|
||||||
if (platform === "win32") {
|
if (platform === "win32") {
|
||||||
if (requestedShell === "wsl") {
|
if (requestedShell === "wsl") {
|
||||||
return { file: "wsl.exe", args: [] };
|
return { file: "wsl.exe", args: [] };
|
||||||
@@ -9,13 +21,47 @@ function resolveLocalShell(platform, requestedShell, env = process.env) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
// A SHELL left pointing at an uninstalled shell makes the spawn fail.
|
||||||
file:
|
const fallbacks =
|
||||||
env.TERMIX_LOCAL_SHELL ||
|
platform === "darwin"
|
||||||
env.SHELL ||
|
? ["/bin/zsh", "/bin/bash", "/bin/sh"]
|
||||||
(platform === "darwin" ? "/bin/zsh" : "/bin/bash"),
|
: ["/bin/bash", "/bin/sh"];
|
||||||
args: ["-l"],
|
const file =
|
||||||
};
|
[env.TERMIX_LOCAL_SHELL, env.SHELL, ...fallbacks].find((candidate) =>
|
||||||
|
isUsableShell(candidate, exists),
|
||||||
|
) || fallbacks[0];
|
||||||
|
return { file, args: ["-l"] };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { resolveLocalShell };
|
/**
|
||||||
|
* node-pty starts every shell through its spawn-helper binary, and a packaged
|
||||||
|
* copy that lost its execute bit fails with "posix_spawnp failed". Returns
|
||||||
|
* how many helpers were fixed.
|
||||||
|
*/
|
||||||
|
function repairSpawnHelpers(ptyDir, fsImpl = fs) {
|
||||||
|
const dir = ptyDir.replace(/app\.asar(?!\.unpacked)/, "app.asar.unpacked");
|
||||||
|
const candidates = [
|
||||||
|
path.join(dir, "build", "Release", "spawn-helper"),
|
||||||
|
path.join(
|
||||||
|
dir,
|
||||||
|
"prebuilds",
|
||||||
|
`${process.platform}-${process.arch}`,
|
||||||
|
"spawn-helper",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
let fixed = 0;
|
||||||
|
for (const helper of candidates) {
|
||||||
|
try {
|
||||||
|
const mode = fsImpl.statSync(helper).mode;
|
||||||
|
if ((mode & 0o111) === 0) {
|
||||||
|
fsImpl.chmodSync(helper, mode | 0o755);
|
||||||
|
fixed += 1;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Missing or read-only; nothing to fix here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { resolveLocalShell, repairSpawnHelpers };
|
||||||
+27
-12
@@ -33,7 +33,7 @@ const { launchNativeRdp } = require("./native-rdp.cjs");
|
|||||||
const { isCloseActiveTabInput } = require("./keyboard-shortcuts.cjs");
|
const { isCloseActiveTabInput } = require("./keyboard-shortcuts.cjs");
|
||||||
const { quitApp } = require("./app-quit.cjs");
|
const { quitApp } = require("./app-quit.cjs");
|
||||||
const { selectLinuxPasswordStore } = require("./linux-password-store.cjs");
|
const { selectLinuxPasswordStore } = require("./linux-password-store.cjs");
|
||||||
const { resolveLocalShell } = require("./local-shell.cjs");
|
const { resolveLocalShell, repairSpawnHelpers } = require("./local-shell.cjs");
|
||||||
const { registerLocalFileHandlers } = require("./local-files.cjs");
|
const { registerLocalFileHandlers } = require("./local-files.cjs");
|
||||||
|
|
||||||
const localTerminalSessions = new Map();
|
const localTerminalSessions = new Map();
|
||||||
@@ -3240,17 +3240,32 @@ ipcMain.handle("local-terminal-start", (event, dimensions = {}) => {
|
|||||||
const rows = Math.min(300, Math.max(1, Number(dimensions.rows) || 24));
|
const rows = Math.min(300, Math.max(1, Number(dimensions.rows) || 24));
|
||||||
const sessionId = crypto.randomUUID();
|
const sessionId = crypto.randomUUID();
|
||||||
const shellConfig = resolveLocalShell(process.platform, dimensions.shell);
|
const shellConfig = resolveLocalShell(process.platform, dimensions.shell);
|
||||||
const child = pty.spawn(shellConfig.file, shellConfig.args, {
|
const spawnShell = () =>
|
||||||
name: "xterm-256color",
|
pty.spawn(shellConfig.file, shellConfig.args, {
|
||||||
cols,
|
name: "xterm-256color",
|
||||||
rows,
|
cols,
|
||||||
cwd: os.homedir(),
|
rows,
|
||||||
env: {
|
cwd: os.homedir(),
|
||||||
...process.env,
|
env: {
|
||||||
TERM: "xterm-256color",
|
...process.env,
|
||||||
COLORTERM: "truecolor",
|
TERM: "xterm-256color",
|
||||||
},
|
COLORTERM: "truecolor",
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
let child;
|
||||||
|
try {
|
||||||
|
child = spawnShell();
|
||||||
|
} catch (error) {
|
||||||
|
const ptyDir = path.dirname(require.resolve("node-pty/package.json"));
|
||||||
|
if (
|
||||||
|
!String(error?.message).includes("posix_spawnp") ||
|
||||||
|
repairSpawnHelpers(ptyDir) === 0
|
||||||
|
) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
logToFile("[local-terminal] restored the spawn-helper execute bit");
|
||||||
|
child = spawnShell();
|
||||||
|
}
|
||||||
const ownerId = event.sender.id;
|
const ownerId = event.sender.id;
|
||||||
const session = { ownerId, process: child, ready: false, buffered: "" };
|
const session = { ownerId, process: child, ready: false, buffered: "" };
|
||||||
localTerminalSessions.set(sessionId, session);
|
localTerminalSessions.set(sessionId, session);
|
||||||
|
|||||||
@@ -2,13 +2,21 @@ import { createRequire } from "node:module";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const { resolveLocalShell } =
|
const { resolveLocalShell, repairSpawnHelpers } =
|
||||||
require("../../../../electron/local-shell.cjs") as {
|
require("../../../../electron/local-shell.cjs") as {
|
||||||
resolveLocalShell: (
|
resolveLocalShell: (
|
||||||
platform: NodeJS.Platform,
|
platform: NodeJS.Platform,
|
||||||
requestedShell?: string,
|
requestedShell?: string,
|
||||||
env?: NodeJS.ProcessEnv,
|
env?: NodeJS.ProcessEnv,
|
||||||
|
exists?: (file: string) => boolean,
|
||||||
) => { file: string; args: string[] };
|
) => { file: string; args: string[] };
|
||||||
|
repairSpawnHelpers: (
|
||||||
|
ptyDir: string,
|
||||||
|
fsImpl: {
|
||||||
|
statSync: (file: string) => { mode: number };
|
||||||
|
chmodSync: (file: string, mode: number) => void;
|
||||||
|
},
|
||||||
|
) => number;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("resolveLocalShell", () => {
|
describe("resolveLocalShell", () => {
|
||||||
@@ -27,9 +35,49 @@ describe("resolveLocalShell", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("preserves the configured shell on non-Windows platforms", () => {
|
it("preserves the configured shell on non-Windows platforms", () => {
|
||||||
expect(resolveLocalShell("linux", "wsl", { SHELL: "/bin/fish" })).toEqual({
|
expect(
|
||||||
|
resolveLocalShell("linux", "wsl", { SHELL: "/bin/fish" }, () => true),
|
||||||
|
).toEqual({
|
||||||
file: "/bin/fish",
|
file: "/bin/fish",
|
||||||
args: ["-l"],
|
args: ["-l"],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips a configured shell that is not installed", () => {
|
||||||
|
const installed = new Set(["/bin/bash", "/bin/zsh"]);
|
||||||
|
expect(
|
||||||
|
resolveLocalShell(
|
||||||
|
"darwin",
|
||||||
|
undefined,
|
||||||
|
{ SHELL: "/opt/homebrew/bin/fish" },
|
||||||
|
(file) => installed.has(file),
|
||||||
|
).file,
|
||||||
|
).toBe("/bin/zsh");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("repairSpawnHelpers", () => {
|
||||||
|
it("restores the execute bit in the unpacked copy only when it is missing", () => {
|
||||||
|
const modes = new Map<string, number>();
|
||||||
|
const chmods: Array<[string, number]> = [];
|
||||||
|
const fsImpl = {
|
||||||
|
statSync: (file: string) => {
|
||||||
|
const mode = modes.get(file);
|
||||||
|
if (mode === undefined) throw new Error("ENOENT");
|
||||||
|
return { mode };
|
||||||
|
},
|
||||||
|
chmodSync: (file: string, mode: number) => {
|
||||||
|
chmods.push([file, mode]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dir =
|
||||||
|
"/Applications/Termix.app/Contents/Resources/app.asar/node_modules/node-pty";
|
||||||
|
const unpacked = dir.replace("app.asar", "app.asar.unpacked");
|
||||||
|
const release = `${unpacked}/build/Release/spawn-helper`;
|
||||||
|
modes.set(release.split("/").join(require("node:path").sep), 0o644);
|
||||||
|
modes.set(release, 0o644);
|
||||||
|
expect(repairSpawnHelpers(dir, fsImpl)).toBe(1);
|
||||||
|
expect(chmods[0][1] & 0o111).not.toBe(0);
|
||||||
|
expect(chmods[0][0]).toContain("app.asar.unpacked");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user