diff --git a/src/Netscript/ScriptDeath.ts b/src/Netscript/ScriptDeath.ts new file mode 100644 index 000000000..18300d6ea --- /dev/null +++ b/src/Netscript/ScriptDeath.ts @@ -0,0 +1,37 @@ +import { WorkerScript } from "./WorkerScript"; + +/** + * Script death marker. + * + * IMPORTANT: the game engine should not base any of it's decisions on the data + * carried in a ScriptDeath instance. + * + * This is because ScriptDeath instances are thrown through player code when a + * script is killed. Which grants the player access to the class and the ability + * to construct new instances with arbitrary data. + */ +export class ScriptDeath { + /** Process ID number. */ + pid: number; + + /** Filename of the script. */ + name: string; + + /** IP Address on which the script was running */ + hostname: string; + + /** Status message in case of script error. */ + errorMessage = ""; + + constructor(ws: WorkerScript) { + this.pid = ws.pid; + this.name = ws.name; + this.hostname = ws.hostname; + this.errorMessage = ws.errorMessage; + + Object.freeze(this); + } +} + +Object.freeze(ScriptDeath); +Object.freeze(ScriptDeath.prototype); diff --git a/src/Netscript/WorkerScript.ts b/src/Netscript/WorkerScript.ts index 2498f36bc..59f8a38f0 100644 --- a/src/Netscript/WorkerScript.ts +++ b/src/Netscript/WorkerScript.ts @@ -60,7 +60,7 @@ export class WorkerScript { env: Environment; /** - * Status message in case of script error. Currently unused I think + * Status message in case of script error. */ errorMessage = ""; diff --git a/src/Netscript/killWorkerScript.ts b/src/Netscript/killWorkerScript.ts index 17a3fd42c..b802f3c7d 100644 --- a/src/Netscript/killWorkerScript.ts +++ b/src/Netscript/killWorkerScript.ts @@ -2,6 +2,7 @@ * Stops an actively-running script (represented by a WorkerScript object) * and removes it from the global pool of active scripts. */ +import { ScriptDeath } from "./ScriptDeath"; import { WorkerScript } from "./WorkerScript"; import { workerScripts } from "./WorkerScripts"; import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter"; @@ -139,7 +140,7 @@ function killNetscriptDelay(workerScript: WorkerScript): void { if (workerScript.delay) { clearTimeout(workerScript.delay); if (workerScript.delayReject) { - workerScript.delayReject(workerScript); + workerScript.delayReject(new ScriptDeath(workerScript)); } } } diff --git a/src/NetscriptEvaluator.ts b/src/NetscriptEvaluator.ts index 11f960b23..4ef8c6a22 100644 --- a/src/NetscriptEvaluator.ts +++ b/src/NetscriptEvaluator.ts @@ -1,15 +1,20 @@ import { isString } from "./utils/helpers/isString"; import { GetServer } from "./Server/AllServers"; +import { ScriptDeath } from "./Netscript/ScriptDeath"; import { WorkerScript } from "./Netscript/WorkerScript"; export function netscriptDelay(time: number, workerScript: WorkerScript): Promise { + // Cancel any pre-existing netscriptDelay'ed function call + // TODO: the rejection almost certainly ends up in the uncaught rejection handler. + // Maybe reject with a stack-trace'd error message? if (workerScript.delayReject) workerScript.delayReject(); + return new Promise(function (resolve, reject) { workerScript.delay = window.setTimeout(() => { workerScript.delay = null; workerScript.delayReject = undefined; - if (workerScript.env.stopFlag) reject(workerScript); + if (workerScript.env.stopFlag) reject(new ScriptDeath(workerScript)); else resolve(); }, time); workerScript.delayReject = reject; diff --git a/src/NetscriptFunctions/Corporation.ts b/src/NetscriptFunctions/Corporation.ts index db93a8139..ae2e61837 100644 --- a/src/NetscriptFunctions/Corporation.ts +++ b/src/NetscriptFunctions/Corporation.ts @@ -637,9 +637,6 @@ export function NetscriptCorporation( const office = getOffice(divisionName, cityName); if (!Object.values(EmployeePositions).includes(job)) throw new Error(`'${job}' is not a valid job.`); return netscriptDelay(1000, workerScript).then(function () { - if (workerScript.env.stopFlag) { - return Promise.reject(workerScript); - } return Promise.resolve(office.setEmployeeToJob(job, amount)); }); }, diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index e0c0512b2..fe8bfaef7 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -3,6 +3,7 @@ * 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"; @@ -59,7 +60,7 @@ export function prestigeWorkerScripts(): void { // 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. -function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Promise { +function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Promise { workerScript.running = true; // The name of the currently running netscript function, to prevent concurrent @@ -79,7 +80,7 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro // This is not a problem for legacy Netscript because it also checks the // stop flag in the evaluator. if (workerScript.env.stopFlag) { - throw workerScript; + throw new ScriptDeath(workerScript); } if (propName === "asleep") return f(...args); // OK for multiple simultaneous calls to sleep. @@ -90,7 +91,7 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro "promise-returning function? (Currently running: %s tried to run: %s)"; if (runningFn) { workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, sprintf(msg, runningFn, propName)); - throw workerScript; + throw new ScriptDeath(workerScript); } runningFn = propName; @@ -135,10 +136,10 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro // Note: the environment that we pass to the JS script only needs to contain the functions visible // to that script, which env.vars does at this point. - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { executeJSScript(player, workerScript.getServer().scripts, workerScript) .then(() => { - resolve(workerScript); + resolve(); }) .catch((e) => reject(e)); }).catch((e) => { @@ -151,20 +152,21 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro e.message + ((e.stack && "\nstack:\n" + e.stack.toString()) || ""), ); } - throw workerScript; + throw new ScriptDeath(workerScript); } else if (isScriptErrorMessage(e)) { workerScript.errorMessage = e; - throw workerScript; - } else if (e instanceof WorkerScript) { + throw new ScriptDeath(workerScript); + } else if (e instanceof ScriptDeath) { throw e; } - workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, e); - throw workerScript; // Don't know what to do with it, let's rethrow. + // Don't know what to do with it, let's try making an error message out of it + workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, "" + e); + throw new ScriptDeath(workerScript); }); } -function startNetscript1Script(workerScript: WorkerScript): Promise { +function startNetscript1Script(workerScript: WorkerScript): Promise { const code = workerScript.code; workerScript.running = true; @@ -179,7 +181,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise | null = null; // Script's resulting promise - if (s.name.endsWith(".js") || s.name.endsWith(".ns")) { - p = startNetscript2Script(player, s); + let scriptExecution: Promise | null = null; // Script's resulting promise + if (workerScript.name.endsWith(".js") || workerScript.name.endsWith(".ns")) { + scriptExecution = startNetscript2Script(player, workerScript); } else { - p = startNetscript1Script(s); - if (!(p instanceof Promise)) { + scriptExecution = startNetscript1Script(workerScript); + if (!(scriptExecution instanceof Promise)) { return false; } } // Once the code finishes (either resolved or rejected, doesnt matter), set its // running status to false - p.then(function (w: WorkerScript) { - w.running = false; - w.env.stopFlag = true; + scriptExecution.then(function () { + workerScript.running = false; + workerScript.env.stopFlag = true; // On natural death, the earnings are transfered to the parent if it still exists. if (parent !== undefined) { if (parent.running) { @@ -583,51 +586,51 @@ function createAndAddWorkerScript( } } - killWorkerScript(s); - w.log("", () => "Script finished running"); - }).catch(function (w) { - if (w instanceof Error) { + 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: " + w.toString()); + console.error("Evaluating workerscript returns an Error. THIS SHOULDN'T HAPPEN: " + e.toString()); return; - } else if (w instanceof WorkerScript) { - if (isScriptErrorMessage(w.errorMessage)) { - const errorTextArray = w.errorMessage.split("|DELIMITER|"); + } else if (e instanceof ScriptDeath) { + if (isScriptErrorMessage(workerScript.errorMessage)) { + const errorTextArray = workerScript.errorMessage.split("|DELIMITER|"); if (errorTextArray.length != 4) { console.error("ERROR: Something wrong with Error text in evaluator..."); - console.error("Error text: " + w.errorMessage); + console.error("Error text: " + workerScript.errorMessage); return; } const hostname = errorTextArray[1]; const scriptName = errorTextArray[2]; const errorMsg = errorTextArray[3]; - let msg = `RUNTIME ERROR
${scriptName}@${hostname}
`; - if (w.args.length > 0) { - msg += `Args: ${arrayToString(w.args)}
`; + let msg = `RUNTIME ERROR
${scriptName}@${hostname} (PID - ${workerScript.pid})
`; + if (workerScript.args.length > 0) { + msg += `Args: ${arrayToString(workerScript.args)}
`; } msg += "
"; msg += errorMsg; dialogBoxCreate(msg); - w.log("", () => "Script crashed with runtime error"); + workerScript.log("", () => "Script crashed with runtime error"); } else { - w.log("", () => "Script killed"); + workerScript.log("", () => "Script killed"); return; // Already killed, so stop here } - } else if (isScriptErrorMessage(w)) { + } else if (isScriptErrorMessage(e)) { 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: " + - w.toString(), + e.toString(), ); return; } else { dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev"); - console.error(w); + console.error(e); } - killWorkerScript(s); + killWorkerScript(workerScript); }); return true; diff --git a/src/UncaughtPromiseHandler.ts b/src/UncaughtPromiseHandler.ts index e0b15c645..775f92fca 100644 --- a/src/UncaughtPromiseHandler.ts +++ b/src/UncaughtPromiseHandler.ts @@ -1,4 +1,4 @@ -import { WorkerScript } from "./Netscript/WorkerScript"; +import { ScriptDeath } from "./Netscript/ScriptDeath"; import { isScriptErrorMessage } from "./NetscriptEvaluator"; import { dialogBoxCreate } from "./ui/React/DialogBox"; @@ -14,9 +14,9 @@ export function setupUncaughtPromiseHandler(): void { msg += "
"; msg += errorMsg; dialogBoxCreate(msg); - } else if (e.reason instanceof WorkerScript) { + } else if (e.reason instanceof ScriptDeath) { const msg = - `UNCAUGHT PROMISE ERROR
You forgot to await a promise
${e.reason.name}@${e.reason.hostname}
` + + `UNCAUGHT PROMISE ERROR
You forgot to await a promise
${e.reason.name}@${e.reason.hostname} (PID - ${e.reason.pid})
` + `Maybe hack / grow / weaken ?`; dialogBoxCreate(msg); }