mirror of
https://github.com/tribufu/node-gamedig
synced 2026-05-18 09:35:50 +00:00
Additional async rewrite
This commit is contained in:
parent
efe12a00aa
commit
29ce0b82d0
24 changed files with 654 additions and 470 deletions
84
lib/GameResolver.js
Normal file
84
lib/GameResolver.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
const Path = require('path'),
|
||||
fs = require('fs');
|
||||
|
||||
class GameResolver {
|
||||
constructor() {
|
||||
this.games = this._readGames();
|
||||
}
|
||||
|
||||
lookup(type) {
|
||||
if(!type) throw Error('No game specified');
|
||||
|
||||
if(type.substr(0,9) === 'protocol-') {
|
||||
return {
|
||||
protocol: type.substr(9)
|
||||
};
|
||||
}
|
||||
|
||||
const game = this.games.get(type);
|
||||
if(!game) throw Error('Invalid game: '+type);
|
||||
return game.options;
|
||||
}
|
||||
|
||||
printReadme() {
|
||||
let out = '';
|
||||
for(const key of Object.keys(games)) {
|
||||
const game = games[key];
|
||||
if (!game.pretty) {
|
||||
continue;
|
||||
}
|
||||
out += "* "+game.pretty+" ("+key+")";
|
||||
if(game.options.port_query_offset || game.options.port_query)
|
||||
out += " [[Separate Query Port](#separate-query-port)]";
|
||||
if(game.extra.doc_notes)
|
||||
out += " [[Additional Notes](#"+game.extra.doc_notes+")]";
|
||||
out += "\n";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
_readGames() {
|
||||
const gamesFile = Path.normalize(__dirname+'/../games.txt');
|
||||
const lines = fs.readFileSync(gamesFile,'utf8').split('\n');
|
||||
const games = new Map();
|
||||
|
||||
for (let line of lines) {
|
||||
// strip comments
|
||||
const comment = line.indexOf('#');
|
||||
if(comment !== -1) line = line.substr(0,comment);
|
||||
line = line.trim();
|
||||
if(!line) continue;
|
||||
|
||||
const split = line.split('|');
|
||||
const gameId = split[0].trim();
|
||||
const options = this._parseList(split[3]);
|
||||
options.protocol = split[2].trim();
|
||||
|
||||
games.set(gameId, {
|
||||
pretty: split[1].trim(),
|
||||
options: options,
|
||||
extra: this._parseList(split[4])
|
||||
});
|
||||
}
|
||||
return games;
|
||||
}
|
||||
|
||||
_parseList(str) {
|
||||
if(!str) return {};
|
||||
const out = {};
|
||||
for (const one of str.split(',')) {
|
||||
const equals = one.indexOf('=');
|
||||
const key = equals === -1 ? one : one.substr(0,equals);
|
||||
let value = equals === -1 ? '' : one.substr(equals+1);
|
||||
|
||||
if(value === 'true' || value === '') value = true;
|
||||
else if(value === 'false') value = false;
|
||||
else if(!isNaN(parseInt(value))) value = parseInt(value);
|
||||
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GameResolver;
|
||||
42
lib/GlobalUdpSocket.js
Normal file
42
lib/GlobalUdpSocket.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
const dgram = require('dgram'),
|
||||
HexUtil = require('./HexUtil');
|
||||
|
||||
class GlobalUdpSocket {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.callbacks = new Set();
|
||||
this.debug = false;
|
||||
}
|
||||
|
||||
_getSocket() {
|
||||
if (!this.socket) {
|
||||
const udpSocket = this.socket = dgram.createSocket('udp4');
|
||||
udpSocket.unref();
|
||||
udpSocket.bind();
|
||||
udpSocket.on('message', (buffer, rinfo) => {
|
||||
for (const cb of this.callbacks) {
|
||||
cb(rinfo.address, rinfo.port, buffer);
|
||||
}
|
||||
});
|
||||
udpSocket.on('error', (e) => {
|
||||
if (this.debug) {
|
||||
console.log("UDP ERROR: " + e);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.socket;
|
||||
}
|
||||
|
||||
send(buffer, address, port) {
|
||||
this._getSocket().send(buffer,0,buffer.length,port,address);
|
||||
}
|
||||
|
||||
addCallback(callback) {
|
||||
this.callbacks.add(callback);
|
||||
}
|
||||
removeCallback(callback) {
|
||||
this.callbacks.delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GlobalUdpSocket;
|
||||
20
lib/Promises.js
Normal file
20
lib/Promises.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
class Promises {
|
||||
static createTimeout(timeoutMs, timeoutMsg) {
|
||||
let cancel = null;
|
||||
const wrapped = new Promise((res, rej) => {
|
||||
const timeout = setTimeout(
|
||||
() => {
|
||||
rej(new Error(timeoutMsg + " - Timed out after " + timeoutMs + "ms"));
|
||||
},
|
||||
timeoutMs
|
||||
);
|
||||
cancel = () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
});
|
||||
wrapped.cancel = cancel;
|
||||
return wrapped;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Promises;
|
||||
22
lib/ProtocolResolver.js
Normal file
22
lib/ProtocolResolver.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const Path = require('path'),
|
||||
fs = require('fs'),
|
||||
Core = require('../protocols/core');
|
||||
|
||||
class ProtocolResolver {
|
||||
constructor() {
|
||||
this.protocolDir = Path.normalize(__dirname+'/../protocols');
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Core
|
||||
*/
|
||||
create(protocolId) {
|
||||
protocolId = Path.basename(protocolId);
|
||||
const path = this.protocolDir+'/'+protocolId;
|
||||
if(!fs.existsSync(path+'.js')) throw Error('Protocol definition file missing: '+type);
|
||||
const protocol = require(path);
|
||||
return new protocol();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ProtocolResolver;
|
||||
103
lib/QueryRunner.js
Normal file
103
lib/QueryRunner.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
const GameResolver = require('./GameResolver'),
|
||||
ProtocolResolver = require('./ProtocolResolver');
|
||||
|
||||
const defaultOptions = {
|
||||
socketTimeout: 2000,
|
||||
attemptTimeout: 10000,
|
||||
maxAttempts: 1
|
||||
};
|
||||
|
||||
class QueryRunner {
|
||||
constructor(udpSocket, debug) {
|
||||
this.debug = debug;
|
||||
this.udpSocket = udpSocket;
|
||||
this.gameResolver = new GameResolver();
|
||||
this.protocolResolver = new ProtocolResolver();
|
||||
}
|
||||
async run(userOptions) {
|
||||
for (const key of Object.keys(userOptions)) {
|
||||
const value = userOptions[key];
|
||||
if (['port'].includes(key)) {
|
||||
userOptions[key] = parseInt(value);
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
port_query: gameQueryPort,
|
||||
port_query_offset: gameQueryPortOffset,
|
||||
...gameOptions
|
||||
} = this.gameResolver.lookup(userOptions.type);
|
||||
const attempts = [];
|
||||
|
||||
if (userOptions.port) {
|
||||
if (gameQueryPortOffset) {
|
||||
attempts.push({
|
||||
...defaultOptions,
|
||||
...gameOptions,
|
||||
...userOptions,
|
||||
port: userOptions.port + gameQueryPortOffset
|
||||
});
|
||||
}
|
||||
if (userOptions.port === gameOptions.port && gameQueryPort) {
|
||||
attempts.push({
|
||||
...defaultOptions,
|
||||
...gameOptions,
|
||||
...userOptions,
|
||||
port: gameQueryPort
|
||||
});
|
||||
}
|
||||
attempts.push({
|
||||
...defaultOptions,
|
||||
...gameOptions,
|
||||
...userOptions
|
||||
});
|
||||
} else if (gameQueryPort) {
|
||||
attempts.push({
|
||||
...defaultOptions,
|
||||
...gameOptions,
|
||||
...userOptions,
|
||||
port: gameQueryPort
|
||||
});
|
||||
} else if (gameOptions.port) {
|
||||
attempts.push({
|
||||
...defaultOptions,
|
||||
...gameOptions,
|
||||
...userOptions
|
||||
});
|
||||
} else {
|
||||
throw new Error("Could not determine port to query. Did you provide a port or gameid?");
|
||||
}
|
||||
|
||||
if (attempts.length === 1) {
|
||||
return await this._attempt(attempts[0]);
|
||||
} else {
|
||||
const errors = [];
|
||||
for (const attempt of attempts) {
|
||||
try {
|
||||
return await this._attempt(attempt);
|
||||
} catch(e) {
|
||||
const e2 = new Error('Failed to query port ' + attempt.port);
|
||||
e2.stack += "\nCaused by:\n" + e.stack;
|
||||
errors.push(e2);
|
||||
}
|
||||
}
|
||||
|
||||
const err = new Error('Failed all port attempts');
|
||||
err.stack = errors.map(e => e.stack).join('\n');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async _attempt(options) {
|
||||
if (this.debug) {
|
||||
console.log("Running attempt with options:");
|
||||
console.log(options);
|
||||
}
|
||||
const core = this.protocolResolver.create(options.protocol);
|
||||
core.options = options;
|
||||
core.udpSocket = this.udpSocket;
|
||||
return await core.runAllAttempts();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = QueryRunner;
|
||||
97
lib/index.js
97
lib/index.js
|
|
@ -1,87 +1,48 @@
|
|||
const dgram = require('dgram'),
|
||||
TypeResolver = require('./typeresolver'),
|
||||
HexUtil = require('./HexUtil');
|
||||
const QueryRunner = require('./QueryRunner'),
|
||||
GlobalUdpSocket = require('./GlobalUdpSocket');
|
||||
|
||||
const activeQueries = new Set();
|
||||
|
||||
const udpSocket = dgram.createSocket('udp4');
|
||||
udpSocket.unref();
|
||||
udpSocket.bind();
|
||||
udpSocket.on('message', (buffer, rinfo) => {
|
||||
if(Gamedig.debug) {
|
||||
console.log(rinfo.address+':'+rinfo.port+" <--UDP");
|
||||
console.log(HexUtil.debugDump(buffer));
|
||||
}
|
||||
for(const query of activeQueries) {
|
||||
if(query.options.address !== rinfo.address) continue;
|
||||
if(query.options.port_query !== rinfo.port) continue;
|
||||
query._udpIncoming(buffer);
|
||||
break;
|
||||
}
|
||||
});
|
||||
udpSocket.on('error', (e) => {
|
||||
if(Gamedig.debug) console.log("UDP ERROR: "+e);
|
||||
});
|
||||
let singleton = null;
|
||||
|
||||
class Gamedig {
|
||||
constructor() {
|
||||
this.udpSocket = new GlobalUdpSocket();
|
||||
this.queryRunner = new QueryRunner(this.udpSocket);
|
||||
this._debug = false;
|
||||
}
|
||||
|
||||
static query(options,callback) {
|
||||
const promise = (async () => {
|
||||
for (const key of Object.keys(options)) {
|
||||
if (['port_query', 'port'].includes(key)) {
|
||||
options[key] = parseInt(options[key]);
|
||||
}
|
||||
}
|
||||
setDebug(on) {
|
||||
this.udpSocket.debug = on;
|
||||
this._debug = on;
|
||||
this.queryRunner.debug = on;
|
||||
}
|
||||
|
||||
let query = TypeResolver.lookup(options.type);
|
||||
query.debug = Gamedig.debug;
|
||||
query.udpSocket = udpSocket;
|
||||
query.type = options.type;
|
||||
|
||||
if(!('port' in query.options) && ('port_query' in query.options)) {
|
||||
if(Gamedig.isCommandLine) {
|
||||
process.stderr.write(
|
||||
"Warning! This game is so old, that we don't know"
|
||||
+" what the server's connection port is. We've guessed that"
|
||||
+" the query port for "+query.type+" is "+query.options.port_query+"."
|
||||
+" If you know the connection port for this type of server, please let"
|
||||
+" us know on the GameDig issue tracker, thanks!\n"
|
||||
);
|
||||
}
|
||||
query.options.port = query.options.port_query;
|
||||
delete query.options.port_query;
|
||||
}
|
||||
|
||||
// copy over options
|
||||
for(const key of Object.keys(options)) {
|
||||
query.options[key] = options[key];
|
||||
}
|
||||
|
||||
activeQueries.add(query);
|
||||
try {
|
||||
return await query.runAll();
|
||||
} finally {
|
||||
activeQueries.delete(query);
|
||||
}
|
||||
})();
|
||||
async query(userOptions) {
|
||||
userOptions.debug |= this._debug;
|
||||
return await this.queryRunner.run(userOptions);
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!singleton) {
|
||||
singleton = new Gamedig();
|
||||
}
|
||||
return singleton;
|
||||
}
|
||||
static query(userOptions, callback) {
|
||||
const promise = Gamedig.getInstance().query(userOptions);
|
||||
if (callback && callback instanceof Function) {
|
||||
if(callback.length === 2) {
|
||||
if (callback.length === 2) {
|
||||
promise
|
||||
.then((state) => callback(null,state))
|
||||
.then((state) => callback(null, state))
|
||||
.catch((error) => callback(error));
|
||||
} else if (callback.length === 1) {
|
||||
promise
|
||||
.then((state) => callback(state))
|
||||
.catch((error) => callback({error:error}));
|
||||
.catch((error) => callback({error: error}));
|
||||
}
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
||||
Gamedig.debug = false;
|
||||
Gamedig.isCommandLine = false;
|
||||
Object.defineProperty(Gamedig, "debug", { set: on => Gamedig.getInstance().setDebug(on) });
|
||||
|
||||
module.exports = Gamedig;
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
const Path = require('path'),
|
||||
fs = require('fs'),
|
||||
Core = require('../protocols/core');
|
||||
|
||||
const protocolDir = Path.normalize(__dirname+'/../protocols');
|
||||
const gamesFile = Path.normalize(__dirname+'/../games.txt');
|
||||
|
||||
function parseList(str) {
|
||||
if(!str) return {};
|
||||
const out = {};
|
||||
for (const one of str.split(',')) {
|
||||
const equals = one.indexOf('=');
|
||||
const key = equals === -1 ? one : one.substr(0,equals);
|
||||
let value = equals === -1 ? '' : one.substr(equals+1);
|
||||
|
||||
if(value === 'true' || value === '') value = true;
|
||||
else if(value === 'false') value = false;
|
||||
else if(!isNaN(parseInt(value))) value = parseInt(value);
|
||||
|
||||
out[key] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function readGames() {
|
||||
const lines = fs.readFileSync(gamesFile,'utf8').split('\n');
|
||||
const games = {};
|
||||
|
||||
for (let line of lines) {
|
||||
// strip comments
|
||||
const comment = line.indexOf('#');
|
||||
if(comment !== -1) line = line.substr(0,comment);
|
||||
line = line.trim();
|
||||
if(!line) continue;
|
||||
|
||||
const split = line.split('|');
|
||||
|
||||
games[split[0].trim()] = {
|
||||
pretty: split[1].trim(),
|
||||
protocol: split[2].trim(),
|
||||
options: parseList(split[3]),
|
||||
params: parseList(split[4])
|
||||
};
|
||||
}
|
||||
return games;
|
||||
}
|
||||
const games = readGames();
|
||||
|
||||
function createProtocolInstance(type) {
|
||||
type = Path.basename(type);
|
||||
|
||||
const path = protocolDir+'/'+type;
|
||||
if(!fs.existsSync(path+'.js')) throw Error('Protocol definition file missing: '+type);
|
||||
const protocol = require(path);
|
||||
|
||||
return new protocol();
|
||||
}
|
||||
|
||||
class TypeResolver {
|
||||
/**
|
||||
* @param {string} type
|
||||
* @returns Core
|
||||
*/
|
||||
static lookup(type) {
|
||||
if(!type) throw Error('No game specified');
|
||||
|
||||
if(type.substr(0,9) === 'protocol-') {
|
||||
return createProtocolInstance(type.substr(9));
|
||||
}
|
||||
|
||||
const game = games[type];
|
||||
if(!game) throw Error('Invalid game: '+type);
|
||||
|
||||
const query = createProtocolInstance(game.protocol);
|
||||
query.pretty = game.pretty;
|
||||
for(const key of Object.keys(game.options)) {
|
||||
query.options[key] = game.options[key];
|
||||
}
|
||||
for(const key of Object.keys(game.params)) {
|
||||
query[key] = game.params[key];
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
static printReadme() {
|
||||
let out = '';
|
||||
for(const key of Object.keys(games)) {
|
||||
const game = games[key];
|
||||
if (!game.pretty) {
|
||||
continue;
|
||||
}
|
||||
out += "* "+game.pretty+" ("+key+")";
|
||||
if(game.options.port_query_offset || game.options.port_query)
|
||||
out += " [[Separate Query Port](#separate-query-port)]";
|
||||
if(game.params.doc_notes)
|
||||
out += " [[Additional Notes](#"+game.params.doc_notes+")]";
|
||||
out += "\n";
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TypeResolver;
|
||||
Loading…
Add table
Add a link
Reference in a new issue