合并远端 master 到 codex/agc-agent-plugins
合并远端 master 的 AGC 启动、进程复用与项目写锁回收修复 合并远端 master 的开发脚本、运维文档与项目记忆更新 保留本分支的通用插件宿主、插件宿主服务与 AGC Plugin SDK 改动
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
// Windows 开发栈清理工具。
|
||||
//
|
||||
// 背景:Windows 下所有长驻服务都经 `cmd.exe /d /s /c` 包装层启动(Node 的
|
||||
// `shell: true`),而 Ctrl+C 会先让包装层退出。一旦中间层退出,按父进程链
|
||||
// 遍历就再也到不了更深的服务进程,`taskkill /T` 也会因为 PID 已消失而失效。
|
||||
// 因此这里同时提供两种定位方式:
|
||||
// 1. `selectProcessTreeIds`:按记录下来的根 PID 做父子链遍历(能处理根已退出、
|
||||
// 但中间层仍留在快照里的情况)。
|
||||
// 2. `selectWorktreeOwnedProcessIds`:按身份匹配(api-server.exe 的绝对路径、
|
||||
// SpacetimeDB 的 --data-dir),不依赖任何仍然存活的包装层。
|
||||
// 两者结合后,即使 `npm run agc` 的 Ctrl+C 只杀掉了 shell 包装层,也不会留下
|
||||
// 属于本工作树的后端进程。
|
||||
|
||||
function normalizeWindowsPath(value) {
|
||||
const raw = String(value ?? '')
|
||||
.trim()
|
||||
.replace(/^\\\\\?\\/u, '');
|
||||
if (!raw) {
|
||||
return '';
|
||||
}
|
||||
return raw.replace(/[\\/]+$/u, '').toLowerCase();
|
||||
}
|
||||
|
||||
function parseWindowsProcessSnapshot(rawText) {
|
||||
const raw = String(rawText ?? '').trim();
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
if (!parsed) {
|
||||
return [];
|
||||
}
|
||||
return Array.isArray(parsed) ? parsed : [parsed];
|
||||
}
|
||||
|
||||
// 一次退出流程里会多次清理(每个服务的进程树 + 最后的身份兜底清扫),
|
||||
// PowerShell 全量进程快照约 1 秒,短时间内复用同一份快照即可,避免 Ctrl+C
|
||||
// 后清理被拖成十几秒。只在默认实现下缓存,注入实现(测试)始终重新读取。
|
||||
const PROCESS_SNAPSHOT_TTL_MS = 1000;
|
||||
let cachedProcessSnapshot = null;
|
||||
let cachedProcessSnapshotAt = 0;
|
||||
|
||||
function readWindowsProcessSnapshot({
|
||||
spawnSyncImpl = spawnSync,
|
||||
env = process.env,
|
||||
now = Date.now,
|
||||
ttlMs = PROCESS_SNAPSHOT_TTL_MS,
|
||||
} = {}) {
|
||||
const cacheable = spawnSyncImpl === spawnSync && env === process.env;
|
||||
if (
|
||||
cacheable &&
|
||||
cachedProcessSnapshot &&
|
||||
now() - cachedProcessSnapshotAt < ttlMs
|
||||
) {
|
||||
return cachedProcessSnapshot;
|
||||
}
|
||||
|
||||
const command = [
|
||||
'$ErrorActionPreference = "SilentlyContinue"',
|
||||
'Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,ExecutablePath,CommandLine | ConvertTo-Json -Compress',
|
||||
].join('\n');
|
||||
const result = spawnSyncImpl(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env,
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
if (result?.error || result?.status !== 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const snapshot = parseWindowsProcessSnapshot(result.stdout);
|
||||
if (cacheable) {
|
||||
cachedProcessSnapshot = snapshot;
|
||||
cachedProcessSnapshotAt = now();
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function selectProcessTreeIds(processes, rootPid) {
|
||||
if (!Number.isInteger(rootPid)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const childrenByParent = new Map();
|
||||
for (const processEntry of processes ?? []) {
|
||||
const parentId = Number(processEntry?.ParentProcessId);
|
||||
const processId = Number(processEntry?.ProcessId);
|
||||
if (!Number.isInteger(parentId) || !Number.isInteger(processId)) {
|
||||
continue;
|
||||
}
|
||||
if (!childrenByParent.has(parentId)) {
|
||||
childrenByParent.set(parentId, []);
|
||||
}
|
||||
childrenByParent.get(parentId).push(processId);
|
||||
}
|
||||
|
||||
const collected = new Set();
|
||||
const queue = [rootPid];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
if (collected.has(current)) {
|
||||
continue;
|
||||
}
|
||||
collected.add(current);
|
||||
for (const childId of childrenByParent.get(current) ?? []) {
|
||||
queue.push(childId);
|
||||
}
|
||||
}
|
||||
return [...collected];
|
||||
}
|
||||
|
||||
function selectWorktreeOwnedProcessIds(
|
||||
processes,
|
||||
{ apiServerExePath = '', spacetimeDataDir = '', selfPid = process.pid } = {},
|
||||
) {
|
||||
const expectedExePath = normalizeWindowsPath(apiServerExePath);
|
||||
const expectedDataDir = normalizeWindowsPath(spacetimeDataDir);
|
||||
if (!expectedExePath && !expectedDataDir) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matched = [];
|
||||
for (const processEntry of processes ?? []) {
|
||||
const processId = Number(processEntry?.ProcessId);
|
||||
if (!Number.isInteger(processId) || processId === selfPid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const executablePath = normalizeWindowsPath(processEntry?.ExecutablePath);
|
||||
if (expectedExePath && executablePath === expectedExePath) {
|
||||
matched.push(processId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!expectedDataDir) {
|
||||
continue;
|
||||
}
|
||||
const name = String(processEntry?.Name ?? '').toLowerCase();
|
||||
if (!name.startsWith('spacetime')) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
normalizeWindowsPath(processEntry?.CommandLine).includes(expectedDataDir)
|
||||
) {
|
||||
matched.push(processId);
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
function stopWindowsProcessIds(
|
||||
processIds,
|
||||
{ spawnSyncImpl = spawnSync, env = process.env, waitForExitMs = 0 } = {},
|
||||
) {
|
||||
const uniqueIds = [
|
||||
...new Set((processIds ?? []).filter((value) => Number.isInteger(value))),
|
||||
];
|
||||
if (uniqueIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const command = [
|
||||
'$ErrorActionPreference = "SilentlyContinue"',
|
||||
'$ids = $env:GENARRATIVE_STOP_PIDS -split ","',
|
||||
'foreach ($id in $ids) {',
|
||||
' if ($id) { Stop-Process -Id ([int]$id) -Force -ErrorAction SilentlyContinue }',
|
||||
'}',
|
||||
...(waitForExitMs > 0
|
||||
? [
|
||||
// 启动前清理旧 api-server 时必须等它真正退出,否则 Windows 仍占用
|
||||
// target\debug\api-server.exe,cargo 会报 failed to remove file。
|
||||
`Wait-Process -Id $ids -Timeout ${Math.ceil(waitForExitMs / 1000)} -ErrorAction SilentlyContinue`,
|
||||
]
|
||||
: []),
|
||||
].join('\n');
|
||||
spawnSyncImpl(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
|
||||
{
|
||||
env: { ...env, GENARRATIVE_STOP_PIDS: uniqueIds.join(',') },
|
||||
stdio: 'ignore',
|
||||
},
|
||||
);
|
||||
return uniqueIds;
|
||||
}
|
||||
|
||||
function stopWindowsProcessTree(
|
||||
rootPid,
|
||||
{ snapshot = null, spawnSyncImpl = spawnSync, env = process.env } = {},
|
||||
) {
|
||||
if (!Number.isInteger(rootPid)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const processes =
|
||||
snapshot ?? readWindowsProcessSnapshot({ spawnSyncImpl, env });
|
||||
return stopWindowsProcessIds(selectProcessTreeIds(processes, rootPid), {
|
||||
spawnSyncImpl,
|
||||
env,
|
||||
});
|
||||
}
|
||||
|
||||
function stopWindowsWorktreeProcesses({
|
||||
apiServerExePath = '',
|
||||
spacetimeDataDir = '',
|
||||
snapshot = null,
|
||||
spawnSyncImpl = spawnSync,
|
||||
env = process.env,
|
||||
} = {}) {
|
||||
const processes =
|
||||
snapshot ?? readWindowsProcessSnapshot({ spawnSyncImpl, env });
|
||||
return stopWindowsProcessIds(
|
||||
selectWorktreeOwnedProcessIds(processes, {
|
||||
apiServerExePath,
|
||||
spacetimeDataDir,
|
||||
}),
|
||||
{ spawnSyncImpl, env },
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
normalizeWindowsPath,
|
||||
parseWindowsProcessSnapshot,
|
||||
readWindowsProcessSnapshot,
|
||||
selectProcessTreeIds,
|
||||
selectWorktreeOwnedProcessIds,
|
||||
stopWindowsProcessIds,
|
||||
stopWindowsProcessTree,
|
||||
stopWindowsWorktreeProcesses,
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
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([]);
|
||||
});
|
||||
});
|
||||
+75
-85
@@ -36,6 +36,13 @@ import {
|
||||
resolveApiServerLogFile,
|
||||
resolveClientHost,
|
||||
} from './dev-utils.mjs';
|
||||
import {
|
||||
readWindowsProcessSnapshot,
|
||||
selectWorktreeOwnedProcessIds,
|
||||
stopWindowsProcessIds,
|
||||
stopWindowsProcessTree as stopWindowsProcessTreeById,
|
||||
stopWindowsWorktreeProcesses,
|
||||
} from './dev-windows-process.mjs';
|
||||
|
||||
// Resolve the workspace from this script's location, not the caller's cwd.
|
||||
// AGC starts this scheduler through `npm --prefix` from its own package; using
|
||||
@@ -917,7 +924,17 @@ class DevService {
|
||||
}
|
||||
|
||||
async function stopProcess(child, label) {
|
||||
if (!child || child.exitCode != null || child.signalCode != null) {
|
||||
if (!child) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Windows 下直接子进程是 `cmd.exe /d /s /c` 包装层,Ctrl+C 会先让它退出。
|
||||
// 包装层退出不代表 cargo / api-server / spacetime 已经退出,所以这里不能像
|
||||
// 以前那样直接 return,必须继续按记录下来的根 PID 清理后代。
|
||||
if (child.exitCode != null || child.signalCode != null) {
|
||||
if (process.platform === 'win32' && Number.isInteger(child.pid)) {
|
||||
stopWindowsProcessTree(child.pid, label);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -938,7 +955,7 @@ async function stopProcess(child, label) {
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
stopWindowsProcessTree(child.pid);
|
||||
stopWindowsProcessTree(child.pid, label);
|
||||
} else {
|
||||
child.kill('SIGTERM');
|
||||
}
|
||||
@@ -1135,51 +1152,46 @@ async function stopStaleLocalExternalGenerationWorkers({
|
||||
return stopped;
|
||||
}
|
||||
|
||||
function stopWindowsProcessTree(pid) {
|
||||
if (!pid) {
|
||||
return;
|
||||
function resolveWindowsApiServerExePath(repoRootPath = repoRoot) {
|
||||
return resolve(repoRootPath, 'server-rs/target/debug/api-server.exe');
|
||||
}
|
||||
|
||||
function stopWindowsProcessTree(pid, label = '') {
|
||||
if (!Number.isInteger(pid)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
spawnSync(
|
||||
'powershell.exe',
|
||||
[
|
||||
'-NoProfile',
|
||||
'-ExecutionPolicy',
|
||||
'Bypass',
|
||||
'-Command',
|
||||
[
|
||||
'$ErrorActionPreference = "SilentlyContinue"',
|
||||
'$root = [int]$env:GENARRATIVE_STOP_PID',
|
||||
'$all = Get-CimInstance Win32_Process',
|
||||
'$childrenByParent = @{}',
|
||||
'foreach ($process in $all) {',
|
||||
' $parent = [int]$process.ParentProcessId',
|
||||
' if (-not $childrenByParent.ContainsKey($parent)) { $childrenByParent[$parent] = @() }',
|
||||
' $childrenByParent[$parent] += [int]$process.ProcessId',
|
||||
'}',
|
||||
'$toStop = New-Object System.Collections.Generic.List[int]',
|
||||
'$queue = New-Object System.Collections.Generic.Queue[int]',
|
||||
'$queue.Enqueue($root)',
|
||||
'while ($queue.Count -gt 0) {',
|
||||
' $current = $queue.Dequeue()',
|
||||
' $toStop.Add($current)',
|
||||
' if ($childrenByParent.ContainsKey($current)) {',
|
||||
' foreach ($child in $childrenByParent[$current]) { $queue.Enqueue($child) }',
|
||||
' }',
|
||||
'}',
|
||||
'foreach ($id in ($toStop | Select-Object -Unique | Sort-Object -Descending)) {',
|
||||
' Stop-Process -Id $id -Force -ErrorAction SilentlyContinue',
|
||||
'}',
|
||||
].join('\n'),
|
||||
],
|
||||
{
|
||||
env: {
|
||||
...process.env,
|
||||
GENARRATIVE_STOP_PID: String(pid),
|
||||
},
|
||||
stdio: 'ignore',
|
||||
},
|
||||
);
|
||||
const stopped = stopWindowsProcessTreeById(pid);
|
||||
if (stopped.length > 1) {
|
||||
console.log(
|
||||
`[dev${label ? `:${label}` : ''}] 已停止进程树: ${stopped.join(', ')}`,
|
||||
);
|
||||
}
|
||||
return stopped;
|
||||
}
|
||||
|
||||
// 兜底清扫:包装层(cmd.exe / cargo / npm)可能已经退出,父进程链断掉后按 PID
|
||||
// 遍历再也找不到真正的服务进程,因此这里按身份再清一次本工作树的后端。
|
||||
function stopWindowsWorktreeBackendProcesses({
|
||||
spacetimeDataDir = '',
|
||||
logStream = null,
|
||||
snapshot = null,
|
||||
} = {}) {
|
||||
if (process.platform !== 'win32') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const stopped = stopWindowsWorktreeProcesses({
|
||||
apiServerExePath: resolveWindowsApiServerExePath(),
|
||||
spacetimeDataDir,
|
||||
snapshot,
|
||||
});
|
||||
if (stopped.length > 0) {
|
||||
const line = `[dev] 已清理本工作树残留后端进程: ${stopped.join(', ')}\n`;
|
||||
process.stdout.write(line);
|
||||
logStream?.write(line);
|
||||
}
|
||||
return stopped;
|
||||
}
|
||||
|
||||
class DevRunner {
|
||||
@@ -2478,6 +2490,14 @@ class DevRunner {
|
||||
await this.services.get(serviceName)?.stop();
|
||||
}
|
||||
|
||||
// 复用别人启动的 SpacetimeDB 时不能连带杀掉对方的 standalone;只有本进程
|
||||
// 自己拉起的 standalone 才属于本次退出的清理范围。
|
||||
stopWindowsWorktreeBackendProcesses({
|
||||
spacetimeDataDir: this.state.spacetimeReused
|
||||
? ''
|
||||
: this.options.spacetimeDataDir,
|
||||
});
|
||||
|
||||
process.exit(code);
|
||||
}
|
||||
}
|
||||
@@ -2487,47 +2507,15 @@ function stopExistingWindowsApiServer(logStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiServerExePath = resolve(
|
||||
repoRoot,
|
||||
'server-rs/target/debug/api-server.exe',
|
||||
);
|
||||
const command = [
|
||||
'$ErrorActionPreference = "Continue"',
|
||||
'$target = [System.IO.Path]::GetFullPath($env:GENARRATIVE_API_SERVER_EXE_TARGET)',
|
||||
'$processes = Get-Process -Name api-server -ErrorAction SilentlyContinue | Where-Object {',
|
||||
' $_.Path -and ([System.IO.Path]::GetFullPath($_.Path) -ieq $target)',
|
||||
'}',
|
||||
'foreach ($process in $processes) {',
|
||||
' try {',
|
||||
' Stop-Process -Id $process.Id -Force -ErrorAction Stop',
|
||||
' Wait-Process -Id $process.Id -Timeout 5 -ErrorAction SilentlyContinue',
|
||||
' Write-Output $process.Id',
|
||||
' } catch {',
|
||||
' Write-Error "[dev:api-server] 忽略旧进程清理瞬时失败 pid=$($process.Id): $($_.Exception.Message)"',
|
||||
' }',
|
||||
'}',
|
||||
'exit 0',
|
||||
].join('\n');
|
||||
const apiServerExePath = resolveWindowsApiServerExePath();
|
||||
const snapshot = readWindowsProcessSnapshot();
|
||||
const processIds = selectWorktreeOwnedProcessIds(snapshot, {
|
||||
apiServerExePath,
|
||||
});
|
||||
const stopped = stopWindowsProcessIds(processIds, { waitForExitMs: 5000 });
|
||||
|
||||
const result = spawnSync(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
GENARRATIVE_API_SERVER_EXE_TARGET: apiServerExePath,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
const output = String(result.stdout ?? '').trim();
|
||||
if (output) {
|
||||
const line = `[dev:api-server] 已停止旧 api-server 进程: ${output}\n`;
|
||||
if (stopped.length > 0) {
|
||||
const line = `[dev:api-server] 已停止旧 api-server 进程: ${stopped.join(', ')}\n`;
|
||||
process.stdout.write(line);
|
||||
logStream?.write(line);
|
||||
}
|
||||
@@ -3505,8 +3493,10 @@ export {
|
||||
resolveDevStackStatePath,
|
||||
resolveLocalSpacetimeApiIdentityPath,
|
||||
resolveLocalSpacetimeRuntimeServiceBootstrapSecretPath,
|
||||
resolveWindowsApiServerExePath,
|
||||
shouldAcceptWatchEvent,
|
||||
shouldTrustExistingSpacetimeToken,
|
||||
stopWindowsWorktreeBackendProcesses,
|
||||
};
|
||||
|
||||
async function main() {
|
||||
|
||||
Reference in New Issue
Block a user