monitor/core/modules/Logger/FXServerLogger/index.ts
2025-04-16 22:30:27 +07:00

179 lines
6.0 KiB
TypeScript

const modulename = 'Logger:FXServer';
import bytes from 'bytes';
import type { Options as RfsOptions } from 'rotating-file-stream';
import { getLogDivider } from '../loggerUtils.js';
import consoleFactory, { processStdioWriteRaw } from '@lib/console.js';
import { LoggerBase } from '../LoggerBase.js';
import ConsoleTransformer from './ConsoleTransformer.js';
import ConsoleLineEnum from './ConsoleLineEnum.js';
import { txHostConfig } from '@core/globalData.js';
const console = consoleFactory(modulename);
//This regex was done in the first place to prevent fxserver output to be interpreted as txAdmin output by the host terminal
//IIRC the issue was that one user with a TM on their nick was making txAdmin's console to close or freeze. I couldn't reproduce the issue.
// \x00-\x08 Control characters in the ASCII table.
// allow \r and \t
// \x0B-\x1A Vertical tab and control characters from shift out to substitute.
// allow \x1B (escape for colors n stuff)
// \x1C-\x1F Control characters (file separator, group separator, record separator, unit separator).
// allow all printable
// \x7F Delete character.
const regexControls = /[\x00-\x08\x0B-\x1A\x1C-\x1F\x7F]|(?:\x1B\[|\x9B)[\d;]+[@-K]/g;
const regexColors = /\x1B[^m]*?m/g;
export default class FXServerLogger extends LoggerBase {
private readonly transformer = new ConsoleTransformer();
private fileBuffer = '';
private recentBuffer = '';
private readonly recentBufferMaxSize = 256 * 1024; //kb
private readonly recentBufferTrimSliceSize = 32 * 1024; //how much will be cut when overflows
constructor(basePath: string, lrProfileConfig: RfsOptions | false) {
const lrDefaultOptions = {
path: basePath,
intervalBoundary: true,
initialRotation: true,
history: 'fxserver.history',
// compress: 'gzip',
interval: '1d',
maxFiles: 7,
maxSize: '5G',
};
super(basePath, 'fxserver', lrDefaultOptions, lrProfileConfig);
setInterval(() => {
this.flushFileBuffer();
}, 5000);
}
/**
* Returns a string with short usage stats
*/
getUsageStats() {
return `Buffer: ${bytes(this.recentBuffer.length)}, lrErrors: ${this.lrErrors}`;
}
/**
* Returns the recent fxserver buffer containing HTML markers, and not XSS escaped.
* The size of this buffer is usually above 64kb, never above 128kb.
*/
getRecentBuffer() {
return this.recentBuffer;
}
/**
* Strips color of the file buffer and flushes it.
* FIXME: this will still allow colors to be written to the file if the buffer cuts
* in the middle of a color sequence, but less often since we are buffering more data.
*/
flushFileBuffer() {
this.lrStream.write(this.fileBuffer.replace(regexColors, ''));
this.fileBuffer = '';
}
/**
* Receives the assembled console blocks, stringifies, marks, colors them and dispatches it to
* lrStream, websocket, and process stdout.
*/
private ingest(type: ConsoleLineEnum, data: string, context?: string) {
//Process the data
const { webBuffer, stdoutBuffer, fileBuffer } = this.transformer.process(type, data, context);
//To file
this.fileBuffer += fileBuffer;
//For the terminal
if (!txConfig.server.quiet && !txHostConfig.forceQuietMode) {
processStdioWriteRaw(stdoutBuffer);
}
//For the live console
txCore.webServer.webSocket.buffer('liveconsole', webBuffer);
this.appendRecent(webBuffer);
}
/**
* Writes to the log an informational message
*/
public logInformational(msg: string) {
this.ingest(ConsoleLineEnum.MarkerInfo, msg + '\n');
}
/**
* Writes to the log that the server is booting
*/
public logFxserverSpawn(pid: string) {
//force line skip to create separation
if (this.recentBuffer.length) {
const lineBreak = this.transformer.lastEol ? '\n' : '\n\n';
this.ingest(ConsoleLineEnum.MarkerInfo, lineBreak);
}
//need to break line
const multiline = getLogDivider(`[${pid}] FXServer Starting`);
for (const line of multiline.split('\n')) {
if (!line.length) break;
this.ingest(ConsoleLineEnum.MarkerInfo, line + '\n');
}
}
/**
* Writes to the log an admin command
*/
public logAdminCommand(author: string, cmd: string) {
this.ingest(ConsoleLineEnum.MarkerAdminCmd, cmd + '\n', author);
}
/**
* Writes to the log a system command.
*/
public logSystemCommand(cmd: string) {
if(cmd.startsWith('txaEvent "consoleCommand"')) return;
// if (/^txaEvent \w+ /.test(cmd)) {
// const [event, payload] = cmd.substring(9).split(' ', 2);
// cmd = chalk.italic(`<broadcasting txAdmin:events:${event}>`);
// }
this.ingest(ConsoleLineEnum.MarkerSystemCmd, cmd + '\n');
}
/**
* Handles all stdio data.
*/
public writeFxsOutput(
source: ConsoleLineEnum.StdOut | ConsoleLineEnum.StdErr,
data: string | Buffer
) {
if (typeof data !== 'string') {
data = data.toString();
}
this.ingest(source, data.replace(regexControls, ''));
}
/**
* Appends data to the recent buffer and recycles it when necessary
*/
private appendRecent(data: string) {
this.recentBuffer += data;
if (this.recentBuffer.length > this.recentBufferMaxSize) {
this.recentBuffer = this.recentBuffer.slice(this.recentBufferTrimSliceSize - this.recentBufferMaxSize);
this.recentBuffer = this.recentBuffer.substring(this.recentBuffer.indexOf('\n'));
//FIXME: precisa encontrar o próximo tsMarker ao invés de \n
//usar String.prototype.search() com regex
//FIXME: salvar em 8 blocos de 32kb
// quando atingir 32, quebrar no primeiro tsMarker
}
}
};