Additional async rewrite

This commit is contained in:
mmorrison 2019-01-12 04:43:36 -06:00
parent efe12a00aa
commit 29ce0b82d0
24 changed files with 654 additions and 470 deletions

84
lib/GameResolver.js Normal file
View 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
View 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
View 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
View 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
View 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;

View file

@ -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;

View file

@ -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;