zen-gitsync 2.16.36 → 2.16.38
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/package.json +1 -1
- package/src/ui/public/assets/AgentView-DWZx4tNO.js +4 -0
- package/src/ui/public/assets/AppVersionBadge-CE0OhuMK.js +2 -0
- package/src/ui/public/assets/BranchSelector-DnNH_zRm.js +1 -0
- package/src/ui/public/assets/CommitForm-Da9hF4Ow.js +6 -0
- package/src/ui/public/assets/CommonDialog-ByvhJIxb.js +1 -0
- package/src/ui/public/assets/CommonDialog-Cyh3xu0p.css +1 -0
- package/src/ui/public/assets/EditorView-0UgFqhAH.js +1 -0
- package/src/ui/public/assets/FlowExecutionViewer-CiK6fWX0.js +1 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BiTuDHu6.js +21 -0
- package/src/ui/public/assets/LogList-BJey2eS5.js +7 -0
- package/src/ui/public/assets/MindmapView-DGbJgFMZ.js +1 -0
- package/src/ui/public/assets/MonitorView-CXrl7ym6.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-Dr8VCQSL.js +1 -0
- package/src/ui/public/assets/RemoteRepoCard-pjxWmG7e.js +1 -0
- package/src/ui/public/assets/SourceMapView-DVcv9WM2.js +3 -0
- package/src/ui/public/assets/SvgIcon-BQ6bR7l0.css +1 -0
- package/src/ui/public/assets/SvgIcon-ryKBwC8P.js +1 -0
- package/src/ui/public/assets/UserInputNode-8v5LCzR9.js +1 -0
- package/src/ui/public/assets/WorkbenchView-BN_6Pb8H.js +12 -0
- package/src/ui/public/assets/_plugin-vue_export-helper-C5Vpcpi0.js +17 -0
- package/src/ui/public/assets/configStore-DdYe2Jg3.js +1 -0
- package/src/ui/public/assets/element-plus-Cchqf1s7.js +19 -0
- package/src/ui/public/assets/element-plus-D3Dn7nX0.css +1 -0
- package/src/ui/public/assets/{flow-mindmap-CatiJi_w.js → flow-mindmap-D4bc307Q.js} +1 -1
- package/src/ui/public/assets/index-D-n9_1ON.js +46 -0
- package/src/ui/public/assets/index-DDmVbc4D.css +1 -0
- package/src/ui/public/assets/{monaco-DPQ3OaiL.js → monaco-43aputPh.js} +2 -2
- package/src/ui/public/assets/office-docx-BHL8ws1l.js +31 -0
- package/src/ui/public/assets/{office-excel-BGq5miA9.js → office-excel-CgdzbtOn.js} +2 -2
- package/src/ui/public/assets/{office-pptx-CNefnIUm.js → office-pptx-DyxMcWQh.js} +1 -1
- package/src/ui/public/assets/socket-io-BZIzo1US.js +1 -0
- package/src/ui/public/assets/toolsStore-C0VjU2dD.js +1 -0
- package/src/ui/public/assets/{ts.worker-METxwbDZ.js → ts.worker-Dth06zuC.js} +16 -1
- package/src/ui/public/assets/{vendor-C_cx2RSh.js → vendor-FHXYVNcm.js} +36 -36
- package/src/ui/public/assets/{vue-flow-CBbapQM_.js → vue-flow-3jJr-V2W.js} +2 -2
- package/src/ui/public/index.html +16 -9
- package/src/ui/server/index.js +11 -3
- package/src/ui/server/routes/workbench/agentSessionStore.js +173 -173
- package/src/ui/server/utils/instanceRegistry.js +234 -60
- package/src/ui/server/utils/instanceRegistry.test.js +396 -0
- package/src/ui/public/assets/AgentView-CFP7GbUl.js +0 -4
- package/src/ui/public/assets/AppVersionBadge-CBSY_oq9.js +0 -2
- package/src/ui/public/assets/BranchSelector-Fi2LA9XU.js +0 -1
- package/src/ui/public/assets/CommitForm-CLIleDCk.js +0 -6
- package/src/ui/public/assets/EditorView-CBVzdZmP.js +0 -1
- package/src/ui/public/assets/FlowExecutionViewer-BBkNLVm3.js +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BZqZPDN5.js +0 -21
- package/src/ui/public/assets/LogList-CJnjZAeU.js +0 -7
- package/src/ui/public/assets/MindmapView-BAGMeCsu.js +0 -1
- package/src/ui/public/assets/MonitorView-CsAGCNaD.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-MPOop7ax.js +0 -1
- package/src/ui/public/assets/RemoteRepoCard-B2wpKIp1.js +0 -1
- package/src/ui/public/assets/SourceMapView-FE6FVmmX.js +0 -3
- package/src/ui/public/assets/UserInputNode-BicQgmt7.js +0 -1
- package/src/ui/public/assets/WorkbenchView-DdipCfIx.js +0 -12
- package/src/ui/public/assets/element-plus-CXfbWXaJ.css +0 -1
- package/src/ui/public/assets/element-plus-utMlO-GC.js +0 -27
- package/src/ui/public/assets/index-2NOwDrtr.js +0 -62
- package/src/ui/public/assets/index-BSEyfbfQ.css +0 -1
- package/src/ui/public/assets/office-docx-CUC3NjrQ.js +0 -31
- package/src/ui/public/assets/socket-io-ObqgZMoP.js +0 -1
- /package/src/ui/public/assets/{dagre-BCXBo6I9.js → dagre-97WoLjFt.js} +0 -0
|
@@ -13,27 +13,44 @@
|
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
//
|
|
15
15
|
// 实例注册表工具
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// 每个 GUI 实例在自己的 <registryPath>/<pid>.json 文件中维护自身条目,
|
|
17
|
+
// 跨进程并发启动时不再因为 read-modify-write 互相覆盖而丢条目。
|
|
18
|
+
//
|
|
19
|
+
// 旧版本是单文件 `~/.zen-gitsync-instances.json`,多进程 register/heartbeat
|
|
20
|
+
// 并发时 read-modify-write 整体不原子,后写会覆盖先写,启动时一同拉起的 N
|
|
21
|
+
// 个实例最终只会有少数几个幸运的 PID 留在表里。改成目录后,写操作天然独
|
|
22
|
+
// 立:register 只动自己的 entry 文件,unregister 只删自己的,readAll 是
|
|
23
|
+
// 遍历目录做合并。
|
|
24
|
+
//
|
|
25
|
+
// stale 判定:PID 不存在 或 lastHeartbeat 超过 STALE_MS。
|
|
26
|
+
// 启动时会自动从旧主文件(~/.zen-gitsync-instances.json)迁移到目录。
|
|
19
27
|
|
|
20
28
|
import nodePath from 'node:path';
|
|
21
|
-
import logger from './logger.js'
|
|
29
|
+
import logger from './logger.js';
|
|
22
30
|
import nodeOs from 'node:os';
|
|
23
31
|
|
|
24
|
-
const STALE_MS = 30_000;
|
|
25
|
-
const WATCH_DEBOUNCE_MS = 100;
|
|
32
|
+
const STALE_MS = 30_000; // 心跳超时阈值(毫秒)
|
|
33
|
+
const WATCH_DEBOUNCE_MS = 100; // fs.watch 防抖时间
|
|
26
34
|
const REGISTRY_VERSION = 1;
|
|
35
|
+
const REGISTRY_DIR_NAME = '.zen-gitsync-instances';
|
|
36
|
+
const REGISTRY_LEGACY_FILE_NAME = '.zen-gitsync-instances.json';
|
|
37
|
+
const MIGRATION_MARKER = '.migrated';
|
|
27
38
|
|
|
28
39
|
/**
|
|
29
|
-
*
|
|
30
|
-
* 用于:npm.js 的 /api/app-restart 自拉起时,父进程轮询此路径判断子进程是否就绪。
|
|
40
|
+
* 新注册表目录路径(每进程一个文件)
|
|
31
41
|
*/
|
|
32
42
|
export function getRegistryPath() {
|
|
33
|
-
return nodePath.join(nodeOs.homedir(),
|
|
43
|
+
return nodePath.join(nodeOs.homedir(), REGISTRY_DIR_NAME);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 旧版单文件注册表路径(仅用于一次性迁移)
|
|
48
|
+
*/
|
|
49
|
+
export function getLegacyRegistryPath() {
|
|
50
|
+
return nodePath.join(nodeOs.homedir(), REGISTRY_LEGACY_FILE_NAME);
|
|
34
51
|
}
|
|
35
52
|
|
|
36
|
-
function
|
|
53
|
+
function defaultIsProcessAlive(pid) {
|
|
37
54
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
38
55
|
try {
|
|
39
56
|
// 信号 0 仅做存活检查,不真正发送信号
|
|
@@ -64,11 +81,38 @@ async function resolveProjectName(projectPath, fsMod, pathMod) {
|
|
|
64
81
|
return pathMod.basename(projectPath);
|
|
65
82
|
}
|
|
66
83
|
|
|
67
|
-
|
|
84
|
+
// entry 文件名匹配:<pid>.json(pid 是正整数)
|
|
85
|
+
function isEntryFileName(name) {
|
|
86
|
+
if (!name || typeof name !== 'string') return false;
|
|
87
|
+
if (name.startsWith('.')) return false; // 跳过 .migrated 等
|
|
88
|
+
if (name.endsWith('.tmp')) return false; // 跳过写入中的临时文件
|
|
89
|
+
if (!name.endsWith('.json')) return false;
|
|
90
|
+
const base = name.slice(0, -'.json'.length);
|
|
91
|
+
return /^\d+$/.test(base);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pidFromFileName(name) {
|
|
95
|
+
return Number(name.slice(0, -'.json'.length));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createInstanceRegistry({
|
|
99
|
+
fs: fsMod,
|
|
100
|
+
path: pathMod,
|
|
101
|
+
os: osMod,
|
|
102
|
+
registryPath,
|
|
103
|
+
isProcessAlive: isProcessAliveOverride,
|
|
104
|
+
}) {
|
|
68
105
|
if (!fsMod || !pathMod || !osMod || !registryPath) {
|
|
69
106
|
throw new Error('createInstanceRegistry: 必须提供 fs/path/os/registryPath');
|
|
70
107
|
}
|
|
71
108
|
|
|
109
|
+
// entry / legacy / marker 路径用注入的 pathMod 而非模块级的 nodePath,
|
|
110
|
+
// 这样测试里可以传跨平台的 mock path,避免 Windows 上 nodePath.join 生成
|
|
111
|
+
// 反斜杠路径而 mock fs 用的 '/' 路径读不到
|
|
112
|
+
const entryFilePath = (pid) => pathMod.join(registryPath, `${pid}.json`);
|
|
113
|
+
const legacyPath = () => pathMod.join(osMod.homedir(), REGISTRY_LEGACY_FILE_NAME);
|
|
114
|
+
const migrationMarkerPath = () => pathMod.join(registryPath, MIGRATION_MARKER);
|
|
115
|
+
|
|
72
116
|
// 进程内写串行化:所有 mutate 操作都 await 这条链
|
|
73
117
|
let writeChain = Promise.resolve();
|
|
74
118
|
|
|
@@ -79,32 +123,136 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
79
123
|
return next;
|
|
80
124
|
}
|
|
81
125
|
|
|
82
|
-
|
|
126
|
+
// 是否启用测试桩:若传入了 isProcessAlive,就用它(避免测试里 process.kill
|
|
127
|
+
// 误杀/找不到 PID 把所有 entry 都 prune 掉);否则用真实 process.kill。
|
|
128
|
+
const isProcessAlive = typeof isProcessAliveOverride === 'function'
|
|
129
|
+
? isProcessAliveOverride
|
|
130
|
+
: defaultIsProcessAlive;
|
|
131
|
+
|
|
132
|
+
// 确保注册表目录存在
|
|
133
|
+
async function ensureDir() {
|
|
83
134
|
try {
|
|
84
|
-
|
|
135
|
+
await fsMod.mkdir(registryPath, { recursive: true });
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err && err.code === 'EEXIST') return;
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 读单个 entry 文件。失败/不存在时返回 null。
|
|
143
|
+
async function readEntry(pid) {
|
|
144
|
+
const fp = entryFilePath(pid);
|
|
145
|
+
try {
|
|
146
|
+
const raw = await fsMod.readFile(fp, 'utf-8');
|
|
85
147
|
const parsed = JSON.parse(raw);
|
|
86
|
-
if (parsed && typeof parsed === 'object'
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
148
|
+
if (parsed && typeof parsed === 'object') return parsed;
|
|
149
|
+
return null;
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) return null;
|
|
152
|
+
logger.warn(`[instanceRegistry] 读 entry pid=${pid} 失败: ${err?.message || err}`);
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 原子写单个 entry 文件:tmp + rename
|
|
158
|
+
async function writeEntry(entry) {
|
|
159
|
+
const fp = entryFilePath(entry.pid);
|
|
160
|
+
const tmpFp = `${fp}.tmp`;
|
|
161
|
+
await fsMod.writeFile(tmpFp, JSON.stringify(entry, null, 2), 'utf-8');
|
|
162
|
+
await fsMod.rename(tmpFp, fp);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 删除单个 entry 文件
|
|
166
|
+
async function deleteEntry(pid) {
|
|
167
|
+
const fp = entryFilePath(pid);
|
|
168
|
+
try {
|
|
169
|
+
await fsMod.unlink(fp);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err && err.code === 'ENOENT') return;
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 遍历目录读取所有 entry,合并成单张表
|
|
177
|
+
async function readAll() {
|
|
178
|
+
await ensureDir();
|
|
179
|
+
let names;
|
|
180
|
+
try {
|
|
181
|
+
names = await fsMod.readdir(registryPath);
|
|
90
182
|
} catch (err) {
|
|
91
183
|
if (err && err.code === 'ENOENT') {
|
|
92
184
|
return { version: REGISTRY_VERSION, instances: {} };
|
|
93
185
|
}
|
|
94
|
-
|
|
95
|
-
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
const instances = {};
|
|
189
|
+
for (const name of names) {
|
|
190
|
+
if (!isEntryFileName(name)) continue;
|
|
191
|
+
const pid = pidFromFileName(name);
|
|
192
|
+
const entry = await readEntry(pid);
|
|
193
|
+
if (entry && Number.isInteger(entry.pid)) {
|
|
194
|
+
instances[String(entry.pid)] = entry;
|
|
195
|
+
}
|
|
96
196
|
}
|
|
197
|
+
return { version: REGISTRY_VERSION, instances };
|
|
97
198
|
}
|
|
98
199
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
200
|
+
// 一次性迁移:从旧主文件 ~/.zen-gitsync-instances.json 把每个 entry
|
|
201
|
+
// 写到新目录里。已迁移过(存在 .migrated 标记)则跳过。
|
|
202
|
+
async function migrateLegacy() {
|
|
203
|
+
await ensureDir();
|
|
204
|
+
const markerPath = migrationMarkerPath();
|
|
205
|
+
try {
|
|
206
|
+
await fsMod.access(markerPath);
|
|
207
|
+
return false; // 已迁移
|
|
208
|
+
} catch (_) {
|
|
209
|
+
// 标记不存在,需要迁移
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// legacy path 走注入的 osMod,这样测试里可以把 homedir 指向 mock 路径
|
|
213
|
+
const legacyPath_ = legacyPath();
|
|
214
|
+
let legacy = null;
|
|
215
|
+
try {
|
|
216
|
+
const raw = await fsMod.readFile(legacyPath_, 'utf-8');
|
|
217
|
+
legacy = JSON.parse(raw);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err && err.code !== 'ENOENT') {
|
|
220
|
+
logger.warn(`[instanceRegistry] 读旧主文件失败: ${err?.message || err}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!legacy || typeof legacy !== 'object' || !legacy.instances) {
|
|
225
|
+
// 没有旧数据,只写标记
|
|
226
|
+
try {
|
|
227
|
+
await fsMod.writeFile(markerPath, new Date().toISOString(), 'utf-8');
|
|
228
|
+
} catch (_) {}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let count = 0;
|
|
233
|
+
for (const [pidStr, entry] of Object.entries(legacy.instances)) {
|
|
234
|
+
if (!entry || !Number.isInteger(entry.pid)) continue;
|
|
235
|
+
try {
|
|
236
|
+
await writeEntry(entry);
|
|
237
|
+
count++;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
logger.warn(`[instanceRegistry] 迁移 entry pid=${pidStr} 失败: ${e?.message || e}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await fsMod.writeFile(markerPath, new Date().toISOString(), 'utf-8');
|
|
245
|
+
} catch (e) {
|
|
246
|
+
logger.warn(`[instanceRegistry] 写迁移标记失败: ${e?.message || e}`);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
await fsMod.unlink(legacyPath_);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// 旧文件删不掉不阻塞,只是留个垃圾,下回再删
|
|
252
|
+
logger.warn(`[instanceRegistry] 删旧主文件失败: ${e?.message || e}`);
|
|
253
|
+
}
|
|
254
|
+
logger.info(`[instanceRegistry] 已从旧主文件迁移 ${count} 个 entry 到目录格式`);
|
|
255
|
+
return true;
|
|
108
256
|
}
|
|
109
257
|
|
|
110
258
|
// 同步裁剪:传入当前内存中的 instances 字典,返回裁剪后的新字典
|
|
@@ -122,6 +270,9 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
122
270
|
}
|
|
123
271
|
|
|
124
272
|
// 公开 API
|
|
273
|
+
//
|
|
274
|
+
// 关键点:register / heartbeat / unregister 都只读写 *自己* 的 entry 文件,
|
|
275
|
+
// 跨进程并发时不同 PID 的写入互不干扰,从根本上消除 read-modify-write 覆盖。
|
|
125
276
|
async function register({ pid, port, projectPath, projectName, hostname } = {}) {
|
|
126
277
|
if (!Number.isInteger(pid) || pid <= 0) throw new Error('register: pid 必填');
|
|
127
278
|
if (!Number.isInteger(port) || port <= 0) throw new Error('register: port 必填');
|
|
@@ -142,9 +293,9 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
142
293
|
};
|
|
143
294
|
|
|
144
295
|
await enqueueWrite(async () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await
|
|
296
|
+
await ensureDir();
|
|
297
|
+
await migrateLegacy();
|
|
298
|
+
await writeEntry(entry);
|
|
148
299
|
});
|
|
149
300
|
return entry;
|
|
150
301
|
}
|
|
@@ -152,32 +303,52 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
152
303
|
async function unregister(pid) {
|
|
153
304
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
154
305
|
await enqueueWrite(async () => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
delete obj.instances[String(pid)];
|
|
158
|
-
await writeAll(obj);
|
|
159
|
-
}
|
|
306
|
+
await ensureDir();
|
|
307
|
+
await deleteEntry(pid);
|
|
160
308
|
});
|
|
161
309
|
}
|
|
162
310
|
|
|
311
|
+
// 心跳。updates 可携带完整注册信息(port/projectPath/projectName/hostname):
|
|
312
|
+
// 条目存在 → 刷新 lastHeartbeat
|
|
313
|
+
// 条目不存在 → 用完整信息自愈重建
|
|
314
|
+
//
|
|
315
|
+
// 这里只读 *自己* 的 entry 文件,不遍历全表。即使全表被同时写,
|
|
316
|
+
// 也只影响这一个文件,自己的 lastHeartbeat 一定能更新到。
|
|
163
317
|
async function heartbeat(pid, updates = {}) {
|
|
164
318
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
165
319
|
await enqueueWrite(async () => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
320
|
+
await ensureDir();
|
|
321
|
+
await migrateLegacy();
|
|
322
|
+
|
|
323
|
+
const existing = await readEntry(pid);
|
|
324
|
+
let entry;
|
|
169
325
|
if (!existing) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return;
|
|
326
|
+
const canRebuild =
|
|
327
|
+
Number.isInteger(updates.port) && updates.port > 0 && !!updates.projectPath;
|
|
328
|
+
if (!canRebuild) return;
|
|
329
|
+
|
|
330
|
+
entry = {
|
|
331
|
+
pid,
|
|
332
|
+
port: updates.port,
|
|
333
|
+
projectName: updates.projectName || '',
|
|
334
|
+
projectPath: updates.projectPath,
|
|
335
|
+
startedAt: Date.now(),
|
|
336
|
+
lastHeartbeat: Date.now(),
|
|
337
|
+
hostname: updates.hostname || osMod.hostname()
|
|
338
|
+
};
|
|
339
|
+
logger.info(
|
|
340
|
+
`[instanceRegistry] 心跳检测到本实例条目丢失,已自愈重建 pid=${pid} port=${updates.port}`
|
|
341
|
+
);
|
|
342
|
+
} else {
|
|
343
|
+
entry = {
|
|
344
|
+
...existing,
|
|
345
|
+
...(updates.projectPath ? { projectPath: updates.projectPath } : {}),
|
|
346
|
+
...(updates.projectName ? { projectName: updates.projectName } : {}),
|
|
347
|
+
...(Number.isInteger(updates.port) && updates.port > 0 ? { port: updates.port } : {}),
|
|
348
|
+
lastHeartbeat: Date.now()
|
|
349
|
+
};
|
|
173
350
|
}
|
|
174
|
-
|
|
175
|
-
...existing,
|
|
176
|
-
...(updates.projectPath ? { projectPath: updates.projectPath } : {}),
|
|
177
|
-
...(updates.projectName ? { projectName: updates.projectName } : {}),
|
|
178
|
-
lastHeartbeat: Date.now()
|
|
179
|
-
};
|
|
180
|
-
await writeAll(obj);
|
|
351
|
+
await writeEntry(entry);
|
|
181
352
|
});
|
|
182
353
|
}
|
|
183
354
|
|
|
@@ -185,15 +356,15 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
185
356
|
const obj = await readAll();
|
|
186
357
|
let instances = obj.instances || {};
|
|
187
358
|
if (pruneStale) {
|
|
359
|
+
const originalKeys = Object.keys(instances);
|
|
188
360
|
instances = pruneInPlace(instances);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
});
|
|
361
|
+
const newKeys = Object.keys(instances);
|
|
362
|
+
if (originalKeys.length !== newKeys.length) {
|
|
363
|
+
// 失效条目的文件也要删,避免堆积
|
|
364
|
+
const removed = originalKeys.filter((k) => !newKeys.includes(k));
|
|
365
|
+
for (const pidStr of removed) {
|
|
366
|
+
await deleteEntry(Number(pidStr));
|
|
367
|
+
}
|
|
197
368
|
}
|
|
198
369
|
}
|
|
199
370
|
const arr = Object.values(instances);
|
|
@@ -201,7 +372,7 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
201
372
|
return arr;
|
|
202
373
|
}
|
|
203
374
|
|
|
204
|
-
//
|
|
375
|
+
// 监听注册表目录变化;callback 会在 debounce 后被调用,参数是最新 list
|
|
205
376
|
// fsWatch 参数:node 'fs' 模块的 watch 函数(同步 + EventEmitter 形式)
|
|
206
377
|
function watch(callback, fsWatch) {
|
|
207
378
|
if (typeof callback !== 'function') {
|
|
@@ -227,13 +398,14 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
227
398
|
};
|
|
228
399
|
|
|
229
400
|
// 周期性 prune:即便没有其他进程写入注册表,也定期清理本地失效条目
|
|
230
|
-
// (例如所有 server 都强 kill
|
|
401
|
+
// (例如所有 server 都强 kill 后,目录里残留的僵尸 entry 会被自动清理)
|
|
231
402
|
pruneTimer = setInterval(() => {
|
|
232
403
|
if (closed) return;
|
|
233
404
|
list({ pruneStale: true }).catch(() => {});
|
|
234
405
|
}, STALE_MS / 3);
|
|
235
406
|
|
|
236
407
|
try {
|
|
408
|
+
// 监听整个目录:任何一个 entry 文件增/删/改都会触发回调
|
|
237
409
|
watcher = fsWatch(registryPath, { persistent: false }, () => {
|
|
238
410
|
if (closed) return;
|
|
239
411
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -277,6 +449,8 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
277
449
|
watch,
|
|
278
450
|
close,
|
|
279
451
|
_resolveProjectName: (p) => resolveProjectName(p, fsMod, pathMod),
|
|
280
|
-
_STALE_MS: STALE_MS
|
|
452
|
+
_STALE_MS: STALE_MS,
|
|
453
|
+
_migrateLegacy: migrateLegacy,
|
|
454
|
+
_isEntryFileName: isEntryFileName
|
|
281
455
|
};
|
|
282
456
|
}
|