Spaces:
Running
Running
; | |
var __create = Object.create; | |
var __defProp = Object.defineProperty; | |
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | |
var __getOwnPropNames = Object.getOwnPropertyNames; | |
var __getProtoOf = Object.getPrototypeOf; | |
var __hasOwnProp = Object.prototype.hasOwnProperty; | |
var __export = (target, all) => { | |
for (var name in all) | |
__defProp(target, name, { get: all[name], enumerable: true }); | |
}; | |
var __copyProps = (to, from, except, desc) => { | |
if (from && typeof from === "object" || typeof from === "function") { | |
for (let key of __getOwnPropNames(from)) | |
if (!__hasOwnProp.call(to, key) && key !== except) | |
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | |
} | |
return to; | |
}; | |
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( | |
// If the importer is in node compatibility mode or this is not an ESM | |
// file that has been converted to a CommonJS file using a Babel- | |
// compatible transform (i.e. "__esModule" has not been set), then set | |
// "default" to the CommonJS "module.exports" for node compatibility. | |
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, | |
mod | |
)); | |
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | |
var process_manager_exports = {}; | |
__export(process_manager_exports, { | |
ProcessManager: () => ProcessManager, | |
QueryProcessManager: () => QueryProcessManager, | |
QueryProcessWrapper: () => QueryProcessWrapper, | |
RawProcessManager: () => RawProcessManager, | |
RawProcessWrapper: () => RawProcessWrapper, | |
StreamProcessManager: () => StreamProcessManager, | |
StreamProcessWrapper: () => StreamProcessWrapper, | |
StreamWorker: () => StreamWorker, | |
exec: () => exec, | |
processManagers: () => processManagers | |
}); | |
module.exports = __toCommonJS(process_manager_exports); | |
var child_process = __toESM(require("child_process")); | |
var cluster = __toESM(require("cluster")); | |
var path = __toESM(require("path")); | |
var Streams = __toESM(require("./streams")); | |
var import_fs = require("./fs"); | |
/** | |
* Process Manager | |
* Pokemon Showdown - http://pokemonshowdown.com/ | |
* | |
* This file abstract out multiprocess logic involved in several tasks. | |
* | |
* Child processes can be queried. | |
* | |
* @license MIT | |
*/ | |
const processManagers = []; | |
function exec(args, execOptions) { | |
if (Array.isArray(args)) { | |
const cmd = args.shift(); | |
if (!cmd) | |
throw new Error(`You must pass a command to ProcessManager.exec.`); | |
return new Promise((resolve, reject) => { | |
child_process.execFile(cmd, args, execOptions, (err, stdout, stderr) => { | |
if (err) | |
reject(err); | |
if (typeof stdout !== "string") | |
stdout = stdout.toString(); | |
if (typeof stderr !== "string") | |
stderr = stderr.toString(); | |
resolve({ stdout, stderr }); | |
}); | |
}); | |
} else { | |
return new Promise((resolve, reject) => { | |
child_process.exec(args, execOptions, (error, stdout, stderr) => { | |
if (error) | |
reject(error); | |
if (typeof stdout !== "string") | |
stdout = stdout.toString(); | |
resolve(stdout); | |
}); | |
}); | |
} | |
} | |
class SubprocessStream extends Streams.ObjectReadWriteStream { | |
constructor(process2, taskId2) { | |
super(); | |
this.process = process2; | |
this.taskId = taskId2; | |
this.process.process.send(`${taskId2} | |
NEW`); | |
} | |
_write(message2) { | |
if (!this.process.process.connected) { | |
this.pushError(new Error(`Process disconnected (possibly crashed?)`)); | |
return; | |
} | |
this.process.process.send(`${this.taskId} | |
WRITE | |
${message2}`); | |
} | |
_writeEnd() { | |
this.process.process.send(`${this.taskId} | |
WRITEEND`); | |
} | |
_destroy() { | |
if (!this.process.process.connected) | |
return; | |
this.process.process.send(`${this.taskId} | |
DESTROY`); | |
this.process.deleteStream(this.taskId); | |
this.process = null; | |
} | |
} | |
class RawSubprocessStream extends Streams.ObjectReadWriteStream { | |
constructor(process2) { | |
super(); | |
this.process = process2; | |
} | |
_write(message2) { | |
if (!this.process.getProcess().connected) { | |
return; | |
} | |
this.process.process.send(message2); | |
} | |
} | |
class QueryProcessWrapper { | |
constructor(file, messageCallback) { | |
this.process = child_process.fork(file, [], { cwd: import_fs.FS.ROOT_PATH }); | |
this.taskId = 0; | |
this.file = file; | |
this.pendingTasks = /* @__PURE__ */ new Map(); | |
this.pendingRelease = null; | |
this.resolveRelease = null; | |
this.messageCallback = messageCallback || null; | |
this.process.on("message", (message2) => { | |
if (message2.startsWith("THROW\n")) { | |
const error = new Error(); | |
error.stack = message2.slice(6); | |
throw error; | |
} | |
if (message2.startsWith("DEBUG\n")) { | |
this.debug = message2.slice(6); | |
return; | |
} | |
if (this.messageCallback && message2.startsWith(`CALLBACK | |
`)) { | |
this.messageCallback(message2.slice(9)); | |
return; | |
} | |
const nlLoc2 = message2.indexOf("\n"); | |
if (nlLoc2 <= 0) | |
throw new Error(`Invalid response ${message2}`); | |
const taskId2 = parseInt(message2.slice(0, nlLoc2)); | |
const resolve = this.pendingTasks.get(taskId2); | |
if (!resolve) | |
throw new Error(`Invalid taskId ${message2.slice(0, nlLoc2)}`); | |
this.pendingTasks.delete(taskId2); | |
const resp = this.safeJSON(message2.slice(nlLoc2 + 1)); | |
resolve(resp); | |
if (this.resolveRelease && !this.getLoad()) | |
this.destroy(); | |
}); | |
} | |
safeJSON(obj) { | |
if (obj === "undefined") { | |
return void 0; | |
} | |
try { | |
return JSON.parse(obj); | |
} catch (e) { | |
global.Monitor?.crashlog?.(e, `a ${path.basename(this.file)} process`, { result: obj }); | |
return void 0; | |
} | |
} | |
getProcess() { | |
return this.process; | |
} | |
getLoad() { | |
return this.pendingTasks.size; | |
} | |
query(input) { | |
this.taskId++; | |
const taskId2 = this.taskId; | |
this.process.send(`${taskId2} | |
${JSON.stringify(input)}`); | |
return new Promise((resolve) => { | |
this.pendingTasks.set(taskId2, resolve); | |
}); | |
} | |
release() { | |
if (this.pendingRelease) | |
return this.pendingRelease; | |
if (!this.getLoad()) { | |
this.destroy(); | |
} else { | |
this.pendingRelease = new Promise((resolve) => { | |
this.resolveRelease = resolve; | |
}); | |
} | |
return this.pendingRelease; | |
} | |
destroy() { | |
if (this.pendingRelease && !this.resolveRelease) { | |
return; | |
} | |
this.process.disconnect(); | |
for (const resolver of this.pendingTasks.values()) { | |
resolver(""); | |
} | |
this.pendingTasks.clear(); | |
if (this.resolveRelease) { | |
this.resolveRelease(); | |
this.resolveRelease = null; | |
} else if (!this.pendingRelease) { | |
this.pendingRelease = Promise.resolve(); | |
} | |
} | |
} | |
class StreamProcessWrapper { | |
constructor(file, messageCallback) { | |
this.taskId = 0; | |
this.activeStreams = /* @__PURE__ */ new Map(); | |
this.pendingRelease = null; | |
this.resolveRelease = null; | |
this.process = child_process.fork(file, [], { cwd: import_fs.FS.ROOT_PATH }); | |
this.messageCallback = messageCallback; | |
this.process.on("message", (message2) => { | |
if (message2.startsWith("THROW\n")) { | |
const error = new Error(); | |
error.stack = message2.slice(6); | |
throw error; | |
} | |
if (this.messageCallback && message2.startsWith(`CALLBACK | |
`)) { | |
this.messageCallback(message2.slice(9)); | |
return; | |
} | |
if (message2.startsWith("DEBUG\n")) { | |
this.setDebug(message2.slice(6)); | |
return; | |
} | |
let nlLoc2 = message2.indexOf("\n"); | |
if (nlLoc2 <= 0) | |
throw new Error(`Invalid response ${message2}`); | |
const taskId2 = parseInt(message2.slice(0, nlLoc2)); | |
const stream2 = this.activeStreams.get(taskId2); | |
if (!stream2) | |
return; | |
message2 = message2.slice(nlLoc2 + 1); | |
nlLoc2 = message2.indexOf("\n"); | |
if (nlLoc2 < 0) | |
nlLoc2 = message2.length; | |
const messageType2 = message2.slice(0, nlLoc2); | |
message2 = message2.slice(nlLoc2 + 1); | |
if (messageType2 === "END") { | |
stream2.pushEnd(); | |
this.deleteStream(taskId2); | |
} else if (messageType2 === "PUSH") { | |
stream2.push(message2); | |
} else if (messageType2 === "THROW") { | |
const error = new Error(); | |
error.stack = message2; | |
stream2.pushError(error, true); | |
} else { | |
throw new Error(`Unrecognized messageType ${messageType2}`); | |
} | |
}); | |
} | |
setDebug(message2) { | |
this.debug = (this.debug || "").slice(-32768) + "\n=====\n" + message2; | |
} | |
getLoad() { | |
return this.activeStreams.size; | |
} | |
getProcess() { | |
return this.process; | |
} | |
deleteStream(taskId2) { | |
this.activeStreams.delete(taskId2); | |
if (this.resolveRelease && !this.getLoad()) | |
void this.destroy(); | |
} | |
createStream() { | |
this.taskId++; | |
const taskId2 = this.taskId; | |
const stream2 = new SubprocessStream(this, taskId2); | |
this.activeStreams.set(taskId2, stream2); | |
return stream2; | |
} | |
release() { | |
if (this.pendingRelease) | |
return this.pendingRelease; | |
if (!this.getLoad()) { | |
void this.destroy(); | |
} else { | |
this.pendingRelease = new Promise((resolve) => { | |
this.resolveRelease = resolve; | |
}); | |
} | |
return this.pendingRelease; | |
} | |
destroy() { | |
if (this.pendingRelease && !this.resolveRelease) { | |
return; | |
} | |
this.process.disconnect(); | |
const destroyed = []; | |
for (const stream2 of this.activeStreams.values()) { | |
destroyed.push(stream2.destroy()); | |
} | |
this.activeStreams.clear(); | |
if (this.resolveRelease) { | |
this.resolveRelease(); | |
this.resolveRelease = null; | |
} else if (!this.pendingRelease) { | |
this.pendingRelease = Promise.resolve(); | |
} | |
return Promise.all(destroyed); | |
} | |
} | |
class StreamWorker { | |
constructor(stream2) { | |
this.load = 0; | |
this.workerid = 0; | |
this.stream = stream2; | |
} | |
} | |
class RawProcessWrapper { | |
constructor(file, isCluster, env) { | |
this.taskId = 0; | |
this.pendingRelease = null; | |
this.resolveRelease = null; | |
this.workerid = 0; | |
/** Not managed by RawProcessWrapper itself */ | |
this.load = 0; | |
if (isCluster) { | |
this.process = cluster.fork(env); | |
this.workerid = this.process.id; | |
} else { | |
this.process = child_process.fork(file, [], { cwd: import_fs.FS.ROOT_PATH, env }); | |
} | |
this.process.on("message", (message2) => { | |
this.stream.push(message2); | |
}); | |
this.stream = new RawSubprocessStream(this); | |
} | |
setDebug(message2) { | |
this.debug = (this.debug || "").slice(-32768) + "\n=====\n" + message2; | |
} | |
getLoad() { | |
return this.load; | |
} | |
getProcess() { | |
return this.process.process ? this.process.process : this.process; | |
} | |
release() { | |
if (this.pendingRelease) | |
return this.pendingRelease; | |
if (!this.getLoad()) { | |
void this.destroy(); | |
} else { | |
this.pendingRelease = new Promise((resolve) => { | |
this.resolveRelease = resolve; | |
}); | |
} | |
return this.pendingRelease; | |
} | |
destroy() { | |
if (this.pendingRelease && !this.resolveRelease) { | |
return; | |
} | |
void this.stream.destroy(); | |
this.process.disconnect(); | |
} | |
} | |
const _ProcessManager = class { | |
constructor(module2) { | |
this.processes = []; | |
this.releasingProcesses = []; | |
this.crashedProcesses = []; | |
this.crashTime = 0; | |
this.crashRespawnCount = 0; | |
this.filename = module2.filename; | |
this.basename = path.basename(module2.filename); | |
this.isParentProcess = process.mainModule !== module2 || !process.send; | |
this.listen(); | |
} | |
acquire() { | |
if (!this.processes.length) { | |
return null; | |
} | |
let lowestLoad = this.processes[0]; | |
for (const process2 of this.processes) { | |
if (process2.getLoad() < lowestLoad.getLoad()) { | |
lowestLoad = process2; | |
} | |
} | |
return lowestLoad; | |
} | |
releaseCrashed(process2) { | |
const index = this.processes.indexOf(process2); | |
if (index < 0) | |
return; | |
this.processes.splice(index, 1); | |
this.destroyProcess(process2); | |
void process2.release().then(() => { | |
const releasingIndex = this.releasingProcesses.indexOf(process2); | |
if (releasingIndex >= 0) { | |
this.releasingProcesses.splice(releasingIndex, 1); | |
} | |
}); | |
const now = Date.now(); | |
if (this.crashTime && now - this.crashTime > 30 * 60 * 1e3) { | |
this.crashTime = 0; | |
this.crashRespawnCount = 0; | |
} | |
if (!this.crashTime) | |
this.crashTime = now; | |
this.crashRespawnCount += 1; | |
void Promise.reject( | |
new Error(`Process ${this.basename} ${process2.getProcess().pid} crashed and had to be restarted`) | |
); | |
this.releasingProcesses.push(process2); | |
this.crashedProcesses.push(process2); | |
if (this.crashRespawnCount <= 5) { | |
this.spawn(this.processes.length + 1); | |
} | |
} | |
unspawn() { | |
return Promise.all([...this.processes].map( | |
(process2) => this.unspawnOne(process2) | |
)); | |
} | |
async unspawnOne(process2) { | |
if (!process2) | |
return; | |
this.destroyProcess(process2); | |
const processIndex = this.processes.indexOf(process2); | |
if (processIndex < 0) | |
throw new Error("Process inactive"); | |
this.processes.splice(this.processes.indexOf(process2), 1); | |
this.releasingProcesses.push(process2); | |
await process2.release(); | |
const index = this.releasingProcesses.indexOf(process2); | |
if (index < 0) | |
return; | |
this.releasingProcesses.splice(index, 1); | |
} | |
spawn(count = 1, force) { | |
if (!this.isParentProcess) | |
return; | |
if (_ProcessManager.disabled && !force) | |
return; | |
const spawnCount = count - this.processes.length; | |
for (let i = 0; i < spawnCount; i++) { | |
this.spawnOne(force); | |
} | |
} | |
spawnOne(force) { | |
if (!this.isParentProcess) | |
throw new Error("Must use in parent process"); | |
if (_ProcessManager.disabled && !force) | |
return null; | |
const process2 = this.createProcess(); | |
process2.process.on("disconnect", () => this.releaseCrashed(process2)); | |
this.processes.push(process2); | |
return process2; | |
} | |
respawn(count = null) { | |
if (count === null) | |
count = this.processes.length; | |
const unspawned = this.unspawn(); | |
this.spawn(count); | |
return unspawned; | |
} | |
destroyProcess(process2) { | |
} | |
destroy() { | |
const index = processManagers.indexOf(this); | |
if (index >= 0) | |
processManagers.splice(index, 1); | |
return this.unspawn(); | |
} | |
}; | |
let ProcessManager = _ProcessManager; | |
ProcessManager.disabled = false; | |
class QueryProcessManager extends ProcessManager { | |
/** | |
* @param timeout The number of milliseconds to wait before terminating a query. Defaults to 900000 ms (15 minutes). | |
*/ | |
constructor(module2, query, timeout = 15 * 60 * 1e3, debugCallback) { | |
super(module2); | |
this._query = query; | |
this.timeout = timeout; | |
this.messageCallback = debugCallback; | |
processManagers.push(this); | |
} | |
async query(input, process2 = this.acquire()) { | |
if (!process2) | |
return this._query(input); | |
const timeout = setTimeout(() => { | |
const debugInfo = process2.debug || "No debug information found."; | |
process2.destroy(); | |
this.spawnOne(); | |
throw new Error( | |
`A query originating in ${this.basename} took too long to complete; the process has been respawned. | |
${debugInfo}` | |
); | |
}, this.timeout); | |
const result = await process2.query(input); | |
clearTimeout(timeout); | |
return result; | |
} | |
queryTemporaryProcess(input, force) { | |
const process2 = this.spawnOne(force); | |
const result = this.query(input, process2); | |
void this.unspawnOne(process2); | |
return result; | |
} | |
createProcess() { | |
return new QueryProcessWrapper(this.filename, this.messageCallback); | |
} | |
listen() { | |
if (this.isParentProcess) | |
return; | |
process.on("message", (message) => { | |
const nlLoc = message.indexOf("\n"); | |
if (nlLoc <= 0) | |
throw new Error(`Invalid response ${message}`); | |
const taskId = message.slice(0, nlLoc); | |
message = message.slice(nlLoc + 1); | |
if (taskId.startsWith("EVAL")) { | |
process.send(`${taskId} | |
` + eval(message)); | |
return; | |
} | |
void Promise.resolve(this._query(JSON.parse(message))).then( | |
(response) => process.send(`${taskId} | |
${JSON.stringify(response)}`) | |
); | |
}); | |
process.on("disconnect", () => { | |
process.exit(); | |
}); | |
} | |
} | |
class StreamProcessManager extends ProcessManager { | |
constructor(module2, createStream, messageCallback) { | |
super(module2); | |
this.activeStreams = /* @__PURE__ */ new Map(); | |
this._createStream = createStream; | |
this.messageCallback = messageCallback; | |
processManagers.push(this); | |
} | |
createStream() { | |
const process2 = this.acquire(); | |
if (!process2) | |
return this._createStream(); | |
return process2.createStream(); | |
} | |
createProcess() { | |
return new StreamProcessWrapper(this.filename, this.messageCallback); | |
} | |
async pipeStream(taskId2, stream2) { | |
let done = false; | |
while (!done) { | |
try { | |
let value; | |
({ value, done } = await stream2.next()); | |
process.send(`${taskId2} | |
PUSH | |
${value}`); | |
} catch (err) { | |
process.send(`${taskId2} | |
THROW | |
${err.stack}`); | |
} | |
} | |
if (!this.activeStreams.has(taskId2)) { | |
return; | |
} | |
process.send(`${taskId2} | |
END`); | |
this.activeStreams.delete(taskId2); | |
} | |
listen() { | |
if (this.isParentProcess) | |
return; | |
process.on("message", (message) => { | |
let nlLoc = message.indexOf("\n"); | |
if (nlLoc <= 0) | |
throw new Error(`Invalid request ${message}`); | |
const taskId = message.slice(0, nlLoc); | |
const stream = this.activeStreams.get(taskId); | |
message = message.slice(nlLoc + 1); | |
nlLoc = message.indexOf("\n"); | |
if (nlLoc < 0) | |
nlLoc = message.length; | |
const messageType = message.slice(0, nlLoc); | |
message = message.slice(nlLoc + 1); | |
if (taskId.startsWith("EVAL")) { | |
process.send(`${taskId} | |
` + eval(message)); | |
return; | |
} | |
if (messageType === "NEW") { | |
if (stream) | |
throw new Error(`NEW: taskId ${taskId} already exists`); | |
const newStream = this._createStream(); | |
this.activeStreams.set(taskId, newStream); | |
void this.pipeStream(taskId, newStream); | |
} else if (messageType === "DESTROY") { | |
if (!stream) | |
throw new Error(`DESTROY: Invalid taskId ${taskId}`); | |
void stream.destroy(); | |
this.activeStreams.delete(taskId); | |
} else if (messageType === "WRITE") { | |
if (!stream) | |
throw new Error(`WRITE: Invalid taskId ${taskId}`); | |
void stream.write(message); | |
} else if (messageType === "WRITEEND") { | |
if (!stream) | |
throw new Error(`WRITEEND: Invalid taskId ${taskId}`); | |
void stream.writeEnd(); | |
} else { | |
throw new Error(`Unrecognized messageType ${messageType}`); | |
} | |
}); | |
process.on("disconnect", () => { | |
process.exit(); | |
}); | |
} | |
} | |
class RawProcessManager extends ProcessManager { | |
constructor(options) { | |
super(options.module); | |
/** full list of processes - parent process only */ | |
this.workers = []; | |
/** if spawning 0 worker processes, the worker is instead stored here in the parent process */ | |
this.masterWorker = null; | |
/** stream used only in the child process */ | |
this.activeStream = null; | |
this.spawnSubscription = null; | |
this.unspawnSubscription = null; | |
/** worker ID of cluster worker - cluster child process only (0 otherwise) */ | |
this.workerid = cluster.worker?.id || 0; | |
this.isCluster = !!options.isCluster; | |
this._setupChild = options.setupChild; | |
this.env = options.env; | |
if (this.isCluster && this.isParentProcess) { | |
cluster.setupMaster({ | |
exec: this.filename, | |
cwd: import_fs.FS.ROOT_PATH | |
}); | |
} | |
processManagers.push(this); | |
} | |
subscribeSpawn(callback) { | |
this.spawnSubscription = callback; | |
} | |
subscribeUnspawn(callback) { | |
this.unspawnSubscription = callback; | |
} | |
spawn(count) { | |
super.spawn(count); | |
if (!this.workers.length) { | |
this.masterWorker = new StreamWorker(this._setupChild()); | |
this.workers.push(this.masterWorker); | |
this.spawnSubscription?.(this.masterWorker); | |
} | |
} | |
createProcess() { | |
const process2 = new RawProcessWrapper(this.filename, this.isCluster, this.env); | |
this.workers.push(process2); | |
this.spawnSubscription?.(process2); | |
return process2; | |
} | |
destroyProcess(process2) { | |
const index = this.workers.indexOf(process2); | |
if (index >= 0) | |
this.workers.splice(index, 1); | |
this.unspawnSubscription?.(process2); | |
} | |
async pipeStream(stream2) { | |
let done = false; | |
while (!done) { | |
try { | |
let value; | |
({ value, done } = await stream2.next()); | |
process.send(value); | |
} catch (err) { | |
process.send(`THROW | |
${err.stack}`); | |
} | |
} | |
} | |
listen() { | |
if (this.isParentProcess) | |
return; | |
setImmediate(() => { | |
this.activeStream = this._setupChild(); | |
void this.pipeStream(this.activeStream); | |
}); | |
process.on("message", (message2) => { | |
void this.activeStream.write(message2); | |
}); | |
process.on("disconnect", () => { | |
process.exit(); | |
}); | |
} | |
} | |
//# sourceMappingURL=process-manager.js.map | |