From 3a2e676c9b889feef8b5a1e1ebd346331766ee69 Mon Sep 17 00:00:00 2001 From: Snarling <84951833+Snarling@users.noreply.github.com> Date: Sat, 6 Aug 2022 17:53:09 -0400 Subject: [PATCH] Move error functionality to new wrapper --- src/Netscript/APIWrapper.ts | 46 ++++++++++------ src/Netscript/Environment.ts | 6 +++ src/NetscriptEvaluator.ts | 7 +-- src/NetscriptFunctions.ts | 10 ++-- src/NetscriptFunctions/Singularity.ts | 2 +- src/NetscriptFunctions/Stanek.ts | 2 +- src/NetscriptWorker.ts | 78 --------------------------- 7 files changed, 46 insertions(+), 105 deletions(-) diff --git a/src/Netscript/APIWrapper.ts b/src/Netscript/APIWrapper.ts index 6844f45f5..3cc5022d1 100644 --- a/src/Netscript/APIWrapper.ts +++ b/src/Netscript/APIWrapper.ts @@ -13,6 +13,8 @@ import { INetscriptHelper, ScriptIdentifier } from "../NetscriptFunctions/INetsc import { GangMember } from "../Gang/GangMember"; import { GangMemberTask } from "../Gang/GangMemberTask"; import { ScriptArg } from "./ScriptArg"; +import { ScriptDeath } from "./ScriptDeath"; +import { sprintf } from "sprintf-js"; type ExternalFunction = (...args: unknown[]) => unknown; export type ExternalAPI = { @@ -63,6 +65,22 @@ type WrappedNetscriptHelpers = { gangTask: (t: unknown) => GangMemberTask; }; +function checkEnv(ctx: NetscriptContext) { + if (ctx.workerScript.env.stopFlag) throw new ScriptDeath(ctx.workerScript); + if (ctx.workerScript.env.runningFn && ctx.function !== "asleep") { + if (ctx.workerScript.delayReject) ctx.workerScript.delayReject(); + const msg = + "Concurrent calls to Netscript functions are not allowed!\n" + + "Did you forget to await hack(), grow(), or some other\n" + + "promise-returning function? (Currently running: %s tried to run: %s)"; + ctx.workerScript.errorMessage = makeRuntimeRejectMsg( + ctx.workerScript, + sprintf(msg, ctx.workerScript.env.runningFn, ctx.function), + ); + throw new ScriptDeath(ctx.workerScript); + } +} + function wrapFunction( helpers: INetscriptHelper, wrappedAPI: ExternalAPI, @@ -107,10 +125,11 @@ function wrapFunction( }, }; function wrappedFunction(...args: unknown[]): unknown { + checkEnv(ctx); helpers.updateDynamicRam(ctx.function, getRamCost(Player, ...tree, ctx.function)); return func(ctx)(...args); } - const parent = getNestedProperty(wrappedAPI, ...tree); + const parent = getNestedProperty(wrappedAPI, tree); Object.defineProperty(parent, functionName, { value: wrappedFunction, writable: true, @@ -129,7 +148,7 @@ export function wrapAPI( if (typeof value === "function") { wrapFunction(helpers, wrappedAPI, workerScript, value, ...tree, key); } else if (Array.isArray(value)) { - setNestedProperty(wrappedAPI, value, key); + setNestedProperty(wrappedAPI, value.slice(), key); } else if (typeof value === "object") { wrapAPI(helpers, wrappedAPI, workerScript, value, ...tree, key); } else { @@ -139,28 +158,21 @@ export function wrapAPI( return wrappedAPI; } -// TODO: This doesn't even work properly. -function setNestedProperty(root: object, value: unknown, ...tree: string[]): void { +function setNestedProperty(root: any, value: unknown, ...tree: string[]): void { let target = root; const key = tree.pop(); - if (!key) { - throw new Error("Failure occured while wrapping netscript api (setNestedProperty)"); + if (!key) throw new Error("Failure occured while wrapping netscript api (setNestedProperty)"); + for (const branch of tree) { + target[branch] ??= {}; + target = target[branch]; } - for (let branch of Object.values(target)) { - if (branch === undefined) { - branch = {}; - } - target = branch; - } - Object.assign(target, { [key]: value }); + target[key] = value; } -function getNestedProperty(root: any, ...tree: string[]): unknown { +function getNestedProperty(root: any, tree: string[]): unknown { let target = root; for (const branch of tree) { - if (target[branch] === undefined) { - target[branch] = {}; - } + target[branch] ??= {}; target = target[branch]; } return target; diff --git a/src/Netscript/Environment.ts b/src/Netscript/Environment.ts index 919d593ab..6985ec043 100644 --- a/src/Netscript/Environment.ts +++ b/src/Netscript/Environment.ts @@ -10,6 +10,12 @@ export class Environment { */ stopFlag = false; + /** + * The currently running function + */ + + runningFn = ""; + /** * Environment variables (currently only Netscript functions) */ diff --git a/src/NetscriptEvaluator.ts b/src/NetscriptEvaluator.ts index bde595160..2f5fbc47a 100644 --- a/src/NetscriptEvaluator.ts +++ b/src/NetscriptEvaluator.ts @@ -4,20 +4,21 @@ import { ScriptDeath } from "./Netscript/ScriptDeath"; import { WorkerScript } from "./Netscript/WorkerScript"; import { NetscriptContext } from "./Netscript/APIWrapper"; -export function netscriptDelay(time: number, workerScript: WorkerScript): Promise { +export function netscriptDelay(fnName: string, 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? + // TODO: This can probably be removed. Concurrency check should error out before this in APIWrapper.ts if (workerScript.delayReject) workerScript.delayReject(); return new Promise(function (resolve, reject) { workerScript.delay = window.setTimeout(() => { workerScript.delay = null; workerScript.delayReject = undefined; + workerScript.env.runningFn = ""; if (workerScript.env.stopFlag) reject(new ScriptDeath(workerScript)); else resolve(); }, time); workerScript.delayReject = reject; + workerScript.env.runningFn = fnName; }); } diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index eefec8927..9ef96e498 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -394,7 +394,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS { )} (t=${numeralWrapper.formatThreads(threads)})`, ); - return netscriptDelay(hackingTime * 1000, workerScript).then(function () { + return netscriptDelay("hack", hackingTime * 1000, workerScript).then(function () { const hackChance = calculateHackingChance(server, Player); const rand = Math.random(); let expGainedOnSuccess = calculateHackingExpGain(server, Player) * threads; @@ -757,7 +757,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS { throw ctx.makeRuntimeErrorMsg("Takes 1 argument."); } ctx.log(() => `Sleeping for ${time} milliseconds`); - return netscriptDelay(time, workerScript).then(function () { + return netscriptDelay("sleep", time, workerScript).then(function () { return Promise.resolve(true); }); }, @@ -798,7 +798,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS { true, )} (t=${numeralWrapper.formatThreads(threads)}).`, ); - return netscriptDelay(growTime * 1000, workerScript).then(function () { + return netscriptDelay("grow", growTime * 1000, workerScript).then(function () { const moneyBefore = server.moneyAvailable <= 0 ? 1 : server.moneyAvailable; processSingleServerGrowth(server, threads, Player, host.cpuCores); const moneyAfter = server.moneyAvailable; @@ -892,7 +892,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS { true, )} (t=${numeralWrapper.formatThreads(threads)})`, ); - return netscriptDelay(weakenTime * 1000, workerScript).then(function () { + return netscriptDelay("weaken", weakenTime * 1000, workerScript).then(function () { const host = GetServer(workerScript.hostname); if (host === null) { ctx.log(() => "Server is null, did it die?"); @@ -926,7 +926,7 @@ export function NetscriptFunctions(workerScript: WorkerScript): NS { const end = StartSharing( workerScript.scriptRef.threads * calculateIntelligenceBonus(Player.skills.intelligence, 2), ); - return netscriptDelay(10000, workerScript).finally(function () { + return netscriptDelay("share", 10000, workerScript).finally(function () { ctx.log(() => "Finished sharing this computer."); end(); }); diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts index ff6402bae..884ec08b9 100644 --- a/src/NetscriptFunctions/Singularity.ts +++ b/src/NetscriptFunctions/Singularity.ts @@ -637,7 +637,7 @@ export function NetscriptSingularity(player: IPlayer, workerScript: WorkerScript () => `Installing backdoor on '${server.hostname}' in ${convertTimeMsToTimeElapsedString(installTime, true)}`, ); - return netscriptDelay(installTime, workerScript).then(function () { + return netscriptDelay("singularity.installBackdoor", installTime, workerScript).then(function () { _ctx.log(() => `Successfully installed backdoor on '${server.hostname}'`); server.backdoorInstalled = true; diff --git a/src/NetscriptFunctions/Stanek.ts b/src/NetscriptFunctions/Stanek.ts index e4e8f1008..dc4ee48bc 100644 --- a/src/NetscriptFunctions/Stanek.ts +++ b/src/NetscriptFunctions/Stanek.ts @@ -57,7 +57,7 @@ export function NetscriptStanek( } //Charge the fragment const time = staneksGift.inBonus() ? 200 : 1000; - return netscriptDelay(time, workerScript).then(function () { + return netscriptDelay("stanek.charge", time, workerScript).then(function () { staneksGift.charge(player, fragment, workerScript.scriptRef.threads); _ctx.log(() => `Charged fragment with ${_ctx.workerScript.scriptRef.threads} threads.`); return Promise.resolve(); diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index 5c8f8ad93..5a4223583 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -29,7 +29,6 @@ import { dialogBoxCreate } from "./ui/React/DialogBox"; import { arrayToString } from "./utils/helpers/arrayToString"; import { roundToTwo } from "./utils/helpers/roundToTwo"; import { isString } from "./utils/helpers/isString"; -import { sprintf } from "sprintf-js"; import { parse } from "acorn"; import { simple as walksimple } from "acorn-walk"; @@ -63,83 +62,6 @@ export function prestigeWorkerScripts(): void { // that resolves or rejects when the corresponding worker script is done. function startNetscript2Script(player: IPlayer, workerScript: WorkerScript): Promise { workerScript.running = true; - - // The name of the currently running netscript function, to prevent concurrent - // calls to hack, grow, etc. - let runningFn: string | null = null; - - // We need to go through the environment and wrap each function in such a way that it - // can be called at most once at a time. This will prevent situations where multiple - // hack promises are outstanding, for example. - function wrap(propName: string, f: (...args: unknown[]) => Promise): (...args: unknown[]) => Promise { - // This function unfortunately cannot be an async function, because we don't - // know if the original one was, and there's no way to tell. - return function (...args: unknown[]) { - // Wrap every netscript function with a check for the stop flag. - // This prevents cases where we never stop because we are only calling - // netscript functions that don't check this. - // This is not a problem for legacy Netscript because it also checks the - // stop flag in the evaluator. - if (workerScript.env.stopFlag) { - throw new ScriptDeath(workerScript); - } - - if (propName === "asleep") return f(...args); // OK for multiple simultaneous calls to sleep. - - const msg = - "Concurrent calls to Netscript functions are not allowed! " + - "Did you forget to await hack(), grow(), or some other " + - "promise-returning function? (Currently running: %s tried to run: %s)"; - if (runningFn) { - workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, sprintf(msg, runningFn, propName)); - throw new ScriptDeath(workerScript); - } - runningFn = propName; - - // If the function throws an error, clear the runningFn flag first, and then re-throw it - // This allows people to properly catch errors thrown by NS functions without getting - // the concurrent call error above - let result; - try { - result = f(...args); - } catch (e: unknown) { - runningFn = null; - throw e; - } - - if (result && result.finally !== undefined) { - return result.finally(function () { - runningFn = null; - }); - } else { - runningFn = null; - return result; - } - }; - } - - function wrapObject(vars: unknown, ...tree: string[]): void { - const isObject = (x: unknown): x is { [key: string]: unknown } => typeof x === "object"; - if (!isObject(vars)) throw new Error("wrong argument sent to wrapObject"); - for (const prop of Object.keys(vars)) { - let e = vars[prop]; - switch (typeof e) { - case "function": { - e = wrap([...tree, prop].join("."), e as any); - break; - } - case "object": { - if (Array.isArray(e)) continue; - wrapObject(e, ...tree, prop); - break; - } - } - } - } - wrapObject(workerScript.env.vars); - - // 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) => { executeJSScript(player, workerScript.getServer().scripts, workerScript) .then(() => {