import * as fs from 'fs'; import * as path from 'path'; import * as util from 'util'; let logFilePath = ''; let historyFilePath = ''; export function initServerLog(logDir: string): void { const dir = path.resolve(logDir); fs.mkdirSync(dir, { recursive: true }); logFilePath = path.join(dir, 'matchmaker.log'); historyFilePath = path.join(dir, 'history.log'); } function formatArg(msg: unknown): string { if (typeof msg === 'string') return msg; try { return util.inspect(msg, { depth: 8, colors: false, maxArrayLength: 200 }); } catch { return String(msg); } } function appendFileSafe(filePath: string, line: string): void { try { fs.appendFileSync(filePath, line, 'utf8'); } catch (e) { console.error('server_log: append failed', filePath, e); } } /** Same visibility rules as console: always. */ export function logInfo(...parts: unknown[]): void { const ts = new Date().toISOString(); const text = parts.map(formatArg).join(' '); const line = `[${ts}] ${text}\n`; console.log(text); if (logFilePath) appendFileSafe(logFilePath, line); } /** Same visibility rules as console: when logLevel > 0. */ export function logDebug(logLevel: number, ...parts: unknown[]): void { if (logLevel <= 0) return; const ts = new Date().toISOString(); const text = parts.map(formatArg).join(' '); const line = `[${ts}] ${text}\n`; console.log(text); if (logFilePath) appendFileSafe(logFilePath, line); } /** Same visibility rules as console: when logLevel > 1. */ export function logVerbose(logLevel: number, ...parts: unknown[]): void { if (logLevel <= 1) return; const ts = new Date().toISOString(); const text = parts.map(formatArg).join(' '); const line = `[${ts}] ${text}\n`; console.log(text); if (logFilePath) appendFileSafe(logFilePath, line); } /** Short human-readable timeline (always appended when called). */ export function recordHistory(message: string): void { const safe = message.replace(/\r?\n/g, ' ').trim(); if (!safe) return; const ts = new Date().toISOString(); const line = `${ts}\t${safe}\n`; if (historyFilePath) appendFileSafe(historyFilePath, line); } export function readLogTail(maxLines: number, maxBytes: number): string { if (!logFilePath || !fs.existsSync(logFilePath)) return ''; return tailTextFile(logFilePath, maxLines, maxBytes); } export function readHistoryTail(maxLines: number, maxBytes: number): string[] { if (!historyFilePath || !fs.existsSync(historyFilePath)) return []; const text = tailTextFile(historyFilePath, maxLines, maxBytes); return text .split('\n') .map((l) => l.trimEnd()) .filter((l) => l.length > 0); } function tailTextFile(filePath: string, maxLines: number, maxBytes: number): string { const st = fs.statSync(filePath); if (st.size === 0) return ''; const readSize = Math.min(st.size, maxBytes); const fd = fs.openSync(filePath, 'r'); try { const buf = Buffer.alloc(readSize); fs.readSync(fd, buf, 0, readSize, st.size - readSize); let s = buf.toString('utf8'); if (st.size > readSize && !s.startsWith('\n')) { const firstNl = s.indexOf('\n'); if (firstNl !== -1) s = s.slice(firstNl + 1); } const lines = s.split('\n'); if (lines.length > maxLines) { return lines.slice(-maxLines).join('\n'); } return s; } finally { fs.closeSync(fd); } } export function logGameStderr(port: number, chunk: Buffer): void { logInfo(`[game:${port}] stderr: ${chunk.toString('utf8').trimEnd()}`); } export function logGameExit(port: number, code: number | null): void { logInfo(`Game instance with port ${port} exited with code ${code}`); recordHistory(`Game on port ${port} exited (code ${code == null ? 'unknown' : code})`); }