diff --git a/src/Script/RamCalculations.ts b/src/Script/RamCalculations.ts index ce94f52de..d3fe69a99 100644 --- a/src/Script/RamCalculations.ts +++ b/src/Script/RamCalculations.ts @@ -22,10 +22,21 @@ export interface RamUsageEntry { cost: number; } -export interface RamCalculation { +export type RamCalculationSuccess = { cost: number; - entries?: RamUsageEntry[]; -} + entries: RamUsageEntry[]; + errorCode?: never; + errorMessage?: never; +}; + +export type RamCalculationFailure = { + cost?: never; + entries?: never; + errorCode: RamCalculationErrorCode; + errorMessage?: string; +}; + +export type RamCalculation = RamCalculationSuccess | RamCalculationFailure; // These special strings are used to reference the presence of a given logical // construct within a user script. @@ -80,115 +91,112 @@ function parseOnlyRamCalculate(otherScripts: Map, code: dependencyMap = Object.assign(dependencyMap, result.dependencyMap); } - try { - // Parse the initial module, which is the "main" script that is being run - const initialModule = "__SPECIAL_INITIAL_MODULE__"; - parseCode(code, initialModule); + // Parse the initial module, which is the "main" script that is being run + const initialModule = "__SPECIAL_INITIAL_MODULE__"; + parseCode(code, initialModule); - // Process additional modules, which occurs if the "main" script has any imports - while (parseQueue.length > 0) { - const nextModule = parseQueue.shift(); - if (nextModule === undefined) throw new Error("nextModule should not be undefined"); - if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue; + // Process additional modules, which occurs if the "main" script has any imports + while (parseQueue.length > 0) { + const nextModule = parseQueue.shift(); + if (nextModule === undefined) throw new Error("nextModule should not be undefined"); + if (nextModule.startsWith("https://") || nextModule.startsWith("http://")) continue; - // Using root as the path base right now. Difficult to implement - const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js"); - if (!filename) return { cost: RamCalculationErrorCode.ImportError }; // Invalid import path - const script = otherScripts.get(filename); - if (!script) return { cost: RamCalculationErrorCode.ImportError }; // No such file on server - - parseCode(script.code, nextModule); + // Using root as the path base right now. Difficult to implement + const filename = resolveScriptFilePath(nextModule, root, ns1 ? ".script" : ".js"); + if (!filename) { + return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `Invalid import path: "${nextModule}"` }; + } + const script = otherScripts.get(filename); + if (!script) { + return { errorCode: RamCalculationErrorCode.ImportError, errorMessage: `No such file on server: "${filename}"` }; } - // Finally, walk the reference map and generate a ram cost. The initial set of keys to scan - // are those that start with __SPECIAL_INITIAL_MODULE__. - let ram = RamCostConstants.Base; - const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }]; - const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule)); - const resolvedRefs = new Set(); - const loadedFns: Record = {}; - while (unresolvedRefs.length > 0) { - const ref = unresolvedRefs.shift(); - if (ref === undefined) throw new Error("ref should not be undefined"); + parseCode(script.code, nextModule); + } - // Check if this is one of the special keys, and add the appropriate ram cost if so. - if (ref === "hacknet" && !resolvedRefs.has("hacknet")) { - ram += RamCostConstants.HacknetNodes; - detailedCosts.push({ type: "ns", name: "hacknet", cost: RamCostConstants.HacknetNodes }); - } - if (ref === "document" && !resolvedRefs.has("document")) { - ram += RamCostConstants.Dom; - detailedCosts.push({ type: "dom", name: "document", cost: RamCostConstants.Dom }); - } - if (ref === "window" && !resolvedRefs.has("window")) { - ram += RamCostConstants.Dom; - detailedCosts.push({ type: "dom", name: "window", cost: RamCostConstants.Dom }); - } + // Finally, walk the reference map and generate a ram cost. The initial set of keys to scan + // are those that start with __SPECIAL_INITIAL_MODULE__. + let ram = RamCostConstants.Base; + const detailedCosts: RamUsageEntry[] = [{ type: "misc", name: "baseCost", cost: RamCostConstants.Base }]; + const unresolvedRefs = Object.keys(dependencyMap).filter((s) => s.startsWith(initialModule)); + const resolvedRefs = new Set(); + const loadedFns: Record = {}; + while (unresolvedRefs.length > 0) { + const ref = unresolvedRefs.shift(); + if (ref === undefined) throw new Error("ref should not be undefined"); - resolvedRefs.add(ref); + // Check if this is one of the special keys, and add the appropriate ram cost if so. + if (ref === "hacknet" && !resolvedRefs.has("hacknet")) { + ram += RamCostConstants.HacknetNodes; + detailedCosts.push({ type: "ns", name: "hacknet", cost: RamCostConstants.HacknetNodes }); + } + if (ref === "document" && !resolvedRefs.has("document")) { + ram += RamCostConstants.Dom; + detailedCosts.push({ type: "dom", name: "document", cost: RamCostConstants.Dom }); + } + if (ref === "window" && !resolvedRefs.has("window")) { + ram += RamCostConstants.Dom; + detailedCosts.push({ type: "dom", name: "window", cost: RamCostConstants.Dom }); + } - if (ref.endsWith(".*")) { - // A prefix reference. We need to find all matching identifiers. - const prefix = ref.slice(0, ref.length - 2); - for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) { - for (const dep of dependencyMap[ident] || []) { - if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); - } - } - } else { - // An exact reference. Add all dependencies of this ref. - for (const dep of dependencyMap[ref] || []) { + resolvedRefs.add(ref); + + if (ref.endsWith(".*")) { + // A prefix reference. We need to find all matching identifiers. + const prefix = ref.slice(0, ref.length - 2); + for (const ident of Object.keys(dependencyMap).filter((k) => k.startsWith(prefix))) { + for (const dep of dependencyMap[ident] || []) { if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); } } - - // Check if this identifier is a function in the workerScript environment. - // If it is, then we need to get its RAM cost. - try { - // Only count each function once - if (loadedFns[ref]) { - continue; - } - loadedFns[ref] = true; - - // This accounts for namespaces (Bladeburner, CodingContract, etc.) - const findFunc = ( - prefix: string, - obj: object, - ref: string, - ): { func: () => number | number; refDetail: string } | undefined => { - if (!obj) return; - const elem = Object.entries(obj).find(([key]) => key === ref); - if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) { - return { func: elem[1], refDetail: `${prefix}${ref}` }; - } - for (const [key, value] of Object.entries(obj)) { - const found = findFunc(`${key}.`, value, ref); - if (found) return found; - } - return undefined; - }; - - const details = findFunc("", RamCosts, ref); - const fnRam = getNumericCost(details?.func ?? 0); - ram += fnRam; - detailedCosts.push({ type: "fn", name: details?.refDetail ?? "", cost: fnRam }); - } catch (error) { - console.error(error); - continue; + } else { + // An exact reference. Add all dependencies of this ref. + for (const dep of dependencyMap[ref] || []) { + if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep); } } - if (ram > RamCostConstants.Max) { - ram = RamCostConstants.Max; - detailedCosts.push({ type: "misc", name: "Max Ram Cap", cost: RamCostConstants.Max }); + + // Check if this identifier is a function in the workerScript environment. + // If it is, then we need to get its RAM cost. + try { + // Only count each function once + if (loadedFns[ref]) { + continue; + } + loadedFns[ref] = true; + + // This accounts for namespaces (Bladeburner, CodingContract, etc.) + const findFunc = ( + prefix: string, + obj: object, + ref: string, + ): { func: () => number | number; refDetail: string } | undefined => { + if (!obj) return; + const elem = Object.entries(obj).find(([key]) => key === ref); + if (elem !== undefined && (typeof elem[1] === "function" || typeof elem[1] === "number")) { + return { func: elem[1], refDetail: `${prefix}${ref}` }; + } + for (const [key, value] of Object.entries(obj)) { + const found = findFunc(`${key}.`, value, ref); + if (found) return found; + } + return undefined; + }; + + const details = findFunc("", RamCosts, ref); + const fnRam = getNumericCost(details?.func ?? 0); + ram += fnRam; + detailedCosts.push({ type: "fn", name: details?.refDetail ?? "", cost: fnRam }); + } catch (error) { + console.error(error); + continue; } - return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) }; - } catch (error) { - // console.info("parse or eval error: ", error); - // This is not unexpected. The user may be editing a script, and it may be in - // a transitory invalid state. - return { cost: RamCalculationErrorCode.SyntaxError }; } + if (ram > RamCostConstants.Max) { + ram = RamCostConstants.Max; + detailedCosts.push({ type: "misc", name: "Max Ram Cap", cost: RamCostConstants.Max }); + } + return { cost: ram, entries: detailedCosts.filter((e) => e.cost > 0) }; } export function checkInfiniteLoop(code: string): number { @@ -362,20 +370,21 @@ function parseOnlyCalculateDeps(code: string, currentModule: string): ParseDepsR /** * Calculate's a scripts RAM Usage - * @param {string} codeCopy - The script's code + * @param {string} code - The script's code * @param {Script[]} otherScripts - All other scripts on the server. * Used to account for imported scripts */ export function calculateRamUsage( - codeCopy: string, + code: string, otherScripts: Map, ns1?: boolean, ): RamCalculation { try { - return parseOnlyRamCalculate(otherScripts, codeCopy, ns1); + return parseOnlyRamCalculate(otherScripts, code, ns1); } catch (e) { - console.error(`Failed to parse script for RAM calculations:`); - console.error(e); - return { cost: RamCalculationErrorCode.SyntaxError }; + return { + errorCode: RamCalculationErrorCode.SyntaxError, + errorMessage: e instanceof Error ? e.message : undefined, + }; } } diff --git a/src/Script/Script.ts b/src/Script/Script.ts index a8786e52b..da8db54d5 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -17,6 +17,7 @@ export class Script implements ContentFile { // Ram calculation, only exists after first poll of ram cost after updating ramUsage: number | null = null; ramUsageEntries: RamUsageEntry[] = []; + ramCalculationError: string | null = null; // Runtime data that only exists when the script has been initiated. Cleared when script or a dependency script is updated. mod: LoadedModule | null = null; @@ -65,6 +66,7 @@ export class Script implements ContentFile { // Always clear ram usage this.ramUsage = null; this.ramUsageEntries.length = 0; + this.ramCalculationError = null; // Early return if there's already no URL if (!this.mod) return; this.mod = null; @@ -88,12 +90,15 @@ export class Script implements ContentFile { */ updateRamUsage(otherScripts: Map): void { const ramCalc = calculateRamUsage(this.code, otherScripts, this.filename.endsWith(".script")); - if (ramCalc.cost >= RamCostConstants.Base) { + if (ramCalc.cost && ramCalc.cost >= RamCostConstants.Base) { this.ramUsage = roundToTwo(ramCalc.cost); this.ramUsageEntries = ramCalc.entries as RamUsageEntry[]; - } else { - this.ramUsage = null; + this.ramCalculationError = null; + return; } + + this.ramUsage = null; + this.ramCalculationError = ramCalc.errorMessage ?? null; } /** Remove script from server. Fails if the provided server isn't the server for this script. */ diff --git a/src/ScriptEditor/ui/ScriptEditorContext.tsx b/src/ScriptEditor/ui/ScriptEditorContext.tsx index d408ce92a..e23465ca3 100644 --- a/src/ScriptEditor/ui/ScriptEditorContext.tsx +++ b/src/ScriptEditor/ui/ScriptEditorContext.tsx @@ -34,9 +34,9 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React setRamEntries([["N/A", ""]]); return; } - const codeCopy = newCode + ""; - const ramUsage = calculateRamUsage(codeCopy, server.scripts); - if (ramUsage.cost > 0) { + + const ramUsage = calculateRamUsage(newCode, server.scripts); + if (ramUsage.cost && ramUsage.cost > 0) { const entries = ramUsage.entries?.sort((a, b) => b.cost - a.cost) ?? []; const entriesDisp = []; for (const entry of entries) { @@ -48,23 +48,20 @@ export function ScriptEditorContextProvider({ children, vim }: { children: React return; } - let RAM = ""; - const entriesDisp = []; - switch (ramUsage.cost) { - case RamCalculationErrorCode.ImportError: { - RAM = "RAM: Import Error"; - entriesDisp.push(["Import Error", ""]); - break; - } - case RamCalculationErrorCode.SyntaxError: - default: { - RAM = "RAM: Syntax Error"; - entriesDisp.push(["Syntax Error", ""]); - break; + if (ramUsage.errorCode !== undefined) { + setRamEntries([["Syntax Error", ramUsage.errorMessage ?? ""]]); + switch (ramUsage.errorCode) { + case RamCalculationErrorCode.ImportError: + setRAM("RAM: Import Error"); + break; + case RamCalculationErrorCode.SyntaxError: + setRAM("RAM: Syntax Error"); + break; } + } else { + setRAM("RAM: Syntax Error"); + setRamEntries([["Syntax Error", ""]]); } - setRAM(RAM); - setRamEntries(entriesDisp); }; const [isUpdatingRAM, { on: startUpdatingRAM, off: finishUpdatingRAM }] = useBoolean(false); diff --git a/src/Terminal/commands/runScript.ts b/src/Terminal/commands/runScript.ts index dcded6ca2..5cd13b7c9 100644 --- a/src/Terminal/commands/runScript.ts +++ b/src/Terminal/commands/runScript.ts @@ -24,18 +24,19 @@ export function runScript(path: ScriptFilePath, commandArgs: (string | number | if (!isPositiveInteger(numThreads)) { return Terminal.error("Invalid number of threads specified. Number of threads must be an integer greater than 0"); } + if (!server.hasAdminRights) return Terminal.error("Need root access to run script"); // Todo: Switch out arg for something with typescript support const args = flags._ as ScriptArg[]; const singleRamUsage = script.getRamUsage(server.scripts); - if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script."); + if (!singleRamUsage) { + return Terminal.error(`Error while calculating ram usage for this script. ${script.ramCalculationError}`); + } const ramUsage = singleRamUsage * numThreads; const ramAvailable = server.maxRam - server.ramUsed; - if (!server.hasAdminRights) return Terminal.error("Need root access to run script"); - if (ramUsage > ramAvailable + 0.001) { return Terminal.error( "This machine does not have enough RAM to run this script" +