@felan-ai/daemon 0.0.0-stage → 0.1.0
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 +21 -0
- package/NOTICE +69 -0
- package/README.md +61 -2
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -0
- package/dist/daemon/admission.d.ts +39 -0
- package/dist/daemon/admission.d.ts.map +1 -0
- package/dist/daemon/admission.js +175 -0
- package/dist/daemon/admission.js.map +1 -0
- package/dist/daemon/agent-logs.d.ts +11 -0
- package/dist/daemon/agent-logs.d.ts.map +1 -0
- package/dist/daemon/agent-logs.js +31 -0
- package/dist/daemon/agent-logs.js.map +1 -0
- package/dist/daemon/bootstrap.d.ts +7 -0
- package/dist/daemon/bootstrap.d.ts.map +1 -0
- package/dist/daemon/bootstrap.js +4 -0
- package/dist/daemon/bootstrap.js.map +1 -0
- package/dist/daemon/broker.d.ts +94 -0
- package/dist/daemon/broker.d.ts.map +1 -0
- package/dist/daemon/broker.js +374 -0
- package/dist/daemon/broker.js.map +1 -0
- package/dist/daemon/commands.d.ts +47 -0
- package/dist/daemon/commands.d.ts.map +1 -0
- package/dist/daemon/commands.js +639 -0
- package/dist/daemon/commands.js.map +1 -0
- package/dist/daemon/control-recovery.d.ts +9 -0
- package/dist/daemon/control-recovery.d.ts.map +1 -0
- package/dist/daemon/control-recovery.js +68 -0
- package/dist/daemon/control-recovery.js.map +1 -0
- package/dist/daemon/control-state.d.ts +1915 -0
- package/dist/daemon/control-state.d.ts.map +1 -0
- package/dist/daemon/control-state.js +154 -0
- package/dist/daemon/control-state.js.map +1 -0
- package/dist/daemon/control-store.d.ts +113 -0
- package/dist/daemon/control-store.d.ts.map +1 -0
- package/dist/daemon/control-store.js +901 -0
- package/dist/daemon/control-store.js.map +1 -0
- package/dist/daemon/execution-policy.d.ts +15 -0
- package/dist/daemon/execution-policy.d.ts.map +1 -0
- package/dist/daemon/execution-policy.js +42 -0
- package/dist/daemon/execution-policy.js.map +1 -0
- package/dist/daemon/execution-services.d.ts +5 -0
- package/dist/daemon/execution-services.d.ts.map +1 -0
- package/dist/daemon/execution-services.js +73 -0
- package/dist/daemon/execution-services.js.map +1 -0
- package/dist/daemon/executions.d.ts +134 -0
- package/dist/daemon/executions.d.ts.map +1 -0
- package/dist/daemon/executions.js +486 -0
- package/dist/daemon/executions.js.map +1 -0
- package/dist/daemon/extension-ui.d.ts +24 -0
- package/dist/daemon/extension-ui.d.ts.map +1 -0
- package/dist/daemon/extension-ui.js +161 -0
- package/dist/daemon/extension-ui.js.map +1 -0
- package/dist/daemon/fanout.d.ts +40 -0
- package/dist/daemon/fanout.d.ts.map +1 -0
- package/dist/daemon/fanout.js +304 -0
- package/dist/daemon/fanout.js.map +1 -0
- package/dist/daemon/headless-json.d.ts +3 -0
- package/dist/daemon/headless-json.d.ts.map +1 -0
- package/dist/daemon/headless-json.js +20 -0
- package/dist/daemon/headless-json.js.map +1 -0
- package/dist/daemon/host-config.d.ts +61 -0
- package/dist/daemon/host-config.d.ts.map +1 -0
- package/dist/daemon/host-config.js +167 -0
- package/dist/daemon/host-config.js.map +1 -0
- package/dist/daemon/host-services.d.ts +61 -0
- package/dist/daemon/host-services.d.ts.map +1 -0
- package/dist/daemon/host-services.js +2 -0
- package/dist/daemon/host-services.js.map +1 -0
- package/dist/daemon/ipc.d.ts +14 -0
- package/dist/daemon/ipc.d.ts.map +1 -0
- package/dist/daemon/ipc.js +233 -0
- package/dist/daemon/ipc.js.map +1 -0
- package/dist/daemon/legacy-sessions.d.ts +48 -0
- package/dist/daemon/legacy-sessions.d.ts.map +1 -0
- package/dist/daemon/legacy-sessions.js +328 -0
- package/dist/daemon/legacy-sessions.js.map +1 -0
- package/dist/daemon/lifecycle.d.ts +44 -0
- package/dist/daemon/lifecycle.d.ts.map +1 -0
- package/dist/daemon/lifecycle.js +164 -0
- package/dist/daemon/lifecycle.js.map +1 -0
- package/dist/daemon/main.d.ts +3 -0
- package/dist/daemon/main.d.ts.map +1 -0
- package/dist/daemon/main.js +83 -0
- package/dist/daemon/main.js.map +1 -0
- package/dist/daemon/message-display.d.ts +4 -0
- package/dist/daemon/message-display.d.ts.map +1 -0
- package/dist/daemon/message-display.js +20 -0
- package/dist/daemon/message-display.js.map +1 -0
- package/dist/daemon/native-ownership.d.ts +13 -0
- package/dist/daemon/native-ownership.d.ts.map +1 -0
- package/dist/daemon/native-ownership.js +173 -0
- package/dist/daemon/native-ownership.js.map +1 -0
- package/dist/daemon/operational-log.d.ts +23 -0
- package/dist/daemon/operational-log.d.ts.map +1 -0
- package/dist/daemon/operational-log.js +79 -0
- package/dist/daemon/operational-log.js.map +1 -0
- package/dist/daemon/ownership.d.ts +35 -0
- package/dist/daemon/ownership.d.ts.map +1 -0
- package/dist/daemon/ownership.js +395 -0
- package/dist/daemon/ownership.js.map +1 -0
- package/dist/daemon/persistence.d.ts +110 -0
- package/dist/daemon/persistence.d.ts.map +1 -0
- package/dist/daemon/persistence.js +540 -0
- package/dist/daemon/persistence.js.map +1 -0
- package/dist/daemon/process-adapter.d.ts +23 -0
- package/dist/daemon/process-adapter.d.ts.map +1 -0
- package/dist/daemon/process-adapter.js +2 -0
- package/dist/daemon/process-adapter.js.map +1 -0
- package/dist/daemon/processes.d.ts +102 -0
- package/dist/daemon/processes.d.ts.map +1 -0
- package/dist/daemon/processes.js +691 -0
- package/dist/daemon/processes.js.map +1 -0
- package/dist/daemon/profile-storage.d.ts +16 -0
- package/dist/daemon/profile-storage.d.ts.map +1 -0
- package/dist/daemon/profile-storage.js +148 -0
- package/dist/daemon/profile-storage.js.map +1 -0
- package/dist/daemon/projection.d.ts +110 -0
- package/dist/daemon/projection.d.ts.map +1 -0
- package/dist/daemon/projection.js +661 -0
- package/dist/daemon/projection.js.map +1 -0
- package/dist/daemon/provider-loader.d.ts +28 -0
- package/dist/daemon/provider-loader.d.ts.map +1 -0
- package/dist/daemon/provider-loader.js +376 -0
- package/dist/daemon/provider-loader.js.map +1 -0
- package/dist/daemon/provider.d.ts +92 -0
- package/dist/daemon/provider.d.ts.map +1 -0
- package/dist/daemon/provider.js +31 -0
- package/dist/daemon/provider.js.map +1 -0
- package/dist/daemon/registry.d.ts +209 -0
- package/dist/daemon/registry.d.ts.map +1 -0
- package/dist/daemon/registry.js +1591 -0
- package/dist/daemon/registry.js.map +1 -0
- package/dist/daemon/runtime-dependencies.d.ts +5 -0
- package/dist/daemon/runtime-dependencies.d.ts.map +1 -0
- package/dist/daemon/runtime-dependencies.js +31 -0
- package/dist/daemon/runtime-dependencies.js.map +1 -0
- package/dist/daemon/server.d.ts +110 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +556 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/service.d.ts +38 -0
- package/dist/daemon/service.d.ts.map +1 -0
- package/dist/daemon/service.js +275 -0
- package/dist/daemon/service.js.map +1 -0
- package/dist/daemon/session-cursor.d.ts +3 -0
- package/dist/daemon/session-cursor.d.ts.map +1 -0
- package/dist/daemon/session-cursor.js +4 -0
- package/dist/daemon/session-cursor.js.map +1 -0
- package/dist/daemon/session-execution.d.ts +88 -0
- package/dist/daemon/session-execution.d.ts.map +1 -0
- package/dist/daemon/session-execution.js +2 -0
- package/dist/daemon/session-execution.js.map +1 -0
- package/dist/daemon/session-executions.d.ts +5 -0
- package/dist/daemon/session-executions.d.ts.map +1 -0
- package/dist/daemon/session-executions.js +146 -0
- package/dist/daemon/session-executions.js.map +1 -0
- package/dist/daemon/session-files.d.ts +28 -0
- package/dist/daemon/session-files.d.ts.map +1 -0
- package/dist/daemon/session-files.js +189 -0
- package/dist/daemon/session-files.js.map +1 -0
- package/dist/daemon/session-lifecycle-contracts.d.ts +104 -0
- package/dist/daemon/session-lifecycle-contracts.d.ts.map +1 -0
- package/dist/daemon/session-lifecycle-contracts.js +2 -0
- package/dist/daemon/session-lifecycle-contracts.js.map +1 -0
- package/dist/daemon/session-lifecycle.d.ts +26 -0
- package/dist/daemon/session-lifecycle.d.ts.map +1 -0
- package/dist/daemon/session-lifecycle.js +246 -0
- package/dist/daemon/session-lifecycle.js.map +1 -0
- package/dist/daemon/session-runtime.d.ts +64 -0
- package/dist/daemon/session-runtime.d.ts.map +1 -0
- package/dist/daemon/session-runtime.js +3 -0
- package/dist/daemon/session-runtime.js.map +1 -0
- package/dist/daemon/session-services.d.ts +6 -0
- package/dist/daemon/session-services.d.ts.map +1 -0
- package/dist/daemon/session-services.js +144 -0
- package/dist/daemon/session-services.js.map +1 -0
- package/dist/daemon/shutdown-error.d.ts +12 -0
- package/dist/daemon/shutdown-error.d.ts.map +1 -0
- package/dist/daemon/shutdown-error.js +51 -0
- package/dist/daemon/shutdown-error.js.map +1 -0
- package/dist/daemon/supervisor.d.ts +28 -0
- package/dist/daemon/supervisor.d.ts.map +1 -0
- package/dist/daemon/supervisor.js +159 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/daemon/target.d.ts +55 -0
- package/dist/daemon/target.d.ts.map +1 -0
- package/dist/daemon/target.js +191 -0
- package/dist/daemon/target.js.map +1 -0
- package/dist/daemon/tree.d.ts +7 -0
- package/dist/daemon/tree.d.ts.map +1 -0
- package/dist/daemon/tree.js +126 -0
- package/dist/daemon/tree.js.map +1 -0
- package/dist/daemon/ui-vocabulary.d.ts +29 -0
- package/dist/daemon/ui-vocabulary.d.ts.map +1 -0
- package/dist/daemon/ui-vocabulary.js +323 -0
- package/dist/daemon/ui-vocabulary.js.map +1 -0
- package/dist/daemon/worker.d.ts +5 -0
- package/dist/daemon/worker.d.ts.map +1 -0
- package/dist/daemon/worker.js +49 -0
- package/dist/daemon/worker.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lock.d.ts +27 -0
- package/dist/lock.d.ts.map +1 -0
- package/dist/lock.js +432 -0
- package/dist/lock.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +56 -0
- package/dist/logger.js.map +1 -0
- package/dist/remote/backoff.d.ts +17 -0
- package/dist/remote/backoff.d.ts.map +1 -0
- package/dist/remote/backoff.js +18 -0
- package/dist/remote/backoff.js.map +1 -0
- package/dist/remote/batch.d.ts +3 -0
- package/dist/remote/batch.d.ts.map +1 -0
- package/dist/remote/batch.js +27 -0
- package/dist/remote/batch.js.map +1 -0
- package/dist/remote/command-journal.d.ts +30 -0
- package/dist/remote/command-journal.d.ts.map +1 -0
- package/dist/remote/command-journal.js +72 -0
- package/dist/remote/command-journal.js.map +1 -0
- package/dist/remote/commands.d.ts +48 -0
- package/dist/remote/commands.d.ts.map +1 -0
- package/dist/remote/commands.js +145 -0
- package/dist/remote/commands.js.map +1 -0
- package/dist/remote/connector.d.ts +135 -0
- package/dist/remote/connector.d.ts.map +1 -0
- package/dist/remote/connector.js +1069 -0
- package/dist/remote/connector.js.map +1 -0
- package/dist/remote/content.d.ts +17 -0
- package/dist/remote/content.d.ts.map +1 -0
- package/dist/remote/content.js +116 -0
- package/dist/remote/content.js.map +1 -0
- package/dist/remote/outbox.d.ts +52 -0
- package/dist/remote/outbox.d.ts.map +1 -0
- package/dist/remote/outbox.js +321 -0
- package/dist/remote/outbox.js.map +1 -0
- package/dist/remote/platform.d.ts +53 -0
- package/dist/remote/platform.d.ts.map +1 -0
- package/dist/remote/platform.js +231 -0
- package/dist/remote/platform.js.map +1 -0
- package/dist/remote/realtime.d.ts +45 -0
- package/dist/remote/realtime.d.ts.map +1 -0
- package/dist/remote/realtime.js +265 -0
- package/dist/remote/realtime.js.map +1 -0
- package/dist/remote/sharing.d.ts +16 -0
- package/dist/remote/sharing.d.ts.map +1 -0
- package/dist/remote/sharing.js +24 -0
- package/dist/remote/sharing.js.map +1 -0
- package/dist/remote/state.d.ts +52 -0
- package/dist/remote/state.d.ts.map +1 -0
- package/dist/remote/state.js +156 -0
- package/dist/remote/state.js.map +1 -0
- package/dist/remote/text.d.ts +11 -0
- package/dist/remote/text.d.ts.map +1 -0
- package/dist/remote/text.js +188 -0
- package/dist/remote/text.js.map +1 -0
- package/dist/remote/token.d.ts +39 -0
- package/dist/remote/token.d.ts.map +1 -0
- package/dist/remote/token.js +238 -0
- package/dist/remote/token.js.map +1 -0
- package/dist/runtime-factory.d.ts +12 -0
- package/dist/runtime-factory.d.ts.map +1 -0
- package/dist/runtime-factory.js +12 -0
- package/dist/runtime-factory.js.map +1 -0
- package/dist/session-history.d.ts +29 -0
- package/dist/session-history.d.ts.map +1 -0
- package/dist/session-history.js +374 -0
- package/dist/session-history.js.map +1 -0
- package/dist/setup/dependencies.d.ts +15 -0
- package/dist/setup/dependencies.d.ts.map +1 -0
- package/dist/setup/dependencies.js +2 -0
- package/dist/setup/dependencies.js.map +1 -0
- package/dist/setup/open.d.ts +8 -0
- package/dist/setup/open.d.ts.map +1 -0
- package/dist/setup/open.js +16 -0
- package/dist/setup/open.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/package.json +65 -3
|
@@ -0,0 +1,691 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { setImmediate as yieldTurn } from 'node:timers/promises';
|
|
5
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
6
|
+
import { DEFAULT_MAX_BYTES, getShellConfig, truncateTail } from '@earendil-works/pi-coding-agent';
|
|
7
|
+
import { LIMITS, canonicalRequestPayload } from '@felan-ai/session-protocol';
|
|
8
|
+
import { boundedText } from './projection.js';
|
|
9
|
+
import { readOwnedOutputRange } from './session-files.js';
|
|
10
|
+
import { pathInside } from './target.js';
|
|
11
|
+
export class RegistryProcesses {
|
|
12
|
+
options;
|
|
13
|
+
live = new Map();
|
|
14
|
+
views = new Map();
|
|
15
|
+
lifetimes = new Set();
|
|
16
|
+
starting = new Set();
|
|
17
|
+
operations = new Map();
|
|
18
|
+
foregroundId = new AsyncLocalStorage();
|
|
19
|
+
foregroundStarts = new Map();
|
|
20
|
+
cleanup = new AsyncLocalStorage();
|
|
21
|
+
now;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
this.now = options.now ?? Date.now;
|
|
25
|
+
}
|
|
26
|
+
wrap(runtime, scope) {
|
|
27
|
+
const storage = new Map();
|
|
28
|
+
const lifetime = { scope, runtime, draining: false, revoked: false };
|
|
29
|
+
const current = () => this.assertLifetime(lifetime);
|
|
30
|
+
const checked = (operation) => this.track((async () => {
|
|
31
|
+
current();
|
|
32
|
+
const value = await operation();
|
|
33
|
+
this.options.assertCurrent();
|
|
34
|
+
return value;
|
|
35
|
+
})(), scope);
|
|
36
|
+
const wrapped = {
|
|
37
|
+
kind: runtime.kind, cwd: runtime.cwd, logger: runtime.logger, ...(runtime.classifier ? { classifier: runtime.classifier } : {}),
|
|
38
|
+
storage: (kind = 'session') => {
|
|
39
|
+
let result = storage.get(kind);
|
|
40
|
+
if (!result) {
|
|
41
|
+
const original = runtime.storage(kind);
|
|
42
|
+
result = {
|
|
43
|
+
root: original.root,
|
|
44
|
+
readFile: (path, opts) => checked(() => original.readFile(path, opts)),
|
|
45
|
+
writeFile: (path, content) => checked(async () => {
|
|
46
|
+
if (kind === 'session')
|
|
47
|
+
this.observeJob(path, content, runtime, scope);
|
|
48
|
+
await original.writeFile(path, content);
|
|
49
|
+
}),
|
|
50
|
+
appendFile: (path, content) => checked(() => original.appendFile(path, content)),
|
|
51
|
+
listFiles: (path, opts) => checked(() => original.listFiles(path, opts)),
|
|
52
|
+
mkdir: (path, opts) => checked(() => original.mkdir(path, opts)),
|
|
53
|
+
remove: (path, opts) => checked(() => original.remove(path, opts)),
|
|
54
|
+
};
|
|
55
|
+
storage.set(kind, result);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
},
|
|
59
|
+
exec: (command, args, opts) => checked(() => runtime.exec(command, args, opts)),
|
|
60
|
+
shell: (command, opts) => checked(() => runtime.shell(command, opts)),
|
|
61
|
+
readFile: (path, opts) => checked(() => runtime.readFile(path, opts)),
|
|
62
|
+
writeFile: (path, content, opts) => checked(() => runtime.writeFile(path, content, opts)),
|
|
63
|
+
listFiles: (path, opts) => checked(() => runtime.listFiles(path, opts)),
|
|
64
|
+
mkdir: (path, opts) => checked(() => runtime.mkdir(path, opts)),
|
|
65
|
+
remove: (path, opts) => checked(() => runtime.remove(path, opts)),
|
|
66
|
+
...(runtime.files ? { files: {
|
|
67
|
+
inspect: path => checked(() => runtime.files.inspect(path)),
|
|
68
|
+
canonicalPath: path => checked(() => runtime.files.canonicalPath(path)),
|
|
69
|
+
move: (source, target) => checked(() => runtime.files.move(source, target)),
|
|
70
|
+
removeEmptyDirectory: path => checked(() => runtime.files.removeEmptyDirectory(path)),
|
|
71
|
+
touch: (path, modifiedAt) => checked(() => runtime.files.touch(path, modifiedAt)),
|
|
72
|
+
ensurePrivateDirectory: (path, options) => checked(() => runtime.files.ensurePrivateDirectory(path, options)),
|
|
73
|
+
snapshot: (path, maxBytes) => checked(() => runtime.files.snapshot(path, maxBytes)),
|
|
74
|
+
append: (path, content, expected) => checked(() => runtime.files.append(path, content, expected)),
|
|
75
|
+
} } : {}),
|
|
76
|
+
...(runtime.readAgentFile ? { readAgentFile: (path) => checked(() => runtime.readAgentFile(path)) } : {}),
|
|
77
|
+
...(runtime.privateRuntime ? { privateRuntime: { ensureDirectory: (name) => checked(() => runtime.privateRuntime.ensureDirectory(name)) } } : {}),
|
|
78
|
+
...(runtime.processes ? { processes: {
|
|
79
|
+
startShell: (command, opts) => this.start(lifetime, 'foreground', command, () => runtime.processes.startShell(command, opts), this.foregroundId.getStore()),
|
|
80
|
+
...(runtime.processes.startStdio ? { startStdio: (command, args, opts) => this.start(lifetime, 'foreground', command, () => runtime.processes.startStdio(command, args, opts), undefined, true) } : {}),
|
|
81
|
+
} } : {}),
|
|
82
|
+
...(runtime.terminals ? { terminals: {
|
|
83
|
+
startShell: async (command, opts) => {
|
|
84
|
+
const id = opts?.id ?? randomUUID();
|
|
85
|
+
await this.start(lifetime, 'pty', command, () => runtime.terminals.startShell(command, opts), id);
|
|
86
|
+
return this.terminalHandle(lifetime, id);
|
|
87
|
+
},
|
|
88
|
+
attach: id => this.terminalHandle(lifetime, id),
|
|
89
|
+
} } : {}),
|
|
90
|
+
};
|
|
91
|
+
lifetime.runtime = wrapped;
|
|
92
|
+
this.lifetimes.add(lifetime);
|
|
93
|
+
return wrapped;
|
|
94
|
+
}
|
|
95
|
+
seal(session) { for (const lifetime of this.lifetimes)
|
|
96
|
+
if (lifetime.scope.session === session)
|
|
97
|
+
lifetime.draining = true; }
|
|
98
|
+
revoke(session) { for (const lifetime of this.lifetimes)
|
|
99
|
+
if (lifetime.scope.session === session) {
|
|
100
|
+
lifetime.revoked = true;
|
|
101
|
+
this.lifetimes.delete(lifetime);
|
|
102
|
+
} }
|
|
103
|
+
revokeRuntime(runtime) { for (const lifetime of this.lifetimes)
|
|
104
|
+
if (lifetime.runtime === runtime) {
|
|
105
|
+
lifetime.revoked = true;
|
|
106
|
+
this.lifetimes.delete(lifetime);
|
|
107
|
+
} }
|
|
108
|
+
sealAll() { for (const lifetime of this.lifetimes)
|
|
109
|
+
lifetime.draining = true; }
|
|
110
|
+
withCleanup(session, operation) {
|
|
111
|
+
return this.cleanup.run(new Set([...(this.cleanup.getStore() ?? []), ...(typeof session === 'string' ? [session] : session)]), operation);
|
|
112
|
+
}
|
|
113
|
+
assertLifetime(lifetime, starting = false) {
|
|
114
|
+
this.options.assertCurrent();
|
|
115
|
+
if (!lifetime.scope.session)
|
|
116
|
+
throw new Error('Session runtime is not bound yet.');
|
|
117
|
+
if (lifetime.revoked || lifetime.draining && (starting || !this.cleanup.getStore()?.has(lifetime.scope.session)))
|
|
118
|
+
throw new Error('Session runtime is closed to new work.');
|
|
119
|
+
}
|
|
120
|
+
runtime(session, root) {
|
|
121
|
+
return [...this.lifetimes].reverse().find(item => !item.revoked && item.scope.session === session && item.scope.root === root)?.runtime;
|
|
122
|
+
}
|
|
123
|
+
terminalHandle(lifetime, id) {
|
|
124
|
+
this.assertLifetime(lifetime);
|
|
125
|
+
const live = this.live.get(id);
|
|
126
|
+
if (!live || live.view.root !== lifetime.scope.root || live.view.kind !== 'pty')
|
|
127
|
+
return undefined;
|
|
128
|
+
const checked = (operation) => {
|
|
129
|
+
this.assertLifetime(lifetime);
|
|
130
|
+
return this.track(operation(), lifetime.scope);
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
pid: live.raw.pid,
|
|
134
|
+
read: (offset, options) => checked(() => live.process.read(offset, options)),
|
|
135
|
+
write: content => checked(() => live.process.write(content)),
|
|
136
|
+
terminate: signal => checked(() => live.process.terminate(signal)),
|
|
137
|
+
...(live.process.interrupt ? { interrupt: () => checked(() => live.process.interrupt()) } : {}),
|
|
138
|
+
dispose: async () => { },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
list(root) {
|
|
142
|
+
return this.options.store.resources().filter(row => row.root === root).map(row => {
|
|
143
|
+
const known = this.views.get(row.id);
|
|
144
|
+
return known ?? { id: row.id, session: row.session, root, kind: row.kind,
|
|
145
|
+
state: row.state === 'reconciling' ? 'interrupted' : row.state, command: '', startedAt: row.updatedAt, controllable: false, outputOffset: 0 };
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
prepareForeground(session, settings = {}) {
|
|
149
|
+
const lifetime = this.currentLifetime(session);
|
|
150
|
+
this.assertLifetime(lifetime, true);
|
|
151
|
+
if (!lifetime.runtime.processes)
|
|
152
|
+
throw processFault('unavailable', 'Foreground processes are unavailable.');
|
|
153
|
+
const shell = getShellConfig(settings.shell).shell;
|
|
154
|
+
return { shell, prefix: settings.prefix, assertCurrent: () => this.assertLifetime(lifetime, true) };
|
|
155
|
+
}
|
|
156
|
+
async startForeground(requestId, session, params, prepared = this.prepareForeground(session)) {
|
|
157
|
+
const lifetime = this.currentLifetime(session);
|
|
158
|
+
this.assertLifetime(lifetime, true);
|
|
159
|
+
const request = this.options.store.request(requestId);
|
|
160
|
+
if (request?.session !== session || request.op !== 'shell.execute' || request.phase !== 'dispatch_intent')
|
|
161
|
+
throw new Error('Foreground start requires its committed shell dispatch.');
|
|
162
|
+
const payload = canonicalRequestPayload({ v: 1, id: requestId, op: 'shell.execute', session, params });
|
|
163
|
+
if (request.identity.bytes !== Buffer.byteLength(payload) || request.identity.sha256 !== createHash('sha256').update(payload).digest('hex'))
|
|
164
|
+
throw new Error('Shell command differs from its admitted payload.');
|
|
165
|
+
if (!lifetime.runtime.processes)
|
|
166
|
+
throw new Error('Foreground processes are unavailable.');
|
|
167
|
+
if (this.foregroundStarts.has(requestId))
|
|
168
|
+
throw new Error('Foreground dispatch cannot be repeated.');
|
|
169
|
+
const resourceId = `foreground:${requestId}`;
|
|
170
|
+
if (this.options.store.resource(resourceId))
|
|
171
|
+
throw processFault('already_resolved', 'Foreground dispatch already has a durable resource intent.');
|
|
172
|
+
this.foregroundStarts.set(requestId, resourceId);
|
|
173
|
+
return this.track((async () => {
|
|
174
|
+
prepared.assertCurrent();
|
|
175
|
+
const command = prepared.prefix ? `${prepared.prefix}\n${params.command}` : params.command;
|
|
176
|
+
const process = await this.foregroundId.run(resourceId, () => lifetime.runtime.processes.startShell(command, { shellFlavor: 'posix', shell: prepared.shell }));
|
|
177
|
+
this.assertLifetime(lifetime, true);
|
|
178
|
+
const live = this.live.get(resourceId);
|
|
179
|
+
if (!live || live.process !== process || live.view.session !== session)
|
|
180
|
+
throw new Error('Foreground process identity changed.');
|
|
181
|
+
return { resourceId };
|
|
182
|
+
})().catch(error => { this.foregroundStarts.delete(requestId); throw error; }), lifetime.scope);
|
|
183
|
+
}
|
|
184
|
+
async waitForeground(requester, id, onOutput) {
|
|
185
|
+
const row = this.authorizeResource(requester, id);
|
|
186
|
+
const live = this.live.get(id);
|
|
187
|
+
if (row.kind !== 'foreground' || !live)
|
|
188
|
+
throw new Error('Foreground process is unavailable.');
|
|
189
|
+
const lifetime = this.currentLifetime(row.session);
|
|
190
|
+
this.assertLifetime(lifetime);
|
|
191
|
+
return this.track((async () => {
|
|
192
|
+
let offset = 0;
|
|
193
|
+
let retained = 0;
|
|
194
|
+
let total = 0;
|
|
195
|
+
let discarded = false;
|
|
196
|
+
const chunks = [];
|
|
197
|
+
const decoder = new TextDecoder();
|
|
198
|
+
let pending = '';
|
|
199
|
+
let fullOutputPath;
|
|
200
|
+
const storage = lifetime.runtime.storage('session');
|
|
201
|
+
const append = async (raw) => {
|
|
202
|
+
const text = stripVTControlCharacters(raw).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\uFFF9-\uFFFB\r]/gu, '');
|
|
203
|
+
if (!text)
|
|
204
|
+
return;
|
|
205
|
+
total += Buffer.byteLength(text);
|
|
206
|
+
if (!fullOutputPath && total > DEFAULT_MAX_BYTES) {
|
|
207
|
+
const directory = join(storage.root, 'shell-output');
|
|
208
|
+
await storage.mkdir(directory, { recursive: true });
|
|
209
|
+
fullOutputPath = join(directory, `${id}.log`);
|
|
210
|
+
await storage.writeFile(fullOutputPath, Buffer.from(chunks.join('')));
|
|
211
|
+
}
|
|
212
|
+
if (fullOutputPath)
|
|
213
|
+
await storage.appendFile(fullOutputPath, Buffer.from(text));
|
|
214
|
+
chunks.push(text);
|
|
215
|
+
retained += Buffer.byteLength(text);
|
|
216
|
+
while (retained > DEFAULT_MAX_BYTES * 2 && chunks.length > 1) {
|
|
217
|
+
retained -= Buffer.byteLength(chunks.shift());
|
|
218
|
+
discarded = true;
|
|
219
|
+
}
|
|
220
|
+
onOutput?.(truncateTail(chunks.join('')).content);
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
for (;;) {
|
|
224
|
+
const result = await live.process.read(offset, { waitMs: 1000, maxBytes: LIMITS.processReadBytes });
|
|
225
|
+
this.assertLifetime(lifetime);
|
|
226
|
+
this.assertLiveIdentity(this.authorizeResource(requester, id), live);
|
|
227
|
+
if (!Number.isSafeInteger(result.nextOffset) || result.nextOffset !== offset + result.output.byteLength || result.output.byteLength > LIMITS.processReadBytes)
|
|
228
|
+
throw new Error('Foreground output range is unavailable.');
|
|
229
|
+
const decoded = pending + decoder.decode(result.output, { stream: true });
|
|
230
|
+
const split = splitShellOutput(decoded);
|
|
231
|
+
pending = split.pending;
|
|
232
|
+
await append(split.complete);
|
|
233
|
+
offset = result.nextOffset;
|
|
234
|
+
if (!result.running && result.output.byteLength === 0) {
|
|
235
|
+
await append(pending + decoder.decode());
|
|
236
|
+
const resultText = truncateTail(chunks.join(''));
|
|
237
|
+
return { output: resultText.content, exitCode: result.exitCode, cancelled: result.exitCode === undefined,
|
|
238
|
+
truncated: discarded || resultText.truncated, ...(fullOutputPath ? { fullOutputPath } : {}) };
|
|
239
|
+
}
|
|
240
|
+
if (!result.output.byteLength)
|
|
241
|
+
await yieldTurn();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
for (const [request, resource] of this.foregroundStarts)
|
|
246
|
+
if (resource === id)
|
|
247
|
+
this.foregroundStarts.delete(request);
|
|
248
|
+
}
|
|
249
|
+
})(), lifetime.scope);
|
|
250
|
+
}
|
|
251
|
+
async cancelForeground(session) {
|
|
252
|
+
const rows = this.options.store.resources().filter(row => row.session === session && row.kind === 'foreground' && row.state === 'running');
|
|
253
|
+
for (const row of rows)
|
|
254
|
+
await this.stop(row.root, row.id);
|
|
255
|
+
}
|
|
256
|
+
async read(requester, id, offset, limit) {
|
|
257
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > LIMITS.processReadBytes)
|
|
258
|
+
throw new Error('Invalid process read range.');
|
|
259
|
+
const row = this.authorizeResource(requester, id);
|
|
260
|
+
if (row.kind === 'detached') {
|
|
261
|
+
const lifetime = this.currentLifetime(requester);
|
|
262
|
+
this.assertLifetime(lifetime);
|
|
263
|
+
if (!row.reference || !row.startIdentity)
|
|
264
|
+
throw new Error('Detached output identity is unavailable.');
|
|
265
|
+
const root = this.currentLifetime(row.root);
|
|
266
|
+
this.assertLifetime(root);
|
|
267
|
+
const storage = root.runtime.storage('session');
|
|
268
|
+
const reference = row.reference;
|
|
269
|
+
const directory = this.options.processAdapter?.storageDirectory(root.runtime);
|
|
270
|
+
if (!directory || !pathInside(directory, reference))
|
|
271
|
+
throw new Error('Detached output is outside its root storage.');
|
|
272
|
+
return this.track((async () => {
|
|
273
|
+
const info = this.describeJob(root.runtime, reference, await storage.readFile(reference, { maxBytes: 65536 }));
|
|
274
|
+
this.assertLifetime(lifetime);
|
|
275
|
+
this.assertLifetime(root);
|
|
276
|
+
const current = this.authorizeResource(requester, id);
|
|
277
|
+
if (current.reference !== reference || current.startIdentity !== row.startIdentity || current.kind !== 'detached'
|
|
278
|
+
|| !info || info.id !== id || info.startIdentity !== row.startIdentity || info.kind !== 'detached')
|
|
279
|
+
throw new Error('Detached output identity changed.');
|
|
280
|
+
const data = readOwnedOutputRange(this.options.store.owner, info.outputPath, offset, limit);
|
|
281
|
+
this.assertLifetime(lifetime);
|
|
282
|
+
this.assertLifetime(root);
|
|
283
|
+
this.authorizeResource(requester, id);
|
|
284
|
+
const state = current.state === 'reconciling' ? 'interrupted' : info.status === 'unknown' ? 'missing'
|
|
285
|
+
: info.status !== 'running' ? 'exited' : current.state;
|
|
286
|
+
return { data, offset, nextOffset: offset + data.byteLength, state,
|
|
287
|
+
...(state === 'exited' && typeof info.exitCode === 'number' && Number.isInteger(info.exitCode) ? { exitCode: info.exitCode } : {}) };
|
|
288
|
+
})(), lifetime.scope);
|
|
289
|
+
}
|
|
290
|
+
const live = this.live.get(id);
|
|
291
|
+
if (!live)
|
|
292
|
+
return { data: new Uint8Array(), offset, nextOffset: offset, state: row.state === 'interrupted' ? 'interrupted' : 'missing' };
|
|
293
|
+
const lifetime = this.creatorLifetime(row);
|
|
294
|
+
this.assertLifetime(lifetime);
|
|
295
|
+
this.assertLiveIdentity(row, live);
|
|
296
|
+
return this.track((async () => {
|
|
297
|
+
const snapshot = await live.process.read(offset, { maxBytes: limit });
|
|
298
|
+
this.assertLifetime(lifetime);
|
|
299
|
+
this.assertLiveIdentity(this.authorizeResource(requester, id), live);
|
|
300
|
+
if (!(snapshot.output instanceof Uint8Array) || snapshot.output.byteLength > limit
|
|
301
|
+
|| !Number.isSafeInteger(snapshot.nextOffset) || snapshot.nextOffset !== offset + snapshot.output.byteLength) {
|
|
302
|
+
throw Object.assign(new Error('Process output range is no longer retained.'), { code: 'resync_required' });
|
|
303
|
+
}
|
|
304
|
+
return { data: new Uint8Array(snapshot.output), offset, nextOffset: snapshot.nextOffset,
|
|
305
|
+
state: snapshot.running ? 'running' : 'exited', ...(snapshot.exitCode === undefined ? {} : { exitCode: snapshot.exitCode }) };
|
|
306
|
+
})(), lifetime.scope);
|
|
307
|
+
}
|
|
308
|
+
async write(requester, id, data) {
|
|
309
|
+
if (!(data instanceof Uint8Array) || data.byteLength > LIMITS.processWriteBytes)
|
|
310
|
+
throw new Error('Invalid process input size.');
|
|
311
|
+
const bytes = new Uint8Array(data);
|
|
312
|
+
const row = this.authorizeResource(requester, id);
|
|
313
|
+
const live = this.live.get(id);
|
|
314
|
+
if (!live || live.disposed || live.settled || !live.view.controllable)
|
|
315
|
+
throw new Error('Process stdin is unavailable.');
|
|
316
|
+
const lifetime = this.creatorLifetime(row);
|
|
317
|
+
this.assertLifetime(lifetime);
|
|
318
|
+
this.assertLiveIdentity(row, live);
|
|
319
|
+
return this.track((async () => {
|
|
320
|
+
await live.process.write(bytes);
|
|
321
|
+
this.assertLifetime(lifetime);
|
|
322
|
+
this.assertLiveIdentity(this.authorizeResource(requester, id), live);
|
|
323
|
+
return bytes.byteLength;
|
|
324
|
+
})(), lifetime.scope);
|
|
325
|
+
}
|
|
326
|
+
assertWritable(requester, id) {
|
|
327
|
+
const row = this.authorizeResource(requester, id);
|
|
328
|
+
const live = this.live.get(id);
|
|
329
|
+
if (!live || live.disposed || live.settled || !live.view.controllable)
|
|
330
|
+
throw processFault('unavailable', 'Process stdin is unavailable.');
|
|
331
|
+
this.assertLifetime(this.creatorLifetime(row));
|
|
332
|
+
this.assertLiveIdentity(row, live);
|
|
333
|
+
}
|
|
334
|
+
async prepareStop(requester, id) {
|
|
335
|
+
let row = this.authorizeResource(requester, id);
|
|
336
|
+
this.assertLifetime(this.creatorLifetime(row));
|
|
337
|
+
if (this.live.has(id))
|
|
338
|
+
return;
|
|
339
|
+
await this.refresh(row.root);
|
|
340
|
+
row = this.authorizeResource(requester, id);
|
|
341
|
+
this.assertLifetime(this.creatorLifetime(row));
|
|
342
|
+
if (row.kind !== 'detached' || !this.views.get(id)?.controllable)
|
|
343
|
+
throw processFault('unavailable', 'Process identity is missing or unverified.');
|
|
344
|
+
}
|
|
345
|
+
async stopForSession(requester, id, signal) {
|
|
346
|
+
if (signal !== 'SIGTERM' && signal !== 'SIGKILL')
|
|
347
|
+
throw new Error('Unsupported process termination signal.');
|
|
348
|
+
const row = this.authorizeResource(requester, id);
|
|
349
|
+
const lifetime = this.creatorLifetime(row);
|
|
350
|
+
this.assertLifetime(lifetime);
|
|
351
|
+
return this.track((async () => {
|
|
352
|
+
await this.stop(row.root, id, signal);
|
|
353
|
+
this.assertLifetime(lifetime);
|
|
354
|
+
this.authorizeResource(requester, id);
|
|
355
|
+
})(), lifetime.scope);
|
|
356
|
+
}
|
|
357
|
+
currentLifetime(session) {
|
|
358
|
+
const lifetime = this.openLifetime(session);
|
|
359
|
+
if (!lifetime)
|
|
360
|
+
throw new Error('Process creator runtime is unavailable.');
|
|
361
|
+
return lifetime;
|
|
362
|
+
}
|
|
363
|
+
openLifetime(session) {
|
|
364
|
+
const stored = this.options.store.sessionMetadata(session);
|
|
365
|
+
return stored.state === 'open' ? [...this.lifetimes].reverse().find(item => !item.revoked && item.scope.session === session && item.scope.root === stored.root) : undefined;
|
|
366
|
+
}
|
|
367
|
+
/** A PTY kept across reload may outlive its child creator's runtime; its root then controls it. */
|
|
368
|
+
creatorLifetime(row) {
|
|
369
|
+
return this.openLifetime(row.session) ?? this.currentLifetime(row.root);
|
|
370
|
+
}
|
|
371
|
+
authorizeResource(requester, id) {
|
|
372
|
+
this.options.assertCurrent();
|
|
373
|
+
const sessions = this.options.store.sessionMetadatas();
|
|
374
|
+
const session = sessions.find(item => item.id === requester);
|
|
375
|
+
const row = this.options.store.resource(id);
|
|
376
|
+
if (!session || session.state !== 'open' || !row || row.root !== session.root)
|
|
377
|
+
throw new Error('Process not found in the authorized session scope.');
|
|
378
|
+
const creator = sessions.find(item => item.id === row.session);
|
|
379
|
+
if (!creator || creator.root !== row.root)
|
|
380
|
+
throw new Error('Process creator identity is invalid.');
|
|
381
|
+
let ancestor = creator;
|
|
382
|
+
const seen = new Set();
|
|
383
|
+
while (ancestor.id !== requester && ancestor.parent && !seen.has(ancestor.id)) {
|
|
384
|
+
seen.add(ancestor.id);
|
|
385
|
+
const parent = sessions.find(item => item.id === ancestor.parent && item.root === row.root);
|
|
386
|
+
if (!parent)
|
|
387
|
+
break;
|
|
388
|
+
ancestor = parent;
|
|
389
|
+
}
|
|
390
|
+
if (ancestor.id !== requester)
|
|
391
|
+
throw new Error('Process belongs to another session lineage.');
|
|
392
|
+
return row;
|
|
393
|
+
}
|
|
394
|
+
assertLiveIdentity(row, live) {
|
|
395
|
+
if (this.live.get(row.id) !== live || live.view.session !== row.session || live.view.root !== row.root || live.view.kind !== row.kind)
|
|
396
|
+
throw new Error('Retained process identity changed.');
|
|
397
|
+
}
|
|
398
|
+
blocksEviction(root) { return this.activeCount(root) > 0; }
|
|
399
|
+
/** In-flight runtime operations plus running or unverified processes of a root; stdio service transports are excluded. */
|
|
400
|
+
activeCount(root) {
|
|
401
|
+
return [...this.operations.values()].filter(({ scope, passive }) => scope.root === root && !passive).length
|
|
402
|
+
+ this.options.store.resources().filter(row => row.root === root && !this.live.get(row.id)?.transport && (row.state === 'running' || row.state === 'reconciling')).length;
|
|
403
|
+
}
|
|
404
|
+
/** Work a daemon stop would interrupt: in-flight runtime operations and live non-transport processes. Detached jobs survive the daemon. */
|
|
405
|
+
async interruptibleCount() {
|
|
406
|
+
await this.observeLive();
|
|
407
|
+
return [...this.operations.values()].filter(({ passive }) => !passive).length
|
|
408
|
+
+ [...this.live.values()].filter(item => !item.transport && !item.settled && !item.disposed).length;
|
|
409
|
+
}
|
|
410
|
+
async observeLive() {
|
|
411
|
+
await Promise.all([...this.live.values()].filter(item => !item.transport).map(item => this.observe(item)));
|
|
412
|
+
}
|
|
413
|
+
/** Roots that still hold undisposed live handles, including PTYs kept by a reload that never reopened its root. */
|
|
414
|
+
liveRoots() { return new Set([...this.live.values()].filter(item => !item.disposed).map(item => item.view.root)); }
|
|
415
|
+
/** Reconciles a root's processes; a failed per-process probe leaves that row unchanged, while store and ownership failures propagate. */
|
|
416
|
+
async refresh(root) {
|
|
417
|
+
for (const row of this.options.store.resources().filter(row => row.root === root)) {
|
|
418
|
+
const live = this.live.get(row.id);
|
|
419
|
+
if (live) {
|
|
420
|
+
await this.observe(live);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// Live handles end with the daemon that started them.
|
|
424
|
+
if (row.kind !== 'detached' && row.state === 'running' && !this.starting.has(row.id)) {
|
|
425
|
+
this.update(row, 'interrupted');
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (row.kind !== 'detached' || !row.reference || !row.startIdentity)
|
|
429
|
+
continue;
|
|
430
|
+
const runtime = this.runtime(row.session, root) ?? this.runtime(root, root);
|
|
431
|
+
if (!runtime)
|
|
432
|
+
continue;
|
|
433
|
+
const bytes = await runtime.storage('session').readFile(row.reference, { maxBytes: 64 * 1024 }).catch(() => undefined);
|
|
434
|
+
this.options.assertCurrent();
|
|
435
|
+
if (!bytes) {
|
|
436
|
+
this.update(row, 'missing');
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const adapter = this.options.processAdapter;
|
|
440
|
+
if (!adapter)
|
|
441
|
+
continue;
|
|
442
|
+
let info;
|
|
443
|
+
try {
|
|
444
|
+
info = this.describeJob(runtime, row.reference, bytes);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
this.update(row, 'missing');
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
if (!info || info.id !== row.id || info.startIdentity !== row.startIdentity || info.kind !== 'detached') {
|
|
451
|
+
this.update(row, 'missing');
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const job = await Promise.resolve().then(() => adapter.inspectDetached(runtime, row.id)).catch(() => undefined);
|
|
455
|
+
this.options.assertCurrent();
|
|
456
|
+
if (!job)
|
|
457
|
+
continue;
|
|
458
|
+
if (job.status !== 'running') {
|
|
459
|
+
this.update(row, job.status === 'unknown' ? 'missing' : 'exited');
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const pid = job.pid;
|
|
463
|
+
const inspection = pid ? await runtime.exec('ps', ['-o', 'command=', '-p', String(pid)]).catch(() => null) : undefined;
|
|
464
|
+
this.options.assertCurrent();
|
|
465
|
+
if (inspection === null)
|
|
466
|
+
continue;
|
|
467
|
+
const verified = inspection?.code === 0 && info.commandIdentity.every(value => inspection.stdout.includes(value)) && inspection.stdout.includes(row.startIdentity);
|
|
468
|
+
this.update(row, verified ? 'running' : 'reconciling', {
|
|
469
|
+
id: row.id, session: row.session, root, kind: 'detached', state: verified ? 'running' : 'interrupted',
|
|
470
|
+
command: boundedText(info.command, 16384), startedAt: info.startedAt, controllable: verified, outputOffset: 0,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async stop(root, id, signal = 'SIGTERM') {
|
|
475
|
+
this.options.assertCurrent();
|
|
476
|
+
const row = this.options.store.resource(id);
|
|
477
|
+
if (!row || row.root !== root)
|
|
478
|
+
throw new Error('Process not found in this root session.');
|
|
479
|
+
const live = this.live.get(id);
|
|
480
|
+
if (live) {
|
|
481
|
+
await live.process.terminate(signal);
|
|
482
|
+
await live.process.read(0, { maxBytes: 0 });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
await this.refresh(root);
|
|
486
|
+
this.options.assertCurrent();
|
|
487
|
+
const runtime = this.runtime(row.session, root) ?? this.runtime(root, root);
|
|
488
|
+
if (!runtime || !this.views.get(id)?.controllable || row.kind !== 'detached')
|
|
489
|
+
throw new Error('Process identity is missing or unverified.');
|
|
490
|
+
const adapter = this.options.processAdapter;
|
|
491
|
+
if (!adapter)
|
|
492
|
+
throw processFault('unavailable', 'Detached process control is unavailable.');
|
|
493
|
+
await adapter.stopDetached(runtime, id, signal);
|
|
494
|
+
await this.refresh(root);
|
|
495
|
+
}
|
|
496
|
+
/** Settles a root's live handles and operations; `keepTerminals` leaves PTYs running for the root's next runtime. */
|
|
497
|
+
async settle(root, fenced = false, sessions, keepTerminals = false) {
|
|
498
|
+
const owns = (scope) => scope.root === root && (!sessions || sessions.has(scope.session));
|
|
499
|
+
const settles = (item) => owns(item.view) && !item.disposed && !(keepTerminals && item.view.kind === 'pty');
|
|
500
|
+
for (const lifetime of this.lifetimes)
|
|
501
|
+
if (owns(lifetime.scope))
|
|
502
|
+
lifetime.draining = true;
|
|
503
|
+
const failures = [];
|
|
504
|
+
const join = async (operations) => {
|
|
505
|
+
const results = await Promise.allSettled(operations);
|
|
506
|
+
for (const result of results)
|
|
507
|
+
if (result.status === 'rejected')
|
|
508
|
+
failures.push(result.reason);
|
|
509
|
+
};
|
|
510
|
+
for (;;) {
|
|
511
|
+
// Terminate first: a pending foreground read may need process exit to unblock.
|
|
512
|
+
const handles = [...this.live.values()].filter(settles);
|
|
513
|
+
await join(handles.map(async (item) => {
|
|
514
|
+
if (!item.settled)
|
|
515
|
+
await item.raw.terminate('SIGTERM');
|
|
516
|
+
}));
|
|
517
|
+
const pending = [...this.operations].filter(([, { scope }]) => owns(scope)).map(([operation]) => operation);
|
|
518
|
+
await join(pending);
|
|
519
|
+
await join(handles.map(async (item) => {
|
|
520
|
+
const snapshot = await item.raw.read(0, { maxBytes: 0 });
|
|
521
|
+
if (snapshot.running)
|
|
522
|
+
throw new Error('A live process did not settle.');
|
|
523
|
+
item.settled = true;
|
|
524
|
+
if (!fenced) {
|
|
525
|
+
item.view = { ...item.view, state: 'exited', controllable: false, outputOffset: snapshot.nextOffset, ...(snapshot.exitCode === undefined ? {} : { exitCode: snapshot.exitCode }) };
|
|
526
|
+
const row = this.options.store.resource(item.view.id);
|
|
527
|
+
this.update(row, 'exited', item.view);
|
|
528
|
+
}
|
|
529
|
+
await item.raw.dispose();
|
|
530
|
+
item.disposed = true;
|
|
531
|
+
}));
|
|
532
|
+
if (failures.length)
|
|
533
|
+
throw new AggregateError(failures, 'Owned processes did not settle.');
|
|
534
|
+
if (![...this.operations.values()].some(({ scope }) => owns(scope)) && ![...this.live.values()].some(settles))
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
start(lifetime, kind, command, create, requestedId, transport = false) {
|
|
539
|
+
const scope = lifetime.scope;
|
|
540
|
+
return this.track((async () => {
|
|
541
|
+
this.assertLifetime(lifetime, true);
|
|
542
|
+
const id = requestedId ?? randomUUID();
|
|
543
|
+
const previous = this.options.store.resource(id);
|
|
544
|
+
if (this.live.has(id) || this.starting.has(id) || previous && (previous.session !== scope.session || previous.root !== scope.root || previous.kind !== kind))
|
|
545
|
+
throw new Error('Process identity is already registered.');
|
|
546
|
+
this.starting.add(id);
|
|
547
|
+
const row = previous ?? { id, ...scope, kind, state: 'running', updatedAt: this.now() };
|
|
548
|
+
this.update(row, 'running', { id, ...scope, kind, state: 'running', command: boundedText(command, 16384), startedAt: this.now(), controllable: false, outputOffset: 0 });
|
|
549
|
+
let process;
|
|
550
|
+
try {
|
|
551
|
+
process = await create();
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
this.update(row, 'missing');
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
this.starting.delete(id);
|
|
559
|
+
}
|
|
560
|
+
try {
|
|
561
|
+
this.options.assertCurrent();
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
await process.terminate('SIGTERM');
|
|
565
|
+
await process.dispose();
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
const view = { id, ...scope, kind, state: 'running', command: boundedText(command, 16384), startedAt: this.now(), controllable: true, outputOffset: 0 };
|
|
569
|
+
const live = { raw: process, process, view, transport, settled: false, disposed: false };
|
|
570
|
+
const wrapped = new Proxy(process, { get: (target, key) => {
|
|
571
|
+
const current = () => { this.options.assertCurrent(); if (live.disposed)
|
|
572
|
+
throw new Error('Process handle is disposed.'); };
|
|
573
|
+
if (key === 'read' || transport && (key === 'readStdout' || key === 'readStderr'))
|
|
574
|
+
return (...args) => {
|
|
575
|
+
if (live.disposed)
|
|
576
|
+
return Promise.resolve({ output: new Uint8Array(), nextOffset: live.view.outputOffset, running: false,
|
|
577
|
+
...(live.view.exitCode === undefined ? {} : { exitCode: live.view.exitCode }), ...(live.signal ? { signal: live.signal } : {}) });
|
|
578
|
+
return this.track((async () => {
|
|
579
|
+
current();
|
|
580
|
+
const read = Reflect.get(target, key, target);
|
|
581
|
+
const snapshot = await read.apply(target, args);
|
|
582
|
+
this.options.assertCurrent();
|
|
583
|
+
this.record(live, row, snapshot, key === 'read');
|
|
584
|
+
return { ...snapshot, running: !live.settled, ...(live.view.exitCode === undefined ? {} : { exitCode: live.view.exitCode }), ...(!snapshot.running && live.signal ? { signal: live.signal } : {}) };
|
|
585
|
+
})(), scope, transport);
|
|
586
|
+
};
|
|
587
|
+
if (key === 'terminate')
|
|
588
|
+
return (signal = 'SIGTERM') => {
|
|
589
|
+
if (live.disposed)
|
|
590
|
+
return Promise.resolve();
|
|
591
|
+
current();
|
|
592
|
+
live.signal = signal;
|
|
593
|
+
return this.track(target.terminate(signal).catch(error => { delete live.signal; throw error; }), scope);
|
|
594
|
+
};
|
|
595
|
+
const value = Reflect.get(target, key, target);
|
|
596
|
+
if (typeof value !== 'function')
|
|
597
|
+
return value;
|
|
598
|
+
return (...args) => {
|
|
599
|
+
if (live.disposed && (key === 'dispose' || key === 'terminate' || key === 'closeInput'))
|
|
600
|
+
return Promise.resolve();
|
|
601
|
+
current();
|
|
602
|
+
// A PTY outlives its starting runtime across reload; its callers check their own current lifetime.
|
|
603
|
+
if ((key === 'write' || key === 'resize') && kind !== 'pty')
|
|
604
|
+
this.assertLifetime(lifetime);
|
|
605
|
+
return this.track(Promise.resolve(Reflect.apply(value, target, args)).then(result => {
|
|
606
|
+
this.options.assertCurrent();
|
|
607
|
+
if (key === 'dispose')
|
|
608
|
+
live.disposed = true;
|
|
609
|
+
return result;
|
|
610
|
+
}), scope);
|
|
611
|
+
};
|
|
612
|
+
} });
|
|
613
|
+
live.process = wrapped;
|
|
614
|
+
this.live.set(id, live);
|
|
615
|
+
this.update(row, 'running', view);
|
|
616
|
+
return wrapped;
|
|
617
|
+
})(), scope);
|
|
618
|
+
}
|
|
619
|
+
describeJob(runtime, path, bytes) {
|
|
620
|
+
const adapter = this.options.processAdapter;
|
|
621
|
+
if (!adapter || !pathInside(adapter.storageDirectory(runtime), path))
|
|
622
|
+
return undefined;
|
|
623
|
+
const info = adapter.describeJob(runtime, path, bytes);
|
|
624
|
+
if (info && !pathInside(adapter.storageDirectory(runtime), info.outputPath))
|
|
625
|
+
throw new Error('Process output is outside its root storage.');
|
|
626
|
+
return info;
|
|
627
|
+
}
|
|
628
|
+
observeJob(path, bytes, runtime, scope) {
|
|
629
|
+
const absolute = resolve(runtime.storage('session').root, path);
|
|
630
|
+
const directory = this.options.processAdapter?.storageDirectory(runtime);
|
|
631
|
+
if (!directory || !pathInside(directory, absolute))
|
|
632
|
+
return;
|
|
633
|
+
const info = this.describeJob(runtime, absolute, bytes);
|
|
634
|
+
if (!info)
|
|
635
|
+
return;
|
|
636
|
+
const previous = this.options.store.resource(info.id);
|
|
637
|
+
const row = previous ?? { id: info.id, ...scope, kind: info.kind, state: 'running', reference: absolute,
|
|
638
|
+
...(info.startIdentity ? { startIdentity: info.startIdentity } : {}), updatedAt: this.now() };
|
|
639
|
+
if (previous && (previous.root !== scope.root || previous.reference !== absolute || previous.startIdentity !== info.startIdentity))
|
|
640
|
+
throw new Error('Background job identity changed.');
|
|
641
|
+
this.update(row, info.status === 'running' ? 'running' : info.status === 'unknown' ? 'missing' : 'exited', {
|
|
642
|
+
id: info.id, session: row.session, root: row.root, kind: row.kind, state: info.status === 'running' ? 'running' : info.status === 'unknown' ? 'missing' : 'exited',
|
|
643
|
+
command: boundedText(info.command, 16384), startedAt: info.startedAt, controllable: this.live.has(info.id), outputOffset: 0,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
record(live, row, snapshot, advance) {
|
|
647
|
+
live.settled ||= !snapshot.running;
|
|
648
|
+
live.view = { ...live.view, state: live.settled ? 'exited' : 'running', controllable: !live.settled, outputOffset: advance ? Math.max(live.view.outputOffset, snapshot.nextOffset) : live.view.outputOffset, ...(snapshot.exitCode === undefined ? {} : { exitCode: snapshot.exitCode }) };
|
|
649
|
+
this.update(row, live.view.state, live.view);
|
|
650
|
+
}
|
|
651
|
+
/** Records an exit nobody has read yet, e.g. a PTY kept across reload; a failed probe leaves the handle as it was. */
|
|
652
|
+
async observe(live) {
|
|
653
|
+
if (live.settled || live.disposed)
|
|
654
|
+
return;
|
|
655
|
+
const snapshot = await this.track(live.raw.read(live.view.outputOffset, { maxBytes: 0 }), live.view, true).catch(() => undefined);
|
|
656
|
+
this.options.assertCurrent();
|
|
657
|
+
const row = this.options.store.resource(live.view.id);
|
|
658
|
+
if (!snapshot || !row || this.live.get(live.view.id) !== live || live.disposed)
|
|
659
|
+
return;
|
|
660
|
+
this.record(live, row, snapshot, false);
|
|
661
|
+
}
|
|
662
|
+
update(row, state, view) {
|
|
663
|
+
this.options.assertCurrent();
|
|
664
|
+
const old = this.options.store.resource(row.id);
|
|
665
|
+
const known = view ?? this.views.get(row.id);
|
|
666
|
+
const next = known ? { ...known, state: state === 'reconciling' ? 'interrupted' : state, controllable: state === 'running' && known.controllable }
|
|
667
|
+
: { id: row.id, session: row.session, root: row.root, kind: row.kind, state: state === 'reconciling' ? 'interrupted' : state,
|
|
668
|
+
command: '', startedAt: row.updatedAt, controllable: false, outputOffset: 0 };
|
|
669
|
+
if (!old || old.state !== state || JSON.stringify(this.views.get(row.id)) !== JSON.stringify(next)) {
|
|
670
|
+
const commit = this.options.store.putResource({ ...row, state });
|
|
671
|
+
this.views.set(row.id, next);
|
|
672
|
+
this.options.changed(row.session, next, commit.revisions[row.session]);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
track(operation, scope, passive = false) {
|
|
676
|
+
this.operations.set(operation, { scope, passive });
|
|
677
|
+
void operation.then(() => this.operations.delete(operation), () => this.operations.delete(operation));
|
|
678
|
+
return operation;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
function processFault(code, message) { return Object.assign(new Error(message), { code }); }
|
|
682
|
+
function splitShellOutput(value) {
|
|
683
|
+
const start = Math.max(0, value.length - 256);
|
|
684
|
+
const tail = value.slice(start);
|
|
685
|
+
const osc = tail.lastIndexOf('\x1b]');
|
|
686
|
+
if (osc >= 0 && !/[\x07\x9c]|\x1b\\/u.test(tail.slice(osc + 2)))
|
|
687
|
+
return { complete: value.slice(0, start + osc), pending: tail.slice(osc) };
|
|
688
|
+
const incomplete = /(?:\x1b(?:\[[0-?]*[ -/]*|[()][ -/]*)?|\x9b[0-?]*[ -/]*)$/u.exec(tail);
|
|
689
|
+
return incomplete ? { complete: value.slice(0, start + incomplete.index), pending: incomplete[0] } : { complete: value, pending: '' };
|
|
690
|
+
}
|
|
691
|
+
//# sourceMappingURL=processes.js.map
|