@actiondock/core 2.2.2 → 2.3.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/README.md +8 -3
- package/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +6 -0
- package/dist/app/types.d.ts +12 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.js +44 -0
- package/dist/execution/service.d.ts +2 -0
- package/dist/execution/service.js +13 -2
- package/dist/execution/types.d.ts +17 -0
- package/dist/host/host.js +46 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ipc/host.d.ts +5 -0
- package/dist/ipc/host.js +43 -1
- package/dist/ipc/target.d.ts +18 -0
- package/dist/ipc/target.js +74 -6
- package/dist/ipc/types.d.ts +10 -1
- package/dist/platform/default.d.ts +7 -1
- package/dist/platform/default.js +40 -2
- package/dist/process/context-process.d.ts +83 -0
- package/dist/process/context-process.js +168 -0
- package/dist/process/cursor.d.ts +52 -0
- package/dist/process/cursor.js +144 -0
- package/dist/process/driver.d.ts +214 -0
- package/dist/process/driver.js +197 -0
- package/dist/process/index.d.ts +6 -0
- package/dist/process/index.js +6 -0
- package/dist/process/metadata-store.d.ts +205 -0
- package/dist/process/metadata-store.js +462 -0
- package/dist/process/output-log.d.ts +136 -0
- package/dist/process/output-log.js +331 -0
- package/dist/process/process-manager.d.ts +279 -0
- package/dist/process/process-manager.js +1879 -0
- package/dist/profile/client.js +63 -21
- package/dist/project/init.js +2 -2
- package/dist/registry/registry.js +12 -3
- package/dist/runtime/context.d.ts +9 -6
- package/dist/runtime/context.js +31 -8
- package/dist/runtime/process.d.ts +19 -2
- package/dist/runtime/process.js +103 -131
- package/dist/runtime/runner.d.ts +45 -24
- package/dist/runtime/runner.js +106 -25
- package/dist/target/remote.js +7 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { createDefaultSqliteDriver } from "../storage/driver.js";
|
|
4
|
+
/**
|
|
5
|
+
* 解析分页游标偏移量。
|
|
6
|
+
*/
|
|
7
|
+
function parsePageToken(pageToken) {
|
|
8
|
+
if (!pageToken) {
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
const parsed = parseInt(pageToken, 10);
|
|
12
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 内存型受管进程元数据存储实现。
|
|
19
|
+
* 适用于确定性测试与无持久化文件系统运行场景。
|
|
20
|
+
*/
|
|
21
|
+
export class MemoryProcessMetadataStore {
|
|
22
|
+
processes = new Map();
|
|
23
|
+
requests = new Map();
|
|
24
|
+
formatRequestKey(key) {
|
|
25
|
+
return `${key.hostEpoch}:${key.scope}:${key.processId ?? ""}:${key.requestId}`;
|
|
26
|
+
}
|
|
27
|
+
async saveProcess(process) {
|
|
28
|
+
const createdAt = process.createdAt ?? new Date().toISOString();
|
|
29
|
+
const ctrl = process.controlState ?? process.control;
|
|
30
|
+
const cloned = {
|
|
31
|
+
...process,
|
|
32
|
+
createdAt,
|
|
33
|
+
controlState: ctrl,
|
|
34
|
+
control: ctrl,
|
|
35
|
+
outputClosed: Boolean(process.outputClosed),
|
|
36
|
+
inputClosed: Boolean(process.inputClosed),
|
|
37
|
+
};
|
|
38
|
+
this.processes.set(process.processId, cloned);
|
|
39
|
+
}
|
|
40
|
+
async getProcess(processId) {
|
|
41
|
+
const record = this.processes.get(processId);
|
|
42
|
+
if (!record) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return { ...record };
|
|
46
|
+
}
|
|
47
|
+
async listProcesses(owner, pageToken, limit) {
|
|
48
|
+
const actualLimit = typeof limit === "number" && limit > 0 ? limit : 50;
|
|
49
|
+
const offset = parsePageToken(pageToken);
|
|
50
|
+
const matched = Array.from(this.processes.values()).filter((p) => p.tenantId === owner.tenantId &&
|
|
51
|
+
p.principalId === owner.principalId &&
|
|
52
|
+
p.packageInstanceId === owner.packageInstanceId &&
|
|
53
|
+
p.generationId === owner.generationId);
|
|
54
|
+
matched.sort((a, b) => {
|
|
55
|
+
const timeA = new Date(a.createdAt || 0).getTime();
|
|
56
|
+
const timeB = new Date(b.createdAt || 0).getTime();
|
|
57
|
+
if (timeA !== timeB) {
|
|
58
|
+
return timeB - timeA;
|
|
59
|
+
}
|
|
60
|
+
return a.processId.localeCompare(b.processId);
|
|
61
|
+
});
|
|
62
|
+
const sliced = matched.slice(offset, offset + actualLimit + 1);
|
|
63
|
+
const hasMore = sliced.length > actualLimit;
|
|
64
|
+
const resultRows = hasMore ? sliced.slice(0, actualLimit) : sliced;
|
|
65
|
+
const processes = resultRows.map((p) => ({ ...p }));
|
|
66
|
+
const nextPageToken = hasMore ? String(offset + actualLimit) : undefined;
|
|
67
|
+
return { processes, nextPageToken };
|
|
68
|
+
}
|
|
69
|
+
async updateProcessState(processId, patch) {
|
|
70
|
+
const target = this.processes.get(processId);
|
|
71
|
+
if (!target) {
|
|
72
|
+
throw new Error(`Process '${processId}' not found`);
|
|
73
|
+
}
|
|
74
|
+
const ctrl = patch.controlState !== undefined ? patch.controlState : patch.control;
|
|
75
|
+
const updated = {
|
|
76
|
+
...target,
|
|
77
|
+
...patch,
|
|
78
|
+
controlState: ctrl !== undefined ? ctrl : target.controlState,
|
|
79
|
+
control: ctrl !== undefined ? ctrl : target.control,
|
|
80
|
+
};
|
|
81
|
+
this.processes.set(processId, updated);
|
|
82
|
+
}
|
|
83
|
+
async recordRequest(key, receipt, payloadHash) {
|
|
84
|
+
const k = this.formatRequestKey(key);
|
|
85
|
+
this.requests.set(k, {
|
|
86
|
+
receipt: JSON.parse(JSON.stringify(receipt)),
|
|
87
|
+
payloadHash,
|
|
88
|
+
createdAt: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async getRequest(key) {
|
|
92
|
+
const k = this.formatRequestKey(key);
|
|
93
|
+
const item = this.requests.get(k);
|
|
94
|
+
if (!item) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
receipt: JSON.parse(JSON.stringify(item.receipt)),
|
|
99
|
+
payloadHash: item.payloadHash,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async initializeHost(hostEpoch) {
|
|
103
|
+
let recoveredCount = 0;
|
|
104
|
+
for (const [id, proc] of Array.from(this.processes.entries())) {
|
|
105
|
+
if (proc.hostEpoch !== hostEpoch &&
|
|
106
|
+
(proc.state === "starting" || proc.state === "running" || proc.state === "stopping")) {
|
|
107
|
+
this.processes.set(id, {
|
|
108
|
+
...proc,
|
|
109
|
+
state: "lost",
|
|
110
|
+
controlState: "closed",
|
|
111
|
+
control: "closed",
|
|
112
|
+
endReason: "host-lost",
|
|
113
|
+
outputClosed: true,
|
|
114
|
+
outputEndReason: "host-lost",
|
|
115
|
+
});
|
|
116
|
+
recoveredCount += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return recoveredCount;
|
|
120
|
+
}
|
|
121
|
+
clear() {
|
|
122
|
+
this.processes.clear();
|
|
123
|
+
this.requests.clear();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 将数据库行反序列化为进程模型。
|
|
128
|
+
*/
|
|
129
|
+
function mapRowToStoredProcess(row) {
|
|
130
|
+
return {
|
|
131
|
+
processId: row.process_id,
|
|
132
|
+
tenantId: row.tenant_id,
|
|
133
|
+
principalId: row.principal_id,
|
|
134
|
+
packageInstanceId: row.package_instance_id,
|
|
135
|
+
generationId: row.generation_id,
|
|
136
|
+
hostEpoch: row.host_epoch,
|
|
137
|
+
state: row.state,
|
|
138
|
+
controlState: row.control_state ?? undefined,
|
|
139
|
+
control: row.control_state ?? undefined,
|
|
140
|
+
ioConfig: row.io_config_json ? JSON.parse(row.io_config_json) : undefined,
|
|
141
|
+
capabilities: row.capabilities_json ? JSON.parse(row.capabilities_json) : undefined,
|
|
142
|
+
createdAt: row.created_at,
|
|
143
|
+
exitCode: row.exit_code !== null && row.exit_code !== undefined ? Number(row.exit_code) : undefined,
|
|
144
|
+
exitSignal: row.exit_signal ?? undefined,
|
|
145
|
+
endReason: row.end_reason ?? undefined,
|
|
146
|
+
outputClosed: Boolean(row.output_closed),
|
|
147
|
+
outputEndReason: row.output_end_reason ?? undefined,
|
|
148
|
+
inputClosed: Boolean(row.input_closed),
|
|
149
|
+
effectiveLimits: row.effective_limits_json ? JSON.parse(row.effective_limits_json) : undefined,
|
|
150
|
+
startRequestId: row.start_request_id ?? undefined,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 基于 SQLite 的受管进程元数据持久化存储实现。
|
|
155
|
+
*/
|
|
156
|
+
export class SqliteProcessMetadataStore {
|
|
157
|
+
driver;
|
|
158
|
+
statementCache = new Map();
|
|
159
|
+
isClosed = false;
|
|
160
|
+
constructor(options = {}) {
|
|
161
|
+
const dbPath = options.dbPath || ":memory:";
|
|
162
|
+
if (dbPath !== ":memory:") {
|
|
163
|
+
const dir = dirname(dbPath);
|
|
164
|
+
if (!existsSync(dir)) {
|
|
165
|
+
try {
|
|
166
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
167
|
+
chmodSync(dir, 0o700);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// 忽略系统权限配置失败
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.driver = options.driver ?? createDefaultSqliteDriver(dbPath);
|
|
175
|
+
if (dbPath !== ":memory:" && existsSync(dbPath)) {
|
|
176
|
+
try {
|
|
177
|
+
chmodSync(dbPath, 0o600);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// 忽略文件权限配置异常
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
this.initTables();
|
|
184
|
+
}
|
|
185
|
+
initTables() {
|
|
186
|
+
this.driver.exec("PRAGMA journal_mode = WAL;");
|
|
187
|
+
this.driver.exec("PRAGMA synchronous = NORMAL;");
|
|
188
|
+
this.driver.exec(`
|
|
189
|
+
CREATE TABLE IF NOT EXISTS managed_processes (
|
|
190
|
+
process_id TEXT PRIMARY KEY,
|
|
191
|
+
tenant_id TEXT NOT NULL,
|
|
192
|
+
principal_id TEXT NOT NULL,
|
|
193
|
+
package_instance_id TEXT NOT NULL,
|
|
194
|
+
generation_id TEXT NOT NULL,
|
|
195
|
+
host_epoch TEXT NOT NULL,
|
|
196
|
+
state TEXT NOT NULL,
|
|
197
|
+
control_state TEXT,
|
|
198
|
+
io_config_json TEXT,
|
|
199
|
+
capabilities_json TEXT,
|
|
200
|
+
created_at TEXT NOT NULL,
|
|
201
|
+
exit_code INTEGER,
|
|
202
|
+
exit_signal TEXT,
|
|
203
|
+
end_reason TEXT,
|
|
204
|
+
output_closed INTEGER NOT NULL DEFAULT 0,
|
|
205
|
+
output_end_reason TEXT,
|
|
206
|
+
input_closed INTEGER NOT NULL DEFAULT 0,
|
|
207
|
+
effective_limits_json TEXT,
|
|
208
|
+
start_request_id TEXT
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
CREATE INDEX IF NOT EXISTS idx_processes_owner_created
|
|
212
|
+
ON managed_processes(tenant_id, principal_id, package_instance_id, generation_id, created_at DESC, process_id ASC);
|
|
213
|
+
|
|
214
|
+
CREATE INDEX IF NOT EXISTS idx_processes_host_state
|
|
215
|
+
ON managed_processes(host_epoch, state);
|
|
216
|
+
|
|
217
|
+
CREATE INDEX IF NOT EXISTS idx_processes_start_req
|
|
218
|
+
ON managed_processes(start_request_id);
|
|
219
|
+
|
|
220
|
+
CREATE TABLE IF NOT EXISTS process_requests (
|
|
221
|
+
host_epoch TEXT NOT NULL,
|
|
222
|
+
scope TEXT NOT NULL,
|
|
223
|
+
process_id TEXT NOT NULL DEFAULT '',
|
|
224
|
+
request_id TEXT NOT NULL,
|
|
225
|
+
receipt_json TEXT NOT NULL,
|
|
226
|
+
payload_hash TEXT,
|
|
227
|
+
created_at TEXT NOT NULL,
|
|
228
|
+
PRIMARY KEY (host_epoch, scope, process_id, request_id)
|
|
229
|
+
);
|
|
230
|
+
`);
|
|
231
|
+
this.migrateSchema();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 旧库结构迁移:逐列探测并补充新增字段,重复执行安全(幂等)。
|
|
235
|
+
*/
|
|
236
|
+
migrateSchema() {
|
|
237
|
+
const columns = new Set();
|
|
238
|
+
for (const row of this.listTableColumns("managed_processes")) {
|
|
239
|
+
columns.add(String(row.name));
|
|
240
|
+
}
|
|
241
|
+
if (!columns.has("input_closed")) {
|
|
242
|
+
this.driver.exec("ALTER TABLE managed_processes ADD COLUMN input_closed INTEGER NOT NULL DEFAULT 0;");
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* 枚举指定表的全部列定义。
|
|
247
|
+
*/
|
|
248
|
+
listTableColumns(tableName) {
|
|
249
|
+
try {
|
|
250
|
+
const stmt = this.driver.prepare(`PRAGMA table_info(${tableName})`);
|
|
251
|
+
return stmt.all() ?? [];
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return [];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
getStatement(sql) {
|
|
258
|
+
let stmt = this.statementCache.get(sql);
|
|
259
|
+
if (!stmt) {
|
|
260
|
+
stmt = this.driver.prepare(sql);
|
|
261
|
+
this.statementCache.set(sql, stmt);
|
|
262
|
+
}
|
|
263
|
+
return stmt;
|
|
264
|
+
}
|
|
265
|
+
async saveProcess(process) {
|
|
266
|
+
if (this.isClosed) {
|
|
267
|
+
throw new Error("SqliteProcessMetadataStore is closed");
|
|
268
|
+
}
|
|
269
|
+
const stmt = this.getStatement(`
|
|
270
|
+
INSERT INTO managed_processes (
|
|
271
|
+
process_id, tenant_id, principal_id, package_instance_id, generation_id,
|
|
272
|
+
host_epoch, state, control_state, io_config_json, capabilities_json,
|
|
273
|
+
created_at, exit_code, exit_signal, end_reason, output_closed,
|
|
274
|
+
output_end_reason, input_closed, effective_limits_json, start_request_id
|
|
275
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
276
|
+
ON CONFLICT(process_id) DO UPDATE SET
|
|
277
|
+
tenant_id = excluded.tenant_id,
|
|
278
|
+
principal_id = excluded.principal_id,
|
|
279
|
+
package_instance_id = excluded.package_instance_id,
|
|
280
|
+
generation_id = excluded.generation_id,
|
|
281
|
+
host_epoch = excluded.host_epoch,
|
|
282
|
+
state = excluded.state,
|
|
283
|
+
control_state = excluded.control_state,
|
|
284
|
+
io_config_json = excluded.io_config_json,
|
|
285
|
+
capabilities_json = excluded.capabilities_json,
|
|
286
|
+
created_at = excluded.created_at,
|
|
287
|
+
exit_code = excluded.exit_code,
|
|
288
|
+
exit_signal = excluded.exit_signal,
|
|
289
|
+
end_reason = excluded.end_reason,
|
|
290
|
+
output_closed = excluded.output_closed,
|
|
291
|
+
output_end_reason = excluded.output_end_reason,
|
|
292
|
+
input_closed = excluded.input_closed,
|
|
293
|
+
effective_limits_json = excluded.effective_limits_json,
|
|
294
|
+
start_request_id = excluded.start_request_id
|
|
295
|
+
`);
|
|
296
|
+
const createdAt = process.createdAt ?? new Date().toISOString();
|
|
297
|
+
const ctrlState = process.controlState ?? process.control ?? null;
|
|
298
|
+
const ioConfigJson = process.ioConfig !== undefined ? JSON.stringify(process.ioConfig) : null;
|
|
299
|
+
const capabilitiesJson = process.capabilities !== undefined ? JSON.stringify(process.capabilities) : null;
|
|
300
|
+
const effectiveLimitsJson = process.effectiveLimits !== undefined ? JSON.stringify(process.effectiveLimits) : null;
|
|
301
|
+
const outputClosed = process.outputClosed ? 1 : 0;
|
|
302
|
+
const inputClosed = process.inputClosed ? 1 : 0;
|
|
303
|
+
stmt.run(process.processId, process.tenantId, process.principalId, process.packageInstanceId, process.generationId, process.hostEpoch, process.state, ctrlState, ioConfigJson, capabilitiesJson, createdAt, process.exitCode !== undefined && process.exitCode !== null ? process.exitCode : null, process.exitSignal !== undefined ? process.exitSignal : null, process.endReason !== undefined ? process.endReason : null, outputClosed, process.outputEndReason !== undefined ? process.outputEndReason : null, inputClosed, effectiveLimitsJson, process.startRequestId !== undefined ? process.startRequestId : null);
|
|
304
|
+
}
|
|
305
|
+
async getProcess(processId) {
|
|
306
|
+
if (this.isClosed) {
|
|
307
|
+
throw new Error("SqliteProcessMetadataStore is closed");
|
|
308
|
+
}
|
|
309
|
+
const stmt = this.getStatement("SELECT * FROM managed_processes WHERE process_id = ?");
|
|
310
|
+
const row = stmt.get(processId);
|
|
311
|
+
if (!row) {
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
return mapRowToStoredProcess(row);
|
|
315
|
+
}
|
|
316
|
+
async listProcesses(owner, pageToken, limit) {
|
|
317
|
+
if (this.isClosed) {
|
|
318
|
+
throw new Error("SqliteProcessMetadataStore is closed");
|
|
319
|
+
}
|
|
320
|
+
const actualLimit = typeof limit === "number" && limit > 0 ? limit : 50;
|
|
321
|
+
const offset = parsePageToken(pageToken);
|
|
322
|
+
const stmt = this.getStatement(`
|
|
323
|
+
SELECT * FROM managed_processes
|
|
324
|
+
WHERE tenant_id = ? AND principal_id = ? AND package_instance_id = ? AND generation_id = ?
|
|
325
|
+
ORDER BY created_at DESC, process_id ASC
|
|
326
|
+
LIMIT ? OFFSET ?
|
|
327
|
+
`);
|
|
328
|
+
const rows = stmt.all(owner.tenantId, owner.principalId, owner.packageInstanceId, owner.generationId, actualLimit + 1, offset);
|
|
329
|
+
const hasMore = rows.length > actualLimit;
|
|
330
|
+
const resultRows = hasMore ? rows.slice(0, actualLimit) : rows;
|
|
331
|
+
const processes = resultRows.map(mapRowToStoredProcess);
|
|
332
|
+
const nextPageToken = hasMore ? String(offset + actualLimit) : undefined;
|
|
333
|
+
return { processes, nextPageToken };
|
|
334
|
+
}
|
|
335
|
+
async updateProcessState(processId, patch) {
|
|
336
|
+
if (this.isClosed) {
|
|
337
|
+
throw new Error("SqliteProcessMetadataStore is closed");
|
|
338
|
+
}
|
|
339
|
+
const sets = [];
|
|
340
|
+
const params = [];
|
|
341
|
+
if (patch.state !== undefined) {
|
|
342
|
+
sets.push("state = ?");
|
|
343
|
+
params.push(patch.state);
|
|
344
|
+
}
|
|
345
|
+
const ctrl = patch.controlState !== undefined ? patch.controlState : patch.control;
|
|
346
|
+
if (ctrl !== undefined) {
|
|
347
|
+
sets.push("control_state = ?");
|
|
348
|
+
params.push(ctrl);
|
|
349
|
+
}
|
|
350
|
+
if (patch.exitCode !== undefined) {
|
|
351
|
+
sets.push("exit_code = ?");
|
|
352
|
+
params.push(patch.exitCode !== null ? patch.exitCode : null);
|
|
353
|
+
}
|
|
354
|
+
if (patch.exitSignal !== undefined) {
|
|
355
|
+
sets.push("exit_signal = ?");
|
|
356
|
+
params.push(patch.exitSignal);
|
|
357
|
+
}
|
|
358
|
+
if (patch.endReason !== undefined) {
|
|
359
|
+
sets.push("end_reason = ?");
|
|
360
|
+
params.push(patch.endReason);
|
|
361
|
+
}
|
|
362
|
+
if (patch.outputClosed !== undefined) {
|
|
363
|
+
sets.push("output_closed = ?");
|
|
364
|
+
params.push(patch.outputClosed ? 1 : 0);
|
|
365
|
+
}
|
|
366
|
+
if (patch.outputEndReason !== undefined) {
|
|
367
|
+
sets.push("output_end_reason = ?");
|
|
368
|
+
params.push(patch.outputEndReason);
|
|
369
|
+
}
|
|
370
|
+
if (patch.inputClosed !== undefined) {
|
|
371
|
+
sets.push("input_closed = ?");
|
|
372
|
+
params.push(patch.inputClosed ? 1 : 0);
|
|
373
|
+
}
|
|
374
|
+
if (patch.effectiveLimits !== undefined) {
|
|
375
|
+
sets.push("effective_limits_json = ?");
|
|
376
|
+
params.push(patch.effectiveLimits !== null ? JSON.stringify(patch.effectiveLimits) : null);
|
|
377
|
+
}
|
|
378
|
+
if (patch.ioConfig !== undefined) {
|
|
379
|
+
sets.push("io_config_json = ?");
|
|
380
|
+
params.push(patch.ioConfig !== null ? JSON.stringify(patch.ioConfig) : null);
|
|
381
|
+
}
|
|
382
|
+
if (patch.capabilities !== undefined) {
|
|
383
|
+
sets.push("capabilities_json = ?");
|
|
384
|
+
params.push(patch.capabilities !== null ? JSON.stringify(patch.capabilities) : null);
|
|
385
|
+
}
|
|
386
|
+
if (patch.hostEpoch !== undefined) {
|
|
387
|
+
sets.push("host_epoch = ?");
|
|
388
|
+
params.push(patch.hostEpoch);
|
|
389
|
+
}
|
|
390
|
+
if (sets.length === 0) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
params.push(processId);
|
|
394
|
+
const sql = `UPDATE managed_processes SET ${sets.join(", ")} WHERE process_id = ?`;
|
|
395
|
+
const stmt = this.getStatement(sql);
|
|
396
|
+
const res = stmt.run(...params);
|
|
397
|
+
if (res.changes === 0) {
|
|
398
|
+
throw new Error(`Process '${processId}' not found`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async recordRequest(key, receipt, payloadHash) {
|
|
402
|
+
if (this.isClosed) {
|
|
403
|
+
throw new Error("SqliteProcessMetadataStore is closed");
|
|
404
|
+
}
|
|
405
|
+
const stmt = this.getStatement(`
|
|
406
|
+
INSERT INTO process_requests (
|
|
407
|
+
host_epoch, scope, process_id, request_id, receipt_json, payload_hash, created_at
|
|
408
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
409
|
+
ON CONFLICT(host_epoch, scope, process_id, request_id) DO UPDATE SET
|
|
410
|
+
receipt_json = excluded.receipt_json,
|
|
411
|
+
payload_hash = excluded.payload_hash,
|
|
412
|
+
created_at = excluded.created_at
|
|
413
|
+
`);
|
|
414
|
+
const processId = key.processId ?? "";
|
|
415
|
+
const receiptJson = JSON.stringify(receipt);
|
|
416
|
+
const createdAt = new Date().toISOString();
|
|
417
|
+
stmt.run(key.hostEpoch, key.scope, processId, key.requestId, receiptJson, payloadHash ?? null, createdAt);
|
|
418
|
+
}
|
|
419
|
+
async getRequest(key) {
|
|
420
|
+
if (this.isClosed) {
|
|
421
|
+
throw new Error("SqliteProcessMetadataStore is closed");
|
|
422
|
+
}
|
|
423
|
+
const stmt = this.getStatement(`
|
|
424
|
+
SELECT receipt_json, payload_hash FROM process_requests
|
|
425
|
+
WHERE host_epoch = ? AND scope = ? AND process_id = ? AND request_id = ?
|
|
426
|
+
`);
|
|
427
|
+
const processId = key.processId ?? "";
|
|
428
|
+
const row = stmt.get(key.hostEpoch, key.scope, processId, key.requestId);
|
|
429
|
+
if (!row) {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
receipt: JSON.parse(row.receipt_json),
|
|
434
|
+
payloadHash: row.payload_hash ?? undefined,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async initializeHost(hostEpoch) {
|
|
438
|
+
if (this.isClosed) {
|
|
439
|
+
return 0;
|
|
440
|
+
}
|
|
441
|
+
const stmt = this.getStatement(`
|
|
442
|
+
UPDATE managed_processes
|
|
443
|
+
SET state = 'lost',
|
|
444
|
+
control_state = 'closed',
|
|
445
|
+
end_reason = 'host-lost',
|
|
446
|
+
output_closed = 1,
|
|
447
|
+
output_end_reason = 'host-lost'
|
|
448
|
+
WHERE host_epoch != ?
|
|
449
|
+
AND state IN ('starting', 'running', 'stopping')
|
|
450
|
+
`);
|
|
451
|
+
const res = stmt.run(hostEpoch);
|
|
452
|
+
return res.changes;
|
|
453
|
+
}
|
|
454
|
+
close() {
|
|
455
|
+
if (this.isClosed) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
this.isClosed = true;
|
|
459
|
+
this.statementCache.clear();
|
|
460
|
+
this.driver.close();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 外部输出分块数据。
|
|
3
|
+
*/
|
|
4
|
+
export interface OutputChunk {
|
|
5
|
+
/** 输出流标识 */
|
|
6
|
+
stream: "stdout" | "stderr" | "pty";
|
|
7
|
+
/** 原始字节数据 */
|
|
8
|
+
data: Uint8Array;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 游标读取返回结果快照。
|
|
12
|
+
*/
|
|
13
|
+
export interface OutputReadResult {
|
|
14
|
+
/** 本次读取到的数据切片列表 */
|
|
15
|
+
chunks: OutputChunk[];
|
|
16
|
+
/** 下次读取应使用的推进游标 */
|
|
17
|
+
nextCursor: string;
|
|
18
|
+
/** 当前保留窗口的最早游标 */
|
|
19
|
+
earliestCursor: string;
|
|
20
|
+
/** 当前日志末尾的最新游标 */
|
|
21
|
+
tailCursor: string;
|
|
22
|
+
/** 是否由于淘汰跳过了缺口数据 */
|
|
23
|
+
truncated: boolean;
|
|
24
|
+
/** 发生缺口跳过时的跨度信息 */
|
|
25
|
+
gap?: {
|
|
26
|
+
fromCursor: string;
|
|
27
|
+
toCursor: string;
|
|
28
|
+
};
|
|
29
|
+
/** 是否已在输出通道关闭后读尽全部数据 */
|
|
30
|
+
eof: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 输出日志配置选项。
|
|
34
|
+
*/
|
|
35
|
+
export interface ProcessOutputLogOptions {
|
|
36
|
+
/** 缓冲区最大保留字节数,默认 4 MiB */
|
|
37
|
+
maxBufferBytes?: number;
|
|
38
|
+
/** 单个进程最大并发等待者上限,默认 8 */
|
|
39
|
+
maxWaiters?: number;
|
|
40
|
+
/** 初始日志序号,默认 0 */
|
|
41
|
+
initialSequence?: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 读取长轮询等待配置选项。
|
|
45
|
+
*/
|
|
46
|
+
export interface OutputReadOptions {
|
|
47
|
+
/** 单次读取最大原始字节数,默认 64 KiB */
|
|
48
|
+
maxBytes?: number;
|
|
49
|
+
/** 遇到淘汰缺口时的处理策略,error 抛出异常,skip 自动跳过缺口 */
|
|
50
|
+
onGap?: "error" | "skip";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 基于内存有界环形缓冲区的受管进程原始字节输出日志。
|
|
54
|
+
*
|
|
55
|
+
* 职责与不变量:
|
|
56
|
+
* - 保留原始字节流,不做字符集猜测与跨流重排序。
|
|
57
|
+
* - 维护单调推进的游标系统,支持按记录切分与不透明游标寻址。
|
|
58
|
+
* - 缓冲区超额时淘汰最旧数据并更新最早保留游标。
|
|
59
|
+
* - 长轮询等待机制保证状态检查与等待者注册处于同一同步边界,杜绝漏唤醒。
|
|
60
|
+
* - 严格遵循等待者配额,超额抛出配额超限异常。
|
|
61
|
+
*/
|
|
62
|
+
export declare class ProcessOutputLog {
|
|
63
|
+
readonly hostEpoch: string;
|
|
64
|
+
readonly processId: string;
|
|
65
|
+
readonly maxBufferBytes: number;
|
|
66
|
+
readonly maxWaiters: number;
|
|
67
|
+
private records;
|
|
68
|
+
private totalBytes;
|
|
69
|
+
private nextSequence;
|
|
70
|
+
private earliestCursorState;
|
|
71
|
+
private tailCursorState;
|
|
72
|
+
private outputClosedState;
|
|
73
|
+
private outputEndReasonState?;
|
|
74
|
+
private waiters;
|
|
75
|
+
constructor(hostEpoch: string, processId: string, options?: ProcessOutputLogOptions);
|
|
76
|
+
/**
|
|
77
|
+
* 当前保留日志的最早可用游标。
|
|
78
|
+
*/
|
|
79
|
+
get earliestCursor(): string;
|
|
80
|
+
/**
|
|
81
|
+
* 当前日志尾部的下一写入游标。
|
|
82
|
+
*/
|
|
83
|
+
get tailCursor(): string;
|
|
84
|
+
/**
|
|
85
|
+
* 输出通道是否已关闭。
|
|
86
|
+
*/
|
|
87
|
+
get outputClosed(): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* 输出通道关闭原因。
|
|
90
|
+
*/
|
|
91
|
+
get outputEndReason(): "natural" | "drain-timeout" | "host-lost" | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* 当前缓冲区占用的原始字节总数。
|
|
94
|
+
*/
|
|
95
|
+
get currentBytes(): number;
|
|
96
|
+
/**
|
|
97
|
+
* 当前正在挂起等待的读取者数量。
|
|
98
|
+
*/
|
|
99
|
+
get waiterCount(): number;
|
|
100
|
+
/**
|
|
101
|
+
* 向输出日志追加新的原始字节数据。
|
|
102
|
+
*
|
|
103
|
+
* 行为约束:
|
|
104
|
+
* - 记录新数据并更新日志尾部游标。
|
|
105
|
+
* - 若总字节数超出缓冲区上限,淘汰最旧记录并更新最早保留游标。
|
|
106
|
+
* - 同步唤醒所有挂起等待者。
|
|
107
|
+
*/
|
|
108
|
+
append(stream: "stdout" | "stderr" | "pty", data: Uint8Array): void;
|
|
109
|
+
/**
|
|
110
|
+
* 标记输出通道关闭并唤醒所有等待者。
|
|
111
|
+
*/
|
|
112
|
+
closeOutput(reason?: "natural" | "drain-timeout" | "host-lost"): void;
|
|
113
|
+
/**
|
|
114
|
+
* 从指定游标处读取输出数据。
|
|
115
|
+
*
|
|
116
|
+
* 行为约束:
|
|
117
|
+
* - 校验游标是否超出当前日志末尾,超出则抛出 INVALID_CURSOR 异常。
|
|
118
|
+
* - 检查游标是否落后于最早保留游标:
|
|
119
|
+
* - 若落后且 onGap 为 error,抛出 OUTPUT_GAP 异常并附带当前最早游标。
|
|
120
|
+
* - 若落后且 onGap 为 skip,从最早可用游标开始读取,标记 truncated 为 true 并提供缺口范围。
|
|
121
|
+
* - 支持记录切分并生成带有内部偏移量的不透明推进游标。
|
|
122
|
+
*/
|
|
123
|
+
read(cursor: string, maxBytes?: number, onGap?: "error" | "skip"): OutputReadResult;
|
|
124
|
+
/**
|
|
125
|
+
* 长轮询等待输出数据或通道关闭。
|
|
126
|
+
*
|
|
127
|
+
* 行为约束:
|
|
128
|
+
* - 状态检查与等待者登记位于同一同步边界,杜绝漏唤醒竞态。
|
|
129
|
+
* - 若已有数据、通道已关闭或超时时长小于等于 0,立即返回读取结果。
|
|
130
|
+
* - 严格校验等待者配额,超额抛出 QUOTA_EXCEEDED 异常。
|
|
131
|
+
* - 外部信号中止时立即清理等待者并不修改日志游标状态。
|
|
132
|
+
* - 等待超时且无新数据时返回空分块结果。
|
|
133
|
+
*/
|
|
134
|
+
waitForData(cursor: string, waitMs: number, signal?: AbortSignal, options?: OutputReadOptions): Promise<OutputReadResult>;
|
|
135
|
+
private notifyWaiters;
|
|
136
|
+
}
|