@wringer/core 0.0.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +71 -2
- package/dist/answer-supervisor-connection.d.ts +8 -0
- package/dist/app-machine.d.ts +76 -0
- package/dist/append-run-event.d.ts +16 -0
- package/dist/bind-drivers.d.ts +9 -0
- package/dist/build-commands.d.ts +5 -0
- package/dist/build-envelope-json-schema.d.ts +1 -0
- package/dist/build-envelope.d.ts +16 -0
- package/dist/build-launch-guard.d.ts +1 -0
- package/dist/build-manifest-write.d.ts +6 -0
- package/dist/build-promised-object-reads.d.ts +7 -0
- package/dist/build-report.d.ts +10 -0
- package/dist/build-run-handle.d.ts +2 -0
- package/dist/build-run-view.d.ts +29 -0
- package/dist/build-service-key.d.ts +1 -0
- package/dist/build-shut-down-steps.d.ts +14 -0
- package/dist/build-skip-worktree-marks.d.ts +7 -0
- package/dist/build-spawn-error.d.ts +2 -0
- package/dist/build-state-paths.d.ts +21 -0
- package/dist/build-submodule-adds.d.ts +7 -0
- package/dist/builtins/build-doctor-command.d.ts +2 -0
- package/dist/builtins/build-exec-command.d.ts +2 -0
- package/dist/builtins/build-lifecycle-commands.d.ts +3 -0
- package/dist/builtins/build-state-commands.d.ts +2 -0
- package/dist/builtins/build-use-pointers.d.ts +9 -0
- package/dist/builtins/build-verdict-commands.d.ts +2 -0
- package/dist/builtins/call-run-supervisor.d.ts +5 -0
- package/dist/builtins/pick-doctor-outcome.d.ts +13 -0
- package/dist/builtins/pick-start-action.d.ts +10 -0
- package/dist/builtins/pick-stop-plan.d.ts +14 -0
- package/dist/builtins/start-run.d.ts +22 -0
- package/dist/builtins/stop-data-schema.d.ts +6 -0
- package/dist/builtins/stop-run.d.ts +3 -0
- package/dist/call-supervisor.d.ts +10 -0
- package/dist/cap-data.d.ts +17 -0
- package/dist/collect-driver-configs.d.ts +7 -0
- package/dist/create-directory.d.ts +1 -0
- package/dist/create-evidence-writer.d.ts +13 -0
- package/dist/create-file-state.d.ts +36 -0
- package/dist/create-log-writer.d.ts +5 -0
- package/dist/create-node-repo-host.d.ts +2 -0
- package/dist/create-node-state-file-system.d.ts +10 -0
- package/dist/create-repo-state.d.ts +2 -0
- package/dist/create-socket-folder.d.ts +1 -0
- package/dist/create-wringer.d.ts +17 -0
- package/dist/decide-guard.d.ts +12 -0
- package/dist/define-driver.d.ts +13 -0
- package/dist/dispatch.d.ts +10 -0
- package/dist/envelope.d.ts +63 -0
- package/dist/error-codes.d.ts +25 -0
- package/dist/find-cancel-failure.d.ts +2 -0
- package/dist/find-colliding-service-key.d.ts +5 -0
- package/dist/find-own-value.d.ts +1 -0
- package/dist/find-remote-stop-failure.d.ts +2 -0
- package/dist/find-verdict-refusal.d.ts +3 -0
- package/dist/follow-process.d.ts +10 -0
- package/dist/get-lock-value.d.ts +2 -0
- package/dist/get-target.d.ts +2 -0
- package/dist/host.d.ts +19 -0
- package/dist/index.d.ts +58 -0
- package/dist/is-error-code.d.ts +1 -0
- package/dist/keep-cancelled-call.d.ts +1 -0
- package/dist/kill-process-group.d.ts +1 -0
- package/dist/launch-line-schema.d.ts +3 -0
- package/dist/listen-supervisor-socket.d.ts +8 -0
- package/dist/local.d.ts +2 -0
- package/dist/make-fence.d.ts +8 -0
- package/dist/make-fenced-exec.d.ts +10 -0
- package/dist/make-process-supervisor-port.d.ts +7 -0
- package/dist/open-drive.d.ts +23 -0
- package/dist/open-runless-drive.d.ts +17 -0
- package/dist/parse-launch-line.d.ts +2 -0
- package/dist/parse-patch-id.d.ts +2 -0
- package/dist/parse-report-block.d.ts +9 -0
- package/dist/parse-run-log.d.ts +2 -0
- package/dist/pick-access-refusal.d.ts +12 -0
- package/dist/pick-command-needs.d.ts +15 -0
- package/dist/pick-command-plan.d.ts +23 -0
- package/dist/pick-context-target.d.ts +7 -0
- package/dist/pick-drive-kind.d.ts +8 -0
- package/dist/pick-envelope-context.d.ts +1 -0
- package/dist/pick-envelope-payload.d.ts +12 -0
- package/dist/pick-envelope-run.d.ts +6 -0
- package/dist/pick-envelope-target.d.ts +4 -0
- package/dist/pick-late-failure-write.d.ts +13 -0
- package/dist/pick-log-refusal.d.ts +4 -0
- package/dist/pick-merge-base-failure.d.ts +2 -0
- package/dist/pick-merge-base-steps.d.ts +13 -0
- package/dist/pick-pending-cleanups.d.ts +10 -0
- package/dist/pick-submodule-refusal.d.ts +2 -0
- package/dist/pick-supervisor-operation.d.ts +16 -0
- package/dist/prepare-args.d.ts +3 -0
- package/dist/probe-process.d.ts +1 -0
- package/dist/quote-shell-word.d.ts +1 -0
- package/dist/race-deadline.d.ts +1 -0
- package/dist/read-changed-paths.d.ts +7 -0
- package/dist/read-default-base.d.ts +2 -0
- package/dist/read-fingerprint.d.ts +13 -0
- package/dist/read-head-sha.d.ts +2 -0
- package/dist/read-launch-host.d.ts +26 -0
- package/dist/read-pid-namespace.d.ts +1 -0
- package/dist/read-stream-line-batches.d.ts +2 -0
- package/dist/read-stream-lines.d.ts +2 -0
- package/dist/read-text.d.ts +1 -0
- package/dist/read-verdicts.d.ts +5 -0
- package/dist/remove-directory.d.ts +1 -0
- package/dist/remove-file.d.ts +1 -0
- package/dist/render-report-markdown.d.ts +4 -0
- package/dist/report-schema.d.ts +51 -0
- package/dist/run-events.d.ts +129 -0
- package/dist/run-git.d.ts +9 -0
- package/dist/run-process.d.ts +19 -0
- package/dist/run-supervisor-process.d.ts +2 -0
- package/dist/run-supervisor.d.ts +16 -0
- package/dist/settle-dispatch-answer.d.ts +9 -0
- package/dist/settle-envelope.d.ts +16 -0
- package/dist/ssh.d.ts +12 -0
- package/dist/start-launch.d.ts +20 -0
- package/dist/start-services.d.ts +16 -0
- package/dist/supervisor-machine.d.ts +78 -0
- package/dist/supervisor-protocol.d.ts +43 -0
- package/dist/to-wringer-error.d.ts +2 -0
- package/dist/types.d.ts +353 -0
- package/dist/wait-for-cancelled-calls.d.ts +2 -0
- package/dist/wringer-error.d.ts +12 -0
- package/dist/write-envelope-log.d.ts +11 -0
- package/dist/write-late-failure.d.ts +11 -0
- package/dist/write-new-file.d.ts +1 -0
- package/envelope.schema.json +175 -0
- package/package.json +21 -5
- package/src/answer-supervisor-connection.ts +117 -0
- package/src/app-machine.ts +174 -0
- package/src/append-run-event.ts +131 -0
- package/src/bind-drivers.ts +33 -0
- package/src/build-commands.ts +104 -0
- package/src/build-envelope-json-schema.ts +34 -0
- package/src/build-envelope.ts +40 -0
- package/src/build-launch-guard.ts +5 -0
- package/src/build-manifest-write.ts +26 -0
- package/src/build-promised-object-reads.ts +48 -0
- package/src/build-report.ts +55 -0
- package/src/build-run-handle.ts +13 -0
- package/src/build-run-view.ts +120 -0
- package/src/build-service-key.ts +4 -0
- package/src/build-shut-down-steps.ts +54 -0
- package/src/build-skip-worktree-marks.ts +36 -0
- package/src/build-spawn-error.ts +12 -0
- package/src/build-state-paths.ts +53 -0
- package/src/build-submodule-adds.ts +64 -0
- package/src/builtins/build-doctor-command.ts +163 -0
- package/src/builtins/build-exec-command.ts +41 -0
- package/src/builtins/build-lifecycle-commands.ts +115 -0
- package/src/builtins/build-state-commands.ts +152 -0
- package/src/builtins/build-use-pointers.ts +26 -0
- package/src/builtins/build-verdict-commands.ts +265 -0
- package/src/builtins/call-run-supervisor.ts +40 -0
- package/src/builtins/pick-doctor-outcome.ts +32 -0
- package/src/builtins/pick-start-action.ts +35 -0
- package/src/builtins/pick-stop-plan.ts +32 -0
- package/src/builtins/start-run.ts +281 -0
- package/src/builtins/stop-data-schema.ts +8 -0
- package/src/builtins/stop-run.ts +49 -0
- package/src/call-supervisor.ts +109 -0
- package/src/cap-data.ts +43 -0
- package/src/collect-driver-configs.ts +36 -0
- package/src/create-directory.ts +6 -0
- package/src/create-evidence-writer.ts +82 -0
- package/src/create-file-state.ts +193 -0
- package/src/create-log-writer.ts +38 -0
- package/src/create-node-repo-host.ts +20 -0
- package/src/create-node-state-file-system.ts +52 -0
- package/src/create-repo-state.ts +15 -0
- package/src/create-socket-folder.ts +24 -0
- package/src/create-wringer.ts +169 -0
- package/src/decide-guard.ts +47 -0
- package/src/define-driver.ts +165 -0
- package/src/dispatch.ts +589 -0
- package/src/envelope.ts +57 -0
- package/src/error-codes.ts +39 -0
- package/src/find-cancel-failure.ts +7 -0
- package/src/find-colliding-service-key.ts +16 -0
- package/src/find-own-value.ts +8 -0
- package/src/find-remote-stop-failure.ts +19 -0
- package/src/find-verdict-refusal.ts +26 -0
- package/src/follow-process.ts +147 -0
- package/src/get-lock-value.ts +17 -0
- package/src/get-target.ts +15 -0
- package/src/host.ts +26 -0
- package/src/index.ts +73 -0
- package/src/is-error-code.ts +3 -0
- package/src/keep-cancelled-call.ts +21 -0
- package/src/kill-process-group.ts +13 -0
- package/src/launch-line-schema.ts +6 -0
- package/src/listen-supervisor-socket.ts +75 -0
- package/src/local.ts +20 -0
- package/src/make-fence.ts +27 -0
- package/src/make-fenced-exec.ts +52 -0
- package/src/make-process-supervisor-port.ts +65 -0
- package/src/open-drive.ts +151 -0
- package/src/open-runless-drive.ts +71 -0
- package/src/parse-launch-line.ts +19 -0
- package/src/parse-patch-id.ts +8 -0
- package/src/parse-report-block.ts +39 -0
- package/src/parse-run-log.ts +34 -0
- package/src/pick-access-refusal.ts +32 -0
- package/src/pick-command-needs.ts +29 -0
- package/src/pick-command-plan.ts +51 -0
- package/src/pick-context-target.ts +22 -0
- package/src/pick-drive-kind.ts +23 -0
- package/src/pick-envelope-context.ts +10 -0
- package/src/pick-envelope-payload.ts +28 -0
- package/src/pick-envelope-run.ts +25 -0
- package/src/pick-envelope-target.ts +10 -0
- package/src/pick-late-failure-write.ts +44 -0
- package/src/pick-log-refusal.ts +18 -0
- package/src/pick-merge-base-failure.ts +15 -0
- package/src/pick-merge-base-steps.ts +37 -0
- package/src/pick-pending-cleanups.ts +23 -0
- package/src/pick-submodule-refusal.ts +46 -0
- package/src/pick-supervisor-operation.ts +52 -0
- package/src/prepare-args.ts +22 -0
- package/src/probe-process.ts +16 -0
- package/src/quote-shell-word.ts +9 -0
- package/src/race-deadline.ts +23 -0
- package/src/read-changed-paths.ts +39 -0
- package/src/read-default-base.ts +21 -0
- package/src/read-fingerprint.ts +271 -0
- package/src/read-head-sha.ts +23 -0
- package/src/read-launch-host.ts +64 -0
- package/src/read-pid-namespace.ts +18 -0
- package/src/read-stream-line-batches.ts +28 -0
- package/src/read-stream-lines.ts +11 -0
- package/src/read-text.ts +5 -0
- package/src/read-verdicts.ts +57 -0
- package/src/remove-directory.ts +6 -0
- package/src/remove-file.ts +6 -0
- package/src/render-report-markdown.ts +53 -0
- package/src/report-schema.ts +37 -0
- package/src/run-events.ts +71 -0
- package/src/run-git.ts +49 -0
- package/src/run-process.ts +168 -0
- package/src/run-supervisor-process.ts +51 -0
- package/src/run-supervisor.ts +327 -0
- package/src/settle-dispatch-answer.ts +26 -0
- package/src/settle-envelope.ts +34 -0
- package/src/ssh.ts +211 -0
- package/src/start-launch.ts +327 -0
- package/src/start-services.ts +108 -0
- package/src/supervisor-machine.ts +169 -0
- package/src/supervisor-protocol.ts +42 -0
- package/src/to-wringer-error.ts +20 -0
- package/src/types.ts +476 -0
- package/src/wait-for-cancelled-calls.ts +37 -0
- package/src/wringer-error.ts +25 -0
- package/src/write-envelope-log.ts +72 -0
- package/src/write-late-failure.ts +40 -0
- package/src/write-new-file.ts +17 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// A key from outside the program, such as a target or stream named `constructor`, would read a
|
|
2
|
+
// value every object inherits; only the record's own keys count.
|
|
3
|
+
export function findOwnValue<Value>(
|
|
4
|
+
record: Readonly<Record<string, Value>>,
|
|
5
|
+
key: string,
|
|
6
|
+
): Value | undefined {
|
|
7
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
8
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ProcessResult } from './run-process.ts';
|
|
2
|
+
import { toWringerError } from './to-wringer-error.ts';
|
|
3
|
+
|
|
4
|
+
// Names the first stop that failed to run or exited non-zero, or null when every stop worked.
|
|
5
|
+
export function findRemoteStopFailure(
|
|
6
|
+
stops: readonly PromiseSettledResult<ProcessResult>[],
|
|
7
|
+
): string | null {
|
|
8
|
+
for (const stop of stops) {
|
|
9
|
+
if (stop.status === 'rejected') {
|
|
10
|
+
return toWringerError(stop.reason).message;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (stop.value.exitCode !== 0) {
|
|
14
|
+
return `the stop exited with code ${stop.value.exitCode}: ${stop.value.stderr.trim()}`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
|
+
import type { RunView } from './build-run-view.ts';
|
|
3
|
+
import { WringerError } from './wringer-error.ts';
|
|
4
|
+
|
|
5
|
+
// `done` judged the run it read when it started. Its verdict stands only while the doctor it
|
|
6
|
+
// trusted is still the latest, which holds while the drives it read still open the run's drives
|
|
7
|
+
// since the doctor: a later doctor event empties that list.
|
|
8
|
+
export function findVerdictRefusal(
|
|
9
|
+
read: RunView | undefined,
|
|
10
|
+
current: RunView | undefined,
|
|
11
|
+
): WringerError | null {
|
|
12
|
+
const readDrives = read?.drivesAfterDoctor ?? [];
|
|
13
|
+
|
|
14
|
+
const stands =
|
|
15
|
+
current?.doctor === 'passed' &&
|
|
16
|
+
isDeepStrictEqual(current.drivesAfterDoctor.slice(0, readDrives.length), readDrives);
|
|
17
|
+
|
|
18
|
+
if (stands) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return new WringerError('doctor-required', {
|
|
23
|
+
message: `The doctor ran again on run ${current?.runID ?? 'none'} while \`done\` judged it.`,
|
|
24
|
+
hint: 'Run `doctor`, then drive the feature again.',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { buildSpawnError } from './build-spawn-error.ts';
|
|
2
|
+
import { killProcessGroup } from './kill-process-group.ts';
|
|
3
|
+
import { readStreamLines } from './read-stream-lines.ts';
|
|
4
|
+
import { WringerError } from './wringer-error.ts';
|
|
5
|
+
|
|
6
|
+
export interface FollowProcessRequest {
|
|
7
|
+
readonly argv: readonly string[];
|
|
8
|
+
readonly signal: AbortSignal;
|
|
9
|
+
readonly onLine: (line: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FollowResult {
|
|
13
|
+
readonly exitCode: number;
|
|
14
|
+
|
|
15
|
+
// The end of stderr, so a long follow keeps a bounded tail, such as ssh's reason it failed.
|
|
16
|
+
readonly stderr: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const stderrTailLength = 4096;
|
|
20
|
+
|
|
21
|
+
// The command runs in a process group of its own, and the signal kills the whole group: a child
|
|
22
|
+
// the command started, such as a `sleep` under `sh -c`, would otherwise keep stdout open, and the
|
|
23
|
+
// follow would never end.
|
|
24
|
+
export async function followProcess(request: FollowProcessRequest): Promise<FollowResult> {
|
|
25
|
+
const [command] = request.argv;
|
|
26
|
+
const executable = resolveExecutable(command);
|
|
27
|
+
|
|
28
|
+
const plan = pickFollowPlan({
|
|
29
|
+
argv: request.argv,
|
|
30
|
+
executable,
|
|
31
|
+
aborted: request.signal.aborted,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (plan instanceof WringerError) {
|
|
35
|
+
throw plan;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!plan.start) {
|
|
39
|
+
return { exitCode: 0, stderr: '' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const child = spawnFollowed(plan.argv);
|
|
43
|
+
|
|
44
|
+
// stderr is read as it comes, so a full pipe never stalls the command.
|
|
45
|
+
const stderr = readStreamTail(child.stderr, stderrTailLength);
|
|
46
|
+
|
|
47
|
+
const killGroup = (): void => {
|
|
48
|
+
killProcessGroup(child.pid);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
request.signal.addEventListener('abort', killGroup, { once: true });
|
|
52
|
+
|
|
53
|
+
// A child the command left behind is killed as the command exits: it may hold stdout open, and
|
|
54
|
+
// nothing could stop it once the abort is unwired. A reader or callback that throws stops the
|
|
55
|
+
// group too, so nothing runs on unread.
|
|
56
|
+
const watchExit = async (): Promise<number> => {
|
|
57
|
+
const exitCode = await child.exited;
|
|
58
|
+
|
|
59
|
+
killGroup();
|
|
60
|
+
|
|
61
|
+
return exitCode;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const exited = watchExit();
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
for await (const line of readStreamLines(child.stdout)) {
|
|
68
|
+
request.onLine(line);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const [exitCode, tail] = await Promise.all([exited, stderr]);
|
|
72
|
+
|
|
73
|
+
return { exitCode, stderr: tail };
|
|
74
|
+
} catch (error) {
|
|
75
|
+
killGroup();
|
|
76
|
+
|
|
77
|
+
await Promise.allSettled([child.exited, stderr]);
|
|
78
|
+
|
|
79
|
+
throw error;
|
|
80
|
+
} finally {
|
|
81
|
+
request.signal.removeEventListener('abort', killGroup);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function resolveExecutable(command: string | undefined): string | null {
|
|
86
|
+
if (command === undefined) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Bun.which(command);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface FollowPlanInput {
|
|
94
|
+
readonly argv: readonly string[];
|
|
95
|
+
readonly executable: string | null;
|
|
96
|
+
readonly aborted: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
type FollowPlan =
|
|
100
|
+
| { readonly start: false }
|
|
101
|
+
| { readonly start: true; readonly argv: readonly string[] };
|
|
102
|
+
|
|
103
|
+
function pickFollowPlan(input: FollowPlanInput): FollowPlan | WringerError {
|
|
104
|
+
const [command, ...args] = input.argv;
|
|
105
|
+
|
|
106
|
+
if (command === undefined) {
|
|
107
|
+
return new WringerError('usage', { message: 'A follow needs a command.' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (input.executable === null) {
|
|
111
|
+
return new WringerError('environment', {
|
|
112
|
+
message: `The command ${command} is not on the PATH.`,
|
|
113
|
+
hint: `Install ${command}, or put it on the PATH.`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (input.aborted) {
|
|
118
|
+
return { start: false };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { start: true, argv: [input.executable, ...args] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function spawnFollowed(argv: readonly string[]) {
|
|
125
|
+
try {
|
|
126
|
+
return Bun.spawn([...argv], {
|
|
127
|
+
stdin: 'ignore',
|
|
128
|
+
stdout: 'pipe',
|
|
129
|
+
stderr: 'pipe',
|
|
130
|
+
detached: true,
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw buildSpawnError(argv.join(' '), error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function readStreamTail(stream: ReadableStream<Uint8Array>, length: number): Promise<string> {
|
|
138
|
+
const decoder = new TextDecoder();
|
|
139
|
+
|
|
140
|
+
let tail = '';
|
|
141
|
+
|
|
142
|
+
for await (const chunk of stream) {
|
|
143
|
+
tail = `${tail}${decoder.decode(chunk, { stream: true })}`.slice(-length);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${tail}${decoder.decode()}`.slice(-length);
|
|
147
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FileLockResult } from '@zgeoff/std/with-file-lock';
|
|
2
|
+
import { match } from 'ts-pattern';
|
|
3
|
+
import { WringerError } from './wringer-error.ts';
|
|
4
|
+
|
|
5
|
+
// The value the locked work returned, or an environment error when another holder kept the lock
|
|
6
|
+
// at `path` past the wait.
|
|
7
|
+
export function getLockValue<Value>(path: string, result: FileLockResult<Value>): Value {
|
|
8
|
+
return match(result)
|
|
9
|
+
.with({ kind: 'ran' }, (ran) => ran.value)
|
|
10
|
+
.with({ kind: 'busy' }, () => {
|
|
11
|
+
throw new WringerError('environment', {
|
|
12
|
+
message: `Another wringer command holds the lock ${path}.`,
|
|
13
|
+
hint: 'Wait for the other command to end, then run this one again.',
|
|
14
|
+
});
|
|
15
|
+
})
|
|
16
|
+
.exhaustive();
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TargetDefinition, Wringer } from './types.ts';
|
|
2
|
+
import { WringerError } from './wringer-error.ts';
|
|
3
|
+
|
|
4
|
+
export function getTarget(wringer: Wringer, name: string): TargetDefinition {
|
|
5
|
+
const target = wringer.targets.get(name);
|
|
6
|
+
|
|
7
|
+
if (target === undefined) {
|
|
8
|
+
throw new WringerError('usage', {
|
|
9
|
+
message: `The repo has no target named ${name}.`,
|
|
10
|
+
hint: `Pick one of: ${[...wringer.targets.keys()].join(', ')}.`,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return target;
|
|
15
|
+
}
|
package/src/host.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ProcessResult } from './run-process.ts';
|
|
2
|
+
|
|
3
|
+
export type Access = 'read-write' | 'read-only';
|
|
4
|
+
|
|
5
|
+
export interface HostRequest {
|
|
6
|
+
readonly argv: readonly string[];
|
|
7
|
+
readonly access: Access;
|
|
8
|
+
readonly stdin?: string | undefined;
|
|
9
|
+
readonly signal?: AbortSignal | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface FollowRequest {
|
|
13
|
+
readonly argv: readonly string[];
|
|
14
|
+
readonly access: Access;
|
|
15
|
+
readonly signal: AbortSignal;
|
|
16
|
+
readonly onLine: (line: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Host {
|
|
20
|
+
readonly kind: 'local' | 'ssh';
|
|
21
|
+
readonly run: (request: HostRequest) => Promise<ProcessResult>;
|
|
22
|
+
|
|
23
|
+
// Runs a long-lived command, such as `tail -F`, and hands each line of its stdout to `onLine`
|
|
24
|
+
// until the command exits or the signal fires. Resolves with the command's exit code.
|
|
25
|
+
readonly follow: (request: FollowRequest) => Promise<number>;
|
|
26
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1,74 @@
|
|
|
1
|
+
export { bindDrivers } from './bind-drivers.ts';
|
|
2
|
+
export type { BoundHelpers } from './bind-drivers.ts';
|
|
3
|
+
export { buildEnvelope } from './build-envelope.ts';
|
|
4
|
+
export type { EnvelopeInput } from './build-envelope.ts';
|
|
5
|
+
export { buildEnvelopeJSONSchema } from './build-envelope-json-schema.ts';
|
|
6
|
+
export { buildRunView } from './build-run-view.ts';
|
|
7
|
+
export type { DoctorState, RunView, SupervisorInfo } from './build-run-view.ts';
|
|
8
|
+
export { buildRunPaths, buildStatePaths } from './build-state-paths.ts';
|
|
9
|
+
export type { RunPaths, StatePaths } from './build-state-paths.ts';
|
|
10
|
+
export { createDirectory } from './create-directory.ts';
|
|
11
|
+
export { createFileState, eventSizeCapBytes, pointersSchema } from './create-file-state.ts';
|
|
12
|
+
export type { FileStateOptions, Pointers, StatePort } from './create-file-state.ts';
|
|
13
|
+
export { createNodeStateFileSystem } from './create-node-state-file-system.ts';
|
|
14
|
+
export { createRepoState } from './create-repo-state.ts';
|
|
15
|
+
export type { StateFileSystem } from './create-node-state-file-system.ts';
|
|
16
|
+
export { createWringer, defaultStateDir } from './create-wringer.ts';
|
|
17
|
+
export type { WringerBuilder, WringerOptions } from './create-wringer.ts';
|
|
18
|
+
export { defineDriver } from './define-driver.ts';
|
|
19
|
+
export { findOwnValue } from './find-own-value.ts';
|
|
20
|
+
export type { DriverDefinition, DriverFactory } from './define-driver.ts';
|
|
21
|
+
export { dataCapBytes } from './cap-data.ts';
|
|
22
|
+
export { defaultTimeoutMs, dispatch } from './dispatch.ts';
|
|
23
|
+
export type { DispatchResult } from './dispatch.ts';
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
envelopeErrorSchema,
|
|
27
|
+
envelopeSchema,
|
|
28
|
+
evidenceSchema,
|
|
29
|
+
streamEventSchema,
|
|
30
|
+
} from './envelope.ts';
|
|
31
|
+
|
|
32
|
+
export type { Envelope, Evidence, StreamEvent } from './envelope.ts';
|
|
1
33
|
export { envelopeVersion } from './envelope-version.ts';
|
|
34
|
+
export { defaultMessageFor, errorCodes, exitCodeFor } from './error-codes.ts';
|
|
35
|
+
export type { ErrorCode, ExitCode } from './error-codes.ts';
|
|
36
|
+
export type { Access, FollowRequest, Host, HostRequest } from './host.ts';
|
|
37
|
+
export { isErrorCode } from './is-error-code.ts';
|
|
38
|
+
export { launchLineSchema } from './launch-line-schema.ts';
|
|
39
|
+
export type { LaunchLine } from './launch-line-schema.ts';
|
|
40
|
+
export { local } from './local.ts';
|
|
41
|
+
export { defaultRefusalHint } from './make-fence.ts';
|
|
42
|
+
export { makeProcessSupervisorPort } from './make-process-supervisor-port.ts';
|
|
43
|
+
export type { ProcessSupervisorOptions } from './make-process-supervisor-port.ts';
|
|
44
|
+
export { emptyFingerprint } from './parse-patch-id.ts';
|
|
45
|
+
export { parseReportBlock } from './parse-report-block.ts';
|
|
46
|
+
export type { ReportBlockResult } from './parse-report-block.ts';
|
|
47
|
+
export { readFingerprint } from './read-fingerprint.ts';
|
|
48
|
+
export type { FingerprintHost, FingerprintRequest } from './read-fingerprint.ts';
|
|
49
|
+
export { readStreamLines } from './read-stream-lines.ts';
|
|
50
|
+
export { readText } from './read-text.ts';
|
|
51
|
+
export { renderReportMarkdown } from './render-report-markdown.ts';
|
|
52
|
+
export { reportSchema, verdictSchema } from './report-schema.ts';
|
|
53
|
+
export type { FeatureStatus, Report, Verdict } from './report-schema.ts';
|
|
54
|
+
|
|
55
|
+
export {
|
|
56
|
+
checkResultSchema,
|
|
57
|
+
endpointsSchema,
|
|
58
|
+
modeSchema,
|
|
59
|
+
runEventSchema,
|
|
60
|
+
runIDSchema,
|
|
61
|
+
} from './run-events.ts';
|
|
62
|
+
|
|
63
|
+
export type { CheckResult, Endpoints, Mode, RunEvent, RunID } from './run-events.ts';
|
|
64
|
+
export { runProcess } from './run-process.ts';
|
|
65
|
+
export type { ProcessRequest, ProcessResult } from './run-process.ts';
|
|
66
|
+
export type { RunStatus } from './append-run-event.ts';
|
|
67
|
+
export { runSupervisorProcess } from './run-supervisor-process.ts';
|
|
68
|
+
export { ssh } from './ssh.ts';
|
|
69
|
+
export type { PrefixLayer, SSHOptions } from './ssh.ts';
|
|
70
|
+
export { supervisorCommandName } from './supervisor-protocol.ts';
|
|
71
|
+
export type * from './types.ts';
|
|
72
|
+
export { createNodeRepoHost } from './create-node-repo-host.ts';
|
|
73
|
+
export { WringerError } from './wringer-error.ts';
|
|
74
|
+
export type { WringerErrorDetails } from './wringer-error.ts';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Keeps a call in `cancelled` only when the command's deadline fires while the call still runs. A
|
|
2
|
+
// call that failed earlier, such as at a doctor check's own deadline, never answers for the timeout.
|
|
3
|
+
export function keepCancelledCall(
|
|
4
|
+
cancelled: Promise<unknown>[],
|
|
5
|
+
calling: Promise<unknown>,
|
|
6
|
+
signal: AbortSignal,
|
|
7
|
+
): void {
|
|
8
|
+
const onAbort = (): void => {
|
|
9
|
+
cancelled.push(calling);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
13
|
+
|
|
14
|
+
const forget = async (): Promise<void> => {
|
|
15
|
+
await Promise.allSettled([calling]);
|
|
16
|
+
|
|
17
|
+
signal.removeEventListener('abort', onAbort);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
void forget();
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { isErrorCode } from './is-error-code.ts';
|
|
2
|
+
|
|
3
|
+
// Kills a child spawned with `detached: true` together with every process it started: a negative
|
|
4
|
+
// PID names the process group. The group may already be gone.
|
|
5
|
+
export function killProcessGroup(pid: number, signal: NodeJS.Signals = 'SIGKILL'): void {
|
|
6
|
+
try {
|
|
7
|
+
process.kill(-pid, signal);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (!isErrorCode(error, 'ESRCH')) {
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { chmod } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import type { Socket } from 'node:net';
|
|
4
|
+
import { answerSupervisorConnection } from './answer-supervisor-connection.ts';
|
|
5
|
+
import type { SupervisorConnectionOptions } from './answer-supervisor-connection.ts';
|
|
6
|
+
import { raceDeadline } from './race-deadline.ts';
|
|
7
|
+
import { removeFile } from './remove-file.ts';
|
|
8
|
+
|
|
9
|
+
export interface SupervisorSocketOptions extends SupervisorConnectionOptions {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SupervisorSocket {
|
|
14
|
+
// Stops taking connections, ends each one that sent no request, and settles once every answer
|
|
15
|
+
// has flushed, or after `graceMs`, when it destroys each connection still open: a service call
|
|
16
|
+
// that never settles would otherwise hold the process open.
|
|
17
|
+
readonly close: (graceMs: number) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The socket's folder is owner-only already; the socket itself is too before the first request.
|
|
21
|
+
export async function listenSupervisorSocket(
|
|
22
|
+
options: SupervisorSocketOptions,
|
|
23
|
+
): Promise<SupervisorSocket> {
|
|
24
|
+
await removeFile(options.path);
|
|
25
|
+
|
|
26
|
+
const closing = new AbortController();
|
|
27
|
+
const answers = new Set<Promise<void>>();
|
|
28
|
+
const connections = new Set<Socket>();
|
|
29
|
+
|
|
30
|
+
// An answer catches its own failures, so it never rejects. A connection stays open after its
|
|
31
|
+
// answer until the client closes its side, so it is tracked until it closes.
|
|
32
|
+
const track = async (connection: Socket): Promise<void> => {
|
|
33
|
+
connections.add(connection);
|
|
34
|
+
|
|
35
|
+
connection.once('close', () => {
|
|
36
|
+
connections.delete(connection);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const answer = answerSupervisorConnection(connection, options, closing.signal);
|
|
40
|
+
|
|
41
|
+
answers.add(answer);
|
|
42
|
+
|
|
43
|
+
await answer;
|
|
44
|
+
|
|
45
|
+
answers.delete(answer);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const server = createServer((connection) => {
|
|
49
|
+
void track(connection);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const listening = Promise.withResolvers<undefined>();
|
|
53
|
+
|
|
54
|
+
server.once('error', listening.reject);
|
|
55
|
+
|
|
56
|
+
server.listen(options.path, () => {
|
|
57
|
+
listening.resolve(undefined);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await listening.promise;
|
|
61
|
+
await chmod(options.path, 0o600);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
close: async (graceMs) => {
|
|
65
|
+
server.close();
|
|
66
|
+
closing.abort();
|
|
67
|
+
|
|
68
|
+
await Promise.allSettled([raceDeadline(Promise.all(answers), AbortSignal.timeout(graceMs))]);
|
|
69
|
+
|
|
70
|
+
for (const connection of connections) {
|
|
71
|
+
connection.destroy();
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
package/src/local.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { followProcess } from './follow-process.ts';
|
|
2
|
+
import type { Host } from './host.ts';
|
|
3
|
+
import { runProcess } from './run-process.ts';
|
|
4
|
+
|
|
5
|
+
export function local(): Host {
|
|
6
|
+
return {
|
|
7
|
+
kind: 'local',
|
|
8
|
+
run: (request) =>
|
|
9
|
+
runProcess({ argv: request.argv, stdin: request.stdin, signal: request.signal }),
|
|
10
|
+
follow: async (request) => {
|
|
11
|
+
const followed = await followProcess({
|
|
12
|
+
argv: request.argv,
|
|
13
|
+
signal: request.signal,
|
|
14
|
+
onLine: request.onLine,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return followed.exitCode;
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Access } from './host.ts';
|
|
2
|
+
import type { Fence } from './types.ts';
|
|
3
|
+
import { WringerError } from './wringer-error.ts';
|
|
4
|
+
|
|
5
|
+
export const defaultRefusalHint = 'This target is read-only. Stop and report the write you needed.';
|
|
6
|
+
|
|
7
|
+
export interface FenceOptions {
|
|
8
|
+
readonly access: Access;
|
|
9
|
+
readonly hint: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Default-deny: on a read-only target, every call whose effect is not `read` is refused before it
|
|
13
|
+
// runs. The driver states the effect, with the target's grants already applied.
|
|
14
|
+
export function makeFence(options: FenceOptions): Fence {
|
|
15
|
+
return (call) => (args) => {
|
|
16
|
+
if (options.access === 'read-only' && call.effect(args) !== 'read') {
|
|
17
|
+
return Promise.reject(
|
|
18
|
+
new WringerError('read-only', {
|
|
19
|
+
message: `${call.describe(args)} is a write, and the target is read-only.`,
|
|
20
|
+
hint: options.hint,
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return call.run(args);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Access, Host } from './host.ts';
|
|
2
|
+
import type { DriverExec, RepoExec } from './types.ts';
|
|
3
|
+
import { WringerError } from './wringer-error.ts';
|
|
4
|
+
|
|
5
|
+
export interface FencedExecOptions {
|
|
6
|
+
readonly host: Host;
|
|
7
|
+
readonly access: Access;
|
|
8
|
+
readonly hint: string;
|
|
9
|
+
readonly signal: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Repo code has no way to mark a raw exec as a read, so a read-only target refuses every one. A
|
|
13
|
+
// driver states the effect of each of its own execs.
|
|
14
|
+
export function makeRepoExec(options: FencedExecOptions): RepoExec {
|
|
15
|
+
return (argv, execOptions) => {
|
|
16
|
+
if (options.access === 'read-only') {
|
|
17
|
+
return Promise.reject(
|
|
18
|
+
new WringerError('read-only', {
|
|
19
|
+
message: `Raw exec of ${argv[0] ?? 'a command'} is refused on a read-only target.`,
|
|
20
|
+
hint: options.hint,
|
|
21
|
+
}),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return options.host.run({
|
|
26
|
+
argv,
|
|
27
|
+
access: options.access,
|
|
28
|
+
stdin: execOptions?.stdin,
|
|
29
|
+
signal: options.signal,
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function makeDriverExec(options: FencedExecOptions): DriverExec {
|
|
35
|
+
return (argv, effect, execOptions) => {
|
|
36
|
+
if (options.access === 'read-only' && effect !== 'read') {
|
|
37
|
+
return Promise.reject(
|
|
38
|
+
new WringerError('read-only', {
|
|
39
|
+
message: `${argv[0] ?? 'A command'} is a write, and the target is read-only.`,
|
|
40
|
+
hint: options.hint,
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return options.host.run({
|
|
46
|
+
argv,
|
|
47
|
+
access: options.access,
|
|
48
|
+
stdin: execOptions?.stdin,
|
|
49
|
+
signal: options.signal,
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { closeSync, mkdirSync, openSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { buildRunPaths } from './build-state-paths.ts';
|
|
5
|
+
import { callSupervisor } from './call-supervisor.ts';
|
|
6
|
+
import { buildSupervisorArgv } from './supervisor-protocol.ts';
|
|
7
|
+
import type { SupervisorPort } from './types.ts';
|
|
8
|
+
import { WringerError } from './wringer-error.ts';
|
|
9
|
+
|
|
10
|
+
export interface ProcessSupervisorOptions {
|
|
11
|
+
readonly selfCommand: readonly string[];
|
|
12
|
+
readonly root: string;
|
|
13
|
+
readonly stateDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The supervisor is the repo's own CLI started again with a hidden command, detached in its own
|
|
17
|
+
// process group, so it outlives the command that started it and loads the same registry. Calls
|
|
18
|
+
// reach it over its Unix socket.
|
|
19
|
+
export function makeProcessSupervisorPort(options: ProcessSupervisorOptions): SupervisorPort {
|
|
20
|
+
return {
|
|
21
|
+
call: callSupervisor,
|
|
22
|
+
start: (request) => {
|
|
23
|
+
const paths = buildRunPaths(join(options.root, options.stateDir), request.runID);
|
|
24
|
+
|
|
25
|
+
mkdirSync(paths.directory, { recursive: true });
|
|
26
|
+
|
|
27
|
+
const log = openSync(paths.supervisorLog, 'a');
|
|
28
|
+
const [command = 'bun', ...args] = options.selfCommand;
|
|
29
|
+
const started = Promise.withResolvers<void>();
|
|
30
|
+
|
|
31
|
+
const child = spawn(
|
|
32
|
+
command,
|
|
33
|
+
[
|
|
34
|
+
...args,
|
|
35
|
+
...buildSupervisorArgv({
|
|
36
|
+
runID: request.runID,
|
|
37
|
+
target: request.target,
|
|
38
|
+
root: options.root,
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
{ cwd: options.root, detached: true, stdio: ['ignore', log, log] },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// A command that cannot start, such as one missing from the PATH, fails after spawn returns.
|
|
45
|
+
child.once('spawn', () => {
|
|
46
|
+
started.resolve();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
child.once('error', (error) => {
|
|
50
|
+
started.reject(
|
|
51
|
+
new WringerError('environment', {
|
|
52
|
+
message: `The supervisor did not start: ${error.message}`,
|
|
53
|
+
hint: `Check that ${command} runs from ${options.root}.`,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
child.unref();
|
|
59
|
+
|
|
60
|
+
closeSync(log);
|
|
61
|
+
|
|
62
|
+
return started.promise;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|