Files
Genarrative/scripts/api-server-maincloud.mjs
kdletters 995661e7cc
Some checks failed
CI / verify (push) Has been cancelled
Preserve partial creation replies on stream failure
2026-05-05 11:31:50 +08:00

221 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}