c5219f7ad5
closes #314 新增 scripts/dev-windows-process.mjs:提供按根 PID 遍历与按身份匹配(api-server.exe 绝对路径、SpacetimeDB --data-dir)两条独立清理路径 scripts/dev.mjs:直接子进程(cmd.exe 包装层)已退出时仍按记录 PID 清理后代,不再提前 return scripts/dev.mjs:退出时按身份兜底清扫本工作树 api-server 与自建 SpacetimeDB,复用他人 standalone 时跳过 scripts/dev.mjs:启动前清理旧 api-server 保留 Wait-Process 等待语义,避免 cargo 报 failed to remove file apps/ai-game-creator-shell/scripts/start-dev-stack.mjs:复用配套后端前校验端口监听进程归属,无法证明归属则改为启动本工作树后端并允许端口漂移 apps/ai-game-creator-shell/scripts/start-dev-stack.mjs:收到信号与 finally 各兜底清扫一次本工作树 api-server.exe,taskkill 失败时降级为按 PID 遍历 apps/ai-game-creator-shell/scripts/start-dev-stack.mjs:等待后端就绪时输出归属校验未通过的具体原因,避免只表现为 600 秒超时 新增 scripts/dev-windows-process.test.ts 并扩充 AGC 复用门禁用例:覆盖断链遍历、身份匹配、归属判定与探测不可用退化 同步 docs/project-memory/shared-memory/pitfalls.md 与本地开发运维文档的进程清理与复用归属口径
181 lines
5.5 KiB
TypeScript
181 lines
5.5 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
||
|
||
import {
|
||
normalizeWindowsPath,
|
||
parseWindowsProcessSnapshot,
|
||
selectProcessTreeIds,
|
||
selectWorktreeOwnedProcessIds,
|
||
} from './dev-windows-process.mjs';
|
||
|
||
function processEntry(overrides) {
|
||
return {
|
||
ProcessId: 1,
|
||
ParentProcessId: 0,
|
||
Name: 'node.exe',
|
||
ExecutablePath: null,
|
||
CommandLine: null,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
describe('Windows 进程快照解析', () => {
|
||
test('单个进程对象也归一为数组', () => {
|
||
const single = parseWindowsProcessSnapshot(
|
||
JSON.stringify({ ProcessId: 42, ParentProcessId: 1 }),
|
||
);
|
||
expect(single).toHaveLength(1);
|
||
expect(single[0].ProcessId).toBe(42);
|
||
});
|
||
|
||
test('空输出和非法 JSON 返回空数组', () => {
|
||
expect(parseWindowsProcessSnapshot('')).toEqual([]);
|
||
expect(parseWindowsProcessSnapshot('null')).toEqual([]);
|
||
expect(parseWindowsProcessSnapshot('not json')).toEqual([]);
|
||
});
|
||
|
||
test('路径归一化去掉 \\\\?\\ 前缀、尾部分隔符并忽略大小写', () => {
|
||
expect(
|
||
normalizeWindowsPath('\\\\?\\C:\\Repo\\target\\debug\\api-server.exe'),
|
||
).toBe('c:\\repo\\target\\debug\\api-server.exe');
|
||
expect(normalizeWindowsPath('C:\\Repo\\data\\')).toBe('c:\\repo\\data');
|
||
expect(normalizeWindowsPath(null)).toBe('');
|
||
});
|
||
});
|
||
|
||
describe('按根 PID 遍历进程树', () => {
|
||
test('中间层进程已从快照消失时无法再到达更深的后代', () => {
|
||
// cmd(10) -> wrapper(20) -> api-server(30)。Ctrl+C 先杀掉 10 和 20,
|
||
// 快照里只剩 30(ParentProcessId 仍指向已消失的 20),父链断开后
|
||
// 按根 PID 遍历只能拿到根自己,这正是后端被漏杀的原因。
|
||
const snapshot = [
|
||
processEntry({
|
||
ProcessId: 30,
|
||
ParentProcessId: 20,
|
||
Name: 'api-server.exe',
|
||
}),
|
||
];
|
||
expect(selectProcessTreeIds(snapshot, 10)).toEqual([10]);
|
||
});
|
||
|
||
test('根已退出但中间层仍在快照里时仍可收全后代', () => {
|
||
const snapshot = [
|
||
processEntry({ ProcessId: 20, ParentProcessId: 10, Name: 'cargo.exe' }),
|
||
processEntry({
|
||
ProcessId: 30,
|
||
ParentProcessId: 20,
|
||
Name: 'api-server.exe',
|
||
}),
|
||
];
|
||
expect(selectProcessTreeIds(snapshot, 10).sort((a, b) => a - b)).toEqual([
|
||
10, 20, 30,
|
||
]);
|
||
});
|
||
|
||
test('父链完整时能收全后代', () => {
|
||
const snapshot = [
|
||
processEntry({ ProcessId: 10, ParentProcessId: 1, Name: 'cmd.exe' }),
|
||
processEntry({ ProcessId: 20, ParentProcessId: 10, Name: 'cargo.exe' }),
|
||
processEntry({
|
||
ProcessId: 30,
|
||
ParentProcessId: 20,
|
||
Name: 'api-server.exe',
|
||
}),
|
||
processEntry({ ProcessId: 40, ParentProcessId: 1, Name: 'other.exe' }),
|
||
];
|
||
expect(selectProcessTreeIds(snapshot, 10).sort((a, b) => a - b)).toEqual([
|
||
10, 20, 30,
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe('按身份匹配本工作树后端进程', () => {
|
||
const apiServerExePath = 'C:\\Repo\\server-rs\\target\\debug\\api-server.exe';
|
||
const spacetimeDataDir =
|
||
'C:\\Repo\\server-rs\\.spacetimedb\\ai-game-creator\\data';
|
||
|
||
test('只收本工作树的 api-server.exe 与同一 data-dir 的 SpacetimeDB', () => {
|
||
const snapshot = [
|
||
processEntry({
|
||
ProcessId: 100,
|
||
Name: 'api-server.exe',
|
||
ExecutablePath: apiServerExePath,
|
||
CommandLine: '"server-rs\\target\\debug\\api-server.exe"',
|
||
}),
|
||
processEntry({
|
||
ProcessId: 101,
|
||
Name: 'api-server.exe',
|
||
ExecutablePath: 'C:\\Other\\server-rs\\target\\debug\\api-server.exe',
|
||
}),
|
||
processEntry({
|
||
ProcessId: 102,
|
||
Name: 'spacetimedb-standalone.exe',
|
||
ExecutablePath:
|
||
'C:\\Users\\me\\SpacetimeDB\\spacetimedb-standalone.exe',
|
||
CommandLine: `spacetimedb-standalone.exe start --data-dir ${spacetimeDataDir} --listen-addr 127.0.0.1:3101`,
|
||
}),
|
||
processEntry({
|
||
ProcessId: 103,
|
||
Name: 'spacetimedb-standalone.exe',
|
||
CommandLine:
|
||
'spacetimedb-standalone.exe start --data-dir C:\\Other\\data --listen-addr 127.0.0.1:3101',
|
||
}),
|
||
processEntry({
|
||
ProcessId: 104,
|
||
Name: 'node.exe',
|
||
CommandLine: `node dev.mjs --spacetime-data-dir ${spacetimeDataDir}`,
|
||
}),
|
||
];
|
||
|
||
expect(
|
||
selectWorktreeOwnedProcessIds(snapshot, {
|
||
apiServerExePath,
|
||
spacetimeDataDir,
|
||
}).sort((a, b) => a - b),
|
||
).toEqual([100, 102]);
|
||
});
|
||
|
||
test('只给 api-server 路径时不会误伤 SpacetimeDB', () => {
|
||
const snapshot = [
|
||
processEntry({
|
||
ProcessId: 100,
|
||
Name: 'api-server.exe',
|
||
ExecutablePath: apiServerExePath,
|
||
}),
|
||
processEntry({
|
||
ProcessId: 102,
|
||
Name: 'spacetimedb-standalone.exe',
|
||
CommandLine: `--data-dir ${spacetimeDataDir}`,
|
||
}),
|
||
];
|
||
|
||
expect(
|
||
selectWorktreeOwnedProcessIds(snapshot, { apiServerExePath }),
|
||
).toEqual([100]);
|
||
});
|
||
|
||
test('排除自身进程且路径大小写不敏感', () => {
|
||
const snapshot = [
|
||
processEntry({
|
||
ProcessId: 200,
|
||
Name: 'api-server.exe',
|
||
ExecutablePath: apiServerExePath.toUpperCase(),
|
||
}),
|
||
];
|
||
|
||
expect(
|
||
selectWorktreeOwnedProcessIds(snapshot, {
|
||
apiServerExePath,
|
||
selfPid: 200,
|
||
}),
|
||
).toEqual([]);
|
||
expect(
|
||
selectWorktreeOwnedProcessIds(snapshot, { apiServerExePath }),
|
||
).toEqual([200]);
|
||
});
|
||
|
||
test('没有可匹配身份时返回空数组', () => {
|
||
const snapshot = [processEntry({ ProcessId: 100, Name: 'api-server.exe' })];
|
||
expect(selectWorktreeOwnedProcessIds(snapshot, {})).toEqual([]);
|
||
});
|
||
});
|