aws-runtime-bridge 1.0.3 → 1.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/dist/adapter/adapter.test.js +4 -4
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +0 -7
- package/dist/adapter/types.test.js +5 -53
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +4 -0
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +36 -0
- package/dist/routes/runtime-binding.d.ts.map +1 -1
- package/dist/routes/runtime-binding.js +45 -0
- package/dist/routes/sessions.js +1 -1
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +48 -14
- package/dist/routes/terminal.test.js +6 -2
- package/dist/services/agent-process-manager.js +4 -4
- package/dist/services/auto-register.d.ts +9 -0
- package/dist/services/auto-register.d.ts.map +1 -1
- package/dist/services/auto-register.js +190 -32
- package/dist/services/aws-client-agent-mcp.test.js +3 -0
- package/dist/services/mcp-launch-binding-queue.d.ts +36 -0
- package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -0
- package/dist/services/mcp-launch-binding-queue.js +92 -0
- package/dist/services/mcp-launch-binding-queue.test.d.ts +2 -0
- package/dist/services/mcp-launch-binding-queue.test.d.ts.map +1 -0
- package/dist/services/mcp-launch-binding-queue.test.js +107 -0
- package/dist/services/orphan-monitor.js +1 -1
- package/dist/services/process-detector.d.ts +1 -1
- package/dist/services/process-detector.d.ts.map +1 -1
- package/dist/services/process-detector.js +2 -11
- package/dist/services/process-registry.d.ts +1 -0
- package/dist/services/process-registry.d.ts.map +1 -1
- package/dist/services/process-registry.js +129 -108
- package/dist/services/terminal-persistence.d.ts.map +1 -1
- package/dist/services/terminal-persistence.js +47 -37
- package/dist/services/terminal-persistence.test.js +47 -1
- package/dist/utils/file-utils.d.ts +3 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +32 -0
- package/package/aws-client-agent-mcp/README.md +288 -288
- package/package.json +76 -76
- package/dist/routes/aws-mcp.d.ts +0 -10
- package/dist/routes/aws-mcp.d.ts.map +0 -1
- package/dist/routes/aws-mcp.js +0 -74
- package/dist/routes/aws-mcp.test.d.ts +0 -2
- package/dist/routes/aws-mcp.test.d.ts.map +0 -1
- package/dist/routes/aws-mcp.test.js +0 -42
- package/dist/routes/memory.d.ts +0 -13
- package/dist/routes/memory.d.ts.map +0 -1
- package/dist/routes/memory.js +0 -429
- package/dist/services/aws-mcp-http.d.ts +0 -11
- package/dist/services/aws-mcp-http.d.ts.map +0 -1
- package/dist/services/aws-mcp-http.js +0 -225
- package/dist/services/aws-mcp-http.test.d.ts +0 -2
- package/dist/services/aws-mcp-http.test.d.ts.map +0 -1
- package/dist/services/aws-mcp-http.test.js +0 -27
- package/dist/services/easytier-manager.d.ts +0 -106
- package/dist/services/easytier-manager.d.ts.map +0 -1
- package/dist/services/easytier-manager.js +0 -331
- package/dist/services/easytier-manager.test.d.ts +0 -5
- package/dist/services/easytier-manager.test.d.ts.map +0 -1
- package/dist/services/easytier-manager.test.js +0 -98
- package/dist/services/memory-service.d.ts +0 -195
- package/dist/services/memory-service.d.ts.map +0 -1
- package/dist/services/memory-service.js +0 -650
- package/dist/services/session-lookup.d.ts +0 -20
- package/dist/services/session-lookup.d.ts.map +0 -1
- package/dist/services/session-lookup.js +0 -43
- package/dist/services/user-api-key-service.d.ts +0 -28
- package/dist/services/user-api-key-service.d.ts.map +0 -1
- package/dist/services/user-api-key-service.js +0 -75
- package/node_modules/@cc-switch/sdk/dist/adapters/common.d.ts +0 -38
- package/node_modules/@cc-switch/sdk/dist/adapters/common.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/common.js +0 -47
- package/node_modules/@cc-switch/sdk/dist/adapters/index.d.ts +0 -5
- package/node_modules/@cc-switch/sdk/dist/adapters/index.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/index.js +0 -28
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claude.d.ts +0 -10
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claude.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claude.js +0 -39
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claudecode.d.ts +0 -10
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claudecode.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-claudecode.js +0 -40
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.d.ts +0 -18
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.js +0 -63
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.test.d.ts +0 -2
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.test.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-opencode.test.js +0 -86
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-placeholder.d.ts +0 -9
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-placeholder.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/mcp-placeholder.js +0 -14
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-claude.d.ts +0 -10
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-claude.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-claude.js +0 -51
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-claudecode.d.ts +0 -10
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-claudecode.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-claudecode.js +0 -51
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-opencode.d.ts +0 -10
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-opencode.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-opencode.js +0 -51
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-placeholder.d.ts +0 -9
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-placeholder.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/adapters/skill-placeholder.js +0 -14
- package/node_modules/@cc-switch/sdk/dist/services/instance-service.d.ts +0 -78
- package/node_modules/@cc-switch/sdk/dist/services/instance-service.d.ts.map +0 -1
- package/node_modules/@cc-switch/sdk/dist/services/instance-service.js +0 -180
- package/package/cc-switch-sdk/dist/adapters/common.d.ts +0 -38
- package/package/cc-switch-sdk/dist/adapters/common.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/common.js +0 -47
- package/package/cc-switch-sdk/dist/adapters/index.d.ts +0 -5
- package/package/cc-switch-sdk/dist/adapters/index.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/index.js +0 -28
- package/package/cc-switch-sdk/dist/adapters/mcp-claude.d.ts +0 -10
- package/package/cc-switch-sdk/dist/adapters/mcp-claude.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/mcp-claude.js +0 -39
- package/package/cc-switch-sdk/dist/adapters/mcp-claudecode.d.ts +0 -10
- package/package/cc-switch-sdk/dist/adapters/mcp-claudecode.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/mcp-claudecode.js +0 -40
- package/package/cc-switch-sdk/dist/adapters/mcp-opencode.d.ts +0 -18
- package/package/cc-switch-sdk/dist/adapters/mcp-opencode.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/mcp-opencode.js +0 -63
- package/package/cc-switch-sdk/dist/adapters/mcp-opencode.test.d.ts +0 -2
- package/package/cc-switch-sdk/dist/adapters/mcp-opencode.test.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/mcp-opencode.test.js +0 -86
- package/package/cc-switch-sdk/dist/adapters/mcp-placeholder.d.ts +0 -9
- package/package/cc-switch-sdk/dist/adapters/mcp-placeholder.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/mcp-placeholder.js +0 -14
- package/package/cc-switch-sdk/dist/adapters/skill-claude.d.ts +0 -10
- package/package/cc-switch-sdk/dist/adapters/skill-claude.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/skill-claude.js +0 -51
- package/package/cc-switch-sdk/dist/adapters/skill-claudecode.d.ts +0 -10
- package/package/cc-switch-sdk/dist/adapters/skill-claudecode.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/skill-claudecode.js +0 -51
- package/package/cc-switch-sdk/dist/adapters/skill-opencode.d.ts +0 -10
- package/package/cc-switch-sdk/dist/adapters/skill-opencode.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/skill-opencode.js +0 -51
- package/package/cc-switch-sdk/dist/adapters/skill-placeholder.d.ts +0 -9
- package/package/cc-switch-sdk/dist/adapters/skill-placeholder.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/adapters/skill-placeholder.js +0 -14
- package/package/cc-switch-sdk/dist/services/instance-service.d.ts +0 -78
- package/package/cc-switch-sdk/dist/services/instance-service.d.ts.map +0 -1
- package/package/cc-switch-sdk/dist/services/instance-service.js +0 -180
|
@@ -715,7 +715,7 @@ function getChildProcesses(pid) {
|
|
|
715
715
|
* @param timeoutMs 超时时间(毫秒)
|
|
716
716
|
* @returns 是否成功终止
|
|
717
717
|
*/
|
|
718
|
-
export function waitForProcessExit(pid, timeoutMs = 5000) {
|
|
718
|
+
export async function waitForProcessExit(pid, timeoutMs = 5000) {
|
|
719
719
|
const startTime = Date.now();
|
|
720
720
|
while (Date.now() - startTime < timeoutMs) {
|
|
721
721
|
if (!isProcessRunning(pid)) {
|
|
@@ -724,16 +724,7 @@ export function waitForProcessExit(pid, timeoutMs = 5000) {
|
|
|
724
724
|
// 等待 100ms 后重试
|
|
725
725
|
const delay = Math.min(100, timeoutMs - (Date.now() - startTime));
|
|
726
726
|
if (delay > 0) {
|
|
727
|
-
|
|
728
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
|
|
729
|
-
}
|
|
730
|
-
catch {
|
|
731
|
-
// SharedArrayBuffer 不可用时回退到 setTimeout
|
|
732
|
-
const startWait = Date.now();
|
|
733
|
-
while (Date.now() - startWait < delay) {
|
|
734
|
-
// 忙等待
|
|
735
|
-
}
|
|
736
|
-
}
|
|
727
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
737
728
|
}
|
|
738
729
|
}
|
|
739
730
|
return false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"process-registry.d.ts","sourceRoot":"","sources":["../../src/services/process-registry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAUH;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAC;AAEvG;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED;;;;GAIG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyC;IACjE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAkC;IAE/D;;OAEG;IACH,OAAO;IAEP;;;;OAIG;WACW,WAAW,IAAI,eAAe;IAO5C;;OAEG;YACW,OAAO;
|
|
1
|
+
{"version":3,"file":"process-registry.d.ts","sourceRoot":"","sources":["../../src/services/process-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@ import os from 'node:os';
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { promises as fs } from 'node:fs';
|
|
9
9
|
import { createLogger } from '../utils/logger.js';
|
|
10
|
-
import {
|
|
10
|
+
import { atomicWriteJsonFile, withFileLock } from '../utils/file-utils.js';
|
|
11
11
|
const log = createLogger('process-registry');
|
|
12
12
|
/**
|
|
13
13
|
* 获取持久化注册表文件路径
|
|
@@ -47,15 +47,17 @@ export class ProcessRegistry {
|
|
|
47
47
|
async persist() {
|
|
48
48
|
const filePath = getProcessRegistryFile();
|
|
49
49
|
try {
|
|
50
|
-
await ensureDir(path.dirname(filePath));
|
|
51
50
|
const records = Array.from(this.records.values());
|
|
52
|
-
await
|
|
51
|
+
await atomicWriteJsonFile(filePath, records);
|
|
53
52
|
log.debug(`[persist] 已持久化 ${records.length} 条进程记录`);
|
|
54
53
|
}
|
|
55
54
|
catch (error) {
|
|
56
55
|
log.error('[persist] 持久化失败:', error);
|
|
57
56
|
}
|
|
58
57
|
}
|
|
58
|
+
async mutate(operation) {
|
|
59
|
+
return withFileLock(getProcessRegistryFile(), async () => operation());
|
|
60
|
+
}
|
|
59
61
|
/**
|
|
60
62
|
* 注册新进程
|
|
61
63
|
*
|
|
@@ -63,19 +65,21 @@ export class ProcessRegistry {
|
|
|
63
65
|
* @returns 是否注册成功
|
|
64
66
|
*/
|
|
65
67
|
async register(record) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
await this.mutate(async () => {
|
|
69
|
+
const fullRecord = {
|
|
70
|
+
...record,
|
|
71
|
+
createdAt: new Date().toISOString(),
|
|
72
|
+
};
|
|
73
|
+
// 如果已存在相同 agentId,先移除旧的映射
|
|
74
|
+
const existing = this.records.get(record.agentId);
|
|
75
|
+
if (existing) {
|
|
76
|
+
this.pidToAgentId.delete(existing.pid);
|
|
77
|
+
}
|
|
78
|
+
this.records.set(record.agentId, fullRecord);
|
|
79
|
+
this.pidToAgentId.set(record.pid, record.agentId);
|
|
80
|
+
log.info(`[register] 注册进程: agentId=${record.agentId}, pid=${record.pid}, mode=${record.mode}, state=${record.state}`);
|
|
81
|
+
await this.persist();
|
|
82
|
+
});
|
|
79
83
|
}
|
|
80
84
|
/**
|
|
81
85
|
* 更新进程记录
|
|
@@ -85,20 +89,22 @@ export class ProcessRegistry {
|
|
|
85
89
|
* @returns 是否更新成功
|
|
86
90
|
*/
|
|
87
91
|
async update(agentId, updates) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
return this.mutate(async () => {
|
|
93
|
+
const record = this.records.get(agentId);
|
|
94
|
+
if (!record) {
|
|
95
|
+
log.warn(`[update] 未找到进程: agentId=${agentId}`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
// 如果更新包含 pid,需要更新 pid 映射
|
|
99
|
+
if (updates.pid !== undefined && updates.pid !== record.pid) {
|
|
100
|
+
this.pidToAgentId.delete(record.pid);
|
|
101
|
+
this.pidToAgentId.set(updates.pid, agentId);
|
|
102
|
+
}
|
|
103
|
+
Object.assign(record, updates);
|
|
104
|
+
log.debug(`[update] 更新进程: agentId=${agentId}, updates=${JSON.stringify(updates)}`);
|
|
105
|
+
await this.persist();
|
|
106
|
+
return true;
|
|
107
|
+
});
|
|
102
108
|
}
|
|
103
109
|
/**
|
|
104
110
|
* 通过 agentId 获取进程记录
|
|
@@ -164,16 +170,18 @@ export class ProcessRegistry {
|
|
|
164
170
|
* @returns 是否标记成功
|
|
165
171
|
*/
|
|
166
172
|
async markOrphaned(agentId) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
return this.mutate(async () => {
|
|
174
|
+
const record = this.records.get(agentId);
|
|
175
|
+
if (!record) {
|
|
176
|
+
log.warn(`[markOrphaned] 未找到进程: agentId=${agentId}`);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
record.state = 'orphaned';
|
|
180
|
+
record.orphanedAt = new Date().toISOString();
|
|
181
|
+
log.info(`[markOrphaned] 标记进程为孤儿: agentId=${agentId}, orphanedAt=${record.orphanedAt}`);
|
|
182
|
+
await this.persist();
|
|
183
|
+
return true;
|
|
184
|
+
});
|
|
177
185
|
}
|
|
178
186
|
/**
|
|
179
187
|
* 标记进程为终止状态
|
|
@@ -182,15 +190,17 @@ export class ProcessRegistry {
|
|
|
182
190
|
* @returns 是否标记成功
|
|
183
191
|
*/
|
|
184
192
|
async markTerminated(agentId) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
return this.mutate(async () => {
|
|
194
|
+
const record = this.records.get(agentId);
|
|
195
|
+
if (!record) {
|
|
196
|
+
log.warn(`[markTerminated] 未找到进程: agentId=${agentId}`);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
record.state = 'terminated';
|
|
200
|
+
log.info(`[markTerminated] 标记进程为终止: agentId=${agentId}`);
|
|
201
|
+
await this.persist();
|
|
202
|
+
return true;
|
|
203
|
+
});
|
|
194
204
|
}
|
|
195
205
|
/**
|
|
196
206
|
* 移除进程记录
|
|
@@ -199,16 +209,18 @@ export class ProcessRegistry {
|
|
|
199
209
|
* @returns 是否移除成功
|
|
200
210
|
*/
|
|
201
211
|
async remove(agentId) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
+
return this.mutate(async () => {
|
|
213
|
+
const record = this.records.get(agentId);
|
|
214
|
+
if (!record) {
|
|
215
|
+
log.warn(`[remove] 未找到进程: agentId=${agentId}`);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
this.records.delete(agentId);
|
|
219
|
+
this.pidToAgentId.delete(record.pid);
|
|
220
|
+
log.info(`[remove] 移除进程: agentId=${agentId}, pid=${record.pid}`);
|
|
221
|
+
await this.persist();
|
|
222
|
+
return true;
|
|
223
|
+
});
|
|
212
224
|
}
|
|
213
225
|
/**
|
|
214
226
|
* 更新进程心跳时间
|
|
@@ -217,15 +229,17 @@ export class ProcessRegistry {
|
|
|
217
229
|
* @returns 是否更新成功
|
|
218
230
|
*/
|
|
219
231
|
async updateHeartbeat(agentId) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
232
|
+
return this.mutate(async () => {
|
|
233
|
+
const record = this.records.get(agentId);
|
|
234
|
+
if (!record) {
|
|
235
|
+
log.warn(`[updateHeartbeat] 未找到进程: agentId=${agentId}`);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
record.lastHeartbeat = new Date().toISOString();
|
|
239
|
+
log.debug(`[updateHeartbeat] 更新心跳: agentId=${agentId}, lastHeartbeat=${record.lastHeartbeat}`);
|
|
240
|
+
await this.persist();
|
|
241
|
+
return true;
|
|
242
|
+
});
|
|
229
243
|
}
|
|
230
244
|
/**
|
|
231
245
|
* 更新进程健康状态
|
|
@@ -235,15 +249,17 @@ export class ProcessRegistry {
|
|
|
235
249
|
* @returns 是否更新成功
|
|
236
250
|
*/
|
|
237
251
|
async updateHealth(agentId, health) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
252
|
+
return this.mutate(async () => {
|
|
253
|
+
const record = this.records.get(agentId);
|
|
254
|
+
if (!record) {
|
|
255
|
+
log.warn(`[updateHealth] 未找到进程: agentId=${agentId}`);
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
record.healthStatus = health;
|
|
259
|
+
log.debug(`[updateHealth] 更新健康状态: agentId=${agentId}, health=${JSON.stringify(health)}`);
|
|
260
|
+
await this.persist();
|
|
261
|
+
return true;
|
|
262
|
+
});
|
|
247
263
|
}
|
|
248
264
|
/**
|
|
249
265
|
* 从持久化会话重建注册表
|
|
@@ -251,30 +267,32 @@ export class ProcessRegistry {
|
|
|
251
267
|
* @param sessions - 持久化会话数组
|
|
252
268
|
*/
|
|
253
269
|
async rebuildFromPersisted(sessions) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
270
|
+
await this.mutate(async () => {
|
|
271
|
+
log.info(`[rebuildFromPersisted] 从 ${sessions.length} 个持久化会话重建注册表`);
|
|
272
|
+
this.records.clear();
|
|
273
|
+
this.pidToAgentId.clear();
|
|
274
|
+
for (const session of sessions) {
|
|
275
|
+
if (!session.pid) {
|
|
276
|
+
log.warn(`[rebuildFromPersisted] 跳过无 PID 的会话: agentId=${session.agentId}`);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const record = {
|
|
280
|
+
agentId: session.agentId,
|
|
281
|
+
sessionId: session.sessionId,
|
|
282
|
+
pid: session.pid,
|
|
283
|
+
mode: session.mode || 'sdk',
|
|
284
|
+
state: 'unknown', // 重建时状态未知,需要外部检测
|
|
285
|
+
workspacePath: session.workspacePath || '',
|
|
286
|
+
command: session.command || '',
|
|
287
|
+
createdAt: new Date().toISOString(),
|
|
288
|
+
};
|
|
289
|
+
this.records.set(record.agentId, record);
|
|
290
|
+
this.pidToAgentId.set(record.pid, record.agentId);
|
|
291
|
+
log.debug(`[rebuildFromPersisted] 重建进程记录: agentId=${record.agentId}, pid=${record.pid}`);
|
|
261
292
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
pid: session.pid,
|
|
266
|
-
mode: session.mode || 'sdk',
|
|
267
|
-
state: 'unknown', // 重建时状态未知,需要外部检测
|
|
268
|
-
workspacePath: session.workspacePath || '',
|
|
269
|
-
command: session.command || '',
|
|
270
|
-
createdAt: new Date().toISOString(),
|
|
271
|
-
};
|
|
272
|
-
this.records.set(record.agentId, record);
|
|
273
|
-
this.pidToAgentId.set(record.pid, record.agentId);
|
|
274
|
-
log.debug(`[rebuildFromPersisted] 重建进程记录: agentId=${record.agentId}, pid=${record.pid}`);
|
|
275
|
-
}
|
|
276
|
-
log.info(`[rebuildFromPersisted] 重建完成,共 ${this.records.size} 条记录`);
|
|
277
|
-
await this.persist();
|
|
293
|
+
log.info(`[rebuildFromPersisted] 重建完成,共 ${this.records.size} 条记录`);
|
|
294
|
+
await this.persist();
|
|
295
|
+
});
|
|
278
296
|
}
|
|
279
297
|
/**
|
|
280
298
|
* 导出所有进程记录用于持久化
|
|
@@ -308,11 +326,13 @@ export class ProcessRegistry {
|
|
|
308
326
|
* 清空所有进程记录
|
|
309
327
|
*/
|
|
310
328
|
async clear() {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
329
|
+
await this.mutate(async () => {
|
|
330
|
+
const count = this.records.size;
|
|
331
|
+
this.records.clear();
|
|
332
|
+
this.pidToAgentId.clear();
|
|
333
|
+
log.info(`[clear] 清空所有进程记录: ${count} 条`);
|
|
334
|
+
await this.persist();
|
|
335
|
+
});
|
|
316
336
|
}
|
|
317
337
|
}
|
|
318
338
|
ProcessRegistry.instance = null;
|
|
@@ -348,7 +368,8 @@ export async function loadPersistedProcessRegistry() {
|
|
|
348
368
|
*/
|
|
349
369
|
export async function savePersistedProcessRegistry(records) {
|
|
350
370
|
const filePath = getProcessRegistryFile();
|
|
351
|
-
await
|
|
352
|
-
|
|
353
|
-
|
|
371
|
+
await withFileLock(filePath, async () => {
|
|
372
|
+
await atomicWriteJsonFile(filePath, records);
|
|
373
|
+
log.debug(`[savePersistedProcessRegistry] 保存了 ${records.length} 条进程记录`);
|
|
374
|
+
});
|
|
354
375
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terminal-persistence.d.ts","sourceRoot":"","sources":["../../src/services/terminal-persistence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"terminal-persistence.d.ts","sourceRoot":"","sources":["../../src/services/terminal-persistence.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA6BpD;;;;GAIG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAOzE;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAKvF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrF;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB7E;AAED;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAGvC;AAED;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBpF"}
|
|
@@ -6,9 +6,30 @@
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import { promises as fs } from 'node:fs';
|
|
9
|
-
import {
|
|
9
|
+
import { atomicWriteJsonFile, withFileLock } from '../utils/file-utils.js';
|
|
10
10
|
import { createLogger } from '../utils/logger.js';
|
|
11
11
|
const log = createLogger('terminal-persistence');
|
|
12
|
+
async function readAllPersistedSessions(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const err = error;
|
|
19
|
+
if (err.code !== 'ENOENT') {
|
|
20
|
+
log.warn(`[readAllPersistedSessions] 读取持久化会话失败: ${filePath}, error=${err.message}`);
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function updatePersistedSessions(updater) {
|
|
26
|
+
const filePath = getPersistedSessionsFile();
|
|
27
|
+
await withFileLock(filePath, async () => {
|
|
28
|
+
const sessions = await readAllPersistedSessions(filePath);
|
|
29
|
+
const nextSessions = await updater(sessions);
|
|
30
|
+
await atomicWriteJsonFile(filePath, nextSessions);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
12
33
|
/**
|
|
13
34
|
* 获取持久化会话文件路径
|
|
14
35
|
*
|
|
@@ -24,15 +45,11 @@ export function getPersistedSessionsFile() {
|
|
|
24
45
|
*/
|
|
25
46
|
export async function loadPersistedSessions() {
|
|
26
47
|
const filePath = getPersistedSessionsFile();
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const sessions = JSON.parse(raw);
|
|
48
|
+
return withFileLock(filePath, async () => {
|
|
49
|
+
const sessions = await readAllPersistedSessions(filePath);
|
|
30
50
|
// 只返回状态为 running 的会话
|
|
31
51
|
return sessions.filter(s => s.status === 'running');
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
return [];
|
|
35
|
-
}
|
|
52
|
+
});
|
|
36
53
|
}
|
|
37
54
|
/**
|
|
38
55
|
* 保存持久化的终端会话列表
|
|
@@ -41,8 +58,9 @@ export async function loadPersistedSessions() {
|
|
|
41
58
|
*/
|
|
42
59
|
export async function savePersistedSessions(sessions) {
|
|
43
60
|
const filePath = getPersistedSessionsFile();
|
|
44
|
-
await
|
|
45
|
-
|
|
61
|
+
await withFileLock(filePath, async () => {
|
|
62
|
+
await atomicWriteJsonFile(filePath, sessions);
|
|
63
|
+
});
|
|
46
64
|
}
|
|
47
65
|
/**
|
|
48
66
|
* 添加或更新持久化会话
|
|
@@ -50,15 +68,17 @@ export async function savePersistedSessions(sessions) {
|
|
|
50
68
|
* @param session - 会话数据
|
|
51
69
|
*/
|
|
52
70
|
export async function upsertPersistedSession(session) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
await updatePersistedSessions((sessions) => {
|
|
72
|
+
const runningSessions = sessions.filter(s => s.status === 'running');
|
|
73
|
+
const index = runningSessions.findIndex(s => s.sessionId === session.sessionId);
|
|
74
|
+
if (index >= 0) {
|
|
75
|
+
runningSessions[index] = session;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
runningSessions.push(session);
|
|
79
|
+
}
|
|
80
|
+
return runningSessions;
|
|
81
|
+
});
|
|
62
82
|
}
|
|
63
83
|
/**
|
|
64
84
|
* 移除持久化会话(从完整文件中删除,包括 stopped 状态的)
|
|
@@ -67,9 +87,8 @@ export async function upsertPersistedSession(session) {
|
|
|
67
87
|
*/
|
|
68
88
|
export async function removePersistedSession(sessionId) {
|
|
69
89
|
const filePath = getPersistedSessionsFile();
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const allSessions = JSON.parse(raw);
|
|
90
|
+
await withFileLock(filePath, async () => {
|
|
91
|
+
const allSessions = await readAllPersistedSessions(filePath);
|
|
73
92
|
const beforeCount = allSessions.length;
|
|
74
93
|
const filtered = allSessions.filter(s => s.sessionId !== sessionId);
|
|
75
94
|
const afterCount = filtered.length;
|
|
@@ -79,12 +98,8 @@ export async function removePersistedSession(sessionId) {
|
|
|
79
98
|
else {
|
|
80
99
|
log.warn(`[removePersistedSession] 未找到要删除的会话: sessionId=${sessionId}, 当前会话数=${beforeCount}`);
|
|
81
100
|
}
|
|
82
|
-
await
|
|
83
|
-
}
|
|
84
|
-
catch (error) {
|
|
85
|
-
// 文件不存在,无需删除
|
|
86
|
-
log.debug(`[removePersistedSession] 文件不存在或读取失败: ${filePath}`);
|
|
87
|
-
}
|
|
101
|
+
await atomicWriteJsonFile(filePath, filtered);
|
|
102
|
+
});
|
|
88
103
|
}
|
|
89
104
|
/**
|
|
90
105
|
* 根据 agentId 查找持久化会话
|
|
@@ -104,9 +119,8 @@ export async function findPersistedSessionByAgentId(agentId) {
|
|
|
104
119
|
*/
|
|
105
120
|
export async function removePersistedSessionByAgentId(agentId) {
|
|
106
121
|
const filePath = getPersistedSessionsFile();
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
const allSessions = JSON.parse(raw);
|
|
122
|
+
await withFileLock(filePath, async () => {
|
|
123
|
+
const allSessions = await readAllPersistedSessions(filePath);
|
|
110
124
|
const beforeCount = allSessions.length;
|
|
111
125
|
const filtered = allSessions.filter(s => s.agentId !== agentId);
|
|
112
126
|
const afterCount = filtered.length;
|
|
@@ -116,10 +130,6 @@ export async function removePersistedSessionByAgentId(agentId) {
|
|
|
116
130
|
else {
|
|
117
131
|
log.debug(`[removePersistedSessionByAgentId] 未找到要删除的会话: agentId=${agentId}`);
|
|
118
132
|
}
|
|
119
|
-
await
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
// 文件不存在,无需删除
|
|
123
|
-
log.debug(`[removePersistedSessionByAgentId] 文件不存在或读取失败: ${filePath}`);
|
|
124
|
-
}
|
|
133
|
+
await atomicWriteJsonFile(filePath, filtered);
|
|
134
|
+
});
|
|
125
135
|
}
|
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Terminal Persistence 服务单元测试
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
const tmpRoot = path.join(process.cwd(), '.tmp-vitest-terminal-persistence');
|
|
8
|
+
vi.mock('node:os', () => ({
|
|
9
|
+
default: {
|
|
10
|
+
tmpdir: () => tmpRoot,
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
const persistence = await import('./terminal-persistence.js');
|
|
14
|
+
function makeSession(sessionId, agentId) {
|
|
15
|
+
return {
|
|
16
|
+
sessionId,
|
|
17
|
+
agentId,
|
|
18
|
+
workspacePath: `/workspace/${agentId}`,
|
|
19
|
+
command: 'claude',
|
|
20
|
+
startedAt: new Date(0).toISOString(),
|
|
21
|
+
status: 'running',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
5
24
|
describe('terminal-persistence service', () => {
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await fs.rm(tmpRoot, { recursive: true, force: true });
|
|
30
|
+
});
|
|
6
31
|
it('generates correct file path', () => {
|
|
7
32
|
const getPersistedSessionsFile = (tmpdir) => {
|
|
8
33
|
return `${tmpdir}/agentswork-runtime-bridge/sessions.json`;
|
|
@@ -85,4 +110,25 @@ describe('terminal-persistence service', () => {
|
|
|
85
110
|
};
|
|
86
111
|
expect(parseSessions('invalid json')).toEqual([]);
|
|
87
112
|
});
|
|
113
|
+
it('serializes concurrent upserts without losing sessions', async () => {
|
|
114
|
+
await Promise.all([
|
|
115
|
+
persistence.upsertPersistedSession(makeSession('session-1', 'agent-1')),
|
|
116
|
+
persistence.upsertPersistedSession(makeSession('session-2', 'agent-2')),
|
|
117
|
+
persistence.upsertPersistedSession(makeSession('session-3', 'agent-3')),
|
|
118
|
+
]);
|
|
119
|
+
const sessions = await persistence.loadPersistedSessions();
|
|
120
|
+
expect(sessions.map(session => session.sessionId).sort()).toEqual(['session-1', 'session-2', 'session-3']);
|
|
121
|
+
});
|
|
122
|
+
it('does not revive a removed session during concurrent writes', async () => {
|
|
123
|
+
await persistence.savePersistedSessions([
|
|
124
|
+
makeSession('session-1', 'agent-1'),
|
|
125
|
+
makeSession('session-2', 'agent-2'),
|
|
126
|
+
]);
|
|
127
|
+
await Promise.all([
|
|
128
|
+
persistence.removePersistedSession('session-1'),
|
|
129
|
+
persistence.upsertPersistedSession(makeSession('session-3', 'agent-3')),
|
|
130
|
+
]);
|
|
131
|
+
const sessions = await persistence.loadPersistedSessions();
|
|
132
|
+
expect(sessions.map(session => session.sessionId).sort()).toEqual(['session-2', 'session-3']);
|
|
133
|
+
});
|
|
88
134
|
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { SkillPackage, DownloadResult, Skill } from '../types.js';
|
|
2
2
|
import type { AppFlags, InstalledSkill, CcSwitchSdk } from '@cc-switch/sdk';
|
|
3
3
|
export declare function ensureDir(targetDir: string): Promise<string>;
|
|
4
|
+
export declare function withFileLock<T>(targetPath: string, operation: () => Promise<T>): Promise<T>;
|
|
5
|
+
export declare function atomicWriteFile(targetPath: string, content: string): Promise<void>;
|
|
6
|
+
export declare function atomicWriteJsonFile(targetPath: string, value: unknown): Promise<void>;
|
|
4
7
|
export declare function downloadSkillZip(skillPackage: SkillPackage, targetDir: string): Promise<DownloadResult | null>;
|
|
5
8
|
export declare function runCommand(command: string, args: string[]): Promise<void>;
|
|
6
9
|
export declare function escapePowerShellLiteral(text: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/utils/file-utils.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAI5E,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAGlE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAqBjG;AAED,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWxF;AAED,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3F;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,YAAY,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA0ChC;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/E;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhG;AAED,wBAAsB,oCAAoC,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CASlG;AAED,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,QAAQ,CAW1E;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAM1D;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKhF;AAED,wBAAsB,wBAAwB,CAC5C,GAAG,EAAE,WAAW,EAChB,YAAY,EAAE,YAAY,EAC1B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAyBhC"}
|
package/dist/utils/file-utils.js
CHANGED
|
@@ -4,10 +4,42 @@ import { promises as fs } from 'node:fs';
|
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import axios from 'axios';
|
|
6
6
|
import { schedulerBaseUrl, runtimeToken } from '../config.js';
|
|
7
|
+
const fileLocks = new Map();
|
|
7
8
|
export async function ensureDir(targetDir) {
|
|
8
9
|
await fs.mkdir(targetDir, { recursive: true });
|
|
9
10
|
return targetDir;
|
|
10
11
|
}
|
|
12
|
+
export async function withFileLock(targetPath, operation) {
|
|
13
|
+
const lockKey = path.resolve(targetPath);
|
|
14
|
+
const previous = fileLocks.get(lockKey) ?? Promise.resolve();
|
|
15
|
+
let release;
|
|
16
|
+
const current = new Promise((resolve) => {
|
|
17
|
+
release = resolve;
|
|
18
|
+
});
|
|
19
|
+
const chained = previous.then(() => current);
|
|
20
|
+
fileLocks.set(lockKey, chained);
|
|
21
|
+
await previous;
|
|
22
|
+
try {
|
|
23
|
+
return await operation();
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
release();
|
|
27
|
+
if (fileLocks.get(lockKey) === chained) {
|
|
28
|
+
fileLocks.delete(lockKey);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function atomicWriteFile(targetPath, content) {
|
|
33
|
+
await ensureDir(path.dirname(targetPath));
|
|
34
|
+
const targetDir = path.dirname(targetPath);
|
|
35
|
+
const targetBase = path.basename(targetPath);
|
|
36
|
+
const tmpPath = path.join(targetDir, `.${targetBase}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
37
|
+
await fs.writeFile(tmpPath, content, 'utf-8');
|
|
38
|
+
await fs.rename(tmpPath, targetPath);
|
|
39
|
+
}
|
|
40
|
+
export async function atomicWriteJsonFile(targetPath, value) {
|
|
41
|
+
await atomicWriteFile(targetPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
42
|
+
}
|
|
11
43
|
export async function downloadSkillZip(skillPackage, targetDir) {
|
|
12
44
|
if (!skillPackage || !skillPackage.downloadUrl) {
|
|
13
45
|
return null;
|