-
Notifications
You must be signed in to change notification settings - Fork 529
/
outputHandler.ts
167 lines (149 loc) · 6.26 KB
/
outputHandler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
const modulename = 'OutputHandler';
import { anyUndefined } from '@core/extras/helpers';
import TxAdmin from '@core/txAdmin';
import consoleFactory from '@extras/console';
const console = consoleFactory(modulename);
//Helpers
const deferError = (m: string, t = 500) => {
setTimeout(() => {
console.error(m);
}, t);
};
type StructuredTraceType = {
key: number;
value: {
channel: string;
data: any;
file: string;
func: string;
line: number;
}
}
/**
* FXServer output helper that mostly relays to other components.
*/
export default class OutputHandler {
readonly #txAdmin: TxAdmin;
constructor(txAdmin: TxAdmin) {
this.#txAdmin = txAdmin;
}
/**
* Processes FD3 traces
*
* Mapped straces:
* nucleus_connected
* watchdog_bark
* bind_error
* script_log
* script_structured_trace (handled by server logger)
*/
trace(mutex: string, trace: StructuredTraceType) {
try {
//Filter valid and fresh packages
if (mutex !== this.#txAdmin.fxRunner.currentMutex) 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 {
if (!this.#txAdmin.fxRunner.restartDelayOverride) {
this.#txAdmin.fxRunner.restartDelayOverride = 10000;
} else if (this.#txAdmin.fxRunner.restartDelayOverride <= 45000) {
this.#txAdmin.fxRunner.restartDelayOverride += 5000;
}
const [_ip, port] = data.address.split(':');
deferError(`Detected FXServer error: Port ${port} is busy! Increasing restart delay to ${this.#txAdmin.fxRunner.restartDelayOverride}.`);
} 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`);
this.#txAdmin.fxRunner.cfxId = matches[2];
this.#txAdmin.persistentCache.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') {
try {
deferError(`Detected FXServer thread ${data.thread} hung with stack:`);
deferError(`\t${data.stack}`); //TODO: add to diagnostics page
deferError('Please check the resource above to prevent further hangs.');
} catch (e) { }
return;
}
//Handle script traces
if (
channel == 'citizen-server-impl'
&& data.type == 'script_structured_trace'
&& data.resource === 'monitor'
) {
if (data.payload.type === 'txAdminHeartBeat') {
this.#txAdmin.healthMonitor.handleHeartBeat('fd3');
} else if (data.payload.type === 'txAdminLogData') {
this.#txAdmin.logger.server.write(data.payload.logs, mutex);
} else if (data.payload.type === 'txAdminResourceEvent') {
this.#txAdmin.resourcesManager.handleServerEvents(data.payload, mutex);
} else if (data.payload.type === 'txAdminPlayerlistEvent') {
this.#txAdmin.playerlistManager.handleServerEvents(data.payload, mutex);
} else if (data.payload.type === 'txAdminCommandBridge') {
this.bridgeCommand(data.payload);
}
}
} catch (error) {
console.verbose.error('Error processing FD3 stream output:');
console.verbose.dir(error);
}
}
/**
* handles stdout and stderr from child fxserver and send to be processed by the logger
* TODO: use zod for type safety
*/
bridgeCommand(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;
this.#txAdmin.logger.admin.write(author, `Sending announcement: ${message}`);
// Dispatch `txAdmin:events:announcement`
this.#txAdmin.fxRunner.sendEvent('announcement', { message, author });
// Sending discord announcement
this.#txAdmin.discordBot.sendAnnouncement({
type: 'info',
title: {
key: 'nui_menu.misc.announcement_title',
data: { author }
},
description: message
});
} catch (error) {
console.verbose.warn(`bridgeCommand handler error:`);
console.verbose.dir(error);
}
} else {
console.warn(`Command bridge received invalid command:`);
console.dir(payload);
}
}
/**
* handles stdout and stderr from child fxserver and send to be processed by the logger
*/
write(source: string, mutex: string, data: string | Buffer) {
data = data.toString();
this.#txAdmin.logger.fxserver.writeStdIO(source, data);
}
};