mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-04-02 07:06:06 -05:00
The new FS module is an abstraction layer over the built-in fs module.
The main reason it exists is because I need an abstraction layer I can
disable writing from. But that'll be in another commit.
Currently, mine is better because:
- paths are always relative to PS's base directory
- Promises (seriously wtf Node Core what are you thinking)
- PS-style API: FS("foo.txt").write("bar") for easier argument order
- mkdirp
This also increases the minimum supported Node version from v6.0 to
v7.7, because we now use async/await. Sorry for the inconvenience!
202 lines
5.5 KiB
JavaScript
202 lines
5.5 KiB
JavaScript
/**
|
|
* Monitor
|
|
* Pokemon Showdown - http://pokemonshowdown.com/
|
|
*
|
|
* Various utility functions to make sure PS is running healthy.
|
|
*
|
|
* @license MIT license
|
|
*/
|
|
'use strict';
|
|
|
|
const FS = require('./fs');
|
|
|
|
class TimedCounter extends Map {
|
|
increment(key, timeLimit) {
|
|
let val = this.get(key);
|
|
let now = Date.now();
|
|
if (!val || now > val[1] + timeLimit) {
|
|
this.set(key, [1, Date.now()]);
|
|
return [1, 0];
|
|
} else {
|
|
val[0]++;
|
|
return [val[0], now - val[1]];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Config.loglevel is:
|
|
// 0 = everything
|
|
// 1 = debug (same as 0 for now)
|
|
// 2 = notice (default)
|
|
// 3 = warning
|
|
// (4 is currently unused)
|
|
// 5 = supposedly completely silent, but for now a lot of PS output doesn't respect loglevel
|
|
if (Config.loglevel === undefined) Config.loglevel = 2;
|
|
|
|
const Monitor = module.exports = {
|
|
|
|
/*********************************************************
|
|
* Logging
|
|
*********************************************************/
|
|
|
|
log: function (text) {
|
|
this.notice(text);
|
|
if (Rooms('staff')) {
|
|
Rooms('staff').add('|c|~|' + text).update();
|
|
}
|
|
},
|
|
adminlog: function (text) {
|
|
this.notice(text);
|
|
if (Rooms('upperstaff')) {
|
|
Rooms('upperstaff').add('|c|~|' + text).update();
|
|
}
|
|
},
|
|
logHTML: function (text) {
|
|
this.notice(text);
|
|
if (Rooms('staff')) {
|
|
Rooms('staff').add('|html|' + text).update();
|
|
}
|
|
},
|
|
debug: function (text) {
|
|
if (Config.loglevel <= 1) console.log(text);
|
|
},
|
|
warn: function (text) {
|
|
if (Config.loglevel <= 3) console.log(text);
|
|
},
|
|
notice: function (text) {
|
|
if (Config.loglevel <= 2) console.log(text);
|
|
},
|
|
|
|
/*********************************************************
|
|
* Resource Monitor
|
|
*********************************************************/
|
|
|
|
clean: function () {
|
|
Monitor.clearNetworkUse();
|
|
Monitor.battlePreps.clear();
|
|
Monitor.battles.clear();
|
|
Monitor.connections.clear();
|
|
Dnsbl.cache.clear();
|
|
},
|
|
connections: new TimedCounter(),
|
|
battles: new TimedCounter(),
|
|
battlePreps: new TimedCounter(),
|
|
groupChats: new TimedCounter(),
|
|
networkUse: {},
|
|
networkCount: {},
|
|
hotpatchLock: false,
|
|
/**
|
|
* Counts a connection. Returns true if the connection should be terminated for abuse.
|
|
*/
|
|
countConnection: function (ip, name) {
|
|
let val = this.connections.increment(ip, 30 * 60 * 1000);
|
|
let count = val[0], duration = val[1];
|
|
name = (name ? ': ' + name : '');
|
|
if (count === 500) {
|
|
this.adminlog('[ResourceMonitor] IP ' + ip + ' banned for cflooding (' + count + ' times in ' + Chat.toDurationString(duration) + name + ')');
|
|
return true;
|
|
} else if (count > 500) {
|
|
if (count % 500 === 0) {
|
|
let c = count / 500;
|
|
if (c === 2 || c === 4 || c === 10 || c === 20 || c % 40 === 0) {
|
|
this.adminlog('[ResourceMonitor] IP ' + ip + ' still cflooding (' + count + ' times in ' + Chat.toDurationString(duration) + name + ')');
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Counts a battle. Returns true if the connection should be terminated for abuse.
|
|
*/
|
|
countBattle: function (ip, name) {
|
|
let val = this.battles.increment(ip, 30 * 60 * 1000);
|
|
let count = val[0], duration = val[1];
|
|
name = (name ? ': ' + name : '');
|
|
if (duration < 5 * 60 * 1000 && count % 30 === 0) {
|
|
this.adminlog('[ResourceMonitor] IP ' + ip + ' has battled ' + count + ' times in the last ' + Chat.toDurationString(duration) + name);
|
|
} else if (count % 150 === 0) {
|
|
this.adminlog('[ResourceMonitor] IP ' + ip + ' has battled ' + count + ' times in the last ' + Chat.toDurationString(duration) + name);
|
|
}
|
|
},
|
|
/**
|
|
* Counts battle prep. Returns true if too much
|
|
*/
|
|
countPrepBattle: function (ip, connection) {
|
|
let count = this.battlePreps.increment(ip, 3 * 60 * 1000)[0];
|
|
if (count > 12) {
|
|
if (Punishments.sharedIps.has(ip) && count < 120) return;
|
|
connection.popup(`Due to high load, you are limited to 12 battles and team validations every 3 minutes.`);
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Counts concurrent battles. Returns true if too much
|
|
*/
|
|
countConcurrentBattle: function (count, connection) {
|
|
if (count > 5) {
|
|
connection.popup(`Due to high load, you are limited to 5 games at the same time.`);
|
|
return true;
|
|
}
|
|
},
|
|
/**
|
|
* Counts group chat creation. Returns true if too much.
|
|
*/
|
|
countGroupChat: function (ip) {
|
|
let count = this.groupChats.increment(ip, 60 * 60 * 1000)[0];
|
|
if (count > 4) return true;
|
|
},
|
|
/**
|
|
* data
|
|
*/
|
|
countNetworkUse: function (size) {
|
|
if (Config.emergency) {
|
|
if (this.activeIp in this.networkUse) {
|
|
this.networkUse[this.activeIp] += size;
|
|
this.networkCount[this.activeIp]++;
|
|
} else {
|
|
this.networkUse[this.activeIp] = size;
|
|
this.networkCount[this.activeIp] = 1;
|
|
}
|
|
}
|
|
},
|
|
writeNetworkUse: function () {
|
|
let buf = '';
|
|
for (let i in this.networkUse) {
|
|
buf += '' + this.networkUse[i] + '\t' + this.networkCount[i] + '\t' + i + '\n';
|
|
}
|
|
FS('logs/networkuse.tsv').write(buf);
|
|
},
|
|
clearNetworkUse: function () {
|
|
if (Config.emergency) {
|
|
this.networkUse = {};
|
|
this.networkCount = {};
|
|
}
|
|
},
|
|
/**
|
|
* Counts roughly the size of an object to have an idea of the server load.
|
|
*/
|
|
sizeOfObject: function (object) {
|
|
let objectList = [];
|
|
let stack = [object];
|
|
let bytes = 0;
|
|
|
|
while (stack.length) {
|
|
let value = stack.pop();
|
|
if (typeof value === 'boolean') {
|
|
bytes += 4;
|
|
} else if (typeof value === 'string') {
|
|
bytes += value.length * 2;
|
|
} else if (typeof value === 'number') {
|
|
bytes += 8;
|
|
} else if (typeof value === 'object' && !objectList.includes(value)) {
|
|
objectList.push(value);
|
|
for (let i in value) stack.push(value[i]);
|
|
}
|
|
}
|
|
|
|
return bytes;
|
|
},
|
|
};
|
|
|
|
Monitor.cleanInterval = setInterval(() => Monitor.clean(), 2 * 60 * 60 * 1000);
|