bitburner-src/src/NetscriptWorker.ts

625 lines
22 KiB
TypeScript
Raw Normal View History

/**
* Functions for handling WorkerScripts, which are the underlying mechanism
* that allows for scripts to run
*/
import { killWorkerScript } from "./Netscript/killWorkerScript";
import { ScriptDeath } from "./Netscript/ScriptDeath";
import { WorkerScript } from "./Netscript/WorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";
import { generateNextPid } from "./Netscript/Pid";
import { CONSTANTS } from "./Constants";
2021-09-25 00:42:13 +02:00
import { Interpreter } from "./ThirdParty/JSInterpreter";
import { NetscriptFunctions } from "./NetscriptFunctions";
2022-07-18 08:28:21 +02:00
import { executeJSScript, Node } from "./NetscriptJSEvaluator";
import { IPort } from "./NetscriptPort";
import { RunningScript } from "./Script/RunningScript";
import { getRamUsageFromRunningScript } from "./Script/RunningScriptHelpers";
2021-05-01 09:17:31 +02:00
import { scriptCalculateOfflineProduction } from "./Script/ScriptHelpers";
2021-09-24 23:56:30 +02:00
import { Script } from "./Script/Script";
2021-10-07 22:04:04 +02:00
import { GetAllServers } from "./Server/AllServers";
2021-09-24 23:56:30 +02:00
import { BaseServer } from "./Server/BaseServer";
import { Settings } from "./Settings/Settings";
import { generate } from "escodegen";
2021-09-25 20:42:57 +02:00
import { dialogBoxCreate } from "./ui/React/DialogBox";
import { arrayToString } from "./utils/helpers/arrayToString";
import { roundToTwo } from "./utils/helpers/roundToTwo";
import { isString } from "./utils/helpers/isString";
2021-05-01 09:17:31 +02:00
import { parse } from "acorn";
2021-05-29 20:48:56 +02:00
import { simple as walksimple } from "acorn-walk";
2021-11-06 02:01:23 +01:00
import { areFilesEqual } from "./Terminal/DirectoryHelpers";
2021-11-16 05:49:33 +01:00
import { Player } from "./Player";
import { Terminal } from "./Terminal";
2022-07-18 08:28:21 +02:00
import { ScriptArg } from "./Netscript/ScriptArg";
2022-08-08 19:43:41 +02:00
import { helpers } from "./Netscript/NetscriptHelpers";
export const NetscriptPorts: Map<number, IPort> = new Map();
2017-07-22 00:54:55 +02:00
2021-09-25 08:36:49 +02:00
export function prestigeWorkerScripts(): void {
2021-09-05 01:09:30 +02:00
for (const ws of workerScripts.values()) {
ws.env.stopFlag = true;
killWorkerScript(ws);
}
2022-09-01 16:27:31 +02:00
NetscriptPorts.clear();
2021-09-18 21:44:39 +02:00
WorkerScriptStartStopEventEmitter.emit();
2021-09-05 01:09:30 +02:00
workerScripts.clear();
}
// JS script promises need a little massaging to have the same guarantees as netscript
// promises. This does said massaging and kicks the script off. It returns a promise
// that resolves or rejects when the corresponding worker script is done.
2022-08-28 02:56:12 +02:00
function startNetscript2Script(workerScript: WorkerScript): Promise<void> {
return new Promise<void>((resolve, reject) => {
2022-08-28 02:56:12 +02:00
executeJSScript(Player, workerScript.getServer().scripts, workerScript)
2021-09-25 01:09:19 +02:00
.then(() => {
resolve();
2021-09-25 01:09:19 +02:00
})
.catch((e) => reject(e));
}).catch((e) => {
2021-09-24 23:56:30 +02:00
if (e instanceof Error) {
if (e instanceof SyntaxError) {
2022-08-08 19:43:41 +02:00
workerScript.errorMessage = helpers.makeRuntimeRejectMsg(
workerScript,
e.message + " (sorry we can't be more helpful)",
);
} else {
2022-08-08 19:43:41 +02:00
workerScript.errorMessage = helpers.makeRuntimeRejectMsg(
workerScript,
e.message + ((e.stack && "\nstack:\n" + e.stack.toString()) || ""),
);
}
throw new ScriptDeath(workerScript);
2022-08-08 19:43:41 +02:00
} else if (helpers.isScriptErrorMessage(e)) {
2021-09-24 23:56:30 +02:00
workerScript.errorMessage = e;
throw new ScriptDeath(workerScript);
} else if (e instanceof ScriptDeath) {
2021-12-14 04:26:22 +01:00
throw e;
2021-09-24 23:56:30 +02:00
}
2021-12-14 04:26:22 +01:00
// Don't know what to do with it, let's try making an error message out of it
2022-08-08 19:43:41 +02:00
workerScript.errorMessage = helpers.makeRuntimeRejectMsg(workerScript, "" + e);
throw new ScriptDeath(workerScript);
2021-09-24 23:56:30 +02:00
});
2021-09-05 01:09:30 +02:00
}
function startNetscript1Script(workerScript: WorkerScript): Promise<void> {
2021-09-05 01:09:30 +02:00
const code = workerScript.code;
//Process imports
2021-09-25 07:26:03 +02:00
let codeWithImports, codeLineOffset;
2021-09-05 01:09:30 +02:00
try {
2021-09-25 07:26:03 +02:00
const importProcessingRes = processNetscript1Imports(code, workerScript);
2021-09-05 01:09:30 +02:00
codeWithImports = importProcessingRes.code;
codeLineOffset = importProcessingRes.lineOffset;
2022-07-15 07:51:30 +02:00
} catch (e: unknown) {
dialogBoxCreate("Error processing Imports in " + workerScript.name + ":<br>" + String(e));
2021-09-05 01:09:30 +02:00
workerScript.env.stopFlag = true;
killWorkerScript(workerScript);
return Promise.resolve();
2021-09-05 01:09:30 +02:00
}
2022-08-28 02:56:12 +02:00
function wrapNS1Layer(int: Interpreter, intLayer: unknown, path: string[] = []) {
//TODO: Better typing layers of interpreter scope and ns
interface BasicObject {
2022-08-10 00:08:04 +02:00
[key: string]: any;
}
2022-08-28 02:56:12 +02:00
const nsLayer = path.reduce((prev, newPath) => prev[newPath], workerScript.env.vars as BasicObject);
for (const [name, entry] of Object.entries(nsLayer)) {
2021-09-05 01:09:30 +02:00
if (typeof entry === "function") {
2022-08-28 02:56:12 +02:00
// Async functions need to be wrapped. See JS-Interpreter documentation
const wrapper = async (...args: unknown[]) => {
// This async wrapper is sent a resolver function as an extra arg.
// See JSInterpreter.js:3209
try {
const callback = args.pop() as (value: unknown) => void;
const result = await entry(...args.map(int.pseudoToNative));
return callback(int.nativeToPseudo(result));
} catch (e: unknown) {
// TODO: Unify error handling, this was stolen from previous async handler
if (typeof e === "string") {
console.error(e);
const errorTextArray = e.split("|DELIMITER|");
const hostname = errorTextArray[1];
const scriptName = errorTextArray[2];
const errorMsg = errorTextArray[3];
let msg = `${scriptName}@${hostname}<br>`;
msg += "<br>";
msg += errorMsg;
dialogBoxCreate(msg);
workerScript.env.stopFlag = true;
killWorkerScript(workerScript);
return;
}
2022-08-28 02:56:12 +02:00
}
};
int.setProperty(intLayer, name, int.createAsyncFunction(wrapper));
} else if (Array.isArray(entry) || typeof entry !== "object") {
2022-08-28 02:56:12 +02:00
// args, strings on enums, etc
int.setProperty(intLayer, name, int.nativeToPseudo(entry));
2021-09-05 01:09:30 +02:00
} else {
2022-08-28 02:56:12 +02:00
// new object layer, e.g. bladeburner
int.setProperty(intLayer, name, int.nativeToPseudo({}));
wrapNS1Layer(int, (intLayer as BasicObject).properties[name], [...path, name]);
2021-09-05 01:09:30 +02:00
}
2021-04-30 05:52:56 +02:00
}
}
2021-09-05 01:09:30 +02:00
2022-07-15 07:51:30 +02:00
let interpreter: Interpreter;
2021-09-05 01:09:30 +02:00
try {
2022-08-28 02:56:12 +02:00
interpreter = new Interpreter(codeWithImports, wrapNS1Layer, codeLineOffset);
2022-07-15 07:51:30 +02:00
} catch (e: unknown) {
dialogBoxCreate("Syntax ERROR in " + workerScript.name + ":<br>" + String(e));
2021-09-05 01:09:30 +02:00
workerScript.env.stopFlag = true;
killWorkerScript(workerScript);
return Promise.resolve();
2021-09-05 01:09:30 +02:00
}
return new Promise(function (resolve, reject) {
2021-09-25 08:36:49 +02:00
function runInterpreter(): void {
2021-09-05 01:09:30 +02:00
try {
if (workerScript.env.stopFlag) {
return reject(new ScriptDeath(workerScript));
}
2021-11-05 19:32:21 +01:00
let more = true;
let i = 0;
while (i < 3 && more) {
more = more && interpreter.step();
i++;
}
if (more) {
setTimeout(runInterpreter, Settings.CodeInstructionRunTime);
2021-09-05 01:09:30 +02:00
} else {
resolve();
}
2022-07-15 07:51:30 +02:00
} catch (_e: unknown) {
let e = String(_e);
2022-08-08 19:43:41 +02:00
if (!helpers.isScriptErrorMessage(e)) {
e = helpers.makeRuntimeRejectMsg(workerScript, e);
2021-09-05 01:09:30 +02:00
}
workerScript.errorMessage = e;
return reject(new ScriptDeath(workerScript));
2021-09-05 01:09:30 +02:00
}
}
try {
2021-09-05 01:09:30 +02:00
runInterpreter();
2022-07-15 07:51:30 +02:00
} catch (e: unknown) {
2021-09-05 01:09:30 +02:00
if (isString(e)) {
workerScript.errorMessage = e;
return reject(new ScriptDeath(workerScript));
} else if (e instanceof ScriptDeath) {
2021-09-05 01:09:30 +02:00
return reject(e);
} else {
console.error(e);
return reject(new ScriptDeath(workerScript));
2021-09-05 01:09:30 +02:00
}
}
2021-09-05 01:09:30 +02:00
});
}
/* Since the JS Interpreter used for Netscript 1.0 only supports ES5, the keyword
'import' throws an error. However, since we want to support import funtionality
we'll implement it ourselves by parsing the Nodes in the AST out.
@param code - The script's code
@returns {Object} {
code: Newly-generated code with imported functions
lineOffset: Net number of lines of code added/removed due to imported functions
Should typically be positive
}
*/
2022-07-18 08:28:21 +02:00
function processNetscript1Imports(code: string, workerScript: WorkerScript): { code: string; lineOffset: number } {
2021-09-05 01:09:30 +02:00
//allowReserved prevents 'import' from throwing error in ES5
2022-07-18 08:28:21 +02:00
const ast: Node = parse(code, {
2021-09-05 01:09:30 +02:00
ecmaVersion: 9,
allowReserved: true,
sourceType: "module",
});
2021-09-25 07:26:03 +02:00
const server = workerScript.getServer();
2021-09-05 01:09:30 +02:00
if (server == null) {
throw new Error("Failed to find underlying Server object for script");
}
2021-09-24 23:56:30 +02:00
function getScript(scriptName: string): Script | null {
2021-09-05 01:09:30 +02:00
for (let i = 0; i < server.scripts.length; ++i) {
if (server.scripts[i].filename === scriptName) {
return server.scripts[i];
}
}
2021-09-05 01:09:30 +02:00
return null;
}
let generatedCode = ""; // Generated Javascript Code
let hasImports = false;
// Walk over the tree and process ImportDeclaration nodes
walksimple(ast, {
2022-07-18 08:28:21 +02:00
ImportDeclaration: (node: Node) => {
2021-09-05 01:09:30 +02:00
hasImports = true;
let scriptName = node.source.value;
if (scriptName.startsWith("./")) {
scriptName = scriptName.slice(2);
}
2021-09-25 07:26:03 +02:00
const script = getScript(scriptName);
2021-09-05 01:09:30 +02:00
if (script == null) {
throw new Error("'Import' failed due to invalid script: " + scriptName);
}
2021-09-25 07:26:03 +02:00
const scriptAst = parse(script.code, {
2021-09-05 01:09:30 +02:00
ecmaVersion: 9,
allowReserved: true,
sourceType: "module",
});
2021-09-09 05:47:34 +02:00
if (node.specifiers.length === 1 && node.specifiers[0].type === "ImportNamespaceSpecifier") {
2021-09-05 01:09:30 +02:00
// import * as namespace from script
2021-09-25 07:26:03 +02:00
const namespace = node.specifiers[0].local.name;
const fnNames: string[] = []; //Names only
2022-07-18 08:28:21 +02:00
const fnDeclarations: Node[] = []; //FunctionDeclaration Node objects
2021-09-05 01:09:30 +02:00
walksimple(scriptAst, {
2022-07-18 08:28:21 +02:00
FunctionDeclaration: (node: Node) => {
2021-09-05 01:09:30 +02:00
fnNames.push(node.id.name);
fnDeclarations.push(node);
},
});
//Now we have to generate the code that would create the namespace
generatedCode += `var ${namespace};\n(function (namespace) {\n`;
2021-09-05 01:09:30 +02:00
//Add the function declarations
2022-07-18 08:28:21 +02:00
fnDeclarations.forEach((fn: Node) => {
2021-09-05 01:09:30 +02:00
generatedCode += generate(fn);
generatedCode += "\n";
});
//Add functions to namespace
fnNames.forEach((fnName) => {
generatedCode += "namespace." + fnName + " = " + fnName;
generatedCode += "\n";
});
//Finish
generatedCode += `})(${namespace} || (" + namespace + " = {}));\n`;
2021-09-05 01:09:30 +02:00
} else {
//import {...} from script
//Get array of all fns to import
2021-09-25 07:26:03 +02:00
const fnsToImport: string[] = [];
2022-07-18 08:28:21 +02:00
node.specifiers.forEach((e: Node) => {
2021-09-05 01:09:30 +02:00
fnsToImport.push(e.local.name);
});
//Walk through script and get FunctionDeclaration code for all specified fns
2022-07-18 08:28:21 +02:00
const fnDeclarations: Node[] = [];
2021-09-05 01:09:30 +02:00
walksimple(scriptAst, {
2022-07-18 08:28:21 +02:00
FunctionDeclaration: (node: Node) => {
2021-09-05 01:09:30 +02:00
if (fnsToImport.includes(node.id.name)) {
fnDeclarations.push(node);
}
2021-09-05 01:09:30 +02:00
},
});
//Convert FunctionDeclarations into code
2022-07-18 08:28:21 +02:00
fnDeclarations.forEach((fn: Node) => {
2021-09-05 01:09:30 +02:00
generatedCode += generate(fn);
generatedCode += "\n";
});
}
},
});
//If there are no imports, just return the original code
if (!hasImports) {
return { code: code, lineOffset: 0 };
}
//Remove ImportDeclarations from AST. These ImportDeclarations must be in top-level
2021-09-25 07:26:03 +02:00
let linesRemoved = 0;
2021-09-05 01:09:30 +02:00
if (ast.type !== "Program" || ast.body == null) {
throw new Error("Code could not be properly parsed");
}
for (let i = ast.body.length - 1; i >= 0; --i) {
if (ast.body[i].type === "ImportDeclaration") {
ast.body.splice(i, 1);
++linesRemoved;
}
2021-09-05 01:09:30 +02:00
}
2021-09-05 01:09:30 +02:00
//Calculated line offset
2021-09-25 07:26:03 +02:00
const lineOffset = (generatedCode.match(/\n/g) || []).length - linesRemoved;
2021-09-05 01:09:30 +02:00
//Convert the AST back into code
code = generate(ast);
2021-09-05 01:09:30 +02:00
//Add the imported code and re-generate in ES5 (JS Interpreter for NS1 only supports ES5);
code = generatedCode + code;
2021-09-25 07:26:03 +02:00
const res = {
2021-09-05 01:09:30 +02:00
code: code,
lineOffset: lineOffset,
};
return res;
}
/**
* Used to start a RunningScript (by creating and starting its
* corresponding WorkerScript), and add the RunningScript to the server on which
* it is active
*/
export function startWorkerScript(runningScript: RunningScript, server: BaseServer, parent?: WorkerScript): number {
2022-08-28 02:56:12 +02:00
if (createAndAddWorkerScript(runningScript, server, parent)) {
2021-09-05 01:09:30 +02:00
// Push onto runningScripts.
// This has to come after createAndAddWorkerScript() because that fn updates RAM usage
2021-09-24 23:56:30 +02:00
server.runScript(runningScript);
2021-09-05 01:09:30 +02:00
// Once the WorkerScript is constructed in createAndAddWorkerScript(), the RunningScript
// object should have a PID assigned to it, so we return that
return runningScript.pid;
}
return 0;
}
/**
* Given a RunningScript object, constructs its corresponding WorkerScript,
* adds it to the global 'workerScripts' pool, and begins executing it.
* @param {RunningScript} runningScriptObj - Script that's being run
* @param {Server} server - Server on which the script is to be run
* returns {boolean} indicating whether or not the workerScript was successfully added
*/
function createAndAddWorkerScript(runningScriptObj: RunningScript, server: BaseServer, parent?: WorkerScript): boolean {
2021-09-05 01:09:30 +02:00
// Update server's ram usage
let threads = 1;
if (runningScriptObj.threads && !isNaN(runningScriptObj.threads)) {
threads = runningScriptObj.threads;
} else {
runningScriptObj.threads = 1;
}
2021-10-28 00:55:06 +02:00
const oneRamUsage = getRamUsageFromRunningScript(runningScriptObj);
const ramUsage = roundToTwo(oneRamUsage * threads);
2021-09-05 01:09:30 +02:00
const ramAvailable = server.maxRam - server.ramUsed;
if (ramUsage > ramAvailable + 0.001) {
2021-09-05 01:09:30 +02:00
dialogBoxCreate(
2022-09-05 14:19:24 +02:00
`Not enough RAM to run script ${runningScriptObj.filename} with args ${arrayToString(runningScriptObj.args)}.\n` +
`This can occur when you reload the game and the script's RAM usage has increased (either because of an update to the game or ` +
2022-09-05 14:29:03 +02:00
`your changes to the script).\nThis can also occur if you have attempted to launch a script from a tail window with insufficient RAM. `,
2021-09-05 01:09:30 +02:00
);
return false;
}
2021-11-16 05:49:33 +01:00
server.updateRamUsed(roundToTwo(server.ramUsed + ramUsage), Player);
2021-09-05 01:09:30 +02:00
// Get the pid
const pid = generateNextPid();
if (pid === -1) {
throw new Error(
`Failed to start script because could not find available PID. This is most ` +
`because you have too many scripts running.`,
);
}
// Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying
// RunningScript's PID as well
const workerScript = new WorkerScript(runningScriptObj, pid, NetscriptFunctions);
workerScript.ramUsage = oneRamUsage;
2021-09-05 01:09:30 +02:00
// Add the WorkerScript to the global pool
workerScripts.set(pid, workerScript);
2021-09-18 21:44:39 +02:00
WorkerScriptStartStopEventEmitter.emit();
2021-09-05 01:09:30 +02:00
// Start the script's execution
let scriptExecution: Promise<void> | null = null; // Script's resulting promise
2022-07-21 21:09:55 +02:00
if (workerScript.name.endsWith(".js")) {
2022-08-28 02:56:12 +02:00
scriptExecution = startNetscript2Script(workerScript);
2021-09-05 01:09:30 +02:00
} else {
scriptExecution = startNetscript1Script(workerScript);
if (!(scriptExecution instanceof Promise)) {
2021-09-05 01:09:30 +02:00
return false;
}
2021-09-05 01:09:30 +02:00
}
// Once the code finishes (either resolved or rejected, doesnt matter), set its
// running status to false
2022-04-12 03:45:55 +02:00
scriptExecution
.then(function () {
workerScript.env.stopFlag = true;
// On natural death, the earnings are transfered to the parent if it still exists.
if (parent !== undefined && !parent.env.stopFlag) {
parent.scriptRef.onlineExpGained += runningScriptObj.onlineExpGained;
parent.scriptRef.onlineMoneyMade += runningScriptObj.onlineMoneyMade;
2022-04-12 03:45:55 +02:00
}
2022-04-12 03:45:55 +02:00
killWorkerScript(workerScript);
workerScript.log("", () => "Script finished running");
})
.catch(function (e) {
if (e instanceof Error) {
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
console.error("Evaluating workerscript returns an Error. THIS SHOULDN'T HAPPEN: " + e.toString());
return;
} else if (e instanceof ScriptDeath) {
2022-08-08 19:43:41 +02:00
if (helpers.isScriptErrorMessage(workerScript.errorMessage)) {
2022-04-12 03:45:55 +02:00
const errorTextArray = workerScript.errorMessage.split("|DELIMITER|");
if (errorTextArray.length != 4) {
console.error("ERROR: Something wrong with Error text in evaluator...");
console.error("Error text: " + workerScript.errorMessage);
return;
}
const hostname = errorTextArray[1];
const scriptName = errorTextArray[2];
const errorMsg = errorTextArray[3];
let msg = `RUNTIME ERROR<br>${scriptName}@${hostname} (PID - ${workerScript.pid})<br>`;
if (workerScript.args.length > 0) {
msg += `Args: ${arrayToString(workerScript.args)}<br>`;
}
msg += "<br>";
msg += errorMsg;
dialogBoxCreate(msg);
workerScript.log("", () => "Script crashed with runtime error");
} else {
workerScript.log("", () => "Script killed");
return; // Already killed, so stop here
}
2022-08-08 19:43:41 +02:00
} else if (helpers.isScriptErrorMessage(e)) {
2022-04-12 03:45:55 +02:00
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
console.error(
"ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " +
e.toString(),
);
return;
2021-09-05 01:09:30 +02:00
} else {
2022-04-12 03:45:55 +02:00
dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev");
console.error(e);
2021-09-05 01:09:30 +02:00
}
2022-04-12 03:45:55 +02:00
killWorkerScript(workerScript);
});
2021-09-05 01:09:30 +02:00
return true;
}
/**
* Updates the online running time stat of all running scripts
*/
2021-09-25 08:36:49 +02:00
export function updateOnlineScriptTimes(numCycles = 1): void {
2021-09-25 07:26:03 +02:00
const time = (numCycles * CONSTANTS._idleSpeed) / 1000; //seconds
2021-09-05 01:09:30 +02:00
for (const ws of workerScripts.values()) {
ws.scriptRef.onlineRunningTime += time;
}
}
/**
* Called when the game is loaded. Loads all running scripts (from all servers)
* into worker scripts so that they will start running
*/
2022-08-28 02:56:12 +02:00
export function loadAllRunningScripts(): void {
2021-09-25 07:26:03 +02:00
const skipScriptLoad = window.location.href.toLowerCase().indexOf("?noscripts") !== -1;
2021-09-05 01:09:30 +02:00
if (skipScriptLoad) {
2021-12-29 08:51:41 +01:00
Terminal.warn("Skipped loading player scripts during startup");
2021-09-05 01:09:30 +02:00
console.info("Skipping the load of any scripts during startup");
}
2021-10-07 22:04:04 +02:00
for (const server of GetAllServers()) {
// Reset each server's RAM usage to 0
server.ramUsed = 0;
2021-09-05 01:09:30 +02:00
2021-10-07 22:04:04 +02:00
// Reset modules on all scripts
for (let i = 0; i < server.scripts.length; ++i) {
server.scripts[i].markUpdated();
}
2021-09-05 01:09:30 +02:00
2021-10-07 22:04:04 +02:00
if (skipScriptLoad) {
// Start game with no scripts
server.runningScripts.length = 0;
} else {
for (let j = 0; j < server.runningScripts.length; ++j) {
const fileName = server.runningScripts[j].filename;
2022-08-28 02:56:12 +02:00
createAndAddWorkerScript(server.runningScripts[j], server);
2021-09-05 01:09:30 +02:00
if (!server.runningScripts[j]) {
// createAndAddWorkerScript can modify the server.runningScripts array if a script is invalid
console.error(`createAndAddWorkerScript removed ${fileName} from ${server}`);
continue;
}
2021-10-07 22:04:04 +02:00
// Offline production
scriptCalculateOfflineProduction(server.runningScripts[j]);
2021-09-05 01:09:30 +02:00
}
}
}
}
/**
* Run a script from inside another script (run(), exec(), spawn(), etc.)
*/
2021-09-24 23:56:30 +02:00
export function runScriptFromScript(
caller: string,
server: BaseServer,
scriptname: string,
2022-07-18 08:28:21 +02:00
args: ScriptArg[],
2021-09-24 23:56:30 +02:00
workerScript: WorkerScript,
threads = 1,
): number {
2021-09-05 01:09:30 +02:00
// Sanitize arguments
if (!(workerScript instanceof WorkerScript)) {
return 0;
}
if (typeof scriptname !== "string" || !Array.isArray(args)) {
workerScript.log(caller, () => `Invalid arguments: scriptname='${scriptname} args='${args}'`);
2021-09-05 01:09:30 +02:00
console.error(`runScriptFromScript() failed due to invalid arguments`);
return 0;
}
2021-12-12 19:30:28 +01:00
args.forEach((arg, i) => {
if (typeof arg !== "string" && typeof arg !== "number" && typeof arg !== "boolean")
2021-12-12 19:30:28 +01:00
throw new Error(
"Only strings, numbers, and booleans can be passed as arguments to other scripts.\n" +
`${scriptname} argument index ${i} is of type ${typeof arg} and value ${JSON.stringify(arg)}`,
);
2021-10-16 01:13:05 +02:00
});
2021-09-05 01:09:30 +02:00
// Check if the script is already running
2021-09-25 07:26:03 +02:00
const runningScriptObj = server.getRunningScript(scriptname, args);
2021-09-05 01:09:30 +02:00
if (runningScriptObj != null) {
workerScript.log(caller, () => `'${scriptname}' is already running on '${server.hostname}'`);
2021-09-05 01:09:30 +02:00
return 0;
}
// 'null/undefined' arguments are not allowed
for (let i = 0; i < args.length; ++i) {
if (args[i] == null) {
workerScript.log(caller, () => "Cannot execute a script with null/undefined as an argument");
2021-09-05 01:09:30 +02:00
return 0;
}
2021-09-05 01:09:30 +02:00
}
// Check if the script exists and if it does run it
for (let i = 0; i < server.scripts.length; ++i) {
2021-11-06 02:01:23 +01:00
if (!areFilesEqual(server.scripts[i].filename, scriptname)) continue;
// Check for admin rights and that there is enough RAM availble to run
const script = server.scripts[i];
let ramUsage = script.ramUsage;
2021-12-29 08:51:41 +01:00
threads = Math.floor(Number(threads));
2021-11-06 02:01:23 +01:00
if (threads === 0) {
return 0;
}
ramUsage = ramUsage * threads;
const ramAvailable = server.maxRam - server.ramUsed;
2021-11-06 02:01:23 +01:00
if (server.hasAdminRights == false) {
workerScript.log(caller, () => `You do not have root access on '${server.hostname}'`);
2021-11-06 02:01:23 +01:00
return 0;
} else if (ramUsage > ramAvailable + 0.001) {
2021-11-06 02:01:23 +01:00
workerScript.log(
caller,
() =>
`Cannot run script '${scriptname}' (t=${threads}) on '${server.hostname}' because there is not enough available RAM!`,
2021-11-06 02:01:23 +01:00
);
return 0;
}
2022-03-11 16:46:43 +01:00
// Able to run script
workerScript.log(
caller,
() => `'${scriptname}' on '${server.hostname}' with ${threads} threads and args: ${arrayToString(args)}.`,
);
const runningScriptObj = new RunningScript(script, args);
runningScriptObj.threads = threads;
runningScriptObj.server = server.hostname;
2022-08-28 02:56:12 +02:00
return startWorkerScript(runningScriptObj, server, workerScript);
2021-09-05 01:09:30 +02:00
}
workerScript.log(caller, () => `Could not find script '${scriptname}' on '${server.hostname}'`);
2021-09-05 01:09:30 +02:00
return 0;
}