修正 dev:all 摘要表输出时序
改用无引号纯文本表格显示启动服务和实际地址。 等待 Tauri Running DevCommand 后再打印摘要。 补充启动器输出转发、回归测试和开发运维说明。
This commit is contained in:
@@ -568,7 +568,7 @@ function spawnChild(command, args, options, spawnImpl = spawn) {
|
||||
// POSIX 下让每个长驻服务拥有独立进程组,退出时可以连同 npm、
|
||||
// Node、Cargo 及其子进程一起清理,避免残留订阅任务持续刷日志。
|
||||
detached: isPosix,
|
||||
stdio: 'inherit',
|
||||
stdio: options.stdio ?? 'inherit',
|
||||
});
|
||||
const lifecycle = {
|
||||
failure: null,
|
||||
|
||||
@@ -33,6 +33,7 @@ const tauriCliPath = resolve(repoRoot, 'node_modules/@tauri-apps/cli/tauri.js');
|
||||
const AGC_DESIGN_DEBUG_ENV = 'GENARRATIVE_AGC_DESIGN_DEBUG';
|
||||
const designDebugEnabled =
|
||||
process.env[AGC_DESIGN_DEBUG_ENV]?.trim() === '0' ? '0' : '1';
|
||||
const tauriDevReadyMarker = '[ai-game-creator-shell] Tauri DevCommand 已启动';
|
||||
|
||||
function buildTauriArguments(argv, devUrl = readAgcDevEndpoint().url) {
|
||||
const args = [...argv];
|
||||
@@ -103,11 +104,27 @@ function prepareBundledResourcesBeforeTauri(
|
||||
}
|
||||
|
||||
function spawnTauriCli(argv, { env = process.env } = {}) {
|
||||
return spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
||||
const child = spawnChild(process.execPath, [tauriCliPath, ...argv], {
|
||||
cwd: appRoot,
|
||||
env,
|
||||
shell: false,
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
});
|
||||
let output = '';
|
||||
let announced = false;
|
||||
const forwardOutput = (stream, target) => {
|
||||
stream?.on('data', (chunk) => {
|
||||
target.write(chunk);
|
||||
output = `${output}${chunk.toString()}`.slice(-4096);
|
||||
if (!announced && output.includes('Running DevCommand')) {
|
||||
announced = true;
|
||||
console.log(tauriDevReadyMarker);
|
||||
}
|
||||
});
|
||||
};
|
||||
forwardOutput(child.stdout, process.stdout);
|
||||
forwardOutput(child.stderr, process.stderr);
|
||||
return child;
|
||||
}
|
||||
|
||||
/// Tauri dev 的 Cargo 直接继承启动器环境,用户级 / 仓库级 Cargo 配置里的
|
||||
|
||||
@@ -78,7 +78,7 @@ npm run dev:all
|
||||
|
||||
`npm run dev:all` 保持 `npm run dev` 的主站完整栈语义不变,使用 AGC 当前配套后端的数据库 `genarrative-game-creator-dev` 与 data dir `server-rs/.spacetimedb/ai-game-creator/data` 启动一份 SpacetimeDB、BgFilter worker、api-server、主站 Vite 和后台 Vite;待这五个服务就绪后,再启动 AGC Vite 与 Tauri。AGC 通过匹配的 `.app/dev-stack.json` 复用这份后端,且设置 `AGC_DEV_ADMIN_WEB=0`,因此一键入口只保留一份管理后台。该入口不接受覆盖 database 或 SpacetimeDB data dir 的参数;需要独立数据库时分别使用 `npm run dev` / `npm run agc`。
|
||||
|
||||
一键入口由自身负责收束根 dev 栈与 AGC 客户端的进程树。AGC Vite marker 可访问后,终端会打印包含 SpacetimeDB、BgFilter worker、api-server、主站、后台、AGC Vite 和 Tauri 窗口的实际地址表;任一子进程异常退出都应停止另一侧并返回非零退出码。端口漂移和运行态地址仍以启动日志及 `.app/dev-stack.json` 为准。
|
||||
一键入口由自身负责收束根 dev 栈与 AGC 客户端的进程树。AGC Vite marker 和 Tauri `Running DevCommand` 就绪标记都出现后,终端会打印包含 SpacetimeDB、BgFilter worker、api-server、主站、后台、AGC Vite 和 Tauri 窗口的实际地址表;任一子进程异常退出都应停止另一侧并返回非零退出码。端口漂移和运行态地址仍以启动日志及 `.app/dev-stack.json` 为准。
|
||||
|
||||
通过 `nohup` 在仓库根目录启动 dev 栈且未显式重定向 stdout / stderr 时,默认 `nohup.out` 会持续收集 SpacetimeDB、api-server、bgfilter-worker、主站 Vite 和后台 Vite 的整套 dev 栈输出;该文件已被主站 Vite watcher 和 Git 忽略,避免日志追加触发页面刷新循环,重启主站 Vite 后生效。若把输出显式重定向到其它仓库内文件(例如 `> dev.out`),该自定义文件不会自动获得同样的 watcher 保护,应改为写到 Vite root 之外,或同步配置精确的忽略规则。
|
||||
|
||||
|
||||
+78
-8
@@ -20,6 +20,7 @@ const rootReadyPollMs = 500;
|
||||
const childGracefulTimeoutMs = 2_500;
|
||||
const childForceTimeoutMs = 2_000;
|
||||
const agcReadyPollMs = 500;
|
||||
const tauriDevReadyMarker = '[ai-game-creator-shell] Tauri DevCommand 已启动';
|
||||
|
||||
const rootOnlyOptions = new Set([
|
||||
'--database',
|
||||
@@ -278,9 +279,39 @@ function buildStartupSummary(state, agcEndpoint) {
|
||||
];
|
||||
}
|
||||
|
||||
function printStartupSummary(state, agcEndpoint, table = console.table) {
|
||||
console.log('\n[dev:all] 全部开发服务已启动');
|
||||
table(buildStartupSummary(state, agcEndpoint));
|
||||
function getDisplayWidth(value) {
|
||||
return [...String(value)].reduce(
|
||||
(width, character) => width + (character.charCodeAt(0) > 0xff ? 2 : 1),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
function renderStartupTable(rows) {
|
||||
const columns = ['项目', '状态', '地址'];
|
||||
const values = rows.map((row) => columns.map((column) => row[column] ?? '—'));
|
||||
const widths = columns.map((column, index) =>
|
||||
Math.max(
|
||||
getDisplayWidth(column),
|
||||
...values.map((row) => getDisplayWidth(row[index])),
|
||||
),
|
||||
);
|
||||
const border = `+${widths.map((width) => '-'.repeat(width + 2)).join('+')}+`;
|
||||
const renderRow = (row) =>
|
||||
`| ${row.map((value, index) => `${value}${' '.repeat(widths[index] - getDisplayWidth(value))}`).join(' | ')} |`;
|
||||
|
||||
return [
|
||||
border,
|
||||
renderRow(columns),
|
||||
border,
|
||||
...values.map(renderRow),
|
||||
border,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
function printStartupSummary(state, agcEndpoint) {
|
||||
console.log(
|
||||
`\n[dev:all] 全部开发服务已启动\n${renderStartupTable(buildStartupSummary(state, agcEndpoint))}`,
|
||||
);
|
||||
}
|
||||
|
||||
function waitForChildExit(child) {
|
||||
@@ -313,19 +344,53 @@ function waitForChildExit(child) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildManagedSpawnOptions(env) {
|
||||
function buildManagedSpawnOptions(env, { captureOutput = false } = {}) {
|
||||
return {
|
||||
cwd: repoRoot,
|
||||
env,
|
||||
stdio: 'inherit',
|
||||
stdio: captureOutput ? ['inherit', 'pipe', 'pipe'] : 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
detached: process.platform !== 'win32',
|
||||
windowsHide: process.platform === 'win32',
|
||||
};
|
||||
}
|
||||
|
||||
function spawnManaged(command, args, env) {
|
||||
return spawn(command, args, buildManagedSpawnOptions(env));
|
||||
function createTauriReadyPromise(child) {
|
||||
return new Promise((resolveReady) => {
|
||||
let ready = false;
|
||||
let output = '';
|
||||
const resolveOnce = () => {
|
||||
if (!ready) {
|
||||
ready = true;
|
||||
resolveReady();
|
||||
}
|
||||
};
|
||||
const forward = (stream, target) => {
|
||||
stream?.on('data', (chunk) => {
|
||||
target.write(chunk);
|
||||
output = `${output}${chunk.toString()}`.slice(-4096);
|
||||
if (output.includes(tauriDevReadyMarker)) {
|
||||
resolveOnce();
|
||||
}
|
||||
});
|
||||
};
|
||||
forward(child.stdout, process.stdout);
|
||||
forward(child.stderr, process.stderr);
|
||||
child.once('error', resolveOnce);
|
||||
child.once('exit', resolveOnce);
|
||||
});
|
||||
}
|
||||
|
||||
function spawnManaged(command, args, env, options = {}) {
|
||||
const child = spawn(command, args, buildManagedSpawnOptions(env, options));
|
||||
child.tauriReadyPromise = options.captureOutput
|
||||
? createTauriReadyPromise(child)
|
||||
: Promise.resolve();
|
||||
return child;
|
||||
}
|
||||
|
||||
function waitForTauriProcess(child) {
|
||||
return child?.tauriReadyPromise ?? Promise.resolve();
|
||||
}
|
||||
|
||||
function wait(ms) {
|
||||
@@ -449,6 +514,7 @@ async function runDevAll(argv = process.argv.slice(2), dependencies = {}) {
|
||||
const readState = dependencies.readState ?? (() => readDevStackState());
|
||||
const waitForRoot = dependencies.waitForRoot ?? waitForRootStack;
|
||||
const waitForAgc = dependencies.waitForAgc ?? waitForAgcFrontend;
|
||||
const waitForTauri = dependencies.waitForTauri ?? waitForTauriProcess;
|
||||
|
||||
let rootChild = null;
|
||||
let agcChild = null;
|
||||
@@ -511,9 +577,12 @@ async function runDevAll(argv = process.argv.slice(2), dependencies = {}) {
|
||||
}
|
||||
|
||||
console.log('[dev:all] 根开发栈已就绪,启动 AGC Vite 与 Tauri');
|
||||
agcChild = spawnProcess(npmCommand, ['run', 'agc'], env);
|
||||
agcChild = spawnProcess(npmCommand, ['run', 'agc'], env, {
|
||||
captureOutput: true,
|
||||
});
|
||||
const agcExit = waitForChildExit(agcChild);
|
||||
await waitForAgc(agcChild, endpoint);
|
||||
await waitForTauri(agcChild);
|
||||
if (rootChild.exitCode !== null || rootChild.signalCode !== null) {
|
||||
throw new Error('根开发栈在 AGC 就绪前退出');
|
||||
}
|
||||
@@ -565,6 +634,7 @@ export {
|
||||
buildStartupSummary,
|
||||
isSharedRootStackReady,
|
||||
readDevStackState,
|
||||
renderStartupTable,
|
||||
runDevAll,
|
||||
validateDevAllArgs,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
buildRootDevArgs,
|
||||
buildStartupSummary,
|
||||
isSharedRootStackReady,
|
||||
renderStartupTable,
|
||||
runDevAll,
|
||||
validateDevAllArgs,
|
||||
} from './dev-all.mjs';
|
||||
@@ -91,6 +92,9 @@ describe('dev:all 参数和共享开发栈合同', () => {
|
||||
expect(options.shell).toBe(process.platform === 'win32');
|
||||
expect(options.windowsHide).toBe(process.platform === 'win32');
|
||||
expect(options.detached).toBe(process.platform !== 'win32');
|
||||
expect(
|
||||
buildManagedSpawnOptions(process.env, { captureOutput: true }).stdio,
|
||||
).toEqual(['inherit', 'pipe', 'pipe']);
|
||||
});
|
||||
|
||||
test('按最终端口构造完整启动摘要表', () => {
|
||||
@@ -118,6 +122,17 @@ describe('dev:all 参数和共享开发栈合同', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('渲染摘要表时不输出字符串引号', () => {
|
||||
const rows = buildStartupSummary(makeReadyState(), {
|
||||
url: 'http://127.0.0.1:3080/',
|
||||
});
|
||||
const table = renderStartupTable(rows);
|
||||
|
||||
expect(table).not.toContain("'");
|
||||
expect(table).toContain('| Web 主站');
|
||||
expect(table).toContain('http://127.0.0.1:3080/');
|
||||
});
|
||||
|
||||
test('仅接受同一工作树、实例、数据库和 data dir 的完整栈', () => {
|
||||
const root = resolve('fixture-root');
|
||||
const ready = makeReadyState(root);
|
||||
|
||||
Reference in New Issue
Block a user