@agentshouse/kit 0.1.0-alpha.38 → 0.1.0-alpha.39
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/README.md +2 -2
- package/bin/connect-linux.sh +1 -0
- package/bin/kit.js +18 -2
- package/bootstrap.json +5 -5
- package/docs/linux.md +6 -0
- package/package.json +1 -1
- package/src/configuration.js +1 -2
- package/src/contract.js +6 -2
- package/src/workspace.js +194 -48
package/README.md
CHANGED
|
@@ -13,13 +13,13 @@ The Skill set Kit installs is Matt Pocock's
|
|
|
13
13
|
Linux (`amd64`, `arm64`) or macOS (Apple silicon, Intel):
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
16
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh)
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Windows 11 (`amd64`), in PowerShell:
|
|
20
20
|
|
|
21
21
|
```powershell
|
|
22
|
-
& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
22
|
+
& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1)))
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The command sets up Docker if it is missing, creates `~/.house-kit` (Kit home)
|
package/bin/connect-linux.sh
CHANGED
package/bin/kit.js
CHANGED
|
@@ -13,7 +13,7 @@ import { runResident, runWorkers } from "../src/worker.js";
|
|
|
13
13
|
import { openSession, releaseControl, requestDrain } from "../src/house.js";
|
|
14
14
|
import { reportHost } from "../src/host.js";
|
|
15
15
|
import { signInAgent, updateAgents } from "../src/agents.js";
|
|
16
|
-
import { serveWorkspaces } from "../src/workspace.js";
|
|
16
|
+
import { serveWorkspaces, updateWorkspaces } from "../src/workspace.js";
|
|
17
17
|
import { captureRepository } from "../src/repository.js";
|
|
18
18
|
import { bootstrap, pull, rebootstrap, submit } from "../src/sync.js";
|
|
19
19
|
import { formatMount, formatUnmount, mountPackage, unmountSubtree } from "../src/mount.js";
|
|
@@ -35,7 +35,7 @@ const LOGIN_USAGE =
|
|
|
35
35
|
const HOST_USAGE = "usage: kit host report [--config <path>]";
|
|
36
36
|
const AGENTS_USAGE =
|
|
37
37
|
"usage: kit agents update [--config <path>] | kit agents sign-in <kind> [--config <path>]";
|
|
38
|
-
const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>]";
|
|
38
|
+
const WORKSPACE_USAGE = "usage: kit workspace host [--config <path>] [--passes <count>] | kit workspace update";
|
|
39
39
|
const SOURCE_USAGE =
|
|
40
40
|
"usage: kit source <capture|status> <account|instance> [service] [--config <path>]\n" +
|
|
41
41
|
" kit source <reset|retry-files> <instance> [service] [--config <path>]\n" +
|
|
@@ -895,6 +895,21 @@ async function runWorkspaceHost(args, directory) {
|
|
|
895
895
|
}
|
|
896
896
|
}
|
|
897
897
|
|
|
898
|
+
async function runWorkspaceUpdate(args, directory) {
|
|
899
|
+
declared(args, [], 0, { exact: true });
|
|
900
|
+
const answered = await updateWorkspaces({
|
|
901
|
+
house: await storedHouse(directory),
|
|
902
|
+
configuration: join(directory, "kit.json"),
|
|
903
|
+
});
|
|
904
|
+
for (const { workspace, running } of answered.updated) {
|
|
905
|
+
write(running ? `Updated workspace ${workspace}.` : `Updated workspace ${workspace}; it stays stopped.`);
|
|
906
|
+
}
|
|
907
|
+
for (const { workspace, cause } of answered.unchanged) {
|
|
908
|
+
write(`Workspace ${workspace} is unchanged: ${cause}`);
|
|
909
|
+
}
|
|
910
|
+
if (answered.unchanged.length > 0) process.exitCode = 1;
|
|
911
|
+
}
|
|
912
|
+
|
|
898
913
|
async function runTelegramConnect(args, directory) {
|
|
899
914
|
declared(args, [], 0, { exact: true });
|
|
900
915
|
const house = await storedHouse(directory);
|
|
@@ -1027,6 +1042,7 @@ async function main() {
|
|
|
1027
1042
|
if (command === "workspace") {
|
|
1028
1043
|
const [action, ...rest] = args;
|
|
1029
1044
|
if (action === "host") return runWorkspaceHost(rest, directory);
|
|
1045
|
+
if (action === "update") return runWorkspaceUpdate(rest, directory);
|
|
1030
1046
|
throw new Error(WORKSPACE_USAGE);
|
|
1031
1047
|
}
|
|
1032
1048
|
if (command === "telegram") {
|
package/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.39",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"windows": {
|
|
17
|
-
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
17
|
+
"command": "& ([scriptblock]::Create((irm https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1)))",
|
|
18
18
|
"asset": {
|
|
19
19
|
"name": "connect-windows.ps1",
|
|
20
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
20
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.39/connect-windows.ps1"
|
|
21
21
|
},
|
|
22
22
|
"workspace": {
|
|
23
23
|
"argument": "--workspace",
|
package/docs/linux.md
CHANGED
|
@@ -69,3 +69,9 @@ The server connects as a continuous Environment. Put `--linux` before `kit` or
|
|
|
69
69
|
`house` to run a Kit command, as in
|
|
70
70
|
`bash <(curl -fsSL …/connect-linux.sh) --linux kit apply`, and run a newer
|
|
71
71
|
release's command with `--linux` to update it, as above.
|
|
72
|
+
|
|
73
|
+
The same run updates the container Environments the server hosts to that
|
|
74
|
+
release. A running one waits, and is named, until its own Agent sessions end;
|
|
75
|
+
run the command again then. A stopped one is updated and stays stopped. Each
|
|
76
|
+
keeps its files, sign-ins and CPU, memory and disk limits; only what was
|
|
77
|
+
installed outside its workspace and Kit home is not kept.
|
package/package.json
CHANGED
package/src/configuration.js
CHANGED
|
@@ -160,12 +160,11 @@ function maximaOf(value, declaration) {
|
|
|
160
160
|
|
|
161
161
|
function hostOf(value, declaration) {
|
|
162
162
|
if (value === null) return null;
|
|
163
|
-
const host = closed(value, ["workspace", "maxima"
|
|
163
|
+
const host = closed(value, ["workspace", "maxima"], declaration);
|
|
164
164
|
if (host.maxima === undefined) refuse(`${declaration}.maxima`);
|
|
165
165
|
return {
|
|
166
166
|
workspace: absolutePath(host.workspace, `${declaration}.workspace`),
|
|
167
167
|
maxima: maximaOf(host.maxima, `${declaration}.maxima`),
|
|
168
|
-
image: host.image === undefined || host.image === null ? null : text(host.image, `${declaration}.image`, 255),
|
|
169
168
|
};
|
|
170
169
|
}
|
|
171
170
|
|
package/src/contract.js
CHANGED
|
@@ -276,16 +276,20 @@ export const KIT_WORKSPACES_ACQUIRE_PATH = "/kit/workspaces/acquire";
|
|
|
276
276
|
export const WORKSPACE_ACQUIRE_MAX = 10;
|
|
277
277
|
export const WORKSPACE_POLL_MS = 2_000;
|
|
278
278
|
export const WORKSPACE_LABEL = "house-kit.workspace";
|
|
279
|
-
export const
|
|
280
|
-
export const WORKSPACE_KIT_HOME = "/workspace/.house-kit";
|
|
279
|
+
export const WORKSPACE_KIT_HOME = "/kit-home";
|
|
281
280
|
export const WORKSPACE_DISK_IMAGE = "disk.img";
|
|
282
281
|
export const WORKSPACE_DISK_MOUNT = "mnt";
|
|
282
|
+
export const WORKSPACE_DISK_KIT_HOME = "kit-home";
|
|
283
|
+
export const WORKSPACE_DISK_HOUSE = "house";
|
|
284
|
+
export const WORKSPACE_SELECTED_IMAGE = "/opt/house-kit/image";
|
|
285
|
+
export const WORKSPACE_RELEASE_IMAGE = /^ghcr\.io\/agentshouse\/kit@sha256:[0-9a-f]{64}$/;
|
|
283
286
|
export const WORKSPACE_MOUNT_OPTIONS = "loop,nosuid,nodev";
|
|
284
287
|
export const HOST_MOUNT_INFO = "/proc/self/mountinfo";
|
|
285
288
|
export const WORKSPACE_SYSTEM_RESERVE_BYTES = 512 * 1024 * 1024;
|
|
286
289
|
export const WORKSPACE_COMMAND_TIMEOUT_MS = 120_000;
|
|
287
290
|
export const WORKSPACE_READY_ATTEMPTS = 60;
|
|
288
291
|
export const WORKSPACE_READY_POLL_MS = 1_000;
|
|
292
|
+
export const WORKSPACE_LOCK_POLL_MS = 1_000;
|
|
289
293
|
export const WORKSPACE_STOP_TIMEOUT_SECONDS = 10;
|
|
290
294
|
export const WORKSPACE_OPERATION_WITHDRAWN = "hosted_environment_operation_not_found";
|
|
291
295
|
export const CGROUP_CONTROLLERS = "/sys/fs/cgroup/cgroup.controllers";
|
package/src/workspace.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { access, mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { createServer } from "node:net";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { readConfiguration } from "./configuration.js";
|
|
4
5
|
import {
|
|
5
6
|
CGROUP_CONTROLLERS,
|
|
6
7
|
HOST_MOUNT_INFO,
|
|
8
|
+
LAUNCH_DIRECTORY_BASE,
|
|
7
9
|
LOOP_CONTROL,
|
|
8
10
|
WORKSPACE_ACQUIRE_MAX,
|
|
9
11
|
WORKSPACE_COMMAND_TIMEOUT_MS,
|
|
12
|
+
WORKSPACE_DISK_HOUSE,
|
|
10
13
|
WORKSPACE_DISK_IMAGE,
|
|
14
|
+
WORKSPACE_DISK_KIT_HOME,
|
|
11
15
|
WORKSPACE_DISK_MOUNT,
|
|
12
16
|
WORKSPACE_KIT_HOME,
|
|
13
17
|
WORKSPACE_MOUNT_OPTIONS,
|
|
14
18
|
WORKSPACE_LABEL,
|
|
15
|
-
|
|
19
|
+
WORKSPACE_LOCK_POLL_MS,
|
|
16
20
|
WORKSPACE_OPERATION_WITHDRAWN,
|
|
17
21
|
WORKSPACE_POLL_MS,
|
|
18
22
|
WORKSPACE_READY_ATTEMPTS,
|
|
19
23
|
WORKSPACE_READY_POLL_MS,
|
|
24
|
+
WORKSPACE_RELEASE_IMAGE,
|
|
25
|
+
WORKSPACE_SELECTED_IMAGE,
|
|
20
26
|
WORKSPACE_STOP_TIMEOUT_SECONDS,
|
|
21
27
|
WORKSPACE_SYSTEM_RESERVE_BYTES,
|
|
22
28
|
} from "./contract.js";
|
|
@@ -27,10 +33,13 @@ import {
|
|
|
27
33
|
openSession,
|
|
28
34
|
releaseControl,
|
|
29
35
|
reportWorkspaceOutcome,
|
|
36
|
+
updateDrain,
|
|
30
37
|
} from "./house.js";
|
|
31
38
|
import { houseFailed, KitRefusal } from "./refusal.js";
|
|
32
39
|
|
|
33
40
|
const REQUIRED_CONTROLLERS = ["cpu", "memory"];
|
|
41
|
+
const PROVISIONED_DISK = "/disk";
|
|
42
|
+
const STOPPED_STATES = ["created", "exited"];
|
|
34
43
|
const MEMINFO = "/proc/meminfo";
|
|
35
44
|
const MEM_AVAILABLE = /^MemAvailable:\s+(\d+)\s+kB$/m;
|
|
36
45
|
|
|
@@ -106,30 +115,35 @@ export function mountedPaths(document) {
|
|
|
106
115
|
);
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
export function workspaceCreateArguments(identity,
|
|
118
|
+
export function workspaceCreateArguments(identity, workspace, image, house, root) {
|
|
119
|
+
const disk = workspaceMountPath(root, identity);
|
|
110
120
|
return [
|
|
111
121
|
"create",
|
|
112
122
|
"--name",
|
|
113
123
|
identity,
|
|
114
124
|
"--label",
|
|
115
|
-
`${WORKSPACE_LABEL}=${
|
|
125
|
+
`${WORKSPACE_LABEL}=${workspace.environment}`,
|
|
116
126
|
"--cpus",
|
|
117
|
-
(
|
|
127
|
+
(workspace.maxima.cpu_millicores / 1000).toFixed(3),
|
|
118
128
|
"--memory",
|
|
119
|
-
`${
|
|
129
|
+
`${workspace.maxima.memory_bytes}b`,
|
|
120
130
|
"--memory-swap",
|
|
121
|
-
`${
|
|
131
|
+
`${workspace.maxima.memory_bytes}b`,
|
|
122
132
|
"--restart",
|
|
123
133
|
"no",
|
|
134
|
+
...(new URL(house).protocol === "https:" ? [] : ["--network", "host"]),
|
|
124
135
|
"--mount",
|
|
125
|
-
`type=bind,source=${
|
|
136
|
+
`type=bind,source=${disk}/${WORKSPACE_DISK_KIT_HOME},target=${WORKSPACE_KIT_HOME}`,
|
|
137
|
+
"--mount",
|
|
138
|
+
`type=bind,source=${disk}/${WORKSPACE_DISK_HOUSE},target=${LAUNCH_DIRECTORY_BASE}`,
|
|
139
|
+
"--env",
|
|
140
|
+
`HOME=${WORKSPACE_KIT_HOME}`,
|
|
126
141
|
"--env",
|
|
127
142
|
`HOUSE_KIT_HOME=${WORKSPACE_KIT_HOME}`,
|
|
128
143
|
"--env",
|
|
129
|
-
|
|
130
|
-
"--workdir",
|
|
131
|
-
WORKSPACE_MOUNT,
|
|
144
|
+
"HOUSE_KIT_CONTAINER=1",
|
|
132
145
|
image,
|
|
146
|
+
"resident",
|
|
133
147
|
];
|
|
134
148
|
}
|
|
135
149
|
|
|
@@ -198,7 +212,7 @@ export async function workspacePrerequisites(host, image) {
|
|
|
198
212
|
if (formatter.status !== 0) {
|
|
199
213
|
return {
|
|
200
214
|
supported: false,
|
|
201
|
-
refusal: `the
|
|
215
|
+
refusal: `the selected Kit image ${image} carries no mkfs.ext4, so no volume ceiling can be created`,
|
|
202
216
|
};
|
|
203
217
|
}
|
|
204
218
|
return { supported: true, runtime: `docker ${version.stdout.trim()}` };
|
|
@@ -215,34 +229,45 @@ function bootstrapConfiguration() {
|
|
|
215
229
|
});
|
|
216
230
|
}
|
|
217
231
|
|
|
218
|
-
function
|
|
232
|
+
function diskArguments(root, identity, image, script) {
|
|
219
233
|
return [
|
|
220
234
|
"run",
|
|
221
235
|
"--rm",
|
|
222
236
|
"--interactive",
|
|
223
237
|
"--mount",
|
|
224
|
-
`type=bind,source=${workspaceMountPath(root, identity)},target=${
|
|
238
|
+
`type=bind,source=${workspaceMountPath(root, identity)},target=${PROVISIONED_DISK}`,
|
|
225
239
|
"--entrypoint",
|
|
226
240
|
"sh",
|
|
227
241
|
image,
|
|
228
242
|
"-c",
|
|
229
|
-
|
|
243
|
+
script,
|
|
230
244
|
];
|
|
231
245
|
}
|
|
232
246
|
|
|
233
|
-
async function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
247
|
+
async function onDisk(host, settings, identity, script, stdin = "") {
|
|
248
|
+
const written = await docker(host, diskArguments(settings.root, identity, settings.image, script), stdin);
|
|
249
|
+
if (written.status !== 0) {
|
|
250
|
+
throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function provisionWorkspaceState(host, settings, identity, operation) {
|
|
255
|
+
const home = `${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`;
|
|
256
|
+
const house = `${PROVISIONED_DISK}/${WORKSPACE_DISK_HOUSE}`;
|
|
257
|
+
const files = [
|
|
258
|
+
...(operation.authority === undefined ? [] : [["credential", operation.authority.credential]]),
|
|
259
|
+
["kit.json", bootstrapConfiguration()],
|
|
260
|
+
["house", settings.house],
|
|
261
|
+
["environment", operation.environment],
|
|
262
|
+
];
|
|
263
|
+
for (const [name, content] of files) {
|
|
264
|
+
await onDisk(
|
|
239
265
|
host,
|
|
240
|
-
|
|
241
|
-
|
|
266
|
+
settings,
|
|
267
|
+
identity,
|
|
268
|
+
`set -e; mkdir -p ${home} ${house}; chmod 700 ${home} ${house}; cat > ${home}/${name}; chmod 600 ${home}/${name}`,
|
|
269
|
+
`${content}\n`,
|
|
242
270
|
);
|
|
243
|
-
if (written.status !== 0) {
|
|
244
|
-
throw new KitRefusal("kit_workspace_unprovisioned", written.stderr.trim() || identity);
|
|
245
|
-
}
|
|
246
271
|
}
|
|
247
272
|
}
|
|
248
273
|
|
|
@@ -289,9 +314,7 @@ export async function createWorkspace(host, settings, operation) {
|
|
|
289
314
|
}
|
|
290
315
|
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
291
316
|
if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
|
|
292
|
-
|
|
293
|
-
await provisionWorkspaceState(host, settings.root, identity, settings.image, operation.authority);
|
|
294
|
-
}
|
|
317
|
+
await provisionWorkspaceState(host, settings, identity, operation);
|
|
295
318
|
const created = await docker(
|
|
296
319
|
host,
|
|
297
320
|
workspaceCreateArguments(identity, operation, settings.image, settings.house, settings.root),
|
|
@@ -312,9 +335,42 @@ async function workspacePresent(host, identity) {
|
|
|
312
335
|
return inspected.status === 0 ? inspected.stdout.trim() : null;
|
|
313
336
|
}
|
|
314
337
|
|
|
338
|
+
async function heldWorkspace(host, identity) {
|
|
339
|
+
const inspected = await docker(host, [
|
|
340
|
+
"container",
|
|
341
|
+
"inspect",
|
|
342
|
+
"--format",
|
|
343
|
+
`{{.State.Status}} {{.Config.Image}} {{index .Config.Labels "${WORKSPACE_LABEL}"}} {{.HostConfig.NanoCpus}} {{.HostConfig.Memory}}`,
|
|
344
|
+
identity,
|
|
345
|
+
]);
|
|
346
|
+
if (inspected.status !== 0) return null;
|
|
347
|
+
const [state, image, environment, nanoCpus, memory] = inspected.stdout.trim().split(/\s+/);
|
|
348
|
+
return {
|
|
349
|
+
state,
|
|
350
|
+
image,
|
|
351
|
+
environment,
|
|
352
|
+
maxima: { cpu_millicores: Number(nanoCpus) / 1_000_000, memory_bytes: Number(memory) },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function replaceWorkspace(host, settings, identity, held) {
|
|
357
|
+
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
358
|
+
if (mountRefusal !== null) return mountRefusal;
|
|
359
|
+
await onDisk(host, settings, identity, `rm -f ${updateDrain(`${PROVISIONED_DISK}/${WORKSPACE_DISK_KIT_HOME}`)}`);
|
|
360
|
+
const removed = await docker(host, ["rm", "--force", identity]);
|
|
361
|
+
if (removed.status !== 0) return `the workspace was not replaced: ${removed.stderr.trim()}`;
|
|
362
|
+
const created = await docker(
|
|
363
|
+
host,
|
|
364
|
+
workspaceCreateArguments(identity, held, settings.image, settings.house, settings.root),
|
|
365
|
+
);
|
|
366
|
+
if (created.status !== 0) return `the workspace was not recreated: ${created.stderr.trim()}`;
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
315
370
|
export async function startWorkspace(host, settings, operation) {
|
|
316
371
|
const identity = operation.workspace ?? workspaceIdentity(operation);
|
|
317
|
-
|
|
372
|
+
const held = await heldWorkspace(host, identity);
|
|
373
|
+
if (held === null) return { outcome: "absent" };
|
|
318
374
|
const available = memoryAvailable(await host.read(MEMINFO));
|
|
319
375
|
if (available - operation.maxima.memory_bytes < WORKSPACE_SYSTEM_RESERVE_BYTES) {
|
|
320
376
|
return {
|
|
@@ -324,6 +380,10 @@ export async function startWorkspace(host, settings, operation) {
|
|
|
324
380
|
}
|
|
325
381
|
const mountRefusal = await mountWorkspace(host, settings, identity);
|
|
326
382
|
if (mountRefusal !== null) return { outcome: "unsupported", refusal: mountRefusal };
|
|
383
|
+
if (STOPPED_STATES.includes(held.state) && held.image !== settings.image) {
|
|
384
|
+
const refusal = await replaceWorkspace(host, settings, identity, held);
|
|
385
|
+
if (refusal !== null) return { outcome: "failed", refusal };
|
|
386
|
+
}
|
|
327
387
|
const started = await docker(host, ["start", identity]);
|
|
328
388
|
if (started.status !== 0) {
|
|
329
389
|
return { outcome: "failed", refusal: `the workspace did not start: ${started.stderr.trim()}` };
|
|
@@ -425,37 +485,123 @@ export async function reconcileWorkspaces(host, authorized) {
|
|
|
425
485
|
);
|
|
426
486
|
const stopped = [];
|
|
427
487
|
for (const managed of await managedWorkspaces(host)) {
|
|
428
|
-
if (managed.state !== "running") continue;
|
|
429
488
|
if (expected.get(managed.environment) === "running") continue;
|
|
430
|
-
const settled = await
|
|
431
|
-
"
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
489
|
+
const settled = await exclusively(host, managed.name, async () => {
|
|
490
|
+
if ((await workspacePresent(host, managed.name)) !== "running") return false;
|
|
491
|
+
const stopping = await docker(host, [
|
|
492
|
+
"stop",
|
|
493
|
+
"--timeout",
|
|
494
|
+
String(WORKSPACE_STOP_TIMEOUT_SECONDS),
|
|
495
|
+
managed.name,
|
|
496
|
+
]);
|
|
497
|
+
return stopping.status === 0;
|
|
498
|
+
});
|
|
499
|
+
if (settled) stopped.push(managed.name);
|
|
437
500
|
}
|
|
438
501
|
return stopped;
|
|
439
502
|
}
|
|
440
503
|
|
|
504
|
+
function workspaceLock(identity) {
|
|
505
|
+
return new Promise((resolve, reject) => {
|
|
506
|
+
const lock = createServer();
|
|
507
|
+
lock.once("error", reject);
|
|
508
|
+
lock.listen({ path: `\0${WORKSPACE_LABEL}/${identity}` }, () => resolve(lock));
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function exclusively(host, identity, act) {
|
|
513
|
+
let lock = null;
|
|
514
|
+
while (lock === null) {
|
|
515
|
+
try {
|
|
516
|
+
lock = await workspaceLock(identity);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (error.code !== "EADDRINUSE") throw error;
|
|
519
|
+
await host.wait(WORKSPACE_LOCK_POLL_MS);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
return await act();
|
|
524
|
+
} finally {
|
|
525
|
+
await new Promise((resolve) => lock.close(resolve));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
441
529
|
export async function performOperation(host, settings, operation) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
530
|
+
const selected = operation.kind === "stop" ? settings : { ...settings, image: await selectedImage(host) };
|
|
531
|
+
return exclusively(host, operation.workspace ?? workspaceIdentity(operation), () => {
|
|
532
|
+
if (operation.kind === "stop") return stopWorkspace(host, operation);
|
|
533
|
+
if (operation.kind === "create") return createWorkspace(host, selected, operation);
|
|
534
|
+
if (operation.kind === "start") return startWorkspace(host, selected, operation);
|
|
535
|
+
return deleteWorkspace(host, selected, operation);
|
|
536
|
+
});
|
|
446
537
|
}
|
|
447
538
|
|
|
448
|
-
|
|
539
|
+
async function selectedImage(host) {
|
|
540
|
+
let image;
|
|
541
|
+
try {
|
|
542
|
+
image = (await host.read(WORKSPACE_SELECTED_IMAGE)).trim();
|
|
543
|
+
} catch {
|
|
544
|
+
image = null;
|
|
545
|
+
}
|
|
546
|
+
if (image === null || !WORKSPACE_RELEASE_IMAGE.test(image)) {
|
|
547
|
+
throw new KitRefusal("kit_workspace_host_unconfigured", WORKSPACE_SELECTED_IMAGE);
|
|
548
|
+
}
|
|
549
|
+
return image;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function hostSettings(options) {
|
|
449
553
|
const configuration = await readConfiguration(options.configuration);
|
|
450
|
-
if (configuration.host === null
|
|
554
|
+
if (configuration.host === null) {
|
|
451
555
|
throw new KitRefusal("kit_workspace_host_unconfigured", options.configuration);
|
|
452
556
|
}
|
|
557
|
+
return { root: configuration.host.workspace, house: options.house };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function updateWorkspace(host, settings, name) {
|
|
561
|
+
try {
|
|
562
|
+
const held = await heldWorkspace(host, name);
|
|
563
|
+
if (held === null || held.image === settings.image) return null;
|
|
564
|
+
const running = !STOPPED_STATES.includes(held.state);
|
|
565
|
+
if (running) {
|
|
566
|
+
const drained = await docker(host, ["exec", name, "kit", "resident", "drain"]);
|
|
567
|
+
if (drained.status !== 0) {
|
|
568
|
+
return { workspace: name, cause: drained.stdout.trim() || drained.stderr.trim() };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
const refusal = await replaceWorkspace(host, settings, name, held);
|
|
572
|
+
if (refusal !== null) return { workspace: name, cause: refusal };
|
|
573
|
+
if (running) {
|
|
574
|
+
const started = await docker(host, ["start", name]);
|
|
575
|
+
if (started.status !== 0) {
|
|
576
|
+
return { workspace: name, cause: `the workspace did not start: ${started.stderr.trim()}` };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { workspace: name, running };
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (!(error instanceof KitRefusal)) throw error;
|
|
582
|
+
return { workspace: name, cause: error.message };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export async function updateWorkspaces(options, dependencies = {}) {
|
|
453
587
|
const host = adapter(dependencies);
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
};
|
|
588
|
+
const updated = [];
|
|
589
|
+
const unchanged = [];
|
|
590
|
+
const managed = await managedWorkspaces(host);
|
|
591
|
+
if (managed.length === 0) return { updated, unchanged };
|
|
592
|
+
const settings = { ...(await hostSettings(options)), image: await selectedImage(host) };
|
|
593
|
+
for (const { name } of managed) {
|
|
594
|
+
const answer = await exclusively(host, name, () => updateWorkspace(host, settings, name));
|
|
595
|
+
if (answer === null) continue;
|
|
596
|
+
if (answer.cause === undefined) updated.push(answer);
|
|
597
|
+
else unchanged.push(answer);
|
|
598
|
+
}
|
|
599
|
+
return { updated, unchanged };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export async function serveWorkspaces(options, dependencies = {}) {
|
|
603
|
+
const host = adapter(dependencies);
|
|
604
|
+
const settings = await hostSettings(options);
|
|
459
605
|
const session = await openSession(options, dependencies);
|
|
460
606
|
const settled = [];
|
|
461
607
|
const withdrawn = [];
|