fix(server): self-heal node-pty spawn-helper execute bit
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
import { describe, it, expect, afterEach } from "vitest";
|
import { describe, it, expect, afterEach } from "vitest";
|
||||||
import { createTerminal, type TerminalSession } from "./terminal.js";
|
import {
|
||||||
|
createTerminal,
|
||||||
|
ensureNodePtySpawnHelperExecutableForCurrentPlatform,
|
||||||
|
type TerminalSession,
|
||||||
|
} from "./terminal.js";
|
||||||
|
import { chmodSync, mkdtempSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
|
||||||
// Extract text from a single row
|
// Extract text from a single row
|
||||||
function getRowText(state: ReturnType<TerminalSession["getState"]>, rowIndex: number): string {
|
function getRowText(state: ReturnType<TerminalSession["getState"]>, rowIndex: number): string {
|
||||||
@@ -40,12 +47,19 @@ async function waitForLines(
|
|||||||
|
|
||||||
describe("Terminal", () => {
|
describe("Terminal", () => {
|
||||||
const sessions: TerminalSession[] = [];
|
const sessions: TerminalSession[] = [];
|
||||||
|
const temporaryDirs: string[] = [];
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
session.kill();
|
session.kill();
|
||||||
}
|
}
|
||||||
sessions.length = 0;
|
sessions.length = 0;
|
||||||
|
while (temporaryDirs.length > 0) {
|
||||||
|
const dir = temporaryDirs.pop();
|
||||||
|
if (dir) {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function trackSession(session: TerminalSession): TerminalSession {
|
function trackSession(session: TerminalSession): TerminalSession {
|
||||||
@@ -54,6 +68,24 @@ describe("Terminal", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("createTerminal", () => {
|
describe("createTerminal", () => {
|
||||||
|
it("ensures darwin prebuild spawn-helper is executable", () => {
|
||||||
|
const packageRoot = mkdtempSync(join(tmpdir(), "terminal-node-pty-helper-"));
|
||||||
|
temporaryDirs.push(packageRoot);
|
||||||
|
const prebuildDir = join(packageRoot, "prebuilds", `darwin-${process.arch}`);
|
||||||
|
mkdirSync(prebuildDir, { recursive: true });
|
||||||
|
const helperPath = join(prebuildDir, "spawn-helper");
|
||||||
|
writeFileSync(helperPath, "#!/bin/sh\necho helper\n");
|
||||||
|
chmodSync(helperPath, 0o644);
|
||||||
|
|
||||||
|
ensureNodePtySpawnHelperExecutableForCurrentPlatform({
|
||||||
|
packageRoot,
|
||||||
|
platform: "darwin",
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(statSync(helperPath).mode & 0o111).toBe(0o111);
|
||||||
|
});
|
||||||
|
|
||||||
it("creates a terminal session with an id, name, and cwd", async () => {
|
it("creates a terminal session with an id, name, and cwd", async () => {
|
||||||
const session = trackSession(
|
const session = trackSession(
|
||||||
await createTerminal({
|
await createTerminal({
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import * as pty from "node-pty";
|
import * as pty from "node-pty";
|
||||||
import xterm, { type Terminal as TerminalType } from "@xterm/headless";
|
import xterm, { type Terminal as TerminalType } from "@xterm/headless";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
|
import { chmodSync, existsSync, statSync } from "node:fs";
|
||||||
|
import { dirname, join } from "node:path";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
const { Terminal } = xterm;
|
const { Terminal } = xterm;
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
let nodePtySpawnHelperChecked = false;
|
||||||
|
|
||||||
export interface Cell {
|
export interface Cell {
|
||||||
char: string;
|
char: string;
|
||||||
@@ -83,6 +88,73 @@ export interface CreateTerminalOptions {
|
|||||||
name?: string;
|
name?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EnsureNodePtySpawnHelperExecutableOptions = {
|
||||||
|
packageRoot?: string;
|
||||||
|
platform?: NodeJS.Platform;
|
||||||
|
arch?: string;
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function resolveNodePtyPackageRoot(): string | null {
|
||||||
|
try {
|
||||||
|
const packageJsonPath = require.resolve("node-pty/package.json");
|
||||||
|
return dirname(packageJsonPath);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureExecutableBit(path: string): void {
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stat = statSync(path);
|
||||||
|
if (!stat.isFile()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// node-pty 1.1.0 shipped darwin prebuild spawn-helper without execute bit.
|
||||||
|
if ((stat.mode & 0o111) === 0o111) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chmodSync(path, stat.mode | 0o111);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureNodePtySpawnHelperExecutableForCurrentPlatform(
|
||||||
|
options: EnsureNodePtySpawnHelperExecutableOptions = {}
|
||||||
|
): void {
|
||||||
|
const platform = options.platform ?? process.platform;
|
||||||
|
if (platform !== "darwin") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (nodePtySpawnHelperChecked && !options.force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageRoot = options.packageRoot ?? resolveNodePtyPackageRoot();
|
||||||
|
if (!packageRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const arch = options.arch ?? process.arch;
|
||||||
|
|
||||||
|
const candidates = [
|
||||||
|
join(packageRoot, "build", "Release", "spawn-helper"),
|
||||||
|
join(packageRoot, "build", "Debug", "spawn-helper"),
|
||||||
|
join(packageRoot, "prebuilds", `darwin-${arch}`, "spawn-helper"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const candidate of candidates) {
|
||||||
|
try {
|
||||||
|
ensureExecutableBit(candidate);
|
||||||
|
} catch {
|
||||||
|
// best-effort hardening only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.force) {
|
||||||
|
nodePtySpawnHelperChecked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function extractCell(terminal: TerminalType, row: number, col: number): Cell {
|
function extractCell(terminal: TerminalType, row: number, col: number): Cell {
|
||||||
const buffer = terminal.buffer.active;
|
const buffer = terminal.buffer.active;
|
||||||
const line = buffer.getLine(row);
|
const line = buffer.getLine(row);
|
||||||
@@ -212,6 +284,8 @@ export async function createTerminal(options: CreateTerminalOptions): Promise<Te
|
|||||||
allowProposedApi: true,
|
allowProposedApi: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ensureNodePtySpawnHelperExecutableForCurrentPlatform();
|
||||||
|
|
||||||
// Create PTY
|
// Create PTY
|
||||||
const ptyProcess = pty.spawn(shell, [], {
|
const ptyProcess = pty.spawn(shell, [], {
|
||||||
name: "xterm-256color",
|
name: "xterm-256color",
|
||||||
|
|||||||
Reference in New Issue
Block a user