monitor/core/modules/FxRunner/handleFd3Messages.ts
2025-04-16 22:30:27 +07:00

160 lines
5.6 KiB
TypeScript

import { anyUndefined } from '@lib/misc';
import consoleFactory from '@lib/console';
const console = consoleFactory('FXProc:FD3');
//Types
type StructuredTraceType = {
key: number;
value: {
channel: string;
data: any;
file: string;
func: string;
line: number;
}
}
/**
* Handles bridged commands from txResource.
* TODO: use zod for type safety
*/
const handleBridgedCommands = (payload: any) => {
if (payload.command === 'announcement') {
try {
//Validate input
if (typeof payload.author !== 'string') throw new Error(`invalid author`);
if (typeof payload.message !== 'string') throw new Error(`invalid message`);
const message = (payload.message ?? '').trim();
if (!message.length) throw new Error(`empty message`);
//Resolve admin
const author = payload.author;
txCore.logger.admin.write(author, `Sending announcement: ${message}`);
// Dispatch `txAdmin:events:announcement`
txCore.fxRunner.sendEvent('announcement', { message, author });
// Sending discord announcement
const publicAuthor = txCore.adminStore.getAdminPublicName(payload.author, 'message');
txCore.discordBot.sendAnnouncement({
type: 'info',
title: {
key: 'nui_menu.misc.announcement_title',
data: { author: publicAuthor }
},
description: message
});
} catch (error) {
console.verbose.warn(`handleBridgedCommands handler error:`);
console.verbose.dir(error);
}
} else {
console.warn(`Command bridge received invalid command:`);
console.dir(payload);
}
}
/**
* Processes FD3 Messages
*
* Mapped message types:
* - nucleus_connected
* - watchdog_bark
* - bind_error
* - script_log
* - script_structured_trace (handled by server logger)
*/
const handleFd3Messages = (mutex: string, trace: StructuredTraceType) => {
//Filter valid and fresh packages
if (!mutex || mutex !== txCore.fxRunner.child?.mutex) return;
if (anyUndefined(trace, trace.value, trace.value.data, trace.value.channel)) return;
const { channel, data } = trace.value;
//Handle bind errors
if (channel === 'citizen-server-impl' && data?.type === 'bind_error') {
try {
const newDelayBackoffMs = txCore.fxRunner.signalSpawnBackoffRequired(true);
const [_ip, port] = data.address.split(':');
const secs = Math.floor(newDelayBackoffMs / 1000);
console.defer().error(`Detected FXServer error: Port ${port} is busy! Setting backoff delay to ${secs}s.`);
} catch (e) { }
return;
}
//Handle nucleus auth
if (channel === 'citizen-server-impl' && data.type === 'nucleus_connected') {
if (typeof data.url !== 'string') {
console.error(`FD3 nucleus_connected event without URL.`);
} else {
try {
const matches = /^(https:\/\/)?.*-([0-9a-z]{6,})\.users\.cfx\.re\/?$/.exec(data.url);
if (!matches || !matches[2]) throw new Error(`invalid cfxid`);
txCore.cacheStore.set('fxsRuntime:cfxId', matches[2]);
} catch (error) {
console.error(`Error decoding server nucleus URL.`);
}
}
return;
}
//Handle watchdog
if (channel === 'citizen-server-impl' && data.type === 'watchdog_bark') {
setTimeout(() => {
const thread = data?.thread ?? 'UNKNOWN';
if(!data?.stack || data.stack.trim() === 'root'){
console.error(`Detected server thread ${thread} hung without a stack trace.`);
} else {
console.error(`Detected server thread ${thread} hung with stack:`);
console.error(`- ${data.stack}`);
console.error('Please check the resource above to prevent server restarts.');
}
}, 250);
return;
}
// if (data.type == 'script_log') {
// return console.dir(data);
// }
//Handle script traces
if (
channel === 'citizen-server-impl'
&& data.type === 'script_structured_trace'
&& data.resource === 'monitor'
) {
if (data.payload.type === 'txAdminHeartBeat') {
txCore.fxMonitor.handleHeartBeat('fd3');
} else if (data.payload.type === 'txAdminLogData') {
txCore.logger.server.write(data.payload.logs, mutex);
} else if (data.payload.type === 'txAdminLogNodeHeap') {
txCore.metrics.svRuntime.logServerNodeMemory(data.payload);
} else if (data.payload.type === 'txAdminResourceEvent') {
txCore.fxResources.handleServerEvents(data.payload, mutex);
} else if (data.payload.type === 'txAdminPlayerlistEvent') {
txCore.fxPlayerlist.handleServerEvents(data.payload, mutex);
} else if (data.payload.type === 'txAdminCommandBridge') {
handleBridgedCommands(data.payload);
} else if (data.payload.type === 'txAdminAckWarning') {
txCore.database.actions.ackWarn(data.payload.actionId);
}
}
}
/**
* Handles all the FD3 traces from the FXServer
* NOTE: this doesn't need to be a class, but might need to hold state in the future
*/
export default (mutex: string, trace: StructuredTraceType) => {
try {
handleFd3Messages(mutex, trace);
} catch (error) {
console.verbose.error('Error processing FD3 stream output:');
console.verbose.dir(error);
}
};