zen-gitsync 2.16.37 → 2.16.39
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/{AppVersionBadge-CcX2ckae.js → AppVersionBadge-Crbg_My9.js} +1 -1
- package/src/ui/public/assets/{BranchSelector-CmWzK_lJ.js → BranchSelector-D5veuJEK.js} +1 -1
- package/src/ui/public/assets/{CommitForm-xwusUSow.js → CommitForm-DOlVaeAm.js} +1 -1
- package/src/ui/public/assets/{EditorView-CJMwWilV.js → EditorView-DIWt74ii.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-Bvqc_KwR.js → FlowOrchestrationWorkspace-QzKGpumZ.js} +1 -1
- package/src/ui/public/assets/{LogList-C8POmSML.js → LogList-DmvJXefN.js} +1 -1
- package/src/ui/public/assets/{MonitorView-Dy7Htpwd.js → MonitorView-BlfmRbaV.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-DyjNYb-a.js → RemoteRepoCard-CrC-ph-p.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-BcpRuySS.js → SourceMapView-BIfkwd0_.js} +1 -1
- package/src/ui/public/assets/{WorkbenchView-C-FcOxAl.js → WorkbenchView-KKlaNqT0.js} +1 -1
- package/src/ui/public/assets/index-hyzLLFKf.js +46 -0
- package/src/ui/public/index.html +1 -1
- package/src/ui/server/index.js +6 -3
- package/src/ui/server/utils/instanceRegistry.js +215 -67
- package/src/ui/server/utils/instanceRegistry.test.js +356 -69
- package/src/ui/public/assets/index-CupPXceE.js +0 -46
package/src/ui/public/index.html
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
11
11
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
12
12
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-hyzLLFKf.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-BM3Ffeng.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/monaco-43aputPh.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/element-plus-Cchqf1s7.js">
|
package/src/ui/server/index.js
CHANGED
|
@@ -48,7 +48,7 @@ import { registerInstancesRoutes } from './routes/instances.js';
|
|
|
48
48
|
import { registerMonitorRoutes } from './routes/monitor.js';
|
|
49
49
|
import { registerMindmapRoutes } from './routes/mindmap.js';
|
|
50
50
|
import { registerAgentRoutes } from './routes/workbench/agentRoutes.js';
|
|
51
|
-
import { createInstanceRegistry } from './utils/instanceRegistry.js';
|
|
51
|
+
import { createInstanceRegistry, getRegistryPath } from './utils/instanceRegistry.js';
|
|
52
52
|
import { createSavePortToFile } from './utils/createSavePortToFile.js';
|
|
53
53
|
import { createOriginGuard, createOriginCheckerFromEnv } from './middleware/originGuard.js';
|
|
54
54
|
import { startServerOnAvailablePort } from './utils/startServerOnAvailablePort.js';
|
|
@@ -229,12 +229,15 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
229
229
|
registerSocketIO(io);
|
|
230
230
|
|
|
231
231
|
// 构建实例注册表(用于跨进程共享"当前运行中的 GUI"信息)
|
|
232
|
-
//
|
|
232
|
+
// 8b725c5c 起改为"每实例一个文件"目录方案:register/heartbeat/unregister
|
|
233
|
+
// 只读写 *自己* 的 <pid>.json,跨进程并发永不互相覆盖。路径用 instanceRegistry
|
|
234
|
+
// 暴露的 getRegistryPath() 统一,避免再次出现"硬编码 .json 路径传给目录方案"
|
|
235
|
+
// 这种错配(8b725c5c 漏改 server/index.js 导致的 ENOTDIR scandir 报错)。
|
|
233
236
|
const instanceRegistry = createInstanceRegistry({
|
|
234
237
|
fs,
|
|
235
238
|
path,
|
|
236
239
|
os,
|
|
237
|
-
registryPath:
|
|
240
|
+
registryPath: getRegistryPath()
|
|
238
241
|
});
|
|
239
242
|
|
|
240
243
|
// 注册全局错误处理器:任意未捕获异常/拒绝都会触发 fatalExit,
|
|
@@ -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,35 +303,31 @@ 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
|
|
|
163
311
|
// 心跳。updates 可携带完整注册信息(port/projectPath/projectName/hostname):
|
|
164
312
|
// 条目存在 → 刷新 lastHeartbeat
|
|
165
313
|
// 条目不存在 → 用完整信息自愈重建
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// 读-改-写整体不原子)。被覆盖的实例不会自己恢复,这里借 5s 心跳兜底补回。
|
|
314
|
+
//
|
|
315
|
+
// 这里只读 *自己* 的 entry 文件,不遍历全表。即使全表被同时写,
|
|
316
|
+
// 也只影响这一个文件,自己的 lastHeartbeat 一定能更新到。
|
|
170
317
|
async function heartbeat(pid, updates = {}) {
|
|
171
318
|
if (!Number.isInteger(pid) || pid <= 0) return;
|
|
172
319
|
await enqueueWrite(async () => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const existing = obj.instances[key];
|
|
320
|
+
await ensureDir();
|
|
321
|
+
await migrateLegacy();
|
|
176
322
|
|
|
323
|
+
const existing = await readEntry(pid);
|
|
324
|
+
let entry;
|
|
177
325
|
if (!existing) {
|
|
178
|
-
// 信息不全就无法自愈,保持原行为(跳过)
|
|
179
326
|
const canRebuild =
|
|
180
327
|
Number.isInteger(updates.port) && updates.port > 0 && !!updates.projectPath;
|
|
181
328
|
if (!canRebuild) return;
|
|
182
329
|
|
|
183
|
-
|
|
330
|
+
entry = {
|
|
184
331
|
pid,
|
|
185
332
|
port: updates.port,
|
|
186
333
|
projectName: updates.projectName || '',
|
|
@@ -189,21 +336,19 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
189
336
|
lastHeartbeat: Date.now(),
|
|
190
337
|
hostname: updates.hostname || osMod.hostname()
|
|
191
338
|
};
|
|
192
|
-
await writeAll(obj);
|
|
193
339
|
logger.info(
|
|
194
340
|
`[instanceRegistry] 心跳检测到本实例条目丢失,已自愈重建 pid=${pid} port=${updates.port}`
|
|
195
341
|
);
|
|
196
|
-
|
|
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
|
+
};
|
|
197
350
|
}
|
|
198
|
-
|
|
199
|
-
obj.instances[key] = {
|
|
200
|
-
...existing,
|
|
201
|
-
...(updates.projectPath ? { projectPath: updates.projectPath } : {}),
|
|
202
|
-
...(updates.projectName ? { projectName: updates.projectName } : {}),
|
|
203
|
-
...(Number.isInteger(updates.port) && updates.port > 0 ? { port: updates.port } : {}),
|
|
204
|
-
lastHeartbeat: Date.now()
|
|
205
|
-
};
|
|
206
|
-
await writeAll(obj);
|
|
351
|
+
await writeEntry(entry);
|
|
207
352
|
});
|
|
208
353
|
}
|
|
209
354
|
|
|
@@ -211,15 +356,15 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
211
356
|
const obj = await readAll();
|
|
212
357
|
let instances = obj.instances || {};
|
|
213
358
|
if (pruneStale) {
|
|
359
|
+
const originalKeys = Object.keys(instances);
|
|
214
360
|
instances = pruneInPlace(instances);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
});
|
|
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
|
+
}
|
|
223
368
|
}
|
|
224
369
|
}
|
|
225
370
|
const arr = Object.values(instances);
|
|
@@ -227,7 +372,7 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
227
372
|
return arr;
|
|
228
373
|
}
|
|
229
374
|
|
|
230
|
-
//
|
|
375
|
+
// 监听注册表目录变化;callback 会在 debounce 后被调用,参数是最新 list
|
|
231
376
|
// fsWatch 参数:node 'fs' 模块的 watch 函数(同步 + EventEmitter 形式)
|
|
232
377
|
function watch(callback, fsWatch) {
|
|
233
378
|
if (typeof callback !== 'function') {
|
|
@@ -253,13 +398,14 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
253
398
|
};
|
|
254
399
|
|
|
255
400
|
// 周期性 prune:即便没有其他进程写入注册表,也定期清理本地失效条目
|
|
256
|
-
// (例如所有 server 都强 kill
|
|
401
|
+
// (例如所有 server 都强 kill 后,目录里残留的僵尸 entry 会被自动清理)
|
|
257
402
|
pruneTimer = setInterval(() => {
|
|
258
403
|
if (closed) return;
|
|
259
404
|
list({ pruneStale: true }).catch(() => {});
|
|
260
405
|
}, STALE_MS / 3);
|
|
261
406
|
|
|
262
407
|
try {
|
|
408
|
+
// 监听整个目录:任何一个 entry 文件增/删/改都会触发回调
|
|
263
409
|
watcher = fsWatch(registryPath, { persistent: false }, () => {
|
|
264
410
|
if (closed) return;
|
|
265
411
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
@@ -303,6 +449,8 @@ export function createInstanceRegistry({ fs: fsMod, path: pathMod, os: osMod, re
|
|
|
303
449
|
watch,
|
|
304
450
|
close,
|
|
305
451
|
_resolveProjectName: (p) => resolveProjectName(p, fsMod, pathMod),
|
|
306
|
-
_STALE_MS: STALE_MS
|
|
452
|
+
_STALE_MS: STALE_MS,
|
|
453
|
+
_migrateLegacy: migrateLegacy,
|
|
454
|
+
_isEntryFileName: isEntryFileName
|
|
307
455
|
};
|
|
308
456
|
}
|