[router] 修复: 同项目双实例不再两个都改名,startedAt 最新者保 base 名

同 projectPath 双实例(spawn 竞态僵尸)进普通撞名改名会被改成
<base>-<路径末段>/-2,没人保住 base 名,<base>__* 前缀路由全部失配
(2026-06-11 forest 96802/26426 事故)。改为组内 startedAt 最新者
作代表参与命名,其余改名 <base>-dup<pid> 并打告警。
This commit is contained in:
furao
2026-06-11 15:56:48 +08:00
parent 2526bc3beb
commit 0aaa1abff8
+23 -4
View File
@@ -156,6 +156,7 @@ async function discover() {
shortName: sanitizeShortName(info.projectShortName), // dedupeShortNames 会按冲突重设 shortName: sanitizeShortName(info.projectShortName), // dedupeShortNames 会按冲突重设
projectPath: info.projectPath, projectPath: info.projectPath,
pid: info.pid, pid: info.pid,
startedAt: Date.parse(info.startedAt) || 0, // 同项目双实例时 dedupe 按它选代表
url: info.url, url: info.url,
tools: tools, tools: tools,
resources: resources, resources: resources,
@@ -212,12 +213,30 @@ function notifyToolsChanged() {
* 冲突的用 projectPath 末段(client / server)加后缀区分,单实例保持原名不变。 * 冲突的用 projectPath 末段(client / server)加后缀区分,单实例保持原名不变。
*/ */
function dedupeShortNames() { function dedupeShortNames() {
var counts = {}; // 第一步:同 projectPath 的重复实例是异常态(spawn 竞态留下的双实例),不能进普通撞名改名——
// 否则后缀同为路径末段,两个实例都被改名(forest-forest / forest-forest-2),
// 没人保住 base 名 → `<base>__*` 前缀路由全部失配(2026-06-11 forest 96802/26426 双实例事故)。
// 规则:组内 startedAt 最新的实例代表该项目参与命名,其余改名 `<base>-dup<pid>`,仍可显式触达。
var byPath = {};
for (var ed of editors.values()) { for (var ed of editors.values()) {
counts[ed.baseShortName] = (counts[ed.baseShortName] || 0) + 1; var k = ed.projectPath || ed.url;
(byPath[k] || (byPath[k] = [])).push(ed);
} }
var reps = [];
Object.keys(byPath).forEach(function (k) {
var group = byPath[k].sort(function (a, b) { return (b.startedAt || 0) - (a.startedAt || 0); });
reps.push(group[0]);
for (var i = 1; i < group.length; i++) {
group[i].shortName = group[i].baseShortName + '-dup' + (group[i].pid || i);
logErr('duplicate editor on same project', k, 'pid=' + group[i].pid, '→', group[i].shortName);
}
});
// 第二步:代表之间的撞名(不同项目 projectShortName 相同,如 my-app/client 与 my-app/server
// 都算成 my-app)按 projectPath 末段加后缀区分,单实例保持原名不变。
var counts = {};
reps.forEach(function (ed) { counts[ed.baseShortName] = (counts[ed.baseShortName] || 0) + 1; });
var used = {}; var used = {};
for (var ed2 of editors.values()) { reps.forEach(function (ed2) {
if (counts[ed2.baseShortName] > 1) { if (counts[ed2.baseShortName] > 1) {
var suffix = sanitizeShortName(path.basename(ed2.projectPath || 'unknown')); var suffix = sanitizeShortName(path.basename(ed2.projectPath || 'unknown'));
var name = ed2.baseShortName + '-' + suffix; var name = ed2.baseShortName + '-' + suffix;
@@ -229,7 +248,7 @@ function dedupeShortNames() {
} else { } else {
ed2.shortName = ed2.baseShortName; ed2.shortName = ed2.baseShortName;
} }
} });
} }
function buildAggregatedToolList() { function buildAggregatedToolList() {