Merge jaguilar's changes to incorporate Netscript JS (PR #212)

This commit is contained in:
danielyxie 2018-05-05 17:13:35 -05:00
parent b6e49c1a82
commit 8dba456b65
7 changed files with 4699 additions and 134066 deletions

131423
dist/bundle.js vendored

File diff suppressed because one or more lines are too long

3239
dist/engine.bundle.js vendored

File diff suppressed because one or more lines are too long

3756
dist/tests.bundle.js vendored

File diff suppressed because one or more lines are too long

@ -32,7 +32,8 @@ import {Locations} from "./Location.js";
import {Message, Messages} from "./Message.js";
import {inMission} from "./Missions.js";
import {Player} from "./Player.js";
import {Script, findRunningScript, RunningScript} from "./Script.js";
import {Script, findRunningScript, RunningScript,
isScriptFilename} from "./Script.js";
import {Server, getServer, AddToAllServers,
AllServers, processSingleServerGrowth,
GetServerByHostname} from "./Server.js";
@ -741,7 +742,7 @@ function NetscriptFunctions(workerScript) {
});
return res;
}
if (!scriptname.endsWith(".lit") && !scriptname.endsWith(".script") &&
if (!scriptname.endsWith(".lit") && !isScriptFilename(scriptname) &&
!scriptname.endsWith("txt")) {
throw makeRuntimeRejectMsg(workerScript, "Error: scp() does not work with this file type. It only works for .script, .lit, and .txt files");
}
@ -1917,7 +1918,7 @@ function NetscriptFunctions(workerScript) {
return true;
}
}
} else if (fn.endsWith(".script")) {
} else if (isScriptFilename(fn)) {
for (var i = 0; i < s.scripts.length; ++i) {
if (s.scripts[i].filename === fn) {
//Check that the script isnt currently running

@ -5,7 +5,9 @@ import {CONSTANTS} from "./Constants.js";
import {Engine} from "./engine.js";
import {Environment} from "./NetscriptEnvironment.js";
import {evaluate, isScriptErrorMessage,
makeRuntimeRejectMsg,
killNetscriptDelay} from "./NetscriptEvaluator.js";
import {executeJSScript} from "./NetscriptJSEvaluator.js";
import {NetscriptPort} from "./NetscriptPort.js";
import {AllServers} from "./Server.js";
import {Settings} from "./Settings.js";
@ -54,6 +56,64 @@ function prestigeWorkerScripts() {
workerScripts.length = 0;
}
// JS script promises need a little massaging to have the same guarantees as netscript
// promises. This does said massaging and kicks the script off. It returns a promise
// that resolves or rejects when the corresponding worker script is done.
function startJsScript(workerScript) {
workerScript.running = true;
// 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, f) {
let running = null; // The name of the currently running netscript function.
// 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) {
const msg = "Concurrent calls to Netscript functions not allowed! " +
"Did you forget to await hack(), grow(), or some other " +
"promise-returning function? (Currently running: %s tried to run: %s)"
if (running) {
workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, sprintf(msg, running, propName), null)
throw workerScript;
}
running = propName;
let result = f(...args);
if (result && result.finally !== undefined) {
return result.finally(function () {
running = null;
});
} else {
running = null;
return result;
}
}
};
for (let prop in workerScript.env.vars) {
if (typeof workerScript.env.vars[prop] !== "function") continue;
if (prop === "sleep") continue; // OK for multiple simultaneous calls to sleep.
workerScript.env.vars[prop] = wrap(prop, workerScript.env.vars[prop]);
}
// 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 executeJSScript(workerScript.scriptRef.scriptRef,
workerScript.getServer().scripts,
workerScript.env.vars).then(function (mainReturnValue) {
if (mainReturnValue === undefined) return workerScript;
return [mainReturnValue, workerScript];
}).catch(e => {
if (e instanceof Error) {
workerScript.errorMessage = makeRuntimeRejectMsg(workerScript, e.message + (e.stack && ("\nstack:\n" + e.stack.toString()) || ""));
throw workerScript;
} else if (isScriptErrorMessage(e)) {
workerScript.errorMessage = e;
throw workerScript;
}
throw e; // Don't know what to do with it, let's rethrow.
});
}
//Loop through workerScripts and run every script that is not currently running
function runScriptsLoop() {
//Delete any scripts that finished or have been killed. Loop backwards bc removing
@ -87,18 +147,23 @@ function runScriptsLoop() {
for (var i = 0; i < workerScripts.length; i++) {
//If it isn't running, start the script
if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == false) {
try {
var ast = parse(workerScripts[i].code, {sourceType:"module"});
//console.log(ast);
} catch (e) {
console.log("Error parsing script: " + workerScripts[i].name);
dialogBoxCreate("Syntax ERROR in " + workerScripts[i].name + ":<br>" + e);
workerScripts[i].env.stopFlag = true;
continue;
}
let p = null; // p is the script's result promise.
if (workerScripts[i].name.endsWith(".js")) {
p = startJsScript(workerScripts[i]);
} else {
try {
var ast = parse(workerScripts[i].code, {sourceType:"module"});
//console.log(ast);
} catch (e) {
console.log("Error parsing script: " + workerScripts[i].name);
dialogBoxCreate("Syntax ERROR in " + workerScripts[i].name + ":<br>" + e);
workerScripts[i].env.stopFlag = true;
continue;
}
workerScripts[i].running = true;
p = evaluate(ast, workerScripts[i]);
}
workerScripts[i].running = true;
var p = evaluate(ast, workerScripts[i]);
//Once the code finishes (either resolved or rejected, doesnt matter), set its
//running status to false
p.then(function(w) {

@ -13,6 +13,9 @@ require("brace/keybinding/vim");
require("brace/keybinding/emacs");
require("brace/ext/language_tools");
// Importing this doesn't work for some reason.
const walk = require("acorn/dist/walk");
import {CONSTANTS} from "./Constants.js";
import {Engine} from "./engine.js";
import {parseFconfSettings} from "./Fconf.js";
@ -42,6 +45,10 @@ var keybindings = {
emacs: "ace/keyboard/emacs",
};
function isScriptFilename(f) {
return f.endsWith(".js") || f.endsWith(".script");
}
var scriptEditorRamCheck = null, scriptEditorRamText = null;
function scriptEditorInit() {
//Create buttons at the bottom of script editor
@ -209,7 +216,7 @@ function scriptEditorInit() {
//Updates RAM usage in script
function updateScriptEditorContent() {
var filename = document.getElementById("script-editor-filename").value;
if (scriptEditorRamCheck == null || !scriptEditorRamCheck.checked || !filename.endsWith(".script")) {
if (scriptEditorRamCheck == null || !scriptEditorRamCheck.checked || !isScriptFilename(filename)) {
scriptEditorRamText.innerText = "N/A";
return;
}
@ -270,7 +277,7 @@ function saveAndCloseScriptEditor() {
dialogBoxCreate("Invalid .fconf file");
return;
}
} else if (filename.endsWith(".script")) {
} else if (isScriptFilename(filename)) {
//If the current script already exists on the server, overwrite it
for (var i = 0; i < s.scripts.length; i++) {
if (filename == s.scripts[i].filename) {
@ -348,6 +355,219 @@ Script.prototype.updateRamUsage = function() {
}
}
// These special strings are used to reference the presence of a given logical
// construct within a user script.
const specialReferenceIF = "__SPECIAL_referenceIf";
const specialReferenceFOR = "__SPECIAL_referenceFor";
const specialReferenceWHILE = "__SPECIAL_referenceWhile";
// The global scope of a script is registered under this key during parsing.
const memCheckGlobalKey = ".__GLOBAL__";
// Calcluates the amount of RAM a script uses. Uses parsing and AST walking only,
// rather than NetscriptEvaluator. This is useful because NetscriptJS code does
// not work under NetscriptEvaluator.
function parseOnlyRamCalculate(server, code, workerScript) {
try {
// Maps dependent identifiers to their dependencies.
//
// The initial identifier is __SPECIAL_INITIAL_MODULE__.__GLOBAL__.
// It depends on all the functions declared in the module, all the global scopes
// of its imports, and any identifiers referenced in this global scope. Each
// function depends on all the identifiers referenced internally.
// We walk the dependency graph to calculate RAM usage, given that some identifiers
// reference Netscript functions which have a RAM cost.
let dependencyMap = {};
// Scripts we've parsed.
const completedParses = new Set();
// Scripts we've discovered that need to be parsed.
const parseQueue = [];
// Parses a chunk of code with a given module name, and updates parseQueue and dependencyMap.
function parseCode(code, moduleName) {
const result = parseOnlyCalculateDeps(code, moduleName);
completedParses.add(moduleName);
// Add any additional modules to the parse queue;
for (let i = 0; i < result.additionalModules.length; ++i) {
if (!completedParses.has(result.additionalModules[i])) {
parseQueue.push(result.additionalModules[i]);
}
}
// Splice all the references in.
dependencyMap = {...dependencyMap, ...result.dependencyMap};
}
const initialModule = "__SPECIAL_INITIAL_MODULE__";
parseCode(code, initialModule);
while (parseQueue.length > 0) {
// Get the code from the server.
const nextModule = parseQueue.shift();
const script = server.getScript(nextModule);
if (!script) return -1; // No such script on the server.
// Not sure why we always take copies, but let's do that here too.
parseCode(script.code.repeat(1), nextModule);
}
// 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 = 1.4;
const unresolvedRefs = Object.keys(dependencyMap).filter(s => s.startsWith(initialModule));
const resolvedRefs = new Set();
while (unresolvedRefs.length > 0) {
const ref = unresolvedRefs.shift();
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 (let ident of Object.keys(dependencyMap).filter(k => k.startsWith(prefix))) {
for (let dep of dependencyMap[ident] || []) {
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
}
}
} else {
// An exact reference. Add all dependencies of this ref.
for (let dep of dependencyMap[ref] || []) {
if (!resolvedRefs.has(dep)) unresolvedRefs.push(dep);
}
}
// Check if this is one of the special keys, and add the appropriate ram cost if so.
if (ref == specialReferenceIF) ram += CONSTANTS.ScriptIfRamCost;
if (ref == specialReferenceFOR) ram += CONSTANTS.ScriptForRamCost;
if (ref == specialReferenceWHILE) ram += CONSTANTS.ScriptWhileRamCost;
if (ref == "hacknetnodes") ram += CONSTANTS.ScriptHacknetNodesRamCost;
// Check if this ident is a function in the workerscript env. If it is, then we need to
// get its RAM cost. We do this by calling it, which works because the running script
// is in checkingRam mode.
//
// TODO it would be simpler to just reference a dictionary.
try {
var func = workerScript.env.get(ref);
if (typeof func === "function") {
try {
var res = func.apply(null, []);
if (typeof res === "number") {
ram += res;
}
} catch(e) {
console.log("ERROR applying function: " + e);
}
}
} catch (error) { continue; }
}
return ram;
} 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 -1;
}
}
// Parses one script and calculates its ram usage, for the global scope and each function.
// Returns a cost map and a dependencyMap for the module. Returns a reference map to be joined
// onto the main reference map, and a list of modules that need to be parsed.
function parseOnlyCalculateDeps(code, currentModule) {
const ast = parse(code, {sourceType:"module", ecmaVersion: 8});
// Everything from the global scope goes in ".". Everything else goes in ".function", where only
// the outermost layer of functions counts.
const globalKey = currentModule + memCheckGlobalKey;
const dependencyMap = {};
dependencyMap[globalKey] = new Set();
// If we reference this internal name, we're really referencing that external name.
// Filled when we import names from other modules.
let internalToExternal = {};
var additionalModules = [];
// References get added pessimistically. They are added for thisModule.name, name, and for
// any aliases.
function addRef(key, name) {
const s = dependencyMap[key] || (dependencyMap[key] = new Set());
if (name in internalToExternal) {
s.add(internalToExternal[name]);
}
s.add(currentModule + "." + name);
s.add(name); // For builtins like hack.
}
// If we discover a dependency identifier, state.key is the dependent identifier.
// walkDeeper is for doing recursive walks of expressions in composites that we handle.
function commonVisitors() {
return {
Identifier: (node, st, walkDeeper) => {
addRef(st.key, node.name);
},
WhileStatement: (node, st, walkDeeper) => {
addRef(st.key, specialReferenceWHILE);
node.test && walkDeeper(node.test, st);
node.body && walkDeeper(node.body, st);
},
DoWhileStatement: (node, st, walkDeeper) => {
addRef(st.key, specialReferenceWHILE);
node.test && walkDeeper(node.test, st);
node.body && walkDeeper(node.body, st);
},
ForStatement: (node, st, walkDeeper) => {
addRef(st.key, specialReferenceFOR);
node.init && walkDeeper(node.init, st);
node.test && walkDeeper(node.test, st);
node.update && walkDeeper(node.update, st);
node.body && walkDeeper(node.body, st);
},
IfStatement: (node, st, walkDeeper) => {
addRef(st.key, specialReferenceIF);
node.test && walkDeeper(node.test, st);
node.consequent && walkDeeper(node.consequent, st);
node.alternate && walkDeeper(node.alternate, st);
},
}
}
walk.recursive(ast, {key: globalKey}, {
ImportDeclaration: (node, st, walkDeeper) => {
const importModuleName = node.source.value;
additionalModules.push(importModuleName);
// This module's global scope refers to that module's global scope, no matter how we
// import it.
dependencyMap[st.key].add(importModuleName + memCheckGlobalKey);
for (let i = 0; i < node.specifiers.length; ++i) {
const spec = node.specifiers[i];
if (spec.imported !== undefined && spec.local !== undefined) {
// We depend on specific things.
internalToExternal[spec.local.name] = importModuleName + "." + spec.imported.name;
} else {
// We depend on everything.
dependencyMap[st.key].add(importModuleName + ".*");
}
}
},
FunctionDeclaration: (node, st, walkDeeper) => {
// Don't use walkDeeper, because we are changing the visitor set.
const key = currentModule + "." + node.id.name;
walk.recursive(node, {key: key}, commonVisitors());
},
...commonVisitors()
});
return {dependencyMap: dependencyMap, additionalModules: additionalModules};
}
function calculateRamUsage(codeCopy) {
//Create a temporary/mock WorkerScript and an AST from the code
var currServ = Player.getCurrentServer();
@ -359,6 +579,14 @@ function calculateRamUsage(codeCopy) {
workerScript.checkingRam = true; //Netscript functions will return RAM usage
workerScript.serverIp = currServ.ip;
try {
return parseOnlyRamCalculate(currServ, codeCopy, workerScript);
} catch (e) {
console.log("Failed to parse ram using new method. Falling back.", e);
}
// Try the old way.
try {
var ast = parse(codeCopy, {sourceType:"module"});
} catch(e) {
@ -712,4 +940,4 @@ AllServersMap.fromJSON = function(value) {
Reviver.constructors.AllServersMap = AllServersMap;
export {updateScriptEditorContent, loadAllRunningScripts, findRunningScript,
RunningScript, Script, AllServersMap, scriptEditorInit};
RunningScript, Script, AllServersMap, scriptEditorInit, isScriptFilename};

@ -22,7 +22,8 @@ import {killWorkerScript, addWorkerScript} from "./NetscriptWorker.js";
import {Player} from "./Player.js";
import {hackWorldDaemon} from "./RedPill.js";
import {findRunningScript, RunningScript,
AllServersMap, Script} from "./Script.js";
AllServersMap, Script,
isScriptFilename} from "./Script.js";
import {AllServers, GetServerByHostname,
getServer, Server} from "./Server.js";
import {Settings} from "./Settings.js";
@ -980,7 +981,7 @@ let Terminal = {
}
//Can only tail script files
if (scriptName.endsWith(".script") == false) {
if (isScriptFilename(scriptName) == false) {
post("Error: tail can only be called on .script files (filename must end with .script)"); return;
}
@ -1055,7 +1056,7 @@ let Terminal = {
FileSaver.saveAs(content, filename);
});
return;
} else if (fn.endsWith(".script")) {
} else if (isScriptFilename(fn)) {
//Download a single script
for (var i = 0; i < s.scripts.length; ++i) {
if (s.scripts[i].filename === fn) {
@ -1210,7 +1211,7 @@ let Terminal = {
var text = createFconf();
Engine.loadScriptEditorContent(filename, text);
return;
} else if (filename.endsWith(".script")) {
} else if (isScriptFilename(filename)) {
for (var i = 0; i < s.scripts.length; i++) {
if (filename == s.scripts[i].filename) {
Engine.loadScriptEditorContent(filename, s.scripts[i].code);
@ -1257,7 +1258,7 @@ let Terminal = {
return;
}
}
} else if (delTarget.endsWith(".script")) {
} else if (isScriptFilename(delTarget)) {
for (var i = 0; i < s.scripts.length; ++i) {
if (s.scripts[i].filename == delTarget) {
//Check that the script isnt currently running
@ -1303,8 +1304,8 @@ let Terminal = {
}
//Check if its a script or just a program/executable
if (executableName.indexOf(".script") == -1) {
//Not a script
if (isScriptFilename(executableName) == -1) {
// Not a script
Terminal.runProgram(executableName);
} else {
//Script
@ -1361,7 +1362,7 @@ let Terminal = {
return;
}
var scriptname = args[0];
if (!scriptname.endsWith(".lit") && !scriptname.endsWith(".script") &&
if (!scriptname.endsWith(".lit") && !isScriptFilename(scriptName) &&
!scriptname.endsWith(".txt")){
post("Error: scp only works for .script, .txt, and .lit files");
return;
@ -1480,7 +1481,7 @@ let Terminal = {
}
//Can only tail script files
if (scriptName.endsWith(".script") == false) {
if (isScriptFilename(scriptName) == false) {
post("Error: tail can only be called on .script files (filename must end with .script)"); return;
}