import type { ResourceTelemetryProcessCategory, ServerProcessDiagnosticsEntry, ServerProcessDiagnosticsResult, ServerProcessSignal, ServerSignalProcessResult, } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as ResourceTelemetry from "../resourceTelemetry/ResourceTelemetry.ts"; export class ProcessSignalFailed extends Schema.TaggedError()( "ProcessSignalFailed", { pid: Schema.Number, signal: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to signal process ${this.pid} with ${this.signal}.`; } } export class ProcessDiagnostics extends Context.Service< ProcessDiagnostics, { readonly read: Effect.Effect; readonly signal: (input: { readonly pid: number; readonly startTimeMs: number; readonly signal: ServerProcessSignal; }) => Effect.Effect; } >()("t3/diagnostics/ProcessDiagnostics") {} function formatElapsed(runTimeMs: number): string { const totalSeconds = Math.max(0, Math.floor(runTimeMs / 1_000)); const hours = Math.floor(totalSeconds / 3_600); const minutes = Math.floor((totalSeconds % 3_600) / 60); const seconds = totalSeconds % 60; return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}` : `${minutes}:${String(seconds).padStart(2, "0")}`; } function canSignalCategory(category: ResourceTelemetryProcessCategory): boolean { return ( category === "server-child" || category === "provider-root" || category === "terminal-root" ); } /** @public Service construction is part of the canonical Effect module API. */ export const make = Effect.fn("makeProcessDiagnostics")(function* () { const telemetry = yield* ResourceTelemetry.ResourceTelemetry; const refreshedTelemetry = telemetry.refresh.pipe(Effect.catch(() => telemetry.latest)); const read: ProcessDiagnostics["Service"]["read"] = refreshedTelemetry.pipe( Effect.map((snapshot) => { const processes = snapshot.processes .filter((entry) => canSignalCategory(entry.category)) .map((entry): ServerProcessDiagnosticsEntry => ({ pid: entry.identity.pid, startTimeMs: entry.identity.startTimeMs, ppid: entry.ppid, pgid: Option.none(), status: entry.status || "Unknown", cpuPercent: entry.cpuPercent, rssBytes: entry.residentBytes, elapsed: formatElapsed(entry.runTimeMs), command: entry.command || entry.name || "unknown", depth: Math.max(0, entry.depth - 1), childPids: entry.childPids, })); return { serverPid: process.pid, readAt: snapshot.readAt, processCount: processes.length, totalRssBytes: processes.reduce((total, entry) => total + entry.rssBytes, 0), totalCpuPercent: processes.reduce((total, entry) => total + entry.cpuPercent, 0), processes, error: Option.map(snapshot.health.native.lastError, (message) => ({ message })), }; }), ); const signal: ProcessDiagnostics["Service"]["signal"] = Effect.fn("ProcessDiagnostics.signal")( function* (input) { if (input.pid === process.pid) { return { pid: input.pid, signal: input.signal, signaled: false, message: Option.some("Refusing to signal the T3 server process."), }; } const current = yield* telemetry.refresh.pipe(Effect.option); if (Option.isNone(current)) { return { pid: input.pid, signal: input.signal, signaled: false, message: Option.some( `Could not refresh process ${input.pid}; refusing to signal a stale identity.`, ), }; } const selected = current.value.processes.find( (entry) => entry.identity.pid === input.pid && entry.identity.startTimeMs === input.startTimeMs, ); if (!selected) { return { pid: input.pid, signal: input.signal, signaled: false, message: Option.some( `Process ${input.pid} no longer matches the selected process identity.`, ), }; } if (!canSignalCategory(selected.category)) { return { pid: input.pid, signal: input.signal, signaled: false, message: Option.some(`Process ${input.pid} is not a signalable T3 backend descendant.`), }; } return yield* Effect.try({ try: () => { process.kill(input.pid, input.signal); return { pid: input.pid, signal: input.signal, signaled: true, message: Option.none(), }; }, catch: (cause) => new ProcessSignalFailed({ pid: input.pid, signal: input.signal, cause, }), }).pipe( Effect.catch((error) => Effect.succeed({ pid: input.pid, signal: input.signal, signaled: false, message: Option.some( error instanceof Error ? error.message : "Failed to signal process.", ), }), ), ); }, ); return ProcessDiagnostics.of({ read, signal }); }); export const layer = Layer.effect(ProcessDiagnostics, make());