mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
fix(desktop): find spawn-helper in universal macOS builds (#1630)
The universal build ships app-arm64.asar and app-x64.asar instead of app.asar, so node-pty's helper path rewrite never pointed at the unpacked copy and every local terminal failed with posix_spawnp failed.
This commit is contained in:
1 parent
4c9ee5923e
commit
1396b3a70c
4 files changed
+129
-36
No files matched your search
@@ -39,7 +39,11 @@ function resolveLocalShell(
|
||||
* how many helpers were fixed.
|
||||
*/
|
||||
function repairSpawnHelpers(ptyDir, fsImpl = fs) {
|
||||
const dir = ptyDir.replace(/app\.asar(?!\.unpacked)/, "app.asar.unpacked");
|
||||
// A universal macOS build ships app-arm64.asar and app-x64.asar.
|
||||
const dir = ptyDir.replace(
|
||||
/\b(app(?:-[a-z0-9]+)?)\.asar(?!\.unpacked)/,
|
||||
"$1.asar.unpacked",
|
||||
);
|
||||
const candidates = [
|
||||
path.join(dir, "build", "Release", "spawn-helper"),
|
||||
path.join(
|
||||
|
||||
+48
-34
@@ -3,43 +3,45 @@ const path = require("node:path");
|
||||
|
||||
const nodePtyDir = path.join(__dirname, "..", "node_modules", "node-pty");
|
||||
|
||||
if (!fs.existsSync(nodePtyDir)) {
|
||||
console.log("[patch-node-pty] node-pty not found, skipping");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const unixTerminalPath = path.join(nodePtyDir, "lib", "unixTerminal.js");
|
||||
|
||||
const originalHelperRewrite = [
|
||||
"helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
|
||||
"helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
|
||||
].join("\n");
|
||||
// Older installs may still carry the first version of this patch.
|
||||
const knownHelperRewrites = [
|
||||
[
|
||||
"helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
|
||||
"helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
|
||||
].join("\n"),
|
||||
[
|
||||
"if (!helperPath.includes('app.asar.unpacked')) {",
|
||||
" helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
|
||||
"}",
|
||||
"if (!helperPath.includes('node_modules.asar.unpacked')) {",
|
||||
" helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
|
||||
"}",
|
||||
].join("\n"),
|
||||
];
|
||||
|
||||
const patchedHelperRewrite = [
|
||||
"if (!helperPath.includes('app.asar.unpacked')) {",
|
||||
" helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
|
||||
"}",
|
||||
"if (!helperPath.includes('node_modules.asar.unpacked')) {",
|
||||
" helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
|
||||
"}",
|
||||
].join("\n");
|
||||
// A universal macOS build ships app-arm64.asar and app-x64.asar, not app.asar.
|
||||
const patchedHelperRewrite =
|
||||
"helperPath = helperPath.replace(/\\b(app(?:-[a-z0-9]+)?|node_modules)\\.asar(?!\\.unpacked)/g, '$1.asar.unpacked');";
|
||||
|
||||
function patchUnixTerminalHelperRewrite() {
|
||||
if (!fs.existsSync(unixTerminalPath)) {
|
||||
function patchUnixTerminalHelperRewrite(file = unixTerminalPath) {
|
||||
if (!fs.existsSync(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const source = fs.readFileSync(unixTerminalPath, "utf8");
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
if (source.includes(patchedHelperRewrite)) {
|
||||
return false;
|
||||
}
|
||||
if (!source.includes(originalHelperRewrite)) {
|
||||
const known = knownHelperRewrites.find((rewrite) => source.includes(rewrite));
|
||||
if (!known) {
|
||||
return false;
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
unixTerminalPath,
|
||||
source.replace(originalHelperRewrite, patchedHelperRewrite),
|
||||
file,
|
||||
source.replace(known, () => patchedHelperRewrite),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -67,19 +69,31 @@ function chmodSpawnHelpers(rootDir) {
|
||||
return fixed;
|
||||
}
|
||||
|
||||
const helperRewritePatched = patchUnixTerminalHelperRewrite();
|
||||
const helpersChmodded = chmodSpawnHelpers(nodePtyDir);
|
||||
// Tests and afterPack require this file; only a direct run patches node_modules.
|
||||
function main() {
|
||||
if (!fs.existsSync(nodePtyDir)) {
|
||||
console.log("[patch-node-pty] node-pty not found, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
if (helperRewritePatched) {
|
||||
console.log("[patch-node-pty] Patched unixTerminal helper path rewrite");
|
||||
const helperRewritePatched = patchUnixTerminalHelperRewrite();
|
||||
const helpersChmodded = chmodSpawnHelpers(nodePtyDir);
|
||||
|
||||
if (helperRewritePatched) {
|
||||
console.log("[patch-node-pty] Patched unixTerminal helper path rewrite");
|
||||
}
|
||||
if (helpersChmodded > 0) {
|
||||
console.log(
|
||||
`[patch-node-pty] Restored execute bit on ${helpersChmodded} spawn-helper binary(ies)`,
|
||||
);
|
||||
}
|
||||
if (!helperRewritePatched && helpersChmodded === 0) {
|
||||
console.log("[patch-node-pty] Already patched or target files not found");
|
||||
}
|
||||
}
|
||||
if (helpersChmodded > 0) {
|
||||
console.log(
|
||||
`[patch-node-pty] Restored execute bit on ${helpersChmodded} spawn-helper binary(ies)`,
|
||||
);
|
||||
}
|
||||
if (!helperRewritePatched && helpersChmodded === 0) {
|
||||
console.log("[patch-node-pty] Already patched or target files not found");
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { chmodSpawnHelpers, patchUnixTerminalHelperRewrite };
|
||||
@@ -80,4 +80,20 @@ describe("repairSpawnHelpers", () => {
|
||||
expect(chmods[0][1] & 0o111).not.toBe(0);
|
||||
expect(chmods[0][0]).toContain("app.asar.unpacked");
|
||||
});
|
||||
|
||||
it.each(["app-arm64.asar", "app-x64.asar"])(
|
||||
"repairs the helper in the unpacked copy of a universal build's %s",
|
||||
(asar) => {
|
||||
const chmods: string[] = [];
|
||||
const fsImpl = {
|
||||
statSync: () => ({ mode: 0o644 }),
|
||||
chmodSync: (file: string) => {
|
||||
chmods.push(file.split(require("node:path").sep).join("/"));
|
||||
},
|
||||
};
|
||||
const dir = `/Applications/Termix.app/Contents/Resources/${asar}/node_modules/node-pty`;
|
||||
repairSpawnHelpers(dir, fsImpl);
|
||||
expect(chmods[0]).toContain(`/${asar}.unpacked/node_modules/node-pty/`);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -11,7 +11,48 @@ import { tmpdir } from "node:os";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { chmodSpawnHelpers } = require("../../../../scripts/patch-node-pty.cjs");
|
||||
const {
|
||||
chmodSpawnHelpers,
|
||||
patchUnixTerminalHelperRewrite,
|
||||
} = require("../../../../scripts/patch-node-pty.cjs");
|
||||
|
||||
const upstreamRewrite = [
|
||||
"helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
|
||||
"helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
|
||||
].join("\n");
|
||||
|
||||
const previouslyPatchedRewrite = [
|
||||
"if (!helperPath.includes('app.asar.unpacked')) {",
|
||||
" helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
|
||||
"}",
|
||||
"if (!helperPath.includes('node_modules.asar.unpacked')) {",
|
||||
" helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
||||
const resources = "/Applications/Termix.app/Contents/Resources";
|
||||
const helperPaths = [
|
||||
["app.asar", "app.asar.unpacked"],
|
||||
["app-arm64.asar", "app-arm64.asar.unpacked"],
|
||||
["app-x64.asar", "app-x64.asar.unpacked"],
|
||||
["app.asar.unpacked", "app.asar.unpacked"],
|
||||
["app-arm64.asar.unpacked", "app-arm64.asar.unpacked"],
|
||||
].map(([from, to]) => [
|
||||
`${resources}/${from}/node_modules/node-pty/build/Release/spawn-helper`,
|
||||
`${resources}/${to}/node_modules/node-pty/build/Release/spawn-helper`,
|
||||
]);
|
||||
|
||||
function writeUnixTerminal(rewrite: string) {
|
||||
const file = join(
|
||||
tmpdir(),
|
||||
`termix-unix-terminal-${Date.now()}-${Math.random()}.cjs`,
|
||||
);
|
||||
writeFileSync(
|
||||
file,
|
||||
`module.exports = function (helperPath) {\n${rewrite}\nreturn helperPath;\n};\n`,
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
// chmod's execute bit is a POSIX concept; Windows/NTFS has no equivalent mode bit.
|
||||
const itPosix = process.platform === "win32" ? it.skip : it;
|
||||
@@ -30,4 +71,22 @@ describe("patch-node-pty", () => {
|
||||
expect(chmodSpawnHelpers(root)).toBe(0);
|
||||
expect(existsSync(helper)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["upstream", upstreamRewrite],
|
||||
["previously patched", previouslyPatchedRewrite],
|
||||
])(
|
||||
"points the %s helper path at the unpacked copy of any app asar",
|
||||
(_, rewrite) => {
|
||||
const file = writeUnixTerminal(rewrite);
|
||||
|
||||
expect(patchUnixTerminalHelperRewrite(file)).toBe(true);
|
||||
expect(patchUnixTerminalHelperRewrite(file)).toBe(false);
|
||||
|
||||
const rewriteHelperPath = require(file);
|
||||
for (const [from, to] of helperPaths) {
|
||||
expect(rewriteHelperPath(from)).toBe(to);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user