import { DEFAULT_TERMINAL_ID, type TerminalEvent, type TerminalSessionSnapshot, type TerminalSessionStatus,} from "@t3tools/contracts";import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";import * as DateTime from "effect/DateTime";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as Equal from "effect/Equal";import * as Exit from "effect/Exit";import * as Fiber from "effect/Fiber";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Semaphore from "effect/Semaphore";import * as SynchronizedRef from "effect/SynchronizedRef"; import { ServerConfig } from "../../config.ts";import { increment, terminalRestartsTotal, terminalSessionsTotal,} from "../../observability/Metrics.ts";import * as ProcessRunner from "../../processRunner.ts";import { TerminalCwdError, TerminalHistoryError, TerminalManager, TerminalNotRunningError, TerminalSessionLookupError, type TerminalManagerShape,} from "../Services/Manager.ts";import { PtyAdapter, PtySpawnError, type PtyAdapterShape, type PtyExitEvent, type PtyProcess,} from "../Services/PTY.ts"; const DEFAULT_HISTORY_LINE_LIMIT = 5_000;const DEFAULT_PERSIST_DEBOUNCE_MS = 40;const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000;const DEFAULT_PROCESS_KILL_GRACE_MS = 1_000;const DEFAULT_MAX_RETAINED_INACTIVE_SESSIONS = 128;const DEFAULT_OPEN_COLS = 120;const DEFAULT_OPEN_ROWS = 30;const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]);const nowIso = Effect.map(DateTime.now, DateTime.formatIso); class TerminalSubprocessCheckError extends Schema.TaggedErrorClass<TerminalSubprocessCheckError>()( "TerminalSubprocessCheckError", { message: Schema.String, cause: Schema.optional(Schema.Defect), terminalPid: Schema.Number, command: Schema.Literals(["powershell", "pgrep", "ps"]), },) {} class TerminalProcessSignalError extends Schema.TaggedErrorClass<TerminalProcessSignalError>()( "TerminalProcessSignalError", { message: Schema.String, cause: Schema.optional(Schema.Defect), signal: Schema.Literals(["SIGTERM", "SIGKILL"]), },) {} interface TerminalSubprocessChecker { ( terminalPid: number, ): Effect.Effect<boolean, TerminalSubprocessCheckError, ProcessRunner.ProcessRunner>;} interface ShellCandidate { shell: string; args?: string[];} interface TerminalStartInput { threadId: string; terminalId: string; cwd: string; worktreePath?: string | null; cols: number; rows: number; env?: Record<string, string>;} interface TerminalSessionState { threadId: string; terminalId: string; cwd: string; worktreePath: string | null; status: TerminalSessionStatus; pid: number | null; history: string; pendingHistoryControlSequence: string; pendingProcessEvents: Array<PendingProcessEvent>; pendingProcessEventIndex: number; processEventDrainRunning: boolean; exitCode: number | null; exitSignal: number | null; updatedAt: string; cols: number; rows: number; process: PtyProcess | null; unsubscribeData: (() => void) | null; unsubscribeExit: (() => void) | null; hasRunningSubprocess: boolean; runtimeEnv: Record<string, string> | null;} interface PersistHistoryRequest { history: string; immediate: boolean;} type PendingProcessEvent = { type: "output"; data: string } | { type: "exit"; event: PtyExitEvent }; type DrainProcessEventAction = | { type: "idle" } | { type: "output"; threadId: string; terminalId: string; history: string | null; data: string; } | { type: "exit"; process: PtyProcess | null; threadId: string; terminalId: string; exitCode: number | null; exitSignal: number | null; }; interface TerminalManagerState { sessions: Map<string, TerminalSessionState>; killFibers: Map<PtyProcess, Fiber.Fiber<void, never>>;} function snapshot(session: TerminalSessionState): TerminalSessionSnapshot { return { threadId: session.threadId, terminalId: session.terminalId, cwd: session.cwd, worktreePath: session.worktreePath, status: session.status, pid: session.pid, history: session.history, exitCode: session.exitCode, exitSignal: session.exitSignal, updatedAt: session.updatedAt, };} function cleanupProcessHandles(session: TerminalSessionState): void { session.unsubscribeData?.(); session.unsubscribeData = null; session.unsubscribeExit?.(); session.unsubscribeExit = null;} function enqueueProcessEvent( session: TerminalSessionState, expectedPid: number, event: PendingProcessEvent,): boolean { if (!session.process || session.status !== "running" || session.pid !== expectedPid) { return false; } session.pendingProcessEvents.push(event); if (session.processEventDrainRunning) { return false; } session.processEventDrainRunning = true; return true;} function defaultShellResolver( platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env,): string { if (platform === "win32") { return "pwsh.exe"; } return env.SHELL ?? "bash";} function normalizeShellCommand( value: string | undefined, platform: NodeJS.Platform = process.platform,): string | null { if (!value) return null; const trimmed = value.trim(); if (trimmed.length === 0) return null; if (platform === "win32") { return trimmed; } const firstToken = trimmed.split(/\s+/g)[0]?.trim(); if (!firstToken) return null; return firstToken.replace(/^['"]|['"]$/g, "");} function basenameForPlatform(command: string, platform: NodeJS.Platform): string { const normalized = platform === "win32" ? command.replaceAll("/", "\\") : command.replaceAll("\\", "/"); const parts = normalized .split(platform === "win32" ? /\\+/ : /\/+/) .filter((part) => part.length > 0); return parts.at(-1) ?? normalized;} function joinWindowsPath(...parts: ReadonlyArray<string>): string { return parts .map((part, index) => { if (index === 0) return part.replace(/[\\/]+$/g, ""); return part.replace(/^[\\/]+|[\\/]+$/g, ""); }) .filter((part) => part.length > 0) .join("\\");} function shellCandidateFromCommand( command: string | null, platform: NodeJS.Platform = process.platform,): ShellCandidate | null { if (!command || command.length === 0) return null; const shellName = basenameForPlatform(command, platform).toLowerCase(); if (platform === "win32" && (shellName === "pwsh.exe" || shellName === "powershell.exe")) { return { shell: command, args: ["-NoLogo"] }; } if (platform !== "win32" && shellName === "zsh") { return { shell: command, args: ["-o", "nopromptsp"] }; } return { shell: command };} function windowsSystemRoot(env: NodeJS.ProcessEnv): string { return env.SystemRoot?.trim() || env.windir?.trim() || "C:\\Windows";} function windowsPowerShellPath(env: NodeJS.ProcessEnv): string { return joinWindowsPath( windowsSystemRoot(env), "System32", "WindowsPowerShell", "v1.0", "powershell.exe", );} function windowsCmdPath(env: NodeJS.ProcessEnv): string { return joinWindowsPath(windowsSystemRoot(env), "System32", "cmd.exe");} function formatShellCandidate(candidate: ShellCandidate): string { if (!candidate.args || candidate.args.length === 0) return candidate.shell; return `${candidate.shell} ${candidate.args.join(" ")}`;} function uniqueShellCandidates(candidates: Array<ShellCandidate | null>): ShellCandidate[] { const seen = new Set<string>(); const ordered: ShellCandidate[] = []; for (const candidate of candidates) { if (!candidate) continue; const key = formatShellCandidate(candidate); if (seen.has(key)) continue; seen.add(key); ordered.push(candidate); } return ordered;} function resolveShellCandidates( shellResolver: () => string, platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env,): ShellCandidate[] { const requested = shellCandidateFromCommand( normalizeShellCommand(shellResolver(), platform), platform, ); if (platform === "win32") { return uniqueShellCandidates([ requested, shellCandidateFromCommand("pwsh.exe", platform), shellCandidateFromCommand(windowsPowerShellPath(env), platform), shellCandidateFromCommand("powershell.exe", platform), shellCandidateFromCommand(env.ComSpec ?? null, platform), shellCandidateFromCommand(windowsCmdPath(env), platform), shellCandidateFromCommand("cmd.exe", platform), ]); } return uniqueShellCandidates([ requested, shellCandidateFromCommand(normalizeShellCommand(env.SHELL, platform), platform), shellCandidateFromCommand("/bin/zsh", platform), shellCandidateFromCommand("/bin/bash", platform), shellCandidateFromCommand("/bin/sh", platform), shellCandidateFromCommand("zsh", platform), shellCandidateFromCommand("bash", platform), shellCandidateFromCommand("sh", platform), ]);} function isRetryableShellSpawnError(error: PtySpawnError): boolean { const queue: unknown[] = [error]; const seen = new Set<unknown>(); const messages: string[] = []; while (queue.length > 0) { const current = queue.shift(); if (!current || seen.has(current)) { continue; } seen.add(current); if (typeof current === "string") { messages.push(current); continue; } if (current instanceof Error) { messages.push(current.message); if (current.cause) { queue.push(current.cause); } continue; } if (typeof current === "object") { const value = current as { message?: unknown; cause?: unknown }; if (typeof value.message === "string") { messages.push(value.message); } if (value.cause) { queue.push(value.cause); } } } const message = messages.join(" ").toLowerCase(); return ( message.includes("posix_spawnp failed") || message.includes("enoent") || message.includes("not found") || message.includes("file not found") || message.includes("no such file") );} function checkWindowsSubprocessActivity( terminalPid: number,): Effect.Effect<boolean, TerminalSubprocessCheckError, ProcessRunner.ProcessRunner> { const command = [ `$children = Get-CimInstance Win32_Process -Filter "ParentProcessId = ${terminalPid}" -ErrorAction SilentlyContinue`, "if ($children) { exit 0 }", "exit 1", ].join("; "); return Effect.gen(function* () { const processRunner = yield* ProcessRunner.ProcessRunner; return yield* processRunner.run({ command: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-Command", command], timeout: "1500 millis", maxOutputBytes: 32_768, outputMode: "truncate", shell: process.platform === "win32", timeoutBehavior: "timedOutResult", }); }).pipe( Effect.map((result) => result.code === 0), Effect.mapError( (cause) => new TerminalSubprocessCheckError({ message: "Failed to check Windows terminal subprocess activity.", cause, terminalPid, command: "powershell", }), ), );} const checkPosixSubprocessActivity = Effect.fn("terminal.checkPosixSubprocessActivity")(function* ( terminalPid: number,): Effect.fn.Return<boolean, TerminalSubprocessCheckError, ProcessRunner.ProcessRunner> { const processRunner = yield* ProcessRunner.ProcessRunner; const runPgrep = processRunner .run({ command: "pgrep", args: ["-P", String(terminalPid)], timeout: "1 second", maxOutputBytes: 32_768, outputMode: "truncate", timeoutBehavior: "timedOutResult", }) .pipe( Effect.mapError( (cause) => new TerminalSubprocessCheckError({ message: "Failed to inspect terminal subprocesses with pgrep.", cause, terminalPid, command: "pgrep", }), ), ); const runPs = processRunner .run({ command: "ps", args: ["-eo", "pid=,ppid="], timeout: "1 second", maxOutputBytes: 262_144, outputMode: "truncate", timeoutBehavior: "timedOutResult", }) .pipe( Effect.mapError( (cause) => new TerminalSubprocessCheckError({ message: "Failed to inspect terminal subprocesses with ps.", cause, terminalPid, command: "ps", }), ), ); const pgrepResult = yield* Effect.exit(runPgrep); if (pgrepResult._tag === "Success") { if (pgrepResult.value.code === 0) { return pgrepResult.value.stdout.trim().length > 0; } if (pgrepResult.value.code === 1) { return false; } } const psResult = yield* Effect.exit(runPs); if (psResult._tag === "Failure" || psResult.value.code !== 0) { return false; } for (const line of psResult.value.stdout.split(/\r?\n/g)) { const [pidRaw, ppidRaw] = line.trim().split(/\s+/g); const pid = Number(pidRaw); const ppid = Number(ppidRaw); if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue; if (ppid === terminalPid) { return true; } } return false;}); const defaultSubprocessChecker = Effect.fn("terminal.defaultSubprocessChecker")(function* ( terminalPid: number,): Effect.fn.Return<boolean, TerminalSubprocessCheckError, ProcessRunner.ProcessRunner> { if (!Number.isInteger(terminalPid) || terminalPid <= 0) { return false; } if (process.platform === "win32") { return yield* checkWindowsSubprocessActivity(terminalPid); } return yield* checkPosixSubprocessActivity(terminalPid);}); function capHistory(history: string, maxLines: number): string { if (history.length === 0) return history; const hasTrailingNewline = history.endsWith("\n"); const lines = history.split("\n"); if (hasTrailingNewline) { lines.pop(); } if (lines.length <= maxLines) return history; const capped = lines.slice(lines.length - maxLines).join("\n"); return hasTrailingNewline ? `${capped}\n` : capped;} function isCsiFinalByte(codePoint: number): boolean { return codePoint >= 0x40 && codePoint <= 0x7e;} Lines 1–500 of 1972. Load more
import { DEFAULT_TERMINAL_ID, type TerminalEvent, type TerminalSessionSnapshot, type TerminalSessionStatus, } from "@t3tools/contracts"; import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as Equal from "effect/Equal"; import * as Exit from "effect/Exit"; import * as Fiber from "effect/Fiber"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; import * as SynchronizedRef from "effect/SynchronizedRef"; import { ServerConfig } from "../../config.ts"; import { increment, terminalRestartsTotal, terminalSessionsTotal, } from "../../observability/Metrics.ts"; import * as ProcessRunner from "../../processRunner.ts"; import { TerminalCwdError, TerminalHistoryError, TerminalManager, TerminalNotRunningError, TerminalSessionLookupError, type TerminalManagerShape, } from "../Services/Manager.ts"; import { PtyAdapter, PtySpawnError, type PtyAdapterShape, type PtyExitEvent, type PtyProcess, } from "../Services/PTY.ts"; const DEFAULT_HISTORY_LINE_LIMIT = 5_000; const DEFAULT_PERSIST_DEBOUNCE_MS = 40; const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000; const DEFAULT_PROCESS_KILL_GRACE_MS = 1_000; const DEFAULT_MAX_RETAINED_INACTIVE_SESSIONS = 128; const DEFAULT_OPEN_COLS = 120; const DEFAULT_OPEN_ROWS = 30; const TERMINAL_ENV_BLOCKLIST = new Set(["PORT", "ELECTRON_RENDERER_PORT", "ELECTRON_RUN_AS_NODE"]); const nowIso = Effect.map(DateTime.now, DateTime.formatIso); class TerminalSubprocessCheckError extends Schema.TaggedErrorClass<TerminalSubprocessCheckError>()( "TerminalSubprocessCheckError", { message: Schema.String, cause: Schema.optional(Schema.Defect), terminalPid: Schema.Number, command: Schema.Literals(["powershell", "pgrep", "ps"]), }, ) {} class TerminalProcessSignalError extends Schema.TaggedErrorClass<TerminalProcessSignalError>()( "TerminalProcessSignalError", { message: Schema.String, cause: Schema.optional(Schema.Defect), signal: Schema.Literals(["SIGTERM", "SIGKILL"]), }, ) {} interface TerminalSubprocessChecker { ( terminalPid: number, ): Effect.Effect<boolean, TerminalSubprocessCheckError, ProcessRunner.ProcessRunner>; } interface ShellCandidate { shell: string; args?: string[]; } interface TerminalStartInput { threadId: string; terminalId: string; cwd: string; worktreePath?: string | null; cols: number; rows: number; env?: Record<string, string>; } interface TerminalSessionState { threadId: string; terminalId: string; cwd: string; worktreePath: string | null; status: TerminalSessionStatus; pid: number | null; history: string; pendingHistoryControlSequence: string; pendingProcessEvents: Array<PendingProcessEvent>; pendingProcessEventIndex: number; processEventDrainRunning: boolean; exitCode: number | null; exitSignal: number | null; updatedAt: string; cols: number; rows: number; process: PtyProcess | null; unsubscribeData: (() => void) | null; unsubscribeExit: (() => void) | null; hasRunningSubprocess: boolean; runtimeEnv: Record<string, string> | null; } interface PersistHistoryRequest { history: string; immediate: boolean; } type PendingProcessEvent = { type: "output"; data: string } | { type: "exit"; event: PtyExitEvent }; type DrainProcessEventAction = | { type: "idle" } | { type: "output"; threadId: string; terminalId: string; history: string | null; data: string; } | { type: "exit"; process: PtyProcess | null; threadId: string; terminalId: string; exitCode: number | null; exitSignal: number | null; }; interface TerminalManagerState { sessions: Map<string, TerminalSessionState>; killFibers: Map<PtyProcess, Fiber.Fiber<void, never>>; } function snapshot(session: TerminalSessionState): TerminalSessionSnapshot { return { threadId: session.threadId, terminalId: session.terminalId, cwd: session.cwd, worktreePath: session.worktreePath, status: session.status, pid: session.pid, history: session.history, exitCode: session.exitCode, exitSignal: session.exitSignal, updatedAt: session.updatedAt, }; } function cleanupProcessHandles(session: TerminalSessionState): void { session.unsubscribeData?.(); session.unsubscribeData = null; session.unsubscribeExit?.(); session.unsubscribeExit = null; } function enqueueProcessEvent( session: TerminalSessionState, expectedPid: number, event: PendingProcessEvent, ): boolean { if (!session.process || session.status !== "running" || session.pid !== expectedPid) { return false; } session.pendingProcessEvents.push(event); if (session.processEventDrainRunning) { return false; } session.processEventDrainRunning = true; return true; } function defaultShellResolver( platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env, ): string { if (platform === "win32") { return "pwsh.exe"; } return env.SHELL ?? "bash"; } function normalizeShellCommand( value: string | undefined, platform: NodeJS.Platform = process.platform, ): string | null { if (!value) return null; const trimmed = value.trim(); if (trimmed.length === 0) return null; if (platform === "win32") { return trimmed; } const firstToken = trimmed.split(/\s+/g)[0]?.trim(); if (!firstToken) return null; return firstToken.replace(/^['"]|['"]$/g, ""); } function basenameForPlatform(command: string, platform: NodeJS.Platform): string { const normalized = platform === "win32" ? command.replaceAll("/", "\\") : command.replaceAll("\\", "/"); const parts = normalized .split(platform === "win32" ? /\\+/ : /\/+/) .filter((part) => part.length > 0); return parts.at(-1) ?? normalized; } function joinWindowsPath(...parts: ReadonlyArray<string>): string { return parts .map((part, index) => { if (index === 0) return part.replace(/[\\/]+$/g, ""); return part.replace(/^[\\/]+|[\\/]+$/g, ""); }) .filter((part) => part.length > 0) .join("\\"); } function shellCandidateFromCommand( command: string | null, platform: NodeJS.Platform = process.platform, ): ShellCandidate | null { if (!command || command.length === 0) return null; const shellName = basenameForPlatform(command, platform).toLowerCase(); if (platform === "win32" && (shellName === "pwsh.exe" || shellName === "powershell.exe")) { return { shell: command, args: ["-NoLogo"] }; } if (platform !== "win32" && shellName === "zsh") { return { shell: command, args: ["-o", "nopromptsp"] }; } return { shell: command }; } function windowsSystemRoot(env: NodeJS.ProcessEnv): string { return env.SystemRoot?.trim() || env.windir?.trim() || "C:\\Windows"; } function windowsPowerShellPath(env: NodeJS.ProcessEnv): string { return joinWindowsPath( windowsSystemRoot(env), "System32", "WindowsPowerShell", "v1.0", "powershell.exe", ); } function windowsCmdPath(env: NodeJS.ProcessEnv): string { return joinWindowsPath(windowsSystemRoot(env), "System32", "cmd.exe"); } function formatShellCandidate(candidate: ShellCandidate): string { if (!candidate.args || candidate.args.length === 0) return candidate.shell; return `${candidate.shell} ${candidate.args.join(" ")}`; } function uniqueShellCandidates(candidates: Array<ShellCandidate | null>): ShellCandidate[] { const seen = new Set<string>(); const ordered: ShellCandidate[] = []; for (const candidate of candidates) { if (!candidate) continue; const key = formatShellCandidate(candidate); if (seen.has(key)) continue; seen.add(key); ordered.push(candidate); } return ordered; } function resolveShellCandidates( shellResolver: () => string, platform: NodeJS.Platform = process.platform, env: NodeJS.ProcessEnv = process.env, ): ShellCandidate[] { const requested = shellCandidateFromCommand( normalizeShellCommand(shellResolver(), platform), platform, ); if (platform === "win32") { return uniqueShellCandidates([ requested, shellCandidateFromCommand("pwsh.exe", platform), shellCandidateFromCommand(windowsPowerShellPath(env), platform), shellCandidateFromCommand("powershell.exe", platform), shellCandidateFromCommand(env.ComSpec ?? null, platform), shellCandidateFromCommand(windowsCmdPath(env), platform), shellCandidateFromCommand("cmd.exe", platform), ]); } return uniqueShellCandidates([ requested, shellCandidateFromCommand(normalizeShellCommand(env.SHELL, platform), platform), shellCandidateFromCommand("/bin/zsh", platform), shellCandidateFromCommand("/bin/bash", platform), shellCandidateFromCommand("/bin/sh", platform), shellCandidateFromCommand("zsh", platform), shellCandidateFromCommand("bash", platform), shellCandidateFromCommand("sh", platform), ]); } function isRetryableShellSpawnError(error: PtySpawnError): boolean { const queue: unknown[] = [error]; const seen = new Set<unknown>(); const messages: string[] = []; while (queue.length > 0) { const current = queue.shift(); if (!current || seen.has(current)) { continue; } seen.add(current); if (typeof current === "string") { messages.push(current); continue; } if (current instanceof Error) { messages.push(current.message); if (current.cause) { queue.push(current.cause); } continue; } if (typeof current === "object") { const value = current as { message?: unknown; cause?: unknown }; if (typeof value.message === "string") { messages.push(value.message); } if (value.cause) { queue.push(value.cause); } } } const message = messages.join(" ").toLowerCase(); return ( message.includes("posix_spawnp failed") || message.includes("enoent") || message.includes("not found") || message.includes("file not found") || message.includes("no such file") ); } function checkWindowsSubprocessActivity( terminalPid: number, ): Effect.Effect<boolean, TerminalSubprocessCheckError, ProcessRunner.ProcessRunner> { const command = [ `$children = Get-CimInstance Win32_Process -Filter "ParentProcessId = ${terminalPid}" -ErrorAction SilentlyContinue`, "if ($children) { exit 0 }", "exit 1", ].join("; "); return Effect.gen(function* () { const processRunner = yield* ProcessRunner.ProcessRunner; return yield* processRunner.run({ command: "powershell.exe", args: ["-NoProfile", "-NonInteractive", "-Command", command], timeout: "1500 millis", maxOutputBytes: 32_768, outputMode: "truncate", shell: process.platform === "win32", timeoutBehavior: "timedOutResult", }); }).pipe( Effect.map((result) => result.code === 0), Effect.mapError( (cause) => new TerminalSubprocessCheckError({ message: "Failed to check Windows terminal subprocess activity.", cause, terminalPid, command: "powershell", }), ), ); } const checkPosixSubprocessActivity = Effect.fn("terminal.checkPosixSubprocessActivity")(function* ( terminalPid: number, ): Effect.fn.Return<boolean, TerminalSubprocessCheckError, ProcessRunner.ProcessRunner> { const processRunner = yield* ProcessRunner.ProcessRunner; const runPgrep = processRunner .run({ command: "pgrep", args: ["-P", String(terminalPid)], timeout: "1 second", maxOutputBytes: 32_768, outputMode: "truncate", timeoutBehavior: "timedOutResult", }) .pipe( Effect.mapError( (cause) => new TerminalSubprocessCheckError({ message: "Failed to inspect terminal subprocesses with pgrep.", cause, terminalPid, command: "pgrep", }), ), ); const runPs = processRunner .run({ command: "ps", args: ["-eo", "pid=,ppid="], timeout: "1 second", maxOutputBytes: 262_144, outputMode: "truncate", timeoutBehavior: "timedOutResult", }) .pipe( Effect.mapError( (cause) => new TerminalSubprocessCheckError({ message: "Failed to inspect terminal subprocesses with ps.", cause, terminalPid, command: "ps", }), ), ); const pgrepResult = yield* Effect.exit(runPgrep); if (pgrepResult._tag === "Success") { if (pgrepResult.value.code === 0) { return pgrepResult.value.stdout.trim().length > 0; } if (pgrepResult.value.code === 1) { return false; } } const psResult = yield* Effect.exit(runPs); if (psResult._tag === "Failure" || psResult.value.code !== 0) { return false; } for (const line of psResult.value.stdout.split(/\r?\n/g)) { const [pidRaw, ppidRaw] = line.trim().split(/\s+/g); const pid = Number(pidRaw); const ppid = Number(ppidRaw); if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue; if (ppid === terminalPid) { return true; } } return false; }); const defaultSubprocessChecker = Effect.fn("terminal.defaultSubprocessChecker")(function* ( terminalPid: number, ): Effect.fn.Return<boolean, TerminalSubprocessCheckError, ProcessRunner.ProcessRunner> { if (!Number.isInteger(terminalPid) || terminalPid <= 0) { return false; } if (process.platform === "win32") { return yield* checkWindowsSubprocessActivity(terminalPid); } return yield* checkPosixSubprocessActivity(terminalPid); }); function capHistory(history: string, maxLines: number): string { if (history.length === 0) return history; const hasTrailingNewline = history.endsWith("\n"); const lines = history.split("\n"); if (hasTrailingNewline) { lines.pop(); } if (lines.length <= maxLines) return history; const capped = lines.slice(lines.length - maxLines).join("\n"); return hasTrailingNewline ? `${capped}\n` : capped; } function isCsiFinalByte(codePoint: number): boolean { return codePoint >= 0x40 && codePoint <= 0x7e; }