SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
5cb63c1 verified
Raw History Blame Contribute Delete
6.76 kB
// PTY adapter wraps pseudo-terminal processes for the process supervisor.
import { createDeferredCore } from "../../../shared/deferred.js";
import { signalPtySessionTree } from "../../kill-tree.js";
import { prepareOomScoreAdjustedSpawn } from "../../linux-oom-score.js";
import {
readPtyTerminalName,
resolvePtyTerminalName,
setPtyTerminalName,
} from "../../pty-terminal-name.js";
import type { TerminalPtySubscription } from "../../terminal-pty.js";
import type { ManagedRunStdin, ProcessAdapterConstruction, SpawnProcessAdapter } from "../types.js";
import { toStringEnv } from "./env.js";
const FORCE_KILL_WAIT_FALLBACK_MS = 4000;
declare const WORKER_DEPLOY_BUILD: boolean;
type PtyAdapter = SpawnProcessAdapter;
export async function createPtyAdapter(
params: ProcessAdapterConstruction & {
shell: string;
args: string[];
cwd?: string;
env?: NodeJS.ProcessEnv;
cols?: number;
rows?: number;
name?: string;
},
): Promise<PtyAdapter> {
// Worker deploys are portable JavaScript artifacts; exec falls back to the child adapter
// instead of binding the Gateway host's native PTY binary into the bundle.
if (typeof WORKER_DEPLOY_BUILD === "boolean" && WORKER_DEPLOY_BUILD) {
throw new Error("PTY is unavailable in the portable worker runtime");
}
const { spawnTerminalPty } = await import("../../terminal-pty.js");
const baseEnv = params.env ? toStringEnv(params.env) : undefined;
const preparedSpawn = prepareOomScoreAdjustedSpawn(params.shell, params.args, { env: baseEnv });
const terminalName = resolvePtyTerminalName(
params.name ??
readPtyTerminalName(preparedSpawn.env, process.platform) ??
readPtyTerminalName(process.env, process.platform),
);
const spawnEnv = preparedSpawn.env
? toStringEnv(preparedSpawn.env)
: process.platform === "win32"
? toStringEnv(process.env)
: undefined;
// Unix node-pty rewrites child TERM from name; Windows forwards env unchanged.
if (spawnEnv) {
setPtyTerminalName({ env: spawnEnv, name: terminalName, platform: process.platform });
}
params.assertCurrent?.();
if (params.abortSignal?.aborted) {
throw new Error("PTY construction aborted");
}
const pty = await spawnTerminalPty(
{
file: preparedSpawn.command,
args: preparedSpawn.args,
cwd: params.cwd,
env: spawnEnv,
name: terminalName,
cols: params.cols ?? 120,
rows: params.rows ?? 30,
},
{
abortSignal: params.abortSignal,
assertCurrent: () => {
params.assertCurrent?.();
params.beforeSpawn?.();
},
},
);
try {
params.assertCurrent?.();
if (params.abortSignal?.aborted) {
throw new Error("PTY construction aborted");
}
} catch (error) {
try {
pty.kill();
} catch {
// The stale PTY may already have exited while the ownership check ran.
}
throw error;
}
const cleanup = createDeferredCore();
void cleanup.promise.catch(() => {});
params.onSpawnCleanup?.(cleanup.promise);
let dataListener: TerminalPtySubscription | null = null;
let exitListener: TerminalPtySubscription | null = null;
const completion = createDeferredCore<{
code: number | null;
signal: NodeJS.Signals | number | null;
}>();
let waitSettled = false;
let forceKillWaitFallbackTimer: NodeJS.Timeout | null = null;
let stdinDestroyed = false;
let stdinEnded = false;
const clearForceKillWaitFallback = () => {
if (!forceKillWaitFallbackTimer) {
return;
}
clearTimeout(forceKillWaitFallbackTimer);
forceKillWaitFallbackTimer = null;
};
const settleWait = (value: { code: number | null; signal: NodeJS.Signals | number | null }) => {
if (waitSettled) {
return;
}
waitSettled = true;
clearForceKillWaitFallback();
stdinDestroyed = true;
stdinEnded = true;
completion.resolve(value);
};
const scheduleForceKillWaitFallback = (signal: NodeJS.Signals) => {
clearForceKillWaitFallback();
// Some PTY hosts fail to emit onExit after kill; use a delayed fallback
// so callers can still unblock without marking termination immediately.
forceKillWaitFallbackTimer = setTimeout(() => {
cleanup.reject(new Error("PTY cleanup could not be confirmed before the kill deadline"));
settleWait({ code: null, signal });
}, FORCE_KILL_WAIT_FALLBACK_MS);
forceKillWaitFallbackTimer.unref();
};
exitListener =
pty.onExit((event) => {
cleanup.resolve();
const signal = event.signal && event.signal !== 0 ? event.signal : null;
settleWait({ code: event.exitCode ?? null, signal });
}) ?? null;
const stdin: ManagedRunStdin = {
get destroyed() {
return stdinDestroyed;
},
get writable() {
return !stdinDestroyed && !stdinEnded;
},
get writableEnded() {
return stdinEnded;
},
get writableFinished() {
return stdinEnded;
},
write: (data, cb) => {
try {
pty.write(data);
cb?.(null);
} catch (err) {
cb?.(err as Error);
}
},
end: () => {
try {
stdinEnded = true;
const eof = process.platform === "win32" ? "\x1a" : "\x04";
pty.write(eof);
} catch {
// ignore EOF errors
}
},
destroy: () => {
stdinDestroyed = true;
stdinEnded = true;
},
};
const onStdout = (listener: (chunk: string) => void) => {
dataListener =
pty.onData((chunk) => {
listener(chunk);
}) ?? null;
};
const onStderr = (_listener: (chunk: string) => void) => {
// PTY gives a unified output stream.
};
const wait = async () => await completion.promise;
const kill = (signal: NodeJS.Signals = "SIGKILL") => {
try {
if (
(signal === "SIGKILL" || signal === "SIGTERM") &&
typeof pty.pid === "number" &&
pty.pid > 0
) {
signalPtySessionTree(pty.pid, signal);
} else {
pty.kill(signal);
}
} catch {
// ignore kill errors
}
if (signal === "SIGKILL") {
scheduleForceKillWaitFallback(signal);
}
};
const dispose = () => {
stdinDestroyed = true;
stdinEnded = true;
try {
dataListener?.dispose();
} catch {
// ignore disposal errors
}
try {
exitListener?.dispose();
} catch {
// ignore disposal errors
}
clearForceKillWaitFallback();
dataListener = null;
exitListener = null;
settleWait({ code: null, signal: null });
};
return {
pid: pty.pid || undefined,
stdin,
oomScoreWrapperSelected: preparedSpawn.wrapped,
supportsRawOutput: false,
onStdout,
onStderr,
wait,
kill,
dispose,
};
}