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.
This commit is contained in:
Heikki Aitakangas 2022-02-06 02:15:42 +02:00
parent 1ed1f78222
commit a578763b89
6 changed files with 92 additions and 51 deletions

@ -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);

@ -2,6 +2,7 @@
* Stops an actively-running script (represented by a WorkerScript object) * Stops an actively-running script (represented by a WorkerScript object)
* and removes it from the global pool of active scripts. * and removes it from the global pool of active scripts.
*/ */
import { ScriptDeath } from "./ScriptDeath";
import { WorkerScript } from "./WorkerScript"; import { WorkerScript } from "./WorkerScript";
import { workerScripts } from "./WorkerScripts"; import { workerScripts } from "./WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter"; import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter";
@ -139,7 +140,7 @@ function killNetscriptDelay(workerScript: WorkerScript): void {
if (workerScript.delay) { if (workerScript.delay) {
clearTimeout(workerScript.delay); clearTimeout(workerScript.delay);
if (workerScript.delayReject) { if (workerScript.delayReject) {
workerScript.delayReject(workerScript); workerScript.delayReject(new ScriptDeath(workerScript));
} }
} }
} }

@ -1,15 +1,18 @@
import { isString } from "./utils/helpers/isString"; import { isString } from "./utils/helpers/isString";
import { GetServer } from "./Server/AllServers"; import { GetServer } from "./Server/AllServers";
import { ScriptDeath } from "./Netscript/ScriptDeath";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
export function netscriptDelay(time: number, workerScript: WorkerScript): Promise<void> { export function netscriptDelay(time: number, workerScript: WorkerScript): Promise<void> {
if (workerScript.delayReject) workerScript.delayReject(); if (workerScript.delayReject)
workerScript.delayReject(new ScriptDeath(workerScript));
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
workerScript.delay = window.setTimeout(() => { workerScript.delay = window.setTimeout(() => {
workerScript.delay = null; workerScript.delay = null;
workerScript.delayReject = undefined; workerScript.delayReject = undefined;
if (workerScript.env.stopFlag) reject(workerScript); if (workerScript.env.stopFlag) reject(new ScriptDeath(workerScript));
else resolve(); else resolve();
}, time); }, time);
workerScript.delayReject = reject; workerScript.delayReject = reject;

@ -637,9 +637,6 @@ export function NetscriptCorporation(
const office = getOffice(divisionName, cityName); const office = getOffice(divisionName, cityName);
if (!Object.values(EmployeePositions).includes(job)) throw new Error(`'${job}' is not a valid job.`); if (!Object.values(EmployeePositions).includes(job)) throw new Error(`'${job}' is not a valid job.`);
return netscriptDelay(1000, workerScript).then(function () { return netscriptDelay(1000, workerScript).then(function () {
if (workerScript.env.stopFlag) {
return Promise.reject(workerScript);
}
return Promise.resolve(office.setEmployeeToJob(job, amount)); return Promise.resolve(office.setEmployeeToJob(job, amount));
}); });
}, },

@ -3,6 +3,7 @@
* that allows for scripts to run * that allows for scripts to run
*/ */
import { killWorkerScript } from "./Netscript/killWorkerScript"; import { killWorkerScript } from "./Netscript/killWorkerScript";
import { ScriptDeath } from "./Netscript/ScriptDeath";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts"; import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter"; 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 // This is not a problem for legacy Netscript because it also checks the
// stop flag in the evaluator. // stop flag in the evaluator.
if (workerScript.env.stopFlag) { if (workerScript.env.stopFlag) {
throw workerScript; throw new ScriptDeath(workerScript);
} }
if (propName === "asleep") return f(...args); // OK for multiple simultaneous calls to sleep. 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)"; "promise-returning function? (Currently running: %s tried to run: %s)";
if (runningFn) { if (runningFn) {
workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, sprintf(msg, runningFn, propName)); workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, sprintf(msg, runningFn, propName));
throw workerScript; throw new ScriptDeath(workerScript);
} }
runningFn = propName; runningFn = propName;
@ -140,16 +141,16 @@ function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Pro
e.message + ((e.stack && "\nstack:\n" + e.stack.toString()) || ""), e.message + ((e.stack && "\nstack:\n" + e.stack.toString()) || ""),
); );
} }
throw workerScript; throw new ScriptDeath(workerScript);
} else if (isScriptErrorMessage(e)) { } else if (isScriptErrorMessage(e)) {
workerScript.errorMessage = e; workerScript.errorMessage = e;
throw workerScript; throw new ScriptDeath(workerScript);
} else if (e instanceof WorkerScript) { } else if (e instanceof ScriptDeath) {
throw e; throw e;
} }
workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, 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<WorkerScript
}) })
.catch(function (err: any) { .catch(function (err: any) {
// workerscript is when you cancel a delay // workerscript is when you cancel a delay
if (!(err instanceof WorkerScript)) { if (!(err instanceof ScriptDeath)) {
console.error(err); console.error(err);
const errorTextArray = err.split("|DELIMITER|"); const errorTextArray = err.split("|DELIMITER|");
const hostname = errorTextArray[1]; const hostname = errorTextArray[1];
@ -212,7 +213,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
workerScript.env.stopFlag = true; workerScript.env.stopFlag = true;
workerScript.running = false; workerScript.running = false;
killWorkerScript(workerScript); killWorkerScript(workerScript);
return Promise.resolve(workerScript); return Promise.resolve();
} }
}); });
}; };
@ -282,7 +283,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
function runInterpreter(): void { function runInterpreter(): void {
try { try {
if (workerScript.env.stopFlag) { if (workerScript.env.stopFlag) {
return reject(workerScript); return reject(new ScriptDeath(workerScript));
} }
let more = true; let more = true;
@ -303,7 +304,7 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
e = makeRuntimeRejectMsg(workerScript, e); e = makeRuntimeRejectMsg(workerScript, e);
} }
workerScript.errorMessage = e; workerScript.errorMessage = e;
return reject(workerScript); return reject(new ScriptDeath(workerScript));
} }
} }
@ -312,11 +313,12 @@ function startNetscript1Script(workerScript: WorkerScript): Promise<WorkerScript
} catch (e: any) { } catch (e: any) {
if (isString(e)) { if (isString(e)) {
workerScript.errorMessage = e; workerScript.errorMessage = e;
return reject(workerScript); return reject(new ScriptDeath(workerScript));
} else if (e instanceof WorkerScript) { } else if (e instanceof ScriptDeath) {
return reject(e); return reject(e);
} else { } else {
return reject(workerScript); console.error(e);
return reject(new ScriptDeath(workerScript));
} }
} }
}); });
@ -539,29 +541,30 @@ function createAndAddWorkerScript(
// Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying // Create the WorkerScript. NOTE: WorkerScript ctor will set the underlying
// RunningScript's PID as well // RunningScript's PID as well
const s = new WorkerScript(runningScriptObj, pid, NetscriptFunctions); const workerScript = new WorkerScript(runningScriptObj, pid, NetscriptFunctions);
s.ramUsage = oneRamUsage; workerScript.ramUsage = oneRamUsage;
// Add the WorkerScript to the global pool // Add the WorkerScript to the global pool
workerScripts.set(pid, s); workerScripts.set(pid, workerScript);
WorkerScriptStartStopEventEmitter.emit(); WorkerScriptStartStopEventEmitter.emit();
// Start the script's execution // Start the script's execution
let p: Promise<WorkerScript> | null = null; // Script's resulting promise let scriptExecution: Promise<WorkerScript> | null = null; // Script's resulting promise
if (s.name.endsWith(".js") || s.name.endsWith(".ns")) { if (workerScript.name.endsWith(".js") || workerScript.name.endsWith(".ns")) {
p = startNetscript2Script(player, s); scriptExecution = startNetscript2Script(player, workerScript);
} else { } else {
p = startNetscript1Script(s); scriptExecution = startNetscript1Script(workerScript);
if (!(p instanceof Promise)) { if (!(scriptExecution instanceof Promise)) {
return false; return false;
} }
} }
// Once the code finishes (either resolved or rejected, doesnt matter), set its // Once the code finishes (either resolved or rejected, doesnt matter), set its
// running status to false // running status to false
p.then(function (w: WorkerScript) { scriptExecution.then(function (w: WorkerScript) {
w.running = false; if(w !== workerScript) console.error("!BUG! Wrong WorkerScript instance !BUG!")
w.env.stopFlag = true; workerScript.running = false;
workerScript.env.stopFlag = true;
// On natural death, the earnings are transfered to the parent if it still exists. // On natural death, the earnings are transfered to the parent if it still exists.
if (parent !== undefined) { if (parent !== undefined) {
if (parent.running) { if (parent.running) {
@ -570,51 +573,51 @@ function createAndAddWorkerScript(
} }
} }
killWorkerScript(s); killWorkerScript(workerScript);
w.log("", () => "Script finished running"); workerScript.log("", () => "Script finished running");
}).catch(function (w) { }).catch(function (e) {
if (w instanceof Error) { if (e instanceof Error) {
dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer"); 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; return;
} else if (w instanceof WorkerScript) { } else if (e instanceof ScriptDeath) {
if (isScriptErrorMessage(w.errorMessage)) { if (isScriptErrorMessage(workerScript.errorMessage)) {
const errorTextArray = w.errorMessage.split("|DELIMITER|"); const errorTextArray = workerScript.errorMessage.split("|DELIMITER|");
if (errorTextArray.length != 4) { if (errorTextArray.length != 4) {
console.error("ERROR: Something wrong with Error text in evaluator..."); console.error("ERROR: Something wrong with Error text in evaluator...");
console.error("Error text: " + w.errorMessage); console.error("Error text: " + workerScript.errorMessage);
return; return;
} }
const hostname = errorTextArray[1]; const hostname = errorTextArray[1];
const scriptName = errorTextArray[2]; const scriptName = errorTextArray[2];
const errorMsg = errorTextArray[3]; const errorMsg = errorTextArray[3];
let msg = `RUNTIME ERROR<br>${scriptName}@${hostname}<br>`; let msg = `RUNTIME ERROR<br>${scriptName}@${hostname} (PID - ${workerScript.pid})<br>`;
if (w.args.length > 0) { if (workerScript.args.length > 0) {
msg += `Args: ${arrayToString(w.args)}<br>`; msg += `Args: ${arrayToString(workerScript.args)}<br>`;
} }
msg += "<br>"; msg += "<br>";
msg += errorMsg; msg += errorMsg;
dialogBoxCreate(msg); dialogBoxCreate(msg);
w.log("", () => "Script crashed with runtime error"); workerScript.log("", () => "Script crashed with runtime error");
} else { } else {
w.log("", () => "Script killed"); workerScript.log("", () => "Script killed");
return; // Already killed, so stop here 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"); dialogBoxCreate("Script runtime unknown error. This is a bug please contact game developer");
console.error( console.error(
"ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " + "ERROR: Evaluating workerscript returns only error message rather than WorkerScript object. THIS SHOULDN'T HAPPEN: " +
w.toString(), e.toString(),
); );
return; return;
} else { } else {
dialogBoxCreate("An unknown script died for an unknown reason. This is a bug please contact game dev"); 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; return true;

@ -1,4 +1,4 @@
import { WorkerScript } from "./Netscript/WorkerScript"; import { ScriptDeath } from "./Netscript/ScriptDeath";
import { isScriptErrorMessage } from "./NetscriptEvaluator"; import { isScriptErrorMessage } from "./NetscriptEvaluator";
import { dialogBoxCreate } from "./ui/React/DialogBox"; import { dialogBoxCreate } from "./ui/React/DialogBox";
@ -14,9 +14,9 @@ export function setupUncaughtPromiseHandler(): void {
msg += "<br>"; msg += "<br>";
msg += errorMsg; msg += errorMsg;
dialogBoxCreate(msg); dialogBoxCreate(msg);
} else if (e.reason instanceof WorkerScript) { } else if (e.reason instanceof ScriptDeath) {
const msg = const msg =
`UNCAUGHT PROMISE ERROR<br>You forgot to await a promise<br>${e.reason.name}@${e.reason.hostname}<br>` + `UNCAUGHT PROMISE ERROR<br>You forgot to await a promise<br>${e.reason.name}@${e.reason.hostname} (PID - ${e.reason.pid})<br>` +
`Maybe hack / grow / weaken ?`; `Maybe hack / grow / weaken ?`;
dialogBoxCreate(msg); dialogBoxCreate(msg);
} }