From 65331ab22e485d4fcb4174e2172a1da342b07aab Mon Sep 17 00:00:00 2001 From: James Aguilar <799564+jaguilar@users.noreply.github.com> Date: Sun, 2 Jun 2019 15:21:08 -0400 Subject: [PATCH 1/6] recompile ns2 scripts if dependencies change --- src/NetscriptJSEvaluator.js | 58 ++++++++++++++++++++++++++++++++----- src/Script/Script.ts | 18 ++++++++++-- src/Server/BaseServer.ts | 2 +- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/NetscriptJSEvaluator.js b/src/NetscriptJSEvaluator.js index 0f8032f90..65b714c06 100644 --- a/src/NetscriptJSEvaluator.js +++ b/src/NetscriptJSEvaluator.js @@ -1,10 +1,22 @@ import { makeRuntimeRejectMsg } from "./NetscriptEvaluator"; +import { Script } from "./Script/Script"; // Makes a blob that contains the code of a given script. export function makeScriptBlob(code) { return new Blob([code], {type: "text/javascript"}); } +class ScriptUrl { + /** + * @param {string} filename + * @param {string} url + */ + constructor(filename, url) { + this.filename = filename; + this.url = url; + } +} + // Begin executing a user JS script, and return a promise that resolves // or rejects when the script finishes. // - script is a script to execute (see Script.js). We depend only on .filename and .code. @@ -15,9 +27,9 @@ export function makeScriptBlob(code) { // running the main function of the script. export async function executeJSScript(scripts = [], workerScript) { let loadedModule; - let urlStack = null; + let urls = null; let script = workerScript.getScript(); - if (script.module === "") { + if (shouldCompile(script, scripts)) { // The URL at the top is the one we want to import. It will // recursively import all the other modules in the urlStack. // @@ -25,8 +37,9 @@ export async function executeJSScript(scripts = [], workerScript) { // but not really behaves like import. Particularly, it cannot // load fully dynamic content. So we hide the import from webpack // by placing it inside an eval call. - urlStack = _getScriptUrls(script, scripts, []); - script.module = await eval('import(urlStack[urlStack.length - 1])'); + urls = _getScriptUrls(script, scripts, []); + script.module = await eval('import(urls[urls.length - 1].url)'); + script.dependencies = urls.map(u => u.filename); } loadedModule = script.module; @@ -41,12 +54,31 @@ export async function executeJSScript(scripts = [], workerScript) { return loadedModule.main(ns); } finally { // Revoke the generated URLs - if (urlStack != null) { - for (const url in urlStack) URL.revokeObjectURL(url); + if (urls != null) { + for (const b in urls) URL.revokeObjectURL(b.url); } }; } +/** Returns whether we should compile the script parameter. + * + * @param {Script} script + * @param {Script[]} scripts + */ +function shouldCompile(script, scripts) { + if (script.module === "") return true; + return script.dependencies.some(dep => { + const depScript = scripts.find(s => s.filename == dep); + + // If the script is not present on the server, we should recompile, if only to get any necessary + // compilation errors. + if (!depScript) return true; + + const depIsMoreRecent = depScript.updateTimestamp > script.updateTimestamp + return depIsMoreRecent; + }); +} + // Gets a stack of blob urls, the top/right-most element being // the blob url for the named script on the named server. // @@ -58,8 +90,18 @@ export async function executeJSScript(scripts = [], workerScript) { // different parts of the tree. That hasn't presented any problem with during // testing, but it might be an idea for the future. Would require a topo-sort // then url-izing from leaf-most to root-most. +/** + * @param {Script} script + * @param {Script[]} scripts + * @param {Script[]} seen + * @returns {ScriptUrl[]} All of the compiled scripts, with the final one + * in the list containing the blob corresponding to + * the script parameter. + */ +// BUG: apparently seen is never consulted. Oops. export function _getScriptUrls(script, scripts, seen) { // Inspired by: https://stackoverflow.com/a/43834063/91401 + /** @type {ScriptUrl[]} */ const urlStack = []; seen.push(script); try { @@ -86,7 +128,7 @@ export function _getScriptUrls(script, scripts, seen) { // The top url in the stack is the replacement import file for this script. urlStack.push(...urls); - return [prefix, urls[urls.length - 1], suffix].join(''); + return [prefix, urls[urls.length - 1].url, suffix].join(''); } ); @@ -96,7 +138,7 @@ export function _getScriptUrls(script, scripts, seen) { // If we successfully transformed the code, create a blob url for it and // push that URL onto the top of the stack. - urlStack.push(URL.createObjectURL(makeScriptBlob(transformedCode))); + urlStack.push(new ScriptUrl(script.filename, URL.createObjectURL(makeScriptBlob(transformedCode)))); return urlStack; } catch (err) { // If there is an error, we need to clean up the URLs. diff --git a/src/Script/Script.ts b/src/Script/Script.ts index cb3200a6c..dc8a69367 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -31,6 +31,14 @@ export class Script { // This is only applicable for NetscriptJS module: any = ""; + // The timestamp when when the script was last updated. + updateTimestamp: number = 0; + + // Only used with NS2 scripts; the list of dependency script filenames. This is constructed + // whenever the script is first evaluated, and therefore may be out of date if the script + // has been updated since it was last run. + dependencies: string[] = []; + // Amount of RAM this Script requres to run ramUsage: number = 0; @@ -85,11 +93,17 @@ export class Script { } this.filename = filenameElem!.value; this.server = serverIp; - this.updateRamUsage(otherScripts); - this.module = ""; + this.updateRamUsage(otherScripts); + this.markUpdated(); } } + // Marks that this script has been updated. Causes recompilation of NS2 modules. + markUpdated() { + this.module = ""; + this.updateTimestamp = Date.now(); + } + /** * Calculates and updates the script's RAM usage based on its code * @param {Script[]} otherScripts - Other scripts on the server. Used to process imports diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts index 74f297189..143570abf 100644 --- a/src/Server/BaseServer.ts +++ b/src/Server/BaseServer.ts @@ -233,7 +233,7 @@ export class BaseServer { let script = this.scripts[i]; script.code = code; script.updateRamUsage(this.scripts); - script.module = ""; + script.markUpdated(); ret.overwritten = true; ret.success = true; return ret; From 3ef9042051acb2db53d4b850cf04c030b4cbb5e4 Mon Sep 17 00:00:00 2001 From: James Aguilar <799564+jaguilar@users.noreply.github.com> Date: Sun, 2 Jun 2019 15:38:45 -0400 Subject: [PATCH 2/6] fix two cases where markUpdated was not properly called --- src/NetscriptFunctions.js | 2 +- src/NetscriptWorker.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index aa4c877dc..cbc0c6162 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -1139,7 +1139,7 @@ function NetscriptFunctions(workerScript) { var oldScript = destServer.scripts[i]; oldScript.code = sourceScript.code; oldScript.ramUsage = sourceScript.ramUsage; - oldScript.module = ""; + oldScript.module.markUpdated(); return true; } } diff --git a/src/NetscriptWorker.js b/src/NetscriptWorker.js index 319693358..095ba401d 100644 --- a/src/NetscriptWorker.js +++ b/src/NetscriptWorker.js @@ -534,7 +534,7 @@ export function loadAllRunningScripts() { // Reset modules on all scripts for (let i = 0; i < server.scripts.length; ++i) { - server.scripts[i].module = ""; + server.scripts[i].markUpdated(); } if (skipScriptLoad) { From 2201dfc371e06199dd55aba2cbd1d465421f5d05 Mon Sep 17 00:00:00 2001 From: James Aguilar <799564+jaguilar@users.noreply.github.com> Date: Sun, 2 Jun 2019 15:40:13 -0400 Subject: [PATCH 3/6] fix typo --- src/NetscriptFunctions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index cbc0c6162..5fef44888 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -1139,7 +1139,7 @@ function NetscriptFunctions(workerScript) { var oldScript = destServer.scripts[i]; oldScript.code = sourceScript.code; oldScript.ramUsage = sourceScript.ramUsage; - oldScript.module.markUpdated(); + oldScript.markUpdated(); return true; } } From d45689c7dff0d3e5852459273bb9fa44228b739e Mon Sep 17 00:00:00 2001 From: James Aguilar <799564+jaguilar@users.noreply.github.com> Date: Sun, 2 Jun 2019 20:17:27 -0400 Subject: [PATCH 4/6] use sequence numbers rather than timestamps for script expiration --- src/NetscriptJSEvaluator.js | 2 +- src/Script/Script.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/NetscriptJSEvaluator.js b/src/NetscriptJSEvaluator.js index 65b714c06..d3241f557 100644 --- a/src/NetscriptJSEvaluator.js +++ b/src/NetscriptJSEvaluator.js @@ -74,7 +74,7 @@ function shouldCompile(script, scripts) { // compilation errors. if (!depScript) return true; - const depIsMoreRecent = depScript.updateTimestamp > script.updateTimestamp + const depIsMoreRecent = depScript.moduleSequenceNumber > script.moduleSequenceNumber return depIsMoreRecent; }); } diff --git a/src/Script/Script.ts b/src/Script/Script.ts index dc8a69367..3b9df8135 100644 --- a/src/Script/Script.ts +++ b/src/Script/Script.ts @@ -15,6 +15,8 @@ import { } from "../../utils/JSONReviver"; import { roundToTwo } from "../../utils/helpers/roundToTwo"; +let globalModuleSequenceNumber = 0; + export class Script { // Initializes a Script Object from a JSON save state static fromJSON(value: any): Script { @@ -32,7 +34,7 @@ export class Script { module: any = ""; // The timestamp when when the script was last updated. - updateTimestamp: number = 0; + moduleSequenceNumber: number; // Only used with NS2 scripts; the list of dependency script filenames. This is constructed // whenever the script is first evaluated, and therefore may be out of date if the script @@ -51,6 +53,7 @@ export class Script { this.ramUsage = 0; this.server = server; // IP of server this script is on this.module = ""; + this.moduleSequenceNumber = ++globalModuleSequenceNumber; if (this.code !== "") { this.updateRamUsage(otherScripts); } }; @@ -101,7 +104,7 @@ export class Script { // Marks that this script has been updated. Causes recompilation of NS2 modules. markUpdated() { this.module = ""; - this.updateTimestamp = Date.now(); + this.moduleSequenceNumber = ++globalModuleSequenceNumber; } /** From eecb0c0f010e59d3b337a40e09e6f1a65d13af89 Mon Sep 17 00:00:00 2001 From: Heikki Aitakangas Date: Sat, 25 May 2019 21:20:24 +0300 Subject: [PATCH 5/6] Update script.module immediately, so we avoid accidentally evaling the same module several times --- src/NetscriptJSEvaluator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NetscriptJSEvaluator.js b/src/NetscriptJSEvaluator.js index 0f8032f90..c1632b0ae 100644 --- a/src/NetscriptJSEvaluator.js +++ b/src/NetscriptJSEvaluator.js @@ -26,9 +26,9 @@ export async function executeJSScript(scripts = [], workerScript) { // load fully dynamic content. So we hide the import from webpack // by placing it inside an eval call. urlStack = _getScriptUrls(script, scripts, []); - script.module = await eval('import(urlStack[urlStack.length - 1])'); + script.module = new Promise(resolve => resolve(eval('import(urlStack[urlStack.length - 1])'))); } - loadedModule = script.module; + loadedModule = await script.module; let ns = workerScript.env.vars; From e5e3fec1a941267ed5bc49e69c9fdd411f1e97b5 Mon Sep 17 00:00:00 2001 From: James Aguilar <799564+jaguilar@users.noreply.github.com> Date: Sat, 1 Jun 2019 22:48:48 -0400 Subject: [PATCH 6/6] close dialog boxes on escape --- utils/DialogBox.js | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/utils/DialogBox.js b/utils/DialogBox.js index 127740271..aa165bb94 100644 --- a/utils/DialogBox.js +++ b/utils/DialogBox.js @@ -1,3 +1,5 @@ +import { KEY } from "./helpers/keyCodes"; + /** * Create and display a pop-up dialog box. * This dialog box does not allow for any interaction and should close when clicking @@ -9,28 +11,31 @@ let dialogBoxes = []; $(document).click(function(event) { if (dialogBoxOpened && dialogBoxes.length >= 1) { if (!$(event.target).closest(dialogBoxes[0]).length){ - dialogBoxes[0].remove(); - dialogBoxes.splice(0, 1); - if (dialogBoxes.length == 0) { - dialogBoxOpened = false; - } else { - dialogBoxes[0].style.visibility = "visible"; - } + closeTopmostDialogBox(); } } }); +function closeTopmostDialogBox() { + if (!dialogBoxOpened || dialogBoxes.length === 0) return; + dialogBoxes[0].remove(); + dialogBoxes.shift(); + if (dialogBoxes.length == 0) { + dialogBoxOpened = false; + } else { + dialogBoxes[0].style.visibility = "visible"; + } +} // Dialog box close buttons $(document).on('click', '.dialog-box-close-button', function( event ) { - if (dialogBoxOpened && dialogBoxes.length >= 1) { - dialogBoxes[0].remove(); - dialogBoxes.splice(0, 1); - if (dialogBoxes.length == 0) { - dialogBoxOpened = false; - } else { - dialogBoxes[0].style.visibility = "visible"; - } + closeTopmostDialogBox(); +}); + +document.addEventListener("keydown", function (event) { + if (event.keyCode == KEY.ESC && dialogBoxOpened) { + closeTopmostDialogBox(); + event.preventDefault(); } });