115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
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})`);
|
|
}
|