bitburner-src/src/NetscriptWorker.ts

487 lines
18 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-08-29 08:41:17 +02:00
import { compile, 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";
2022-09-05 15:55:57 +02:00
import { dialogBoxCreate } from "./ui/React/DialogBox";
2021-09-25 20:42:57 +02:00
import { arrayToString } from "./utils/helpers/arrayToString";
import { roundToTwo } from "./utils/helpers/roundToTwo";
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";
import { Terminal } from "./Terminal";
2022-07-18 08:28:21 +02:00
import { ScriptArg } from "./Netscript/ScriptArg";
2022-09-05 15:55:57 +02:00
import { handleUnknownError } 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();
}
2022-08-29 08:41:17 +02:00
async function startNetscript2Script(workerScript: WorkerScript): Promise<void> {
const scripts = workerScript.getServer().scripts;
const script = workerScript.getScript();
if (script === null) throw "workerScript had no associated script. This is a bug.";
const loadedModule = await compile(script, scripts);
workerScript.ramUsage = script.ramUsage;
const ns = workerScript.env.vars;
if (!loadedModule) throw `${script.filename} cannot be run because the script module won't load`;
// TODO: Better error for "unexpected reserved word" when using await in non-async function?
if (typeof loadedModule.main !== "function")
throw `${script.filename} cannot be run because it does not have a main function.`;
if (!ns) throw `${script.filename} cannot be run because the NS object hasn't been constructed properly.`;
await loadedModule.main(ns);
}
2021-09-05 01:09:30 +02:00
2022-08-29 08:41:17 +02:00
async function startNetscript1Script(workerScript: WorkerScript): Promise<void> {
2021-09-05 01:09:30 +02:00
const code = workerScript.code;
2022-08-29 08:41:17 +02:00
let errorToThrow: unknown;
2021-09-05 01:09:30 +02:00
//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) {
2022-08-29 08:41:17 +02:00
throw `Error processing Imports in ${workerScript.name}@${workerScript.hostname}:\n\n${e}`;
2021-09-05 01:09:30 +02:00
}
2022-08-28 11:33:38 +02:00
interface BasicObject {
[key: string]: any;
}
function wrapNS1Layer(int: Interpreter, intLayer: unknown, nsLayer = workerScript.env.vars as BasicObject) {
2022-08-28 02:56:12 +02:00
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
const wrapper = async (...args: unknown[]) => {
try {
2022-08-29 08:41:17 +02:00
// Sent a resolver function as an extra arg. See createAsyncFunction JSInterpreter.js:3209
2022-08-28 02:56:12 +02:00
const callback = args.pop() as (value: unknown) => void;
2022-09-27 06:03:16 +02:00
const result = await entry(...args.map((arg) => int.pseudoToNative(arg)));
2022-08-28 02:56:12 +02:00
return callback(int.nativeToPseudo(result));
} catch (e: unknown) {
2022-08-29 08:41:17 +02:00
errorToThrow = e;
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({}));
2022-08-28 11:33:38 +02:00
wrapNS1Layer(int, (intLayer as BasicObject).properties[name], nsLayer[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) {
2022-08-29 08:41:17 +02:00
throw `Syntax ERROR in ${workerScript.name}@${workerScript.hostname}:\n\n${String(e)}`;
2021-09-05 01:09:30 +02:00
}
2022-08-29 08:41:17 +02:00
let more = true;
while (more) {
if (errorToThrow) throw errorToThrow;
if (workerScript.env.stopFlag) return;
for (let i = 0; more && i < 3; i++) more = interpreter.step();
if (more) await new Promise((r) => setTimeout(r, Settings.CodeInstructionRunTime));
}
}
/* Since the JS Interpreter used for Netscript 1.0 only supports ES5, the keyword
2022-10-09 07:25:31 +02:00
'import' throws an error. However, since we want to support import functionality
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
2022-08-29 08:41:17 +02:00
server.updateRamUsed(roundToTwo(server.ramUsed + ramUsage));
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
2022-08-29 08:41:17 +02:00
// Start the script's execution using the correct function for file type
(workerScript.name.endsWith(".js") ? startNetscript2Script : startNetscript1Script)(workerScript)
// Once the code finishes (either resolved or rejected, doesnt matter), set its
// running status to false
2022-04-12 03:45:55 +02:00
.then(function () {
2022-10-09 07:25:31 +02:00
// On natural death, the earnings are transferred to the parent if it still exists.
2022-08-28 11:33:38 +02:00
if (parent && !parent.env.stopFlag) {
parent.scriptRef.onlineExpGained += runningScriptObj.onlineExpGained;
parent.scriptRef.onlineMoneyMade += runningScriptObj.onlineMoneyMade;
2022-04-12 03:45:55 +02:00
}
killWorkerScript(workerScript);
workerScript.log("", () => "Script finished running");
})
.catch(function (e) {
2022-09-05 15:55:57 +02:00
handleUnknownError(e, workerScript);
2022-08-29 08:41:17 +02:00
workerScript.log("", () => (e instanceof ScriptDeath ? "Script killed." : "Script crashed due to an error."));
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;
2022-10-09 07:25:31 +02:00
// Check for admin rights and that there is enough RAM available to run
2021-11-06 02:01:23 +01:00
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;
}