diff --git a/css/activescripts.scss b/css/activescripts.scss new file mode 100644 index 000000000..82b7ae7b9 --- /dev/null +++ b/css/activescripts.scss @@ -0,0 +1,119 @@ +@import "theme"; + +.active-scripts-list { + list-style-type: none; +} + +#active-scripts-container { + position: fixed; + padding-top: 10px; + + > p { + width: 70%; + margin: 6px; + padding: 4px; + } +} + +.active-scripts-server-header { + background-color: #444; + font-size: $defaultFontSize * 1.25; + color: #fff; + margin: 6px 6px 0 6px; + padding: 6px; + cursor: pointer; + width: 60%; + text-align: left; + border: none; + outline: none; +} + +.active-scripts-server-header.active, +.active-scripts-server-header:hover { + background-color: #555; +} + +.active-scripts-server-header.active:hover { + background-color: #666; +} + +.active-scripts-server-header:after { + content: '\02795'; /* "plus" sign (+) */ + font-size: $defaultFontSize * 0.8125; + color: #fff; + float: right; + margin-left: 5px; +} + +.active-scripts-server-header.active:after { + content: "\2796"; /* "minus" sign (-) */ + font-size: $defaultFontSize * 0.8125; + color: #fff; + float: right; + margin-left: 5px; +} + +.active-scripts-server-panel { + margin: 0 6px 6px 6px; + padding: 0 6px 6px 6px; + width: 55%; + margin-left: 5%; + display: none; +} + +.active-scripts-server-panel div, +.active-scripts-server-panel ul, +.active-scripts-server-panel ul > li { + background-color: #555; +} + +.active-scripts-script-header { + background-color: #555; + color: var(--my-font-color); + padding: 4px 25px 4px 10px; + cursor: pointer; + width: auto; + text-align: left; + border: none; + outline: none; + position: relative; + + &:after { + content: '\02795'; /* "plus" sign (+) */ + font-size: $defaultFontSize * 0.8125; + float: right; + margin-left: 5px; + color: transparent; + text-shadow: 0 0 0 var(--my-font-color); + position: absolute; + bottom: 4px; + } + + &.active:after { + content: "\2796"; /* "minus" sign (-) */ + } + + &:hover, + &.active:hover { + background-color: #666; + } + + &.active { + background-color: #555; + } +} + +.active-scripts-script-panel { + padding: 0 18px; + background-color: #555; + width: auto; + display: none; + margin-bottom: 6px; + + p, h2, ul, li { + background-color: #555; + width: auto; + color: #fff; + margin-left: 5%; + } +} diff --git a/css/menupages.scss b/css/menupages.scss index b0b45c723..2c94436de 100644 --- a/css/menupages.scss +++ b/css/menupages.scss @@ -18,126 +18,6 @@ position: fixed; } -/* Active scripts */ -.active-scripts-list { - list-style-type: none; -} - -#active-scripts-container { - position: fixed; - padding-top: 10px; -} - -#active-scripts-text, -#active-scripts-total-prod { - width: 70%; - margin: 6px; - padding: 4px; -} - -.active-scripts-server-header { - background-color: #444; - font-size: $defaultFontSize * 1.25; - color: #fff; - margin: 6px 6px 0 6px; - padding: 6px; - cursor: pointer; - width: 60%; - text-align: left; - border: none; - outline: none; -} - -.active-scripts-server-header.active, -.active-scripts-server-header:hover { - background-color: #555; -} - -.active-scripts-server-header.active:hover { - background-color: #666; -} - -.active-scripts-server-header:after { - content: '\02795'; /* "plus" sign (+) */ - font-size: $defaultFontSize * 0.8125; - color: #fff; - float: right; - margin-left: 5px; -} - -.active-scripts-server-header.active:after { - content: "\2796"; /* "minus" sign (-) */ - font-size: $defaultFontSize * 0.8125; - color: #fff; - float: right; - margin-left: 5px; -} - -.active-scripts-server-panel { - margin: 0 6px 6px 6px; - padding: 0 6px 6px 6px; - width: 55%; - margin-left: 5%; - display: none; -} - -.active-scripts-server-panel div, -.active-scripts-server-panel ul, -.active-scripts-server-panel ul > li { - background-color: #555; -} - -.active-scripts-script-header { - background-color: #555; - color: var(--my-font-color); - padding: 4px 25px 4px 10px; - cursor: pointer; - width: auto; - text-align: left; - border: none; - outline: none; - position: relative; - - &:after { - content: '\02795'; /* "plus" sign (+) */ - font-size: $defaultFontSize * 0.8125; - float: right; - margin-left: 5px; - color: transparent; - text-shadow: 0 0 0 var(--my-font-color); - position: absolute; - bottom: 4px; - } - - &.active:after { - content: "\2796"; /* "minus" sign (-) */ - } - - &:hover, - &.active:hover { - background-color: #666; - } - - &.active { - background-color: #555; - } -} - -.active-scripts-script-panel { - padding: 0 18px; - background-color: #555; - width: auto; - display: none; - margin-bottom: 6px; - - p, h2, ul, li { - background-color: #555; - width: auto; - color: #fff; - margin-left: 5%; - } -} - /* World */ #world-container { position: fixed; diff --git a/src/Netscript/WorkerScript.ts b/src/Netscript/WorkerScript.ts index 2549c06e0..6b99c43d5 100644 --- a/src/Netscript/WorkerScript.ts +++ b/src/Netscript/WorkerScript.ts @@ -32,6 +32,11 @@ export class WorkerScript { */ delay: number | null = null; + /** + * Holds the Promise resolve() function for when the script is "blocked" by an async op + */ + delayResolve?: () => void; + /** * Stores names of all functions that have logging disabled */ diff --git a/src/Netscript/WorkerScripts.ts b/src/Netscript/WorkerScripts.ts new file mode 100644 index 000000000..50515f413 --- /dev/null +++ b/src/Netscript/WorkerScripts.ts @@ -0,0 +1,6 @@ +/** + * Global pool of all active scripts (scripts that are currently running) + */ +import { WorkerScript } from "./WorkerScript"; + +export const workerScripts: WorkerScript[] = []; diff --git a/src/Netscript/killWorkerScript.ts b/src/Netscript/killWorkerScript.ts new file mode 100644 index 000000000..ee60576f8 --- /dev/null +++ b/src/Netscript/killWorkerScript.ts @@ -0,0 +1,123 @@ +/** + * Function that stops an active script (represented by a WorkerScript object) + * and removes it from the global pool of active scripts. + */ +import { WorkerScript } from "./WorkerScript"; +import { workerScripts } from "./WorkerScripts"; + +import { RunningScript } from "../Script/RunningScript"; +import { AllServers } from "../Server/AllServers"; + +import { compareArrays } from "../../utils/helpers/compareArrays"; +import { roundToTwo } from "../../utils/helpers/roundToTwo"; + +export function killWorkerScript(runningScriptObj: RunningScript, serverIp: string): boolean; +export function killWorkerScript(workerScript: WorkerScript): boolean; +export function killWorkerScript(script: RunningScript | WorkerScript, serverIp?: string): boolean { + if (script instanceof WorkerScript) { + script.env.stopFlag = true; + killNetscriptDelay(script); + removeWorkerScript(script); + + return true; + } else if (script instanceof RunningScript && typeof serverIp === "string") { + for (let i = 0; i < workerScripts.length; i++) { + if (workerScripts[i].name == script.filename && workerScripts[i].serverIp == serverIp && + compareArrays(workerScripts[i].args, script.args)) { + workerScripts[i].env.stopFlag = true; + killNetscriptDelay(workerScripts[i]); + removeWorkerScript(workerScripts[i]); + + return true; + } + } + + return false; + } else { + return false; + } +} + +/** + * Helper function that removes the script being killed from the global pool. + * Also handles other cleanup-time operations + * + * @param {WorkerScript | number} - Identifier for WorkerScript. Either the object itself, or + * its index in the global workerScripts array + */ +function removeWorkerScript(id: WorkerScript | number): void { + // Get a reference to the WorkerScript and its index in the global pool + let workerScript: WorkerScript; + let index: number | null = null; + + if (typeof id === "number") { + if (id < 0 || id >= workerScripts.length) { + console.error(`Too high of an index passed into removeWorkerScript(): ${id}`); + return; + } + + workerScript = workerScripts[id]; + index = id; + } else if (id instanceof WorkerScript) { + workerScript = id; + for (let i = 0; i < workerScripts.length; ++i) { + if (workerScripts[i] == id) { + index = i; + break; + } + } + + if (index == null) { + console.error(`Could not find WorkerScript in global pool:`); + console.error(workerScript); + } + } else { + console.error(`Invalid argument passed into removeWorkerScript(): ${id}`); + return; + } + + const ip = workerScript.serverIp; + const name = workerScript.name; + + // Get the server on which the script runs + const server = AllServers[ip]; + if (server == null) { + console.error(`Could not find server on which this script is running: ${ip}`); + return; + } + + // Recalculate ram used on that server + server.ramUsed = roundToTwo(server.ramUsed - workerScript.ramUsage); + if (server.ramUsed < 0) { + console.warn(`Server RAM usage went negative (if it's due to floating pt imprecision, it's okay): ${server.ramUsed}`); + server.ramUsed = 0; + } + + // Delete the RunningScript object from that server + for (let i = 0; i < server.runningScripts.length; ++i) { + const runningScript = server.runningScripts[i]; + if (runningScript.filename === name && compareArrays(runningScript.args, workerScript.args)) { + server.runningScripts.splice(i, 1); + break; + } + } + + // Delete script from global pool (workerScripts) + workerScripts.splice(index, 1); +} + +/** + * Helper function that interrupts a script's delay if it is in the middle of a + * timed, blocked operation (like hack(), sleep(), etc.). This allows scripts to + * be killed immediately even if they're in the middle of one of those long operations + */ +function killNetscriptDelay(workerScript: WorkerScript) { + if (workerScript instanceof WorkerScript) { + if (workerScript.delay) { + clearTimeout(workerScript.delay); + if (workerScript.delayResolve) { + workerScript.delayResolve(); + } + } + } +} diff --git a/src/NetscriptEvaluator.js b/src/NetscriptEvaluator.js index cad189a8e..323919ad4 100644 --- a/src/NetscriptEvaluator.js +++ b/src/NetscriptEvaluator.js @@ -1,21 +1,9 @@ -import { WorkerScript } from "./Netscript/WorkerScript"; -import { getServer } from "./Server/ServerHelpers"; - import { setTimeoutRef } from "./utils/SetTimeoutRef"; import { parse, Node } from "../utils/acorn"; import { isValidIPAddress } from "../utils/helpers/isValidIPAddress"; import { isString } from "../utils/helpers/isString"; -export function killNetscriptDelay(workerScript) { - if (workerScript instanceof WorkerScript) { - if (workerScript.delay) { - clearTimeout(workerScript.delay); - workerScript.delayResolve(); - } - } -} - export function netscriptDelay(time, workerScript) { return new Promise(function(resolve, reject) { workerScript.delay = setTimeoutRef(() => { diff --git a/src/NetscriptFunctions.js b/src/NetscriptFunctions.js index 308054333..e41ff542c 100644 --- a/src/NetscriptFunctions.js +++ b/src/NetscriptFunctions.js @@ -120,11 +120,11 @@ import { } from "./NetscriptBladeburner"; import * as nsGang from "./NetscriptGang"; import { - workerScripts, - killWorkerScript, NetscriptPorts, runScriptFromScript, } from "./NetscriptWorker"; +import { killWorkerScript } from "./Netscript/killWorkerScript"; +import { workerScripts } from "./Netscript/WorkerScripts"; import { makeRuntimeRejectMsg, netscriptDelay, diff --git a/src/NetscriptWorker.js b/src/NetscriptWorker.js index 9c519e8b1..e5548ee57 100644 --- a/src/NetscriptWorker.js +++ b/src/NetscriptWorker.js @@ -3,6 +3,7 @@ * that allows for scripts to run */ import { WorkerScript } from "./Netscript/WorkerScript"; +import { workerScripts } from "./Netscript/WorkerScripts"; import { addActiveScriptsItem, @@ -15,7 +16,6 @@ import { Interpreter } from "./JSInterpreter"; import { isScriptErrorMessage, makeRuntimeRejectMsg, - killNetscriptDelay } from "./NetscriptEvaluator"; import { NetscriptFunctions } from "./NetscriptFunctions"; import { executeJSScript } from "./NetscriptJSEvaluator"; @@ -29,6 +29,7 @@ import { } from "./Script/ScriptHelpers"; import { AllServers } from "./Server/AllServers"; import { Settings } from "./Settings/Settings"; +import { EventEmitter } from "./utils/EventEmitter"; import { setTimeoutRef } from "./utils/SetTimeoutRef"; import { generate } from "escodegen"; @@ -42,14 +43,15 @@ import { isString } from "../utils/StringHelperFunctions"; const walk = require("acorn/dist/walk"); -//Array containing all scripts that are running across all servers, to easily run them all -export const workerScripts = []; - +// Netscript Ports are instantiated here export const NetscriptPorts = []; for (var i = 0; i < CONSTANTS.NumNetscriptPorts; ++i) { NetscriptPorts.push(new NetscriptPort()); } +// WorkerScript-related event emitter. Used for the UI +export const WorkerScriptEventEmitter = new EventEmitter(); + export function prestigeWorkerScripts() { for (var i = 0; i < workerScripts.length; ++i) { deleteActiveScriptsItem(workerScripts[i]); @@ -415,46 +417,6 @@ function processNetscript1Imports(code, workerScript) { // Loop through workerScripts and run every script that is not currently running export function runScriptsLoop() { - let scriptDeleted = false; - - // Delete any scripts that finished or have been killed. Loop backwards bc removing items screws up indexing - for (let i = workerScripts.length - 1; i >= 0; i--) { - if (workerScripts[i].running == false && workerScripts[i].env.stopFlag == true) { - scriptDeleted = true; - // Delete script from the runningScripts array on its host serverIp - const ip = workerScripts[i].serverIp; - const name = workerScripts[i].name; - - // Recalculate ram used - AllServers[ip].ramUsed = 0; - for (let j = 0; j < workerScripts.length; j++) { - if (workerScripts[j].serverIp !== ip) { - continue; - } - if (j === i) { // not this one - continue; - } - AllServers[ip].ramUsed += workerScripts[j].ramUsage; - } - - // Delete script from Active Scripts - deleteActiveScriptsItem(workerScripts[i]); - - for (let j = 0; j < AllServers[ip].runningScripts.length; j++) { - if (AllServers[ip].runningScripts[j].filename == name && - compareArrays(AllServers[ip].runningScripts[j].args, workerScripts[i].args)) { - AllServers[ip].runningScripts.splice(j, 1); - break; - } - } - - // Delete script from workerScripts - workerScripts.splice(i, 1); - } - } - if (scriptDeleted) { updateActiveScriptsItems(); } // Force Update - - // Run any scripts that haven't been started for (let i = 0; i < workerScripts.length; i++) { // If it isn't running, start the script @@ -520,24 +482,6 @@ export function runScriptsLoop() { setTimeoutRef(runScriptsLoop, 3e3); } -/** - * Queues a script to be killed by setting its stop flag to true. This - * kills and timed/blocking Netscript functions (like hack(), sleep(), etc.) and - * prevents any further execution of Netscript functions. - * The runScriptsLoop() handles the actual deletion of the WorkerScript - */ -export function killWorkerScript(runningScriptObj, serverIp) { - for (var i = 0; i < workerScripts.length; i++) { - if (workerScripts[i].name == runningScriptObj.filename && workerScripts[i].serverIp == serverIp && - compareArrays(workerScripts[i].args, runningScriptObj.args)) { - workerScripts[i].env.stopFlag = true; - killNetscriptDelay(workerScripts[i]); - return true; - } - } - return false; -} - /** * Given a RunningScript object, queues that script to be run */ diff --git a/src/Terminal.js b/src/Terminal.js index b88832954..12e150742 100644 --- a/src/Terminal.js +++ b/src/Terminal.js @@ -53,7 +53,8 @@ import { import { showLiterature } from "./Literature"; import { Message } from "./Message/Message"; import { showMessage } from "./Message/MessageHelpers"; -import { killWorkerScript, addWorkerScript } from "./NetscriptWorker"; +import { addWorkerScript } from "./NetscriptWorker"; +import { killWorkerScript } from "./Netscript/killWorkerScript"; import { Player } from "./Player"; import { hackWorldDaemon } from "./RedPill"; import { RunningScript } from "./Script/RunningScript"; diff --git a/src/engine.jsx b/src/engine.jsx index 1e8e8731b..28518e377 100644 --- a/src/engine.jsx +++ b/src/engine.jsx @@ -1,3 +1,8 @@ +/** + * Game engine. Handles the main game loop as well as the main UI pages + * + * TODO: Separate UI functionality into its own component + */ import { convertTimeMsToTimeElapsedString, replaceAt diff --git a/src/engineStyle.js b/src/engineStyle.js index c297b37cc..f9895a77a 100644 --- a/src/engineStyle.js +++ b/src/engineStyle.js @@ -9,6 +9,7 @@ import "../css/characteroverview.scss"; import "../css/terminal.scss"; import "../css/scripteditor.scss"; import "../css/codemirror-overrides.scss"; +import "../css/activescripts.scss"; import "../css/hacknetnodes.scss"; import "../css/menupages.scss"; import "../css/augmentations.scss"; diff --git a/src/ui/ActiveScripts/Root.tsx b/src/ui/ActiveScripts/Root.tsx new file mode 100644 index 000000000..aecacb192 --- /dev/null +++ b/src/ui/ActiveScripts/Root.tsx @@ -0,0 +1,31 @@ +/** + * Root React Component for the "Active Scripts" UI page. This page displays + * and provides information about all of the player's scripts that are currently running + */ +import * as React from "react"; + +import { WorkerScript } from "../../Netscript/WorkerScript"; + +type IProps = { + workerScripts: WorkerScript[]; +} + +export class ActiveScriptsRoot extends React.Component { + constructor(props: IProps) { + super(props); + } + + render() { + return ( + <> +

+ This page displays a list of all of your scripts that are currently + running across every machine. It also provides information about each + script's production. The scripts are categorized by the hostname of + the servers on which they are running. +

+ + + ) + } +} diff --git a/src/ui/ActiveScripts/WorkerScriptAccordion.tsx b/src/ui/ActiveScripts/WorkerScriptAccordion.tsx new file mode 100644 index 000000000..117c2b045 --- /dev/null +++ b/src/ui/ActiveScripts/WorkerScriptAccordion.tsx @@ -0,0 +1,40 @@ +/** + * React Component for displaying a single WorkerScript's info as an + * Accordion element + */ +import * as React from "react"; + +import { Accordion } from "../React/Accordion"; + +import { WorkerScript } from "../../Netscript/WorkerScript"; + +import { arrayToString } from "../../../utils/helpers/arrayToString"; + +type IProps = { + workerScript: WorkerScript; +} + +export function WorkerScriptAccordion(props: IProps): React.ReactElement { + + + return ( + + + } + panelClass="active-scripts-script-panel" + panelContent={ + <> +

+ Threads: {props.workerScript.scriptRef.threads} +

+

+ Args: {arrayToString(props.workerScript.args)} +

+ + } + /> + ) +} diff --git a/src/ui/React/Accordion.tsx b/src/ui/React/Accordion.tsx index 5833f29f7..948039076 100644 --- a/src/ui/React/Accordion.tsx +++ b/src/ui/React/Accordion.tsx @@ -4,7 +4,9 @@ import * as React from "react"; type IProps = { + headerClass?: string; // Override default class headerContent: React.ReactElement; + panelClass?: string; // Override default class panelContent: React.ReactElement; panelInitiallyOpened?: boolean; } @@ -44,12 +46,21 @@ export class Accordion extends React.Component { } render() { + let className = "accordion-header"; + if (typeof this.props.headerClass === "string") { + className = this.props.headerClass; + } + return ( <> - - + ) } @@ -57,6 +68,7 @@ export class Accordion extends React.Component { type IPanelProps = { opened: boolean; + panelClass?: string; // Override default class panelContent: React.ReactElement; } @@ -66,8 +78,13 @@ class AccordionPanel extends React.Component { } render() { + let className = "accordion-panel" + if (typeof this.props.panelClass === "string") { + className = this.props.panelClass; + } + return ( -
+
{this.props.panelContent}
) diff --git a/src/utils/EventEmitter.ts b/src/utils/EventEmitter.ts new file mode 100644 index 000000000..b513d76d5 --- /dev/null +++ b/src/utils/EventEmitter.ts @@ -0,0 +1,50 @@ +/** + * Generic Event Emitter class following a subscribe/publish paradigm. + */ +import { IMap } from "../types"; + +type cbFn = (...args: any[]) => any; + +export interface ISubscriber { + /** + * Callback function that will be run when an event is emitted + */ + cb: cbFn; + + /** + * Name/identifier for this subscriber + */ + id: string; +} + +export class EventEmitter { + /** + * Map of Subscriber name -> Callback function + */ + subscribers: IMap = {}; + + constructor(subs?: ISubscriber[]) { + if (Array.isArray(subs)) { + for (const s of subs) { + this.addSubscriber(s); + } + } + } + + addSubscriber(s: ISubscriber) { + this.subscribers[s.id] = s.cb; + } + + emitEvent(...args: any[]): void { + for (const s in this.subscribers) { + const cb = this.subscribers[s]; + + cb(args); + } + } + + removeSubscriber(id: string) { + delete this.subscribers[id]; + } + +} diff --git a/utils/LogBox.js b/utils/LogBox.js index 615567610..cedbeb967 100644 --- a/utils/LogBox.js +++ b/utils/LogBox.js @@ -1,6 +1,6 @@ -import {killWorkerScript} from "../src/NetscriptWorker"; -import {clearEventListeners} from "./uiHelpers/clearEventListeners"; -import {arrayToString} from "./helpers/arrayToString"; +import { killWorkerScript } from "../src/Netscript/killWorkerScript"; +import { clearEventListeners } from "./uiHelpers/clearEventListeners"; +import { arrayToString } from "./helpers/arrayToString"; $(document).keydown(function(event) { if (logBoxOpened && event.keyCode == 27) {