From aa7facd4ba23ded5b51dca2b2dc96b7e4b583ece Mon Sep 17 00:00:00 2001 From: David Walker Date: Thu, 27 Apr 2023 15:21:06 -0700 Subject: [PATCH] NETSCRIPT: Greatly speed up script launching, and remove the limitation unique args per script (#440) * Remove the limitation unique args per script * Internal changes to how runningScripts are stored on the server, to make common usage faster. --- doc/source/basicgameplay/scripts.rst | 32 +++-- doc/source/basicgameplay/terminal.rst | 14 +- markdown/bitburner.ns.exec.md | 2 +- markdown/bitburner.ns.isrunning.md | 2 +- markdown/bitburner.ns.kill_1.md | 6 +- markdown/bitburner.ns.md | 2 +- markdown/bitburner.ns.run.md | 2 +- markdown/bitburner.runoptions.md | 1 + .../bitburner.runoptions.preventduplicates.md | 13 ++ src/Achievements/Achievements.ts | 9 +- src/Hacknet/HacknetServer.ts | 16 +-- src/Netscript/Environment.ts | 3 +- src/Netscript/NetscriptHelpers.ts | 66 ++++++--- src/Netscript/WorkerScript.ts | 4 +- src/Netscript/killWorkerScript.ts | 60 +++----- src/NetscriptFunctions.ts | 134 ++++++++---------- src/NetscriptWorker.ts | 29 ++-- src/Paths/Directory.ts | 2 +- src/Script/RunningScript.ts | 5 + src/Script/ScriptHelpers.ts | 21 ++- src/ScriptEditor/NetscriptDefinitions.d.ts | 19 ++- src/Server/AllServers.ts | 18 +-- src/Server/BaseServer.ts | 72 +++++++--- src/Server/Server.ts | 7 +- src/Server/ServerHelpers.ts | 4 +- src/Server/ServerPurchases.ts | 13 +- src/Terminal/commands/check.ts | 11 +- src/Terminal/commands/kill.ts | 61 +++++--- src/Terminal/commands/killall.ts | 10 +- src/Terminal/commands/ps.ts | 28 ++-- src/Terminal/commands/runScript.ts | 5 - src/Terminal/commands/tail.ts | 82 ++++------- src/Terminal/commands/top.ts | 38 ++--- src/ui/ActiveScripts/ServerAccordions.tsx | 25 +++- .../ActiveScripts/WorkerScriptAccordion.tsx | 4 +- src/ui/GameRoot.tsx | 2 +- src/ui/React/LogBoxManager.tsx | 25 +--- src/utils/JSONReviver.ts | 2 +- src/utils/helpers/roundToTwo.ts | 4 +- src/utils/helpers/scriptKey.ts | 29 ++++ src/utils/v2APIBreak.ts | 2 +- test/jest/Netscript/RunScript.test.ts | 7 +- test/jest/Save.test.ts | 64 +++++---- test/jest/__snapshots__/Save.test.ts.snap | 111 ++++++++------- 44 files changed, 573 insertions(+), 493 deletions(-) create mode 100644 markdown/bitburner.runoptions.preventduplicates.md create mode 100644 src/utils/helpers/scriptKey.ts diff --git a/doc/source/basicgameplay/scripts.rst b/doc/source/basicgameplay/scripts.rst index df2b0ac70..bc5392b7e 100644 --- a/doc/source/basicgameplay/scripts.rst +++ b/doc/source/basicgameplay/scripts.rst @@ -31,8 +31,16 @@ Many commands and functions act on an executing script be a way to specify which script you want those commands & functions to act on. -**A script that is being executed is uniquely identified by both its -name and the arguments that it was run with.** +The best way to identify a script is by its PID (Process IDentifier). This +unique number is returned from :js:func:`run`, :js:func:`exec`, etc., and also +shows in the output of "ps". + +A secondary way to identify scripts is by name **and** arguments. However (by +default) you can run a multiple copies of a script with the same arguments, so +this does not necessarily **uniquely** identify a script. In case of multiple +matches, most functions will return an arbitrary one (typically the first one +to be started). An exception is :js:func:`kill`, which will kill all the +matching scripts. The arguments must be an **exact** match. This means that both the order and type of the arguments matter. @@ -88,12 +96,12 @@ to the check command:: Shows the current server's RAM usage and availability -**kill [script] [args...]** +**kill [pid]** or **kill [script] [args...]** -Stops a script that is running with the specified script name and +Stops a script that is running with the specified PID, or script name and arguments. Arguments should be separated by a space. Remember that -scripts are uniquely identified by their arguments as well as -their name. For example, if you ran a script `foo.js` with +scripts are identified by their arguments as well as their name. +For example, if you ran a script `foo.js` with the argument 1 and 2, then just typing "`kill foo.js`" will not work. You have to use:: @@ -142,13 +150,13 @@ Run 'foo.js' with 50 threads and a single argument: [foodnstuff]:: $ run foo.js -t 50 foodnstuff -**tail [script] [args...]** +**tail [pid]** or **tail [script] [args...]** -Displays the logs of the script specified by the name and arguments. Note that scripts - are uniquely identified by their arguments as well as their name. For example, if you - ran a script 'foo.js' with the argument 'foodnstuff' then in order to 'tail' it you - must also add the 'foodnstuff' argument to the tail command as so: tail foo.js - foodnstuff +Displays the logs of the script specified by the PID or name and arguments. Note that + scripts are identified by their arguments as well as their name. For example, + if you ran a script 'foo.js' with the argument 'foodnstuff' then in order to + 'tail' it you must also add the 'foodnstuff' argument to the tail command as + so: tail foo.js foodnstuff **top** diff --git a/doc/source/basicgameplay/terminal.rst b/doc/source/basicgameplay/terminal.rst index 5a5297dda..12e2ad86e 100644 --- a/doc/source/basicgameplay/terminal.rst +++ b/doc/source/basicgameplay/terminal.rst @@ -318,7 +318,7 @@ to home and want to kill a script running on n00dles, you have to either use it' or :code:`connect` to n00dles first and then use the the kill command. If you are killing the script using its filename and arguments, then each argument -must be separated by a space. Remember that a running script is uniquely identified +must be separated by a space. Remember that a running script is identified by both its name and the arguments that are used to start it. So, if a script was ran with the following arguments:: @@ -328,6 +328,9 @@ Then to kill this script the same arguments would have to be used:: $ kill foo.js 50e3 sigma-cosmetics +If there are multiple copies of a script running with the same arguments, all +of them will be killed. + If you are killing the script using its PID, then the PID argument must be numeric. killall @@ -543,11 +546,13 @@ Prints whether or not you have root access to the current server. tail ^^^^ + $ tail [pid] + or $ tail [script name] [args...] -Displays dynamic logs for the script specified by the script name and arguments. +Displays dynamic logs for the script specified by PID or the script name and arguments. Each argument must be separated by a space. Remember that a running script is -uniquely identified by both its name and the arguments that were used to run +identified by both its name and the arguments that were used to run it. So, if a script was ran with the following arguments:: $ run foo.js 10 50000 @@ -642,3 +647,6 @@ with a semicolon (;). Example:: $ run foo.js; tail foo.js + +This does *not* wait for commands with a delay to finish executing, so it +generally doesn't work with things like :code:`hack`, :code:`wget`, etc. diff --git a/markdown/bitburner.ns.exec.md b/markdown/bitburner.ns.exec.md index 3b8582779..b999d658c 100644 --- a/markdown/bitburner.ns.exec.md +++ b/markdown/bitburner.ns.exec.md @@ -24,7 +24,7 @@ exec( | script | string | Filename of script to execute. | | hostname | string | Hostname of the target server on which to execute the script. | | threadOrOptions | number \| [RunOptions](./bitburner.runoptions.md) | _(Optional)_ Either an integer number of threads for new script, or a [RunOptions](./bitburner.runoptions.md) object. Threads defaults to 1. | -| args | (string \| number \| boolean)\[\] | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument numThreads must be filled in with a value. | +| args | (string \| number \| boolean)\[\] | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument threadOrOptions must be filled in with a value. | **Returns:** diff --git a/markdown/bitburner.ns.isrunning.md b/markdown/bitburner.ns.isrunning.md index 7532583f3..698345bb6 100644 --- a/markdown/bitburner.ns.isrunning.md +++ b/markdown/bitburner.ns.isrunning.md @@ -30,7 +30,7 @@ True if the specified script is running on the target server, and false otherwis RAM cost: 0.1 GB -Returns a boolean indicating whether the specified script is running on the target server. If you use a PID instead of a filename, the hostname and args parameters are unnecessary. Remember that a script is uniquely identified by both its name and its arguments. +Returns a boolean indicating whether the specified script is running on the target server. If you use a PID instead of a filename, the hostname and args parameters are unnecessary. Remember that a script is semi-uniquely identified by both its name and its arguments. (You can run multiple copies of scripts with the same arguments, but for the purposes of functions like this that check based on filename, the filename plus arguments forms the key.) ## Example diff --git a/markdown/bitburner.ns.kill_1.md b/markdown/bitburner.ns.kill_1.md index 16626b38d..3ef5175e0 100644 --- a/markdown/bitburner.ns.kill_1.md +++ b/markdown/bitburner.ns.kill_1.md @@ -4,7 +4,7 @@ ## NS.kill() method -Terminate the script with the provided filename, hostname, and script arguments. +Terminate the script(s) with the provided filename, hostname, and script arguments. **Signature:** @@ -24,13 +24,13 @@ kill(filename: string, hostname?: string, ...args: ScriptArg[]): boolean; boolean -True if the script is successfully killed, and false otherwise. +True if the scripts were successfully killed, and false otherwise. ## Remarks RAM cost: 0.5 GB -Kills the script with the provided filename, running on the specified host with the specified args. To instead kill a script using its PID, see [the other ns.kill entry](./bitburner.ns.kill.md). +Kills the script(s) with the provided filename, running on the specified host with the specified args. To instead kill a script using its PID, see [the other ns.kill entry](./bitburner.ns.kill.md). ## Example diff --git a/markdown/bitburner.ns.md b/markdown/bitburner.ns.md index 7de26f7bb..78a62bbb5 100644 --- a/markdown/bitburner.ns.md +++ b/markdown/bitburner.ns.md @@ -134,7 +134,7 @@ export async function main(ns) { | [isLogEnabled(fn)](./bitburner.ns.islogenabled.md) | Checks the status of the logging for the given function. | | [isRunning(script, host, args)](./bitburner.ns.isrunning.md) | Check if a script is running. | | [kill(pid)](./bitburner.ns.kill.md) | Terminate the script with the provided PID. | -| [kill(filename, hostname, args)](./bitburner.ns.kill_1.md) | Terminate the script with the provided filename, hostname, and script arguments. | +| [kill(filename, hostname, args)](./bitburner.ns.kill_1.md) | Terminate the script(s) with the provided filename, hostname, and script arguments. | | [killall(host, safetyguard)](./bitburner.ns.killall.md) | Terminate all scripts on a server. | | [ls(host, substring)](./bitburner.ns.ls.md) | List files on a server. | | [moveTail(x, y, pid)](./bitburner.ns.movetail.md) | Move a tail window. | diff --git a/markdown/bitburner.ns.run.md b/markdown/bitburner.ns.run.md index 0aa0a39fb..0f9ee9501 100644 --- a/markdown/bitburner.ns.run.md +++ b/markdown/bitburner.ns.run.md @@ -18,7 +18,7 @@ run(script: string, threadOrOptions?: number | RunOptions, ...args: (string | nu | --- | --- | --- | | script | string | Filename of script to run. | | threadOrOptions | number \| [RunOptions](./bitburner.runoptions.md) | _(Optional)_ Either an integer number of threads for new script, or a [RunOptions](./bitburner.runoptions.md) object. Threads defaults to 1. | -| args | (string \| number \| boolean)\[\] | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument numThreads must be filled in with a value. | +| args | (string \| number \| boolean)\[\] | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument threadOrOptions must be filled in with a value. | **Returns:** diff --git a/markdown/bitburner.runoptions.md b/markdown/bitburner.runoptions.md index da8ce57c9..dbe92e176 100644 --- a/markdown/bitburner.runoptions.md +++ b/markdown/bitburner.runoptions.md @@ -15,6 +15,7 @@ interface RunOptions | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [preventDuplicates?](./bitburner.runoptions.preventduplicates.md) | | boolean | _(Optional)_ Should we fail to run if another instance is running with the exact same arguments? This used to be the default behavior, now defaults to false. | | [ramOverride?](./bitburner.runoptions.ramoverride.md) | | number |

_(Optional)_ The RAM allocation to launch each thread of the script with.

Lowering this will not automatically let you get away with using less RAM: the dynamic RAM check enforces that all [NS](./bitburner.ns.md) functions actually called incur their cost. However, if you know that certain functions that are statically present (and thus included in the static RAM cost) will never be called in a particular circumstance, you can use this to avoid paying for them.

You can also use this to increase the RAM if the static RAM checker has missed functions that you need to call.

Must be greater-or-equal to the base RAM cost. Defaults to the statically calculated cost.

| | [temporary?](./bitburner.runoptions.temporary.md) | | boolean | _(Optional)_ Whether this script is excluded from saves, defaults to false | | [threads?](./bitburner.runoptions.threads.md) | | number | _(Optional)_ Number of threads that the script will run with, defaults to 1 | diff --git a/markdown/bitburner.runoptions.preventduplicates.md b/markdown/bitburner.runoptions.preventduplicates.md new file mode 100644 index 000000000..7132aa8d9 --- /dev/null +++ b/markdown/bitburner.runoptions.preventduplicates.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [bitburner](./bitburner.md) > [RunOptions](./bitburner.runoptions.md) > [preventDuplicates](./bitburner.runoptions.preventduplicates.md) + +## RunOptions.preventDuplicates property + +Should we fail to run if another instance is running with the exact same arguments? This used to be the default behavior, now defaults to false. + +**Signature:** + +```typescript +preventDuplicates?: boolean; +``` diff --git a/src/Achievements/Achievements.ts b/src/Achievements/Achievements.ts index 50b3279ac..945fda837 100644 --- a/src/Achievements/Achievements.ts +++ b/src/Achievements/Achievements.ts @@ -24,6 +24,7 @@ import { FactionNames } from "../Faction/data/FactionNames"; import { BlackOperationNames } from "../Bladeburner/data/BlackOperationNames"; import { isClassWork } from "../Work/ClassWork"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; +import { workerScripts } from "../Netscript/WorkerScripts"; import type { PlayerObject } from "../PersonObjects/Player/PlayerObject"; @@ -284,13 +285,7 @@ export const achievements: Record = { RUNNING_SCRIPTS_1000: { ...achievementData["RUNNING_SCRIPTS_1000"], Icon: "run1000", - Condition: (): boolean => { - let running = 0; - for (const s of GetAllServers()) { - running += s.runningScripts.length; - } - return running >= 1000; - }, + Condition: (): boolean => workerScripts.size >= 1000, }, DRAIN_SERVER: { ...achievementData["DRAIN_SERVER"], diff --git a/src/Hacknet/HacknetServer.ts b/src/Hacknet/HacknetServer.ts index 84b94d614..95aff0e9b 100644 --- a/src/Hacknet/HacknetServer.ts +++ b/src/Hacknet/HacknetServer.ts @@ -3,7 +3,6 @@ import { CONSTANTS } from "../Constants"; import { IHacknetNode } from "./IHacknetNode"; import { BaseServer } from "../Server/BaseServer"; -import { RunningScript } from "../Script/RunningScript"; import { HacknetServerConstants } from "./data/Constants"; import { calculateHashGainRate, @@ -15,7 +14,7 @@ import { import { createRandomIp } from "../utils/IPAddress"; -import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; +import { IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; import { Player } from "@player"; interface IConstructorParams { @@ -113,14 +112,6 @@ export class HacknetServer extends BaseServer implements IHacknetNode { return true; } - // Whenever a script is run, we must update this server's hash rate - runScript(script: RunningScript, prodMult?: number): void { - super.runScript(script); - if (prodMult != null && typeof prodMult === "number") { - this.updateHashRate(prodMult); - } - } - updateRamUsed(ram: number): void { super.updateRamUsed(ram); this.updateHashRate(Player.mults.hacknet_node_money); @@ -144,13 +135,14 @@ export class HacknetServer extends BaseServer implements IHacknetNode { // Serialize the current object to a JSON save state toJSON(): IReviverValue { - return Generic_toJSON("HacknetServer", this); + return this.toJSONBase("HacknetServer", includedKeys); } // Initializes a HacknetServer Object from a JSON save state static fromJSON(value: IReviverValue): HacknetServer { - return Generic_fromJSON(HacknetServer, value.data); + return BaseServer.fromJSONBase(value, HacknetServer, includedKeys); } } +const includedKeys = BaseServer.getIncludedKeys(HacknetServer); constructorsForReviver.HacknetServer = HacknetServer; diff --git a/src/Netscript/Environment.ts b/src/Netscript/Environment.ts index a33f1aed6..8d083156d 100644 --- a/src/Netscript/Environment.ts +++ b/src/Netscript/Environment.ts @@ -5,11 +5,10 @@ import { NSFull } from "../NetscriptFunctions"; * Netscript functions and arguments for that script. */ export class Environment { - /** Whether or not the script that uses this Environment should stop running */ + /** Whether or not the script that uses this Environment is stopped */ stopFlag = false; /** The currently running function */ - runningFn = ""; /** Environment variables (currently only Netscript functions) */ diff --git a/src/Netscript/NetscriptHelpers.ts b/src/Netscript/NetscriptHelpers.ts index 7220a8d72..7ab0eaf54 100644 --- a/src/Netscript/NetscriptHelpers.ts +++ b/src/Netscript/NetscriptHelpers.ts @@ -1,5 +1,6 @@ import { NetscriptContext } from "./APIWrapper"; import { WorkerScript } from "./WorkerScript"; +import { killWorkerScript } from "./killWorkerScript"; import { GetAllServers, GetServer } from "../Server/AllServers"; import { Player } from "@player"; import { ScriptDeath } from "./ScriptDeath"; @@ -26,7 +27,7 @@ import { GangMemberTask } from "../Gang/GangMemberTask"; import { RunningScript } from "../Script/RunningScript"; import { toNative } from "../NetscriptFunctions/toNative"; import { ScriptIdentifier } from "./ScriptIdentifier"; -import { findRunningScript, findRunningScriptByPid } from "../Script/ScriptHelpers"; +import { findRunningScripts, findRunningScriptByPid } from "../Script/ScriptHelpers"; import { arrayToString } from "../utils/helpers/arrayToString"; import { HacknetServer } from "../Hacknet/HacknetServer"; import { BaseServer } from "../Server/BaseServer"; @@ -35,6 +36,8 @@ import { checkEnum } from "../utils/helpers/enum"; import { RamCostConstants } from "./RamCostGenerator"; import { isPositiveInteger, PositiveInteger, Unknownify } from "../types"; import { Engine } from "../engine"; +import { resolveFilePath, FilePath } from "../Paths/FilePath"; +import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath"; export const helpers = { string, @@ -61,8 +64,10 @@ export const helpers = { gangMember, gangTask, log, + filePath, + scriptPath, getRunningScript, - getRunningScriptByArgs, + getRunningScriptsByArgs, getCannotFindRunningScriptErrorMessage, createPublicRunningScript, failOnHacknetServer, @@ -73,6 +78,7 @@ export interface CompleteRunOptions { threads: PositiveInteger; temporary: boolean; ramOverride?: number; + preventDuplicates: boolean; } export function assertMember( @@ -186,6 +192,7 @@ function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRun const result: CompleteRunOptions = { threads: 1 as PositiveInteger, temporary: false, + preventDuplicates: false, }; function checkThreads(threads: unknown, argName: string) { if (threads !== null && threads !== undefined) { @@ -200,6 +207,7 @@ function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRun const options = threadOrOption as Unknownify; checkThreads(options.threads, "RunOptions.threads"); result.temporary = !!options.temporary; + result.preventDuplicates = !!options.preventDuplicates; if (options.ramOverride !== undefined && options.ramOverride !== null) { result.ramOverride = number(ctx, "RunOptions.ramOverride", options.ramOverride); if (result.ramOverride < RamCostConstants.Base) { @@ -348,10 +356,8 @@ function checkEnvFlags(ctx: NetscriptContext): void { throw new ScriptDeath(ws); } if (ws.env.runningFn && ctx.function !== "asleep") { - ws.delayReject?.(new ScriptDeath(ws)); - ws.env.stopFlag = true; log(ctx, () => "Failed to run due to failed concurrency check."); - throw makeRuntimeErrorMsg( + const err = makeRuntimeErrorMsg( ctx, `Concurrent calls to Netscript functions are not allowed! Did you forget to await hack(), grow(), or some other @@ -359,6 +365,8 @@ function checkEnvFlags(ctx: NetscriptContext): void { Currently running: ${ws.env.runningFn} tried to run: ${ctx.function}`, "CONCURRENCY", ); + killWorkerScript(ws); + throw err; } } @@ -388,8 +396,7 @@ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void { ws.dynamicRamUsage = Math.min(ws.dynamicRamUsage + ramCost, RamCostConstants.Max); if (ws.dynamicRamUsage > 1.01 * ws.scriptRef.ramUsage) { log(ctx, () => "Insufficient static ram available."); - ws.env.stopFlag = true; - throw makeRuntimeErrorMsg( + const err = makeRuntimeErrorMsg( ctx, `Dynamic RAM usage calculated to be greater than RAM allocation. This is probably because you somehow circumvented the static RAM calculation. @@ -410,6 +417,8 @@ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void { Sorry :(`, "RAM USAGE", ); + killWorkerScript(ws); + throw err; } } @@ -651,22 +660,35 @@ function log(ctx: NetscriptContext, message: () => string) { ctx.workerScript.log(ctx.functionPath, message); } +export function filePath(ctx: NetscriptContext, argName: string, filename: unknown): FilePath { + assertString(ctx, argName, filename); + const path = resolveFilePath(filename, ctx.workerScript.name); + if (path) return path; + throw makeRuntimeErrorMsg(ctx, `Invalid ${argName}, was not a valid path: ${filename}`); +} + +export function scriptPath(ctx: NetscriptContext, argName: string, filename: unknown): ScriptFilePath { + const path = filePath(ctx, argName, filename); + if (hasScriptExtension(path)) return path; + throw makeRuntimeErrorMsg(ctx, `Invalid ${argName}, must be a script: ${filename}`); +} + /** - * Searches for and returns the RunningScript object for the specified script. + * Searches for and returns the RunningScript objects for the specified script. * If the 'fn' argument is not specified, this returns the current RunningScript. * @param fn - Filename of script * @param hostname - Hostname/ip of the server on which the script resides * @param scriptArgs - Running script's arguments - * @returns Running script identified by the parameters, or null if no such script - * exists, or the current running script if the first argument 'fn' + * @returns Running scripts identified by the parameters, or empty if no such script + * exists, or only the current running script if the first argument 'fn' * is not specified. */ -function getRunningScriptByArgs( +export function getRunningScriptsByArgs( ctx: NetscriptContext, fn: string, hostname: string, scriptArgs: ScriptArg[], -): RunningScript | null { +): Map | null { if (!Array.isArray(scriptArgs)) { throw helpers.makeRuntimeErrorMsg( ctx, @@ -675,18 +697,14 @@ function getRunningScriptByArgs( ); } - if (fn != null && typeof fn === "string") { - // Get Logs of another script - if (hostname == null) { - hostname = ctx.workerScript.hostname; - } - const server = helpers.getServer(ctx, hostname); - - return findRunningScript(fn, scriptArgs, server); + const path = scriptPath(ctx, "filename", fn); + // Lookup server to scope search + if (hostname == null) { + hostname = ctx.workerScript.hostname; } + const server = helpers.getServer(ctx, hostname); - // If no arguments are specified, return the current RunningScript - return ctx.workerScript.scriptRef; + return findRunningScripts(path, scriptArgs, server); } function getRunningScriptByPid(pid: number): RunningScript | null { @@ -701,7 +719,9 @@ function getRunningScript(ctx: NetscriptContext, ident: ScriptIdentifier): Runni if (typeof ident === "number") { return getRunningScriptByPid(ident); } else { - return getRunningScriptByArgs(ctx, ident.scriptname, ident.hostname, ident.args); + const scripts = getRunningScriptsByArgs(ctx, ident.scriptname, ident.hostname, ident.args); + if (scripts === null) return null; + return scripts.values().next().value; } } diff --git a/src/Netscript/WorkerScript.ts b/src/Netscript/WorkerScript.ts index 1bafc81ea..821f0f541 100644 --- a/src/Netscript/WorkerScript.ts +++ b/src/Netscript/WorkerScript.ts @@ -33,7 +33,7 @@ export class WorkerScript { delay: number | null = null; /** Holds the Promise reject() function while the script is "blocked" by an async op */ - delayReject?: (reason?: ScriptDeath) => void; + delayReject: ((reason?: ScriptDeath) => void) | undefined = undefined; /** Stores names of all functions that have logging disabled */ disableLogs: Record = {}; @@ -79,7 +79,7 @@ export class WorkerScript { hostname: string; /** Function called when the script ends. */ - atExit?: () => void; + atExit: (() => void) | undefined = undefined; constructor(runningScriptObj: RunningScript, pid: number, nsFuncsGenerator?: (ws: WorkerScript) => NSFull) { this.name = runningScriptObj.filename; diff --git a/src/Netscript/killWorkerScript.ts b/src/Netscript/killWorkerScript.ts index 9b1850054..354ed2912 100644 --- a/src/Netscript/killWorkerScript.ts +++ b/src/Netscript/killWorkerScript.ts @@ -7,47 +7,24 @@ import { WorkerScript } from "./WorkerScript"; import { workerScripts } from "./WorkerScripts"; import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter"; -import { RunningScript } from "../Script/RunningScript"; import { GetServer } from "../Server/AllServers"; - import { AddRecentScript } from "./RecentScripts"; import { ITutorial } from "../InteractiveTutorial"; import { AlertEvents } from "../ui/React/AlertManager"; import { handleUnknownError } from "./NetscriptHelpers"; +import { roundToTwo } from "../utils/helpers/roundToTwo"; -export type killScriptParams = WorkerScript | number | { runningScript: RunningScript; hostname: string }; - -export function killWorkerScript(params: killScriptParams): boolean { +export function killWorkerScript(ws: WorkerScript): boolean { if (ITutorial.isRunning) { AlertEvents.emit("Processes cannot be killed during the tutorial."); return false; } - if (params instanceof WorkerScript) { - stopAndCleanUpWorkerScript(params); + stopAndCleanUpWorkerScript(ws); - return true; - } else if (typeof params === "number") { - return killWorkerScriptByPid(params); - } else { - // Try to kill by PID - const res = killWorkerScriptByPid(params.runningScript.pid); - if (res) { - return res; - } - - // If for some reason that doesn't work, we'll try the old way - for (const ws of workerScripts.values()) { - if (ws.scriptRef === params.runningScript) { - stopAndCleanUpWorkerScript(ws); - return true; - } - } - - return false; - } + return true; } -function killWorkerScriptByPid(pid: number): boolean { +export function killWorkerScriptByPid(pid: number): boolean { const ws = workerScripts.get(pid); if (ws instanceof WorkerScript) { stopAndCleanUpWorkerScript(ws); @@ -58,6 +35,10 @@ function killWorkerScriptByPid(pid: number): boolean { } function stopAndCleanUpWorkerScript(ws: WorkerScript): void { + // Only clean up once. + // Important: Only this function can set stopFlag! + if (ws.env.stopFlag) return; + //Clean up any ongoing netscriptDelay if (ws.delay) clearTimeout(ws.delay); ws.delayReject?.(new ScriptDeath(ws)); @@ -65,7 +46,6 @@ function stopAndCleanUpWorkerScript(ws: WorkerScript): void { if (typeof ws.atExit === "function") { try { - ws.env.stopFlag = false; const atExit = ws.atExit; ws.atExit = undefined; atExit(); @@ -95,21 +75,21 @@ function removeWorkerScript(workerScript: WorkerScript): void { } // Delete the RunningScript object from that server - for (let i = 0; i < server.runningScripts.length; ++i) { - const runningScript = server.runningScripts[i]; - if (runningScript === workerScript.scriptRef) { - server.runningScripts.splice(i, 1); - break; + const rs = workerScript.scriptRef; + const byPid = server.runningScriptMap.get(rs.scriptKey); + if (!byPid) { + console.error(`Couldn't find runningScriptMap for key ${rs.scriptKey}`); + } else { + byPid.delete(workerScript.pid); + if (byPid.size === 0) { + server.runningScriptMap.delete(rs.scriptKey); } } - // Recalculate ram used on that server + // Update ram used. Reround to prevent accumulation of error. + server.updateRamUsed(roundToTwo(server.ramUsed - rs.ramUsage * rs.threads)); - server.updateRamUsed(0); - for (const rs of server.runningScripts) server.updateRamUsed(server.ramUsed + rs.ramUsage * rs.threads); - - // Delete script from global pool (workerScripts) after verifying it's the right script (PIDs reset on aug install) - if (workerScripts.get(workerScript.pid) === workerScript) workerScripts.delete(workerScript.pid); + workerScripts.delete(workerScript.pid); AddRecentScript(workerScript); WorkerScriptStartStopEventEmitter.emit(); } diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts index b98f01727..f3e5d11ed 100644 --- a/src/NetscriptFunctions.ts +++ b/src/NetscriptFunctions.ts @@ -1,6 +1,5 @@ import $ from "jquery"; import { vsprintf, sprintf } from "sprintf-js"; -import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter"; import { BitNodeMultipliers, IBitNodeMultipliers } from "./BitNode/BitNodeMultipliers"; import { CONSTANTS } from "./Constants"; import { @@ -35,7 +34,7 @@ import { import { Server } from "./Server/Server"; import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing"; import { runScriptFromScript } from "./NetscriptWorker"; -import { killWorkerScript } from "./Netscript/killWorkerScript"; +import { killWorkerScript, killWorkerScriptByPid } from "./Netscript/killWorkerScript"; import { workerScripts } from "./Netscript/WorkerScripts"; import { WorkerScript } from "./Netscript/WorkerScript"; import { helpers, assertObjectType } from "./Netscript/NetscriptHelpers"; @@ -71,6 +70,7 @@ import { NetscriptSingularity } from "./NetscriptFunctions/Singularity"; import { dialogBoxCreate } from "./ui/React/DialogBox"; import { SnackbarEvents, ToastVariant } from "./ui/React/Snackbar"; import { checkEnum } from "./utils/helpers/enum"; +import { matchScriptPathExact } from "./utils/helpers/scriptKey"; import { Flags } from "./NetscriptFunctions/Flags"; import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence"; @@ -82,12 +82,12 @@ import { ScriptDeath } from "./Netscript/ScriptDeath"; import { getBitNodeMultipliers } from "./BitNode/BitNode"; import { assert, arrayAssert, stringAssert, objectAssert } from "./utils/helpers/typeAssertion"; import { CityName, JobName, CrimeType, GymType, LocationName, UniversityClassType } from "./Enums"; -import { cloneDeep } from "lodash"; +import { cloneDeep, escapeRegExp } from "lodash"; import { FactionWorkType } from "./Enums"; import numeral from "numeral"; import { clearPort, peekPort, portHandle, readPort, tryWritePort, writePort } from "./NetscriptPort"; -import { FilePath, resolveFilePath } from "./Paths/FilePath"; -import { hasScriptExtension, resolveScriptFilePath } from "./Paths/ScriptFilePath"; +import { FilePath } from "./Paths/FilePath"; +import { hasScriptExtension } from "./Paths/ScriptFilePath"; import { hasTextExtension } from "./Paths/TextFilePath"; import { ContentFilePath } from "./Paths/ContentFile"; import { LiteratureName } from "./Literature/data/LiteratureNames"; @@ -691,8 +691,7 @@ export const ns: InternalAPI = { run: (ctx) => (_scriptname, _thread_or_opt = 1, ..._args) => { - const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); - if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`); + const path = helpers.scriptPath(ctx, "scriptname", _scriptname); const runOpts = helpers.runOptions(ctx, _thread_or_opt); const args = helpers.scriptArgs(ctx, _args); const scriptServer = ctx.workerScript.getServer(); @@ -702,8 +701,7 @@ export const ns: InternalAPI = { exec: (ctx) => (_scriptname, _hostname, _thread_or_opt = 1, ..._args) => { - const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); - if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`); + const path = helpers.scriptPath(ctx, "scriptname", _scriptname); const hostname = helpers.string(ctx, "hostname", _hostname); const runOpts = helpers.runOptions(ctx, _thread_or_opt); const args = helpers.scriptArgs(ctx, _args); @@ -713,8 +711,7 @@ export const ns: InternalAPI = { spawn: (ctx) => (_scriptname, _thread_or_opt = 1, ..._args) => { - const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); - if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`); + const path = helpers.scriptPath(ctx, "scriptname", _scriptname); const runOpts = helpers.runOptions(ctx, _thread_or_opt); const args = helpers.scriptArgs(ctx, _args); const spawnDelay = 10; @@ -741,21 +738,23 @@ export const ns: InternalAPI = { const killByPid = typeof ident === "number"; if (killByPid) { // Kill by pid - res = killWorkerScript(ident); + res = killWorkerScriptByPid(ident); } else { // Kill by filename/hostname if (scriptID === undefined) { throw helpers.makeRuntimeErrorMsg(ctx, "Usage: kill(scriptname, server, [arg1], [arg2]...)"); } - const server = helpers.getServer(ctx, ident.hostname); - const runningScriptObj = helpers.getRunningScriptByArgs(ctx, ident.scriptname, ident.hostname, ident.args); - if (runningScriptObj == null) { + const byPid = helpers.getRunningScriptsByArgs(ctx, ident.scriptname, ident.hostname, ident.args); + if (byPid === null) { helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(ident)); return false; } - res = killWorkerScript({ runningScript: runningScriptObj, hostname: server.hostname }); + res = true; + for (const pid of byPid.keys()) { + res &&= killWorkerScriptByPid(pid); + } } if (res) { @@ -771,7 +770,7 @@ export const ns: InternalAPI = { } else { helpers.log( ctx, - () => `No such script '${scriptID}' on '${hostname}' with args: ${arrayToString(scriptArgs)}`, + () => `Internal error killing '${scriptID}' on '${hostname}' with args: ${arrayToString(scriptArgs)}`, ); } return false; @@ -786,12 +785,13 @@ export const ns: InternalAPI = { let scriptsKilled = 0; - for (let i = server.runningScripts.length - 1; i >= 0; --i) { - if (safetyguard === true && server.runningScripts[i].pid == ctx.workerScript.pid) continue; - killWorkerScript({ runningScript: server.runningScripts[i], hostname: server.hostname }); - ++scriptsKilled; + for (const byPid of server.runningScriptMap.values()) { + for (const pid of byPid.keys()) { + if (safetyguard === true && pid == ctx.workerScript.pid) continue; + killWorkerScriptByPid(pid); + ++scriptsKilled; + } } - WorkerScriptStartStopEventEmitter.emit(); helpers.log( ctx, () => `Killing all scripts on '${server.hostname}'. May take a few minutes for the scripts to die.`, @@ -810,28 +810,19 @@ export const ns: InternalAPI = { const destServer = helpers.getServer(ctx, destination); const sourceServer = helpers.getServer(ctx, source); const files = Array.isArray(_files) ? _files : [_files]; - const lits: (FilePath & LiteratureName)[] = []; + const lits: FilePath[] = []; const contentFiles: ContentFilePath[] = []; //First loop through filenames to find all errors before moving anything. for (const file of files) { - // Not a string - if (typeof file !== "string") { - throw helpers.makeRuntimeErrorMsg(ctx, "files should be a string or an array of strings."); - } - if (hasScriptExtension(file) || hasTextExtension(file)) { - const path = resolveScriptFilePath(file, ctx.workerScript.name); - if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${file}`); + const path = helpers.filePath(ctx, "files", file); + if (hasScriptExtension(path) || hasTextExtension(path)) { contentFiles.push(path); continue; } - if (!file.endsWith(".lit")) { + if (!path.endsWith(".lit")) { throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files."); } - const sanitizedPath = resolveFilePath(file, ctx.workerScript.name); - if (!sanitizedPath || !checkEnum(LiteratureName, sanitizedPath)) { - throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${file}'`); - } - lits.push(sanitizedPath); + lits.push(path); } let noFailures = true; @@ -864,7 +855,8 @@ export const ns: InternalAPI = { continue; } - destServer.messages.push(litFilePath); + // It exists in sourceServer.messages, so it's a valid name. + destServer.messages.push(litFilePath as LiteratureName); helpers.log(ctx, () => `File '${litFilePath}' copied over to '${destServer?.hostname}'.`); continue; } @@ -898,14 +890,16 @@ export const ns: InternalAPI = { const hostname = helpers.string(ctx, "hostname", _hostname); const server = helpers.getServer(ctx, hostname); const processes: ProcessInfo[] = []; - for (const script of server.runningScripts) { - processes.push({ - filename: script.filename, - threads: script.threads, - args: script.args.slice(), - pid: script.pid, - temporary: script.temporary, - }); + for (const byPid of server.runningScriptMap.values()) { + for (const script of byPid.values()) { + processes.push({ + filename: script.filename, + threads: script.threads, + args: script.args.slice(), + pid: script.pid, + temporary: script.temporary, + }); + } } return processes; }, @@ -1267,7 +1261,7 @@ export const ns: InternalAPI = { } // Delete all scripts running on server - if (server.runningScripts.length > 0) { + if (server.runningScriptMap.size > 0) { helpers.log(ctx, () => `Cannot delete server '${hostname}' because it still has scripts running.`); return false; } @@ -1325,12 +1319,10 @@ export const ns: InternalAPI = { return writePort(portNumber, data); }, write: (ctx) => (_filename, _data, _mode) => { - const filepath = resolveFilePath(helpers.string(ctx, "filename", _filename), ctx.workerScript.name); + const filepath = helpers.filePath(ctx, "filename", _filename); const data = helpers.string(ctx, "data", _data ?? ""); const mode = helpers.string(ctx, "mode", _mode ?? "a"); - if (!filepath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filepath: ${filepath}`); - const server = helpers.getServer(ctx, ctx.workerScript.hostname); if (hasScriptExtension(filepath)) { @@ -1369,8 +1361,8 @@ export const ns: InternalAPI = { return readPort(portNumber); }, read: (ctx) => (_filename) => { - const path = resolveFilePath(helpers.string(ctx, "filename", _filename), ctx.workerScript.name); - if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) return ""; + const path = helpers.filePath(ctx, "filename", _filename); + if (!hasScriptExtension(path) && !hasTextExtension(path)) return ""; const server = ctx.workerScript.getServer(); return server.getContentFile(path)?.content ?? ""; }, @@ -1379,8 +1371,8 @@ export const ns: InternalAPI = { return peekPort(portNumber); }, clear: (ctx) => (_file) => { - const path = resolveFilePath(helpers.string(ctx, "file", _file), ctx.workerScript.name); - if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) { + const path = helpers.filePath(ctx, "file", _file); + if (!hasScriptExtension(path) && !hasTextExtension(path)) { throw helpers.makeRuntimeErrorMsg(ctx, `Invalid file path or extension: ${_file}`); } const server = ctx.workerScript.getServer(); @@ -1398,7 +1390,7 @@ export const ns: InternalAPI = { return portHandle(portNumber); }, rm: (ctx) => (_fn, _hostname) => { - const filepath = resolveFilePath(helpers.string(ctx, "fn", _fn), ctx.workerScript.name); + const filepath = helpers.filePath(ctx, "fn", _fn); const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname); const s = helpers.getServer(ctx, hostname); if (!filepath) { @@ -1414,34 +1406,30 @@ export const ns: InternalAPI = { return status.res; }, scriptRunning: (ctx) => (_scriptname, _hostname) => { - const scriptname = helpers.string(ctx, "scriptname", _scriptname); + const scriptname = helpers.scriptPath(ctx, "scriptname", _scriptname); const hostname = helpers.string(ctx, "hostname", _hostname); const server = helpers.getServer(ctx, hostname); - for (let i = 0; i < server.runningScripts.length; ++i) { - if (server.runningScripts[i].filename == scriptname) { - return true; - } - } - return false; + return server.isRunning(scriptname); }, scriptKill: (ctx) => (_scriptname, _hostname) => { - const scriptname = helpers.string(ctx, "scriptname", _scriptname); + const path = helpers.scriptPath(ctx, "scriptname", _scriptname); const hostname = helpers.string(ctx, "hostname", _hostname); const server = helpers.getServer(ctx, hostname); let suc = false; - for (let i = 0; i < server.runningScripts.length; i++) { - if (server.runningScripts[i].filename == scriptname) { - killWorkerScript({ runningScript: server.runningScripts[i], hostname: server.hostname }); - suc = true; - i--; + + const pattern = matchScriptPathExact(escapeRegExp(path)); + for (const [key, byPid] of server.runningScriptMap) { + if (!pattern.test(key)) continue; + suc = true; + for (const pid of byPid.keys()) { + killWorkerScriptByPid(pid); } } return suc; }, getScriptName: (ctx) => () => ctx.workerScript.name, getScriptRam: (ctx) => (_scriptname, _hostname) => { - const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); - if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `Could not parse file path ${_scriptname}`); + const path = helpers.scriptPath(ctx, "scriptname", _scriptname); const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname); const server = helpers.getServer(ctx, hostname); const script = server.scripts.get(path); @@ -1640,7 +1628,7 @@ export const ns: InternalAPI = { }, wget: (ctx) => async (_url, _target, _hostname) => { const url = helpers.string(ctx, "url", _url); - const target = resolveFilePath(helpers.string(ctx, "target", _target), ctx.workerScript.name); + const target = helpers.scriptPath(ctx, "target", _target); const hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname; const server = helpers.getServer(ctx, hostname); if (!target || (!hasTextExtension(target) && !hasScriptExtension(target))) { @@ -1725,10 +1713,8 @@ export const ns: InternalAPI = { mv: (ctx) => (_host, _source, _destination) => { const hostname = helpers.string(ctx, "host", _host); const server = helpers.getServer(ctx, hostname); - const sourcePath = resolveFilePath(helpers.string(ctx, "source", _source)); - if (!sourcePath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid source filename: '${_source}'`); - const destinationPath = resolveFilePath(helpers.string(ctx, "destination", _destination), sourcePath); - if (!destinationPath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid destination filename: '${destinationPath}'`); + const sourcePath = helpers.filePath(ctx, "source", _source); + const destinationPath = helpers.filePath(ctx, "destination", _destination); if ( (!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) || diff --git a/src/NetscriptWorker.ts b/src/NetscriptWorker.ts index 640169c63..12a9914c1 100644 --- a/src/NetscriptWorker.ts +++ b/src/NetscriptWorker.ts @@ -31,7 +31,7 @@ import { parse } from "acorn"; import { simple as walksimple } from "acorn-walk"; import { Terminal } from "./Terminal"; import { ScriptArg } from "@nsdefs"; -import { handleUnknownError, CompleteRunOptions } from "./Netscript/NetscriptHelpers"; +import { handleUnknownError, CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers"; import { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath"; import { root } from "./Paths/Directory"; @@ -39,14 +39,10 @@ export const NetscriptPorts: Map = new Map(); export function prestigeWorkerScripts(): void { for (const ws of workerScripts.values()) { - ws.env.stopFlag = true; killWorkerScript(ws); } NetscriptPorts.clear(); - - WorkerScriptStartStopEventEmitter.emit(); - workerScripts.clear(); } async function startNetscript2Script(workerScript: WorkerScript): Promise { @@ -360,18 +356,15 @@ export function loadAllRunningScripts(): void { // Reset each server's RAM usage to 0 server.ramUsed = 0; - if (skipScriptLoad) { + const rsList = server.savedScripts; + server.savedScripts = undefined; + if (skipScriptLoad || !rsList) { // Start game with no scripts - server.runningScripts.length = 0; continue; } - // Using backwards index iteration to avoid complications when removing elements during iteration. - for (let i = server.runningScripts.length - 1; i >= 0; i--) { - const runningScript = server.runningScripts[i]; - const success = createAndAddWorkerScript(runningScript, server); + for (const runningScript of rsList) { + startWorkerScript(runningScript, server); scriptCalculateOfflineProduction(runningScript); - // Remove the RunningScript if the WorkerScript failed to start. - if (!success) server.runningScripts.splice(i, 1); } } } @@ -392,7 +385,15 @@ export function runScriptFromScript( } // Check if script is already running on server and fail if it is. - if (host.getRunningScript(scriptname, args)) { + if ( + runOpts.preventDuplicates && + getRunningScriptsByArgs( + { workerScript, function: "runScriptFromScript", functionPath: "internal.runScriptFromScript" }, + scriptname, + host.hostname, + args, + ) !== null + ) { workerScript.log(caller, () => `'${scriptname}' is already running on '${host.hostname}'`); return 0; } diff --git a/src/Paths/Directory.ts b/src/Paths/Directory.ts index 0b579edce..c87f72310 100644 --- a/src/Paths/Directory.ts +++ b/src/Paths/Directory.ts @@ -1,6 +1,6 @@ import { allContentFiles } from "./ContentFile"; import type { BaseServer } from "../Server/BaseServer"; -import { FilePath } from "./FilePath"; +import type { FilePath } from "./FilePath"; /** The directory part of a BasicFilePath. Everything up to and including the last / * e.g. "file.js" => "", or "dir/file.js" => "dir/", or "../test.js" => "../" */ diff --git a/src/Script/RunningScript.ts b/src/Script/RunningScript.ts index 1ddf167c5..34cdbe91a 100644 --- a/src/Script/RunningScript.ts +++ b/src/Script/RunningScript.ts @@ -15,6 +15,7 @@ import { RamCostConstants } from "../Netscript/RamCostGenerator"; import { PositiveInteger } from "../types"; import { getKeyList } from "../utils/helpers/getKeyList"; import { ScriptFilePath } from "../Paths/ScriptFilePath"; +import { ScriptKey, scriptKey } from "../utils/helpers/scriptKey"; export class RunningScript { // Script arguments @@ -61,6 +62,9 @@ export class RunningScript { // hostname of the server on which this script is running server = ""; + // Cached key for ByArgs lookups + scriptKey: ScriptKey = ""; + // Number of threads that this script is running with threads = 1 as PositiveInteger; @@ -75,6 +79,7 @@ export class RunningScript { if (!ramUsage) throw new Error("Must provide a ramUsage for RunningScript initialization."); this.filename = script.filename; this.args = args; + this.scriptKey = scriptKey(this.filename, args); this.server = script.server; this.ramUsage = ramUsage; this.dependencies = script.dependencies; diff --git a/src/Script/ScriptHelpers.ts b/src/Script/ScriptHelpers.ts index eaee3cd99..b80760ba1 100644 --- a/src/Script/ScriptHelpers.ts +++ b/src/Script/ScriptHelpers.ts @@ -7,7 +7,9 @@ import { processSingleServerGrowth } from "../Server/ServerHelpers"; import { GetServer } from "../Server/AllServers"; import { formatPercent } from "../ui/formatNumber"; import { workerScripts } from "../Netscript/WorkerScripts"; -import { compareArrays } from "../utils/helpers/compareArrays"; +import { scriptKey } from "../utils/helpers/scriptKey"; + +import type { ScriptFilePath } from "../Paths/ScriptFilePath"; export function scriptCalculateOfflineProduction(runningScript: RunningScript): void { //The Player object stores the last update time from when we were online @@ -80,19 +82,14 @@ export function scriptCalculateOfflineProduction(runningScript: RunningScript): } } -//Returns a RunningScript object matching the filename and arguments on the -//designated server, and false otherwise -export function findRunningScript( - filename: string, +//Returns a RunningScript map containing scripts matching the filename and +//arguments on the designated server, or null if none were found +export function findRunningScripts( + path: ScriptFilePath, args: (string | number | boolean)[], server: BaseServer, -): RunningScript | null { - for (let i = 0; i < server.runningScripts.length; ++i) { - if (server.runningScripts[i].filename === filename && compareArrays(server.runningScripts[i].args, args)) { - return server.runningScripts[i]; - } - } - return null; +): Map | null { + return server.runningScriptMap.get(scriptKey(path, args)) ?? null; } //Returns a RunningScript object matching the pid on the diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts index e4db7bd00..727ff4f01 100644 --- a/src/ScriptEditor/NetscriptDefinitions.d.ts +++ b/src/ScriptEditor/NetscriptDefinitions.d.ts @@ -226,6 +226,11 @@ interface RunOptions { * Must be greater-or-equal to the base RAM cost. Defaults to the statically calculated cost. */ ramOverride?: number; + /** + * Should we fail to run if another instance is running with the exact same arguments? + * This used to be the default behavior, now defaults to false. + */ + preventDuplicates?: boolean; } /** @public */ @@ -5500,7 +5505,7 @@ export interface NS { * ``` * @param script - Filename of script to run. * @param threadOrOptions - Either an integer number of threads for new script, or a {@link RunOptions} object. Threads defaults to 1. - * @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument numThreads must be filled in with a value. + * @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the second argument threadOrOptions must be filled in with a value. * @returns Returns the PID of a successfully started script, and 0 otherwise. */ run(script: string, threadOrOptions?: number | RunOptions, ...args: (string | number | boolean)[]): number; @@ -5540,7 +5545,7 @@ export interface NS { * @param script - Filename of script to execute. * @param hostname - Hostname of the `target server` on which to execute the script. * @param threadOrOptions - Either an integer number of threads for new script, or a {@link RunOptions} object. Threads defaults to 1. - * @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument numThreads must be filled in with a value. + * @param args - Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument threadOrOptions must be filled in with a value. * @returns Returns the PID of a successfully started script, and 0 otherwise. */ exec( @@ -5595,11 +5600,11 @@ export interface NS { kill(pid: number): boolean; /** - * Terminate the script with the provided filename, hostname, and script arguments. + * Terminate the script(s) with the provided filename, hostname, and script arguments. * @remarks * RAM cost: 0.5 GB * - * Kills the script with the provided filename, running on the specified host with the specified args. + * Kills the script(s) with the provided filename, running on the specified host with the specified args. * To instead kill a script using its PID, see {@link NS.(kill:1) | the other ns.kill entry}. * * @example @@ -5616,7 +5621,7 @@ export interface NS { * @param filename - Filename of the script to kill. * @param hostname - Hostname where the script to kill is running. Defaults to the current server. * @param args - Arguments of the script to kill. - * @returns True if the script is successfully killed, and false otherwise. + * @returns True if the scripts were successfully killed, and false otherwise. */ kill(filename: string, hostname?: string, ...args: ScriptArg[]): boolean; @@ -5998,7 +6003,9 @@ export interface NS { * * Returns a boolean indicating whether the specified script is running on the target server. * If you use a PID instead of a filename, the hostname and args parameters are unnecessary. - * Remember that a script is uniquely identified by both its name and its arguments. + * Remember that a script is semi-uniquely identified by both its name and its arguments. + * (You can run multiple copies of scripts with the same arguments, but for the purposes of + * functions like this that check based on filename, the filename plus arguments forms the key.) * * @example * ```js diff --git a/src/Server/AllServers.ts b/src/Server/AllServers.ts index f79634a2d..506138b9f 100644 --- a/src/Server/AllServers.ts +++ b/src/Server/AllServers.ts @@ -10,10 +10,9 @@ import { getRandomInt } from "../utils/helpers/getRandomInt"; import { Reviver } from "../utils/JSONReviver"; import { SpecialServers } from "./data/SpecialServers"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; -import "../Script/RunningScript"; // For reviver side-effect - import { IPAddress, isIPAddress } from "../Types/strings"; -import type { RunningScript } from "../Script/RunningScript"; + +import "../Script/RunningScript"; // For reviver side-effect /** * Map of all Servers that exist in the game @@ -207,17 +206,6 @@ function excludeReplacer(key: string, value: any): any { return value; } -function scriptFilter(script: RunningScript): boolean { - return !script.temporary; -} - -function includeReplacer(key: string, value: any): any { - if (key === "runningScripts") { - return value.filter(scriptFilter); - } - return value; -} - export function saveAllServers(excludeRunningScripts = false): string { - return JSON.stringify(AllServers, excludeRunningScripts ? excludeReplacer : includeReplacer); + return JSON.stringify(AllServers, excludeRunningScripts ? excludeReplacer : undefined); } diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts index 0bb4c49b2..c1b1ef6cd 100644 --- a/src/Server/BaseServer.ts +++ b/src/Server/BaseServer.ts @@ -7,10 +7,10 @@ import { IReturnStatus } from "../types"; import { ScriptFilePath, hasScriptExtension } from "../Paths/ScriptFilePath"; import { TextFilePath, hasTextExtension } from "../Paths/TextFilePath"; +import { Generic_toJSON, Generic_fromJSON, IReviverValue } from "../utils/JSONReviver"; +import { matchScriptPathExact } from "../utils/helpers/scriptKey"; import { createRandomIp } from "../utils/IPAddress"; -import { compareArrays } from "../utils/helpers/compareArrays"; -import { ScriptArg } from "../Netscript/ScriptArg"; import { JSONMap } from "../Types/Jsonable"; import { IPAddress, ServerName } from "../Types/strings"; import { FilePath } from "../Paths/FilePath"; @@ -19,6 +19,10 @@ import { ProgramFilePath, hasProgramExtension } from "../Paths/ProgramFilePath"; import { MessageFilename } from "src/Message/MessageHelpers"; import { LiteratureName } from "src/Literature/data/LiteratureNames"; import { CompletedProgramName } from "src/Programs/Programs"; +import { getKeyList } from "../utils/helpers/getKeyList"; +import lodash from "lodash"; + +import type { ScriptKey } from "../utils/helpers/scriptKey"; interface IConstructorParams { adminRights?: boolean; @@ -64,7 +68,7 @@ export abstract class BaseServer implements IServer { maxRam = 0; // Message files AND Literature files on this Server - messages: (MessageFilename | LiteratureName | FilePath)[] = []; + messages: (MessageFilename | LiteratureName)[] = []; // Name of company/faction/etc. that this server belongs to. // Optional, not applicable to all Servers @@ -77,8 +81,12 @@ export abstract class BaseServer implements IServer { // RAM (GB) used. i.e. unavailable RAM ramUsed = 0; - // RunningScript files on this server - runningScripts: RunningScript[] = []; + // RunningScript files on this server. Keyed first by name/args, then by PID. + runningScriptMap: Map> = new Map(); + + // RunningScript files loaded from the savegame. Only stored here temporarily, + // this field is undef while the game is running. + savedScripts: RunningScript[] | undefined = undefined; // Script files on this Server scripts: JSONMap = new JSONMap(); @@ -138,23 +146,16 @@ export abstract class BaseServer implements IServer { return null; } - /** Find an actively running script on this server by filepath and args. */ - getRunningScript(path: ScriptFilePath, scriptArgs: ScriptArg[]): RunningScript | null { - for (const rs of this.runningScripts) { - if (rs.filename === path && compareArrays(rs.args, scriptArgs)) return rs; - } - return null; - } - /** Get a TextFile or Script depending on the input path type. */ getContentFile(path: ContentFilePath): ContentFile | null { return (hasTextExtension(path) ? this.textFiles.get(path) : this.scripts.get(path)) ?? null; } /** Returns boolean indicating whether the given script is running on this server */ - isRunning(fn: string): boolean { - for (const runningScriptObj of this.runningScripts) { - if (runningScriptObj.filename === fn) { + isRunning(path: ScriptFilePath): boolean { + const pattern = matchScriptPathExact(lodash.escapeRegExp(path)); + for (const k of this.runningScriptMap.keys()) { + if (pattern.test(k)) { return true; } } @@ -216,7 +217,12 @@ export abstract class BaseServer implements IServer { * be run. */ runScript(script: RunningScript): void { - this.runningScripts.push(script); + let byPid = this.runningScriptMap.get(script.scriptKey); + if (!byPid) { + byPid = new Map(); + this.runningScriptMap.set(script.scriptKey, byPid); + } + byPid.set(script.pid, script); } setMaxRam(ram: number): void { @@ -279,4 +285,36 @@ export abstract class BaseServer implements IServer { if (hasTextExtension(path)) return this.writeToTextFile(path, content); return this.writeToScriptFile(path, content); } + + // Serialize the current object to a JSON save state + // Called by subclasses, not stringify. + toJSONBase(ctorName: string, keys: readonly (keyof this)[]): IReviverValue { + // RunningScripts are stored as a simple array, both for backward compatibility, + // compactness, and ease of filtering them here. + const result = Generic_toJSON(ctorName, this, keys); + + const rsArray: RunningScript[] = []; + for (const byPid of this.runningScriptMap.values()) { + for (const rs of byPid.values()) { + if (!rs.temporary) { + rsArray.push(rs); + } + } + } + result.data.runningScripts = rsArray; + return result; + } + + // Initializes a Server Object from a JSON save state + // Called by subclasses, not Reviver. + static fromJSONBase(value: IReviverValue, ctor: new () => T, keys: readonly (keyof T)[]): T { + const result = Generic_fromJSON(ctor, value.data, keys); + result.savedScripts = value.data.runningScripts; + return result; + } + + // Customize a prune list for a subclass. + static getIncludedKeys(ctor: new () => T): readonly (keyof T)[] { + return getKeyList(ctor, { removedKeys: ["runningScriptMap", "savedScripts", "ramUsed"] }); + } } diff --git a/src/Server/Server.ts b/src/Server/Server.ts index 1296a32ec..94b90777b 100644 --- a/src/Server/Server.ts +++ b/src/Server/Server.ts @@ -5,7 +5,7 @@ import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { createRandomString } from "../utils/helpers/createRandomString"; import { createRandomIp } from "../utils/IPAddress"; -import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; +import { IReviverValue, constructorsForReviver } from "../utils/JSONReviver"; import { IPAddress, ServerName } from "../Types/strings"; export interface IConstructorParams { @@ -147,13 +147,14 @@ export class Server extends BaseServer { /** Serialize the current object to a JSON save state */ toJSON(): IReviverValue { - return Generic_toJSON("Server", this); + return this.toJSONBase("Server", includedKeys); } // Initializes a Server Object from a JSON save state static fromJSON(value: IReviverValue): Server { - return Generic_fromJSON(Server, value.data); + return BaseServer.fromJSONBase(value, Server, includedKeys); } } +const includedKeys = BaseServer.getIncludedKeys(Server); constructorsForReviver.Server = Server; diff --git a/src/Server/ServerHelpers.ts b/src/Server/ServerHelpers.ts index 0fac502d1..d202eab28 100644 --- a/src/Server/ServerHelpers.ts +++ b/src/Server/ServerHelpers.ts @@ -252,7 +252,6 @@ export function prestigeHomeComputer(homeComp: Server): void { const hasBitflume = homeComp.programs.includes(CompletedProgramName.bitFlume); homeComp.programs.length = 0; //Remove programs - homeComp.runningScripts = []; homeComp.serversOnNetwork = []; homeComp.isConnectedTo = true; homeComp.ramUsed = 0; @@ -263,6 +262,9 @@ export function prestigeHomeComputer(homeComp: Server): void { homeComp.messages.length = 0; //Remove .lit and .msg files homeComp.messages.push(LiteratureName.HackersStartingHandbook); + if (homeComp.runningScriptMap.size !== 0) { + throw new Error("All programs weren't already killed!"); + } } // Returns the i-th server on the specified server's network diff --git a/src/Server/ServerPurchases.ts b/src/Server/ServerPurchases.ts index ff045ab5f..0284f069d 100644 --- a/src/Server/ServerPurchases.ts +++ b/src/Server/ServerPurchases.ts @@ -11,6 +11,7 @@ import { Player } from "@player"; import { dialogBoxCreate } from "../ui/React/DialogBox"; import { isPowerOfTwo } from "../utils/helpers/isPowerOfTwo"; +import { WorkerScript } from "../Netscript/WorkerScript"; import { workerScripts } from "../Netscript/WorkerScripts"; // Returns the cost of purchasing a server with the given RAM @@ -72,12 +73,16 @@ export const renamePurchasedServer = (hostname: string, newName: string): void = const home = Player.getHomeComputer(); home.serversOnNetwork = replace(home.serversOnNetwork, hostname, newName); server.serversOnNetwork = replace(server.serversOnNetwork, hostname, newName); - server.runningScripts.forEach((r) => (r.server = newName)); + for (const byPid of server.runningScriptMap.values()) { + for (const r of byPid.values()) { + r.server = newName; + // Lookup can't fail. + const ws = workerScripts.get(r.pid) as WorkerScript; + ws.hostname = newName; + } + } server.scripts.forEach((r) => (r.server = newName)); server.hostname = newName; - workerScripts.forEach((w) => { - if (w.hostname === hostname) w.hostname = newName; - }); renameServer(hostname, newName); }; diff --git a/src/Terminal/commands/check.ts b/src/Terminal/commands/check.ts index 8019a93f0..0c9553f9c 100644 --- a/src/Terminal/commands/check.ts +++ b/src/Terminal/commands/check.ts @@ -1,6 +1,6 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { findRunningScript } from "../../Script/ScriptHelpers"; +import { findRunningScripts } from "../../Script/ScriptHelpers"; import { hasScriptExtension, validScriptExtensions } from "../../Paths/ScriptFilePath"; export function check(args: (string | number | boolean)[], server: BaseServer): void { @@ -16,8 +16,11 @@ export function check(args: (string | number | boolean)[], server: BaseServer): } // Check that the script is running on this machine - const runningScript = findRunningScript(scriptName, args.slice(1), server); - if (runningScript == null) return Terminal.error(`No script named ${scriptName} is running on the server`); - runningScript.displayLog(); + const runningScripts = findRunningScripts(scriptName, args.slice(1), server); + if (runningScripts === null) { + Terminal.error(`No script named ${scriptName} is running on the server`); + return; + } + runningScripts.values().next().value.displayLog(); } } diff --git a/src/Terminal/commands/kill.ts b/src/Terminal/commands/kill.ts index e2f38500c..ca20f5574 100644 --- a/src/Terminal/commands/kill.ts +++ b/src/Terminal/commands/kill.ts @@ -1,25 +1,50 @@ import { Terminal } from "../../Terminal"; -import { BaseServer } from "../../Server/BaseServer"; -import { killWorkerScript } from "../../Netscript/killWorkerScript"; +import { findRunningScripts } from "../../Script/ScriptHelpers"; +import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript"; import { hasScriptExtension } from "../../Paths/ScriptFilePath"; +import type { BaseServer } from "../../Server/BaseServer"; + export function kill(args: (string | number | boolean)[], server: BaseServer): void { - if (args.length < 1) { - return Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]..."); - } - if (typeof args[0] === "number") { - const pid = args[0]; - if (killWorkerScript(pid)) return Terminal.print(`Killing script with PID ${pid}`); - } - // Shift args doesn't need to be sliced to check runningScript args - const fileName = String(args.shift()); - const path = Terminal.getFilepath(fileName); - if (!path) return Terminal.error(`Could not parse filename: ${fileName}`); - if (!hasScriptExtension(path)) return Terminal.error(`${path} does not have a script file extension`); + try { + if (args.length < 1 || typeof args[0] === "boolean") { + Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]..."); + return; + } - const runningScript = server.getRunningScript(path, args); - if (runningScript == null) return Terminal.error("No such script is running. Nothing to kill"); + // Kill by PID + if (typeof args[0] === "number") { + const pid = args[0]; + const res = killWorkerScriptByPid(pid); + if (res) { + Terminal.print(`Killing script with PID ${pid}`); + } else { + Terminal.error(`Failed to kill script with PID ${pid}. No such script is running`); + } - killWorkerScript(runningScript.pid); - Terminal.print(`Killing ${path}`); + return; + } + + const path = Terminal.getFilepath(args[0]); + if (!path) return Terminal.error(`Invalid filename: ${args[0]}`); + if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Kill can only be used on scripts.`); + const runningScripts = findRunningScripts(path, args.slice(1), server); + if (runningScripts === null) { + Terminal.error("No such script is running. Nothing to kill"); + return; + } + let killed = 0; + for (const pid of runningScripts.keys()) { + killed++; + if (killed < 5) { + Terminal.print(`Killing ${path} with pid ${pid}`); + } + killWorkerScriptByPid(pid); + } + if (killed >= 5) { + Terminal.print(`... killed ${killed} instances total`); + } + } catch (e) { + Terminal.error(e + ""); + } } diff --git a/src/Terminal/commands/killall.ts b/src/Terminal/commands/killall.ts index e0df4afce..1ce2a69d0 100644 --- a/src/Terminal/commands/killall.ts +++ b/src/Terminal/commands/killall.ts @@ -1,10 +1,12 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { killWorkerScript } from "../../Netscript/killWorkerScript"; -import { WorkerScriptStartStopEventEmitter } from "../../Netscript/WorkerScriptStartStopEventEmitter"; +import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript"; export function killall(_args: (string | number | boolean)[], server: BaseServer): void { Terminal.print("Killing all running scripts"); - for (const runningScript of server.runningScripts) killWorkerScript(runningScript.pid); - WorkerScriptStartStopEventEmitter.emit(); + for (const byPid of server.runningScriptMap.values()) { + for (const runningScript of byPid.values()) { + killWorkerScriptByPid(runningScript.pid); + } + } } diff --git a/src/Terminal/commands/ps.ts b/src/Terminal/commands/ps.ts index a4e226bb7..f875cb6be 100644 --- a/src/Terminal/commands/ps.ts +++ b/src/Terminal/commands/ps.ts @@ -1,5 +1,6 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; +import { matchScriptPathUnanchored } from "../../utils/helpers/scriptKey"; import * as libarg from "arg"; export function ps(args: (string | number | boolean)[], server: BaseServer): void { @@ -17,26 +18,15 @@ export function ps(args: (string | number | boolean)[], server: BaseServer): voi Terminal.error("Incorrect usage of ps command. Usage: ps [-g, --grep pattern]"); return; } - const pattern = flags["--grep"]; - if (pattern) { - const re = new RegExp(pattern.toString()); - const matching = server.runningScripts.filter((x) => re.test(x.filename)); - for (let i = 0; i < matching.length; i++) { - const rsObj = matching[i]; - let res = `(PID - ${rsObj.pid}) ${rsObj.filename}`; - for (let j = 0; j < rsObj.args.length; ++j) { - res += " " + rsObj.args[j].toString(); - } - Terminal.print(res); - } + let pattern = flags["--grep"]; + if (!pattern) { + pattern = ".*"; // Match anything } - if (args.length === 0) { - for (let i = 0; i < server.runningScripts.length; i++) { - const rsObj = server.runningScripts[i]; - let res = `(PID - ${rsObj.pid}) ${rsObj.filename}`; - for (let j = 0; j < rsObj.args.length; ++j) { - res += " " + rsObj.args[j].toString(); - } + const re = matchScriptPathUnanchored(pattern); + for (const [k, byPid] of server.runningScriptMap) { + if (!re.test(k)) continue; + for (const rsObj of byPid.values()) { + const res = `(PID - ${rsObj.pid}) ${rsObj.filename} ${rsObj.args.join(" ")}`; Terminal.print(res); } } diff --git a/src/Terminal/commands/runScript.ts b/src/Terminal/commands/runScript.ts index eec62fcfc..6b3c7f31d 100644 --- a/src/Terminal/commands/runScript.ts +++ b/src/Terminal/commands/runScript.ts @@ -28,11 +28,6 @@ export function runScript(path: ScriptFilePath, commandArgs: (string | number | // Todo: Switch out arg for something with typescript support const args = flags["_"] as ScriptArg[]; - // Check if this script is already running - if (server.getRunningScript(path, args)) { - return Terminal.error("This script is already running with the same args."); - } - const singleRamUsage = script.getRamUsage(server.scripts); if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script."); diff --git a/src/Terminal/commands/tail.ts b/src/Terminal/commands/tail.ts index ebddb5531..9e667a790 100644 --- a/src/Terminal/commands/tail.ts +++ b/src/Terminal/commands/tail.ts @@ -1,66 +1,38 @@ import { Terminal } from "../../Terminal"; import { BaseServer } from "../../Server/BaseServer"; -import { findRunningScriptByPid } from "../../Script/ScriptHelpers"; -import { compareArrays } from "../../utils/helpers/compareArrays"; +import { findRunningScripts, findRunningScriptByPid } from "../../Script/ScriptHelpers"; import { LogBoxEvents } from "../../ui/React/LogBoxManager"; import { hasScriptExtension } from "../../Paths/ScriptFilePath"; export function tail(commandArray: (string | number | boolean)[], server: BaseServer): void { - if (commandArray.length < 1) { - return Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]..."); - } - if (typeof commandArray[0] === "number") { - const runningScript = findRunningScriptByPid(commandArray[0], server); - if (!runningScript) return Terminal.error(`No script with PID ${commandArray[0]} is running on the server`); - LogBoxEvents.emit(runningScript); - return; - } + try { + if (commandArray.length < 1) { + Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]..."); + } else if (typeof commandArray[0] === "string") { + const [rawName, ...args] = commandArray; + const path = Terminal.getFilepath(rawName); + if (!path) return Terminal.error(`Invalid filename: ${rawName}`); + if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Tail can only be used on scripts.`); - const path = Terminal.getFilepath(String(commandArray[0])); - if (!path) return Terminal.error(`Invalid file path: ${commandArray[0]}`); - if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Tail can only be used on scripts.`); + const candidates = findRunningScripts(path, args, server); - // Get script arguments - const args = []; - for (let i = 1; i < commandArray.length; ++i) { - args.push(commandArray[i]); - } - - // go over all the running scripts. If there's a perfect - // match, use it! - for (let i = 0; i < server.runningScripts.length; ++i) { - if (server.runningScripts[i].filename === path && compareArrays(server.runningScripts[i].args, args)) { - LogBoxEvents.emit(server.runningScripts[i]); - return; + // if there's no candidate then we just don't know. + if (candidates === null) { + Terminal.error(`No script named ${path} with args ${JSON.stringify(args)} is running on the server`); + return; + } + // Just use the first one (if there are multiple with the same + // arguments, they can't be distinguished except by pid). + LogBoxEvents.emit(candidates.values().next().value); + } else if (typeof commandArray[0] === "number") { + const runningScript = findRunningScriptByPid(commandArray[0], server); + if (runningScript == null) { + Terminal.error(`No script with PID ${commandArray[0]} is running on the server`); + return; + } + LogBoxEvents.emit(runningScript); } + } catch (e) { + Terminal.error(e + ""); } - - // Find all scripts that are potential candidates. - const candidates = []; - for (let i = 0; i < server.runningScripts.length; ++i) { - // only scripts that have more arguments (equal arguments is already caught) - if (server.runningScripts[i].args.length < args.length) continue; - // make a smaller copy of the args. - const args2 = server.runningScripts[i].args.slice(0, args.length); - if (server.runningScripts[i].filename === path && compareArrays(args2, args)) { - candidates.push(server.runningScripts[i]); - } - } - - // If there's only 1 possible choice, use that. - if (candidates.length === 1) { - LogBoxEvents.emit(candidates[0]); - return; - } - - // otherwise lists all possible conflicting choices. - if (candidates.length > 1) { - Terminal.error("Found several potential candidates:"); - for (const candidate of candidates) Terminal.error(`${candidate.filename} ${candidate.args.join(" ")}`); - Terminal.error("Script arguments need to be specified."); - return; - } - - // if there's no candidate then we just don't know. - Terminal.error(`No script named ${path} is running on the server`); } diff --git a/src/Terminal/commands/top.ts b/src/Terminal/commands/top.ts index 9975151f4..7c6a0263a 100644 --- a/src/Terminal/commands/top.ts +++ b/src/Terminal/commands/top.ts @@ -26,29 +26,29 @@ export function top(args: (string | number | boolean)[], server: BaseServer): vo Terminal.print(headers); - const currRunningScripts = server.runningScripts; + const currRunningScripts = server.runningScriptMap; // Iterate through scripts on current server - for (let i = 0; i < currRunningScripts.length; i++) { - const script = currRunningScripts[i]; + for (const byPid of currRunningScripts.values()) { + for (const script of byPid.values()) { + // Calculate name padding + const numSpacesScript = Math.max(0, scriptWidth - script.filename.length); + const spacesScript = " ".repeat(numSpacesScript); - // Calculate name padding - const numSpacesScript = Math.max(0, scriptWidth - script.filename.length); - const spacesScript = " ".repeat(numSpacesScript); + // Calculate PID padding + const numSpacesPid = Math.max(0, pidWidth - (script.pid + "").length); + const spacesPid = " ".repeat(numSpacesPid); - // Calculate PID padding - const numSpacesPid = Math.max(0, pidWidth - (script.pid + "").length); - const spacesPid = " ".repeat(numSpacesPid); + // Calculate thread padding + const numSpacesThread = Math.max(0, threadsWidth - (script.threads + "").length); + const spacesThread = " ".repeat(numSpacesThread); - // Calculate thread padding - const numSpacesThread = Math.max(0, threadsWidth - (script.threads + "").length); - const spacesThread = " ".repeat(numSpacesThread); + // Calculate and transform RAM usage + const ramUsage = formatRam(script.ramUsage * script.threads); - // Calculate and transform RAM usage - const ramUsage = formatRam(script.ramUsage * script.threads); - - const entry = [script.filename, spacesScript, script.pid, spacesPid, script.threads, spacesThread, ramUsage].join( - "", - ); - Terminal.print(entry); + const entry = [script.filename, spacesScript, script.pid, spacesPid, script.threads, spacesThread, ramUsage].join( + "", + ); + Terminal.print(entry); + } } } diff --git a/src/ui/ActiveScripts/ServerAccordions.tsx b/src/ui/ActiveScripts/ServerAccordions.tsx index b092e31c2..11820219b 100644 --- a/src/ui/ActiveScripts/ServerAccordions.tsx +++ b/src/ui/ActiveScripts/ServerAccordions.tsx @@ -17,6 +17,8 @@ import { Settings } from "../../Settings/Settings"; import { TablePaginationActionsAll } from "../React/TablePaginationActionsAll"; import SearchIcon from "@mui/icons-material/Search"; import { useRerender } from "../React/hooks"; +import { matchScriptPathUnanchored } from "../../utils/helpers/scriptKey"; +import lodash from "lodash"; // Map of server hostname -> all workerscripts on that server for all active scripts interface IServerData { @@ -36,7 +38,13 @@ export function ServerAccordions(props: IProps): React.ReactElement { const [filter, setFilter] = useState(""); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(Settings.ActiveScriptsServerPageSize); - const rerender = useRerender(); + const rerenderHook = useRerender(); + let scheduledRerender = false; + const rerender = () => { + if (scheduledRerender) return; + scheduledRerender = true; + requestAnimationFrame(rerenderHook); + }; const handleChangePage = (event: unknown, newPage: number): void => { setPage(newPage); @@ -73,11 +81,16 @@ export function ServerAccordions(props: IProps): React.ReactElement { if (data !== undefined) data.workerScripts.push(ws); } - const filtered = Object.values(serverToScriptMap).filter( - (data) => - data && - (data.server.hostname.includes(filter) || data.server.runningScripts.find((s) => s.filename.includes(filter))), - ); + // Match filter in the scriptname part of the key + const pattern = matchScriptPathUnanchored(lodash.escapeRegExp(filter)); + const filtered = Object.values(serverToScriptMap).filter((data) => { + if (!data) return false; + if (data.server.hostname.includes(filter)) return true; + for (const k of data.server.runningScriptMap.keys()) { + if (pattern.test(k)) return true; + } + return false; + }); useEffect(() => WorkerScriptStartStopEventEmitter.subscribe(rerender)); diff --git a/src/ui/ActiveScripts/WorkerScriptAccordion.tsx b/src/ui/ActiveScripts/WorkerScriptAccordion.tsx index 610f9e46f..ed0db7352 100644 --- a/src/ui/ActiveScripts/WorkerScriptAccordion.tsx +++ b/src/ui/ActiveScripts/WorkerScriptAccordion.tsx @@ -24,7 +24,7 @@ import Collapse from "@mui/material/Collapse"; import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandMore from "@mui/icons-material/ExpandMore"; -import { killWorkerScript } from "../../Netscript/killWorkerScript"; +import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript"; import { WorkerScript } from "../../Netscript/WorkerScript"; import { dialogBoxCreate } from "../React/DialogBox"; @@ -53,7 +53,7 @@ export function WorkerScriptAccordion(props: IProps): React.ReactElement { function logClickHandler(): void { LogBoxEvents.emit(scriptRef); } - const killScript = killWorkerScript.bind(null, { runningScript: scriptRef, hostname: scriptRef.server }); + const killScript = killWorkerScriptByPid.bind(null, scriptRef.pid); function killScriptClickHandler(): void { if (killScript()) dialogBoxCreate("Killing script"); diff --git a/src/ui/GameRoot.tsx b/src/ui/GameRoot.tsx index 3568b074b..3d8fb1211 100644 --- a/src/ui/GameRoot.tsx +++ b/src/ui/GameRoot.tsx @@ -170,7 +170,7 @@ export function GameRoot(): React.ReactElement { function killAllScripts(): void { for (const server of GetAllServers()) { - server.runningScripts = []; + server.runningScriptMap.clear(); } saveObject.saveGame(); setTimeout(() => htmlLocation.reload(), 2000); diff --git a/src/ui/React/LogBoxManager.tsx b/src/ui/React/LogBoxManager.tsx index b5a3b03b9..ce51b6a41 100644 --- a/src/ui/React/LogBoxManager.tsx +++ b/src/ui/React/LogBoxManager.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from "react"; import { EventEmitter } from "../../utils/EventEmitter"; import { RunningScript } from "../../Script/RunningScript"; -import { killWorkerScript } from "../../Netscript/killWorkerScript"; +import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; @@ -14,7 +14,7 @@ import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import { workerScripts } from "../../Netscript/WorkerScripts"; import { startWorkerScript } from "../../NetscriptWorker"; import { GetServer } from "../../Server/AllServers"; -import { findRunningScript } from "../../Script/ScriptHelpers"; +import { findRunningScriptByPid } from "../../Script/ScriptHelpers"; import { debounce } from "lodash"; import { Settings } from "../../Settings/Settings"; import { ANSIITypography } from "./ANSIITypography"; @@ -160,23 +160,6 @@ function LogWindow(props: IProps): React.ReactElement { setSize([size.width, size.height]); }; - // useEffect( - // () => - // WorkerScriptStartStopEventEmitter.subscribe(() => { - // setTimeout(() => { - // const server = GetServer(script.server); - // if (server === null) return; - // const existingScript = findRunningScript(script.filename, script.args, server); - // if (existingScript) { - // existingScript.logs = script.logs.concat(existingScript.logs) - // setScript(existingScript) - // } - // rerender(); - // }, 100) - // }), - // [], - // ); - const setPosition = ({ x, y }: LogBoxPositionData) => { const node = rootRef?.current; if (!node) return; @@ -213,13 +196,13 @@ function LogWindow(props: IProps): React.ReactElement { }, []); function kill(): void { - killWorkerScript({ runningScript: script, hostname: script.server }); + killWorkerScriptByPid(script.pid); } function run(): void { const server = GetServer(script.server); if (server === null) return; - const s = findRunningScript(script.filename, script.args, server); + const s = findRunningScriptByPid(script.pid, server); if (s === null) { const baseScript = server.scripts.get(script.filename); if (!baseScript) { diff --git a/src/utils/JSONReviver.ts b/src/utils/JSONReviver.ts index a81e7db0b..7c73c5394 100644 --- a/src/utils/JSONReviver.ts +++ b/src/utils/JSONReviver.ts @@ -88,7 +88,7 @@ export function Generic_fromJSON>( if (keys) { for (const key of keys) { const val = data[key]; - if (val) obj[key] = val; + if (val !== undefined) obj[key] = val; } return obj; } diff --git a/src/utils/helpers/roundToTwo.ts b/src/utils/helpers/roundToTwo.ts index be5d9ec56..a311d640a 100644 --- a/src/utils/helpers/roundToTwo.ts +++ b/src/utils/helpers/roundToTwo.ts @@ -3,7 +3,5 @@ * @param decimal A decimal value to trim to two places. */ export function roundToTwo(decimal: number): number { - const leftShift: number = Math.round(parseFloat(`${decimal}e+2`)); - - return +`${leftShift}e-2`; + return Math.round(decimal * 100) / 100; } diff --git a/src/utils/helpers/scriptKey.ts b/src/utils/helpers/scriptKey.ts new file mode 100644 index 000000000..16dd10efd --- /dev/null +++ b/src/utils/helpers/scriptKey.ts @@ -0,0 +1,29 @@ +import type { ScriptArg } from "../../Netscript/ScriptArg"; +import type { ScriptFilePath } from "../../Paths/ScriptFilePath"; + +// This needs to be high in the dependency graph, with few/no dependencies of +// its own, since many key modules depend on it. + +export type ScriptKey = string /*& { __type: "ScriptKey" }*/; + +// The key used to lookup worker scripts in their map. +export function scriptKey(path: ScriptFilePath, args: ScriptArg[]): ScriptKey { + // Asterisk is used as a delimiter because it' not a valid character in paths. + return (path + "*" + JSON.stringify(args)) as ScriptKey; +} + +// Returns a RegExp that can be used to find scripts with a path that fully +// matches "pattern" in the scriptKey. +export function matchScriptPathExact(pattern: string) { + // Must fully match pattern, starting at the beginning and ending with the + // asterisk delimiter, which can't appear in script paths. + return new RegExp("^" + pattern + "\\*"); +} + +// Returns a RegExp that can be used to find scripts with a path that +// matches "pattern" somewhere in the scriptKey. +export function matchScriptPathUnanchored(pattern: string) { + // Don't let the match extend into the arguments part (script paths can't + // include "["). + return matchScriptPathExact("[^[]*" + pattern + "[^[]*"); +} diff --git a/src/utils/v2APIBreak.ts b/src/utils/v2APIBreak.ts index 73dd75468..754419895 100644 --- a/src/utils/v2APIBreak.ts +++ b/src/utils/v2APIBreak.ts @@ -237,7 +237,7 @@ export const v2APIBreak = () => { openV2Modal(); for (const server of GetAllServers()) { - server.runningScripts = []; + server.runningScriptMap = new Map(); } saveObject.exportGame(); }; diff --git a/test/jest/Netscript/RunScript.test.ts b/test/jest/Netscript/RunScript.test.ts index c2436a259..67ed7305b 100644 --- a/test/jest/Netscript/RunScript.test.ts +++ b/test/jest/Netscript/RunScript.test.ts @@ -103,9 +103,12 @@ test.each([ alerted, new Promise((resolve) => { eventDelete = WorkerScriptStartStopEventEmitter.subscribe(() => { - if (!server.runningScripts.includes(runningScript)) { - resolve(null); + for (const byPid of server.runningScriptMap.values()) { + for (const rs of byPid.values()) { + if (rs === runningScript) return; + } } + resolve(null); }); }), ]); diff --git a/test/jest/Save.test.ts b/test/jest/Save.test.ts index b2b8a664b..14a6c0551 100644 --- a/test/jest/Save.test.ts +++ b/test/jest/Save.test.ts @@ -1,6 +1,9 @@ import "../../src/Player"; import { loadAllServers, saveAllServers } from "../../src/Server/AllServers"; +import { loadAllRunningScripts } from "../../src/NetscriptWorker"; + +jest.useFakeTimers(); // Direct tests of loading and saving. // Tests here should try to be comprehensive (cover as much stuff as possible) @@ -46,6 +49,7 @@ function loadStandardServers() { "pid": 3, "ramUsage": 1.6, "server": "home", + "scriptKey": "script.js*[]", "temporary": true, "dependencies": [ { @@ -71,6 +75,7 @@ function loadStandardServers() { "pid": 2, "ramUsage": 1.6, "server": "home", + "scriptKey": "script.js*[]", "dependencies": [ { "filename": "script.js", @@ -81,33 +86,39 @@ function loadStandardServers() { } } ], - "scripts": [ - { - "ctor": "Script", - "data": { - "code": "/** @param {NS} ns */\\nexport async function main(ns) {\\n return ns.asleep(1000000);\\n}", - "filename": "script.js", - "module": {}, - "dependencies": [ - { + "scripts": { + "ctor": "JSONMap", + "data": [ + [ + "script.js", + { + "ctor": "Script", + "data": { + "code": "/** @param {NS} ns */\\nexport async function main(ns) {\\n return ns.asleep(1000000);\\n}", "filename": "script.js", - "url": "blob:http://localhost/e0abfafd-2c73-42fc-9eea-288c03820c47", - "moduleSequenceNumber": 5 + "module": {}, + "dependencies": [ + { + "filename": "script.js", + "url": "blob:http://localhost/e0abfafd-2c73-42fc-9eea-288c03820c47", + "moduleSequenceNumber": 5 + } + ], + "ramUsage": 1.6, + "server": "home", + "moduleSequenceNumber": 5, + "ramUsageEntries": [ + { + "type": "misc", + "name": "baseCost", + "cost": 1.6 + } + ] } - ], - "ramUsage": 1.6, - "server": "home", - "moduleSequenceNumber": 5, - "ramUsageEntries": [ - { - "type": "misc", - "name": "baseCost", - "cost": 1.6 - } - ] - } - } - ], + } + ] + ] + }, "serversOnNetwork": [ "n00dles" ], @@ -131,12 +142,13 @@ function loadStandardServers() { } } }`); // Fix confused highlighting ` + loadAllRunningScripts(); } test("load/saveAllServers", () => { // Feed a JSON object through loadAllServers/saveAllServers. // The object is a pruned set of servers that was extracted from a real (dev) game. - + jest.setSystemTime(123456789000); loadStandardServers(); // Re-stringify with indenting for nicer diffs diff --git a/test/jest/__snapshots__/Save.test.ts.snap b/test/jest/__snapshots__/Save.test.ts.snap index 2a0db2f89..210f6d3af 100644 --- a/test/jest/__snapshots__/Save.test.ts.snap +++ b/test/jest/__snapshots__/Save.test.ts.snap @@ -21,37 +21,22 @@ exports[`load/saveAllServers 1`] = ` "programs": [ "NUKE.exe" ], - "ramUsed": 1.6, - "runningScripts": [ - { - "ctor": "RunningScript", - "data": { - "args": [], - "dataMap": {}, - "filename": "script.js", - "offlineExpGained": 0, - "offlineMoneyMade": 0, - "offlineRunningTime": 0.01, - "onlineExpGained": 0, - "onlineMoneyMade": 0, - "onlineRunningTime": 7.210000000000004, - "ramUsage": 1.6, - "server": "home", - "threads": 1, - "temporary": false - } - } - ], - "scripts": [ - { - "ctor": "Script", - "data": { - "code": "/** @param {NS} ns */\\\\nexport async function main(ns) {\\\\n return ns.asleep(1000000);\\\\n}", - "filename": "script.js", - "server": "home" - } - } - ], + "scripts": { + "ctor": "JSONMap", + "data": [ + [ + "script.js", + { + "ctor": "Script", + "data": { + "code": "/** @param {NS} ns */\\\\nexport async function main(ns) {\\\\n return ns.asleep(1000000);\\\\n}", + "filename": "script.js", + "server": "home" + } + } + ] + ] + }, "serversOnNetwork": [ "n00dles" ], @@ -72,7 +57,28 @@ exports[`load/saveAllServers 1`] = ` "numOpenPortsRequired": 5, "openPortCount": 0, "requiredHackingSkill": 1, - "serverGrowth": 1 + "serverGrowth": 1, + "runningScripts": [ + { + "ctor": "RunningScript", + "data": { + "args": [], + "dataMap": {}, + "filename": "script.js", + "offlineExpGained": 0, + "offlineMoneyMade": 0, + "offlineRunningTime": 123456789.01, + "onlineExpGained": 0, + "onlineMoneyMade": 0, + "onlineRunningTime": 7.210000000000004, + "ramUsage": 1.6, + "server": "home", + "scriptKey": "script.js*[]", + "threads": 1, + "temporary": false + } + } + ] } }, "n00dles": { @@ -90,8 +96,6 @@ exports[`load/saveAllServers 1`] = ` "messages": [], "organizationName": "Noodle Bar", "programs": [], - "ramUsed": 0, - "runningScripts": [], "scripts": { "ctor": "JSONMap", "data": [] @@ -116,7 +120,8 @@ exports[`load/saveAllServers 1`] = ` "numOpenPortsRequired": 0, "openPortCount": 0, "requiredHackingSkill": 1, - "serverGrowth": 3000 + "serverGrowth": 3000, + "runningScripts": [] } } }" @@ -143,18 +148,22 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = ` "programs": [ "NUKE.exe" ], - "ramUsed": 1.6, - "runningScripts": [], - "scripts": [ - { - "ctor": "Script", - "data": { - "code": "/** @param {NS} ns */\\\\nexport async function main(ns) {\\\\n return ns.asleep(1000000);\\\\n}", - "filename": "script.js", - "server": "home" - } - } - ], + "scripts": { + "ctor": "JSONMap", + "data": [ + [ + "script.js", + { + "ctor": "Script", + "data": { + "code": "/** @param {NS} ns */\\\\nexport async function main(ns) {\\\\n return ns.asleep(1000000);\\\\n}", + "filename": "script.js", + "server": "home" + } + } + ] + ] + }, "serversOnNetwork": [ "n00dles" ], @@ -175,7 +184,8 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = ` "numOpenPortsRequired": 5, "openPortCount": 0, "requiredHackingSkill": 1, - "serverGrowth": 1 + "serverGrowth": 1, + "runningScripts": [] } }, "n00dles": { @@ -193,8 +203,6 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = ` "messages": [], "organizationName": "Noodle Bar", "programs": [], - "ramUsed": 0, - "runningScripts": [], "scripts": { "ctor": "JSONMap", "data": [] @@ -219,7 +227,8 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = ` "numOpenPortsRequired": 0, "openPortCount": 0, "requiredHackingSkill": 1, - "serverGrowth": 3000 + "serverGrowth": 3000, + "runningScripts": [] } } }"