@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,37 @@
|
|
|
1
|
+
import type { GitRequest } from './run-git.ts';
|
|
2
|
+
import { WringerError } from './wringer-error.ts';
|
|
3
|
+
|
|
4
|
+
export interface MergeBaseStepInput {
|
|
5
|
+
readonly root: string;
|
|
6
|
+
readonly base: string;
|
|
7
|
+
|
|
8
|
+
// The text of `git rev-list --max-count=1 --all`: empty when the repo has no commit.
|
|
9
|
+
readonly newestCommit: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MergeBaseSteps {
|
|
13
|
+
readonly verifyBase: GitRequest;
|
|
14
|
+
readonly mergeBase: GitRequest;
|
|
15
|
+
|
|
16
|
+
readonly noSharedCommit: WringerError;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// `merge-base` names no cause when HEAD is unborn or the base is missing, so a repo with no commit
|
|
20
|
+
// fails before it, and a missing base fails `verifyBase` with git's own message.
|
|
21
|
+
export function pickMergeBaseSteps(input: MergeBaseStepInput): MergeBaseSteps | WringerError {
|
|
22
|
+
if (input.newestCommit.trim() === '') {
|
|
23
|
+
return new WringerError('environment', {
|
|
24
|
+
message: 'The repo has no commit yet, so its change has no fingerprint.',
|
|
25
|
+
hint: 'Commit once, then run the command again.',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
verifyBase: { cwd: input.root, args: ['rev-parse', '--verify', `${input.base}^{commit}`] },
|
|
31
|
+
mergeBase: { cwd: input.root, args: ['merge-base', 'HEAD', input.base] },
|
|
32
|
+
noSharedCommit: new WringerError('environment', {
|
|
33
|
+
message: `HEAD and ${input.base} share no commit in this clone, so the change has no fingerprint.`,
|
|
34
|
+
hint: 'In a shallow clone, fetch more history, such as with `git fetch --unshallow`.',
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface CleanupFacts {
|
|
2
|
+
readonly launched: boolean;
|
|
3
|
+
readonly serviceKeys: readonly string[];
|
|
4
|
+
|
|
5
|
+
// Each part whose stop has already started.
|
|
6
|
+
readonly stopping: ReadonlySet<string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PendingCleanups {
|
|
10
|
+
readonly app: readonly string[];
|
|
11
|
+
readonly services: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const appCleanupKey = 'app';
|
|
15
|
+
|
|
16
|
+
// Each part that is up and not yet stopping. The app stops before the services, so a service that
|
|
17
|
+
// records its log, such as a stream recorder, sees its last lines.
|
|
18
|
+
export function pickPendingCleanups(facts: CleanupFacts): PendingCleanups {
|
|
19
|
+
return {
|
|
20
|
+
app: facts.launched && !facts.stopping.has(appCleanupKey) ? [appCleanupKey] : [],
|
|
21
|
+
services: facts.serviceKeys.filter((key) => !facts.stopping.has(key)),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { WringerError } from './wringer-error.ts';
|
|
2
|
+
|
|
3
|
+
// `status --porcelain=v2` gives each submodule a field `S<c><m><u>`: `m` is `M` when its tracked
|
|
4
|
+
// files changed and `u` is `U` when it holds untracked files. A moved commit alone reaches the
|
|
5
|
+
// fingerprint through the gitlink, but work inside a submodule never does.
|
|
6
|
+
export function pickSubmoduleRefusal(statusEntries: Uint8Array): WringerError | null {
|
|
7
|
+
const dirty = collectDirtySubmodules(Buffer.from(statusEntries).toString('utf8'));
|
|
8
|
+
|
|
9
|
+
if (dirty.length === 0) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return new WringerError('environment', {
|
|
14
|
+
message: `The submodule ${dirty.join(', ')} holds work that is not committed in it, so the change has no fingerprint.`,
|
|
15
|
+
hint: 'Commit or stash the work inside the submodule, then run the command again.',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A changed entry is `1`, a renamed one `2` and an unmerged one `u`, each with this many fields
|
|
20
|
+
// before its path; with `-z`, a renamed entry's old path follows as an entry of its own.
|
|
21
|
+
const fieldsBeforePath: ReadonlyMap<string, number> = new Map([
|
|
22
|
+
['1', 8],
|
|
23
|
+
['2', 9],
|
|
24
|
+
['u', 10],
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
function collectDirtySubmodules(status: string): string[] {
|
|
28
|
+
const entries = status.split('\0');
|
|
29
|
+
const dirty: string[] = [];
|
|
30
|
+
let index = 0;
|
|
31
|
+
|
|
32
|
+
while (index < entries.length) {
|
|
33
|
+
const fields = (entries[index] ?? '').split(' ');
|
|
34
|
+
const [kind = '', , submodule = ''] = fields;
|
|
35
|
+
const pathIndex = fieldsBeforePath.get(kind);
|
|
36
|
+
const isDirty = submodule[2] === 'M' || submodule[3] === 'U';
|
|
37
|
+
|
|
38
|
+
if (pathIndex !== undefined && submodule.startsWith('S') && isDirty) {
|
|
39
|
+
dirty.push(fields.slice(pathIndex).join(' '));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
index += kind === '2' ? 2 : 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return dirty;
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { findOwnValue } from './find-own-value.ts';
|
|
2
|
+
import { supervisorServiceName } from './supervisor-protocol.ts';
|
|
3
|
+
import type { SupervisorRequest } from './supervisor-protocol.ts';
|
|
4
|
+
import type { ServiceInstance } from './types.ts';
|
|
5
|
+
import { WringerError } from './wringer-error.ts';
|
|
6
|
+
|
|
7
|
+
export type SupervisorOperation =
|
|
8
|
+
| { readonly kind: 'stop' }
|
|
9
|
+
| { readonly kind: 'status' }
|
|
10
|
+
| {
|
|
11
|
+
readonly kind: 'call';
|
|
12
|
+
|
|
13
|
+
// Empty for a request with no deadline; otherwise the one delay after which the call aborts.
|
|
14
|
+
readonly cancelAfterMs: readonly number[];
|
|
15
|
+
readonly call: (signal: AbortSignal) => Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
| { readonly kind: 'refuse'; readonly error: WringerError };
|
|
18
|
+
|
|
19
|
+
// A service method's signal aborts this long before the caller's deadline, so the method has time
|
|
20
|
+
// to stop its work, such as a browser closing its page, before the caller reports a timeout.
|
|
21
|
+
const serviceCancelReserveMs = 1000;
|
|
22
|
+
|
|
23
|
+
export function pickSupervisorOperation(
|
|
24
|
+
request: SupervisorRequest,
|
|
25
|
+
services: ReadonlyMap<string, Pick<ServiceInstance, 'methods'>>,
|
|
26
|
+
nowMs: number,
|
|
27
|
+
): SupervisorOperation {
|
|
28
|
+
if (request.service === supervisorServiceName) {
|
|
29
|
+
return request.method === 'stop' ? { kind: 'stop' } : { kind: 'status' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const methods = services.get(request.service)?.methods ?? {};
|
|
33
|
+
const method = findOwnValue(methods, request.method);
|
|
34
|
+
|
|
35
|
+
if (method === undefined) {
|
|
36
|
+
return {
|
|
37
|
+
kind: 'refuse',
|
|
38
|
+
error: new WringerError('usage', {
|
|
39
|
+
message: `The run has no service call ${request.service} ${request.method}.`,
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
kind: 'call',
|
|
46
|
+
cancelAfterMs:
|
|
47
|
+
request.deadlineAt === undefined
|
|
48
|
+
? []
|
|
49
|
+
: [Math.max(0, request.deadlineAt - serviceCancelReserveMs - nowMs)],
|
|
50
|
+
call: (signal) => method(request.args, signal),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { ArgsResult } from './types.ts';
|
|
3
|
+
|
|
4
|
+
// Parses raw arguments with a command's own schema and hands the typed result to `build`, so the
|
|
5
|
+
// typed arguments never leave the closure that knows their type.
|
|
6
|
+
export function prepareArgs<Args, Prepared>(
|
|
7
|
+
schema: z.ZodType<Args>,
|
|
8
|
+
raw: unknown,
|
|
9
|
+
build: (args: Args) => Prepared,
|
|
10
|
+
): ArgsResult<Prepared> {
|
|
11
|
+
const parsed = schema.safeParse(raw ?? {});
|
|
12
|
+
|
|
13
|
+
if (!parsed.success) {
|
|
14
|
+
const problems = parsed.error.issues.map((issue) =>
|
|
15
|
+
issue.path.length === 0 ? issue.message : `${issue.path.join('.')}: ${issue.message}`,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return { ok: false, message: `The arguments are not valid: ${problems.join('; ')}` };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { ok: true, prepared: build(parsed.data) };
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isErrorCode } from './is-error-code.ts';
|
|
2
|
+
|
|
3
|
+
// Signal 0 checks that the process exists and sends nothing. EPERM means it runs as another user.
|
|
4
|
+
export function probeProcess(pid: number): boolean {
|
|
5
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
process.kill(pid, 0);
|
|
11
|
+
|
|
12
|
+
return true;
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return isErrorCode(error, 'EPERM');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { WringerError } from './wringer-error.ts';
|
|
2
|
+
|
|
3
|
+
// The work's own promise may never settle once its deadline passes, so the deadline settles the
|
|
4
|
+
// race with a timeout, including a deadline that passed before the race began.
|
|
5
|
+
export async function raceDeadline<T>(work: Promise<T>, deadline: AbortSignal): Promise<T> {
|
|
6
|
+
const aborted = Promise.withResolvers<T>();
|
|
7
|
+
|
|
8
|
+
const onAbort = (): void => {
|
|
9
|
+
aborted.reject(new WringerError('timeout'));
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
deadline.addEventListener('abort', onAbort, { once: true });
|
|
13
|
+
|
|
14
|
+
if (deadline.aborted) {
|
|
15
|
+
onAbort();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return await Promise.race([work, aborted.promise]);
|
|
20
|
+
} finally {
|
|
21
|
+
deadline.removeEventListener('abort', onAbort);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FingerprintHost } from './read-fingerprint.ts';
|
|
2
|
+
|
|
3
|
+
export interface ChangedPathsRequest {
|
|
4
|
+
readonly cwd: string;
|
|
5
|
+
readonly base: string;
|
|
6
|
+
readonly exclude: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// The paths the branch changes against its merge base: committed, staged and unstaged edits, and
|
|
10
|
+
// untracked files that git does not ignore.
|
|
11
|
+
export async function readChangedPaths(
|
|
12
|
+
host: Pick<FingerprintHost, 'runGit'>,
|
|
13
|
+
request: ChangedPathsRequest,
|
|
14
|
+
): Promise<readonly string[]> {
|
|
15
|
+
const mergeBaseResult = await host.runGit({
|
|
16
|
+
cwd: request.cwd,
|
|
17
|
+
args: ['merge-base', 'HEAD', request.base],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const mergeBase = mergeBaseResult.stdout.trim();
|
|
21
|
+
const excludes = request.exclude.map((path) => `:(exclude)${path}`);
|
|
22
|
+
|
|
23
|
+
// `-z` gives each path as it is: without it, git quotes a path with a tab, a newline or a
|
|
24
|
+
// non-ASCII letter, and the quoted form matches no feature's glob.
|
|
25
|
+
const [tracked, untracked] = await Promise.all([
|
|
26
|
+
host.runGit({
|
|
27
|
+
cwd: request.cwd,
|
|
28
|
+
args: ['diff', '--name-only', '-z', '--no-renames', mergeBase, '--', '.', ...excludes],
|
|
29
|
+
}),
|
|
30
|
+
host.runGit({
|
|
31
|
+
cwd: request.cwd,
|
|
32
|
+
args: ['ls-files', '-z', '--others', '--exclude-standard', '--', '.', ...excludes],
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const paths = `${tracked.stdout}\0${untracked.stdout}`.split('\0').filter((path) => path !== '');
|
|
37
|
+
|
|
38
|
+
return [...new Set(paths)].toSorted();
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FingerprintHost } from './read-fingerprint.ts';
|
|
2
|
+
|
|
3
|
+
// The base a branch merges into: the remote's default branch when the clone knows it, else `main`.
|
|
4
|
+
// git exits non-zero when the clone has no remote default branch, which is a normal answer here.
|
|
5
|
+
export async function readDefaultBase(
|
|
6
|
+
host: Pick<FingerprintHost, 'runGit'>,
|
|
7
|
+
root: string,
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
try {
|
|
10
|
+
const result = await host.runGit({
|
|
11
|
+
cwd: root,
|
|
12
|
+
args: ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const base = result.stdout.trim();
|
|
16
|
+
|
|
17
|
+
return base === '' ? 'main' : base;
|
|
18
|
+
} catch {
|
|
19
|
+
return 'main';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { buildPromisedObjectReads } from './build-promised-object-reads.ts';
|
|
4
|
+
import { buildSkipWorktreeMarks } from './build-skip-worktree-marks.ts';
|
|
5
|
+
import { buildSubmoduleAdds } from './build-submodule-adds.ts';
|
|
6
|
+
import { parsePatchID } from './parse-patch-id.ts';
|
|
7
|
+
import { pickMergeBaseFailure } from './pick-merge-base-failure.ts';
|
|
8
|
+
import { pickMergeBaseSteps } from './pick-merge-base-steps.ts';
|
|
9
|
+
import { pickSubmoduleRefusal } from './pick-submodule-refusal.ts';
|
|
10
|
+
import type { GitRequest } from './run-git.ts';
|
|
11
|
+
import type { ProcessResult } from './run-process.ts';
|
|
12
|
+
import { WringerError } from './wringer-error.ts';
|
|
13
|
+
|
|
14
|
+
export interface FingerprintHost {
|
|
15
|
+
readonly runGit: (request: GitRequest) => Promise<ProcessResult>;
|
|
16
|
+
readonly runGitBytes: (request: GitRequest) => Promise<Uint8Array>;
|
|
17
|
+
readonly removeDirectory: (path: string) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FingerprintRequest {
|
|
21
|
+
readonly cwd: string;
|
|
22
|
+
readonly base: string;
|
|
23
|
+
readonly exclude: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const pinnedDiffOptions = [
|
|
27
|
+
'--unified=3',
|
|
28
|
+
'--inter-hunk-context=0',
|
|
29
|
+
'--no-renames',
|
|
30
|
+
'--diff-algorithm=myers',
|
|
31
|
+
'--indent-heuristic',
|
|
32
|
+
'--src-prefix=a/',
|
|
33
|
+
'--dst-prefix=b/',
|
|
34
|
+
'--ignore-submodules=none',
|
|
35
|
+
'--submodule=short',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// The index is built and diffed in a git folder of its own, so only the repo's tracked files and
|
|
39
|
+
// git's defaults shape the change: no config, attributes or ignore rules of the clone, the user or
|
|
40
|
+
// the system reach it, and the same change gets the same fingerprint in any clone.
|
|
41
|
+
export async function readFingerprint(
|
|
42
|
+
host: FingerprintHost,
|
|
43
|
+
request: FingerprintRequest,
|
|
44
|
+
): Promise<string> {
|
|
45
|
+
const root = await readRepoRoot(host, request.cwd);
|
|
46
|
+
const mergeBase = await readMergeBase(host, root, request.base);
|
|
47
|
+
|
|
48
|
+
await readPromisedObjects(host, root, mergeBase);
|
|
49
|
+
await readSubmoduleWork(host, root);
|
|
50
|
+
|
|
51
|
+
const gitDir = await readGitPath(host, root, `wringer-${randomUUID()}`);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const env = await createIsolatedGitDir(host, root, gitDir);
|
|
55
|
+
|
|
56
|
+
// The index starts as a copy of the repo's own, so a file staged past an ignore rule counts as
|
|
57
|
+
// a commit would count it. `--index-info` takes the lines `ls-files --stage` prints.
|
|
58
|
+
const staged = await host.runGitBytes({ cwd: root, args: ['ls-files', '--stage', '-z'] });
|
|
59
|
+
|
|
60
|
+
await host.runGit({
|
|
61
|
+
cwd: root,
|
|
62
|
+
args: ['update-index', '-z', '--index-info'],
|
|
63
|
+
stdin: staged,
|
|
64
|
+
env,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await copySkipWorktreeFlags(host, root, env);
|
|
68
|
+
|
|
69
|
+
// With sparse checkout on, git 2.37 and later clears the skip-worktree mark of each file on disk
|
|
70
|
+
// as it reads the index, so an edit to a marked file counts and only an absent one stays out.
|
|
71
|
+
await host.runGit({
|
|
72
|
+
cwd: root,
|
|
73
|
+
args: ['-c', 'core.sparseCheckout=true', 'add', '--all'],
|
|
74
|
+
env,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await addSubmodules(host, root, env);
|
|
78
|
+
|
|
79
|
+
// The exclusions apply to the diff, not to `add`: `add` refuses an ignored path in a pathspec,
|
|
80
|
+
// and a committed change to an excluded path reaches the index from the repo's own anyway.
|
|
81
|
+
const diff = await host.runGitBytes({
|
|
82
|
+
cwd: root,
|
|
83
|
+
args: [
|
|
84
|
+
'diff',
|
|
85
|
+
'--cached',
|
|
86
|
+
'--binary',
|
|
87
|
+
'--no-color',
|
|
88
|
+
'--no-ext-diff',
|
|
89
|
+
'--no-textconv',
|
|
90
|
+
'--no-relative',
|
|
91
|
+
...pinnedDiffOptions,
|
|
92
|
+
mergeBase,
|
|
93
|
+
'--',
|
|
94
|
+
':(top)',
|
|
95
|
+
...request.exclude.map((path) => `:(top,exclude)${path}`),
|
|
96
|
+
],
|
|
97
|
+
env,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const patchID = await host.runGit({
|
|
101
|
+
cwd: root,
|
|
102
|
+
|
|
103
|
+
// `--verbatim` hashes with the stable algorithm and keeps whitespace, so `"a b"` and `"ab"`
|
|
104
|
+
// differ; `--stable` alone drops it, and git refuses the two flags together.
|
|
105
|
+
args: ['patch-id', '--verbatim'],
|
|
106
|
+
stdin: diff,
|
|
107
|
+
env,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return parsePatchID(patchID.stdout);
|
|
111
|
+
} finally {
|
|
112
|
+
await host.removeDirectory(gitDir);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// From a subfolder, `ls-files` lists only that folder, with paths relative to it, so every index
|
|
117
|
+
// step runs from the root.
|
|
118
|
+
async function readRepoRoot(host: FingerprintHost, cwd: string): Promise<string> {
|
|
119
|
+
const result = await host.runGit({ cwd, args: ['rev-parse', '--show-toplevel'] });
|
|
120
|
+
|
|
121
|
+
// A folder name may end in a space, so only the line end git adds comes off.
|
|
122
|
+
return resolve(cwd, result.stdout.replace(/\n$/u, ''));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readMergeBase(host: FingerprintHost, root: string, base: string): Promise<string> {
|
|
126
|
+
const commits = await host.runGit({ cwd: root, args: ['rev-list', '--max-count=1', '--all'] });
|
|
127
|
+
|
|
128
|
+
const steps = pickMergeBaseSteps({ root, base, newestCommit: commits.stdout });
|
|
129
|
+
|
|
130
|
+
if (steps instanceof WringerError) {
|
|
131
|
+
throw steps;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await host.runGit(steps.verifyBase);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const result = await host.runGit(steps.mergeBase);
|
|
138
|
+
|
|
139
|
+
return result.stdout.trim();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
throw pickMergeBaseFailure(error, steps.noSharedCommit);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readPromisedObjects(
|
|
146
|
+
host: FingerprintHost,
|
|
147
|
+
root: string,
|
|
148
|
+
mergeBase: string,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const config = await host.runGit({ cwd: root, args: ['config', '--list', '-z'] });
|
|
151
|
+
|
|
152
|
+
for (const request of buildPromisedObjectReads({ root, mergeBase, configList: config.stdout })) {
|
|
153
|
+
await host.runGit(request);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// `--no-optional-locks` keeps status from writing the repo's own index, and `-uno` skips the
|
|
158
|
+
// repo's own untracked files, which `add --all` counts anyway.
|
|
159
|
+
async function readSubmoduleWork(host: FingerprintHost, root: string): Promise<void> {
|
|
160
|
+
const status = await host.runGitBytes({
|
|
161
|
+
cwd: root,
|
|
162
|
+
args: [
|
|
163
|
+
'--no-optional-locks',
|
|
164
|
+
'status',
|
|
165
|
+
'--porcelain=v2',
|
|
166
|
+
'-z',
|
|
167
|
+
'--ignore-submodules=none',
|
|
168
|
+
'--untracked-files=no',
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const refusal = pickSubmoduleRefusal(status);
|
|
173
|
+
|
|
174
|
+
if (refusal !== null) {
|
|
175
|
+
throw refusal;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// git prints a path inside the git folder relative to the folder it runs in; the path leaves here
|
|
180
|
+
// absolute.
|
|
181
|
+
async function readGitPath(host: FingerprintHost, root: string, name: string): Promise<string> {
|
|
182
|
+
const result = await host.runGit({ cwd: root, args: ['rev-parse', '--git-path', name] });
|
|
183
|
+
|
|
184
|
+
return resolve(root, result.stdout.replace(/\n$/u, ''));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Only git's defaults apply: no system or global config, and no `-c` from a calling git. The two
|
|
188
|
+
// keys turn off the attributes and ignore files git reads from the user's config folder by default.
|
|
189
|
+
const isolatedConfigEnv = {
|
|
190
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
191
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
192
|
+
GIT_CONFIG_PARAMETERS: '',
|
|
193
|
+
GIT_CONFIG_COUNT: '2',
|
|
194
|
+
GIT_CONFIG_KEY_0: 'core.attributesFile',
|
|
195
|
+
GIT_CONFIG_VALUE_0: '/dev/null',
|
|
196
|
+
GIT_CONFIG_KEY_1: 'core.excludesFile',
|
|
197
|
+
GIT_CONFIG_VALUE_1: '/dev/null',
|
|
198
|
+
GIT_ATTR_NOSYSTEM: '1',
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// The folder reads the repo's objects as alternates and writes its own, so the repo gains no
|
|
202
|
+
// object. A bare folder made with no template has no `info/attributes` and no `info/exclude`.
|
|
203
|
+
async function createIsolatedGitDir(
|
|
204
|
+
host: FingerprintHost,
|
|
205
|
+
root: string,
|
|
206
|
+
gitDir: string,
|
|
207
|
+
): Promise<Readonly<Record<string, string>>> {
|
|
208
|
+
const objects = await readGitPath(host, root, 'objects');
|
|
209
|
+
const format = await host.runGit({ cwd: root, args: ['rev-parse', '--show-object-format'] });
|
|
210
|
+
|
|
211
|
+
await host.runGit({
|
|
212
|
+
cwd: root,
|
|
213
|
+
args: ['init', '--quiet', '--bare', '--template=', `--object-format=${format.stdout.trim()}`],
|
|
214
|
+
env: { ...isolatedConfigEnv, GIT_DIR: gitDir, GIT_COMMON_DIR: gitDir },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return buildIsolatedEnv(gitDir, root, objects);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Each variable a calling git, such as a hook's, may set is overwritten, so the steps cannot reach
|
|
221
|
+
// the repo's own index, objects or git folder. git lets GIT_DIFF_OPTS override `--unified`, and
|
|
222
|
+
// the pathspec variables change what `:(top)` and each exclusion match.
|
|
223
|
+
function buildIsolatedEnv(
|
|
224
|
+
gitDir: string,
|
|
225
|
+
root: string,
|
|
226
|
+
objects: string,
|
|
227
|
+
): Readonly<Record<string, string>> {
|
|
228
|
+
return {
|
|
229
|
+
...isolatedConfigEnv,
|
|
230
|
+
GIT_DIR: gitDir,
|
|
231
|
+
GIT_COMMON_DIR: gitDir,
|
|
232
|
+
GIT_WORK_TREE: root,
|
|
233
|
+
GIT_INDEX_FILE: join(gitDir, 'index'),
|
|
234
|
+
GIT_OBJECT_DIRECTORY: join(gitDir, 'objects'),
|
|
235
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES: toQuotedGitPath(objects),
|
|
236
|
+
GIT_DIFF_OPTS: '--unified=3',
|
|
237
|
+
GIT_LITERAL_PATHSPECS: '0',
|
|
238
|
+
GIT_GLOB_PATHSPECS: '0',
|
|
239
|
+
GIT_NOGLOB_PATHSPECS: '0',
|
|
240
|
+
GIT_ICASE_PATHSPECS: '0',
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// git splits the alternates list at `:`, unless an entry is quoted the way C quotes a string.
|
|
245
|
+
function toQuotedGitPath(path: string): string {
|
|
246
|
+
return `"${path.replaceAll('\\', String.raw`\\`).replaceAll('"', String.raw`\"`)}"`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function copySkipWorktreeFlags(
|
|
250
|
+
host: FingerprintHost,
|
|
251
|
+
root: string,
|
|
252
|
+
env: Readonly<Record<string, string>>,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
const tagged = await host.runGitBytes({ cwd: root, args: ['ls-files', '-t', '-z'] });
|
|
255
|
+
|
|
256
|
+
for (const request of buildSkipWorktreeMarks({ root, env, taggedEntries: tagged })) {
|
|
257
|
+
await host.runGit(request);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function addSubmodules(
|
|
262
|
+
host: FingerprintHost,
|
|
263
|
+
root: string,
|
|
264
|
+
env: Readonly<Record<string, string>>,
|
|
265
|
+
): Promise<void> {
|
|
266
|
+
const staged = await host.runGitBytes({ cwd: root, args: ['ls-files', '--stage', '-z'], env });
|
|
267
|
+
|
|
268
|
+
for (const request of buildSubmoduleAdds({ root, env, stagedEntries: staged })) {
|
|
269
|
+
await host.runGit(request);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FingerprintHost } from './read-fingerprint.ts';
|
|
2
|
+
import { WringerError } from './wringer-error.ts';
|
|
3
|
+
|
|
4
|
+
// The SHA only labels an envelope, so a git that fails or cannot start leaves it null and the
|
|
5
|
+
// command's own result stands.
|
|
6
|
+
export async function readHeadSHA(
|
|
7
|
+
host: Pick<FingerprintHost, 'runGit'>,
|
|
8
|
+
root: string,
|
|
9
|
+
): Promise<string | null> {
|
|
10
|
+
const result = await host
|
|
11
|
+
.runGit({ cwd: root, args: ['rev-parse', '--short', 'HEAD'] })
|
|
12
|
+
.catch((error: unknown) => {
|
|
13
|
+
if (error instanceof WringerError) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
throw error;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const sha = result?.stdout.trim() ?? '';
|
|
21
|
+
|
|
22
|
+
return sha === '' ? null : sha;
|
|
23
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { appendFile } from 'node:fs/promises';
|
|
2
|
+
import { buildLaunchGuard } from './build-launch-guard.ts';
|
|
3
|
+
import { createDirectory } from './create-directory.ts';
|
|
4
|
+
import { createSocketFolder } from './create-socket-folder.ts';
|
|
5
|
+
import { killProcessGroup } from './kill-process-group.ts';
|
|
6
|
+
import { listenSupervisorSocket } from './listen-supervisor-socket.ts';
|
|
7
|
+
import type { SupervisorSocket, SupervisorSocketOptions } from './listen-supervisor-socket.ts';
|
|
8
|
+
import { removeDirectory } from './remove-directory.ts';
|
|
9
|
+
import { runProcess } from './run-process.ts';
|
|
10
|
+
import type { ProcessRequest, ProcessResult } from './run-process.ts';
|
|
11
|
+
|
|
12
|
+
export interface AppSpawnOptions {
|
|
13
|
+
readonly cwd: string;
|
|
14
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AppProcess {
|
|
18
|
+
readonly pid: number;
|
|
19
|
+
readonly stdout: ReadableStream<Uint8Array>;
|
|
20
|
+
readonly stderr: ReadableStream<Uint8Array>;
|
|
21
|
+
readonly exited: Promise<number>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Everything the launch reads from or does to this machine.
|
|
25
|
+
export interface LaunchHost {
|
|
26
|
+
readonly guard: readonly string[];
|
|
27
|
+
readonly cwd: string;
|
|
28
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
29
|
+
readonly spawnApp: (argv: readonly string[], options: AppSpawnOptions) => AppProcess;
|
|
30
|
+
readonly appendLog: (path: string, text: string) => Promise<void>;
|
|
31
|
+
readonly createDirectory: (path: string) => Promise<void>;
|
|
32
|
+
|
|
33
|
+
// Returns the socket's path, in the temp folder when the preferred path is too long for a socket.
|
|
34
|
+
readonly createSocketFolder: (preferredSocket: string) => Promise<string>;
|
|
35
|
+
readonly removeDirectory: (path: string) => Promise<void>;
|
|
36
|
+
readonly listenSupervisorSocket: (options: SupervisorSocketOptions) => Promise<SupervisorSocket>;
|
|
37
|
+
readonly killProcessGroup: (pid: number, signal?: NodeJS.Signals) => void;
|
|
38
|
+
readonly runProcess: (request: ProcessRequest) => Promise<ProcessResult>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The app gets a process group of its own, so a stop can signal every process it started.
|
|
42
|
+
export function readLaunchHost(): LaunchHost {
|
|
43
|
+
return {
|
|
44
|
+
guard: buildLaunchGuard(Bun.which('setpriv')),
|
|
45
|
+
cwd: process.cwd(),
|
|
46
|
+
env: { ...process.env },
|
|
47
|
+
spawnApp: (argv, options) =>
|
|
48
|
+
Bun.spawn([...argv], {
|
|
49
|
+
cwd: options.cwd,
|
|
50
|
+
env: { ...options.env },
|
|
51
|
+
stdin: 'ignore',
|
|
52
|
+
stdout: 'pipe',
|
|
53
|
+
stderr: 'pipe',
|
|
54
|
+
detached: true,
|
|
55
|
+
}),
|
|
56
|
+
appendLog: appendFile,
|
|
57
|
+
createDirectory,
|
|
58
|
+
createSocketFolder,
|
|
59
|
+
removeDirectory,
|
|
60
|
+
listenSupervisorSocket,
|
|
61
|
+
killProcessGroup,
|
|
62
|
+
runProcess,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readlinkSync } from 'node:fs';
|
|
2
|
+
import { isErrorCode } from './is-error-code.ts';
|
|
3
|
+
|
|
4
|
+
const namespacePattern = /\[(?<id>\d+)\]/v;
|
|
5
|
+
|
|
6
|
+
// A PID names a process only inside its PID namespace, so two containers that share a folder can
|
|
7
|
+
// both run a process 4100. A host with no `/proc`, such as macOS, has the one namespace `host`.
|
|
8
|
+
export function readPIDNamespace(): string {
|
|
9
|
+
try {
|
|
10
|
+
return namespacePattern.exec(readlinkSync('/proc/self/ns/pid'))?.groups?.['id'] ?? 'host';
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (isErrorCode(error, 'ENOENT') || isErrorCode(error, 'EACCES')) {
|
|
13
|
+
return 'host';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|