| ... | @@ -45,10 +45,15 @@ export class ProcessDiagnostics extends Context.Service< | ... | @@ -45,10 +45,15 @@ export class ProcessDiagnostics extends Context.Service< |
| 45 | | 45 | |
| 46 | class ProcessDiagnosticsQueryTimeoutError extends Schema.TaggedErrorClass<ProcessDiagnosticsQueryTimeoutError>()( | 46 | class ProcessDiagnosticsQueryTimeoutError extends Schema.TaggedErrorClass<ProcessDiagnosticsQueryTimeoutError>()( |
| 47 | "ProcessDiagnosticsQueryTimeoutError", | 47 | "ProcessDiagnosticsQueryTimeoutError", |
| 48 | { command: Schema.String }, | 48 | { |
| | 49 | command: Schema.String, |
| | 50 | argCount: Schema.Number, |
| | 51 | cwd: Schema.String, |
| | 52 | timeoutMillis: Schema.Number, |
| | 53 | }, |
| 49 | ) { | 54 | ) { |
| 50 | override get message(): string { | 55 | override get message(): string { |
| 51 | return `Process diagnostics query '${this.command}' timed out.`; | 56 | return `Process diagnostics query '${this.command}' timed out after ${this.timeoutMillis}ms in '${this.cwd}'.`; |
| 52 | } | 57 | } |
| 53 | } | 58 | } |
| 54 | | 59 | |
| ... | @@ -56,12 +61,19 @@ class ProcessDiagnosticsQueryFailedError extends Schema.TaggedErrorClass<Process | ... | @@ -56,12 +61,19 @@ class ProcessDiagnosticsQueryFailedError extends Schema.TaggedErrorClass<Process |
| 56 | "ProcessDiagnosticsQueryFailedError", | 61 | "ProcessDiagnosticsQueryFailedError", |
| 57 | { | 62 | { |
| 58 | command: Schema.String, | 63 | command: Schema.String, |
| 59 | stderr: Schema.optional(Schema.String), | 64 | argCount: Schema.Number, |
| | 65 | cwd: Schema.String, |
| | 66 | exitCode: Schema.optional(Schema.Number), |
| | 67 | stdoutBytes: Schema.optional(Schema.Number), |
| | 68 | stderrBytes: Schema.optional(Schema.Number), |
| | 69 | stdoutTruncated: Schema.optional(Schema.Boolean), |
| | 70 | stderrTruncated: Schema.optional(Schema.Boolean), |
| 60 | cause: Schema.optional(Schema.Defect()), | 71 | cause: Schema.optional(Schema.Defect()), |
| 61 | }, | 72 | }, |
| 62 | ) { | 73 | ) { |
| 63 | override get message(): string { | 74 | override get message(): string { |
| 64 | return this.stderr?.trim() || `Failed to query process diagnostics with '${this.command}'.`; | 75 | const exitCode = this.exitCode === undefined ? "" : ` with exit code ${this.exitCode}`; |
| | 76 | return `Process diagnostics query '${this.command}' failed${exitCode} in '${this.cwd}'.`; |
| 65 | } | 77 | } |
| 66 | } | 78 | } |
| 67 | | 79 | |
| ... | @@ -76,7 +88,10 @@ class ProcessDiagnosticsServerProcessSignalError extends Schema.TaggedErrorClass | ... | @@ -76,7 +88,10 @@ class ProcessDiagnosticsServerProcessSignalError extends Schema.TaggedErrorClass |
| 76 | | 88 | |
| 77 | class ProcessDiagnosticsNotDescendantError extends Schema.TaggedErrorClass<ProcessDiagnosticsNotDescendantError>()( | 89 | class ProcessDiagnosticsNotDescendantError extends Schema.TaggedErrorClass<ProcessDiagnosticsNotDescendantError>()( |
| 78 | "ProcessDiagnosticsNotDescendantError", | 90 | "ProcessDiagnosticsNotDescendantError", |
| 79 | { pid: Schema.Number }, | 91 | { |
| | 92 | pid: Schema.Number, |
| | 93 | serverPid: Schema.Number, |
| | 94 | }, |
| 80 | ) { | 95 | ) { |
| 81 | override get message(): string { | 96 | override get message(): string { |
| 82 | return `Process ${this.pid} is not a live descendant of the T3 server.`; | 97 | return `Process ${this.pid} is not a live descendant of the T3 server.`; |
| ... | @@ -312,20 +327,29 @@ function makeResult(input: { | ... | @@ -312,20 +327,29 @@ function makeResult(input: { |
| 312 | } | 327 | } |
| 313 | | 328 | |
| 314 | interface ProcessOutput { | 329 | interface ProcessOutput { |
| | 330 | readonly cwd: string; |
| 315 | readonly exitCode: number; | 331 | readonly exitCode: number; |
| 316 | readonly stdout: string; | 332 | readonly stdout: string; |
| | 333 | readonly stdoutBytes: number; |
| | 334 | readonly stdoutTruncated: boolean; |
| 317 | readonly stderr: string; | 335 | readonly stderr: string; |
| | 336 | readonly stderrBytes: number; |
| | 337 | readonly stderrTruncated: boolean; |
| 318 | } | 338 | } |
| 319 | | 339 | |
| 320 | const runProcess = Effect.fn("runProcess")( | 340 | const runProcess = Effect.fn("runProcess")(function* (input: { |
| 321 | function* (input: { readonly command: string; readonly args: ReadonlyArray<string> }) { | 341 | readonly command: string; |
| | 342 | readonly args: ReadonlyArray<string>; |
| | 343 | }) { |
| | 344 | const cwd = process.cwd(); |
| | 345 | return yield* Effect.gen(function* () { |
| 322 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 346 | const spawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 323 | // `ps` and `powershell.exe` are real executables; spawning through cmd.exe | 347 | // `ps` and `powershell.exe` are real executables; spawning through cmd.exe |
| 324 | // shell mode would re-tokenize the PowerShell `-Command` payload (which | 348 | // shell mode would re-tokenize the PowerShell `-Command` payload (which |
| 325 | // contains pipes) before PowerShell ever sees it. | 349 | // contains pipes) before PowerShell ever sees it. |
| 326 | const child = yield* spawner.spawn( | 350 | const child = yield* spawner.spawn( |
| 327 | ChildProcess.make(input.command, input.args, { | 351 | ChildProcess.make(input.command, input.args, { |
| 328 | cwd: process.cwd(), | 352 | cwd, |
| 329 | }), | 353 | }), |
| 330 | ); | 354 | ); |
| 331 | const [stdout, stderr, exitCode] = yield* Effect.all( | 355 | const [stdout, stderr, exitCode] = yield* Effect.all( |
| ... | @@ -346,36 +370,44 @@ const runProcess = Effect.fn("runProcess")( | ... | @@ -346,36 +370,44 @@ const runProcess = Effect.fn("runProcess")( |
| 346 | ); | 370 | ); |
| 347 | | 371 | |
| 348 | return { | 372 | return { |
| | 373 | cwd, |
| 349 | exitCode, | 374 | exitCode, |
| 350 | stdout: stdout.text, | 375 | stdout: stdout.text, |
| | 376 | stdoutBytes: stdout.bytes, |
| | 377 | stdoutTruncated: stdout.truncated, |
| 351 | stderr: stderr.text, | 378 | stderr: stderr.text, |
| | 379 | stderrBytes: stderr.bytes, |
| | 380 | stderrTruncated: stderr.truncated, |
| 352 | } satisfies ProcessOutput; | 381 | } satisfies ProcessOutput; |
| 353 | }, | 382 | }).pipe( |
| 354 | (effect, input) => | 383 | Effect.scoped, |
| 355 | effect.pipe( | 384 | Effect.timeoutOption(Duration.millis(PROCESS_QUERY_TIMEOUT_MS)), |
| 356 | Effect.scoped, | 385 | Effect.flatMap((result) => |
| 357 | Effect.timeoutOption(Duration.millis(PROCESS_QUERY_TIMEOUT_MS)), | 386 | Option.match(result, { |
| 358 | Effect.flatMap((result) => | 387 | onNone: () => |
| 359 | Option.match(result, { | 388 | Effect.fail( |
| 360 | onNone: () => | 389 | new ProcessDiagnosticsQueryTimeoutError({ |
| 361 | Effect.fail( | | |
| 362 | new ProcessDiagnosticsQueryTimeoutError({ | | |
| 363 | command: input.command, | | |
| 364 | }), | | |
| 365 | ), | | |
| 366 | onSome: Effect.succeed, | | |
| 367 | }), | | |
| 368 | ), | | |
| 369 | Effect.mapError((cause) => | | |
| 370 | isProcessDiagnosticsError(cause) | | |
| 371 | ? cause | | |
| 372 | : new ProcessDiagnosticsQueryFailedError({ | | |
| 373 | command: input.command, | 390 | command: input.command, |
| 374 | cause, | 391 | argCount: input.args.length, |
| | 392 | cwd, |
| | 393 | timeoutMillis: PROCESS_QUERY_TIMEOUT_MS, |
| 375 | }), | 394 | }), |
| 376 | ), | 395 | ), |
| | 396 | onSome: Effect.succeed, |
| | 397 | }), |
| | 398 | ), |
| | 399 | Effect.mapError((cause) => |
| | 400 | isProcessDiagnosticsError(cause) |
| | 401 | ? cause |
| | 402 | : new ProcessDiagnosticsQueryFailedError({ |
| | 403 | command: input.command, |
| | 404 | argCount: input.args.length, |
| | 405 | cwd, |
| | 406 | cause, |
| | 407 | }), |
| 377 | ), | 408 | ), |
| 378 | ); | 409 | ); |
| | 410 | }); |
| 379 | | 411 | |
| 380 | function readPosixProcessRows(): Effect.Effect< | 412 | function readPosixProcessRows(): Effect.Effect< |
| 381 | ReadonlyArray<ProcessRow>, | 413 | ReadonlyArray<ProcessRow>, |
| ... | @@ -391,7 +423,13 @@ function readPosixProcessRows(): Effect.Effect< | ... | @@ -391,7 +423,13 @@ function readPosixProcessRows(): Effect.Effect< |
| 391 | ? Effect.fail( | 423 | ? Effect.fail( |
| 392 | new ProcessDiagnosticsQueryFailedError({ | 424 | new ProcessDiagnosticsQueryFailedError({ |
| 393 | command: "ps", | 425 | command: "ps", |
| 394 | stderr: result.stderr.trim() || "ps failed.", | 426 | argCount: 2, |
| | 427 | cwd: result.cwd, |
| | 428 | exitCode: result.exitCode, |
| | 429 | stdoutBytes: result.stdoutBytes, |
| | 430 | stderrBytes: result.stderrBytes, |
| | 431 | stdoutTruncated: result.stdoutTruncated, |
| | 432 | stderrTruncated: result.stderrTruncated, |
| 395 | }), | 433 | }), |
| 396 | ) | 434 | ) |
| 397 | : Effect.succeed(parsePosixProcessRows(result.stdout)), | 435 | : Effect.succeed(parsePosixProcessRows(result.stdout)), |
| ... | @@ -421,7 +459,13 @@ function readWindowsProcessRows(): Effect.Effect< | ... | @@ -421,7 +459,13 @@ function readWindowsProcessRows(): Effect.Effect< |
| 421 | ? Effect.fail( | 459 | ? Effect.fail( |
| 422 | new ProcessDiagnosticsQueryFailedError({ | 460 | new ProcessDiagnosticsQueryFailedError({ |
| 423 | command: "powershell.exe", | 461 | command: "powershell.exe", |
| 424 | stderr: result.stderr.trim() || "PowerShell process query failed.", | 462 | argCount: 4, |
| | 463 | cwd: result.cwd, |
| | 464 | exitCode: result.exitCode, |
| | 465 | stdoutBytes: result.stdoutBytes, |
| | 466 | stderrBytes: result.stderrBytes, |
| | 467 | stdoutTruncated: result.stdoutTruncated, |
| | 468 | stderrTruncated: result.stderrTruncated, |
| 425 | }), | 469 | }), |
| 426 | ) | 470 | ) |
| 427 | : Effect.succeed(parseWindowsProcessRows(result.stdout)), | 471 | : Effect.succeed(parseWindowsProcessRows(result.stdout)), |
| ... | @@ -464,6 +508,7 @@ function assertDescendantPid( | ... | @@ -464,6 +508,7 @@ function assertDescendantPid( |
| 464 | : Effect.fail( | 508 | : Effect.fail( |
| 465 | new ProcessDiagnosticsNotDescendantError({ | 509 | new ProcessDiagnosticsNotDescendantError({ |
| 466 | pid, | 510 | pid, |
| | 511 | serverPid: process.pid, |
| 467 | }), | 512 | }), |
| 468 | ); | 513 | ); |
| 469 | }), | 514 | }), |