| @@ -28,7 +28,7 @@ export interface ProcessRow { · 27 unmodified lines |
| 28 | 28 | readonly command: string; |
| 29 | 29 | } |
| 30 | 30 | |
| 31 | | −const PROCESS_QUERY_TIMEOUT_MS = 1_000; |
| 31 | +const PROCESS_QUERY_TIMEOUT = Duration.seconds(1); |
| 32 | 32 | const POSIX_PROCESS_QUERY_COMMAND = "pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command="; |
| 33 | 33 | const PROCESS_QUERY_MAX_OUTPUT_BYTES = 2 * 1024 * 1024; |
| 34 | 34 | |
| @@ -121,6 +121,19 @@ const ProcessDiagnosticsError = Schema.Union([ · 86 unmodified lines |
| 121 | 121 | type ProcessDiagnosticsError = typeof ProcessDiagnosticsError.Type; |
| 122 | 122 | const isProcessDiagnosticsError = Schema.is(ProcessDiagnosticsError); |
| 123 | 123 | |
| 124 | +const WindowsCimProcessRecord = Schema.Struct({ |
| 125 | + ProcessId: Schema.Number, |
| 126 | + ParentProcessId: Schema.Number, |
| 127 | + Name: Schema.optional(Schema.NullOr(Schema.String)), |
| 128 | + CommandLine: Schema.optional(Schema.NullOr(Schema.String)), |
| 129 | + Status: Schema.optional(Schema.NullOr(Schema.String)), |
| 130 | + WorkingSetSize: Schema.optional(Schema.Number), |
| 131 | + PercentProcessorTime: Schema.optional(Schema.Number), |
| 132 | +}); |
| 133 | +type WindowsCimProcessRecord = typeof WindowsCimProcessRecord.Type; |
| 134 | +const decodeWindowsProcessJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json)); |
| 135 | +const decodeWindowsProcessRecord = Schema.decodeUnknownOption(WindowsCimProcessRecord); |
| 136 | + |
| 124 | 137 | function parsePositiveInt(value: string): number | null { |
| 125 | 138 | const parsed = Number.parseInt(value, 10); |
| 126 | 139 | return Number.isInteger(parsed) && parsed > 0 ? parsed : null; |
| @@ -201,51 +214,53 @@ export function parsePosixProcessRows(output: string): ReadonlyArray<ProcessRow> · 74 unmodified lines |
| 201 | 214 | return rows; |
| 202 | 215 | } |
| 203 | 216 | |
| 204 | | −function normalizeWindowsProcessRow(value: unknown): ProcessRow | null { |
| 205 | | − if (typeof value !== "object" || value === null) return null; |
| 206 | | − const record = value as Record<string, unknown>; |
| 207 | | − const pid = typeof record.ProcessId === "number" ? record.ProcessId : null; |
| 208 | | − const ppid = typeof record.ParentProcessId === "number" ? record.ParentProcessId : null; |
| 209 | | − const commandLine = |
| 210 | | − typeof record.CommandLine === "string" && record.CommandLine.trim().length > 0 |
| 211 | | − ? record.CommandLine |
| 212 | | − : typeof record.Name === "string" |
| 213 | | − ? record.Name |
| 214 | | − : null; |
| 215 | | − const workingSet = |
| 216 | | − typeof record.WorkingSetSize === "number" && Number.isFinite(record.WorkingSetSize) |
| 217 | | − ? Math.max(0, Math.round(record.WorkingSetSize)) |
| 218 | | − : 0; |
| 219 | | − const cpuPercent = |
| 220 | | − typeof record.PercentProcessorTime === "number" && Number.isFinite(record.PercentProcessorTime) |
| 221 | | − ? Math.max(0, record.PercentProcessorTime) |
| 222 | | − : 0; |
| 223 | | − |
| 224 | | − if (!pid || pid <= 0 || ppid === null || ppid < 0 || !commandLine) return null; |
| 225 | | − return { |
| 226 | | − pid, |
| 227 | | − ppid, |
| 228 | | − pgid: null, |
| 229 | | − status: typeof record.Status === "string" && record.Status.length > 0 ? record.Status : "Live", |
| 230 | | − cpuPercent, |
| 231 | | − rssBytes: workingSet, |
| 232 | | − elapsed: "", |
| 233 | | − command: commandLine, |
| 234 | | − }; |
| 217 | +function nonEmptyStringOption(value: string | null | undefined): Option.Option<string> { |
| 218 | + return typeof value === "string" && value.trim().length > 0 ? Option.some(value) : Option.none(); |
| 235 | 219 | } |
| 236 | 220 | |
| 237 | | −function parseWindowsProcessRows(output: string): ReadonlyArray<ProcessRow> { |
| 221 | +function normalizeWindowsProcessRow(value: unknown): Option.Option<ProcessRow> { |
| 222 | + return Option.flatMap(decodeWindowsProcessRecord(value), (record: WindowsCimProcessRecord) => { |
| 223 | + const pid = record.ProcessId; |
| 224 | + const ppid = record.ParentProcessId; |
| 225 | + if (!Number.isInteger(pid) || pid <= 0 || !Number.isInteger(ppid) || ppid < 0) { |
| 226 | + return Option.none(); |
| 227 | + } |
| 228 | + |
| 229 | + const commandLine = nonEmptyStringOption(record.CommandLine).pipe( |
| 230 | + Option.orElse(() => nonEmptyStringOption(record.Name)), |
| 231 | + ); |
| 232 | + return Option.map(commandLine, (command) => ({ |
| 233 | + pid, |
| 234 | + ppid, |
| 235 | + pgid: null, |
| 236 | + status: Option.getOrElse(nonEmptyStringOption(record.Status), () => "Live"), |
| 237 | + cpuPercent: |
| 238 | + typeof record.PercentProcessorTime === "number" && Number.isFinite(record.PercentProcessorTime) |
| 239 | + ? Math.max(0, record.PercentProcessorTime) |
| 240 | + : 0, |
| 241 | + rssBytes: |
| 242 | + typeof record.WorkingSetSize === "number" && Number.isFinite(record.WorkingSetSize) |
| 243 | + ? Math.max(0, Math.round(record.WorkingSetSize)) |
| 244 | + : 0, |
| 245 | + elapsed: "", |
| 246 | + command, |
| 247 | + })); |
| 248 | + }); |
| 249 | +} |
| 250 | + |
| 251 | +export function parseWindowsProcessRows(output: string): ReadonlyArray<ProcessRow> { |
| 238 | 252 | if (output.trim().length === 0) return []; |
| 239 | | − try { |
| 240 | | − const parsed = JSON.parse(output) as unknown; |
| 241 | | − const records = Array.isArray(parsed) ? parsed : [parsed]; |
| 242 | | − return records.flatMap((record) => { |
| 243 | | − const row = normalizeWindowsProcessRow(record); |
| 244 | | − return row ? [row] : []; |
| 245 | | − }); |
| 246 | | − } catch { |
| 253 | + const parsed = decodeWindowsProcessJson(output); |
| 254 | + if (Option.isNone(parsed)) { |
| 247 | 255 | return []; |
| 248 | 256 | } |
| 257 | + const records = Array.isArray(parsed.value) ? parsed.value : [parsed.value]; |
| 258 | + return records.flatMap((record) => |
| 259 | + Option.match(normalizeWindowsProcessRow(record), { |
| 260 | + onNone: () => [], |
| 261 | + onSome: (row) => [row], |
| 262 | + }), |
| 263 | + ); |
| 249 | 264 | } |
| 250 | 265 | |
| 251 | 266 | export function buildDescendantEntries( |
| @@ -381,7 +396,7 @@ const runProcess = Effect.fn("runProcess")(function* (input: { · 129 unmodified lines |
| 381 | 396 | } satisfies ProcessOutput; |
| 382 | 397 | }).pipe( |
| 383 | 398 | Effect.scoped, |
| 384 | | − Effect.timeoutOption(Duration.millis(PROCESS_QUERY_TIMEOUT_MS)), |
| 399 | + Effect.timeoutOption(PROCESS_QUERY_TIMEOUT), |
| 385 | 400 | Effect.flatMap((result) => |
| 386 | 401 | Option.match(result, { |
| 387 | 402 | onNone: () => |
| @@ -390,7 +405,7 @@ const runProcess = Effect.fn("runProcess")(function* (input: { · 2 unmodified lines |
| 390 | 405 | command: input.command, |
| 391 | 406 | argCount: input.args.length, |
| 392 | 407 | cwd, |
| 393 | | − timeoutMillis: PROCESS_QUERY_TIMEOUT_MS, |
| 408 | + timeoutMillis: Duration.toMillis(PROCESS_QUERY_TIMEOUT), |
| 394 | 409 | }), |
| 395 | 410 | ), |
| 396 | 411 | onSome: Effect.succeed, |