| @@ -28,7 +28,7 @@ export interface ProcessRow { · 27 unmodified lines |
| 28 | readonly command: string; | 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 | const POSIX_PROCESS_QUERY_COMMAND = "pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command="; | 32 | const POSIX_PROCESS_QUERY_COMMAND = "pid=,ppid=,pgid=,stat=,pcpu=,rss=,etime=,command="; |
| 33 | const PROCESS_QUERY_MAX_OUTPUT_BYTES = 2 * 1024 * 1024; | 33 | const PROCESS_QUERY_MAX_OUTPUT_BYTES = 2 * 1024 * 1024; |
| 34 | | 34 | |
| @@ -121,6 +121,19 @@ const ProcessDiagnosticsError = Schema.Union([ · 86 unmodified lines |
| 121 | type ProcessDiagnosticsError = typeof ProcessDiagnosticsError.Type; | 121 | type ProcessDiagnosticsError = typeof ProcessDiagnosticsError.Type; |
| 122 | const isProcessDiagnosticsError = Schema.is(ProcessDiagnosticsError); | 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 | function parsePositiveInt(value: string): number | null { | 137 | function parsePositiveInt(value: string): number | null { |
| 125 | const parsed = Number.parseInt(value, 10); | 138 | const parsed = Number.parseInt(value, 10); |
| 126 | return Number.isInteger(parsed) && parsed > 0 ? parsed : null; | 139 | return Number.isInteger(parsed) && parsed > 0 ? parsed : null; |
| @@ -201,51 +214,53 @@ export function parsePosixProcessRows(output: string): ReadonlyArray<ProcessRow> · 74 unmodified lines |
| 201 | return rows; | 214 | return rows; |
| 202 | } | 215 | } |
| 203 | | 216 | |
| 204 | function normalizeWindowsProcessRow(value: unknown): ProcessRow | null { | 217 | function nonEmptyStringOption(value: string | null | undefined): Option.Option<string> { |
| 205 | if (typeof value !== "object" || value === null) return null; | 218 | return typeof value === "string" && value.trim().length > 0 ? Option.some(value) : Option.none(); |
| 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 | }; | | |
| 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 | if (output.trim().length === 0) return []; | 252 | if (output.trim().length === 0) return []; |
| 239 | try { | 253 | const parsed = decodeWindowsProcessJson(output); |
| 240 | const parsed = JSON.parse(output) as unknown; | 254 | if (Option.isNone(parsed)) { |
| 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 { | | |
| 247 | return []; | 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 | export function buildDescendantEntries( | 266 | export function buildDescendantEntries( |
| @@ -381,7 +396,7 @@ const runProcess = Effect.fn("runProcess")(function* (input: { · 129 unmodified lines |
| 381 | } satisfies ProcessOutput; | 396 | } satisfies ProcessOutput; |
| 382 | }).pipe( | 397 | }).pipe( |
| 383 | Effect.scoped, | 398 | Effect.scoped, |
| 384 | Effect.timeoutOption(Duration.millis(PROCESS_QUERY_TIMEOUT_MS)), | 399 | Effect.timeoutOption(PROCESS_QUERY_TIMEOUT), |
| 385 | Effect.flatMap((result) => | 400 | Effect.flatMap((result) => |
| 386 | Option.match(result, { | 401 | Option.match(result, { |
| 387 | onNone: () => | 402 | onNone: () => |
| @@ -390,7 +405,7 @@ const runProcess = Effect.fn("runProcess")(function* (input: { · 2 unmodified lines |
| 390 | command: input.command, | 405 | command: input.command, |
| 391 | argCount: input.args.length, | 406 | argCount: input.args.length, |
| 392 | cwd, | 407 | cwd, |
| 393 | timeoutMillis: PROCESS_QUERY_TIMEOUT_MS, | 408 | timeoutMillis: Duration.toMillis(PROCESS_QUERY_TIMEOUT), |
| 394 | }), | 409 | }), |
| 395 | ), | 410 | ), |
| 396 | onSome: Effect.succeed, | 411 | onSome: Effect.succeed, |