diff --git a/src/Constants.ts b/src/Constants.ts index 2d1814adc..bac13e1b4 100644 --- a/src/Constants.ts +++ b/src/Constants.ts @@ -84,6 +84,7 @@ export const CONSTANTS: { SoARepMult: number; EntropyEffect: number; TotalNumBitNodes: number; + InfiniteLoopLimit: number; Donations: number; // number of blood/plasma/palette donation the dev have verified., boosts NFG LatestUpdate: string; } = { @@ -226,6 +227,8 @@ export const CONSTANTS: { // BitNode/Source-File related stuff TotalNumBitNodes: 24, + InfiniteLoopLimit: 1000, + Donations: 7, LatestUpdate: ` diff --git a/src/GameOptions/ui/CurrentOptionsPage.tsx b/src/GameOptions/ui/CurrentOptionsPage.tsx index ff154f768..9414241fa 100644 --- a/src/GameOptions/ui/CurrentOptionsPage.tsx +++ b/src/GameOptions/ui/CurrentOptionsPage.tsx @@ -179,6 +179,12 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => { } /> + (Settings.InfinityLoopSafety = newValue)} + text="Script infinite loop safety net" + tooltip={<>If this is set the game will attempt to automatically kill scripts stuck in infinite loops.} + /> ), [GameOptionsTab.INTERFACE]: ( diff --git a/src/Netscript/APIWrapper.ts b/src/Netscript/APIWrapper.ts index 3dc0845a7..9d7257966 100644 --- a/src/Netscript/APIWrapper.ts +++ b/src/Netscript/APIWrapper.ts @@ -5,6 +5,8 @@ import type { WorkerScript } from "./WorkerScript"; import { makeRuntimeRejectMsg } from "../NetscriptEvaluator"; import { Player } from "../Player"; import { CityName } from "src/Locations/data/CityNames"; +import { Settings } from "../Settings/Settings"; +import { CONSTANTS } from "../Constants"; type ExternalFunction = (...args: any[]) => any; type ExternalAPI = { @@ -91,8 +93,14 @@ function wrapFunction( getValidPort: (port: any) => helpers.getValidPort(functionPath, port), }, }; + const safetyEnabled = Settings.InfinityLoopSafety; function wrappedFunction(...args: unknown[]): unknown { helpers.updateDynamicRam(ctx.function, getRamCost(Player, ...tree, ctx.function)); + if (safetyEnabled) workerScript.infiniteLoopSafetyCounter++; + if (workerScript.infiniteLoopSafetyCounter > CONSTANTS.InfiniteLoopLimit) + throw new Error( + `Infinite loop without sleep detected. ${CONSTANTS.InfiniteLoopLimit} ns functions were called without sleep. This will cause your UI to hang.`, + ); return func(ctx)(...args); } const parent = getNestedProperty(wrappedAPI, ...tree); diff --git a/src/Netscript/WorkerScript.ts b/src/Netscript/WorkerScript.ts index 59f8a38f0..699270b15 100644 --- a/src/Netscript/WorkerScript.ts +++ b/src/Netscript/WorkerScript.ts @@ -111,6 +111,11 @@ export class WorkerScript { */ atExit: any; + /** + * Once this counter reaches it's limit the script crashes. It is reset when a promise completes. + */ + infiniteLoopSafetyCounter = 0; + constructor(runningScriptObj: RunningScript, pid: number, nsFuncsGenerator?: (ws: WorkerScript) => any) { this.name = runningScriptObj.filename; this.hostname = runningScriptObj.server; diff --git a/src/NetscriptEvaluator.ts b/src/NetscriptEvaluator.ts index 4ef8c6a22..586b276aa 100644 --- a/src/NetscriptEvaluator.ts +++ b/src/NetscriptEvaluator.ts @@ -14,6 +14,7 @@ export function netscriptDelay(time: number, workerScript: WorkerScript): Promis workerScript.delay = null; workerScript.delayReject = undefined; + workerScript.infiniteLoopSafetyCounter = 0; if (workerScript.env.stopFlag) reject(new ScriptDeath(workerScript)); else resolve(); }, time); diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts index c8d1cfd0c..ea7eaacdd 100644 --- a/src/Settings/Settings.ts +++ b/src/Settings/Settings.ts @@ -54,6 +54,11 @@ interface IDefaultSettings { */ EnableBashHotkeys: boolean; + /** + * Infinite loop safety net + */ + InfinityLoopSafety: boolean; + /** * Timestamps format */ @@ -201,6 +206,7 @@ export const defaultSettings: IDefaultSettings = { DisableOverviewProgressBars: false, EnableBashHotkeys: false, TimestampsFormat: "", + InfinityLoopSafety: true, Locale: "en", MaxRecentScriptsCapacity: 50, MaxLogCapacity: 50, @@ -241,6 +247,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = { DisableOverviewProgressBars: defaultSettings.DisableOverviewProgressBars, EnableBashHotkeys: defaultSettings.EnableBashHotkeys, TimestampsFormat: defaultSettings.TimestampsFormat, + InfinityLoopSafety: defaultSettings.InfinityLoopSafety, Locale: "en", MaxRecentScriptsCapacity: defaultSettings.MaxRecentScriptsCapacity, MaxLogCapacity: defaultSettings.MaxLogCapacity,