Merge pull request #3624 from danielyxie/infinite-loop-safety

MISC: Implemented infinite loop safety net.
This commit is contained in:
hydroflame 2022-05-18 15:35:12 -04:00 committed by GitHub
commit f86eae5276
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 30 additions and 0 deletions

@ -84,6 +84,7 @@ export const CONSTANTS: {
SoARepMult: number; SoARepMult: number;
EntropyEffect: number; EntropyEffect: number;
TotalNumBitNodes: number; TotalNumBitNodes: number;
InfiniteLoopLimit: number;
Donations: number; // number of blood/plasma/palette donation the dev have verified., boosts NFG Donations: number; // number of blood/plasma/palette donation the dev have verified., boosts NFG
LatestUpdate: string; LatestUpdate: string;
} = { } = {
@ -226,6 +227,8 @@ export const CONSTANTS: {
// BitNode/Source-File related stuff // BitNode/Source-File related stuff
TotalNumBitNodes: 24, TotalNumBitNodes: 24,
InfiniteLoopLimit: 1000,
Donations: 7, Donations: 7,
LatestUpdate: ` LatestUpdate: `

@ -179,6 +179,12 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => {
</> </>
} }
/> />
<OptionSwitch
checked={Settings.InfinityLoopSafety}
onChange={(newValue) => (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.</>}
/>
</GameOptionsPage> </GameOptionsPage>
), ),
[GameOptionsTab.INTERFACE]: ( [GameOptionsTab.INTERFACE]: (

@ -5,6 +5,8 @@ import type { WorkerScript } from "./WorkerScript";
import { makeRuntimeRejectMsg } from "../NetscriptEvaluator"; import { makeRuntimeRejectMsg } from "../NetscriptEvaluator";
import { Player } from "../Player"; import { Player } from "../Player";
import { CityName } from "src/Locations/data/CityNames"; import { CityName } from "src/Locations/data/CityNames";
import { Settings } from "../Settings/Settings";
import { CONSTANTS } from "../Constants";
type ExternalFunction = (...args: any[]) => any; type ExternalFunction = (...args: any[]) => any;
type ExternalAPI = { type ExternalAPI = {
@ -91,8 +93,14 @@ function wrapFunction(
getValidPort: (port: any) => helpers.getValidPort(functionPath, port), getValidPort: (port: any) => helpers.getValidPort(functionPath, port),
}, },
}; };
const safetyEnabled = Settings.InfinityLoopSafety;
function wrappedFunction(...args: unknown[]): unknown { function wrappedFunction(...args: unknown[]): unknown {
helpers.updateDynamicRam(ctx.function, getRamCost(Player, ...tree, ctx.function)); 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); return func(ctx)(...args);
} }
const parent = getNestedProperty(wrappedAPI, ...tree); const parent = getNestedProperty(wrappedAPI, ...tree);

@ -111,6 +111,11 @@ export class WorkerScript {
*/ */
atExit: any; 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) { constructor(runningScriptObj: RunningScript, pid: number, nsFuncsGenerator?: (ws: WorkerScript) => any) {
this.name = runningScriptObj.filename; this.name = runningScriptObj.filename;
this.hostname = runningScriptObj.server; this.hostname = runningScriptObj.server;

@ -14,6 +14,7 @@ export function netscriptDelay(time: number, workerScript: WorkerScript): Promis
workerScript.delay = null; workerScript.delay = null;
workerScript.delayReject = undefined; workerScript.delayReject = undefined;
workerScript.infiniteLoopSafetyCounter = 0;
if (workerScript.env.stopFlag) reject(new ScriptDeath(workerScript)); if (workerScript.env.stopFlag) reject(new ScriptDeath(workerScript));
else resolve(); else resolve();
}, time); }, time);

@ -54,6 +54,11 @@ interface IDefaultSettings {
*/ */
EnableBashHotkeys: boolean; EnableBashHotkeys: boolean;
/**
* Infinite loop safety net
*/
InfinityLoopSafety: boolean;
/** /**
* Timestamps format * Timestamps format
*/ */
@ -201,6 +206,7 @@ export const defaultSettings: IDefaultSettings = {
DisableOverviewProgressBars: false, DisableOverviewProgressBars: false,
EnableBashHotkeys: false, EnableBashHotkeys: false,
TimestampsFormat: "", TimestampsFormat: "",
InfinityLoopSafety: true,
Locale: "en", Locale: "en",
MaxRecentScriptsCapacity: 50, MaxRecentScriptsCapacity: 50,
MaxLogCapacity: 50, MaxLogCapacity: 50,
@ -241,6 +247,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
DisableOverviewProgressBars: defaultSettings.DisableOverviewProgressBars, DisableOverviewProgressBars: defaultSettings.DisableOverviewProgressBars,
EnableBashHotkeys: defaultSettings.EnableBashHotkeys, EnableBashHotkeys: defaultSettings.EnableBashHotkeys,
TimestampsFormat: defaultSettings.TimestampsFormat, TimestampsFormat: defaultSettings.TimestampsFormat,
InfinityLoopSafety: defaultSettings.InfinityLoopSafety,
Locale: "en", Locale: "en",
MaxRecentScriptsCapacity: defaultSettings.MaxRecentScriptsCapacity, MaxRecentScriptsCapacity: defaultSettings.MaxRecentScriptsCapacity,
MaxLogCapacity: defaultSettings.MaxLogCapacity, MaxLogCapacity: defaultSettings.MaxLogCapacity,