From 39d5fc504123c7919068c649e5a7e54db5a1034d Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Fri, 4 Sep 2026 01:41:01 +0530 Subject: [PATCH 1/7] fix(server): use native Windows process snapshots --- apps/server/package.json | 3 + apps/server/src/terminal/Manager.ts | 90 ++++++++++------------- pnpm-lock.yaml | 19 +++++ pnpm-workspace.yaml | 1 + scripts/build-desktop-artifact.ts | 18 ++++- scripts/lib/cli-external-packages.test.ts | 12 ++- scripts/lib/cli-external-packages.ts | 1 + 7 files changed, 90 insertions(+), 54 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index ca74368348ea..274578242115 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -48,6 +48,9 @@ "effect-codex-app-server": "workspace:*", "vite-plus": "catalog:" }, + "optionalDependencies": { + "@vscode/windows-process-tree": "0.8.0" + }, "engines": { "node": "^22.16 || ^23.11 || >=24.10" } diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 64c2dbb913fb..fed1434a476d 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -6,6 +6,7 @@ * * @module TerminalManager */ +import type { IProcessInfo } from "@vscode/windows-process-tree"; import { DEFAULT_TERMINAL_ID, TerminalCwdError, @@ -77,6 +78,7 @@ export { const DEFAULT_HISTORY_LINE_LIMIT = 5_000; const DEFAULT_PERSIST_DEBOUNCE_MS = 40; const DEFAULT_SUBPROCESS_POLL_INTERVAL_MS = 1_000; +const MAX_SUBPROCESS_POLL_INTERVAL_MS = 60_000; const DEFAULT_PROCESS_KILL_GRACE_MS = 1_000; const DEFAULT_MAX_RETAINED_INACTIVE_SESSIONS = 128; const DEFAULT_OPEN_COLS = 120; @@ -89,7 +91,7 @@ class TerminalSubprocessCheckError extends Schema.TaggedErrorClass, +): TerminalProcessTableSnapshot { const childrenByParent = new Map(); const commandById = new Map(); - for (const line of stdout.split(/\r?\n/g)) { - const [pidRaw, parentPidRaw, nameRaw] = line.trim().split("|", 3); - const pid = Number(pidRaw); - const parentPid = Number(parentPidRaw); + for (const process of processes) { + const { pid, ppid: parentPid, name } = process; if (!Number.isInteger(pid) || !Number.isInteger(parentPid)) continue; - commandById.set(pid, nameRaw?.trim() ?? ""); + commandById.set(pid, name.trim()); const children = childrenByParent.get(parentPid) ?? []; children.push(pid); childrenByParent.set(parentPid, children); @@ -741,46 +743,19 @@ const posixProcessTableSnapshot = Effect.fn("terminal.posixProcessTableSnapshot" }); const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnapshot")( - function* (): Effect.fn.Return< - TerminalProcessTableSnapshot, - TerminalSubprocessCheckError, - ProcessRunner.ProcessRunner - > { - const command = - 'Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.Name)" }'; - const processRunner = yield* ProcessRunner.ProcessRunner; - const result = yield* processRunner - .run({ - // powershell.exe is a real executable — never spawn it through cmd.exe - // shell mode, which would re-tokenize the `-Command` payload (pipes, - // semicolons) before PowerShell ever sees it. - command: "powershell.exe", - args: ["-NoProfile", "-NonInteractive", "-Command", command], - timeout: "1500 millis", - maxOutputBytes: 262_144, - outputMode: "truncate", - timeoutBehavior: "timedOutResult", - }) - .pipe( - Effect.mapError( - (cause) => - new TerminalSubprocessCheckError({ - cause, - command: "powershell", - }), - ), - ); - if (result.code !== 0 || result.timedOut || result.stdoutTruncated) { - // Not authoritative: an empty or partial table would mark every terminal - // idle and clear its registered process ids. Failing skips the tick. - return yield* new TerminalSubprocessCheckError({ - command: "powershell", - exitCode: result.code, - timedOut: result.timedOut, - stdoutTruncated: result.stdoutTruncated, - }); - } - return parseWindowsProcessTable(result.stdout); + function* (): Effect.fn.Return { + const processes = yield* Effect.tryPromise({ + try: async () => { + const { getAllProcesses } = await import("@vscode/windows-process-tree"); + return new Promise>((resolve) => getAllProcesses(resolve)); + }, + catch: (cause) => + new TerminalSubprocessCheckError({ + cause, + command: "windows-process-tree", + }), + }); + return windowsProcessTableSnapshotFromProcesses(processes); }, ); @@ -2008,7 +1983,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func ); if (runningSessions.length === 0) { - return; + return true; } const inspectorOption = yield* acquireSubprocessInspector.pipe( @@ -2021,7 +1996,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func ); if (Option.isNone(inspectorOption)) { - return; + return false; } const subprocessInspector = inspectorOption.value; @@ -2093,6 +2068,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func concurrency: "unbounded", discard: true, }); + return true; }); const hasRunningSessions = readManagerState.pipe( @@ -2101,14 +2077,26 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func ), ); + let subprocessSnapshotFailureCount = 0; yield* Effect.forever( hasRunningSessions.pipe( Effect.flatMap((active) => active ? pollSubprocessActivity().pipe( - Effect.flatMap(() => Effect.sleep(subprocessPollIntervalMs)), + Effect.flatMap((snapshotSucceeded) => { + subprocessSnapshotFailureCount = snapshotSucceeded + ? 0 + : Math.min(subprocessSnapshotFailureCount + 1, 30); + const delayMs = Math.min( + subprocessPollIntervalMs * 2 ** subprocessSnapshotFailureCount, + MAX_SUBPROCESS_POLL_INTERVAL_MS, + ); + return Effect.sleep(delayMs); + }), ) - : Effect.sleep(subprocessPollIntervalMs), + : Effect.sync(() => { + subprocessSnapshotFailureCount = 0; + }).pipe(Effect.flatMap(() => Effect.sleep(subprocessPollIntervalMs))), ), ), ).pipe(Effect.forkIn(workerScope)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76cf816327be..b6eca2b3deb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -542,6 +542,10 @@ importers: vite-plus: specifier: 'catalog:' version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + optionalDependencies: + '@vscode/windows-process-tree': + specifier: 0.8.0 + version: 0.8.0 apps/web: dependencies: @@ -5046,6 +5050,9 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@vscode/windows-process-tree@0.8.0': + resolution: {integrity: sha512-TI+h2GRwX+igD/YYJMQQVAFcsCNSg7Te2yYxQpKMzwto5RsJ8d2KKgOeur/p/6sAOQwZvopiTDOClyTHEn9MhQ==} + '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} @@ -8230,6 +8237,10 @@ packages: resolution: {integrity: sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==} engines: {node: '>=22.12.0'} + node-addon-api@7.1.0: + resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} + engines: {node: ^16 || ^18 || >= 20} + node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -14864,6 +14875,11 @@ snapshots: '@vscode/l10n@0.0.18': {} + '@vscode/windows-process-tree@0.8.0': + dependencies: + node-addon-api: 7.1.0 + optional: true + '@xmldom/xmldom@0.8.13': {} '@xmldom/xmldom@0.9.10': {} @@ -18743,6 +18759,9 @@ snapshots: dependencies: semver: 7.8.5 + node-addon-api@7.1.0: + optional: true + node-addon-api@7.1.1: {} node-api-version@0.2.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7d2f9f998617..c4a75855547c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,7 @@ packages: # true = allowed to run build scripts; false mirrors the pnpm 10 behavior # where anything outside onlyBuiltDependencies was silently not built. allowBuilds: + "@vscode/windows-process-tree": true browser-tabs-lock: false bufferutil: false core-js: false diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index f4bbad9d5407..165aef1a33b0 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -3259,8 +3259,24 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( cause, }), }); + const resolvedServerOptionalDependencies = yield* Effect.try({ + try: () => + resolveCatalogDependencies( + serverPackageJson.optionalDependencies ?? {}, + workspaceCatalog, + "apps/server", + ), + catch: (cause) => + new DesktopBuildDependencyResolutionError({ + kind: "server-production", + manifestPath: "apps/server/package.json", + cause, + }), + }); const resolvedServerRuntimeExternalDependencies = selectCliRuntimeExternalDependencies( - resolvedServerDependencies, + options.platform === "win" + ? { ...resolvedServerDependencies, ...resolvedServerOptionalDependencies } + : resolvedServerDependencies, ); const resolvedDesktopRuntimeDependencies = yield* Effect.try({ try: () => resolveDesktopRuntimeDependencies(desktopPackageJson.dependencies, workspaceCatalog), diff --git a/scripts/lib/cli-external-packages.test.ts b/scripts/lib/cli-external-packages.test.ts index 754cd646f17d..5068246fceda 100644 --- a/scripts/lib/cli-external-packages.test.ts +++ b/scripts/lib/cli-external-packages.test.ts @@ -44,6 +44,7 @@ describe("shouldBundleCliDependency", () => { it("leaves native addons and their dlopen wrappers external", () => { for (const id of [ "node-pty", + "@vscode/windows-process-tree", "ffi-rs", "@yuuang/ffi-rs-win32-x64-msvc", "@ff-labs/fff-node", @@ -76,18 +77,25 @@ describe("selectCliRuntimeExternalDependencies", () => { "@ff-labs/fff-node": "2.0.0", effect: "3.0.0", "node-pty": "4.0.0", + "@vscode/windows-process-tree": "0.8.0", }), { "@ff-labs/fff-node": "2.0.0", "node-pty": "4.0.0", + "@vscode/windows-process-tree": "0.8.0", }, ); }); it("selects every external root declared by the server", () => { assert.deepStrictEqual( - Object.keys(selectCliRuntimeExternalDependencies(serverPackageJson.dependencies)).sort(), - ["@ff-labs/fff-node", "msgpackr-extract", "node-pty"], + Object.keys( + selectCliRuntimeExternalDependencies({ + ...serverPackageJson.dependencies, + ...serverPackageJson.optionalDependencies, + }), + ).sort(), + ["@ff-labs/fff-node", "@vscode/windows-process-tree", "msgpackr-extract", "node-pty"], ); }); }); diff --git a/scripts/lib/cli-external-packages.ts b/scripts/lib/cli-external-packages.ts index d7a89bc408a4..b66f30ff11ab 100644 --- a/scripts/lib/cli-external-packages.ts +++ b/scripts/lib/cli-external-packages.ts @@ -27,6 +27,7 @@ */ export const CLI_RUNTIME_EXTERNAL_PREFIXES = [ "node-pty", + "@vscode/windows-process-tree", "ffi-rs", "@yuuang/", "@ff-labs/", From 3ecf145e134dd53cfe6a13bd0bf18a1a1fba3839 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Fri, 4 Sep 2026 01:53:19 +0530 Subject: [PATCH 2/7] fix(server): defer Windows process module resolution --- apps/server/src/terminal/Manager.ts | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index fed1434a476d..a92a8cc3baa7 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -6,7 +6,6 @@ * * @module TerminalManager */ -import type { IProcessInfo } from "@vscode/windows-process-tree"; import { DEFAULT_TERMINAL_ID, TerminalCwdError, @@ -47,6 +46,7 @@ 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 Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; @@ -626,6 +626,16 @@ interface TerminalProcessTableSnapshot { readonly commandById: ReadonlyMap; } +interface WindowsProcessInfo { + readonly pid: number; + readonly ppid: number; + readonly name: string; +} + +type GetAllWindowsProcesses = ( + callback: (processes: ReadonlyArray) => void, +) => void; + function parsePosixProcessTable(stdout: string): TerminalProcessTableSnapshot { const childrenByParent = new Map(); const commandById = new Map(); @@ -646,7 +656,7 @@ function parsePosixProcessTable(stdout: string): TerminalProcessTableSnapshot { } function windowsProcessTableSnapshotFromProcesses( - processes: ReadonlyArray, + processes: ReadonlyArray, ): TerminalProcessTableSnapshot { const childrenByParent = new Map(); const commandById = new Map(); @@ -746,8 +756,15 @@ const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnaps function* (): Effect.fn.Return { const processes = yield* Effect.tryPromise({ try: async () => { - const { getAllProcesses } = await import("@vscode/windows-process-tree"); - return new Promise>((resolve) => getAllProcesses(resolve)); + const packageName: string = "@vscode/windows-process-tree"; + const loaded: unknown = await import(packageName); + if (!Predicate.isObject(loaded) || !Predicate.isFunction(loaded.getAllProcesses)) { + throw new TypeError(`${packageName} does not export getAllProcesses`); + } + const getAllProcesses = loaded.getAllProcesses as GetAllWindowsProcesses; + return new Promise>((resolve) => + getAllProcesses(resolve), + ); }, catch: (cause) => new TerminalSubprocessCheckError({ From f905cd163a5ad7c3d29574a5221cd6e93974ea11 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Fri, 4 Sep 2026 02:34:45 +0530 Subject: [PATCH 3/7] test(server): cover Windows process snapshot recovery --- apps/server/src/terminal/Manager.test.ts | 42 +++++++++++++++ apps/server/src/terminal/Manager.ts | 68 ++++++++++++++---------- 2 files changed, 83 insertions(+), 27 deletions(-) diff --git a/apps/server/src/terminal/Manager.test.ts b/apps/server/src/terminal/Manager.test.ts index 47d91e4516ec..573b6f427d74 100644 --- a/apps/server/src/terminal/Manager.test.ts +++ b/apps/server/src/terminal/Manager.test.ts @@ -210,6 +210,7 @@ interface CreateManagerOptions { readonly childCommand: string | null; readonly processIds: ReadonlyArray; }>; + windowsProcessTreeModuleLoader?: () => Promise; subprocessPollIntervalMs?: number; processKillGraceMs?: number; maxRetainedInactiveSessions?: number; @@ -248,6 +249,9 @@ const createManager = ( ...(options.subprocessInspector !== undefined ? { subprocessInspector: options.subprocessInspector } : {}), + ...(options.windowsProcessTreeModuleLoader !== undefined + ? { windowsProcessTreeModuleLoader: options.windowsProcessTreeModuleLoader } + : {}), ...(options.subprocessPollIntervalMs !== undefined ? { subprocessPollIntervalMs: options.subprocessPollIntervalMs } : {}), @@ -1073,6 +1077,44 @@ it.layer( }), ); + it("calculates snapshot failure backoff and success reset delays", () => { + assert.equal(TerminalManager.subprocessSnapshotPollDelayMs(1_000, 0), 1_000); + assert.equal(TerminalManager.subprocessSnapshotPollDelayMs(1_000, 1), 2_000); + assert.equal(TerminalManager.subprocessSnapshotPollDelayMs(1_000, 2), 4_000); + assert.equal(TerminalManager.subprocessSnapshotPollDelayMs(1_000, 30), 60_000); + }); + + it.effect("loads Windows process snapshots through the injectable module boundary", () => + Effect.gen(function* () { + let loadCalls = 0; + const { manager, getEvents } = yield* createManager(5, { + subprocessPollIntervalMs: 20, + windowsProcessTreeModuleLoader: async () => { + loadCalls += 1; + return { + getAllProcesses: ( + callback: ( + processes: ReadonlyArray<{ pid: number; ppid: number; name: string }>, + ) => void, + ) => callback([{ pid: 100, ppid: 9000, name: "ping.exe" }]), + }; + }, + }).pipe(Effect.provide(withHostPlatform("win32"))); + + yield* manager.open(openInput()); + yield* waitFor( + Effect.map(getEvents, (events) => + events.some( + (event) => + event.type === "activity" && event.hasRunningSubprocess && event.label === "ping", + ), + ), + "1200 millis", + ); + expect(loadCalls).toBeGreaterThan(0); + }), + ); + it.effect("caps persisted history to configured line limit", () => Effect.gen(function* () { const { manager, ptyAdapter } = yield* createManager(3); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index a92a8cc3baa7..ec5eb8880af3 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -636,6 +636,22 @@ type GetAllWindowsProcesses = ( callback: (processes: ReadonlyArray) => void, ) => void; +type WindowsProcessTreeModuleLoader = () => Promise; + +export function subprocessSnapshotPollDelayMs( + pollIntervalMs: number, + failureCount: number, +): number { + return Math.min(pollIntervalMs * 2 ** failureCount, MAX_SUBPROCESS_POLL_INTERVAL_MS); +} + +const loadWindowsProcessTreeModule: WindowsProcessTreeModuleLoader = () => { + // This optional native dependency is installed only on Windows. Keeping the + // specifier widened lets other platforms typecheck and bundle without it. + const packageName: string = "@vscode/windows-process-tree"; + return import(packageName); +}; + function parsePosixProcessTable(stdout: string): TerminalProcessTableSnapshot { const childrenByParent = new Map(); const commandById = new Map(); @@ -752,29 +768,26 @@ const posixProcessTableSnapshot = Effect.fn("terminal.posixProcessTableSnapshot" return parsePosixProcessTable(result.stdout); }); -const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnapshot")( - function* (): Effect.fn.Return { - const processes = yield* Effect.tryPromise({ - try: async () => { - const packageName: string = "@vscode/windows-process-tree"; - const loaded: unknown = await import(packageName); - if (!Predicate.isObject(loaded) || !Predicate.isFunction(loaded.getAllProcesses)) { - throw new TypeError(`${packageName} does not export getAllProcesses`); - } - const getAllProcesses = loaded.getAllProcesses as GetAllWindowsProcesses; - return new Promise>((resolve) => - getAllProcesses(resolve), - ); - }, - catch: (cause) => - new TerminalSubprocessCheckError({ - cause, - command: "windows-process-tree", - }), - }); - return windowsProcessTableSnapshotFromProcesses(processes); - }, -); +const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnapshot")(function* ( + loadModule: WindowsProcessTreeModuleLoader = loadWindowsProcessTreeModule, +): Effect.fn.Return { + const processes = yield* Effect.tryPromise({ + try: async () => { + const loaded = await loadModule(); + if (!Predicate.isObject(loaded) || !Predicate.isFunction(loaded.getAllProcesses)) { + throw new TypeError("@vscode/windows-process-tree does not export getAllProcesses"); + } + const getAllProcesses = loaded.getAllProcesses as GetAllWindowsProcesses; + return new Promise>((resolve) => getAllProcesses(resolve)); + }, + catch: (cause) => + new TerminalSubprocessCheckError({ + cause, + command: "windows-process-tree", + }), + }); + return windowsProcessTableSnapshotFromProcesses(processes); +}); function capHistory(history: string, maxLines: number): string { if (history.length === 0) return history; @@ -1107,6 +1120,7 @@ interface TerminalManagerOptions { shellResolver?: () => string; env?: NodeJS.ProcessEnv; subprocessInspector?: TerminalSubprocessInspector; + windowsProcessTreeModuleLoader?: WindowsProcessTreeModuleLoader; subprocessPollIntervalMs?: number; processKillGraceMs?: number; maxRetainedInactiveSessions?: number; @@ -1156,7 +1170,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func // can exhaust the PID space on hosts with many sessions (#6332). const fetchProcessTableSnapshot = ( platform === "win32" - ? windowsProcessTableSnapshot() + ? windowsProcessTableSnapshot(options.windowsProcessTreeModuleLoader) : posixProcessTableSnapshot(yield* resolvePosixPsCommand()) ).pipe(Effect.provideService(ProcessRunner.ProcessRunner, processRunner)); const customSubprocessInspector = options.subprocessInspector; @@ -2104,9 +2118,9 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func subprocessSnapshotFailureCount = snapshotSucceeded ? 0 : Math.min(subprocessSnapshotFailureCount + 1, 30); - const delayMs = Math.min( - subprocessPollIntervalMs * 2 ** subprocessSnapshotFailureCount, - MAX_SUBPROCESS_POLL_INTERVAL_MS, + const delayMs = subprocessSnapshotPollDelayMs( + subprocessPollIntervalMs, + subprocessSnapshotFailureCount, ); return Effect.sleep(delayMs); }), From 34b84ea4cf11c506ecc5f61915aa431a81158e76 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Fri, 4 Sep 2026 02:44:56 +0530 Subject: [PATCH 4/7] fix(server): reject capped Windows process snapshots --- apps/server/src/terminal/Manager.test.ts | 23 ++++++++++++++++++++++- apps/server/src/terminal/Manager.ts | 10 +++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/apps/server/src/terminal/Manager.test.ts b/apps/server/src/terminal/Manager.test.ts index 573b6f427d74..afaebb789bb3 100644 --- a/apps/server/src/terminal/Manager.test.ts +++ b/apps/server/src/terminal/Manager.test.ts @@ -1087,6 +1087,7 @@ it.layer( it.effect("loads Windows process snapshots through the injectable module boundary", () => Effect.gen(function* () { let loadCalls = 0; + let returnCappedSnapshot = false; const { manager, getEvents } = yield* createManager(5, { subprocessPollIntervalMs: 20, windowsProcessTreeModuleLoader: async () => { @@ -1096,7 +1097,16 @@ it.layer( callback: ( processes: ReadonlyArray<{ pid: number; ppid: number; name: string }>, ) => void, - ) => callback([{ pid: 100, ppid: 9000, name: "ping.exe" }]), + ) => + callback( + returnCappedSnapshot + ? Array.from({ length: 1_024 }, (_, pid) => ({ + pid, + ppid: 0, + name: "process.exe", + })) + : [{ pid: 100, ppid: 9000, name: "ping.exe" }], + ), }; }, }).pipe(Effect.provide(withHostPlatform("win32"))); @@ -1112,6 +1122,17 @@ it.layer( "1200 millis", ); expect(loadCalls).toBeGreaterThan(0); + + const successfulLoadCalls = loadCalls; + returnCappedSnapshot = true; + yield* waitFor( + Effect.sync(() => loadCalls >= successfulLoadCalls + 3), + "1200 millis", + ); + + const activityEvents = (yield* getEvents).filter((event) => event.type === "activity"); + expect(activityEvents.length).toBeGreaterThan(0); + expect(activityEvents.every((event) => event.hasRunningSubprocess === true)).toBe(true); }), ); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index ec5eb8880af3..83749d15c80c 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -636,6 +636,8 @@ type GetAllWindowsProcesses = ( callback: (processes: ReadonlyArray) => void, ) => void; +const WINDOWS_PROCESS_SNAPSHOT_LIMIT = 1_024; + type WindowsProcessTreeModuleLoader = () => Promise; export function subprocessSnapshotPollDelayMs( @@ -778,7 +780,13 @@ const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnaps throw new TypeError("@vscode/windows-process-tree does not export getAllProcesses"); } const getAllProcesses = loaded.getAllProcesses as GetAllWindowsProcesses; - return new Promise>((resolve) => getAllProcesses(resolve)); + const processes = await new Promise>((resolve) => + getAllProcesses(resolve), + ); + if (processes.length >= WINDOWS_PROCESS_SNAPSHOT_LIMIT) { + throw new Error("Windows process snapshot reached the native enumeration limit"); + } + return processes; }, catch: (cause) => new TerminalSubprocessCheckError({ From 05f55b44b809f28bbae8072428bce0f46cab3916 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Fri, 4 Sep 2026 02:53:55 +0530 Subject: [PATCH 5/7] fix(server): report capped process snapshots --- apps/server/src/terminal/Manager.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 83749d15c80c..77798314ddb0 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -783,9 +783,6 @@ const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnaps const processes = await new Promise>((resolve) => getAllProcesses(resolve), ); - if (processes.length >= WINDOWS_PROCESS_SNAPSHOT_LIMIT) { - throw new Error("Windows process snapshot reached the native enumeration limit"); - } return processes; }, catch: (cause) => @@ -794,6 +791,13 @@ const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnaps command: "windows-process-tree", }), }); + if (processes.length >= WINDOWS_PROCESS_SNAPSHOT_LIMIT) { + // Not authoritative: a capped table would mark live terminals idle. + return yield* new TerminalSubprocessCheckError({ + command: "windows-process-tree", + stdoutTruncated: true, + }); + } return windowsProcessTableSnapshotFromProcesses(processes); }); From a0bc756caed6eafe932b59524131e124521db4aa Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Fri, 4 Sep 2026 03:23:10 +0530 Subject: [PATCH 6/7] refactor(server): reuse resource monitor process snapshots --- apps/server/package.json | 3 - .../diagnostics/ProcessDiagnostics.test.ts | 2 +- .../src/resourceTelemetry/Model.test.ts | 2 +- .../NativeTelemetryClient.test.ts | 10 +- .../NativeTelemetryClient.ts | 93 +++++++++++++- .../ResourceTelemetry.test.ts | 4 +- .../ResourceTelemetryHistory.test.ts | 2 +- apps/server/src/server.ts | 1 + apps/server/src/terminal/Manager.test.ts | 50 ++------ apps/server/src/terminal/Manager.ts | 119 +++++++++--------- docs/internals/resource-telemetry.md | 2 + native/resource-monitor/src/main.rs | 70 ++++++++++- packages/contracts/src/resourceTelemetry.ts | 26 +++- pnpm-lock.yaml | 19 --- pnpm-workspace.yaml | 1 - scripts/lib/cli-external-packages.test.ts | 6 +- scripts/lib/cli-external-packages.ts | 1 - 17 files changed, 277 insertions(+), 134 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index 274578242115..ca74368348ea 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -48,9 +48,6 @@ "effect-codex-app-server": "workspace:*", "vite-plus": "catalog:" }, - "optionalDependencies": { - "@vscode/windows-process-tree": "0.8.0" - }, "engines": { "node": "^22.16 || ^23.11 || >=24.10" } diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts index 2efa3375d275..5bcc74206893 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts @@ -19,7 +19,7 @@ function makeNativeSnapshot( processes: ResourceMonitorSnapshotEvent["processes"], ): ResourceMonitorSnapshotEvent { return { - version: 2, + version: 3, type: "snapshot", sequence: 1, sampledAtUnixMs: DateTime.toEpochMillis(DateTime.makeUnsafe("2026-05-05T10:00:00.000Z")), diff --git a/apps/server/src/resourceTelemetry/Model.test.ts b/apps/server/src/resourceTelemetry/Model.test.ts index 94690e3967bc..6f759ac9744f 100644 --- a/apps/server/src/resourceTelemetry/Model.test.ts +++ b/apps/server/src/resourceTelemetry/Model.test.ts @@ -39,7 +39,7 @@ function nativeSnapshot( sequence = 1, ): ResourceMonitorSnapshotEvent { return { - version: 2, + version: 3, type: "snapshot", sequence, sampledAtUnixMs, diff --git a/apps/server/src/resourceTelemetry/NativeTelemetryClient.test.ts b/apps/server/src/resourceTelemetry/NativeTelemetryClient.test.ts index 8a595bc8b480..8732f09e278b 100644 --- a/apps/server/src/resourceTelemetry/NativeTelemetryClient.test.ts +++ b/apps/server/src/resourceTelemetry/NativeTelemetryClient.test.ts @@ -83,7 +83,7 @@ describe("canCommandNativeTelemetrySidecar", () => { }); describe("NativeTelemetryRequestTimedOut", () => { - it("models history and sample request deadlines without a fabricated cause", () => { + it("models request deadlines without a fabricated cause", () => { const historyTimeout = new NativeTelemetryRequestTimedOut({ operation: "readHistory", timeoutMs: 15_000, @@ -92,6 +92,10 @@ describe("NativeTelemetryRequestTimedOut", () => { operation: "sampleNow", timeoutMs: 5_000, }); + const processTableTimeout = new NativeTelemetryRequestTimedOut({ + operation: "processTable", + timeoutMs: 5_000, + }); expect(historyTimeout.message).toBe( "Resource monitor 'readHistory' request timed out after 15000ms.", @@ -99,8 +103,12 @@ describe("NativeTelemetryRequestTimedOut", () => { expect(sampleTimeout.message).toBe( "Resource monitor 'sampleNow' request timed out after 5000ms.", ); + expect(processTableTimeout.message).toBe( + "Resource monitor 'processTable' request timed out after 5000ms.", + ); expect("cause" in historyTimeout).toBe(false); expect("cause" in sampleTimeout).toBe(false); + expect("cause" in processTableTimeout).toBe(false); }); }); diff --git a/apps/server/src/resourceTelemetry/NativeTelemetryClient.ts b/apps/server/src/resourceTelemetry/NativeTelemetryClient.ts index 232079d9dc9b..0299e813bb55 100644 --- a/apps/server/src/resourceTelemetry/NativeTelemetryClient.ts +++ b/apps/server/src/resourceTelemetry/NativeTelemetryClient.ts @@ -5,6 +5,7 @@ import type { ResourceMonitorEvent, ResourceMonitorExternalProcess, ResourceMonitorHelloEvent, + ResourceMonitorProcessTableEntry, ResourceMonitorSnapshotEvent, ResourceTelemetrySourceStatus, } from "@t3tools/contracts"; @@ -44,6 +45,7 @@ const BATTERY_SAMPLE_INTERVAL_MS = 5_000; const CONSTRAINED_SAMPLE_INTERVAL_MS = 15_000; const HANDSHAKE_TIMEOUT = Duration.seconds(5); const SAMPLE_REQUEST_TIMEOUT = Duration.seconds(5); +const PROCESS_TABLE_REQUEST_TIMEOUT = Duration.seconds(5); const HISTORY_REQUEST_TIMEOUT = Duration.seconds(15); const INITIAL_RESTART_DELAY = Duration.millis(500); const MAX_RESTART_DELAY = Duration.seconds(10); @@ -76,7 +78,7 @@ export class NativeTelemetryHandshakeTimedOut extends Schema.TaggedErrorClass()( "NativeTelemetryRequestTimedOut", { - operation: Schema.Literals(["readHistory", "sampleNow"]), + operation: Schema.Literals(["processTable", "readHistory", "sampleNow"]), timeoutMs: Schema.Number, }, ) { @@ -192,6 +194,10 @@ export class NativeTelemetryClient extends Context.Service< snapshot: HostPowerSnapshot, ) => Effect.Effect; readonly sampleNow: Effect.Effect; + readonly processTable: Effect.Effect< + ReadonlyArray, + NativeTelemetryClientError + >; readonly retry: Effect.Effect; readonly health: Effect.Effect; readonly subscribeHealth: Effect.Effect< @@ -386,6 +392,12 @@ export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(fu const pendingSamples = yield* Ref.make( new Map>(), ); + const pendingProcessTables = yield* Ref.make( + new Map< + string, + Deferred.Deferred, NativeTelemetryClientError> + >(), + ); const pendingHistories = yield* Ref.make(new Map()); const snapshots = yield* PubSub.sliding(8); const healthChanges = yield* PubSub.sliding(4); @@ -403,10 +415,14 @@ export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(fu const failPending = (error: NativeTelemetryClientError) => Effect.gen(function* () { const samples = yield* Ref.getAndSet(pendingSamples, new Map()); + const processTables = yield* Ref.getAndSet(pendingProcessTables, new Map()); const histories = yield* Ref.getAndSet(pendingHistories, new Map()); yield* Effect.forEach(samples.values(), (deferred) => Deferred.fail(deferred, error), { discard: true, }); + yield* Effect.forEach(processTables.values(), (deferred) => Deferred.fail(deferred, error), { + discard: true, + }); yield* Effect.forEach( histories.values(), (request) => Deferred.fail(request.deferred, error), @@ -485,6 +501,21 @@ export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(fu } } }); + case "processTable": + return Ref.modify(pendingProcessTables, (pending) => { + const next = new Map(pending); + const deferred = next.get(event.requestId); + next.delete(event.requestId); + return [Option.fromUndefinedOr(deferred), next] as const; + }).pipe( + Effect.flatMap( + Option.match({ + onNone: () => Effect.void, + onSome: (deferred) => Deferred.succeed(deferred, event.processes), + }), + ), + Effect.asVoid, + ); case "historyChunk": return Effect.gen(function* () { const latestSnapshot = event.snapshots.at(-1); @@ -940,6 +971,60 @@ export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(fu ); }); + const processTable: NativeTelemetryClient["Service"]["processTable"] = Effect.gen(function* () { + const current = yield* Ref.get(state); + if (!canCommandNativeTelemetrySidecar(current.status, Option.isSome(current.handle))) { + return yield* new NativeTelemetryUnavailable({ + reason: Option.getOrElse(current.lastError, () => "sidecar is not running"), + }); + } + + const requestId = yield* crypto.randomUUIDv4.pipe( + Effect.mapError( + (cause) => new NativeTelemetryCommandFailed({ operation: "createRequestId", cause }), + ), + ); + const deferred = yield* Deferred.make< + ReadonlyArray, + NativeTelemetryClientError + >(); + yield* Ref.update(pendingProcessTables, (pending) => { + const next = new Map(pending); + next.set(requestId, deferred); + return next; + }); + return yield* writeCommand(Option.getOrThrow(current.handle), { + version: RESOURCE_MONITOR_PROTOCOL_VERSION, + type: "processTable", + requestId, + }).pipe( + Effect.andThen( + Deferred.await(deferred).pipe( + Effect.timeoutOption(PROCESS_TABLE_REQUEST_TIMEOUT), + Effect.flatMap( + Option.match({ + onNone: () => + Effect.fail( + new NativeTelemetryRequestTimedOut({ + operation: "processTable", + timeoutMs: Duration.toMillis(PROCESS_TABLE_REQUEST_TIMEOUT), + }), + ), + onSome: Effect.succeed, + }), + ), + ), + ), + Effect.ensuring( + Ref.update(pendingProcessTables, (pending) => { + const next = new Map(pending); + next.delete(requestId); + return next; + }), + ), + ); + }); + const health = currentHealth; return NativeTelemetryClient.of({ @@ -961,6 +1046,7 @@ export const make = Effect.fn("resourceTelemetry.nativeTelemetryClient.make")(fu setExternalProcesses, setHostPowerState, sampleNow, + processTable, retry: Ref.get(state).pipe( Effect.flatMap((current) => !canRequestNativeTelemetryRetry(current.status, Option.isSome(current.handle)) @@ -1014,6 +1100,11 @@ export const layerTest = ( reason: "No resource monitor sample was configured for this test.", }), ), + processTable: Effect.fail( + new NativeTelemetryUnavailable({ + reason: "No resource monitor process table was configured for this test.", + }), + ), retry: Effect.succeed(false), health, subscribeHealth: diff --git a/apps/server/src/resourceTelemetry/ResourceTelemetry.test.ts b/apps/server/src/resourceTelemetry/ResourceTelemetry.test.ts index a96423607baf..9c371078332d 100644 --- a/apps/server/src/resourceTelemetry/ResourceTelemetry.test.ts +++ b/apps/server/src/resourceTelemetry/ResourceTelemetry.test.ts @@ -82,7 +82,7 @@ function nativeSnapshot(input: { }), ]; return { - version: 2, + version: 3, type: "snapshot", sequence: input.sequence, sampledAtUnixMs: input.sampledAtUnixMs, @@ -497,7 +497,7 @@ describe("ResourceTelemetry", () => { const nativeHealth = yield* Ref.make({ status: "healthy", hello: Option.some({ - version: 2, + version: 3, type: "hello", sidecarVersion: "0.1.0", sidecarPid: 9_000, diff --git a/apps/server/src/resourceTelemetry/ResourceTelemetryHistory.test.ts b/apps/server/src/resourceTelemetry/ResourceTelemetryHistory.test.ts index 879c83d86dee..fff4588c8468 100644 --- a/apps/server/src/resourceTelemetry/ResourceTelemetryHistory.test.ts +++ b/apps/server/src/resourceTelemetry/ResourceTelemetryHistory.test.ts @@ -64,7 +64,7 @@ function snapshot( }), ]; return { - version: 2, + version: 3, type: "snapshot", sequence, sampledAtUnixMs, diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 3a93adc6d761..1578e7916e87 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -363,6 +363,7 @@ const PortScannerLayerLive = PortScanner.layer.pipe(Layer.provide(ProcessRunner. const TerminalLayerLive = TerminalManager.layer.pipe( Layer.provide(PtyAdapterLive), Layer.provide(PortScannerLayerLive), + Layer.provide(NativeTelemetryLayerLive), ); const PreviewLayerLive = Layer.empty.pipe( diff --git a/apps/server/src/terminal/Manager.test.ts b/apps/server/src/terminal/Manager.test.ts index afaebb789bb3..1f59f23503b6 100644 --- a/apps/server/src/terminal/Manager.test.ts +++ b/apps/server/src/terminal/Manager.test.ts @@ -210,7 +210,10 @@ interface CreateManagerOptions { readonly childCommand: string | null; readonly processIds: ReadonlyArray; }>; - windowsProcessTreeModuleLoader?: () => Promise; + processTable?: Effect.Effect< + ReadonlyArray<{ readonly pid: number; readonly ppid: number; readonly name: string }>, + never + >; subprocessPollIntervalMs?: number; processKillGraceMs?: number; maxRetainedInactiveSessions?: number; @@ -249,9 +252,7 @@ const createManager = ( ...(options.subprocessInspector !== undefined ? { subprocessInspector: options.subprocessInspector } : {}), - ...(options.windowsProcessTreeModuleLoader !== undefined - ? { windowsProcessTreeModuleLoader: options.windowsProcessTreeModuleLoader } - : {}), + ...(options.processTable !== undefined ? { processTable: options.processTable } : {}), ...(options.subprocessPollIntervalMs !== undefined ? { subprocessPollIntervalMs: options.subprocessPollIntervalMs } : {}), @@ -1084,31 +1085,15 @@ it.layer( assert.equal(TerminalManager.subprocessSnapshotPollDelayMs(1_000, 30), 60_000); }); - it.effect("loads Windows process snapshots through the injectable module boundary", () => + it.effect("uses process snapshots from the resource monitor", () => Effect.gen(function* () { - let loadCalls = 0; - let returnCappedSnapshot = false; + let snapshotCalls = 0; const { manager, getEvents } = yield* createManager(5, { subprocessPollIntervalMs: 20, - windowsProcessTreeModuleLoader: async () => { - loadCalls += 1; - return { - getAllProcesses: ( - callback: ( - processes: ReadonlyArray<{ pid: number; ppid: number; name: string }>, - ) => void, - ) => - callback( - returnCappedSnapshot - ? Array.from({ length: 1_024 }, (_, pid) => ({ - pid, - ppid: 0, - name: "process.exe", - })) - : [{ pid: 100, ppid: 9000, name: "ping.exe" }], - ), - }; - }, + processTable: Effect.sync(() => { + snapshotCalls += 1; + return [{ pid: 100, ppid: 9000, name: "ping.exe" }]; + }), }).pipe(Effect.provide(withHostPlatform("win32"))); yield* manager.open(openInput()); @@ -1121,18 +1106,7 @@ it.layer( ), "1200 millis", ); - expect(loadCalls).toBeGreaterThan(0); - - const successfulLoadCalls = loadCalls; - returnCappedSnapshot = true; - yield* waitFor( - Effect.sync(() => loadCalls >= successfulLoadCalls + 3), - "1200 millis", - ); - - const activityEvents = (yield* getEvents).filter((event) => event.type === "activity"); - expect(activityEvents.length).toBeGreaterThan(0); - expect(activityEvents.every((event) => event.hasRunningSubprocess === true)).toBe(true); + expect(snapshotCalls).toBeGreaterThan(0); }), ); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 77798314ddb0..44ae34b8e838 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -26,6 +26,7 @@ import { type TerminalMetadataStreamEvent, type TerminalOpenInput, type TerminalResizeInput, + type ResourceMonitorProcessTableEntry, type TerminalRestartInput, type TerminalSessionSnapshot, type TerminalSessionStatus, @@ -46,7 +47,6 @@ 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 Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; import * as Scope from "effect/Scope"; import * as Semaphore from "effect/Semaphore"; @@ -60,6 +60,7 @@ import { } from "../observability/Metrics.ts"; import * as ProcessRunner from "../processRunner.ts"; import * as PortScanner from "../preview/PortScanner.ts"; +import * as NativeTelemetryClient from "../resourceTelemetry/NativeTelemetryClient.ts"; import * as PtyAdapter from "./PtyAdapter.ts"; export { @@ -91,7 +92,7 @@ class TerminalSubprocessCheckError extends Schema.TaggedErrorClass; } -interface WindowsProcessInfo { - readonly pid: number; - readonly ppid: number; - readonly name: string; -} - -type GetAllWindowsProcesses = ( - callback: (processes: ReadonlyArray) => void, -) => void; - -const WINDOWS_PROCESS_SNAPSHOT_LIMIT = 1_024; - -type WindowsProcessTreeModuleLoader = () => Promise; - export function subprocessSnapshotPollDelayMs( pollIntervalMs: number, failureCount: number, @@ -647,13 +634,6 @@ export function subprocessSnapshotPollDelayMs( return Math.min(pollIntervalMs * 2 ** failureCount, MAX_SUBPROCESS_POLL_INTERVAL_MS); } -const loadWindowsProcessTreeModule: WindowsProcessTreeModuleLoader = () => { - // This optional native dependency is installed only on Windows. Keeping the - // specifier widened lets other platforms typecheck and bundle without it. - const packageName: string = "@vscode/windows-process-tree"; - return import(packageName); -}; - function parsePosixProcessTable(stdout: string): TerminalProcessTableSnapshot { const childrenByParent = new Map(); const commandById = new Map(); @@ -673,8 +653,8 @@ function parsePosixProcessTable(stdout: string): TerminalProcessTableSnapshot { return { childrenByParent, commandById }; } -function windowsProcessTableSnapshotFromProcesses( - processes: ReadonlyArray, +function processTableSnapshotFromProcesses( + processes: ReadonlyArray, ): TerminalProcessTableSnapshot { const childrenByParent = new Map(); const commandById = new Map(); @@ -770,36 +750,48 @@ const posixProcessTableSnapshot = Effect.fn("terminal.posixProcessTableSnapshot" return parsePosixProcessTable(result.stdout); }); -const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnapshot")(function* ( - loadModule: WindowsProcessTreeModuleLoader = loadWindowsProcessTreeModule, -): Effect.fn.Return { - const processes = yield* Effect.tryPromise({ - try: async () => { - const loaded = await loadModule(); - if (!Predicate.isObject(loaded) || !Predicate.isFunction(loaded.getAllProcesses)) { - throw new TypeError("@vscode/windows-process-tree does not export getAllProcesses"); - } - const getAllProcesses = loaded.getAllProcesses as GetAllWindowsProcesses; - const processes = await new Promise>((resolve) => - getAllProcesses(resolve), +const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnapshot")( + function* (): Effect.fn.Return< + TerminalProcessTableSnapshot, + TerminalSubprocessCheckError, + ProcessRunner.ProcessRunner + > { + const processRunner = yield* ProcessRunner.ProcessRunner; + const command = + 'Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.Name)" }'; + const result = yield* processRunner + .run({ + command: "powershell.exe", + args: ["-NoProfile", "-NonInteractive", "-Command", command], + timeout: "1500 millis", + maxOutputBytes: 262_144, + outputMode: "truncate", + timeoutBehavior: "timedOutResult", + }) + .pipe( + Effect.mapError( + (cause) => new TerminalSubprocessCheckError({ cause, command: "powershell" }), + ), ); - return processes; - }, - catch: (cause) => - new TerminalSubprocessCheckError({ - cause, - command: "windows-process-tree", - }), - }); - if (processes.length >= WINDOWS_PROCESS_SNAPSHOT_LIMIT) { - // Not authoritative: a capped table would mark live terminals idle. - return yield* new TerminalSubprocessCheckError({ - command: "windows-process-tree", - stdoutTruncated: true, + if (result.code !== 0 || result.timedOut || result.stdoutTruncated) { + return yield* new TerminalSubprocessCheckError({ + command: "powershell", + exitCode: result.code, + timedOut: result.timedOut, + stdoutTruncated: result.stdoutTruncated, + }); + } + const processes = result.stdout.split(/\r?\n/g).flatMap((line) => { + const [pidRaw, ppidRaw, name = ""] = line.trim().split("|", 3); + const pid = Number(pidRaw); + const ppid = Number(ppidRaw); + return Number.isInteger(pid) && pid > 0 && Number.isInteger(ppid) + ? [{ pid, ppid, name }] + : []; }); - } - return windowsProcessTableSnapshotFromProcesses(processes); -}); + return processTableSnapshotFromProcesses(processes); + }, +); function capHistory(history: string, maxLines: number): string { if (history.length === 0) return history; @@ -1132,7 +1124,10 @@ interface TerminalManagerOptions { shellResolver?: () => string; env?: NodeJS.ProcessEnv; subprocessInspector?: TerminalSubprocessInspector; - windowsProcessTreeModuleLoader?: WindowsProcessTreeModuleLoader; + processTable?: Effect.Effect< + ReadonlyArray, + TerminalSubprocessCheckError + >; subprocessPollIntervalMs?: number; processKillGraceMs?: number; maxRetainedInactiveSessions?: number; @@ -1151,9 +1146,15 @@ export const make = Effect.fn("TerminalManager.make")(function* () { const { terminalLogsDir } = yield* ServerConfig.ServerConfig; const ptyAdapter = yield* PtyAdapter.PtyAdapter; const portDiscovery = yield* PortScanner.PortDiscovery; + const nativeTelemetry = yield* NativeTelemetryClient.NativeTelemetryClient; return yield* makeWithOptions({ logsDir: terminalLogsDir, ptyAdapter, + processTable: nativeTelemetry.processTable.pipe( + Effect.mapError( + (cause) => new TerminalSubprocessCheckError({ cause, command: "resource-monitor" }), + ), + ), registerTerminalProcesses: portDiscovery.registerTerminalProcesses, unregisterTerminal: portDiscovery.unregisterTerminal, }); @@ -1180,11 +1181,17 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func // One process-table snapshot per poll tick, shared across every terminal. // Per-terminal `pgrep`/`ps` calls multiply spawn load by terminal count and // can exhaust the PID space on hosts with many sessions (#6332). - const fetchProcessTableSnapshot = ( + const fallbackProcessTableSnapshot = ( platform === "win32" - ? windowsProcessTableSnapshot(options.windowsProcessTreeModuleLoader) + ? windowsProcessTableSnapshot() : posixProcessTableSnapshot(yield* resolvePosixPsCommand()) ).pipe(Effect.provideService(ProcessRunner.ProcessRunner, processRunner)); + const fetchProcessTableSnapshot = options.processTable + ? options.processTable.pipe( + Effect.map(processTableSnapshotFromProcesses), + Effect.catch(() => fallbackProcessTableSnapshot), + ) + : fallbackProcessTableSnapshot; const customSubprocessInspector = options.subprocessInspector; const acquireSubprocessInspector: Effect.Effect< TerminalSubprocessInspector, diff --git a/docs/internals/resource-telemetry.md b/docs/internals/resource-telemetry.md index 0f3e6674ff79..cbc09ea14710 100644 --- a/docs/internals/resource-telemetry.md +++ b/docs/internals/resource-telemetry.md @@ -80,10 +80,12 @@ line on stdout: - `setSampleInterval` - `setStreaming` - `sampleNow` +- `processTable` - `readHistory` - `shutdown` - `hello` - `snapshot` +- `processTable` - `historyChunk` - `error` diff --git a/native/resource-monitor/src/main.rs b/native/resource-monitor/src/main.rs index 0596aea977cd..6845ea70a628 100644 --- a/native/resource-monitor/src/main.rs +++ b/native/resource-monitor/src/main.rs @@ -8,7 +8,7 @@ use sysinfo::{ MINIMUM_CPU_UPDATE_INTERVAL, Pid, ProcessRefreshKind, ProcessesToUpdate, System, UpdateKind, }; -const PROTOCOL_VERSION: u32 = 2; +const PROTOCOL_VERSION: u32 = 3; const MIN_SAMPLE_INTERVAL_MS: u64 = 250; const MAX_SAMPLE_INTERVAL_MS: u64 = 60_000; const PROCESS_START_TIME_PRECISION_MS: u64 = 1_000; @@ -66,6 +66,10 @@ enum Command { version: u32, request_id: String, }, + ProcessTable { + version: u32, + request_id: String, + }, ReadHistory { version: u32, request_id: String, @@ -84,6 +88,7 @@ impl Command { | Self::SetSampleInterval { version, .. } | Self::SetStreaming { version, .. } | Self::SampleNow { version, .. } + | Self::ProcessTable { version, .. } | Self::ReadHistory { version, .. } | Self::Shutdown { version } => *version, } @@ -146,6 +151,24 @@ struct ProcessSample { io_semantics: IoSemantics, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct ProcessTableEntry { + pid: u32, + ppid: u32, + name: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct ProcessTableEvent<'a> { + version: u32, + #[serde(rename = "type")] + event_type: &'static str, + request_id: &'a str, + processes: Vec, +} + impl ProcessSample { fn estimated_history_bytes(&self) -> usize { std::mem::size_of::() @@ -351,6 +374,29 @@ impl Collector { self.cpu_baseline_refreshed_at = Some(Instant::now()); } + fn process_table(&mut self) -> Vec { + self.system.refresh_processes_specifics( + ProcessesToUpdate::All, + true, + ProcessRefreshKind::nothing().without_tasks(), + ); + let mut processes = self + .system + .processes() + .iter() + .map(|(pid, process)| ProcessTableEntry { + pid: pid.as_u32(), + ppid: process.parent().map(Pid::as_u32).unwrap_or(0), + name: truncate_utf8( + process.name().to_string_lossy().into_owned(), + MAX_PROCESS_NAME_BYTES, + ), + }) + .collect::>(); + processes.sort_by_key(|process| process.pid); + processes + } + fn sample(&mut self, config: &CollectorConfig, request_id: Option) -> SnapshotEvent { if let Some(delay) = remaining_cpu_measurement_delay(self.cpu_baseline_refreshed_at.take(), Instant::now()) @@ -815,6 +861,15 @@ fn main() -> io::Result<()> { )?; } } + Command::ProcessTable { request_id, .. } => { + let event = ProcessTableEvent { + version: PROTOCOL_VERSION, + event_type: "processTable", + request_id: &request_id, + processes: collector.process_table(), + }; + write_event(&mut writer, &event)?; + } Command::ReadHistory { request_id, window_ms, @@ -892,7 +947,7 @@ mod tests { #[test] fn decodes_protocol_commands() { let configure = serde_json::from_str::( - r#"{"version":2,"type":"configure","rootPid":42,"sampleIntervalMs":1000,"externalProcesses":[{"pid":7}]}"#, + r#"{"version":3,"type":"configure","rootPid":42,"sampleIntervalMs":1000,"externalProcesses":[{"pid":7}]}"#, ) .expect("configure command"); @@ -912,7 +967,7 @@ mod tests { } let read_history = serde_json::from_str::( - r#"{"version":2,"type":"readHistory","requestId":"history-1","windowMs":60000}"#, + r#"{"version":3,"type":"readHistory","requestId":"history-1","windowMs":60000}"#, ) .expect("read history command"); assert!(matches!( @@ -923,6 +978,15 @@ mod tests { .. } if request_id == "history-1" )); + + let process_table = serde_json::from_str::( + r#"{"version":3,"type":"processTable","requestId":"processes-1"}"#, + ) + .expect("process table command"); + assert!(matches!( + process_table, + Command::ProcessTable { request_id, .. } if request_id == "processes-1" + )); } #[test] diff --git a/packages/contracts/src/resourceTelemetry.ts b/packages/contracts/src/resourceTelemetry.ts index 3ec1e4de3ef4..fc519c3bc64f 100644 --- a/packages/contracts/src/resourceTelemetry.ts +++ b/packages/contracts/src/resourceTelemetry.ts @@ -4,7 +4,7 @@ import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchema import { HostPowerSnapshot } from "./background.ts"; import { DesktopUpdateStateSchema } from "./ipc.ts"; -export const RESOURCE_MONITOR_PROTOCOL_VERSION = 2 as const; +export const RESOURCE_MONITOR_PROTOCOL_VERSION = 3 as const; export const ResourceTelemetryIoSemantics = Schema.Literals([ "storage", @@ -102,6 +102,13 @@ export const ResourceMonitorSampleNowCommand = Schema.Struct({ }); export type ResourceMonitorSampleNowCommand = typeof ResourceMonitorSampleNowCommand.Type; +export const ResourceMonitorProcessTableCommand = Schema.Struct({ + version: Schema.Literal(RESOURCE_MONITOR_PROTOCOL_VERSION), + type: Schema.Literal("processTable"), + requestId: TrimmedNonEmptyString, +}); +export type ResourceMonitorProcessTableCommand = typeof ResourceMonitorProcessTableCommand.Type; + export const ResourceMonitorSetSampleIntervalCommand = Schema.Struct({ version: Schema.Literal(RESOURCE_MONITOR_PROTOCOL_VERSION), type: Schema.Literal("setSampleInterval"), @@ -137,6 +144,7 @@ export const ResourceMonitorCommand = Schema.Union([ ResourceMonitorSetSampleIntervalCommand, ResourceMonitorSetStreamingCommand, ResourceMonitorSampleNowCommand, + ResourceMonitorProcessTableCommand, ResourceMonitorReadHistoryCommand, ResourceMonitorShutdownCommand, ]); @@ -168,6 +176,21 @@ export const ResourceMonitorSnapshotEvent = Schema.Struct({ }); export type ResourceMonitorSnapshotEvent = typeof ResourceMonitorSnapshotEvent.Type; +export const ResourceMonitorProcessTableEntry = Schema.Struct({ + pid: PositiveInt, + ppid: NonNegativeInt, + name: Schema.String, +}); +export type ResourceMonitorProcessTableEntry = typeof ResourceMonitorProcessTableEntry.Type; + +export const ResourceMonitorProcessTableEvent = Schema.Struct({ + version: Schema.Literal(RESOURCE_MONITOR_PROTOCOL_VERSION), + type: Schema.Literal("processTable"), + requestId: TrimmedNonEmptyString, + processes: Schema.Array(ResourceMonitorProcessTableEntry), +}); +export type ResourceMonitorProcessTableEvent = typeof ResourceMonitorProcessTableEvent.Type; + export const ResourceMonitorHistoryChunkEvent = Schema.Struct({ version: Schema.Literal(RESOURCE_MONITOR_PROTOCOL_VERSION), type: Schema.Literal("historyChunk"), @@ -189,6 +212,7 @@ export type ResourceMonitorErrorEvent = typeof ResourceMonitorErrorEvent.Type; export const ResourceMonitorEvent = Schema.Union([ ResourceMonitorHelloEvent, ResourceMonitorSnapshotEvent, + ResourceMonitorProcessTableEvent, ResourceMonitorHistoryChunkEvent, ResourceMonitorErrorEvent, ]); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6eca2b3deb1..76cf816327be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -542,10 +542,6 @@ importers: vite-plus: specifier: 'catalog:' version: 0.3.0(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) - optionalDependencies: - '@vscode/windows-process-tree': - specifier: 0.8.0 - version: 0.8.0 apps/web: dependencies: @@ -5050,9 +5046,6 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} - '@vscode/windows-process-tree@0.8.0': - resolution: {integrity: sha512-TI+h2GRwX+igD/YYJMQQVAFcsCNSg7Te2yYxQpKMzwto5RsJ8d2KKgOeur/p/6sAOQwZvopiTDOClyTHEn9MhQ==} - '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} @@ -8237,10 +8230,6 @@ packages: resolution: {integrity: sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==} engines: {node: '>=22.12.0'} - node-addon-api@7.1.0: - resolution: {integrity: sha512-mNcltoe1R8o7STTegSOHdnJNN7s5EUvhoS7ShnTHDyOSd+8H+UdWODq6qSv67PjC8Zc5JRT8+oLAMCr0SIXw7g==} - engines: {node: ^16 || ^18 || >= 20} - node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -14875,11 +14864,6 @@ snapshots: '@vscode/l10n@0.0.18': {} - '@vscode/windows-process-tree@0.8.0': - dependencies: - node-addon-api: 7.1.0 - optional: true - '@xmldom/xmldom@0.8.13': {} '@xmldom/xmldom@0.9.10': {} @@ -18759,9 +18743,6 @@ snapshots: dependencies: semver: 7.8.5 - node-addon-api@7.1.0: - optional: true - node-addon-api@7.1.1: {} node-api-version@0.2.1: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c4a75855547c..7d2f9f998617 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,7 +9,6 @@ packages: # true = allowed to run build scripts; false mirrors the pnpm 10 behavior # where anything outside onlyBuiltDependencies was silently not built. allowBuilds: - "@vscode/windows-process-tree": true browser-tabs-lock: false bufferutil: false core-js: false diff --git a/scripts/lib/cli-external-packages.test.ts b/scripts/lib/cli-external-packages.test.ts index 5068246fceda..8ab3c95f41fd 100644 --- a/scripts/lib/cli-external-packages.test.ts +++ b/scripts/lib/cli-external-packages.test.ts @@ -44,7 +44,6 @@ describe("shouldBundleCliDependency", () => { it("leaves native addons and their dlopen wrappers external", () => { for (const id of [ "node-pty", - "@vscode/windows-process-tree", "ffi-rs", "@yuuang/ffi-rs-win32-x64-msvc", "@ff-labs/fff-node", @@ -77,12 +76,10 @@ describe("selectCliRuntimeExternalDependencies", () => { "@ff-labs/fff-node": "2.0.0", effect: "3.0.0", "node-pty": "4.0.0", - "@vscode/windows-process-tree": "0.8.0", }), { "@ff-labs/fff-node": "2.0.0", "node-pty": "4.0.0", - "@vscode/windows-process-tree": "0.8.0", }, ); }); @@ -92,10 +89,9 @@ describe("selectCliRuntimeExternalDependencies", () => { Object.keys( selectCliRuntimeExternalDependencies({ ...serverPackageJson.dependencies, - ...serverPackageJson.optionalDependencies, }), ).sort(), - ["@ff-labs/fff-node", "@vscode/windows-process-tree", "msgpackr-extract", "node-pty"], + ["@ff-labs/fff-node", "msgpackr-extract", "node-pty"], ); }); }); diff --git a/scripts/lib/cli-external-packages.ts b/scripts/lib/cli-external-packages.ts index b66f30ff11ab..d7a89bc408a4 100644 --- a/scripts/lib/cli-external-packages.ts +++ b/scripts/lib/cli-external-packages.ts @@ -27,7 +27,6 @@ */ export const CLI_RUNTIME_EXTERNAL_PREFIXES = [ "node-pty", - "@vscode/windows-process-tree", "ffi-rs", "@yuuang/", "@ff-labs/", From 24ff64e65fc80184dcfb14b71f34361ead544464 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Fri, 4 Sep 2026 03:57:13 +0530 Subject: [PATCH 7/7] fix(server): back off fallback polling and drop pid 0 snapshots Apply exponential backoff when the sidecar fails and the spawned fallback serves instead, so a stalled sidecar no longer hot-loops PowerShell. Skip pid 0 in the Rust process table so one kernel entry cannot fail the whole event decode. Revert the Windows sidecar packaging added for the removed native dependency. --- apps/server/src/terminal/Manager.test.ts | 59 ++++++++++++++++++++++ apps/server/src/terminal/Manager.ts | 60 ++++++++++++++++++----- native/resource-monitor/src/main.rs | 24 ++++++--- scripts/build-desktop-artifact.ts | 18 +------ scripts/lib/cli-external-packages.test.ts | 6 +-- 5 files changed, 127 insertions(+), 40 deletions(-) diff --git a/apps/server/src/terminal/Manager.test.ts b/apps/server/src/terminal/Manager.test.ts index 1f59f23503b6..0e6a4995bd95 100644 --- a/apps/server/src/terminal/Manager.test.ts +++ b/apps/server/src/terminal/Manager.test.ts @@ -10,6 +10,7 @@ import { } from "@t3tools/contracts"; import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Data from "effect/Data"; +import * as Clock from "effect/Clock"; import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; @@ -1110,6 +1111,64 @@ it.layer( }), ); + it.effect("backs off the spawned fallback when the resource monitor snapshot fails", () => + Effect.gen(function* () { + const fallbackCalls: Array = []; + const processRunner: ProcessRunner.ProcessRunner["Service"] = { + run: () => + Clock.currentTimeMillis.pipe( + Effect.map((now) => { + fallbackCalls.push(now); + return { + stdout: " 100 9000 vim", + stderr: "", + code: ChildProcessSpawner.ExitCode(0), + timedOut: false, + stdoutTruncated: false, + stderrInvalidUtf8: false, + stdoutInvalidUtf8: false, + stderrTruncated: false, + }; + }), + ), + }; + + const { manager, getEvents } = yield* createManager(5, { + subprocessPollIntervalMs: 20, + processTable: Effect.fail("sidecar unavailable").pipe( + Effect.mapError((cause) => cause as never), + ), + }).pipe( + Effect.provideService(ProcessRunner.ProcessRunner, processRunner), + Effect.provide(withHostPlatform("linux")), + ); + + yield* manager.open(openInput()); + // The fallback data is still applied while the sidecar is down. + yield* waitFor( + Effect.map(getEvents, (events) => + events.some( + (event) => + event.type === "activity" && + event.hasRunningSubprocess === true && + event.label === "vim", + ), + ), + "1200 millis", + ); + + yield* waitFor( + Effect.sync(() => fallbackCalls.length >= 4), + "2000 millis", + ); + // Four snapshots at the 20 ms base cadence would span ~60 ms. Backoff + // (40 + 80 + 160 ms) stretches the same four snapshots past 150 ms, so + // a stalled sidecar no longer hot-loops the spawned fallback. + const spanMs = fallbackCalls[3]! - fallbackCalls[0]!; + expect(spanMs).toBeGreaterThan(150); + }), + ); + it.effect("caps persisted history to configured line limit", () => Effect.gen(function* () { const { manager, ptyAdapter } = yield* createManager(3); diff --git a/apps/server/src/terminal/Manager.ts b/apps/server/src/terminal/Manager.ts index 44ae34b8e838..a7c84e7793f1 100644 --- a/apps/server/src/terminal/Manager.ts +++ b/apps/server/src/terminal/Manager.ts @@ -1186,24 +1186,55 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func ? windowsProcessTableSnapshot() : posixProcessTableSnapshot(yield* resolvePosixPsCommand()) ).pipe(Effect.provideService(ProcessRunner.ProcessRunner, processRunner)); - const fetchProcessTableSnapshot = options.processTable + const fetchProcessTableSnapshot: Effect.Effect< + { + readonly snapshot: TerminalProcessTableSnapshot; + /** + * False when the sidecar snapshot failed and this table came from the + * spawned fallback. The data is still applied, but the tick counts as + * a failure so polling backs off instead of hot-looping the fallback. + */ + readonly snapshotSucceeded: boolean; + }, + TerminalSubprocessCheckError + > = options.processTable ? options.processTable.pipe( - Effect.map(processTableSnapshotFromProcesses), - Effect.catch(() => fallbackProcessTableSnapshot), + Effect.map((entries) => ({ + snapshot: processTableSnapshotFromProcesses(entries), + snapshotSucceeded: true, + })), + Effect.catch(() => + fallbackProcessTableSnapshot.pipe( + Effect.map((snapshot) => ({ snapshot, snapshotSucceeded: false })), + ), + ), ) - : fallbackProcessTableSnapshot; + : fallbackProcessTableSnapshot.pipe( + Effect.map((snapshot) => ({ snapshot, snapshotSucceeded: true })), + ); const customSubprocessInspector = options.subprocessInspector; const acquireSubprocessInspector: Effect.Effect< - TerminalSubprocessInspector, + { + readonly inspector: TerminalSubprocessInspector; + readonly snapshotSucceeded: boolean; + }, TerminalSubprocessCheckError > = customSubprocessInspector !== undefined - ? Effect.succeed(customSubprocessInspector) + ? Effect.succeed({ inspector: customSubprocessInspector, snapshotSucceeded: true }) : Effect.map( fetchProcessTableSnapshot, - (snapshot): TerminalSubprocessInspector => - (terminalPid) => + ({ + snapshot, + snapshotSucceeded, + }): { + readonly inspector: TerminalSubprocessInspector; + readonly snapshotSucceeded: boolean; + } => ({ + inspector: (terminalPid) => Effect.succeed(deriveSubprocessInspectResult(snapshot, terminalPid, platform)), + snapshotSucceeded, + }), ); const subprocessPollIntervalMs = options.subprocessPollIntervalMs ?? DEFAULT_SUBPROCESS_POLL_INTERVAL_MS; @@ -2041,7 +2072,14 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func Effect.catch((reason) => Effect.logWarning("failed to snapshot processes for terminal subprocess polling", { reason, - }).pipe(Effect.as(Option.none())), + }).pipe( + Effect.as( + Option.none<{ + readonly inspector: TerminalSubprocessInspector; + readonly snapshotSucceeded: boolean; + }>(), + ), + ), ), ); @@ -2049,7 +2087,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func return false; } - const subprocessInspector = inspectorOption.value; + const { inspector: subprocessInspector, snapshotSucceeded } = inspectorOption.value; const checkSubprocessActivity = Effect.fn("terminal.checkSubprocessActivity")(function* ( session: TerminalSessionState & { pid: number }, @@ -2118,7 +2156,7 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func concurrency: "unbounded", discard: true, }); - return true; + return snapshotSucceeded; }); const hasRunningSessions = readManagerState.pipe( diff --git a/native/resource-monitor/src/main.rs b/native/resource-monitor/src/main.rs index 6845ea70a628..14975402121a 100644 --- a/native/resource-monitor/src/main.rs +++ b/native/resource-monitor/src/main.rs @@ -384,13 +384,23 @@ impl Collector { .system .processes() .iter() - .map(|(pid, process)| ProcessTableEntry { - pid: pid.as_u32(), - ppid: process.parent().map(Pid::as_u32).unwrap_or(0), - name: truncate_utf8( - process.name().to_string_lossy().into_owned(), - MAX_PROCESS_NAME_BYTES, - ), + .filter_map(|(pid, process)| { + let pid = pid.as_u32(); + // Pid 0 is the kernel idle process on some platforms. The + // processTable contract requires positive pids, and one zero + // would fail the whole event decode on the server, so drop it + // here. It can never be a terminal descendant. + if pid == 0 { + return None; + } + Some(ProcessTableEntry { + pid, + ppid: process.parent().map(Pid::as_u32).unwrap_or(0), + name: truncate_utf8( + process.name().to_string_lossy().into_owned(), + MAX_PROCESS_NAME_BYTES, + ), + }) }) .collect::>(); processes.sort_by_key(|process| process.pid); diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 165aef1a33b0..f4bbad9d5407 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -3259,24 +3259,8 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( cause, }), }); - const resolvedServerOptionalDependencies = yield* Effect.try({ - try: () => - resolveCatalogDependencies( - serverPackageJson.optionalDependencies ?? {}, - workspaceCatalog, - "apps/server", - ), - catch: (cause) => - new DesktopBuildDependencyResolutionError({ - kind: "server-production", - manifestPath: "apps/server/package.json", - cause, - }), - }); const resolvedServerRuntimeExternalDependencies = selectCliRuntimeExternalDependencies( - options.platform === "win" - ? { ...resolvedServerDependencies, ...resolvedServerOptionalDependencies } - : resolvedServerDependencies, + resolvedServerDependencies, ); const resolvedDesktopRuntimeDependencies = yield* Effect.try({ try: () => resolveDesktopRuntimeDependencies(desktopPackageJson.dependencies, workspaceCatalog), diff --git a/scripts/lib/cli-external-packages.test.ts b/scripts/lib/cli-external-packages.test.ts index 8ab3c95f41fd..754cd646f17d 100644 --- a/scripts/lib/cli-external-packages.test.ts +++ b/scripts/lib/cli-external-packages.test.ts @@ -86,11 +86,7 @@ describe("selectCliRuntimeExternalDependencies", () => { it("selects every external root declared by the server", () => { assert.deepStrictEqual( - Object.keys( - selectCliRuntimeExternalDependencies({ - ...serverPackageJson.dependencies, - }), - ).sort(), + Object.keys(selectCliRuntimeExternalDependencies(serverPackageJson.dependencies)).sort(), ["@ff-labs/fff-node", "msgpackr-extract", "node-pty"], ); });