@parcha/pi-durable-disk 0.0.0-stage → 0.1.0-beta.13
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/CHANGELOG.md +329 -0
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +695 -2
- package/bin/archil-scoped +165 -0
- package/dist/app.d.ts +40 -0
- package/dist/app.js +47 -0
- package/dist/claim.d.ts +222 -0
- package/dist/claim.js +722 -0
- package/dist/cli.d.ts +64 -0
- package/dist/cli.js +664 -0
- package/dist/commands.d.ts +67 -0
- package/dist/commands.js +299 -0
- package/dist/confine.d.ts +91 -0
- package/dist/confine.js +341 -0
- package/dist/env.d.ts +129 -0
- package/dist/env.js +608 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.js +55 -0
- package/dist/fork.d.ts +41 -0
- package/dist/fork.js +181 -0
- package/dist/hosts/daytona-launch.d.ts +52 -0
- package/dist/hosts/daytona-launch.js +270 -0
- package/dist/hosts/daytona.d.ts +147 -0
- package/dist/hosts/daytona.js +368 -0
- package/dist/hosts/docker.d.ts +103 -0
- package/dist/hosts/docker.js +362 -0
- package/dist/hosts/local-host.d.ts +71 -0
- package/dist/hosts/local-host.js +356 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +17 -0
- package/dist/lease.d.ts +7 -0
- package/dist/lease.js +6 -0
- package/dist/local-run.d.ts +10 -0
- package/dist/local-run.js +63 -0
- package/dist/park.d.ts +92 -0
- package/dist/park.js +229 -0
- package/dist/run.d.ts +290 -0
- package/dist/run.js +785 -0
- package/dist/serve.d.ts +101 -0
- package/dist/serve.js +393 -0
- package/dist/status.d.ts +123 -0
- package/dist/status.js +264 -0
- package/dist/store.d.ts +103 -0
- package/dist/store.js +217 -0
- package/dist/supervise.d.ts +340 -0
- package/dist/supervise.js +528 -0
- package/dist/watchdog.d.ts +39 -0
- package/dist/watchdog.js +137 -0
- package/package.json +87 -4
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# archil, with `mount` started in its own transient scope (<unit>-fuse-<n>.scope) when the caller runs inside a system
|
|
3
|
+
# service, so the FUSE daemon that `archil mount` forks lives outside the instance unit's control group: stopping or
|
|
4
|
+
# restarting the instance (KillMode=control-group) kills every command it started but never its mount, and an
|
|
5
|
+
# in-place restart finds its claim still held. Any other verb, and any caller outside a system service, runs
|
|
6
|
+
# archil unchanged. Run through sudo; install it root-owned, since root executes it.
|
|
7
|
+
#
|
|
8
|
+
# `mount` reads its token as one line on stdin and hands it to archil only in the environment of that exec. The token
|
|
9
|
+
# is never an argument and never in an environment sudo sees: sudo logs every variable it preserves. An empty line or
|
|
10
|
+
# no line refuses the mount.
|
|
11
|
+
#
|
|
12
|
+
# `retire <root>/runs/<run> <tag>` is for a kernel that refuses every unmount of a FUSE mount it still lists (Sysbox, as
|
|
13
|
+
# Daytona runs sandboxes: umount2 fails with ENOENT). It moves the run's fuse.archil mount aside to
|
|
14
|
+
# <root>/.released/<run>-<tag> and SIGKILLs its daemon, the process whose argument vector is this archil's `mount`
|
|
15
|
+
# naming exactly that mountpoint, so the run's path is free for its next mount. The caller checks the delegation in
|
|
16
|
+
# first; a dead mount has none left. Nothing else is moved or signalled.
|
|
17
|
+
#
|
|
18
|
+
# `stray <root>/runs/<run> <tag>` is for a run's mountpoint that is no mount and not empty (files a process wrote by
|
|
19
|
+
# path after the run's mount vanished), which archil refuses to mount over. The directory is moved, whole, to
|
|
20
|
+
# <root>/.stray/<run>-<tag> and replaced by an empty one with its owner and mode, made inside .stray first and renamed
|
|
21
|
+
# into place. Nothing in it is read, copied or brought into the run; the output names only the entry counts.
|
|
22
|
+
#
|
|
23
|
+
# `stale <root>/runs/<run>` is for an archil daemon left on a run's mountpoint with no mount in the table (a client that
|
|
24
|
+
# dropped its mount after an authentication failure, or whose mount was lazily unmounted while a process still held a
|
|
25
|
+
# reference into it, keeps its process and the mountpoint's control socket), which makes every later mount there fail
|
|
26
|
+
# with "an older Archil process is still running". It SIGKILLs the daemons whose argument vector is this archil's `mount`
|
|
27
|
+
# naming exactly that mountpoint, and refuses while a mount is listed there (a live mount's daemon is never stale).
|
|
28
|
+
#
|
|
29
|
+
# The run's user may own <root>, so root never follows a link there: <root> and <root>/runs must be their own real
|
|
30
|
+
# paths, the holding directory (.released or .stray) a real directory owned by root (made 0700 when absent), and the
|
|
31
|
+
# destination new (never one that exists).
|
|
32
|
+
set -eu
|
|
33
|
+
ARCHIL=/usr/bin/archil
|
|
34
|
+
MOUNT=/usr/bin/mount
|
|
35
|
+
MOUNTS=/proc/self/mounts
|
|
36
|
+
PROC=/proc
|
|
37
|
+
KILL=kill
|
|
38
|
+
OWNER=0
|
|
39
|
+
|
|
40
|
+
listed() { awk -v m="$1" -v t="$2" '$2 == m && (t == "" || $3 == t) { f = 1 } END { exit !f }' "$MOUNTS"; }
|
|
41
|
+
|
|
42
|
+
# The run's path and, for a verb that moves it, the tag: a plain <root>/runs/<run>, a one-segment tag, no link in <root>
|
|
43
|
+
# or <root>/runs. Sets mp, tag and root.
|
|
44
|
+
run_path() {
|
|
45
|
+
mp=$1 tag=${2-}
|
|
46
|
+
case "$mp" in */./*|*/../*|*/.|*/..|*//*) echo "archil-scoped: $mp is not a plain path" >&2; exit 2 ;; esac
|
|
47
|
+
printf '%s\n' "$mp" | grep -Eqx '/[A-Za-z0-9._/-]+/runs/[A-Za-z0-9][A-Za-z0-9._-]{0,127}' || { echo "archil-scoped: $mp is not <root>/runs/<run>" >&2; exit 2; }
|
|
48
|
+
[ "$#" -lt 2 ] || printf '%s\n' "$tag" | grep -Eqx '[A-Za-z0-9][A-Za-z0-9._-]{0,63}' || { echo "archil-scoped: tag $tag is not one safe path segment" >&2; exit 2; }
|
|
49
|
+
root=${mp%/runs/*}
|
|
50
|
+
for p in "$root" "$root/runs"; do
|
|
51
|
+
[ "$(realpath -e "$p" 2>/dev/null)" = "$p" ] || { echo "archil-scoped: $p is not its own real path (a link, or missing)" >&2; exit 2; }
|
|
52
|
+
done
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# <root>/<name> as a real directory owned by root, made 0700 when absent. Sets held.
|
|
56
|
+
holding() {
|
|
57
|
+
held="$root/$1"
|
|
58
|
+
if [ ! -e "$held" ] && [ ! -L "$held" ]; then
|
|
59
|
+
mkdir -m 0700 "$held" || { echo "archil-scoped: cannot create $held" >&2; exit 6; }
|
|
60
|
+
fi
|
|
61
|
+
if [ -L "$held" ] || [ ! -d "$held" ] || [ "$(stat -c %u "$held")" != "$OWNER" ]; then
|
|
62
|
+
echo "archil-scoped: $held is not a real directory owned by root" >&2
|
|
63
|
+
exit 6
|
|
64
|
+
fi
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
# The archil daemons of $mp: processes whose argument vector is this archil's `mount` naming exactly $mp. Sets pids.
|
|
68
|
+
# One grep over every cmdline finds the few that hold $mp as a whole argument (a per-process pipeline costs seconds on a
|
|
69
|
+
# host with thousands of processes); each of those is then matched exactly.
|
|
70
|
+
daemons_of() {
|
|
71
|
+
pids=
|
|
72
|
+
cands=$(grep -zlFx -- "$mp" "$PROC"/[0-9]*/cmdline 2>/dev/null) || true
|
|
73
|
+
while IFS= read -r f; do
|
|
74
|
+
[ -n "$f" ] || continue
|
|
75
|
+
if tr '\0' '\n' < "$f" 2>/dev/null | awk -v a="$ARCHIL" -v m="$mp" 'NR == 1 { x = ($0 == a) } NR == 2 { y = ($0 == "mount") } NR > 2 && $0 == m { z = 1 } END { exit !(x && y && z) }'; then
|
|
76
|
+
d=${f%/cmdline}
|
|
77
|
+
pids="$pids ${d##*/}"
|
|
78
|
+
fi
|
|
79
|
+
done <<EOF
|
|
80
|
+
$cands
|
|
81
|
+
EOF
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# SIGKILL $pids and wait up to 5 s for them to be gone.
|
|
85
|
+
kill_daemons() {
|
|
86
|
+
[ -n "$pids" ] || return 0
|
|
87
|
+
$KILL -KILL $pids 2>/dev/null || true
|
|
88
|
+
n=0 left=$pids
|
|
89
|
+
while [ -n "$left" ] && [ "$n" -lt 50 ]; do
|
|
90
|
+
sleep 0.1
|
|
91
|
+
n=$((n + 1))
|
|
92
|
+
left=
|
|
93
|
+
for p in $pids; do if [ -d "$PROC/$p" ]; then left="$left $p"; fi; done
|
|
94
|
+
done
|
|
95
|
+
[ -z "$left" ] || { echo "archil-scoped: archil daemon$left still alive" >&2; exit 5; }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
retire() {
|
|
99
|
+
run_path "$1" "$2"
|
|
100
|
+
listed "$mp" fuse.archil || { echo "archil-scoped: no fuse.archil mount at $mp" >&2; exit 3; }
|
|
101
|
+
holding .released
|
|
102
|
+
dest="$held/${mp##*/}-$tag"
|
|
103
|
+
daemons_of
|
|
104
|
+
mkdir -m 0700 "$dest" 2>/dev/null || { echo "archil-scoped: $dest exists or cannot be made" >&2; exit 6; }
|
|
105
|
+
[ "$(realpath -e "$dest")" = "$dest" ] || { echo "archil-scoped: $dest is not its own real path" >&2; exit 6; }
|
|
106
|
+
"$MOUNT" --move "$mp" "$dest" || { echo "archil-scoped: mount --move $mp $dest failed" >&2; exit 4; }
|
|
107
|
+
if listed "$mp" ""; then echo "archil-scoped: $mp is still mounted after the move" >&2; exit 4; fi
|
|
108
|
+
kill_daemons
|
|
109
|
+
echo "archil-scoped: moved $mp to $dest; killed:${pids:- none}"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
stale() {
|
|
113
|
+
run_path "$1"
|
|
114
|
+
if listed "$mp" ""; then echo "archil-scoped: $mp is mounted; its daemon is not stale" >&2; exit 3; fi
|
|
115
|
+
daemons_of
|
|
116
|
+
if [ -z "$pids" ]; then echo "archil-scoped: no archil daemon for $mp"; return; fi
|
|
117
|
+
if listed "$mp" ""; then echo "archil-scoped: $mp was mounted meanwhile; its daemon is not stale" >&2; exit 3; fi
|
|
118
|
+
kill_daemons
|
|
119
|
+
echo "archil-scoped: killed stale archil daemon(s) of $mp:$pids"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
stray() {
|
|
123
|
+
run_path "$1" "$2"
|
|
124
|
+
if [ -L "$mp" ] || [ ! -d "$mp" ] || [ "$(realpath -e "$mp")" != "$mp" ]; then
|
|
125
|
+
echo "archil-scoped: $mp is not a real directory" >&2
|
|
126
|
+
exit 2
|
|
127
|
+
fi
|
|
128
|
+
if listed "$mp" ""; then echo "archil-scoped: $mp is a mount" >&2; exit 3; fi
|
|
129
|
+
entries=$(find "$mp" -mindepth 1 -maxdepth 1 | wc -l)
|
|
130
|
+
if [ "$entries" -eq 0 ]; then echo "archil-scoped: $mp is empty; nothing moved"; return; fi
|
|
131
|
+
total=$(find "$mp" -mindepth 1 | wc -l)
|
|
132
|
+
holding .stray
|
|
133
|
+
dest="$held/${mp##*/}-$tag"
|
|
134
|
+
fresh="$held/.fresh-${mp##*/}-$tag"
|
|
135
|
+
for p in "$dest" "$fresh"; do
|
|
136
|
+
if [ -e "$p" ] || [ -L "$p" ]; then echo "archil-scoped: $p exists" >&2; exit 6; fi
|
|
137
|
+
done
|
|
138
|
+
# Both renames stay on one filesystem: mv would otherwise copy, as root, whatever the directory holds.
|
|
139
|
+
[ "$(stat -c %d "$root/runs")" = "$(stat -c %d "$held")" ] || { echo "archil-scoped: $held is on another filesystem than $root/runs" >&2; exit 6; }
|
|
140
|
+
set -- $(stat -c '%u %g %a' "$mp")
|
|
141
|
+
{ mkdir -m 0700 "$fresh" && chown "$1:$2" "$fresh" && chmod "$3" "$fresh"; } || { echo "archil-scoped: cannot prepare $fresh" >&2; exit 6; }
|
|
142
|
+
mv -T "$mp" "$dest" || { rmdir "$fresh"; echo "archil-scoped: moving $mp to $dest failed" >&2; exit 4; }
|
|
143
|
+
mv -T "$fresh" "$mp" || { echo "archil-scoped: $mp could not be replaced by an empty directory" >&2; exit 4; }
|
|
144
|
+
echo "archil-scoped: moved stray $mp to $dest; entries=$entries total=$total"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "${1:-}" in
|
|
148
|
+
retire | stray)
|
|
149
|
+
[ "$#" -eq 3 ] || { echo "usage: archil-scoped $1 <root>/runs/<run> <tag>" >&2; exit 2; }
|
|
150
|
+
"$1" "$2" "$3"
|
|
151
|
+
exit 0
|
|
152
|
+
;;
|
|
153
|
+
stale)
|
|
154
|
+
[ "$#" -eq 2 ] || { echo "usage: archil-scoped stale <root>/runs/<run>" >&2; exit 2; }
|
|
155
|
+
stale "$2"
|
|
156
|
+
exit 0
|
|
157
|
+
;;
|
|
158
|
+
esac
|
|
159
|
+
[ "${1:-}" = mount ] || exec "$ARCHIL" "$@"
|
|
160
|
+
token=
|
|
161
|
+
IFS= read -r token || true
|
|
162
|
+
[ -n "$token" ] || { echo "archil-scoped: no mount token on stdin" >&2; exit 2; }
|
|
163
|
+
unit=$(sed -n 's|^0::/system\.slice/\(.*/\)\{0,1\}\([A-Za-z0-9:_.-]*\)\.service$|\2|p' /proc/self/cgroup)
|
|
164
|
+
[ -n "$unit" ] || ARCHIL_MOUNT_TOKEN="$token" exec "$ARCHIL" "$@"
|
|
165
|
+
ARCHIL_MOUNT_TOKEN="$token" exec /usr/bin/systemd-run --scope --quiet --collect --unit="$unit-fuse-$(date +%s%N)" -- "$ARCHIL" "$@"
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Harness } from "@earendil-works/pi-durable";
|
|
2
|
+
import type { RunRef } from "./claim.js";
|
|
3
|
+
import { PdaError } from "./errors.js";
|
|
4
|
+
import type { DurableRun, OpenDurableRunOptions } from "./run.js";
|
|
5
|
+
/** Where the run lives on this host; known before it opens, so the app can build paths into its options. */
|
|
6
|
+
export interface AppContext {
|
|
7
|
+
readonly ref: RunRef;
|
|
8
|
+
readonly root: string;
|
|
9
|
+
readonly work: string;
|
|
10
|
+
readonly store: string;
|
|
11
|
+
}
|
|
12
|
+
/** pi's HarnessOptions without `env` (registry, models, settings, conversationCreated, now, onReport), plus the hooks below. */
|
|
13
|
+
export type AppOptions = OpenDurableRunOptions["harness"] & {
|
|
14
|
+
/**
|
|
15
|
+
* Awaited once per incarnation after the Harness is open and before it resumes (`OpenDurableRunOptions.beforeResume`):
|
|
16
|
+
* no resumed task takes a step until it settles. A rejection fails the open (exit 1, retried).
|
|
17
|
+
*/
|
|
18
|
+
beforeResume?: OpenDurableRunOptions["beforeResume"];
|
|
19
|
+
/**
|
|
20
|
+
* Called once per incarnation after the run is open and resumed; `run.generation` tells a first start from a resume.
|
|
21
|
+
* Work that must keep the instance up belongs in pi's tasks and tools: parking sees only those.
|
|
22
|
+
*/
|
|
23
|
+
onOpen?(run: DurableRun): void | Promise<void>;
|
|
24
|
+
/** pi's `root()` options for when `serve` creates the root conversation (its agent: model, extensions). */
|
|
25
|
+
root?: Parameters<Harness["root"]>[1];
|
|
26
|
+
/** Record a parking run's wake (park.ts `ParkOptions.wake`); default run.json `sleeping` with `wakeAt`. */
|
|
27
|
+
wake?(run: DurableRun, at: number | null): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
/** What `--app` names: `export default async (ctx) => ({ registry, models, onOpen })`. */
|
|
30
|
+
export type AppModule = (ctx: AppContext) => AppOptions | Promise<AppOptions>;
|
|
31
|
+
/**
|
|
32
|
+
* The app module is missing, does not load, has no default export function, throws, returns no options, or its
|
|
33
|
+
* `onOpen` failed. Exit 1: not terminal, so the unit retries within systemd's start limit.
|
|
34
|
+
*/
|
|
35
|
+
export declare class AppError extends PdaError {
|
|
36
|
+
constructor(message: string, options?: {
|
|
37
|
+
cause?: unknown;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export declare function loadApp(path: string, ctx: AppContext): Promise<AppOptions>;
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// The app an instance hosts (`pi-durable-disk run --app <module>`): an ES module whose default export is a function
|
|
10
|
+
// of where the run lives that returns pi's Harness options (without `env`, which the run builds on its claim), an
|
|
11
|
+
// optional `beforeResume` for what must settle before the run resumes, an optional `onOpen` for the app to submit or
|
|
12
|
+
// resume work once the run is open and resumed, how `serve` creates the root conversation, and how a parking run records
|
|
13
|
+
// its wake.
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { PdaError } from "./errors.js";
|
|
17
|
+
/**
|
|
18
|
+
* The app module is missing, does not load, has no default export function, throws, returns no options, or its
|
|
19
|
+
* `onOpen` failed. Exit 1: not terminal, so the unit retries within systemd's start limit.
|
|
20
|
+
*/
|
|
21
|
+
export class AppError extends PdaError {
|
|
22
|
+
constructor(message, options = {}) {
|
|
23
|
+
super("APP_FAILED", message, options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function loadApp(path, ctx) {
|
|
27
|
+
let mod;
|
|
28
|
+
try {
|
|
29
|
+
mod = (await import(__rewriteRelativeImportExtension(pathToFileURL(resolve(path)).href)));
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
throw new AppError(`cannot load the app module ${path}: ${err.message}`, { cause: err });
|
|
33
|
+
}
|
|
34
|
+
if (typeof mod.default !== "function")
|
|
35
|
+
throw new AppError(`the app module ${path} has no default export function`);
|
|
36
|
+
let options;
|
|
37
|
+
try {
|
|
38
|
+
options = await mod.default(ctx);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new AppError(`the app module ${path} threw: ${err.message}`, { cause: err });
|
|
42
|
+
}
|
|
43
|
+
if (typeof options !== "object" || options === null || !options.registry || !options.models) {
|
|
44
|
+
throw new AppError(`the app module ${path} returned no Harness options (registry and models)`);
|
|
45
|
+
}
|
|
46
|
+
return options;
|
|
47
|
+
}
|
package/dist/claim.d.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { Delegation } from "disk";
|
|
2
|
+
export type RunRef = {
|
|
3
|
+
disk: string;
|
|
4
|
+
region: string;
|
|
5
|
+
id: string;
|
|
6
|
+
};
|
|
7
|
+
/** The control-plane calls the claim uses; an archil `Disk` (SDK `disk` 1.7.0) satisfies it. Supervisor side only. */
|
|
8
|
+
export interface ControlApi {
|
|
9
|
+
putObject(key: string, body: string, options: {
|
|
10
|
+
uid: number;
|
|
11
|
+
gid: number;
|
|
12
|
+
mode: number;
|
|
13
|
+
}): Promise<unknown>;
|
|
14
|
+
addUser(user: {
|
|
15
|
+
type: "token";
|
|
16
|
+
nickname: string;
|
|
17
|
+
ttl: string;
|
|
18
|
+
oneUse: boolean;
|
|
19
|
+
}): Promise<{
|
|
20
|
+
identifier?: string;
|
|
21
|
+
token?: string;
|
|
22
|
+
}>;
|
|
23
|
+
removeUser(type: "token", identifier: string): Promise<void>;
|
|
24
|
+
listDelegations(): Promise<Delegation[]>;
|
|
25
|
+
revokeDelegation(delegation: Pick<Delegation, "clientId" | "inodeId">): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* `Disk.exec`: a shell command in a container with the disk mounted in shared mode, at the disk root. The claim uses it
|
|
28
|
+
* only to resolve `runs/<id>` to its inode when the control API lists a delegation without a path. Without it, such a
|
|
29
|
+
* delegation cannot be attributed, and `findDelegations` fails rather than read the run as unheld.
|
|
30
|
+
*/
|
|
31
|
+
exec?(command: string): Promise<{
|
|
32
|
+
exitCode: number;
|
|
33
|
+
stdout: string;
|
|
34
|
+
stderr?: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
/** How this host runs the archil client. `archil mount`, `sync`, `delegations` and `unmount` all need root. */
|
|
38
|
+
export interface ArchilHost {
|
|
39
|
+
/**
|
|
40
|
+
* The archil command: `bin/archil-scoped` or a root-owned copy of it (default: this package's). Its `mount` takes the
|
|
41
|
+
* token as one line on stdin; plain `/usr/bin/archil` does not, so a mount through it fails unauthenticated.
|
|
42
|
+
*/
|
|
43
|
+
archil?: string;
|
|
44
|
+
fusermount?: string;
|
|
45
|
+
/** The sudo binary, or false when the process is root. Default: /usr/bin/sudo unless uid 0. */
|
|
46
|
+
sudo?: string | false;
|
|
47
|
+
procMounts?: string;
|
|
48
|
+
/** The process table the claim reads to find a stale archil daemon on the run's mountpoint. Default /proc. */
|
|
49
|
+
proc?: string;
|
|
50
|
+
/** `staleGrace`: how long a daemon left on an unmounted mountpoint may take to exit by itself before it is killed. */
|
|
51
|
+
timeoutMs?: {
|
|
52
|
+
mount?: number;
|
|
53
|
+
sync?: number;
|
|
54
|
+
unmount?: number;
|
|
55
|
+
cli?: number;
|
|
56
|
+
staleGrace?: number;
|
|
57
|
+
};
|
|
58
|
+
/** File access on the mount; replaceable so tests can inject a dead mount (ENOTCONN) or a fence (EIO). */
|
|
59
|
+
fs?: {
|
|
60
|
+
stat?(path: string): Promise<unknown>;
|
|
61
|
+
persist?(path: string, data: string): Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export interface AcquireOptions {
|
|
65
|
+
ref: RunRef;
|
|
66
|
+
/** The mount token. It travels only on the archil wrapper's stdin; never in argv or an environment, never printed or written. */
|
|
67
|
+
token: string;
|
|
68
|
+
/** Default /mnt/archil; the claim mounts at `<mountRoot>/runs/<id>` so paths are identical on every host. */
|
|
69
|
+
mountRoot?: string;
|
|
70
|
+
/** `archil mount --force`: the takeover fallback when the control API cannot revoke. */
|
|
71
|
+
force?: boolean;
|
|
72
|
+
host?: ArchilHost;
|
|
73
|
+
}
|
|
74
|
+
export interface Claim {
|
|
75
|
+
readonly ref: RunRef;
|
|
76
|
+
readonly disk: string;
|
|
77
|
+
readonly root: string;
|
|
78
|
+
readonly work: string;
|
|
79
|
+
readonly store: string;
|
|
80
|
+
/** True when an in-place restart found its own live mount and kept it; no token was used. */
|
|
81
|
+
readonly reused: boolean;
|
|
82
|
+
/** Entries found in the mountpoint before mounting, moved aside whole and never brought into the run; else null. */
|
|
83
|
+
readonly stray?: {
|
|
84
|
+
to: string;
|
|
85
|
+
entries: number;
|
|
86
|
+
} | null;
|
|
87
|
+
readonly forced: boolean;
|
|
88
|
+
readonly timings: {
|
|
89
|
+
mountMs: number;
|
|
90
|
+
verifyMs: number;
|
|
91
|
+
};
|
|
92
|
+
readonly fenced: boolean;
|
|
93
|
+
/** Mark the claim lost (the store's facade calls this on an I/O-class error); later barriers throw at once. */
|
|
94
|
+
markFenced(cause?: unknown): void;
|
|
95
|
+
/**
|
|
96
|
+
* Where the run's environment records the commands it starts, so that the next open of this claim ends what a killed
|
|
97
|
+
* incarnation left running (`reapCommands`, before the Harness opens). Set by a claim that nothing else cleans up after,
|
|
98
|
+
* as a local run's; a mount's host ends them with its unit's cgroup or its container.
|
|
99
|
+
*/
|
|
100
|
+
readonly commands?: string;
|
|
101
|
+
/** `archil sync`: every pending write on the mount is durable when it resolves. A failure fences the claim. */
|
|
102
|
+
barrier(): Promise<{
|
|
103
|
+
ms: number;
|
|
104
|
+
}>;
|
|
105
|
+
/**
|
|
106
|
+
* Barrier, then `archil unmount` (flush and check the delegation in); a dead mount is cleaned with `fusermount -u`.
|
|
107
|
+
* Where the kernel refuses every unmount of a FUSE mount with ENOENT (Sysbox, Daytona's sandboxes): `archil checkin`,
|
|
108
|
+
* then the mount is moved aside and its daemon killed (`moved`).
|
|
109
|
+
*/
|
|
110
|
+
release(): Promise<{
|
|
111
|
+
via: Unmounted;
|
|
112
|
+
}>;
|
|
113
|
+
}
|
|
114
|
+
export declare const DEFAULT_MOUNT_ROOT = "/mnt/archil";
|
|
115
|
+
/** This package's `bin/archil-scoped`. A deployment installs a root-owned copy and passes its path as `archil`. */
|
|
116
|
+
export declare const ARCHIL_SCOPED: string;
|
|
117
|
+
/** The file the claim writes and fsyncs to prove it holds the delegation; written on every acquire. */
|
|
118
|
+
export declare const CLAIM_PROBE = ".claim";
|
|
119
|
+
/** How a mount left its path: unmounted by archil, by fusermount, moved aside (Sysbox), or there was none. */
|
|
120
|
+
export type Unmounted = "archil" | "fusermount" | "moved" | "none";
|
|
121
|
+
export declare function runPath(id: string): string;
|
|
122
|
+
/** PutObject of the `/`-terminated key; uid/gid are required or the directory is root:root. 409: held. */
|
|
123
|
+
export declare function createRunDir(control: ControlApi, id: string, owner: {
|
|
124
|
+
uid: number;
|
|
125
|
+
gid: number;
|
|
126
|
+
mode?: number;
|
|
127
|
+
}): Promise<void>;
|
|
128
|
+
/** A mount token's lifetime: one day, a Go duration (the control plane rejects "1d"). */
|
|
129
|
+
export declare const MOUNT_TOKEN_TTL = "24h";
|
|
130
|
+
export declare const TOKEN_PREFIX = "pda-";
|
|
131
|
+
/** `<prefix><run id>-g<attempt>-<base36 ms>`: the token user names the run and start attempt it was minted for. */
|
|
132
|
+
export declare function tokenNickname(runId: string, attempt: number, at?: number, prefix?: string): string;
|
|
133
|
+
/** The run, attempt and minting time a nickname from `tokenNickname` names; null for anything else. */
|
|
134
|
+
export declare function parseTokenNickname(nickname: string, prefix?: string): {
|
|
135
|
+
runId: string;
|
|
136
|
+
attempt: number;
|
|
137
|
+
at: number;
|
|
138
|
+
} | null;
|
|
139
|
+
/**
|
|
140
|
+
* A reusable mount token with a one-day TTL by default, minted per start attempt. Not single-use: the client
|
|
141
|
+
* re-authenticates at its background endpoint refresh, 5 min after the mount and every 5 min after, and the control plane
|
|
142
|
+
* rejects a spent single-use token for good, after which every write on the mount fails (client 0.8.42). A token
|
|
143
|
+
* whose TTL passes fences its mount the same way at the next refresh, so an instance never outlives its token by more
|
|
144
|
+
* than one refresh. The nickname is given, or built from `run` so the user can be traced back to its run.
|
|
145
|
+
*/
|
|
146
|
+
export declare function mintMountToken(control: ControlApi, opts: {
|
|
147
|
+
nickname?: string;
|
|
148
|
+
run?: {
|
|
149
|
+
id: string;
|
|
150
|
+
attempt: number;
|
|
151
|
+
};
|
|
152
|
+
prefix?: string;
|
|
153
|
+
ttl?: string;
|
|
154
|
+
oneUse?: boolean;
|
|
155
|
+
now?: () => number;
|
|
156
|
+
}): Promise<{
|
|
157
|
+
token: string;
|
|
158
|
+
identifier: string;
|
|
159
|
+
nickname: string;
|
|
160
|
+
}>;
|
|
161
|
+
export declare function removeMountToken(control: ControlApi, identifier: string): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* Delegations on `runs/<id>` and anything under it; never a sibling or a parent. The control API resolves a delegation's
|
|
164
|
+
* path best-effort: it can list one with no path, live or orphaned, on a never-reused run. So when nothing matches by
|
|
165
|
+
* path and some entry has none, the pathless entries are attributed by inode (`pathlessResolver`): those on the inode
|
|
166
|
+
* `runs/<id>` names now are the run's. A pathless entry that cannot be attributed fails CONTROL_API_FAILED: an unseen
|
|
167
|
+
* holder must never read as none, or a caller starts over it.
|
|
168
|
+
*/
|
|
169
|
+
export declare function findDelegations(control: Pick<ControlApi, "listDelegations" | "exec">, id: string, resolve?: PathlessResolver): Promise<Delegation[]>;
|
|
170
|
+
/** `findDelegations` over a listing the caller already holds; share `resolve` across the runs of one listing or pass. */
|
|
171
|
+
export declare function matchDelegations(all: readonly Delegation[], id: string, resolve: PathlessResolver): Promise<Delegation[]>;
|
|
172
|
+
/** The run id whose directory each inode is, or null when it is no run directory (deleted, or not under `runs/`). */
|
|
173
|
+
export type PathlessResolver = (pathless: readonly Delegation[]) => Promise<ReadonlyMap<number, string | null>>;
|
|
174
|
+
/**
|
|
175
|
+
* Attributes pathless delegations by inode: one `exec` maps every inode not seen before to the run directory that has
|
|
176
|
+
* it (`find runs -inum`, whose inode numbers are the control API's inode ids). Share one across a listing or a supervisor
|
|
177
|
+
* pass: it costs one exec per batch of new inodes, never one per run. The output names only the inodes asked for, so it
|
|
178
|
+
* stays small however many runs the disk holds. A failed exec fails this call and every later one, with no new exec.
|
|
179
|
+
* Inodes on no run directory are reported once on stderr: they block nothing, and only a revoke by client and inode
|
|
180
|
+
* removes them.
|
|
181
|
+
*/
|
|
182
|
+
export declare function pathlessResolver(control: Pick<ControlApi, "exec">): PathlessResolver;
|
|
183
|
+
/**
|
|
184
|
+
* The delegations `held`'s clients also hold on their own private directories (`privateDirs`: inode to the client whose
|
|
185
|
+
* private directory it is). A killed client leaves them orphaned, and nothing ties them to a run: they block no run, but
|
|
186
|
+
* they pile up and resurface in listings. A client that holds anything else beyond `held` may hold another run, so none
|
|
187
|
+
* of its delegations is a companion.
|
|
188
|
+
*/
|
|
189
|
+
export declare function companionsOf(all: readonly Delegation[], held: readonly Delegation[], privateDirs: ReadonlyMap<number, string>): Delegation[];
|
|
190
|
+
/**
|
|
191
|
+
* Revoke the companions of `held` (`companionsOf`): only for clients the caller is already revoking because it fences or
|
|
192
|
+
* deletes a run, never as a sweep. From `all` or a fresh listing. Best effort, and hygiene only: a companion blocks no
|
|
193
|
+
* run, so one left behind (or a failed listing or `exec`) costs a listing entry. Returns the ones revoked.
|
|
194
|
+
*/
|
|
195
|
+
export declare function revokeCompanions(control: Pick<ControlApi, "listDelegations" | "revokeDelegation" | "exec">, held: readonly Delegation[], all?: readonly Delegation[]): Promise<Delegation[]>;
|
|
196
|
+
/**
|
|
197
|
+
* Revoke every delegation on the run through the control API, then its holders' private directories
|
|
198
|
+
* (`revokeCompanions`). The old holder's next fsync returns EIO. Returns the run's own delegations.
|
|
199
|
+
*/
|
|
200
|
+
export declare function revoke(control: ControlApi, id: string): Promise<Delegation[]>;
|
|
201
|
+
/**
|
|
202
|
+
* Revoke what `revoke` revokes, skipping every revoke that fails: for cleanup, where deleting the tree next checks what
|
|
203
|
+
* is left. Only the listing (and a pathless resolution) can fail it. Returns the run's own delegations.
|
|
204
|
+
*/
|
|
205
|
+
export declare function revokeBestEffort(control: Pick<ControlApi, "listDelegations" | "revokeDelegation" | "exec">, id: string): Promise<Delegation[]>;
|
|
206
|
+
/** Revoke through the control API, then mount plainly; if the API path fails, mount with `--force`. */
|
|
207
|
+
export declare function takeOver(control: ControlApi, opts: AcquireOptions): Promise<Claim>;
|
|
208
|
+
/**
|
|
209
|
+
* Unmount whatever the run has at `mountpoint`: `archil unmount` flushes and checks the delegation in; a dead mount
|
|
210
|
+
* (which `archil unmount` refuses) is cleaned with `fusermount -u`; a live mount refused with ENOENT (Sysbox) is checked
|
|
211
|
+
* in and moved aside. Any other live mount that refuses is never forced.
|
|
212
|
+
*/
|
|
213
|
+
export declare function unmountClaim(mountpoint: string, host?: ArchilHost): Promise<Unmounted>;
|
|
214
|
+
/**
|
|
215
|
+
* Take the run's claim: mount `<disk>:/runs/<id>` exclusively at `<mountRoot>/runs/<id>` and verify it. A live mount of
|
|
216
|
+
* the same run already there is kept (in-place restart); a dead one is cleaned first. With no mount there, a stale archil
|
|
217
|
+
* daemon on the mountpoint is killed (`stale`) and entries in it are moved aside to `<mountRoot>/.stray` (`stray`) before
|
|
218
|
+
* the mount. Throws HeldError (exit 76) only when another client holds the delegation, FencedError (exit 75) when the
|
|
219
|
+
* verifying write is refused, MOUNTPOINT_NOT_EMPTY when the mountpoint cannot be emptied or archil still finds it not
|
|
220
|
+
* empty, MOUNT_FAILED (exit 1) for a daemon of this host that is still running there.
|
|
221
|
+
*/
|
|
222
|
+
export declare function acquire(opts: AcquireOptions): Promise<Claim>;
|