This commit is contained in:
@@ -1,220 +0,0 @@
|
||||
import { execFileSync, spawn } from 'node:child_process';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const apiServerExePath = resolve(
|
||||
repoRoot,
|
||||
'server-rs/target/debug/api-server.exe',
|
||||
);
|
||||
const defaultHealthHost = '127.0.0.1';
|
||||
const defaultHealthPort = '3100';
|
||||
const healthTimeoutMs =
|
||||
Number(process.env.GENARRATIVE_API_SERVER_MAINCLOUD_SMOKE_TIMEOUT_SECONDS) *
|
||||
1000 || 180_000;
|
||||
|
||||
function loadEnvFile(path, target) {
|
||||
if (!existsSync(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawText = readFileSync(path, 'utf8');
|
||||
for (const rawLine of rawText.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, key, rawValue] = match;
|
||||
if (target[key] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
target[key] = rawValue.replace(/^['"]|['"]$/gu, '');
|
||||
}
|
||||
}
|
||||
|
||||
function stopExistingWindowsApiServer() {
|
||||
if (process.platform !== 'win32') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Windows 下 cargo 重编译不能覆盖正在运行的 exe,只清理本仓库 target 内的 api-server。
|
||||
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 "[api-server:maincloud] 忽略旧进程清理瞬时失败 pid=$($process.Id): $($_.Exception.Message)"',
|
||||
' }',
|
||||
'}',
|
||||
'exit 0',
|
||||
].join('\n');
|
||||
|
||||
const output = execFileSync(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', command],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
GENARRATIVE_API_SERVER_EXE_TARGET: apiServerExePath,
|
||||
},
|
||||
},
|
||||
).trim();
|
||||
|
||||
if (output) {
|
||||
console.log(`[api-server:maincloud] 已停止旧 api-server 进程: ${output}`);
|
||||
}
|
||||
}
|
||||
|
||||
function stopProcessTree(child) {
|
||||
if (!child || child.exitCode !== null || child.signalCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
execFileSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
|
||||
stdio: 'ignore',
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
// taskkill 可能已经被进程自然退出抢先;继续走兜底清理。
|
||||
}
|
||||
}
|
||||
|
||||
child.kill('SIGTERM');
|
||||
}
|
||||
|
||||
async function waitForHealthz({ child, healthUrl }) {
|
||||
const deadline = Date.now() + healthTimeoutMs;
|
||||
let childExit = null;
|
||||
child.once('exit', (code, signal) => {
|
||||
childExit = { code, signal };
|
||||
});
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
if (childExit) {
|
||||
throw new Error(
|
||||
`api-server 在 healthz 就绪前退出:code=${childExit.code ?? ''} signal=${
|
||||
childExit.signal ?? ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(healthUrl, {
|
||||
signal: AbortSignal.timeout(1_000),
|
||||
});
|
||||
const body = await response.text();
|
||||
if (response.status === 200) {
|
||||
return body;
|
||||
}
|
||||
} catch {
|
||||
// 服务启动期间连接失败是预期状态,继续轮询。
|
||||
}
|
||||
|
||||
await delay(500);
|
||||
}
|
||||
|
||||
throw new Error(`等待 /healthz 超时:${healthUrl}`);
|
||||
}
|
||||
|
||||
const mergedEnv = { ...process.env };
|
||||
loadEnvFile(resolve(repoRoot, '.env'), mergedEnv);
|
||||
loadEnvFile(resolve(repoRoot, '.env.local'), mergedEnv);
|
||||
loadEnvFile(resolve(repoRoot, '.env.secrets.local'), mergedEnv);
|
||||
|
||||
mergedEnv.GENARRATIVE_API_HOST =
|
||||
mergedEnv.GENARRATIVE_API_HOST || defaultHealthHost;
|
||||
mergedEnv.GENARRATIVE_API_PORT =
|
||||
mergedEnv.GENARRATIVE_API_PORT || defaultHealthPort;
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL ||
|
||||
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL ||
|
||||
'https://maincloud.spacetimedb.com';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_DATABASE =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE ||
|
||||
mergedEnv.GENARRATIVE_SPACETIME_DATABASE ||
|
||||
'';
|
||||
mergedEnv.GENARRATIVE_SPACETIME_TOKEN =
|
||||
mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN ||
|
||||
mergedEnv.GENARRATIVE_SPACETIME_TOKEN ||
|
||||
'';
|
||||
|
||||
if (!mergedEnv.GENARRATIVE_SPACETIME_DATABASE) {
|
||||
console.error(
|
||||
'[api-server:maincloud] 缺少 GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE 或 GENARRATIVE_SPACETIME_DATABASE。',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
stopExistingWindowsApiServer();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[api-server:maincloud] 清理旧 api-server 进程失败: ${error.message}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[api-server:maincloud] SpacetimeDB ${mergedEnv.GENARRATIVE_SPACETIME_DATABASE} @ ${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`,
|
||||
);
|
||||
|
||||
const child = spawn(
|
||||
'cargo',
|
||||
['run', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: mergedEnv,
|
||||
stdio: 'inherit',
|
||||
},
|
||||
);
|
||||
|
||||
const cleanup = () => {
|
||||
stopProcessTree(child);
|
||||
try {
|
||||
stopExistingWindowsApiServer();
|
||||
} catch {
|
||||
// 退出阶段只做 best-effort 清理,不能覆盖真实 smoke 结果。
|
||||
}
|
||||
};
|
||||
|
||||
process.once('SIGINT', () => {
|
||||
cleanup();
|
||||
process.exit(130);
|
||||
});
|
||||
process.once('SIGTERM', () => {
|
||||
cleanup();
|
||||
process.exit(143);
|
||||
});
|
||||
|
||||
try {
|
||||
const healthHost =
|
||||
mergedEnv.GENARRATIVE_API_HOST === '0.0.0.0'
|
||||
? defaultHealthHost
|
||||
: mergedEnv.GENARRATIVE_API_HOST;
|
||||
const healthUrl = `http://${healthHost}:${mergedEnv.GENARRATIVE_API_PORT}/healthz`;
|
||||
const body = await waitForHealthz({ child, healthUrl });
|
||||
console.log(`[api-server:maincloud] /healthz 通过:${body}`);
|
||||
cleanup();
|
||||
} catch (error) {
|
||||
console.error(`[api-server:maincloud] smoke 失败:${error.message}`);
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user