diff --git a/README.md b/README.md index 1980a71d..162e7b4b 100644 --- a/README.md +++ b/README.md @@ -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", diff --git a/config.json b/config.json index 06237784..1cde20cd 100644 --- a/config.json +++ b/config.json @@ -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", @@ -256,4 +255,4 @@ "RCON": "redBright" } } -} +} \ No newline at end of file diff --git a/core/log-parser/index.js b/core/log-parser/index.js index d5047366..b656ca01 100644 --- a/core/log-parser/index.js +++ b/core/log-parser/index.js @@ -1,3 +1,6 @@ +import fs from 'fs'; +import path from 'path'; +import { URL } from 'url'; import EventEmitter from 'events'; import async from 'async'; @@ -5,15 +8,49 @@ 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. @@ -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} + */ + 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 } + */ 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. @@ -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); @@ -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; @@ -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; + } } diff --git a/squad-server/log-parser/adding-client-connection.js b/core/log-parser/log-events/adding-client-connection.js similarity index 100% rename from squad-server/log-parser/adding-client-connection.js rename to core/log-parser/log-events/adding-client-connection.js diff --git a/squad-server/log-parser/admin-broadcast.js b/core/log-parser/log-events/admin-broadcast.js similarity index 100% rename from squad-server/log-parser/admin-broadcast.js rename to core/log-parser/log-events/admin-broadcast.js diff --git a/squad-server/log-parser/check-permission-resolve-eosid.js b/core/log-parser/log-events/check-permission-resolve-eosid.js similarity index 100% rename from squad-server/log-parser/check-permission-resolve-eosid.js rename to core/log-parser/log-events/check-permission-resolve-eosid.js diff --git a/squad-server/log-parser/client-connected.js b/core/log-parser/log-events/client-connected.js similarity index 100% rename from squad-server/log-parser/client-connected.js rename to core/log-parser/log-events/client-connected.js diff --git a/squad-server/log-parser/client-external-account-info.js b/core/log-parser/log-events/client-external-account-info.js similarity index 100% rename from squad-server/log-parser/client-external-account-info.js rename to core/log-parser/log-events/client-external-account-info.js diff --git a/squad-server/log-parser/client-login.js b/core/log-parser/log-events/client-login.js similarity index 100% rename from squad-server/log-parser/client-login.js rename to core/log-parser/log-events/client-login.js diff --git a/squad-server/log-parser/deployable-damaged.js b/core/log-parser/log-events/deployable-damaged.js similarity index 100% rename from squad-server/log-parser/deployable-damaged.js rename to core/log-parser/log-events/deployable-damaged.js diff --git a/squad-server/log-parser/join-request.js b/core/log-parser/log-events/join-request.js similarity index 100% rename from squad-server/log-parser/join-request.js rename to core/log-parser/log-events/join-request.js diff --git a/squad-server/log-parser/login-request.js b/core/log-parser/log-events/login-request.js similarity index 100% rename from squad-server/log-parser/login-request.js rename to core/log-parser/log-events/login-request.js diff --git a/squad-server/log-parser/new-game.js b/core/log-parser/log-events/new-game.js similarity index 100% rename from squad-server/log-parser/new-game.js rename to core/log-parser/log-events/new-game.js diff --git a/squad-server/log-parser/pending-connection-destroyed.js b/core/log-parser/log-events/pending-connection-destroyed.js similarity index 100% rename from squad-server/log-parser/pending-connection-destroyed.js rename to core/log-parser/log-events/pending-connection-destroyed.js diff --git a/squad-server/log-parser/player-connected.js b/core/log-parser/log-events/player-connected.js similarity index 100% rename from squad-server/log-parser/player-connected.js rename to core/log-parser/log-events/player-connected.js diff --git a/squad-server/log-parser/player-damaged.js b/core/log-parser/log-events/player-damaged.js similarity index 100% rename from squad-server/log-parser/player-damaged.js rename to core/log-parser/log-events/player-damaged.js diff --git a/squad-server/log-parser/player-died.js b/core/log-parser/log-events/player-died.js similarity index 100% rename from squad-server/log-parser/player-died.js rename to core/log-parser/log-events/player-died.js diff --git a/squad-server/log-parser/player-disconnected.js b/core/log-parser/log-events/player-disconnected.js similarity index 100% rename from squad-server/log-parser/player-disconnected.js rename to core/log-parser/log-events/player-disconnected.js diff --git a/squad-server/log-parser/player-join-succeeded.js b/core/log-parser/log-events/player-join-succeeded.js similarity index 100% rename from squad-server/log-parser/player-join-succeeded.js rename to core/log-parser/log-events/player-join-succeeded.js diff --git a/squad-server/log-parser/player-possess.js b/core/log-parser/log-events/player-possess.js similarity index 100% rename from squad-server/log-parser/player-possess.js rename to core/log-parser/log-events/player-possess.js diff --git a/squad-server/log-parser/player-revived.js b/core/log-parser/log-events/player-revived.js similarity index 100% rename from squad-server/log-parser/player-revived.js rename to core/log-parser/log-events/player-revived.js diff --git a/squad-server/log-parser/player-un-possess.js b/core/log-parser/log-events/player-un-possess.js similarity index 100% rename from squad-server/log-parser/player-un-possess.js rename to core/log-parser/log-events/player-un-possess.js diff --git a/squad-server/log-parser/player-wounded.js b/core/log-parser/log-events/player-wounded.js similarity index 100% rename from squad-server/log-parser/player-wounded.js rename to core/log-parser/log-events/player-wounded.js diff --git a/squad-server/log-parser/playercontroller-connected.js b/core/log-parser/log-events/playercontroller-connected.js similarity index 100% rename from squad-server/log-parser/playercontroller-connected.js rename to core/log-parser/log-events/playercontroller-connected.js diff --git a/squad-server/log-parser/round-ended.js b/core/log-parser/log-events/round-ended.js similarity index 100% rename from squad-server/log-parser/round-ended.js rename to core/log-parser/log-events/round-ended.js diff --git a/squad-server/log-parser/round-tickets.js b/core/log-parser/log-events/round-tickets.js similarity index 100% rename from squad-server/log-parser/round-tickets.js rename to core/log-parser/log-events/round-tickets.js diff --git a/squad-server/log-parser/round-winner.js b/core/log-parser/log-events/round-winner.js similarity index 100% rename from squad-server/log-parser/round-winner.js rename to core/log-parser/log-events/round-winner.js diff --git a/squad-server/log-parser/sending-auth-result.js b/core/log-parser/log-events/sending-auth-result.js similarity index 100% rename from squad-server/log-parser/sending-auth-result.js rename to core/log-parser/log-events/sending-auth-result.js diff --git a/squad-server/log-parser/server-tick-rate.js b/core/log-parser/log-events/server-tick-rate.js similarity index 100% rename from squad-server/log-parser/server-tick-rate.js rename to core/log-parser/log-events/server-tick-rate.js diff --git a/squad-server/index.js b/squad-server/index.js index 840df26e..8d45897a 100644 --- a/squad-server/index.js +++ b/squad-server/index.js @@ -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'; diff --git a/squad-server/log-parser/index.js b/squad-server/log-parser/index.js deleted file mode 100644 index fa9b3737..00000000 --- a/squad-server/log-parser/index.js +++ /dev/null @@ -1,62 +0,0 @@ -import LogParser from 'core/log-parser'; - -import AdminBroadcast from './admin-broadcast.js'; -import DeployableDamaged from './deployable-damaged.js'; -import NewGame from './new-game.js'; -import PlayerConnected from './player-connected.js'; -import PlayerControllerConnected from './playercontroller-connected.js'; -import PlayerDisconnected from './player-disconnected.js'; -import PlayerDamaged from './player-damaged.js'; -import PlayerDied from './player-died.js'; -import PlayerPossess from './player-possess.js'; -import PlayerRevived from './player-revived.js'; -import PlayerUnPossess from './player-un-possess.js'; -import PlayerWounded from './player-wounded.js'; -import RoundEnded from './round-ended.js'; -import RoundTickets from './round-tickets.js'; -import RoundWinner from './round-winner.js'; -import ServerTickRate from './server-tick-rate.js'; -import AddingClientConnection from './adding-client-connection.js'; -import ClientLogin from './client-login.js'; -import PendingConnectionDestroyed from './pending-connection-destroyed.js'; -import ClientExternalAccountInfo from './client-external-account-info.js'; -import SendingAuthResult from './sending-auth-result.js'; -import LoginRequest from './login-request.js'; -import JoinRequest from './join-request.js'; -import PlayerJoinSucceeded from './player-join-succeeded.js'; -import CheckPermissionResolveEosid from './check-permission-resolve-eosid.js'; -export default class SquadLogParser extends LogParser { - constructor(options) { - super('SquadGame.log', options); - } - - getRules() { - return [ - AdminBroadcast, - DeployableDamaged, - NewGame, - PlayerConnected, - PlayerControllerConnected, - PlayerDisconnected, - PlayerDamaged, - PlayerDied, - PlayerPossess, - PlayerRevived, - PlayerUnPossess, - PlayerWounded, - RoundEnded, - RoundTickets, - RoundWinner, - ServerTickRate, - AddingClientConnection, - ClientLogin, - PendingConnectionDestroyed, - ClientExternalAccountInfo, - SendingAuthResult, - LoginRequest, - JoinRequest, - PlayerJoinSucceeded, - CheckPermissionResolveEosid - ]; - } -}