@xioflow/kernel 0.2.0 → 0.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 +36 -2
- package/dist/domain.d.ts +21 -1
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +17 -0
- package/dist/domain.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/quick-run.d.ts +27 -0
- package/dist/quick-run.d.ts.map +1 -0
- package/dist/quick-run.js +82 -0
- package/dist/quick-run.js.map +1 -0
- package/dist/store/sqlite.d.ts +3 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +48 -0
- package/dist/store/sqlite.js.map +1 -1
- package/dist/supervisor/supervisor.d.ts +1 -0
- package/dist/supervisor/supervisor.d.ts.map +1 -1
- package/dist/supervisor/supervisor.js +481 -374
- package/dist/supervisor/supervisor.js.map +1 -1
- package/dist/testing/contract-suite.d.ts.map +1 -1
- package/dist/testing/contract-suite.js +179 -1
- package/dist/testing/contract-suite.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +31 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import { UnsupportedCapabilityError, OperationNotActiveError,
|
|
4
|
+
import { UnsupportedCapabilityError, OperationNotActiveError, OperationIdConflictError, RecoveryRequiredError, } from '../types.js';
|
|
5
5
|
/**
|
|
6
6
|
* 规范化计算完整输入的 SHA-256 指纹 (ARCHITECTURE §2 / P0-12)
|
|
7
7
|
* 覆盖 execPath, args, cwd, 键排序的 envWhiteList, inheritEnv, sha256(stdin), 排序后的 requiredResources, timeoutMs, resourceBudget.
|
|
@@ -99,439 +99,543 @@ export class ProcessSupervisor {
|
|
|
99
99
|
throw new UnsupportedCapabilityError('cpuLimit', this.driver.name, 'hard');
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
|
-
|
|
102
|
+
const inputFingerprint = options.inputFingerprint || computeInputFingerprint(options);
|
|
103
|
+
// 0. 验证调用 Run 状态(N3 契约):终态 Run 禁止登记或重放操作
|
|
104
|
+
const callingRun = this.domain.getStore().getRun(options.runId);
|
|
105
|
+
if (!callingRun) {
|
|
106
|
+
throw new Error(`Run "${options.runId}" is not registered in domain "${this.domain.domainId}". ` +
|
|
107
|
+
'Register the task and run first (store.saveTask() + store.saveRun()), then execute operations for that run.');
|
|
108
|
+
}
|
|
109
|
+
if (callingRun.status === 'succeeded' ||
|
|
110
|
+
callingRun.status === 'failed' ||
|
|
111
|
+
callingRun.status === 'cancelled' ||
|
|
112
|
+
callingRun.status === 'indeterminate') {
|
|
113
|
+
throw new Error(`Cannot register operation "${options.opId}" for Run "${options.runId}" because the Run is already finalized with status "${callingRun.status}".`);
|
|
114
|
+
}
|
|
115
|
+
// 1. 在飞操作重放判定(ARCHITECTURE §3.7 / 契约 #46)
|
|
103
116
|
if (this.activeOperations.has(options.opId)) {
|
|
104
|
-
const
|
|
105
|
-
|
|
117
|
+
const inFlight = this.activeOperations.get(options.opId);
|
|
118
|
+
if (inFlight.inputFingerprint !== inputFingerprint) {
|
|
119
|
+
throw new OperationIdConflictError(options.opId, inFlight.inputFingerprint, inputFingerprint, inFlight.phase, inFlight.runId);
|
|
120
|
+
}
|
|
121
|
+
this.domain.getStore().recordReplay(this.domain.domainId, options.opId, 'joined', options.runId);
|
|
122
|
+
if (options.onStreamChunk) {
|
|
123
|
+
inFlight.streamSubscribers.add(options.onStreamChunk);
|
|
124
|
+
}
|
|
125
|
+
if (options.abortSignal) {
|
|
126
|
+
const signal = options.abortSignal;
|
|
127
|
+
if (signal.aborted) {
|
|
128
|
+
if (options.onStreamChunk) {
|
|
129
|
+
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
130
|
+
}
|
|
131
|
+
throw signal.reason || new Error('Operation wait aborted');
|
|
132
|
+
}
|
|
133
|
+
return await new Promise((resolve, reject) => {
|
|
134
|
+
const onAbort = () => {
|
|
135
|
+
if (options.onStreamChunk) {
|
|
136
|
+
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
137
|
+
}
|
|
138
|
+
reject(signal.reason || new Error('Operation wait aborted'));
|
|
139
|
+
};
|
|
140
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
141
|
+
inFlight.resultPromise.then((res) => {
|
|
142
|
+
signal.removeEventListener('abort', onAbort);
|
|
143
|
+
if (options.onStreamChunk) {
|
|
144
|
+
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
145
|
+
}
|
|
146
|
+
resolve({ ...res, replayed: true });
|
|
147
|
+
}, (err) => {
|
|
148
|
+
signal.removeEventListener('abort', onAbort);
|
|
149
|
+
if (options.onStreamChunk) {
|
|
150
|
+
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
151
|
+
}
|
|
152
|
+
reject(err);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const res = await inFlight.resultPromise;
|
|
157
|
+
if (options.onStreamChunk) {
|
|
158
|
+
inFlight.streamSubscribers.delete(options.onStreamChunk);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
...res,
|
|
162
|
+
replayed: true,
|
|
163
|
+
};
|
|
106
164
|
}
|
|
165
|
+
// 2. 已持久化操作重放判定(ARCHITECTURE §3.7 / 契约 #45, #47, #48, #49)
|
|
107
166
|
const existingRecorded = this.domain.getStore().getOperation(options.opId);
|
|
108
167
|
if (existingRecorded) {
|
|
109
|
-
|
|
168
|
+
if (existingRecorded.inputFingerprint !== inputFingerprint) {
|
|
169
|
+
throw new OperationIdConflictError(options.opId, existingRecorded.inputFingerprint, inputFingerprint, existingRecorded.status, existingRecorded.runId);
|
|
170
|
+
}
|
|
171
|
+
// 未终结状态且不在内存中 → 必须先执行恢复(崩溃残留现场)
|
|
172
|
+
if (existingRecorded.status !== 'done') {
|
|
173
|
+
throw new RecoveryRequiredError(options.opId, existingRecorded.status, existingRecorded.runId);
|
|
174
|
+
}
|
|
175
|
+
// indeterminate 状态:绝不重跑,原样返回,保留租约
|
|
176
|
+
if (existingRecorded.result?.status === 'indeterminate') {
|
|
177
|
+
this.domain.getStore().recordReplay(this.domain.domainId, options.opId, 'indeterminate', options.runId);
|
|
178
|
+
return {
|
|
179
|
+
...existingRecorded.result,
|
|
180
|
+
replayed: true,
|
|
181
|
+
runId: existingRecorded.runId,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
// 已结清终态 (succeeded / failed / cancelled):返回已持久化结果
|
|
185
|
+
this.domain.getStore().recordReplay(this.domain.domainId, options.opId, 'recorded', options.runId);
|
|
186
|
+
return {
|
|
187
|
+
...existingRecorded.result,
|
|
188
|
+
replayed: true,
|
|
189
|
+
runId: existingRecorded.runId,
|
|
190
|
+
};
|
|
110
191
|
}
|
|
111
192
|
const op = {
|
|
112
193
|
id: options.opId,
|
|
113
194
|
runId: options.runId,
|
|
114
195
|
kind: 'process',
|
|
115
196
|
name: options.name,
|
|
116
|
-
inputFingerprint
|
|
197
|
+
inputFingerprint,
|
|
117
198
|
requiredResources: options.requiredResources,
|
|
118
199
|
timeoutMs: options.timeoutMs,
|
|
119
200
|
resourceBudget: options.resourceBudget,
|
|
120
201
|
status: 'pending',
|
|
121
202
|
};
|
|
203
|
+
const streamSubscribers = new Set();
|
|
204
|
+
if (options.onStreamChunk) {
|
|
205
|
+
streamSubscribers.add(options.onStreamChunk);
|
|
206
|
+
}
|
|
122
207
|
const opState = {
|
|
123
208
|
opId: options.opId,
|
|
124
209
|
runId: options.runId,
|
|
125
210
|
phase: 'waiting_resources',
|
|
126
211
|
startTime,
|
|
212
|
+
inputFingerprint,
|
|
213
|
+
streamSubscribers,
|
|
127
214
|
cancelRequested: false,
|
|
128
215
|
timedOut: false,
|
|
129
216
|
};
|
|
130
217
|
this.activeOperations.set(options.opId, opState);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// 1. [资源分配与排队等待]
|
|
135
|
-
await this.domain.allocateResourcesWithWait(options.opId, options.requiredResources || [], options.waitTimeoutMs ?? 0, options.resourceBudget, () => opState.cancelRequested === true);
|
|
136
|
-
if (opState.cancelRequested) {
|
|
137
|
-
return this.handlePreSpawnCancellation(options, opState, startTime);
|
|
138
|
-
}
|
|
139
|
-
// 2. [启动协议步骤 1] 写入 SQLite (status: intent_registered)
|
|
140
|
-
try {
|
|
141
|
-
this.domain.getStore().registerOperationIntent(op, this.domain.domainId);
|
|
142
|
-
opState.phase = 'intent_registered';
|
|
143
|
-
}
|
|
144
|
-
catch (err) {
|
|
145
|
-
this.domain.internalReleaseResources(options.opId, options.requiredResources);
|
|
146
|
-
throw err;
|
|
147
|
-
}
|
|
148
|
-
if (opState.cancelRequested) {
|
|
149
|
-
return this.handlePreSpawnCancellation(options, opState, startTime);
|
|
150
|
-
}
|
|
151
|
-
let handle;
|
|
152
|
-
opState.phase = 'spawning';
|
|
153
|
-
try {
|
|
154
|
-
// 3. [启动协议步骤 2] 请求平台驱动启动
|
|
155
|
-
handle = await this.driver.spawn(options.command);
|
|
156
|
-
opState.handle = handle;
|
|
157
|
-
opState.command = options.command;
|
|
218
|
+
if (options.abortSignal) {
|
|
219
|
+
if (options.abortSignal.aborted) {
|
|
220
|
+
opState.cancelRequested = true;
|
|
158
221
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
opState.stopResolve = undefined;
|
|
163
|
-
// 启动失败(如可执行文件不存在),不产生假运行状态,直接失败收尾并释放资源
|
|
164
|
-
const failResult = {
|
|
165
|
-
kind: 'process',
|
|
166
|
-
status: 'failed',
|
|
167
|
-
exitCode: 127,
|
|
168
|
-
signal: null,
|
|
169
|
-
stdout: '',
|
|
170
|
-
stderr: spawnError?.message || String(spawnError),
|
|
171
|
-
spawnFailure: spawnError?.message || String(spawnError),
|
|
172
|
-
isTruncated: false,
|
|
173
|
-
identityVerification: 'not_original_process',
|
|
174
|
-
durationMs: Date.now() - startTime,
|
|
175
|
-
completedAt: new Date().toISOString(),
|
|
222
|
+
else {
|
|
223
|
+
const onAbort = () => {
|
|
224
|
+
this.cancelOperation(options.opId).catch(() => { });
|
|
176
225
|
};
|
|
177
|
-
|
|
226
|
+
options.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
178
227
|
}
|
|
179
|
-
|
|
228
|
+
}
|
|
229
|
+
const executionPromise = (async () => {
|
|
230
|
+
let timeoutTimer = null;
|
|
231
|
+
let samplingInterval = null;
|
|
232
|
+
let stdoutDrainer;
|
|
233
|
+
let stderrDrainer;
|
|
180
234
|
try {
|
|
181
|
-
|
|
182
|
-
opState.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
// N4: spawn 成功但登记 active 失败,防止产生孤儿进程
|
|
188
|
-
opState.phase = 'stopping';
|
|
189
|
-
const stopRes = await this.driver.terminate(handle.identity, 2000);
|
|
190
|
-
// 有界等待退出与关闭流,防止管道悬挂
|
|
191
|
-
await Promise.race([
|
|
192
|
-
(handle.onRootExit ?? handle.onExit).catch(() => { }),
|
|
193
|
-
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
194
|
-
]);
|
|
235
|
+
// 1. [资源分配与排队等待]
|
|
236
|
+
await this.domain.allocateResourcesWithWait(options.opId, options.requiredResources || [], options.waitTimeoutMs ?? 0, options.resourceBudget, () => opState.cancelRequested === true);
|
|
237
|
+
if (opState.cancelRequested) {
|
|
238
|
+
return this.handlePreSpawnCancellation(options, opState, startTime);
|
|
239
|
+
}
|
|
240
|
+
// 2. [启动协议步骤 1] 写入 SQLite (status: intent_registered)
|
|
195
241
|
try {
|
|
196
|
-
|
|
197
|
-
|
|
242
|
+
this.domain.getStore().registerOperationIntent(op, this.domain.domainId);
|
|
243
|
+
opState.phase = 'intent_registered';
|
|
198
244
|
}
|
|
199
|
-
catch {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const resolveFn = opState.stopResolve;
|
|
203
|
-
opState.stopResolve = undefined;
|
|
204
|
-
resolveFn(stopRes);
|
|
245
|
+
catch (err) {
|
|
246
|
+
this.domain.internalReleaseResources(options.opId, options.requiredResources);
|
|
247
|
+
throw err;
|
|
205
248
|
}
|
|
206
|
-
if (
|
|
249
|
+
if (opState.cancelRequested) {
|
|
250
|
+
return this.handlePreSpawnCancellation(options, opState, startTime);
|
|
251
|
+
}
|
|
252
|
+
let handle;
|
|
253
|
+
opState.phase = 'spawning';
|
|
254
|
+
try {
|
|
255
|
+
// 3. [启动协议步骤 2] 请求平台驱动启动
|
|
256
|
+
handle = await this.driver.spawn(options.command);
|
|
257
|
+
opState.handle = handle;
|
|
258
|
+
opState.command = options.command;
|
|
259
|
+
}
|
|
260
|
+
catch (spawnError) {
|
|
261
|
+
opState.phase = 'done';
|
|
262
|
+
opState.stopResolve?.({ stopped: 'confirmed_stopped', scope: 'direct_child', errorDetails: spawnError?.message });
|
|
263
|
+
opState.stopResolve = undefined;
|
|
264
|
+
// 启动失败(如可执行文件不存在),不产生假运行状态,直接失败收尾并释放资源
|
|
207
265
|
const failResult = {
|
|
208
266
|
kind: 'process',
|
|
209
267
|
status: 'failed',
|
|
210
|
-
exitCode:
|
|
268
|
+
exitCode: 127,
|
|
211
269
|
signal: null,
|
|
212
270
|
stdout: '',
|
|
213
|
-
stderr:
|
|
214
|
-
spawnFailure:
|
|
271
|
+
stderr: spawnError?.message || String(spawnError),
|
|
272
|
+
spawnFailure: spawnError?.message || String(spawnError),
|
|
215
273
|
isTruncated: false,
|
|
216
274
|
identityVerification: 'not_original_process',
|
|
217
275
|
durationMs: Date.now() - startTime,
|
|
218
276
|
completedAt: new Date().toISOString(),
|
|
219
277
|
};
|
|
220
|
-
this.finalizeOperation(options.opId, failResult, options.requiredResources, true);
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
const indetResult = {
|
|
224
|
-
kind: 'indeterminate',
|
|
225
|
-
status: 'indeterminate',
|
|
226
|
-
reason: `Failed to register active status: ${statusError?.message || String(statusError)}, and process ${handle.identity.pid} could not be confirmed stopped: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
227
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
228
|
-
durationMs: Date.now() - startTime,
|
|
229
|
-
completedAt: new Date().toISOString(),
|
|
230
|
-
};
|
|
231
|
-
this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
278
|
+
return this.finalizeOperation(options.opId, failResult, options.requiredResources, true);
|
|
232
279
|
}
|
|
233
|
-
|
|
234
|
-
}
|
|
235
|
-
// 准备 artifacts 溢出转储目录
|
|
236
|
-
const artifactsDir = options.artifactsDir || path.join(this.domain.domainPath, 'artifacts');
|
|
237
|
-
let artifactsDirError = undefined;
|
|
238
|
-
if (!fs.existsSync(artifactsDir)) {
|
|
239
|
-
try {
|
|
240
|
-
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
241
|
-
}
|
|
242
|
-
catch (dirErr) {
|
|
243
|
-
artifactsDirError = dirErr.message || String(dirErr);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
const stdoutSpillPath = path.join(artifactsDir, `${options.opId}-stdout.log`);
|
|
247
|
-
const stderrSpillPath = path.join(artifactsDir, `${options.opId}-stderr.log`);
|
|
248
|
-
// 5. [正常监督、有界排空与流式转储 (Spill to Artifacts)]
|
|
249
|
-
let totalOutputBytes = 0;
|
|
250
|
-
const onChunk = (bytes) => {
|
|
251
|
-
totalOutputBytes += bytes;
|
|
252
|
-
if (options.resourceBudget?.enforcement === 'soft' &&
|
|
253
|
-
options.resourceBudget.maxOutputBytes &&
|
|
254
|
-
totalOutputBytes > options.resourceBudget.maxOutputBytes &&
|
|
255
|
-
!opState.terminationReason) {
|
|
256
|
-
opState.terminationReason = 'output_exceeded';
|
|
257
|
-
this.handleStopPipeline(options.opId, 'output_exceeded', 1000).catch(() => { });
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
let streamCallbackError;
|
|
261
|
-
const forwardChunk = (stream) => (chunk) => {
|
|
262
|
-
if (!options.onStreamChunk)
|
|
263
|
-
return;
|
|
280
|
+
// 4. [启动协议步骤 3] 登记执行身份,状态推进为 active
|
|
264
281
|
try {
|
|
265
|
-
options.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
timeoutTimer = setTimeout(() => resolve('timeout'), options.timeoutMs);
|
|
280
|
-
})
|
|
281
|
-
: new Promise(() => { });
|
|
282
|
-
// Soft / Observe 模式资源采样治理
|
|
283
|
-
let peakMemoryBytes = 0;
|
|
284
|
-
let peakCpuTimeMs = 0;
|
|
285
|
-
if (this.driver.sampleMetrics &&
|
|
286
|
-
options.resourceBudget &&
|
|
287
|
-
(options.resourceBudget.enforcement === 'soft' || options.resourceBudget.enforcement === 'observe')) {
|
|
288
|
-
samplingInterval = setInterval(async () => {
|
|
282
|
+
this.domain.getStore().updateOperationStatus(options.opId, 'active', handle.identity);
|
|
283
|
+
opState.phase = 'active';
|
|
284
|
+
handle.releaseGate?.();
|
|
285
|
+
}
|
|
286
|
+
catch (statusError) {
|
|
287
|
+
handle.destroyGate?.();
|
|
288
|
+
// N4: spawn 成功但登记 active 失败,防止产生孤儿进程
|
|
289
|
+
opState.phase = 'stopping';
|
|
290
|
+
const stopRes = await this.driver.terminate(handle.identity, 2000);
|
|
291
|
+
// 有界等待退出与关闭流,防止管道悬挂
|
|
292
|
+
await Promise.race([
|
|
293
|
+
(handle.onRootExit ?? handle.onExit).catch(() => { }),
|
|
294
|
+
new Promise((resolve) => setTimeout(resolve, 500)),
|
|
295
|
+
]);
|
|
289
296
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
peakCpuTimeMs = Math.max(peakCpuTimeMs, metrics.cpuTimeMs);
|
|
293
|
-
if (options.resourceBudget?.enforcement === 'soft') {
|
|
294
|
-
if (options.resourceBudget.maxMemoryBytes && metrics.rssBytes > options.resourceBudget.maxMemoryBytes) {
|
|
295
|
-
opState.terminationReason = 'memory_exceeded';
|
|
296
|
-
if (samplingInterval)
|
|
297
|
-
clearInterval(samplingInterval);
|
|
298
|
-
await this.handleStopPipeline(options.opId, 'memory_exceeded', 1000);
|
|
299
|
-
}
|
|
300
|
-
else if (options.resourceBudget.maxCpuTimeMs && metrics.cpuTimeMs > options.resourceBudget.maxCpuTimeMs) {
|
|
301
|
-
opState.terminationReason = 'cpu_exceeded';
|
|
302
|
-
if (samplingInterval)
|
|
303
|
-
clearInterval(samplingInterval);
|
|
304
|
-
await this.handleStopPipeline(options.opId, 'cpu_exceeded', 1000);
|
|
305
|
-
}
|
|
306
|
-
else if (options.resourceBudget.maxPids && metrics.pidsCount > options.resourceBudget.maxPids) {
|
|
307
|
-
opState.terminationReason = 'pids_exceeded';
|
|
308
|
-
if (samplingInterval)
|
|
309
|
-
clearInterval(samplingInterval);
|
|
310
|
-
await this.handleStopPipeline(options.opId, 'pids_exceeded', 1000);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
297
|
+
handle.stdout.destroy?.();
|
|
298
|
+
handle.stderr.destroy?.();
|
|
313
299
|
}
|
|
314
300
|
catch { }
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
});
|
|
321
|
-
const previousResolve = opState.stopResolve;
|
|
322
|
-
opState.stopResolve = (res) => {
|
|
323
|
-
previousResolve?.(res);
|
|
324
|
-
notifyStop?.(res);
|
|
325
|
-
};
|
|
326
|
-
// 等待根进程退出、超时触发、或外部停止完成
|
|
327
|
-
const rootExitPromise = handle.onRootExit ?? handle.onExit;
|
|
328
|
-
const stopCandidate = opState.stopPromise ? opState.stopPromise : stopTriggerPromise;
|
|
329
|
-
const exitOrTimeout = await Promise.race([
|
|
330
|
-
rootExitPromise.then((res) => ({ type: 'exit', res, stopRes: null })),
|
|
331
|
-
timeoutPromise.then((type) => ({ type, res: null, stopRes: null })),
|
|
332
|
-
stopCandidate.then((stopRes) => ({ type: 'stop', res: null, stopRes })),
|
|
333
|
-
]);
|
|
334
|
-
if (timeoutTimer)
|
|
335
|
-
clearTimeout(timeoutTimer);
|
|
336
|
-
if (samplingInterval)
|
|
337
|
-
clearInterval(samplingInterval);
|
|
338
|
-
let exitResult;
|
|
339
|
-
if (exitOrTimeout.type === 'stop') {
|
|
340
|
-
const stopRes = exitOrTimeout.stopRes;
|
|
341
|
-
if (stopRes.stopped !== 'confirmed_stopped') {
|
|
342
|
-
if (handle.rawProcess && typeof handle.rawProcess.kill === 'function') {
|
|
343
|
-
try {
|
|
344
|
-
handle.rawProcess.kill('SIGKILL');
|
|
345
|
-
}
|
|
346
|
-
catch { }
|
|
301
|
+
opState.phase = 'done';
|
|
302
|
+
if (opState.stopResolve) {
|
|
303
|
+
const resolveFn = opState.stopResolve;
|
|
304
|
+
opState.stopResolve = undefined;
|
|
305
|
+
resolveFn(stopRes);
|
|
347
306
|
}
|
|
307
|
+
if (stopRes.stopped === 'confirmed_stopped') {
|
|
308
|
+
const failResult = {
|
|
309
|
+
kind: 'process',
|
|
310
|
+
status: 'failed',
|
|
311
|
+
exitCode: null,
|
|
312
|
+
signal: null,
|
|
313
|
+
stdout: '',
|
|
314
|
+
stderr: `Failed to register active status: ${statusError?.message || String(statusError)}`,
|
|
315
|
+
spawnFailure: statusError?.message || String(statusError),
|
|
316
|
+
isTruncated: false,
|
|
317
|
+
identityVerification: 'not_original_process',
|
|
318
|
+
durationMs: Date.now() - startTime,
|
|
319
|
+
completedAt: new Date().toISOString(),
|
|
320
|
+
};
|
|
321
|
+
this.finalizeOperation(options.opId, failResult, options.requiredResources, true);
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const indetResult = {
|
|
325
|
+
kind: 'indeterminate',
|
|
326
|
+
status: 'indeterminate',
|
|
327
|
+
reason: `Failed to register active status: ${statusError?.message || String(statusError)}, and process ${handle.identity.pid} could not be confirmed stopped: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
328
|
+
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
329
|
+
durationMs: Date.now() - startTime,
|
|
330
|
+
completedAt: new Date().toISOString(),
|
|
331
|
+
};
|
|
332
|
+
this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
333
|
+
}
|
|
334
|
+
throw statusError;
|
|
335
|
+
}
|
|
336
|
+
// 准备 artifacts 溢出转储目录
|
|
337
|
+
const artifactsDir = options.artifactsDir || path.join(this.domain.domainPath, 'artifacts');
|
|
338
|
+
let artifactsDirError = undefined;
|
|
339
|
+
if (!fs.existsSync(artifactsDir)) {
|
|
348
340
|
try {
|
|
349
|
-
|
|
350
|
-
handle.stderr.destroy?.();
|
|
341
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
351
342
|
}
|
|
352
|
-
catch {
|
|
353
|
-
|
|
354
|
-
if (stored && stored.status === 'done') {
|
|
355
|
-
return (stored.result ?? null);
|
|
343
|
+
catch (dirErr) {
|
|
344
|
+
artifactsDirError = dirErr.message || String(dirErr);
|
|
356
345
|
}
|
|
357
|
-
const indetResult = {
|
|
358
|
-
kind: 'indeterminate',
|
|
359
|
-
status: 'indeterminate',
|
|
360
|
-
reason: `Process ${handle.identity.pid} was cancelled or stopped but could not be confirmed: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
361
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
362
|
-
durationMs: Date.now() - startTime,
|
|
363
|
-
completedAt: new Date().toISOString(),
|
|
364
|
-
};
|
|
365
|
-
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
366
346
|
}
|
|
367
|
-
const
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
(
|
|
384
|
-
|
|
347
|
+
const stdoutSpillPath = path.join(artifactsDir, `${options.opId}-stdout.log`);
|
|
348
|
+
const stderrSpillPath = path.join(artifactsDir, `${options.opId}-stderr.log`);
|
|
349
|
+
// 5. [正常监督、有界排空与流式转储 (Spill to Artifacts)]
|
|
350
|
+
let totalOutputBytes = 0;
|
|
351
|
+
const onChunk = (bytes) => {
|
|
352
|
+
totalOutputBytes += bytes;
|
|
353
|
+
if (options.resourceBudget?.enforcement === 'soft' &&
|
|
354
|
+
options.resourceBudget.maxOutputBytes &&
|
|
355
|
+
totalOutputBytes > options.resourceBudget.maxOutputBytes &&
|
|
356
|
+
!opState.terminationReason) {
|
|
357
|
+
opState.terminationReason = 'output_exceeded';
|
|
358
|
+
this.handleStopPipeline(options.opId, 'output_exceeded', 1000).catch(() => { });
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
let streamCallbackError;
|
|
362
|
+
const forwardChunk = (stream) => (chunk) => {
|
|
363
|
+
for (const subscriber of opState.streamSubscribers) {
|
|
364
|
+
try {
|
|
365
|
+
subscriber(stream, chunk);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
streamCallbackError ??= err instanceof Error ? err.message : String(err);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
stdoutDrainer = this.setupStreamDrainer(handle.stdout, maxBytes, stdoutSpillPath, onChunk, forwardChunk('stdout'), artifactsDirError);
|
|
373
|
+
stderrDrainer = this.setupStreamDrainer(handle.stderr, maxBytes, stderrSpillPath, onChunk, forwardChunk('stderr'), artifactsDirError);
|
|
374
|
+
// 检查在 spawn 途中是否已被请求取消:若已请求取消,立即触发停止流水线向底层发送终止信号
|
|
375
|
+
if (opState.cancelRequested) {
|
|
376
|
+
this.handleStopPipeline(options.opId, 'user_cancelled', opState.cancelGraceMs ?? 2000).catch(() => { });
|
|
377
|
+
}
|
|
378
|
+
const timeoutPromise = options.timeoutMs && options.timeoutMs > 0
|
|
379
|
+
? new Promise((resolve) => {
|
|
380
|
+
timeoutTimer = setTimeout(() => resolve('timeout'), options.timeoutMs);
|
|
381
|
+
})
|
|
382
|
+
: new Promise(() => { });
|
|
383
|
+
// Soft / Observe 模式资源采样治理
|
|
384
|
+
let peakMemoryBytes = 0;
|
|
385
|
+
let peakCpuTimeMs = 0;
|
|
386
|
+
if (this.driver.sampleMetrics &&
|
|
387
|
+
options.resourceBudget &&
|
|
388
|
+
(options.resourceBudget.enforcement === 'soft' || options.resourceBudget.enforcement === 'observe')) {
|
|
389
|
+
samplingInterval = setInterval(async () => {
|
|
390
|
+
try {
|
|
391
|
+
const metrics = await this.driver.sampleMetrics(handle.identity);
|
|
392
|
+
peakMemoryBytes = Math.max(peakMemoryBytes, metrics.rssBytes);
|
|
393
|
+
peakCpuTimeMs = Math.max(peakCpuTimeMs, metrics.cpuTimeMs);
|
|
394
|
+
if (options.resourceBudget?.enforcement === 'soft') {
|
|
395
|
+
if (options.resourceBudget.maxMemoryBytes && metrics.rssBytes > options.resourceBudget.maxMemoryBytes) {
|
|
396
|
+
opState.terminationReason = 'memory_exceeded';
|
|
397
|
+
if (samplingInterval)
|
|
398
|
+
clearInterval(samplingInterval);
|
|
399
|
+
await this.handleStopPipeline(options.opId, 'memory_exceeded', 1000);
|
|
400
|
+
}
|
|
401
|
+
else if (options.resourceBudget.maxCpuTimeMs && metrics.cpuTimeMs > options.resourceBudget.maxCpuTimeMs) {
|
|
402
|
+
opState.terminationReason = 'cpu_exceeded';
|
|
403
|
+
if (samplingInterval)
|
|
404
|
+
clearInterval(samplingInterval);
|
|
405
|
+
await this.handleStopPipeline(options.opId, 'cpu_exceeded', 1000);
|
|
406
|
+
}
|
|
407
|
+
else if (options.resourceBudget.maxPids && metrics.pidsCount > options.resourceBudget.maxPids) {
|
|
408
|
+
opState.terminationReason = 'pids_exceeded';
|
|
409
|
+
if (samplingInterval)
|
|
410
|
+
clearInterval(samplingInterval);
|
|
411
|
+
await this.handleStopPipeline(options.opId, 'pids_exceeded', 1000);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch { }
|
|
416
|
+
}, 100);
|
|
417
|
+
}
|
|
418
|
+
let notifyStop;
|
|
419
|
+
const stopTriggerPromise = new Promise((resolve) => {
|
|
420
|
+
notifyStop = resolve;
|
|
421
|
+
});
|
|
422
|
+
const previousResolve = opState.stopResolve;
|
|
423
|
+
opState.stopResolve = (res) => {
|
|
424
|
+
previousResolve?.(res);
|
|
425
|
+
notifyStop?.(res);
|
|
426
|
+
};
|
|
427
|
+
// 等待根进程退出、超时触发、或外部停止完成
|
|
428
|
+
const rootExitPromise = handle.onRootExit ?? handle.onExit;
|
|
429
|
+
const stopCandidate = opState.stopPromise ? opState.stopPromise : stopTriggerPromise;
|
|
430
|
+
const exitOrTimeout = await Promise.race([
|
|
431
|
+
rootExitPromise.then((res) => ({ type: 'exit', res, stopRes: null })),
|
|
432
|
+
timeoutPromise.then((type) => ({ type, res: null, stopRes: null })),
|
|
433
|
+
stopCandidate.then((stopRes) => ({ type: 'stop', res: null, stopRes })),
|
|
385
434
|
]);
|
|
386
|
-
if (
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
435
|
+
if (timeoutTimer)
|
|
436
|
+
clearTimeout(timeoutTimer);
|
|
437
|
+
if (samplingInterval)
|
|
438
|
+
clearInterval(samplingInterval);
|
|
439
|
+
let exitResult;
|
|
440
|
+
if (exitOrTimeout.type === 'stop') {
|
|
441
|
+
const stopRes = exitOrTimeout.stopRes;
|
|
442
|
+
if (stopRes.stopped !== 'confirmed_stopped') {
|
|
443
|
+
if (handle.rawProcess && typeof handle.rawProcess.kill === 'function') {
|
|
444
|
+
try {
|
|
445
|
+
handle.rawProcess.kill('SIGKILL');
|
|
446
|
+
}
|
|
447
|
+
catch { }
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
handle.stdout.destroy?.();
|
|
451
|
+
handle.stderr.destroy?.();
|
|
452
|
+
}
|
|
453
|
+
catch { }
|
|
454
|
+
const stored = this.domain.getStore().getOperation(options.opId);
|
|
455
|
+
if (stored && stored.status === 'done') {
|
|
456
|
+
return (stored.result ?? null);
|
|
457
|
+
}
|
|
458
|
+
const indetResult = {
|
|
459
|
+
kind: 'indeterminate',
|
|
460
|
+
status: 'indeterminate',
|
|
461
|
+
reason: `Process ${handle.identity.pid} was cancelled or stopped but could not be confirmed: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
462
|
+
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
463
|
+
durationMs: Date.now() - startTime,
|
|
464
|
+
completedAt: new Date().toISOString(),
|
|
465
|
+
};
|
|
466
|
+
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
390
467
|
}
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
468
|
+
const rootExitWaitMs = 1000;
|
|
469
|
+
const rootExitOrTimeout = await Promise.race([
|
|
470
|
+
rootExitPromise.then((res) => ({ exited: true, res })),
|
|
471
|
+
new Promise((resolve) => setTimeout(() => resolve({ exited: false }), rootExitWaitMs)),
|
|
472
|
+
]);
|
|
473
|
+
exitResult = rootExitOrTimeout.exited ? rootExitOrTimeout.res : { exitCode: null, signal: 'SIGKILL' };
|
|
474
|
+
}
|
|
475
|
+
else if (exitOrTimeout.type === 'timeout') {
|
|
476
|
+
opState.timedOut = true;
|
|
477
|
+
opState.terminationReason = 'timed_out';
|
|
478
|
+
const stopRes = await this.handleStopPipeline(options.opId, 'timed_out', 1500);
|
|
479
|
+
// 超时路径:根进程已由停止流水线终止,等待根进程退出事实(避免被持有管道的逃逸后代挂死)
|
|
480
|
+
// 关键防护(问题 4):给等待根进程退出增加有界上限(1000ms),
|
|
481
|
+
// 若驱动停止未确认(stopped !== 'confirmed_stopped')或根进程超出上限仍未退出,转入 indeterminate
|
|
482
|
+
const rootExitWaitMs = 1000;
|
|
483
|
+
const rootExitOrTimeout = await Promise.race([
|
|
484
|
+
(handle.onRootExit ?? handle.onExit).then((res) => ({ exited: true, res })),
|
|
485
|
+
new Promise((resolve) => setTimeout(() => resolve({ exited: false }), rootExitWaitMs)),
|
|
486
|
+
]);
|
|
487
|
+
if (stopRes.stopped !== 'confirmed_stopped' || !rootExitOrTimeout.exited) {
|
|
488
|
+
const stored = this.domain.getStore().getOperation(options.opId);
|
|
489
|
+
if (stored && stored.status === 'done' && stored.result?.status === 'indeterminate') {
|
|
490
|
+
return stored.result;
|
|
491
|
+
}
|
|
492
|
+
const indetResult = {
|
|
493
|
+
kind: 'indeterminate',
|
|
494
|
+
status: 'indeterminate',
|
|
495
|
+
reason: `Process ${handle.identity.pid} timed out and root process could not be confirmed exited within ${rootExitWaitMs}ms bound: ${stopRes.errorDetails || 'root process still alive'}`,
|
|
496
|
+
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
497
|
+
durationMs: Date.now() - startTime,
|
|
498
|
+
completedAt: new Date().toISOString(),
|
|
499
|
+
};
|
|
500
|
+
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
501
|
+
}
|
|
502
|
+
exitResult = rootExitOrTimeout.res;
|
|
400
503
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
else {
|
|
404
|
-
exitResult = exitOrTimeout.res;
|
|
405
|
-
}
|
|
406
|
-
// 有界等待流排空
|
|
407
|
-
const drainPromise = Promise.all([stdoutDrainer.finishPromise, stderrDrainer.finishPromise]);
|
|
408
|
-
const drainTimeoutPromise = new Promise((resolve) => setTimeout(resolve, drainTimeoutMs));
|
|
409
|
-
let drained = await Promise.race([
|
|
410
|
-
drainPromise.then(() => true),
|
|
411
|
-
drainTimeoutPromise.then(() => false),
|
|
412
|
-
]);
|
|
413
|
-
// 排空超时说明根进程已退出但仍有后代持有管道:回收整个进程组,
|
|
414
|
-
// 否则操作会一直挂到超时,且 root 的真实退出事实会被超时掩盖。
|
|
415
|
-
let residualProcessesReaped = false;
|
|
416
|
-
if (!drained && exitOrTimeout.type === 'exit') {
|
|
417
|
-
const reapResult = await this.driver.terminate(handle.identity, 1500);
|
|
418
|
-
if (reapResult.stopped !== 'confirmed_stopped') {
|
|
419
|
-
const indetResult = {
|
|
420
|
-
kind: 'indeterminate',
|
|
421
|
-
status: 'indeterminate',
|
|
422
|
-
reason: `Process ${handle.identity.pid} exited but residual descendants could not be confirmed stopped: ${reapResult.errorDetails}`,
|
|
423
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
424
|
-
durationMs: Date.now() - startTime,
|
|
425
|
-
completedAt: new Date().toISOString(),
|
|
426
|
-
};
|
|
427
|
-
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
504
|
+
else {
|
|
505
|
+
exitResult = exitOrTimeout.res;
|
|
428
506
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
507
|
+
// 有界等待流排空
|
|
508
|
+
const drainPromise = Promise.all([stdoutDrainer.finishPromise, stderrDrainer.finishPromise]);
|
|
509
|
+
const drainTimeoutPromise = new Promise((resolve) => setTimeout(resolve, drainTimeoutMs));
|
|
510
|
+
let drained = await Promise.race([
|
|
432
511
|
drainPromise.then(() => true),
|
|
433
|
-
|
|
512
|
+
drainTimeoutPromise.then(() => false),
|
|
434
513
|
]);
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
514
|
+
// 排空超时说明根进程已退出但仍有后代持有管道:回收整个进程组,
|
|
515
|
+
// 否则操作会一直挂到超时,且 root 的真实退出事实会被超时掩盖。
|
|
516
|
+
let residualProcessesReaped = false;
|
|
517
|
+
if (!drained && exitOrTimeout.type === 'exit') {
|
|
518
|
+
const reapResult = await this.driver.terminate(handle.identity, 1500);
|
|
519
|
+
if (reapResult.stopped !== 'confirmed_stopped') {
|
|
520
|
+
const indetResult = {
|
|
521
|
+
kind: 'indeterminate',
|
|
522
|
+
status: 'indeterminate',
|
|
523
|
+
reason: `Process ${handle.identity.pid} exited but residual descendants could not be confirmed stopped: ${reapResult.errorDetails}`,
|
|
524
|
+
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
525
|
+
durationMs: Date.now() - startTime,
|
|
526
|
+
completedAt: new Date().toISOString(),
|
|
527
|
+
};
|
|
528
|
+
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
529
|
+
}
|
|
530
|
+
residualProcessesReaped = true;
|
|
531
|
+
// 被杀死的后代释放管道后,给排空最后一次机会
|
|
532
|
+
drained = await Promise.race([
|
|
533
|
+
drainPromise.then(() => true),
|
|
534
|
+
new Promise((resolve) => setTimeout(() => resolve(false), 250)),
|
|
535
|
+
]);
|
|
536
|
+
}
|
|
537
|
+
if (!drained) {
|
|
538
|
+
// 排空超时路径强制调用 fsyncSync + closeSync 结清刷盘
|
|
539
|
+
stdoutDrainer.forceFinalize();
|
|
540
|
+
stderrDrainer.forceFinalize();
|
|
541
|
+
}
|
|
542
|
+
const stdoutData = stdoutDrainer.getResult();
|
|
543
|
+
const stderrData = stderrDrainer.getResult();
|
|
544
|
+
const isTruncated = stdoutData.isTruncated || stderrData.isTruncated;
|
|
545
|
+
const spillError = stdoutData.spillError || stderrData.spillError || artifactsDirError;
|
|
546
|
+
const outputRef = (!spillError && (stdoutData.outputRef || stderrData.outputRef)) || undefined;
|
|
547
|
+
const outputHash = (!spillError && (stdoutData.outputHash || stderrData.outputHash)) || undefined;
|
|
548
|
+
// 校验进程身份
|
|
549
|
+
const idVerify = await this.driver.verifyIdentity(handle.identity);
|
|
550
|
+
const isResourceOrTimeoutStopped = opState.timedOut ||
|
|
551
|
+
opState.terminationReason === 'memory_exceeded' ||
|
|
552
|
+
opState.terminationReason === 'cpu_exceeded' ||
|
|
553
|
+
opState.terminationReason === 'pids_exceeded' ||
|
|
554
|
+
opState.terminationReason === 'output_exceeded';
|
|
555
|
+
const isSucceeded = exitResult.exitCode === 0 &&
|
|
556
|
+
!isResourceOrTimeoutStopped &&
|
|
557
|
+
!opState.cancelRequested;
|
|
558
|
+
const finalStatus = isResourceOrTimeoutStopped
|
|
559
|
+
? 'failed'
|
|
560
|
+
: opState.cancelRequested
|
|
561
|
+
? 'cancelled'
|
|
562
|
+
: isSucceeded
|
|
563
|
+
? 'succeeded'
|
|
564
|
+
: 'failed';
|
|
565
|
+
const terminationReason = opState.terminationReason || (finalStatus === 'succeeded' ? 'completed' : undefined);
|
|
566
|
+
const result = {
|
|
567
|
+
kind: 'process',
|
|
568
|
+
status: finalStatus,
|
|
569
|
+
exitCode: exitResult.exitCode,
|
|
570
|
+
signal: exitResult.signal,
|
|
571
|
+
stdout: stdoutData.content,
|
|
572
|
+
stderr: stderrData.content,
|
|
573
|
+
isTruncated,
|
|
574
|
+
stdoutTruncated: stdoutData.isTruncated,
|
|
575
|
+
stderrTruncated: stderrData.isTruncated,
|
|
576
|
+
stdoutRef: !stdoutData.spillError ? stdoutData.outputRef : undefined,
|
|
577
|
+
stderrRef: !stderrData.spillError ? stderrData.outputRef : undefined,
|
|
578
|
+
stdoutBytes: stdoutData.bytesSeen,
|
|
579
|
+
stderrBytes: stderrData.bytesSeen,
|
|
580
|
+
stdoutHash: !stdoutData.spillError ? stdoutData.outputHash : undefined,
|
|
581
|
+
stderrHash: !stderrData.spillError ? stderrData.outputHash : undefined,
|
|
582
|
+
...(residualProcessesReaped ? { residualProcessesReaped: true } : {}),
|
|
583
|
+
...(streamCallbackError ? { streamCallbackError } : {}),
|
|
584
|
+
...(spillError ? { spillError } : {}),
|
|
585
|
+
...(stdoutData.spillError ? { stdoutSpillError: stdoutData.spillError } : {}),
|
|
586
|
+
...(stderrData.spillError ? { stderrSpillError: stderrData.spillError } : {}),
|
|
587
|
+
outputRef,
|
|
588
|
+
outputHash,
|
|
589
|
+
terminationReason,
|
|
590
|
+
peakMemoryBytes,
|
|
591
|
+
cpuTimeMs: peakCpuTimeMs,
|
|
592
|
+
identityVerification: idVerify,
|
|
593
|
+
durationMs: Date.now() - startTime,
|
|
594
|
+
completedAt: new Date().toISOString(),
|
|
595
|
+
};
|
|
596
|
+
if (opState.stopPromise) {
|
|
597
|
+
const stopRes = await opState.stopPromise;
|
|
598
|
+
if (stopRes.stopped !== 'confirmed_stopped') {
|
|
599
|
+
const stored = this.domain.getStore().getOperation(options.opId);
|
|
600
|
+
if (stored && stored.status === 'done') {
|
|
601
|
+
return (stored.result ?? null);
|
|
602
|
+
}
|
|
603
|
+
const indetResult = {
|
|
604
|
+
kind: 'indeterminate',
|
|
605
|
+
status: 'indeterminate',
|
|
606
|
+
reason: `Process ${handle.identity.pid} was cancelled or stopped but could not be confirmed: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
607
|
+
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
608
|
+
durationMs: Date.now() - startTime,
|
|
609
|
+
completedAt: new Date().toISOString(),
|
|
610
|
+
};
|
|
611
|
+
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
501
612
|
}
|
|
502
|
-
const indetResult = {
|
|
503
|
-
kind: 'indeterminate',
|
|
504
|
-
status: 'indeterminate',
|
|
505
|
-
reason: `Process ${handle.identity.pid} was cancelled or stopped but could not be confirmed: ${stopRes.errorDetails || 'residual processes still alive'}`,
|
|
506
|
-
recoveryGuidance: 'Residual PID detected. Inspect system processes manually before releasing resources.',
|
|
507
|
-
durationMs: Date.now() - startTime,
|
|
508
|
-
completedAt: new Date().toISOString(),
|
|
509
|
-
};
|
|
510
|
-
return this.finalizeOperation(options.opId, indetResult, options.requiredResources, false);
|
|
511
613
|
}
|
|
614
|
+
// 6. [事务提交结果与释放资源 - Single Writer]
|
|
615
|
+
// 统一由 finalizeOperation 写入事实与处理资源
|
|
616
|
+
return this.finalizeOperation(options.opId, result, options.requiredResources, true, artifactsDir);
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
stdoutDrainer?.forceFinalize();
|
|
620
|
+
stderrDrainer?.forceFinalize();
|
|
621
|
+
if (timeoutTimer)
|
|
622
|
+
clearTimeout(timeoutTimer);
|
|
623
|
+
if (samplingInterval)
|
|
624
|
+
clearInterval(samplingInterval);
|
|
625
|
+
if (opState.stopResolve) {
|
|
626
|
+
const resolveFn = opState.stopResolve;
|
|
627
|
+
opState.stopResolve = undefined;
|
|
628
|
+
resolveFn({
|
|
629
|
+
stopped: 'confirmed_stopped',
|
|
630
|
+
scope: 'direct_child',
|
|
631
|
+
errorDetails: opState.phase === 'done' ? undefined : 'Operation terminated before process activation',
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
this.activeOperations.delete(options.opId);
|
|
512
635
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
finally {
|
|
518
|
-
stdoutDrainer?.forceFinalize();
|
|
519
|
-
stderrDrainer?.forceFinalize();
|
|
520
|
-
if (timeoutTimer)
|
|
521
|
-
clearTimeout(timeoutTimer);
|
|
522
|
-
if (samplingInterval)
|
|
523
|
-
clearInterval(samplingInterval);
|
|
524
|
-
if (opState.stopResolve) {
|
|
525
|
-
const resolveFn = opState.stopResolve;
|
|
526
|
-
opState.stopResolve = undefined;
|
|
527
|
-
resolveFn({
|
|
528
|
-
stopped: 'confirmed_stopped',
|
|
529
|
-
scope: 'direct_child',
|
|
530
|
-
errorDetails: opState.phase === 'done' ? undefined : 'Operation terminated before process activation',
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
this.activeOperations.delete(options.opId);
|
|
534
|
-
}
|
|
636
|
+
})();
|
|
637
|
+
opState.resultPromise = executionPromise;
|
|
638
|
+
return await executionPromise;
|
|
535
639
|
}
|
|
536
640
|
/**
|
|
537
641
|
* 停止确认流水线 (Stopping Pipeline)
|
|
@@ -662,6 +766,9 @@ export class ProcessSupervisor {
|
|
|
662
766
|
if (existing && existing.status === 'done') {
|
|
663
767
|
throw new Error(`Cannot finalize operation "${opId}": operation is already finalized with status "${existing.status}"`);
|
|
664
768
|
}
|
|
769
|
+
if (existing) {
|
|
770
|
+
result.runId = existing.runId;
|
|
771
|
+
}
|
|
665
772
|
if (result.kind === 'process') {
|
|
666
773
|
const procRes = result;
|
|
667
774
|
const targetDir = artifactsDir || path.join(this.domain.domainPath, 'artifacts');
|