mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
Support hotpatching User/Room/ProcessManager protos (#8095)
This commit is contained in:
parent
36c4dd7fec
commit
07b1c01f75
|
|
@ -95,7 +95,7 @@ class RawSubprocessStream extends Streams.ObjectReadWriteStream<string> {
|
|||
}
|
||||
|
||||
interface ProcessWrapper {
|
||||
load: number;
|
||||
getLoad: () => number;
|
||||
process: ChildProcess | Worker;
|
||||
release: () => Promise<void>;
|
||||
getProcess: () => ChildProcess;
|
||||
|
|
@ -144,7 +144,7 @@ export class QueryProcessWrapper<T, U> implements ProcessWrapper {
|
|||
this.pendingTasks.delete(taskId);
|
||||
resolve(JSON.parse(message.slice(nlLoc + 1)));
|
||||
|
||||
if (this.resolveRelease && !this.load) this.destroy();
|
||||
if (this.resolveRelease && !this.getLoad()) this.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ export class QueryProcessWrapper<T, U> implements ProcessWrapper {
|
|||
return this.process;
|
||||
}
|
||||
|
||||
get load() {
|
||||
getLoad() {
|
||||
return this.pendingTasks.size;
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ export class QueryProcessWrapper<T, U> implements ProcessWrapper {
|
|||
|
||||
release(): Promise<void> {
|
||||
if (this.pendingRelease) return this.pendingRelease;
|
||||
if (!this.load) {
|
||||
if (!this.getLoad()) {
|
||||
this.destroy();
|
||||
} else {
|
||||
this.pendingRelease = new Promise(resolve => {
|
||||
|
|
@ -260,6 +260,10 @@ export class StreamProcessWrapper implements ProcessWrapper {
|
|||
});
|
||||
}
|
||||
|
||||
getLoad() {
|
||||
return this.activeStreams.size;
|
||||
}
|
||||
|
||||
getProcess() {
|
||||
return this.process;
|
||||
}
|
||||
|
|
@ -267,11 +271,7 @@ export class StreamProcessWrapper implements ProcessWrapper {
|
|||
deleteStream(taskId: number) {
|
||||
this.activeStreams.delete(taskId);
|
||||
// try to release
|
||||
if (this.resolveRelease && !this.load) void this.destroy();
|
||||
}
|
||||
|
||||
get load() {
|
||||
return this.activeStreams.size;
|
||||
if (this.resolveRelease && !this.getLoad()) void this.destroy();
|
||||
}
|
||||
|
||||
createStream(): SubprocessStream {
|
||||
|
|
@ -284,7 +284,7 @@ export class StreamProcessWrapper implements ProcessWrapper {
|
|||
|
||||
release(): Promise<void> {
|
||||
if (this.pendingRelease) return this.pendingRelease;
|
||||
if (!this.load) {
|
||||
if (!this.getLoad()) {
|
||||
void this.destroy();
|
||||
} else {
|
||||
this.pendingRelease = new Promise(resolve => {
|
||||
|
|
@ -361,13 +361,16 @@ export class RawProcessWrapper implements ProcessWrapper, StreamWorker {
|
|||
this.stream = new RawSubprocessStream(this);
|
||||
}
|
||||
|
||||
getLoad() {
|
||||
return this.load;
|
||||
}
|
||||
getProcess() {
|
||||
return this.process.process ? this.process.process : this.process;
|
||||
}
|
||||
|
||||
release(): Promise<void> {
|
||||
if (this.pendingRelease) return this.pendingRelease;
|
||||
if (!this.load) {
|
||||
if (!this.getLoad()) {
|
||||
void this.destroy();
|
||||
} else {
|
||||
this.pendingRelease = new Promise(resolve => {
|
||||
|
|
@ -417,7 +420,7 @@ export abstract class ProcessManager<T extends ProcessWrapper = ProcessWrapper>
|
|||
}
|
||||
let lowestLoad = this.processes[0];
|
||||
for (const process of this.processes) {
|
||||
if (process.load < lowestLoad.load) {
|
||||
if (process.getLoad() < lowestLoad.getLoad()) {
|
||||
lowestLoad = process;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,26 @@ function bash(command: string, context: CommandContext, cwd?: string): Promise<[
|
|||
});
|
||||
}
|
||||
|
||||
function keysIncludingNonEnumerable(obj: object) {
|
||||
const methods = new Set<string>();
|
||||
let current = obj;
|
||||
do {
|
||||
const curProps = Object.getOwnPropertyNames(current);
|
||||
for (const prop of curProps) {
|
||||
methods.add(prop);
|
||||
}
|
||||
} while ((current = Object.getPrototypeOf(current)));
|
||||
return [...methods];
|
||||
}
|
||||
|
||||
function keysToCopy(obj: object) {
|
||||
return keysIncludingNonEnumerable(obj).filter(
|
||||
// `__` matches sucrase init methods
|
||||
// FIXME: document what 'prop' is for
|
||||
prop => !(prop.includes('__') || prop.toLowerCase().includes('prop') || ['valueOf', 'constructor'].includes(prop))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether or not the rebase failed
|
||||
*/
|
||||
|
|
@ -406,7 +426,10 @@ export const commands: ChatCommands = {
|
|||
await rebuild(this);
|
||||
|
||||
const lock = Monitor.hotpatchLock;
|
||||
const hotpatches = ['chat', 'formats', 'loginserver', 'punishments', 'dnsbl', 'modlog'];
|
||||
const hotpatches = [
|
||||
'chat', 'formats', 'loginserver', 'punishments', 'dnsbl', 'modlog',
|
||||
'processmanager', 'roomsp', 'usersp',
|
||||
];
|
||||
|
||||
try {
|
||||
Utils.clearRequireCache({exclude: ['/.lib-dist/process-manager']});
|
||||
|
|
@ -422,12 +445,13 @@ export const commands: ChatCommands = {
|
|||
await this.parse(`/hotpatch ${hotpatch}`);
|
||||
}
|
||||
} else if (target === 'chat' || target === 'commands') {
|
||||
if (lock['chat']) {
|
||||
return this.errorReply(`Hot-patching chat has been disabled by ${lock['chat'].by} (${lock['chat'].reason})`);
|
||||
}
|
||||
if (lock['tournaments']) {
|
||||
return this.errorReply(`Hot-patching tournaments has been disabled by ${lock['tournaments'].by} (${lock['tournaments'].reason})`);
|
||||
}
|
||||
if (lock['chat']) {
|
||||
return this.errorReply(`Hot-patching chat has been disabled by ${lock['chat'].by} (${lock['chat'].reason})`);
|
||||
}
|
||||
|
||||
this.sendReply("Hotpatching chat commands...");
|
||||
|
||||
const disabledCommands = Chat.allCommands().filter(c => c.disabled).map(c => `/${c.fullCmd}`);
|
||||
|
|
@ -456,11 +480,100 @@ export const commands: ChatCommands = {
|
|||
this.sendReply("Reloading chat plugins...");
|
||||
Chat.loadPlugins(oldPlugins);
|
||||
this.sendReply("DONE");
|
||||
} else if (target === 'processmanager') {
|
||||
if (lock['processmanager']) {
|
||||
return this.errorReply(
|
||||
`Hot-patching formats has been disabled by ${lock['processmanager'].by} ` +
|
||||
`(${lock['processmanager'].reason})`
|
||||
);
|
||||
}
|
||||
this.sendReply('Hotpatching processmanager prototypes...');
|
||||
|
||||
// keep references
|
||||
const cache = {...require.cache};
|
||||
Utils.clearRequireCache();
|
||||
const newPM = require('../../lib/process-manager');
|
||||
require.cache = cache;
|
||||
|
||||
const protos = [
|
||||
[ProcessManager.QueryProcessManager, newPM.QueryProcessManager],
|
||||
[ProcessManager.StreamProcessManager, newPM.StreamProcessManager],
|
||||
[ProcessManager.ProcessManager, newPM.ProcessManager],
|
||||
[ProcessManager.RawProcessManager, newPM.RawProcessManager],
|
||||
[ProcessManager.QueryProcessWrapper, newPM.QueryProcessWrapper],
|
||||
[ProcessManager.StreamProcessWrapper, newPM.StreamProcessWrapper],
|
||||
[ProcessManager.RawProcessManager, newPM.RawProcessWrapper],
|
||||
].map(part => part.map(constructor => constructor.prototype));
|
||||
|
||||
for (const [oldProto, newProto] of protos) {
|
||||
const newKeys = keysToCopy(newProto);
|
||||
const oldKeys = keysToCopy(oldProto);
|
||||
for (const key of oldKeys) {
|
||||
if (!newProto[key]) {
|
||||
delete oldProto[key];
|
||||
}
|
||||
}
|
||||
for (const key of newKeys) {
|
||||
oldProto[key] = newProto[key];
|
||||
}
|
||||
}
|
||||
this.sendReply('DONE');
|
||||
} else if (target === 'usersp' || target === 'roomsp') {
|
||||
if (lock[target]) {
|
||||
return this.errorReply(`Hot-patching ${target} has been disabled by ${lock[target].by} (${lock[target].reason})`);
|
||||
}
|
||||
let newProto: any, oldProto: any, message: string;
|
||||
switch (target) {
|
||||
case 'usersp':
|
||||
newProto = require('../users').User.prototype;
|
||||
oldProto = Users.User.prototype;
|
||||
message = 'user prototypes';
|
||||
break;
|
||||
case 'roomsp':
|
||||
newProto = require('../rooms').BasicRoom.prototype;
|
||||
oldProto = Rooms.BasicRoom.prototype;
|
||||
message = 'rooms prototypes';
|
||||
break;
|
||||
}
|
||||
|
||||
this.sendReply(`Hotpatching ${message}...`);
|
||||
const newKeys = keysToCopy(newProto);
|
||||
const oldKeys = keysToCopy(oldProto);
|
||||
|
||||
const counts = {
|
||||
added: 0,
|
||||
updated: 0,
|
||||
deleted: 0,
|
||||
};
|
||||
|
||||
for (const key of oldKeys) {
|
||||
if (!newProto[key]) {
|
||||
counts.deleted++;
|
||||
delete oldProto[key];
|
||||
}
|
||||
}
|
||||
for (const key of newKeys) {
|
||||
if (!oldProto[key]) {
|
||||
counts.added++;
|
||||
} else if (
|
||||
// compare source code
|
||||
typeof oldProto[key] !== 'function' || oldProto[key].toString() !== newProto[key].toString()
|
||||
) {
|
||||
counts.updated++;
|
||||
}
|
||||
|
||||
oldProto[key] = newProto[key];
|
||||
}
|
||||
this.sendReply(`DONE`);
|
||||
this.sendReply(
|
||||
`Updated ${Chat.count(counts.updated, 'methods')}` +
|
||||
(counts.added ? `, added ${Chat.count(counts.added, 'new methods')} to ${message}` : '') +
|
||||
(counts.deleted ? `, and removed ${Chat.count(counts.deleted, 'methods')}.` : '.')
|
||||
);
|
||||
} else if (target === 'tournaments') {
|
||||
if (lock['tournaments']) {
|
||||
return this.errorReply(`Hot-patching tournaments has been disabled by ${lock['tournaments'].by} (${lock['tournaments'].reason})`);
|
||||
}
|
||||
|
||||
this.sendReply("Hotpatching tournaments...");
|
||||
|
||||
global.Tournaments = require('../tournaments').Tournaments;
|
||||
|
|
@ -587,7 +700,10 @@ export const commands: ChatCommands = {
|
|||
if (!reason || !target.includes(separator)) return this.parse('/help nohotpatch');
|
||||
|
||||
const lock = Monitor.hotpatchLock;
|
||||
const validDisable = ['chat', 'battles', 'formats', 'validator', 'tournaments', 'punishments', 'modlog', 'all'];
|
||||
const validDisable = [
|
||||
'roomsp', 'usersp', 'chat', 'battles', 'formats', 'validator',
|
||||
'tournaments', 'punishments', 'modlog', 'all', 'processmanager',
|
||||
];
|
||||
|
||||
if (!validDisable.includes(hotpatch)) {
|
||||
return this.errorReply(`Disabling hotpatching "${hotpatch}" is not supported.`);
|
||||
|
|
@ -648,7 +764,7 @@ export const commands: ChatCommands = {
|
|||
for (const manager of ProcessManager.processManagers) {
|
||||
for (const [i, process] of manager.processes.entries()) {
|
||||
const pid = process.getProcess().pid;
|
||||
buf += `<strong>${pid}</strong> - ${manager.basename} ${i} (load ${process.load}`;
|
||||
buf += `<strong>${pid}</strong> - ${manager.basename} ${i} (load ${process.getLoad()}`;
|
||||
const info = processes.get(`${pid}`)!;
|
||||
if (info.cpu) buf += `, CPU: ${info.cpu}`;
|
||||
if (info.time) buf += `, time: ${info.time}`;
|
||||
|
|
@ -657,7 +773,7 @@ export const commands: ChatCommands = {
|
|||
}
|
||||
for (const [i, process] of manager.releasingProcesses.entries()) {
|
||||
const pid = process.getProcess().pid;
|
||||
buf += `<strong>${pid}</strong> - PENDING RELEASE ${manager.basename} ${i} (load ${process.load}`;
|
||||
buf += `<strong>${pid}</strong> - PENDING RELEASE ${manager.basename} ${i} (load ${process.getLoad()}`;
|
||||
const info = processes.get(`${pid}`)!;
|
||||
if (info.cpu) buf += `, CPU: ${info.cpu}`;
|
||||
if (info.time) buf += `, time: ${info.time}`;
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ describe('Simulator abstraction layer features', function () {
|
|||
const PM = require('../../.server-dist/room-battle').PM;
|
||||
assert.equal(PM.processes.length, 0);
|
||||
PM.spawn(1, true);
|
||||
assert.equal(PM.processes[0].load, 0);
|
||||
assert.equal(PM.processes[0].getLoad(), 0);
|
||||
|
||||
const stream = PM.createStream();
|
||||
assert.equal(PM.processes[0].load, 1);
|
||||
assert.equal(PM.processes[0].getLoad(), 1);
|
||||
stream.write(
|
||||
'>version a2393dfd2a2da5594148bf99eea514e72b136c2c\n' +
|
||||
'>start {"formatid":"gen8randombattle","seed":[9619,36790,28450,62465],"rated":"Rated battle"}\n' +
|
||||
|
|
@ -61,10 +61,10 @@ describe('Simulator abstraction layer features', function () {
|
|||
assert((await stream.read()).startsWith('sideupdate\np2\n|request|'));
|
||||
assert((await stream.read()).includes('|move|'));
|
||||
stream.destroy();
|
||||
assert.equal(PM.processes[0].load, 0);
|
||||
assert.equal(PM.processes[0].getLoad(), 0);
|
||||
|
||||
const stream2 = PM.createStream();
|
||||
assert.equal(PM.processes[0].load, 1);
|
||||
assert.equal(PM.processes[0].getLoad(), 1);
|
||||
stream2.write(
|
||||
'>version a2393dfd2a2da5594148bf99eea514e72b136c2c\n' +
|
||||
'>start {"formatid":"gen8randombattle","seed":[9619,36790,28450,62465],"rated":"Rated battle"}\n' +
|
||||
|
|
@ -76,7 +76,7 @@ describe('Simulator abstraction layer features', function () {
|
|||
assert(await stream2.read());
|
||||
stream2.writeEnd();
|
||||
await stream2.readAll();
|
||||
assert.equal(PM.processes[0].load, 0);
|
||||
assert.equal(PM.processes[0].getLoad(), 0);
|
||||
PM.unspawn();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user