@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,48 @@
|
|
|
1
|
+
import type { GitRequest } from './run-git.ts';
|
|
2
|
+
|
|
3
|
+
export interface PromisedObjectReadInput {
|
|
4
|
+
readonly root: string;
|
|
5
|
+
readonly mergeBase: string;
|
|
6
|
+
|
|
7
|
+
// The text of `git config --list -z` in the repo.
|
|
8
|
+
readonly configList: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// The fingerprint's own git folder cannot fetch, so in a partial clone the repo's own git reads
|
|
12
|
+
// each base object the diff will need first: a diff of the working tree reads the same objects.
|
|
13
|
+
export function buildPromisedObjectReads(input: PromisedObjectReadInput): GitRequest[] {
|
|
14
|
+
if (!isPartialClone(input.configList)) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
cwd: input.root,
|
|
21
|
+
args: [
|
|
22
|
+
'diff',
|
|
23
|
+
'--binary',
|
|
24
|
+
'--no-ext-diff',
|
|
25
|
+
'--no-textconv',
|
|
26
|
+
'--output=/dev/null',
|
|
27
|
+
input.mergeBase,
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const promisorPattern = /^remote\..+\.promisor$/u;
|
|
34
|
+
|
|
35
|
+
const trueValues = new Set(['', 'true', 'yes', 'on', '1']);
|
|
36
|
+
|
|
37
|
+
// `config --list -z` prints each entry as the key, a newline and the value, ending in a NUL. A
|
|
38
|
+
// partial clone marks its remote as a promisor, and older clones also set the extension.
|
|
39
|
+
function isPartialClone(configList: string): boolean {
|
|
40
|
+
return configList.split('\0').some((entry) => {
|
|
41
|
+
const [key = '', value = ''] = entry.split('\n');
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
key === 'extensions.partialclone' ||
|
|
45
|
+
(promisorPattern.test(key) && trueValues.has(value.toLowerCase()))
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { FeatureStatus, Report, Verdict } from './report-schema.ts';
|
|
2
|
+
import type { FeatureDefinition } from './types.ts';
|
|
3
|
+
|
|
4
|
+
export interface ReportInput {
|
|
5
|
+
readonly base: string;
|
|
6
|
+
readonly fingerprint: string;
|
|
7
|
+
readonly features: readonly FeatureDefinition[];
|
|
8
|
+
readonly changedPaths: readonly string[];
|
|
9
|
+
readonly verdicts: ReadonlyMap<string, Verdict>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Each feature whose paths the branch changes needs a verdict at the current fingerprint. A feature
|
|
13
|
+
// the branch leaves alone is `unchanged`, whatever its verdict.
|
|
14
|
+
export function buildReport(input: ReportInput): Report {
|
|
15
|
+
const features = input.features.map((feature) => {
|
|
16
|
+
const changed = input.changedPaths.some((path) =>
|
|
17
|
+
feature.paths.some((pattern) => new Bun.Glob(pattern).match(path)),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const verdict = input.verdicts.get(feature.name) ?? null;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
name: feature.name,
|
|
24
|
+
changed,
|
|
25
|
+
status: pickFeatureStatus(changed, verdict, input.fingerprint),
|
|
26
|
+
verdict,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
version: 1,
|
|
32
|
+
base: input.base,
|
|
33
|
+
fingerprint: input.fingerprint,
|
|
34
|
+
complete: features.every(
|
|
35
|
+
(feature) => feature.status === 'verified' || feature.status === 'unchanged',
|
|
36
|
+
),
|
|
37
|
+
features,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pickFeatureStatus(
|
|
42
|
+
changed: boolean,
|
|
43
|
+
verdict: Verdict | null,
|
|
44
|
+
fingerprint: string,
|
|
45
|
+
): FeatureStatus {
|
|
46
|
+
if (!changed) {
|
|
47
|
+
return 'unchanged';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (verdict === null) {
|
|
51
|
+
return 'missing';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return verdict.fingerprint === fingerprint ? 'verified' : 'stale';
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { buildRunPaths } from './build-state-paths.ts';
|
|
3
|
+
import type { RunHandle } from './types.ts';
|
|
4
|
+
|
|
5
|
+
export function buildRunHandle(stateDir: string, runID: string): RunHandle {
|
|
6
|
+
const paths = buildRunPaths(stateDir, runID);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
id: runID,
|
|
10
|
+
paths,
|
|
11
|
+
childLog: (name) => ({ path: join(paths.children, `${name}.log`) }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { match } from 'ts-pattern';
|
|
2
|
+
import { canMoveRunStatus, findRunStatusMove } from './append-run-event.ts';
|
|
3
|
+
import type { RunStatus } from './append-run-event.ts';
|
|
4
|
+
import type { Envelope } from './envelope.ts';
|
|
5
|
+
import type { CheckResult, Endpoints, Mode, RunEvent, RunID } from './run-events.ts';
|
|
6
|
+
|
|
7
|
+
export type DoctorState = 'none' | 'passed' | 'failed';
|
|
8
|
+
|
|
9
|
+
export interface SupervisorInfo {
|
|
10
|
+
readonly pid: number;
|
|
11
|
+
readonly socket: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RunView {
|
|
15
|
+
readonly runID: RunID;
|
|
16
|
+
readonly target: string;
|
|
17
|
+
readonly mode: Mode;
|
|
18
|
+
readonly fingerprint: string;
|
|
19
|
+
readonly status: RunStatus;
|
|
20
|
+
readonly startedAt: string;
|
|
21
|
+
readonly endedAt: string | null;
|
|
22
|
+
readonly crashReason: string | null;
|
|
23
|
+
readonly endpoints: Endpoints;
|
|
24
|
+
readonly services: readonly string[];
|
|
25
|
+
readonly supervisor: SupervisorInfo | null;
|
|
26
|
+
readonly doctor: DoctorState;
|
|
27
|
+
readonly doctorResults: readonly CheckResult[];
|
|
28
|
+
readonly drivesAfterDoctor: readonly Envelope[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface BuildRunViewInput {
|
|
32
|
+
readonly runID: RunID;
|
|
33
|
+
readonly events: readonly RunEvent[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function buildRunView(input: BuildRunViewInput): RunView | undefined {
|
|
37
|
+
const [first, ...rest] = input.events;
|
|
38
|
+
|
|
39
|
+
if (first?.type !== 'run-started') {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const initial: RunView = {
|
|
44
|
+
runID: input.runID,
|
|
45
|
+
target: first.target,
|
|
46
|
+
mode: first.mode,
|
|
47
|
+
fingerprint: first.fingerprint,
|
|
48
|
+
status: 'starting',
|
|
49
|
+
startedAt: first.at,
|
|
50
|
+
endedAt: null,
|
|
51
|
+
crashReason: null,
|
|
52
|
+
endpoints: {},
|
|
53
|
+
services: [],
|
|
54
|
+
supervisor: null,
|
|
55
|
+
doctor: 'none',
|
|
56
|
+
doctorResults: [],
|
|
57
|
+
drivesAfterDoctor: [],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return rest.reduce((view, event) => buildNextRunView(view, event), initial);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildNextRunView(view: RunView, event: RunEvent): RunView {
|
|
64
|
+
const moved = buildMovedRunView(view, event);
|
|
65
|
+
|
|
66
|
+
return match(event)
|
|
67
|
+
.returnType<RunView>()
|
|
68
|
+
.with({ type: 'launch-ready' }, (ready) =>
|
|
69
|
+
view.status === 'starting' ? { ...moved, endpoints: ready.endpoints } : moved,
|
|
70
|
+
)
|
|
71
|
+
.with({ type: 'service-ready' }, (ready) => ({
|
|
72
|
+
...moved,
|
|
73
|
+
services: [...moved.services, ready.service],
|
|
74
|
+
}))
|
|
75
|
+
.with({ type: 'supervisor-ready' }, (ready) => ({
|
|
76
|
+
...moved,
|
|
77
|
+
supervisor: { pid: ready.pid, socket: ready.socket },
|
|
78
|
+
}))
|
|
79
|
+
.with({ type: 'doctor-passed' }, (doctor) => ({
|
|
80
|
+
...moved,
|
|
81
|
+
doctor: 'passed',
|
|
82
|
+
doctorResults: doctor.results,
|
|
83
|
+
drivesAfterDoctor: [],
|
|
84
|
+
}))
|
|
85
|
+
.with({ type: 'doctor-failed' }, (doctor) => ({
|
|
86
|
+
...moved,
|
|
87
|
+
doctor: 'failed',
|
|
88
|
+
doctorResults: doctor.results,
|
|
89
|
+
drivesAfterDoctor: [],
|
|
90
|
+
}))
|
|
91
|
+
.with({ type: 'envelope' }, (logged) =>
|
|
92
|
+
logged.drive && moved.doctor === 'passed'
|
|
93
|
+
? { ...moved, drivesAfterDoctor: [...moved.drivesAfterDoctor, logged.envelope] }
|
|
94
|
+
: moved,
|
|
95
|
+
)
|
|
96
|
+
.with({ type: 'crashed' }, (crashed) =>
|
|
97
|
+
moved === view ? view : { ...moved, crashReason: crashed.reason },
|
|
98
|
+
)
|
|
99
|
+
.with({ type: 'run-ended' }, (ended) =>
|
|
100
|
+
moved === view ? view : { ...moved, endedAt: ended.at },
|
|
101
|
+
)
|
|
102
|
+
.with(
|
|
103
|
+
{ type: 'run-started' },
|
|
104
|
+
{ type: 'run-ready' },
|
|
105
|
+
{ type: 'internal-error' },
|
|
106
|
+
{ type: 'late-failure' },
|
|
107
|
+
() => moved,
|
|
108
|
+
)
|
|
109
|
+
.exhaustive();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildMovedRunView(view: RunView, event: RunEvent): RunView {
|
|
113
|
+
const next = findRunStatusMove(event.type);
|
|
114
|
+
|
|
115
|
+
if (next === null || !canMoveRunStatus(view.status, next)) {
|
|
116
|
+
return view;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { ...view, status: next };
|
|
120
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RunEvent } from './run-events.ts';
|
|
2
|
+
import { WringerError } from './wringer-error.ts';
|
|
3
|
+
|
|
4
|
+
export interface ShutDownFacts {
|
|
5
|
+
readonly crashReason: string | null;
|
|
6
|
+
|
|
7
|
+
// True when the stop stopped waiting for a startup that ignored the abort.
|
|
8
|
+
readonly gaveUpOnStartup: boolean;
|
|
9
|
+
|
|
10
|
+
// Each part whose cleanup failed, with why.
|
|
11
|
+
readonly failures: readonly string[];
|
|
12
|
+
readonly at: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ShutDownSteps {
|
|
16
|
+
readonly abortCommands: boolean;
|
|
17
|
+
readonly events: readonly RunEvent[];
|
|
18
|
+
|
|
19
|
+
// What the stop's caller gets once the final events are written.
|
|
20
|
+
readonly failure: WringerError | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const unfinishedStartup = 'the startup had not finished when the stop stopped waiting for it';
|
|
24
|
+
|
|
25
|
+
// A cleanup the deadline gave up on can leave its command running, so the commands of the parts
|
|
26
|
+
// that came up end only once every such part has stopped. A failed cleanup, or a startup the stop
|
|
27
|
+
// gave up on, makes the stop a crash, so the log and the stop's caller name what may still run.
|
|
28
|
+
export function buildShutDownSteps(facts: ShutDownFacts): ShutDownSteps {
|
|
29
|
+
const leftovers = [
|
|
30
|
+
...(facts.gaveUpOnStartup ? [unfinishedStartup] : []),
|
|
31
|
+
...(facts.failures.length === 0 ? [] : [`cleanup failed for ${facts.failures.join('; ')}`]),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const reasons = [...(facts.crashReason === null ? [] : [facts.crashReason]), ...leftovers];
|
|
35
|
+
|
|
36
|
+
const event: RunEvent =
|
|
37
|
+
reasons.length === 0
|
|
38
|
+
? { type: 'run-ended', at: facts.at }
|
|
39
|
+
: { type: 'crashed', at: facts.at, reason: reasons.join('; ') };
|
|
40
|
+
|
|
41
|
+
const failure =
|
|
42
|
+
leftovers.length === 0
|
|
43
|
+
? null
|
|
44
|
+
: new WringerError('environment', {
|
|
45
|
+
message: `The run ended, and a part may still be up: ${leftovers.join('; ')}.`,
|
|
46
|
+
hint: 'Stop each part the message names by hand before the next `up`.',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
abortCommands: !facts.gaveUpOnStartup,
|
|
51
|
+
events: [event],
|
|
52
|
+
failure,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { GitRequest } from './run-git.ts';
|
|
2
|
+
|
|
3
|
+
export interface SkipWorktreeMarkInput {
|
|
4
|
+
readonly root: string;
|
|
5
|
+
readonly env: Readonly<Record<string, string>>;
|
|
6
|
+
|
|
7
|
+
// The bytes of `git ls-files -t -z` over the repo's own index.
|
|
8
|
+
readonly taggedEntries: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const skipWorktreeTag = 'S ';
|
|
12
|
+
|
|
13
|
+
// A sparse checkout leaves a file outside it out of the working tree and marks its entry
|
|
14
|
+
// skip-worktree; without the mark on the temporary index, `add --all` would stage the file as
|
|
15
|
+
// deleted. latin1 maps each byte to one character, so a path that is not UTF-8 keeps its bytes.
|
|
16
|
+
export function buildSkipWorktreeMarks(input: SkipWorktreeMarkInput): GitRequest[] {
|
|
17
|
+
const paths = Buffer.from(input.taggedEntries)
|
|
18
|
+
.toString('latin1')
|
|
19
|
+
.split('\0')
|
|
20
|
+
.flatMap((entry) =>
|
|
21
|
+
entry.startsWith(skipWorktreeTag) ? [`${entry.slice(skipWorktreeTag.length)}\0`] : [],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (paths.length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
cwd: input.root,
|
|
31
|
+
args: ['update-index', '-z', '--skip-worktree', '--stdin'],
|
|
32
|
+
stdin: Buffer.from(paths.join(''), 'latin1'),
|
|
33
|
+
env: input.env,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { WringerError } from './wringer-error.ts';
|
|
2
|
+
|
|
3
|
+
// A spawn that fails, such as for a working folder that does not exist, is a fault of the
|
|
4
|
+
// environment the command runs in.
|
|
5
|
+
export function buildSpawnError(command: string, error: unknown): WringerError {
|
|
6
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
7
|
+
|
|
8
|
+
return new WringerError('environment', {
|
|
9
|
+
message: `${command} did not start: ${reason}`,
|
|
10
|
+
hint: `Check that ${command} and its working folder exist.`,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import type { RunID } from './run-events.ts';
|
|
3
|
+
|
|
4
|
+
export interface StatePaths {
|
|
5
|
+
readonly root: string;
|
|
6
|
+
readonly state: string;
|
|
7
|
+
readonly locks: string;
|
|
8
|
+
readonly runs: string;
|
|
9
|
+
|
|
10
|
+
// The late failures of commands that ran with no run, whose log would otherwise take them.
|
|
11
|
+
readonly lateFailures: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function buildStatePaths(stateDir: string): StatePaths {
|
|
15
|
+
return {
|
|
16
|
+
root: stateDir,
|
|
17
|
+
state: join(stateDir, 'state.json'),
|
|
18
|
+
locks: join(stateDir, 'locks'),
|
|
19
|
+
runs: join(stateDir, 'runs'),
|
|
20
|
+
lateFailures: join(stateDir, 'late-failures.ndjson'),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RunPaths {
|
|
25
|
+
readonly directory: string;
|
|
26
|
+
readonly manifest: string;
|
|
27
|
+
readonly log: string;
|
|
28
|
+
readonly socket: string;
|
|
29
|
+
readonly supervisorLog: string;
|
|
30
|
+
readonly streams: string;
|
|
31
|
+
readonly children: string;
|
|
32
|
+
readonly features: string;
|
|
33
|
+
|
|
34
|
+
// The lock every writer of the run's log and manifest holds.
|
|
35
|
+
readonly lock: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildRunPaths(stateDir: string, runID: RunID): RunPaths {
|
|
39
|
+
const statePaths = buildStatePaths(stateDir);
|
|
40
|
+
const directory = join(statePaths.runs, runID);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
lock: join(statePaths.locks, `run-${runID}.lock`),
|
|
44
|
+
directory,
|
|
45
|
+
manifest: join(directory, 'manifest.json'),
|
|
46
|
+
log: join(directory, 'log.ndjson'),
|
|
47
|
+
socket: join(directory, 'supervisor', 'sock'),
|
|
48
|
+
supervisorLog: join(directory, 'supervisor.log'),
|
|
49
|
+
streams: join(directory, 'streams'),
|
|
50
|
+
children: join(directory, 'children'),
|
|
51
|
+
features: join(directory, 'features'),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { GitRequest } from './run-git.ts';
|
|
2
|
+
|
|
3
|
+
export interface SubmoduleAddInput {
|
|
4
|
+
// The repo root, where `ls-files` printed each path relative to.
|
|
5
|
+
readonly root: string;
|
|
6
|
+
readonly env: Readonly<Record<string, string>>;
|
|
7
|
+
|
|
8
|
+
// The bytes of `git ls-files --stage -z` over the temporary index.
|
|
9
|
+
readonly stagedEntries: Uint8Array;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const nul = Buffer.from([0]);
|
|
13
|
+
|
|
14
|
+
// `add --all` skips a submodule that the repo's config marks `ignore = all` on newer git, so its
|
|
15
|
+
// new commit would never reach the diff; `--force` stages it whatever the config says. The paths go
|
|
16
|
+
// to git on stdin as the bytes `ls-files` printed, and as literal paths, so `lib[1]` is no glob.
|
|
17
|
+
export function buildSubmoduleAdds(input: SubmoduleAddInput): GitRequest[] {
|
|
18
|
+
const paths = collectSubmodulePaths(Buffer.from(input.stagedEntries));
|
|
19
|
+
|
|
20
|
+
if (paths.length === 0) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
cwd: input.root,
|
|
27
|
+
args: ['add', '--force', '--pathspec-from-file=-', '--pathspec-file-nul'],
|
|
28
|
+
env: { ...input.env, GIT_LITERAL_PATHSPECS: '1' },
|
|
29
|
+
stdin: Buffer.concat(paths.flatMap((path) => [path, nul])),
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const gitlinkPrefix = Buffer.from('160000 ');
|
|
35
|
+
const tab = 0x09;
|
|
36
|
+
|
|
37
|
+
// A path may hold a tab, so only the first tab ends the mode, object and stage.
|
|
38
|
+
function collectSubmodulePaths(stagedEntries: Buffer): Buffer[] {
|
|
39
|
+
return collectEntries(stagedEntries).flatMap((entry) => {
|
|
40
|
+
const pathStart = entry.indexOf(tab) + 1;
|
|
41
|
+
|
|
42
|
+
if (pathStart === 0 || !entry.subarray(0, gitlinkPrefix.length).equals(gitlinkPrefix)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return [entry.subarray(pathStart)];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function collectEntries(stagedEntries: Buffer): Buffer[] {
|
|
51
|
+
const entries: Buffer[] = [];
|
|
52
|
+
let start = 0;
|
|
53
|
+
|
|
54
|
+
while (start < stagedEntries.length) {
|
|
55
|
+
const end = stagedEntries.indexOf(0, start);
|
|
56
|
+
const stop = end === -1 ? stagedEntries.length : end;
|
|
57
|
+
|
|
58
|
+
entries.push(stagedEntries.subarray(start, stop));
|
|
59
|
+
|
|
60
|
+
start = stop + 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return entries;
|
|
64
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildBuiltinCommand } from '../build-commands.ts';
|
|
3
|
+
import { checkResultSchema } from '../run-events.ts';
|
|
4
|
+
import type { CheckResult } from '../run-events.ts';
|
|
5
|
+
import { toWringerError } from '../to-wringer-error.ts';
|
|
6
|
+
import type { BuiltinContext, Drive, RegisteredCommand } from '../types.ts';
|
|
7
|
+
import { WringerError } from '../wringer-error.ts';
|
|
8
|
+
import { writeLateFailure } from '../write-late-failure.ts';
|
|
9
|
+
import { pickDoctorOutcome } from './pick-doctor-outcome.ts';
|
|
10
|
+
|
|
11
|
+
const doctorDataSchema = z.strictObject({
|
|
12
|
+
passed: z.boolean(),
|
|
13
|
+
results: z.array(checkResultSchema),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// The doctor runs every driver check and repo check on a read-only view of the target, so a check
|
|
17
|
+
// can only read. Its result lands in the log of the run it read, so a late result never passes a
|
|
18
|
+
// newer run.
|
|
19
|
+
export function buildDoctorCommand(): RegisteredCommand {
|
|
20
|
+
return buildBuiltinCommand({
|
|
21
|
+
path: ['doctor'],
|
|
22
|
+
summary: 'Check the run is healthy; drive commands are refused until it passes',
|
|
23
|
+
args: z.strictObject({}),
|
|
24
|
+
data: doctorDataSchema,
|
|
25
|
+
effect: 'read',
|
|
26
|
+
needsRun: true,
|
|
27
|
+
needsDoctor: false,
|
|
28
|
+
run: async (_args, ctx) => {
|
|
29
|
+
const results: CheckResult[] = [];
|
|
30
|
+
|
|
31
|
+
for (const check of collectChecks(ctx, ctx.openDrive('read-only'))) {
|
|
32
|
+
const result = await runCheck(ctx, check);
|
|
33
|
+
|
|
34
|
+
results.push(result);
|
|
35
|
+
ctx.emit('check', result);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const outcome = pickDoctorOutcome(results, ctx.now().toISOString());
|
|
39
|
+
const runID = ctx.run?.runID ?? '';
|
|
40
|
+
|
|
41
|
+
await ctx.state.appendEvent(runID, outcome.event);
|
|
42
|
+
await ctx.state.writeManifest(runID);
|
|
43
|
+
|
|
44
|
+
if (outcome.failure !== null) {
|
|
45
|
+
throw outcome.failure;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return outcome.data;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PlannedCheck {
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly timeoutMs: number;
|
|
56
|
+
readonly run: (drive: Drive) => Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Each check runs in a drive of its own, whose clients and execs stop at the check's deadline.
|
|
60
|
+
function collectChecks(ctx: BuiltinContext, drive: Drive): readonly PlannedCheck[] {
|
|
61
|
+
const driverChecks = [...drive.drivers].flatMap(([driverName, opened]) =>
|
|
62
|
+
opened.checks.map((check) => ({
|
|
63
|
+
name: `${driverName}.${check.name}`,
|
|
64
|
+
timeoutMs: check.timeoutMs,
|
|
65
|
+
run: (checkDrive: Drive) => runDriverCheck(ctx, checkDrive, driverName, check.name),
|
|
66
|
+
})),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const repoChecks = ctx.wringer.checks.map((check) => ({
|
|
70
|
+
name: check.name,
|
|
71
|
+
timeoutMs: check.timeoutMs,
|
|
72
|
+
run: (checkDrive: Drive) => check.run(checkDrive.ctx),
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
return [...driverChecks, ...repoChecks];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function runDriverCheck(
|
|
79
|
+
ctx: BuiltinContext,
|
|
80
|
+
drive: Drive,
|
|
81
|
+
driverName: string,
|
|
82
|
+
checkName: string,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const check = drive.drivers.get(driverName)?.checks.find((each) => each.name === checkName);
|
|
85
|
+
|
|
86
|
+
if (check === undefined) {
|
|
87
|
+
return Promise.reject(
|
|
88
|
+
new WringerError('internal', {
|
|
89
|
+
message: `The ${driverName} driver lost its check ${checkName}.`,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return check.run(ctx.feature, ctx.emit);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runCheck(ctx: BuiltinContext, check: PlannedCheck): Promise<CheckResult> {
|
|
98
|
+
const started = performance.now();
|
|
99
|
+
const deadline = AbortSignal.any([ctx.signal, AbortSignal.timeout(check.timeoutMs)]);
|
|
100
|
+
const deadlineAt = Math.min(ctx.deadlineAt, ctx.now().getTime() + check.timeoutMs);
|
|
101
|
+
const timedOut = Promise.withResolvers<never>();
|
|
102
|
+
|
|
103
|
+
// An abort listener added to a signal that already fired never runs, so the check would run on.
|
|
104
|
+
if (deadline.aborted) {
|
|
105
|
+
return {
|
|
106
|
+
name: check.name,
|
|
107
|
+
status: 'failed',
|
|
108
|
+
durationMs: 0,
|
|
109
|
+
code: 'timeout',
|
|
110
|
+
message: `${check.name} did not start: the doctor's deadline had passed.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const onAbort = (): void => {
|
|
115
|
+
timedOut.reject(
|
|
116
|
+
new WringerError('timeout', { message: `${check.name} ran past ${check.timeoutMs} ms.` }),
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
deadline.addEventListener('abort', onAbort, { once: true });
|
|
121
|
+
|
|
122
|
+
// A check that throws before it returns its promise fails like one that rejects.
|
|
123
|
+
const work = Promise.try(() =>
|
|
124
|
+
check.run(ctx.openDrive('read-only', { signal: deadline, at: deadlineAt })),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await Promise.race([work, timedOut.promise]);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
name: check.name,
|
|
132
|
+
status: 'passed',
|
|
133
|
+
durationMs: Math.round(performance.now() - started),
|
|
134
|
+
code: null,
|
|
135
|
+
message: null,
|
|
136
|
+
};
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const failure = toWringerError(error);
|
|
139
|
+
|
|
140
|
+
// The doctor answers at the check's timeout, while the check runs on in the run it read.
|
|
141
|
+
if (deadline.aborted) {
|
|
142
|
+
ctx.keepLate(
|
|
143
|
+
writeLateFailure(work, {
|
|
144
|
+
state: ctx.state,
|
|
145
|
+
runID: ctx.run?.runID ?? '',
|
|
146
|
+
command: `doctor ${check.name}`,
|
|
147
|
+
answered: null,
|
|
148
|
+
now: ctx.now,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
name: check.name,
|
|
155
|
+
status: 'failed',
|
|
156
|
+
durationMs: Math.round(performance.now() - started),
|
|
157
|
+
code: failure.code,
|
|
158
|
+
message: failure.message,
|
|
159
|
+
};
|
|
160
|
+
} finally {
|
|
161
|
+
deadline.removeEventListener('abort', onAbort);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { buildBuiltinCommand } from '../build-commands.ts';
|
|
3
|
+
import type { RegisteredCommand } from '../types.ts';
|
|
4
|
+
import { WringerError } from '../wringer-error.ts';
|
|
5
|
+
|
|
6
|
+
const execArgsSchema = z.strictObject({ argv: z.array(z.string()).min(1) });
|
|
7
|
+
|
|
8
|
+
const execDataSchema = z.strictObject({
|
|
9
|
+
exitCode: z.number().int(),
|
|
10
|
+
stdout: z.string(),
|
|
11
|
+
stderr: z.string(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// A raw exec could do anything, so it is always a write: a read-only target refuses it, and a repo
|
|
15
|
+
// that needs a read there declares a read-only action.
|
|
16
|
+
export function buildExecCommand(): RegisteredCommand {
|
|
17
|
+
return buildBuiltinCommand({
|
|
18
|
+
path: ['exec'],
|
|
19
|
+
summary: "Run a command through the target's host, local or ssh",
|
|
20
|
+
args: execArgsSchema,
|
|
21
|
+
positionals: ['argv'],
|
|
22
|
+
data: execDataSchema,
|
|
23
|
+
effect: 'write',
|
|
24
|
+
needsRun: true,
|
|
25
|
+
needsDoctor: true,
|
|
26
|
+
run: async (args, ctx) => {
|
|
27
|
+
const drive = ctx.openDrive(ctx.target.access);
|
|
28
|
+
|
|
29
|
+
const result = await drive.ctx.exec(args.argv);
|
|
30
|
+
|
|
31
|
+
if (result.exitCode !== 0) {
|
|
32
|
+
throw new WringerError('check-failed', {
|
|
33
|
+
message: `${args.argv[0] ?? 'The command'} exited with code ${result.exitCode}.`,
|
|
34
|
+
data: result,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|