Julius MarmingeT3 Code Mobile [WIP] (#2013)b3e8c035 months agoHistory
2399 lines · 76.2 KB · TypeScript
RawDownload
import {  DEFAULT_TERMINAL_ID,  type TerminalAttachInput,  type TerminalAttachStreamEvent,  type TerminalEvent,  type TerminalMetadataStreamEvent,  type TerminalOpenInput,  type TerminalSessionSnapshot,  type TerminalSessionStatus,  type TerminalSummary,} from "@t3tools/contracts";import { makeKeyedCoalescingWorker } from "@t3tools/shared/KeyedCoalescingWorker";import { getTerminalLabel } from "@t3tools/shared/terminalLabels";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);const MAX_TERMINAL_LABEL_LENGTH = 128; 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 TerminalSubprocessInspectResult {  readonly hasRunningSubprocess: boolean;  readonly childCommand: string | null;} interface TerminalSubprocessInspector {  (    terminalPid: number,  ): Effect.Effect<TerminalSubprocessInspectResult, TerminalSubprocessCheckError>;} 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;  eventSequence: number;  cols: number;  rows: number;  process: PtyProcess | null;  unsubscribeData: (() => void) | null;  unsubscribeExit: (() => void) | null;  hasRunningSubprocess: boolean;  /** Normalized child command name when `hasRunningSubprocess`; cleared when idle. */  childCommandLabel: string | null;  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;      sequence: number;      history: string | null;      data: string;    }  | {      type: "exit";      process: PtyProcess | null;      threadId: string;      terminalId: string;      sequence: number;      exitCode: number | null;      exitSignal: number | null;    }; interface TerminalManagerState {  sessions: Map<string, TerminalSessionState>;  killFibers: Map<PtyProcess, Fiber.Fiber<void, never>>;} function truncateTerminalWireLabel(value: string): string {  if (value.length <= MAX_TERMINAL_LABEL_LENGTH) return value;  return value.slice(0, MAX_TERMINAL_LABEL_LENGTH);} function normalizeChildCommandName(raw: string, platform: NodeJS.Platform): string | null {  let trimmed = raw.trim();  if (trimmed.length === 0) return null;  if (    (trimmed.startsWith("[") && trimmed.endsWith("]")) ||    (trimmed.startsWith("(") && trimmed.endsWith(")"))  ) {    trimmed = trimmed.slice(1, -1).trim();  }  const firstToken = (trimmed.split(/\s+/)[0] ?? trimmed).trim();  if (firstToken.length === 0) return null;  const separators = platform === "win32" ? /[\\/]/ : /\//;  const base = firstToken.split(separators).at(-1) ?? firstToken;  const withoutExe =    platform === "win32" && base.toLowerCase().endsWith(".exe") ? base.slice(0, -4) : base;  return withoutExe.length > 0 ? withoutExe : null;} function terminalWireLabel(session: TerminalSessionState): string {  if (session.hasRunningSubprocess && session.childCommandLabel) {    const trimmed = session.childCommandLabel.trim();    if (trimmed.length > 0) {      return truncateTerminalWireLabel(trimmed);    }  }  return truncateTerminalWireLabel(getTerminalLabel(session.terminalId));} 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,    label: terminalWireLabel(session),    updatedAt: session.updatedAt,    sequence: session.eventSequence,  };} function summary(session: TerminalSessionState): TerminalSummary {  return {    threadId: session.threadId,    terminalId: session.terminalId,    cwd: session.cwd,    worktreePath: session.worktreePath,    status: session.status,    pid: session.pid,    exitCode: session.exitCode,    exitSignal: session.exitSignal,    hasRunningSubprocess: session.hasRunningSubprocess,    label: terminalWireLabel(session),    updatedAt: session.updatedAt,  };} function shouldPublishTerminalMetadataEvent(event: TerminalEvent): boolean {  switch (event.type) {    case "started":    case "restarted":    case "exited":    case "closed":    case "error":    case "activity":      return true;    case "output":    case "cleared":      return false;  }} function terminalEventToAttachEvent(event: TerminalEvent): TerminalAttachStreamEvent | null {  switch (event.type) {    case "started":      return {        type: "snapshot",        snapshot: event.snapshot,      };    case "output":    case "exited":    case "closed":    case "error":    case "cleared":    case "restarted":    case "activity":      return event;  }} function isDuplicateAttachSnapshotEvent(  event: TerminalEvent,  initialSnapshot: TerminalSessionSnapshot,) {  return typeof event.sequence === "number" && typeof initialSnapshot.sequence === "number"    ? event.sequence <= initialSnapshot.sequence    : event.type === "started" &&        event.snapshot.threadId === initialSnapshot.threadId &&        event.snapshot.terminalId === initialSnapshot.terminalId &&        event.snapshot.updatedAt <= initialSnapshot.updatedAt;} function advanceEventSequence(session: TerminalSessionState): {  readonly updatedAt: string;  readonly sequence: number;} {  const updatedAt = DateTime.formatIso(DateTime.nowUnsafe());  session.eventSequence += 1;  session.updatedAt = updatedAt;  return { updatedAt, sequence: session.eventSequence };} 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 parseFirstChildPidFromPgrep(stdout: string): number | null {  for (const line of stdout.split(/\r?\n/g)) {    const n = Number.parseInt(line.trim(), 10);    if (Number.isInteger(n) && n > 0) {      return n;    }  }  return null;} function windowsInspectSubprocess(

Lines 1–500 of 2399.