Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated, bumped Discord.js to v14.11.0 + discord plugins + log-parser #300

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ The following section of the configuration contains information about your Squad
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/log/folder",
"ftp":{
"host": "xxx.xxx.xxx.xxx",
"port": 21,
"user": "FTP Username",
"password": "FTP Password",
Expand Down
3 changes: 1 addition & 2 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"logReaderMode": "tail",
"logDir": "C:/path/to/squad/log/folder",
"ftp": {
"host": "xxx.xxx.xxx.xxx",
"port": 21,
"user": "FTP Username",
"password": "FTP Password",
Expand Down Expand Up @@ -256,4 +255,4 @@
"RCON": "redBright"
}
}
}
}
205 changes: 185 additions & 20 deletions core/log-parser/index.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,56 @@
import fs from 'fs';
import path from 'path';
import { URL } from 'url';
import EventEmitter from 'events';

import async from 'async';
import moment from 'moment';

import Logger from '../logger.js';

import TailLogReader from './log-readers/tail.js';
import FTPLogReader from './log-readers/ftp.js';
import TailModule from 'tail';
import FTPTail from 'ftp-tail';

/**
* @typedef { object } Rule
* @property { RegExp } regex
* @property { (args: RegExpMatchArray, logParser: unknown) => logParser is LogParser } onMatch
*/

/**
* @typedef { object[] } RuleArr
* @property { Rule } rule
* @property { RegExpMatchArray } match
*/

/** @type { RuleArr } */
const addedLines = [];

const Tail = TailModule.Tail;

/**
* Checks if `structure` file contains valid `regex` and `onMatch`
* @param {*} structure
* @returns {boolean}
*/
const predicate = (structure) =>
Boolean(structure) &&
typeof structure === 'object' &&
'regex' in structure &&
'onMatch' in structure &&
RegExp(structure.regex) &&
typeof structure.onMatch === 'function';

export default class LogParser extends EventEmitter {
constructor(filename = 'filename.log', options = {}) {
constructor(options = {}) {
super();

options.filename = filename;
if (!('filename' in options)) {
options.filename = 'SquadGame.log';
}

this.rules = [];
this.options = options;
this.eventStore = {
disconnected: {}, // holding area, cleared on map change.
players: [], // persistent data, steamid, controller, suffix.
Expand All @@ -35,39 +72,155 @@ export default class LogParser extends EventEmitter {

this.queue = async.queue(this.processLine);

switch (options.mode || 'tail') {
this.setup();
}

async setup() {
/**
* @type { Rule[] } Load all
*/
this.rules = await this.loadFiles(new URL('log-events/', import.meta.url));
if (this.rules.length === 0) {
throw new Error('Unable to find valid files in "log-events" folder');
}
switch (this.options.mode || 'tail') {
case 'tail':
this.logReader = new TailLogReader(this.queue.push, options);
// Verify Tail requirements
if (!('logDir' in this.options)) {
throw new Error('logDir must be specified.');
}
// Provide only the filename and use fs.watchFile instead of fs.watch
this.logReader = new Tail(path.join(this.options.logDir, this.options.filename), {
useWatchFile: true
});
this.logReader.on('line', this.queue.push);
break;
case 'ftp':
this.logReader = new FTPLogReader(this.queue.push, options);
// Verify FTPTail requirements
for (const option of ['host', 'user', 'password', 'logDir']) {
if (!(option in this.options)) {
throw new Error(`${option} must be specified.`);
}
}
// Provide only FTPTail options
this.logReader = new FTPTail({
host: this.options.host,
port: this.options.port || 21,
user: this.options.user,
password: this.options.password,
secure: this.options.secure || false,
timeout: this.options.timeout || 2000,
encoding: 'utf8',
verbose: this.options.verbose,

path: path.join(this.options.logDir, this.options.filename),

fetchInterval: this.options.fetchInterval || 0,
maxTempFileSize: this.options.maxTempFileSize || 5 * 1000 * 1000, // 5 MB

useListForSize: this.options.useListForSize || false
});
this.logReader.on('line', this.queue.push);
break;
default:
throw new Error('Invalid mode.');
}
}

/**
* Loads all the files in the provided directory.
*
* @template T
* @param {import('node:fs').PathLike} dir - The directory to load the files from
* @param {boolean} recursive - Whether to recursively load the files in the directory
* @returns {Promise<T[]>}
*/
async loadFiles(dir, recursive = true) {
const statDir = await fs.promises.stat(dir);
if (!statDir.isDirectory()) {
throw new Error(`The directory '${dir}' is not a directory.`);
}
const files = await fs.promises.readdir(dir);
/** @type {T[]} */
const structures = [];
for (const file of files) {
// We only want .js files
if (file === 'index.js' || !file.endsWith('.js')) {
continue;
}
const statFile = await fs.promises.stat(new URL(`${dir}/${file}`));
if (statFile.isDirectory() && recursive) {
structures.push(...(await this.loadFiles(`${dir}/${file}`, recursive)));
continue;
}
const structure = (await import(`${dir}/${file}`)).default;
if (predicate(structure)) {
/**
* Prevent file from being added if it contains `disabled: true`
* Useful when testing or if the file contains errors
*/
if (
'disabled' in structure &&
typeof structure.disabled === 'boolean' &&
structure.disabled
)
continue;
structures.push(structure);
}
}

return structures;
}

/**
* @param { string } line
* @returns { Promise<void> }
*/
async processLine(line) {
Logger.verbose('LogParser', 4, `Matching on line: ${line}`);

for (const rule of this.getRules()) {
let i = this.rules.length;
while (i--) {
const rule = this.rules[i];
const match = line.match(rule.regex);
if (!match) continue;

Logger.verbose('LogParser', 3, `Matched on line: ${match[0]}`);

addedLines.push({ rule, match });
}
if (addedLines.length === 0) return;
this.onLine(addedLines);
addedLines.length = 0;
}

/**
* @param { RuleArr } addedLine
*/
onLine(addedLine) {
this.linesPerMinute += 1;

let i = addedLine.length;
while (i--) {
const { rule, match } = addedLine[i];
match[1] = moment.utc(match[1], 'YYYY.MM.DD-hh.mm.ss:SSS').toDate();
match[2] = parseInt(match[2]);

/**
* Prevent SquadJS from dying
* Might cause a loop if an error occurs??? Needs testing.
*/
// try {
// rule.onMatch(match, this);
// } catch (ex) {
// Logger.verbose('LogParser', 1, 'Error', ex);
// }

rule.onMatch(match, this);

this.matchingLinesPerMinute++;
this.matchingLinesPerMinute += 1;
this.matchingLatency += Date.now() - match[1];

break;
}

this.linesPerMinute++;
}

// manage cleanup disconnected players, session data.
Expand All @@ -83,13 +236,14 @@ export default class LogParser extends EventEmitter {
this.eventStore.session = {};
}

getRules() {
return [];
}

async watch() {
Logger.verbose('LogParser', 1, 'Attempting to watch log file...');
await this.logReader.watch();
// FTPTails.watch is async while Tails.watch is not
if (this.logReader.watch instanceof Promise) {
await this.logReader.watch();
} else {
await Promise.resolve(this.logReader.watch());
}
Logger.verbose('LogParser', 1, 'Watching log file...');

this.parsingStatsInterval = setInterval(this.logStats, 60 * 1000);
Expand All @@ -104,7 +258,9 @@ export default class LogParser extends EventEmitter {
} lines per minute | Matching lines per minute: ${
this.matchingLinesPerMinute
} matching lines per minute | Average matching latency: ${
this.matchingLatency / this.matchingLinesPerMinute
Number.isNaN(this.matchingLatency / this.matchingLinesPerMinute)
? 0
: this.matchingLatency / this.matchingLinesPerMinute
}ms`
);
this.linesPerMinute = 0;
Expand All @@ -113,8 +269,17 @@ export default class LogParser extends EventEmitter {
}

async unwatch() {
await this.logReader.unwatch();
// FTPTails.unwatch is async while Tails.unwatch is not
if (this.logReader.unwatch instanceof Promise) {
await this.logReader.unwatch();
} else {
await Promise.resolve(this.logReader.unwatch());
}

clearInterval(this.parsingStatsInterval);
}

getRules() {
return this.rules;
}
}
2 changes: 1 addition & 1 deletion squad-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { SQUADJS_API_DOMAIN } from 'core/constants';

import { Layers } from './layers/index.js';

import LogParser from './log-parser/index.js';
import LogParser from 'core/log-parser';
import Rcon from './rcon.js';

import { SQUADJS_VERSION } from './utils/constants.js';
Expand Down
62 changes: 0 additions & 62 deletions squad-server/log-parser/index.js

This file was deleted.