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.
This commit is contained in:
David Walker 2023-04-27 15:21:06 -07:00 committed by GitHub
parent f81297dcd6
commit aa7facd4ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 573 additions and 493 deletions

@ -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**

@ -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.

@ -24,7 +24,7 @@ exec(
| script | string | Filename of script to execute. |
| hostname | string | Hostname of the <code>target server</code> 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:**

@ -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

@ -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

@ -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. |

@ -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:**

@ -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 | <p>_(Optional)_ The RAM allocation to launch each thread of the script with.</p><p>Lowering this will <i>not</i> 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.</p><p>You can also use this to <i>increase</i> the RAM if the static RAM checker has missed functions that you need to call.</p><p>Must be greater-or-equal to the base RAM cost. Defaults to the statically calculated cost.</p> |
| [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 |

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [RunOptions](./bitburner.runoptions.md) &gt; [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;
```

@ -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<string, Achievement> = {
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"],

@ -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;

@ -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) */

@ -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<T extends string>(
@ -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<CompleteRunOptions>;
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<number, RunningScript> | 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
const path = scriptPath(ctx, "filename", fn);
// Lookup server to scope search
if (hostname == null) {
hostname = ctx.workerScript.hostname;
}
const server = helpers.getServer(ctx, hostname);
return findRunningScript(fn, scriptArgs, server);
}
// 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;
}
}

@ -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<string, boolean> = {};
@ -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;

@ -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);
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();
}

@ -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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
} 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<NSFull> = {
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 });
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<NSFull> = {
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<NSFull> = {
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,7 +890,8 @@ export const ns: InternalAPI<NSFull> = {
const hostname = helpers.string(ctx, "hostname", _hostname);
const server = helpers.getServer(ctx, hostname);
const processes: ProcessInfo[] = [];
for (const script of server.runningScripts) {
for (const byPid of server.runningScriptMap.values()) {
for (const script of byPid.values()) {
processes.push({
filename: script.filename,
threads: script.threads,
@ -907,6 +900,7 @@ export const ns: InternalAPI<NSFull> = {
temporary: script.temporary,
});
}
}
return processes;
},
hasRootAccess: (ctx) => (_hostname) => {
@ -1267,7 +1261,7 @@ export const ns: InternalAPI<NSFull> = {
}
// 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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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<NSFull> = {
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 });
const pattern = matchScriptPathExact(escapeRegExp(path));
for (const [key, byPid] of server.runningScriptMap) {
if (!pattern.test(key)) continue;
suc = true;
i--;
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<NSFull> = {
},
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<NSFull> = {
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)) ||

@ -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<PortNumber, Port> = 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<void> {
@ -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;
}

@ -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" => "../" */

@ -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;

@ -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<number, RunningScript> | null {
return server.runningScriptMap.get(scriptKey(path, args)) ?? null;
}
//Returns a RunningScript object matching the pid on the

@ -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

@ -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);
}

@ -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<ScriptKey, Map<number, RunningScript>> = 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<ScriptFilePath, Script> = 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<T extends BaseServer>(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<T extends BaseServer>(ctor: new () => T): readonly (keyof T)[] {
return getKeyList(ctor, { removedKeys: ["runningScriptMap", "savedScripts", "ramUsed"] });
}
}

@ -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;

@ -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

@ -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);
};

@ -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();
}
}

@ -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]...");
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;
}
// Kill by PID
if (typeof args[0] === "number") {
const pid = args[0];
if (killWorkerScript(pid)) return Terminal.print(`Killing script with PID ${pid}`);
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`);
}
// 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`);
const runningScript = server.getRunningScript(path, args);
if (runningScript == null) return Terminal.error("No such script is running. Nothing to kill");
return;
}
killWorkerScript(runningScript.pid);
Terminal.print(`Killing ${path}`);
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 + "");
}
}

@ -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);
}
}
}

@ -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);
}
}
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();
let pattern = flags["--grep"];
if (!pattern) {
pattern = ".*"; // Match anything
}
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);
}
}

@ -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.");

@ -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 {
try {
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;
}
const path = Terminal.getFilepath(String(commandArray[0]));
if (!path) return Terminal.error(`Invalid file path: ${commandArray[0]}`);
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.`);
// 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;
}
}
// 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;
}
const candidates = findRunningScripts(path, args, server);
// if there's no candidate then we just don't know.
Terminal.error(`No script named ${path} is running on the server`);
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 + "");
}
}

@ -26,11 +26,10 @@ 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);
@ -51,4 +50,5 @@ export function top(args: (string | number | boolean)[], server: BaseServer): vo
);
Terminal.print(entry);
}
}
}

@ -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));

@ -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");

@ -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);

@ -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) {

@ -88,7 +88,7 @@ export function Generic_fromJSON<T extends Record<string, any>>(
if (keys) {
for (const key of keys) {
const val = data[key];
if (val) obj[key] = val;
if (val !== undefined) obj[key] = val;
}
return obj;
}

@ -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;
}

@ -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 + "[^[]*");
}

@ -237,7 +237,7 @@ export const v2APIBreak = () => {
openV2Modal();
for (const server of GetAllServers()) {
server.runningScripts = [];
server.runningScriptMap = new Map();
}
saveObject.exportGame();
};

@ -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);
});
}),
]);

@ -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,7 +86,11 @@ function loadStandardServers() {
}
}
],
"scripts": [
"scripts": {
"ctor": "JSONMap",
"data": [
[
"script.js",
{
"ctor": "Script",
"data": {
@ -107,7 +116,9 @@ function loadStandardServers() {
]
}
}
],
]
]
},
"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

@ -21,28 +21,11 @@ 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": [
"scripts": {
"ctor": "JSONMap",
"data": [
[
"script.js",
{
"ctor": "Script",
"data": {
@ -51,7 +34,9 @@ exports[`load/saveAllServers 1`] = `
"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,9 +148,11 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"programs": [
"NUKE.exe"
],
"ramUsed": 1.6,
"runningScripts": [],
"scripts": [
"scripts": {
"ctor": "JSONMap",
"data": [
[
"script.js",
{
"ctor": "Script",
"data": {
@ -154,7 +161,9 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"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": []
}
}
}"