Support hotpatching User/Room/ProcessManager protos (#8095)

This commit is contained in:
Mia 2021-03-08 07:23:17 -06:00 committed by GitHub
parent 36c4dd7fec
commit 07b1c01f75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 144 additions and 25 deletions

View File

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

View File

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

View File

@ -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();
});
});