From 1ed1f7822292378d4a1dcdee398826d16f20a8ca Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sun, 6 Feb 2022 01:14:53 +0200 Subject: [PATCH 1/6] Fixed a wrong comment --- src/Netscript/WorkerScript.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = ""; From a578763b89c42891e6911a608c89ff255b8377eb Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sun, 6 Feb 2022 02:15:42 +0200 Subject: [PATCH 2/6] Use a dedicated ScriptDeath type to signal script termination instead of WorkerScript Problem with throwing WorkerScript is that the termination signal object can pass through user code, which permits user to modify that object and all parts of the game state accessible through it. --- src/Netscript/ScriptDeath.ts | 37 ++++++++++++ src/Netscript/killWorkerScript.ts | 3 +- src/NetscriptEvaluator.ts | 7 ++- src/NetscriptFunctions/Corporation.ts | 3 - src/NetscriptWorker.ts | 87 ++++++++++++++------------- src/UncaughtPromiseHandler.ts | 6 +- 6 files changed, 92 insertions(+), 51 deletions(-) create mode 100644 src/Netscript/ScriptDeath.ts diff --git a/src/Netscript/ScriptDeath.ts b/src/Netscript/ScriptDeath.ts new file mode 100644 index 000000000..eb31b6a65 --- /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/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..55501eb67 100644 --- a/src/NetscriptEvaluator.ts +++ b/src/NetscriptEvaluator.ts @@ -1,15 +1,18 @@ 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 { - if (workerScript.delayReject) workerScript.delayReject(); + if (workerScript.delayReject) + workerScript.delayReject(new ScriptDeath(workerScript)); + 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 95b82c99f..be94e3ff4 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 088b55b4d..fa55e140b 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"; @@ -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; @@ -140,16 +141,16 @@ 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. + throw new ScriptDeath(workerScript); // Don't know what to do with it, let's rethrow. }); } @@ -199,7 +200,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 (w: WorkerScript) { + if(w !== workerScript) console.error("!BUG! Wrong WorkerScript instance !BUG!") + 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) { @@ -570,51 +573,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); } From 4fd55f099be71d5b08631b2f79b462f20476403e Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sun, 13 Feb 2022 21:53:23 +0200 Subject: [PATCH 3/6] This should not reject with ScriptDeath, the script is not being killed. Just 'undefined' isn't great either so left a TODO about improving it --- src/NetscriptEvaluator.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/NetscriptEvaluator.ts b/src/NetscriptEvaluator.ts index 55501eb67..4ef8c6a22 100644 --- a/src/NetscriptEvaluator.ts +++ b/src/NetscriptEvaluator.ts @@ -4,8 +4,10 @@ import { ScriptDeath } from "./Netscript/ScriptDeath"; import { WorkerScript } from "./Netscript/WorkerScript"; export function netscriptDelay(time: number, workerScript: WorkerScript): Promise { - if (workerScript.delayReject) - workerScript.delayReject(new ScriptDeath(workerScript)); + // 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(() => { From cbd59975d44cbb77b64b0aaea4c0841876bf8f60 Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sun, 13 Feb 2022 21:54:23 +0200 Subject: [PATCH 4/6] makeRuntimeRejectMsg expects the 'msg' to be string-like --- src/NetscriptWorker.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index fa55e140b..c28fbb39c 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -149,8 +149,9 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro throw e; } - workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, e); - throw new ScriptDeath(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); }); } From a6710eb3e5895b57320631cd8b3b8c35f40f12c6 Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Fri, 11 Mar 2022 00:46:12 +0200 Subject: [PATCH 5/6] No need to return WorkerScript from startNetscript*Script promises, their only calling function already has the correct object in a variable --- src/NetscriptWorker.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index c28fbb39c..f5463fd66 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -60,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 @@ -125,10 +125,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) => { @@ -155,7 +155,7 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro }); } -function startNetscript1Script(workerScript: WorkerScript): Promise { +function startNetscript1Script(workerScript: WorkerScript): Promise { const code = workerScript.code; workerScript.running = true; @@ -170,7 +170,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise | null = null; // Script's resulting promise + let scriptExecution: Promise | null = null; // Script's resulting promise if (workerScript.name.endsWith(".js") || workerScript.name.endsWith(".ns")) { scriptExecution = startNetscript2Script(player, workerScript); } else { @@ -562,8 +562,7 @@ function createAndAddWorkerScript( // Once the code finishes (either resolved or rejected, doesnt matter), set its // running status to false - scriptExecution.then(function (w: WorkerScript) { - if(w !== workerScript) console.error("!BUG! Wrong WorkerScript instance !BUG!") + scriptExecution.then(function () { workerScript.running = false; workerScript.env.stopFlag = true; // On natural death, the earnings are transfered to the parent if it still exists. From a7d200f7c6b8b065b6dd0b6b477b2bbc06cda568 Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sat, 19 Mar 2022 19:55:25 +0200 Subject: [PATCH 6/6] Strip trailing spaces --- src/Netscript/ScriptDeath.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Netscript/ScriptDeath.ts b/src/Netscript/ScriptDeath.ts index eb31b6a65..18300d6ea 100644 --- a/src/Netscript/ScriptDeath.ts +++ b/src/Netscript/ScriptDeath.ts @@ -10,16 +10,16 @@ import { WorkerScript } from "./WorkerScript"; * script is killed. Which grants the player access to the class and the ability * to construct new instances with arbitrary data. */ -export class ScriptDeath { +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 = "";