From f3da91a0d7daaabb34eae8baacefed5b4bfca779 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 29 Jun 2026 16:07:02 +0000 Subject: [PATCH 1/2] Use Effect Schema for Windows process diagnostics Co-authored-by: Julius Marminge --- .../diagnostics/ProcessDiagnostics.test.ts | 94 +++++++++++++++- .../src/diagnostics/ProcessDiagnostics.ts | 101 ++++++++++-------- 2 files changed, 151 insertions(+), 44 deletions(-) diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts index 7d16a11c829..5b5a7009874 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.test.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "@effect/vitest"; +import { assert, describe, expect, it } from "@effect/vitest"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -67,6 +67,98 @@ describe("ProcessDiagnostics", () => { }), ); + it.effect("parses Windows CIM JSON process rows", () => + Effect.sync(() => { + const rows = ProcessDiagnostics.parseWindowsProcessRows(`[ + { + "ProcessId": 10, + "ParentProcessId": 1, + "Name": "node.exe", + "CommandLine": "node server.js", + "Status": "Running", + "WorkingSetSize": 4096, + "PercentProcessorTime": 12.5 + }, + { + "ProcessId": "invalid", + "ParentProcessId": 10, + "Name": "ignored.exe" + }, + { + "ProcessId": 11, + "ParentProcessId": 10, + "Name": "agent.exe", + "CommandLine": null, + "Status": null, + "WorkingSetSize": -128, + "PercentProcessorTime": -1 + } + ]`); + + assert.deepStrictEqual(rows, [ + { + pid: 10, + ppid: 1, + pgid: null, + status: "Running", + cpuPercent: 12.5, + rssBytes: 4096, + elapsed: "", + command: "node server.js", + }, + { + pid: 11, + ppid: 10, + pgid: null, + status: "Live", + cpuPercent: 0, + rssBytes: 0, + elapsed: "", + command: "agent.exe", + }, + ]); + }), + ); + + it.effect("parses single Windows CIM JSON process objects", () => + Effect.sync(() => { + const rows = ProcessDiagnostics.parseWindowsProcessRows(`{ + "ProcessId": 42, + "ParentProcessId": 10, + "Name": "agent.exe", + "CommandLine": "", + "WorkingSetSize": 2048, + "PercentProcessorTime": 5 + }`); + + assert.deepStrictEqual(rows, [ + { + pid: 42, + ppid: 10, + pgid: null, + status: "Live", + cpuPercent: 5, + rssBytes: 2048, + elapsed: "", + command: "agent.exe", + }, + ]); + }), + ); + + it.effect("ignores malformed Windows CIM JSON output", () => + Effect.sync(() => { + assert.deepStrictEqual(ProcessDiagnostics.parseWindowsProcessRows("{not json"), []); + assert.deepStrictEqual( + ProcessDiagnostics.parseWindowsProcessRows(`{ + "ProcessId": 42, + "Name": "missing-parent.exe" + }`), + [], + ); + }), + ); + it.effect("aggregates only descendants of the server process", () => Effect.sync(() => { const diagnostics = ProcessDiagnostics.aggregateProcessDiagnostics({ diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.ts b/apps/server/src/diagnostics/ProcessDiagnostics.ts index b39d560a228..7d2f422c750 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.ts @@ -28,7 +28,7 @@ export interface ProcessRow { readonly command: string; } -const PROCESS_QUERY_TIMEOUT_MS = 1_000; +const PROCESS_QUERY_TIMEOUT = Duration.seconds(1); const POSIX_PROCESS_QUERY_COMMAND = "pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command="; const PROCESS_QUERY_MAX_OUTPUT_BYTES = 2 * 1024 * 1024; @@ -121,6 +121,19 @@ const ProcessDiagnosticsError = Schema.Union([ type ProcessDiagnosticsError = typeof ProcessDiagnosticsError.Type; const isProcessDiagnosticsError = Schema.is(ProcessDiagnosticsError); +const WindowsCimProcessRecord = Schema.Struct({ + ProcessId: Schema.Number, + ParentProcessId: Schema.Number, + Name: Schema.optional(Schema.NullOr(Schema.String)), + CommandLine: Schema.optional(Schema.NullOr(Schema.String)), + Status: Schema.optional(Schema.NullOr(Schema.String)), + WorkingSetSize: Schema.optional(Schema.Number), + PercentProcessorTime: Schema.optional(Schema.Number), +}); +type WindowsCimProcessRecord = typeof WindowsCimProcessRecord.Type; +const decodeWindowsProcessJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json)); +const decodeWindowsProcessRecord = Schema.decodeUnknownOption(WindowsCimProcessRecord); + function parsePositiveInt(value: string): number | null { const parsed = Number.parseInt(value, 10); return Number.isInteger(parsed) && parsed > 0 ? parsed : null; @@ -201,51 +214,53 @@ export function parsePosixProcessRows(output: string): ReadonlyArray return rows; } -function normalizeWindowsProcessRow(value: unknown): ProcessRow | null { - if (typeof value !== "object" || value === null) return null; - const record = value as Record; - const pid = typeof record.ProcessId === "number" ? record.ProcessId : null; - const ppid = typeof record.ParentProcessId === "number" ? record.ParentProcessId : null; - const commandLine = - typeof record.CommandLine === "string" && record.CommandLine.trim().length > 0 - ? record.CommandLine - : typeof record.Name === "string" - ? record.Name - : null; - const workingSet = - typeof record.WorkingSetSize === "number" && Number.isFinite(record.WorkingSetSize) - ? Math.max(0, Math.round(record.WorkingSetSize)) - : 0; - const cpuPercent = - typeof record.PercentProcessorTime === "number" && Number.isFinite(record.PercentProcessorTime) - ? Math.max(0, record.PercentProcessorTime) - : 0; - - if (!pid || pid <= 0 || ppid === null || ppid < 0 || !commandLine) return null; - return { - pid, - ppid, - pgid: null, - status: typeof record.Status === "string" && record.Status.length > 0 ? record.Status : "Live", - cpuPercent, - rssBytes: workingSet, - elapsed: "", - command: commandLine, - }; +function nonEmptyStringOption(value: string | null | undefined): Option.Option { + return typeof value === "string" && value.trim().length > 0 ? Option.some(value) : Option.none(); } -function parseWindowsProcessRows(output: string): ReadonlyArray { +function normalizeWindowsProcessRow(value: unknown): Option.Option { + return Option.flatMap(decodeWindowsProcessRecord(value), (record: WindowsCimProcessRecord) => { + const pid = record.ProcessId; + const ppid = record.ParentProcessId; + if (!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(ppid) || ppid < 0) { + return Option.none(); + } + + const commandLine = nonEmptyStringOption(record.CommandLine).pipe( + Option.orElse(() => nonEmptyStringOption(record.Name)), + ); + return Option.map(commandLine, (command) => ({ + pid, + ppid, + pgid: null, + status: Option.getOrElse(nonEmptyStringOption(record.Status), () => "Live"), + cpuPercent: + typeof record.PercentProcessorTime === "number" && Number.isFinite(record.PercentProcessorTime) + ? Math.max(0, record.PercentProcessorTime) + : 0, + rssBytes: + typeof record.WorkingSetSize === "number" && Number.isFinite(record.WorkingSetSize) + ? Math.max(0, Math.round(record.WorkingSetSize)) + : 0, + elapsed: "", + command, + })); + }); +} + +export function parseWindowsProcessRows(output: string): ReadonlyArray { if (output.trim().length === 0) return []; - try { - const parsed = JSON.parse(output) as unknown; - const records = Array.isArray(parsed) ? parsed : [parsed]; - return records.flatMap((record) => { - const row = normalizeWindowsProcessRow(record); - return row ? [row] : []; - }); - } catch { + const parsed = decodeWindowsProcessJson(output); + if (Option.isNone(parsed)) { return []; } + const records = Array.isArray(parsed.value) ? parsed.value : [parsed.value]; + return records.flatMap((record) => + Option.match(normalizeWindowsProcessRow(record), { + onNone: () => [], + onSome: (row) => [row], + }), + ); } export function buildDescendantEntries( @@ -381,7 +396,7 @@ const runProcess = Effect.fn("runProcess")(function* (input: { } satisfies ProcessOutput; }).pipe( Effect.scoped, - Effect.timeoutOption(Duration.millis(PROCESS_QUERY_TIMEOUT_MS)), + Effect.timeoutOption(PROCESS_QUERY_TIMEOUT), Effect.flatMap((result) => Option.match(result, { onNone: () => @@ -390,7 +405,7 @@ const runProcess = Effect.fn("runProcess")(function* (input: { command: input.command, argCount: input.args.length, cwd, - timeoutMillis: PROCESS_QUERY_TIMEOUT_MS, + timeoutMillis: Duration.toMillis(PROCESS_QUERY_TIMEOUT), }), ), onSome: Effect.succeed, From 0bb0f538220db1f3906a71ea083d2d4894b5c1ea Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 29 Jun 2026 16:13:09 +0000 Subject: [PATCH 2/2] Format Windows process diagnostics parser Co-authored-by: Julius Marminge --- apps/server/src/diagnostics/ProcessDiagnostics.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.ts b/apps/server/src/diagnostics/ProcessDiagnostics.ts index 7d2f422c750..701d684f2b1 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.ts @@ -235,7 +235,8 @@ function normalizeWindowsProcessRow(value: unknown): Option.Option { pgid: null, status: Option.getOrElse(nonEmptyStringOption(record.Status), () => "Live"), cpuPercent: - typeof record.PercentProcessorTime === "number" && Number.isFinite(record.PercentProcessorTime) + typeof record.PercentProcessorTime === "number" && + Number.isFinite(record.PercentProcessorTime) ? Math.max(0, record.PercentProcessorTime) : 0, rssBytes: