|
1 | | -import { spawn, ChildProcess } from 'child_process' |
| 1 | +import { spawn, ChildProcess, StdioOptions } from 'child_process' |
| 2 | +import { Readable } from 'stream' |
2 | 3 | import { CliComponents } from '../../components' |
3 | 4 | import { printProgressInfo, printWarning } from '../../logic/beautiful-logs' |
4 | 5 | import { colors } from '../../components/log' |
@@ -52,6 +53,38 @@ function registerProcessCleanup(cleanup: () => void): () => void { |
52 | 53 | } |
53 | 54 | } |
54 | 55 |
|
| 56 | +// `2026-08-11T14:28:33.522058Z INFO scene_runner::renderer_context: ` — the |
| 57 | +// timestamp/level/target prefix the bevy engine's tracing puts on every line |
| 58 | +const TRACING_PREFIX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s+(INFO|WARN|ERROR|DEBUG|TRACE)\s+[\w:]+:\s?/ |
| 59 | +const HEARTBEAT_LINE = /^\[headless\] alive:/ |
| 60 | + |
| 61 | +/** |
| 62 | + * Forwards a bevy child stream line by line, dropping the tracing prefix (keeping |
| 63 | + * WARN/ERROR markers) and the periodic `[headless] alive:` heartbeat. |
| 64 | + */ |
| 65 | +function forwardEngineLogs(source: Readable | null, sink: NodeJS.WriteStream) { |
| 66 | + if (!source) return |
| 67 | + let pending = '' |
| 68 | + source.setEncoding('utf8') |
| 69 | + source.on('data', (chunk: string) => { |
| 70 | + const lines = (pending + chunk).split('\n') |
| 71 | + pending = lines.pop() ?? '' |
| 72 | + for (const line of lines) { |
| 73 | + if (HEARTBEAT_LINE.test(line)) continue |
| 74 | + const match = line.match(TRACING_PREFIX) |
| 75 | + if (!match) { |
| 76 | + sink.write(line + '\n') |
| 77 | + } else { |
| 78 | + const message = line.slice(match[0].length) |
| 79 | + sink.write((match[1] === 'INFO' ? message : `${match[1]} ${message}`) + '\n') |
| 80 | + } |
| 81 | + } |
| 82 | + }) |
| 83 | + source.on('end', () => { |
| 84 | + if (pending && !HEARTBEAT_LINE.test(pending)) sink.write(pending + '\n') |
| 85 | + }) |
| 86 | +} |
| 87 | + |
55 | 88 | /** |
56 | 89 | * Starts the Multiplayer Server process using npx to install and run in one step |
57 | 90 | */ |
@@ -84,11 +117,19 @@ export function startMultiplayerServer( |
84 | 117 | env.RUST_LOG = 'warn,scene_runner::renderer_context=info' |
85 | 118 | } |
86 | 119 |
|
| 120 | + // Bevy output goes through the line filter below; hammurabi keeps the terminal directly. |
| 121 | + const stdio: StdioOptions = engine === 'bevy' ? ['inherit', 'pipe', 'pipe'] : 'inherit' |
| 122 | + |
87 | 123 | // If npx-cli.js was found, run it directly via process.execPath (node in regular env, |
88 | 124 | // Electron Helper with ELECTRON_RUN_AS_NODE=1 in Electron). Otherwise fall back to npx binary. |
89 | 125 | const serverProcess = npxCliJs |
90 | | - ? spawn(process.execPath, [npxCliJs, ...npxArgs], { cwd: workingDir, shell: false, stdio: 'inherit', env }) |
91 | | - : spawn(getNpxBin(), npxArgs, { cwd: workingDir, shell: false, stdio: 'inherit', env }) |
| 126 | + ? spawn(process.execPath, [npxCliJs, ...npxArgs], { cwd: workingDir, shell: false, stdio, env }) |
| 127 | + : spawn(getNpxBin(), npxArgs, { cwd: workingDir, shell: false, stdio, env }) |
| 128 | + |
| 129 | + if (engine === 'bevy') { |
| 130 | + forwardEngineLogs(serverProcess.stdout, process.stdout) |
| 131 | + forwardEngineLogs(serverProcess.stderr, process.stderr) |
| 132 | + } |
92 | 133 |
|
93 | 134 | serverProcess.on('error', (error) => { |
94 | 135 | printWarning(components.logger, `Multiplayer Server process error: ${error.message}`) |
|
0 commit comments