| ... | ... | @@ -89,12 +89,21 @@ class TerminalSubprocessCheckError extends Schema.TaggedErrorClass<TerminalSubpr |
| 89 | 89 | "TerminalSubprocessCheckError", |
| 90 | 90 | { |
| 91 | 91 | cause: Schema.optional(Schema.Defect()), |
| 92 | | terminalPid: Schema.Number, |
| 93 | | command: Schema.Literals(["powershell", "pgrep", "ps"]), |
| 92 | command: Schema.Literals(["powershell", "ps"]), |
| 93 | exitCode: Schema.optional(Schema.NullOr(Schema.Number)), |
| 94 | timedOut: Schema.optional(Schema.Boolean), |
| 95 | stdoutTruncated: Schema.optional(Schema.Boolean), |
| 94 | 96 | }, |
| 95 | 97 | ) { |
| 96 | 98 | override get message(): string { |
| 97 | | return `Failed to inspect terminal subprocesses for PID ${this.terminalPid} with ${this.command}`; |
| 99 | const details = [ |
| 100 | this.exitCode !== undefined && this.exitCode !== null ? `exit code ${this.exitCode}` : null, |
| 101 | this.timedOut ? "timed out" : null, |
| 102 | this.stdoutTruncated ? "output truncated" : null, |
| 103 | ] |
| 104 | .filter((detail) => detail !== null) |
| 105 | .join(", "); |
| 106 | return `Failed to inspect terminal subprocesses with ${this.command}${details.length > 0 ? ` (${details})` : ""}`; |
| 98 | 107 | } |
| 99 | 108 | } |
| 100 | 109 | |
| ... | ... | @@ -610,125 +619,102 @@ function isRetryableShellSpawnError(error: PtyAdapter.PtySpawnError): boolean { |
| 610 | 619 | ); |
| 611 | 620 | } |
| 612 | 621 | |
| 613 | | function parseFirstChildPidFromPgrep(stdout: string): number | null { |
| 622 | interface TerminalProcessTableSnapshot { |
| 623 | readonly childrenByParent: ReadonlyMap<number, ReadonlyArray<number>>; |
| 624 | readonly commandById: ReadonlyMap<number, string>; |
| 625 | } |
| 626 | |
| 627 | function parsePosixProcessTable(stdout: string): TerminalProcessTableSnapshot { |
| 628 | const childrenByParent = new Map<number, number[]>(); |
| 629 | const commandById = new Map<number, string>(); |
| 614 | 630 | for (const line of stdout.split(/\r?\n/g)) { |
| 615 | | const n = Number.parseInt(line.trim(), 10); |
| 616 | | if (Number.isInteger(n) && n > 0) { |
| 617 | | return n; |
| 618 | | } |
| 631 | // `comm=` is the final column and may itself contain spaces, so only the |
| 632 | // first two tokens are structural. |
| 633 | const match = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(line); |
| 634 | if (!match) continue; |
| 635 | const pid = Number(match[1]); |
| 636 | const ppid = Number(match[2]); |
| 637 | if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue; |
| 638 | commandById.set(pid, (match[3] ?? "").trim()); |
| 639 | const children = childrenByParent.get(ppid) ?? []; |
| 640 | children.push(pid); |
| 641 | childrenByParent.set(ppid, children); |
| 619 | 642 | } |
| 620 | | return null; |
| 643 | return { childrenByParent, commandById }; |
| 621 | 644 | } |
| 622 | 645 | |
| 623 | | function windowsInspectSubprocess( |
| 624 | | terminalPid: number, |
| 625 | | platform: NodeJS.Platform, |
| 626 | | ): Effect.Effect< |
| 627 | | TerminalSubprocessInspectResult, |
| 628 | | TerminalSubprocessCheckError, |
| 629 | | ProcessRunner.ProcessRunner |
| 630 | | > { |
| 631 | | const command = |
| 632 | | 'Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.Name)" }'; |
| 633 | | return Effect.gen(function* () { |
| 634 | | const processRunner = yield* ProcessRunner.ProcessRunner; |
| 635 | | return yield* processRunner.run({ |
| 636 | | // powershell.exe is a real executable — never spawn it through cmd.exe |
| 637 | | // shell mode, which would re-tokenize the `-Command` payload (pipes, |
| 638 | | // semicolons) before PowerShell ever sees it. |
| 639 | | command: "powershell.exe", |
| 640 | | args: ["-NoProfile", "-NonInteractive", "-Command", command], |
| 641 | | timeout: "1500 millis", |
| 642 | | maxOutputBytes: 32_768, |
| 643 | | outputMode: "truncate", |
| 644 | | timeoutBehavior: "timedOutResult", |
| 645 | | }); |
| 646 | | }).pipe( |
| 647 | | Effect.map((result) => { |
| 648 | | if (result.code !== 0) { |
| 649 | | return { hasRunningSubprocess: false, childCommand: null, processIds: [] } as const; |
| 650 | | } |
| 651 | | const processNameById = new Map<number, string>(); |
| 652 | | const childrenByParent = new Map<number, number[]>(); |
| 653 | | for (const line of result.stdout.split(/\r?\n/g)) { |
| 654 | | const [pidRaw, parentPidRaw, nameRaw] = line.trim().split("|", 3); |
| 655 | | const pid = Number(pidRaw); |
| 656 | | const parentPid = Number(parentPidRaw); |
| 657 | | if (!Number.isInteger(pid) || !Number.isInteger(parentPid)) continue; |
| 658 | | processNameById.set(pid, nameRaw?.trim() ?? ""); |
| 659 | | const children = childrenByParent.get(parentPid) ?? []; |
| 660 | | children.push(pid); |
| 661 | | childrenByParent.set(parentPid, children); |
| 662 | | } |
| 663 | | const directChildren = childrenByParent.get(terminalPid) ?? []; |
| 664 | | const childPid = directChildren[0]; |
| 665 | | if (childPid === undefined) { |
| 666 | | return { hasRunningSubprocess: false, childCommand: null, processIds: [] } as const; |
| 667 | | } |
| 668 | | const processIds = new Set<number>([terminalPid]); |
| 669 | | const pending = [terminalPid]; |
| 670 | | while (pending.length > 0) { |
| 671 | | const parentPid = pending.pop(); |
| 672 | | if (parentPid === undefined) continue; |
| 673 | | for (const pid of childrenByParent.get(parentPid) ?? []) { |
| 674 | | if (processIds.has(pid)) continue; |
| 675 | | processIds.add(pid); |
| 676 | | pending.push(pid); |
| 677 | | } |
| 678 | | } |
| 679 | | const normalized = normalizeChildCommandName(processNameById.get(childPid) ?? "", platform); |
| 680 | | return { |
| 681 | | hasRunningSubprocess: true, |
| 682 | | childCommand: normalized ? truncateTerminalWireLabel(normalized) : null, |
| 683 | | processIds: [...processIds], |
| 684 | | } as const; |
| 685 | | }), |
| 686 | | Effect.mapError( |
| 687 | | (cause) => |
| 688 | | new TerminalSubprocessCheckError({ |
| 689 | | cause, |
| 690 | | terminalPid, |
| 691 | | command: "powershell", |
| 692 | | }), |
| 693 | | ), |
| 694 | | ); |
| 646 | function parseWindowsProcessTable(stdout: string): TerminalProcessTableSnapshot { |
| 647 | const childrenByParent = new Map<number, number[]>(); |
| 648 | const commandById = new Map<number, string>(); |
| 649 | for (const line of stdout.split(/\r?\n/g)) { |
| 650 | const [pidRaw, parentPidRaw, nameRaw] = line.trim().split("|", 3); |
| 651 | const pid = Number(pidRaw); |
| 652 | const parentPid = Number(parentPidRaw); |
| 653 | if (!Number.isInteger(pid) || !Number.isInteger(parentPid)) continue; |
| 654 | commandById.set(pid, nameRaw?.trim() ?? ""); |
| 655 | const children = childrenByParent.get(parentPid) ?? []; |
| 656 | children.push(pid); |
| 657 | childrenByParent.set(parentPid, children); |
| 658 | } |
| 659 | return { childrenByParent, commandById }; |
| 695 | 660 | } |
| 696 | 661 | |
| 697 | | const posixInspectSubprocess = Effect.fn("terminal.posixInspectSubprocess")(function* ( |
| 662 | function deriveSubprocessInspectResult( |
| 663 | snapshot: TerminalProcessTableSnapshot, |
| 698 | 664 | terminalPid: number, |
| 699 | 665 | platform: NodeJS.Platform, |
| 666 | ): TerminalSubprocessInspectResult { |
| 667 | const childPid = (snapshot.childrenByParent.get(terminalPid) ?? [])[0]; |
| 668 | if (childPid === undefined) { |
| 669 | return { hasRunningSubprocess: false, childCommand: null, processIds: [] }; |
| 670 | } |
| 671 | const processIds = new Set<number>([terminalPid]); |
| 672 | const pending = [terminalPid]; |
| 673 | while (pending.length > 0) { |
| 674 | const parentPid = pending.pop(); |
| 675 | if (parentPid === undefined) continue; |
| 676 | for (const pid of snapshot.childrenByParent.get(parentPid) ?? []) { |
| 677 | if (processIds.has(pid)) continue; |
| 678 | processIds.add(pid); |
| 679 | pending.push(pid); |
| 680 | } |
| 681 | } |
| 682 | const normalized = normalizeChildCommandName(snapshot.commandById.get(childPid) ?? "", platform); |
| 683 | return { |
| 684 | hasRunningSubprocess: true, |
| 685 | childCommand: normalized ? truncateTerminalWireLabel(normalized) : null, |
| 686 | processIds: [...processIds], |
| 687 | }; |
| 688 | } |
| 689 | |
| 690 | const POSIX_PS_ABSOLUTE_PATHS = ["/bin/ps", "/usr/bin/ps"] as const; |
| 691 | |
| 692 | // Resolve `ps` to an absolute path once at startup. Spawning by bare name |
| 693 | // walks every PATH entry per spawn (one failed posix_spawn per directory |
| 694 | // until the hit), which is measurable at a 1s poll cadence on long PATHs. |
| 695 | const resolvePosixPsCommand = Effect.fn("terminal.resolvePosixPsCommand")(function* () { |
| 696 | const fileSystem = yield* FileSystem.FileSystem; |
| 697 | for (const candidate of POSIX_PS_ABSOLUTE_PATHS) { |
| 698 | const exists = yield* fileSystem.exists(candidate).pipe(Effect.orElseSucceed(() => false)); |
| 699 | if (exists) return candidate; |
| 700 | } |
| 701 | return "ps"; |
| 702 | }); |
| 703 | |
| 704 | const posixProcessTableSnapshot = Effect.fn("terminal.posixProcessTableSnapshot")(function* ( |
| 705 | psCommand: string, |
| 700 | 706 | ): Effect.fn.Return< |
| 701 | | TerminalSubprocessInspectResult, |
| 707 | TerminalProcessTableSnapshot, |
| 702 | 708 | TerminalSubprocessCheckError, |
| 703 | 709 | ProcessRunner.ProcessRunner |
| 704 | 710 | > { |
| 705 | 711 | const processRunner = yield* ProcessRunner.ProcessRunner; |
| 706 | | const runPgrep = processRunner |
| 707 | | .run({ |
| 708 | | command: "pgrep", |
| 709 | | args: ["-P", String(terminalPid)], |
| 710 | | timeout: "1 second", |
| 711 | | maxOutputBytes: 32_768, |
| 712 | | outputMode: "truncate", |
| 713 | | timeoutBehavior: "timedOutResult", |
| 714 | | }) |
| 715 | | .pipe( |
| 716 | | Effect.mapError( |
| 717 | | (cause) => |
| 718 | | new TerminalSubprocessCheckError({ |
| 719 | | cause, |
| 720 | | terminalPid, |
| 721 | | command: "pgrep", |
| 722 | | }), |
| 723 | | ), |
| 724 | | ); |
| 725 | | |
| 726 | | const runPs = processRunner |
| 712 | const result = yield* processRunner |
| 727 | 713 | .run({ |
| 728 | | command: "ps", |
| 729 | | args: ["-eo", "pid=,ppid="], |
| 714 | command: psCommand, |
| 715 | args: ["-eo", "pid=,ppid=,comm="], |
| 730 | 716 | timeout: "1 second", |
| 731 | | maxOutputBytes: 262_144, |
| 717 | maxOutputBytes: 524_288, |
| 732 | 718 | outputMode: "truncate", |
| 733 | 719 | timeoutBehavior: "timedOutResult", |
| 734 | 720 | }) |
| ... | ... | @@ -737,120 +723,66 @@ const posixInspectSubprocess = Effect.fn("terminal.posixInspectSubprocess")(func |
| 737 | 723 | (cause) => |
| 738 | 724 | new TerminalSubprocessCheckError({ |
| 739 | 725 | cause, |
| 740 | | terminalPid, |
| 741 | 726 | command: "ps", |
| 742 | 727 | }), |
| 743 | 728 | ), |
| 744 | 729 | ); |
| 745 | | |
| 746 | | let childPid: number | null = null; |
| 747 | | |
| 748 | | const pgrepResult = yield* Effect.exit(runPgrep); |
| 749 | | if (pgrepResult._tag === "Success") { |
| 750 | | if (pgrepResult.value.code === 0) { |
| 751 | | childPid = parseFirstChildPidFromPgrep(pgrepResult.value.stdout); |
| 752 | | } else if (pgrepResult.value.code === 1) { |
| 753 | | return { hasRunningSubprocess: false, childCommand: null, processIds: [] }; |
| 754 | | } |
| 755 | | } |
| 756 | | |
| 757 | | if (childPid === null) { |
| 758 | | const psResult = yield* Effect.exit(runPs); |
| 759 | | if (psResult._tag === "Failure" || psResult.value.code !== 0) { |
| 760 | | return { hasRunningSubprocess: false, childCommand: null, processIds: [] }; |
| 761 | | } |
| 762 | | for (const line of psResult.value.stdout.split(/\r?\n/g)) { |
| 763 | | const [pidRaw, ppidRaw] = line.trim().split(/\s+/g); |
| 764 | | const pid = Number(pidRaw); |
| 765 | | const ppid = Number(ppidRaw); |
| 766 | | if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue; |
| 767 | | if (ppid === terminalPid) { |
| 768 | | childPid = pid; |
| 769 | | break; |
| 770 | | } |
| 771 | | } |
| 772 | | } |
| 773 | | |
| 774 | | if (childPid === null) { |
| 775 | | return { hasRunningSubprocess: false, childCommand: null, processIds: [] }; |
| 776 | | } |
| 777 | | |
| 778 | | const runComm = processRunner.run({ |
| 779 | | command: "ps", |
| 780 | | args: ["-p", String(childPid), "-o", "comm="], |
| 781 | | timeout: "1 second", |
| 782 | | maxOutputBytes: 8_192, |
| 783 | | outputMode: "truncate", |
| 784 | | timeoutBehavior: "timedOutResult", |
| 785 | | }); |
| 786 | | |
| 787 | | const commResult = yield* Effect.exit(runComm); |
| 788 | | let rawComm: string | null = null; |
| 789 | | if (commResult._tag === "Success" && commResult.value && commResult.value.code === 0) { |
| 790 | | rawComm = commResult.value.stdout.trim(); |
| 791 | | } |
| 792 | | |
| 793 | | if (!rawComm || rawComm.length === 0) { |
| 794 | | const runArgs = processRunner.run({ |
| 730 | if (result.code !== 0 || result.timedOut || result.stdoutTruncated) { |
| 731 | // Not authoritative: an empty or partial table would mark every terminal |
| 732 | // idle and clear its registered process ids. Failing skips the tick. |
| 733 | return yield* new TerminalSubprocessCheckError({ |
| 795 | 734 | command: "ps", |
| 796 | | args: ["-p", String(childPid), "-o", "args="], |
| 797 | | timeout: "1 second", |
| 798 | | maxOutputBytes: 16_384, |
| 799 | | outputMode: "truncate", |
| 800 | | timeoutBehavior: "timedOutResult", |
| 735 | exitCode: result.code, |
| 736 | timedOut: result.timedOut, |
| 737 | stdoutTruncated: result.stdoutTruncated, |
| 801 | 738 | }); |
| 802 | | const argsResult = yield* Effect.exit(runArgs); |
| 803 | | if (argsResult._tag === "Success" && argsResult.value && argsResult.value.code === 0) { |
| 804 | | const first = argsResult.value.stdout.trim().split(/\s+/)[0] ?? ""; |
| 805 | | rawComm = first.length > 0 ? first : null; |
| 806 | | } |
| 807 | 739 | } |
| 808 | | |
| 809 | | const normalized = rawComm ? normalizeChildCommandName(rawComm, platform) : null; |
| 810 | | const processIds = new Set<number>([terminalPid]); |
| 811 | | const psResult = yield* Effect.exit(runPs); |
| 812 | | if (psResult._tag === "Success" && psResult.value.code === 0) { |
| 813 | | const childrenByParent = new Map<number, number[]>(); |
| 814 | | for (const line of psResult.value.stdout.split(/\r?\n/g)) { |
| 815 | | const [pidRaw, ppidRaw] = line.trim().split(/\s+/g); |
| 816 | | const pid = Number(pidRaw); |
| 817 | | const ppid = Number(ppidRaw); |
| 818 | | if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue; |
| 819 | | const children = childrenByParent.get(ppid) ?? []; |
| 820 | | children.push(pid); |
| 821 | | childrenByParent.set(ppid, children); |
| 822 | | } |
| 823 | | const pending = [terminalPid]; |
| 824 | | while (pending.length > 0) { |
| 825 | | const parentPid = pending.pop(); |
| 826 | | if (parentPid === undefined) continue; |
| 827 | | for (const child of childrenByParent.get(parentPid) ?? []) { |
| 828 | | if (processIds.has(child)) continue; |
| 829 | | processIds.add(child); |
| 830 | | pending.push(child); |
| 831 | | } |
| 832 | | } |
| 833 | | } else { |
| 834 | | processIds.add(childPid); |
| 835 | | } |
| 836 | | return { |
| 837 | | hasRunningSubprocess: true, |
| 838 | | childCommand: normalized ? truncateTerminalWireLabel(normalized) : null, |
| 839 | | processIds: [...processIds], |
| 840 | | }; |
| 740 | return parsePosixProcessTable(result.stdout); |
| 841 | 741 | }); |
| 842 | 742 | |
| 843 | | function defaultSubprocessInspectorForPlatform(platform: NodeJS.Platform) { |
| 844 | | return Effect.fn("terminal.defaultSubprocessInspector")(function* (terminalPid: number) { |
| 845 | | if (!Number.isInteger(terminalPid) || terminalPid <= 0) { |
| 846 | | return { hasRunningSubprocess: false, childCommand: null, processIds: [] }; |
| 847 | | } |
| 848 | | if (platform === "win32") { |
| 849 | | return yield* windowsInspectSubprocess(terminalPid, platform); |
| 743 | const windowsProcessTableSnapshot = Effect.fn("terminal.windowsProcessTableSnapshot")( |
| 744 | function* (): Effect.fn.Return< |
| 745 | TerminalProcessTableSnapshot, |
| 746 | TerminalSubprocessCheckError, |
| 747 | ProcessRunner.ProcessRunner |
| 748 | > { |
| 749 | const command = |
| 750 | 'Get-CimInstance Win32_Process -ErrorAction Stop | ForEach-Object { Write-Output "$($_.ProcessId)|$($_.ParentProcessId)|$($_.Name)" }'; |
| 751 | const processRunner = yield* ProcessRunner.ProcessRunner; |
| 752 | const result = yield* processRunner |
| 753 | .run({ |
| 754 | // powershell.exe is a real executable — never spawn it through cmd.exe |
| 755 | // shell mode, which would re-tokenize the `-Command` payload (pipes, |
| 756 | // semicolons) before PowerShell ever sees it. |
| 757 | command: "powershell.exe", |
| 758 | args: ["-NoProfile", "-NonInteractive", "-Command", command], |
| 759 | timeout: "1500 millis", |
| 760 | maxOutputBytes: 262_144, |
| 761 | outputMode: "truncate", |
| 762 | timeoutBehavior: "timedOutResult", |
| 763 | }) |
| 764 | .pipe( |
| 765 | Effect.mapError( |
| 766 | (cause) => |
| 767 | new TerminalSubprocessCheckError({ |
| 768 | cause, |
| 769 | command: "powershell", |
| 770 | }), |
| 771 | ), |
| 772 | ); |
| 773 | if (result.code !== 0 || result.timedOut || result.stdoutTruncated) { |
| 774 | // Not authoritative: an empty or partial table would mark every terminal |
| 775 | // idle and clear its registered process ids. Failing skips the tick. |
| 776 | return yield* new TerminalSubprocessCheckError({ |
| 777 | command: "powershell", |
| 778 | exitCode: result.code, |
| 779 | timedOut: result.timedOut, |
| 780 | stdoutTruncated: result.stdoutTruncated, |
| 781 | }); |
| 850 | 782 | } |
| 851 | | return yield* posixInspectSubprocess(terminalPid, platform); |
| 852 | | }); |
| 853 | | } |
| 783 | return parseWindowsProcessTable(result.stdout); |
| 784 | }, |
| 785 | ); |
| 854 | 786 | |
| 855 | 787 | function capHistory(history: string, maxLines: number): string { |
| 856 | 788 | if (history.length === 0) return history; |
| ... | ... | @@ -1227,12 +1159,27 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 1227 | 1159 | const baseEnv = options.env ?? process.env; |
| 1228 | 1160 | const shellResolver = options.shellResolver ?? (() => defaultShellResolver(platform, baseEnv)); |
| 1229 | 1161 | const processRunner = yield* ProcessRunner.ProcessRunner; |
| 1230 | | const subprocessInspector = |
| 1231 | | options.subprocessInspector ?? |
| 1232 | | ((terminalPid) => |
| 1233 | | defaultSubprocessInspectorForPlatform(platform)(terminalPid).pipe( |
| 1234 | | Effect.provideService(ProcessRunner.ProcessRunner, processRunner), |
| 1235 | | )); |
| 1162 | // One process-table snapshot per poll tick, shared across every terminal. |
| 1163 | // Per-terminal `pgrep`/`ps` calls multiply spawn load by terminal count and |
| 1164 | // can exhaust the PID space on hosts with many sessions (#6332). |
| 1165 | const fetchProcessTableSnapshot = ( |
| 1166 | platform === "win32" |
| 1167 | ? windowsProcessTableSnapshot() |
| 1168 | : posixProcessTableSnapshot(yield* resolvePosixPsCommand()) |
| 1169 | ).pipe(Effect.provideService(ProcessRunner.ProcessRunner, processRunner)); |
| 1170 | const customSubprocessInspector = options.subprocessInspector; |
| 1171 | const acquireSubprocessInspector: Effect.Effect< |
| 1172 | TerminalSubprocessInspector, |
| 1173 | TerminalSubprocessCheckError |
| 1174 | > = |
| 1175 | customSubprocessInspector !== undefined |
| 1176 | ? Effect.succeed(customSubprocessInspector) |
| 1177 | : Effect.map( |
| 1178 | fetchProcessTableSnapshot, |
| 1179 | (snapshot): TerminalSubprocessInspector => |
| 1180 | (terminalPid) => |
| 1181 | Effect.succeed(deriveSubprocessInspectResult(snapshot, terminalPid, platform)), |
| 1182 | ); |
| 1236 | 1183 | const subprocessPollIntervalMs = |
| 1237 | 1184 | options.subprocessPollIntervalMs ?? DEFAULT_SUBPROCESS_POLL_INTERVAL_MS; |
| 1238 | 1185 | const processKillGraceMs = options.processKillGraceMs ?? DEFAULT_PROCESS_KILL_GRACE_MS; |
| ... | ... | @@ -2064,6 +2011,21 @@ export const makeWithOptions = Effect.fn("TerminalManager.makeWithOptions")(func |
| 2064 | 2011 | return; |
| 2065 | 2012 | } |
| 2066 | 2013 | |
| 2014 | const inspectorOption = yield* acquireSubprocessInspector.pipe( |
| 2015 | Effect.map(Option.some), |
| 2016 | Effect.catch((reason) => |
| 2017 | Effect.logWarning("failed to snapshot processes for terminal subprocess polling", { |
| 2018 | reason, |
| 2019 | }).pipe(Effect.as(Option.none<TerminalSubprocessInspector>())), |
| 2020 | ), |
| 2021 | ); |
| 2022 | |
| 2023 | if (Option.isNone(inspectorOption)) { |
| 2024 | return; |
| 2025 | } |
| 2026 | |
| 2027 | const subprocessInspector = inspectorOption.value; |
| 2028 | |
| 2067 | 2029 | const checkSubprocessActivity = Effect.fn("terminal.checkSubprocessActivity")(function* ( |
| 2068 | 2030 | session: TerminalSessionState & { pid: number }, |
| 2069 | 2031 | ) { |