|
| 1 | +/** What `pty kill` may claim, and how it says it. |
| 2 | + * |
| 3 | + * The command signals the daemon and waits for that one PID. The child, and |
| 4 | + * everything the child started, is a separate question. This module answers it |
| 5 | + * from a snapshot taken before the signal, and turns the answer into the lines |
| 6 | + * the command prints. |
| 7 | + * |
| 8 | + * Kept out of `cli.ts` because that module runs `main()` on import and cannot |
| 9 | + * be loaded by a test. |
| 10 | + */ |
| 11 | + |
| 12 | +import type { ProcessIdentity } from "./process-tree.ts"; |
| 13 | + |
| 14 | +/** What the pre-kill snapshot looks like once the daemon has gone. */ |
| 15 | +export interface Aftermath { |
| 16 | + /** The start token still matches, so this is the same process and it is |
| 17 | + * still running. */ |
| 18 | + survived: number[]; |
| 19 | + /** The PID has not exited but its start token could not be read. We cannot |
| 20 | + * tell whether it is the same process or a PID the kernel has reused. |
| 21 | + * |
| 22 | + * This case gets its own list rather than joining either side. Folding it |
| 23 | + * into `survived` would invent a survivor; dropping it would repeat the |
| 24 | + * defect this module exists to remove, which is a failure to measure |
| 25 | + * reported as an answer. */ |
| 26 | + unknown: number[]; |
| 27 | +} |
| 28 | + |
| 29 | +export function allGone(after: Aftermath): boolean { |
| 30 | + return after.survived.length === 0 && after.unknown.length === 0; |
| 31 | +} |
| 32 | + |
| 33 | +/** Re-check a snapshot against the live process table. |
| 34 | + * |
| 35 | + * `exited` must be `hasProcessExitedForReap`, not `!isProcessAlive`. A zombie |
| 36 | + * answers `kill(pid, 0)` and keeps a readable start token, so the two cheaper |
| 37 | + * predicates both call it a survivor. It is a dead process waiting to be |
| 38 | + * reaped, and reporting it as still running would be this command |
| 39 | + * over-claiming again, only in the other direction. |
| 40 | + */ |
| 41 | +export function aftermathOf( |
| 42 | + before: ProcessIdentity[], |
| 43 | + readStartToken: (pid: number) => string | null, |
| 44 | + exited: (pid: number) => boolean, |
| 45 | +): Aftermath { |
| 46 | + const after: Aftermath = { survived: [], unknown: [] }; |
| 47 | + for (const identity of before) { |
| 48 | + if (exited(identity.pid)) continue; |
| 49 | + const token = readStartToken(identity.pid); |
| 50 | + if (token === identity.processStartToken) after.survived.push(identity.pid); |
| 51 | + // A different token is a PID the kernel handed to somebody else. |
| 52 | + else if (token === null) after.unknown.push(identity.pid); |
| 53 | + } |
| 54 | + return after; |
| 55 | +} |
| 56 | + |
| 57 | +/** Say what was verified, and nothing more. |
| 58 | + * |
| 59 | + * `killed` is a claim about the whole tree, so it appears only when every |
| 60 | + * process in the snapshot is gone. Otherwise standard output carries the part |
| 61 | + * that was verified — the daemon stopped — and standard error carries what |
| 62 | + * survived it. The two never appear together, so a reader who greps for the |
| 63 | + * success line cannot find it beside a warning that contradicts it. |
| 64 | + */ |
| 65 | +export function killOutcomeLines( |
| 66 | + name: string, |
| 67 | + after: Aftermath, |
| 68 | +): { out: string[]; err: string[] } { |
| 69 | + if (allGone(after)) return { out: [`Session "${name}" killed.`], err: [] }; |
| 70 | + const err: string[] = []; |
| 71 | + if (after.survived.length > 0) { |
| 72 | + err.push( |
| 73 | + `Session "${name}": ${after.survived.length} process(es) survived the kill ` + |
| 74 | + `and are still running: ${after.survived.join(", ")}`, |
| 75 | + ); |
| 76 | + } |
| 77 | + if (after.unknown.length > 0) { |
| 78 | + err.push( |
| 79 | + `Session "${name}": ${after.unknown.length} process(es) may still be running: ` + |
| 80 | + `${after.unknown.join(", ")}. Their start tokens could not be read, so this ` + |
| 81 | + "is not a conclusion.", |
| 82 | + ); |
| 83 | + } |
| 84 | + return { out: [`Session "${name}" daemon stopped.`], err }; |
| 85 | +} |
0 commit comments