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 be a way to specify which script you want those commands & functions
to act on. to act on.
**A script that is being executed is uniquely identified by both its The best way to identify a script is by its PID (Process IDentifier). This
name and the arguments that it was run with.** 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 arguments must be an **exact** match. This means that both
the order and type of the arguments matter. 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 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 arguments. Arguments should be separated by a space. Remember that
scripts are uniquely identified by their arguments as well as scripts are identified by their arguments as well as their name.
their name. For example, if you ran a script `foo.js` with For example, if you ran a script `foo.js` with
the argument 1 and 2, then just typing "`kill foo.js`" will the argument 1 and 2, then just typing "`kill foo.js`" will
not work. You have to use:: 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 $ 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 Displays the logs of the script specified by the PID or name and arguments. Note that
are uniquely identified by their arguments as well as their name. For example, if you scripts are identified by their arguments as well as their name. For example,
ran a script 'foo.js' with the argument 'foodnstuff' then in order to 'tail' it you if you ran a script 'foo.js' with the argument 'foodnstuff' then in order to
must also add the 'foodnstuff' argument to the tail command as so: tail foo.js 'tail' it you must also add the 'foodnstuff' argument to the tail command as
foodnstuff so: tail foo.js foodnstuff
**top** **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. 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 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 by both its name and the arguments that are used to start it. So, if a script
was ran with the following arguments:: 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 $ 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. If you are killing the script using its PID, then the PID argument must be numeric.
killall killall
@ -543,11 +546,13 @@ Prints whether or not you have root access to the current server.
tail tail
^^^^ ^^^^
$ tail [pid]
or
$ tail [script name] [args...] $ 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 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:: it. So, if a script was ran with the following arguments::
$ run foo.js 10 50000 $ run foo.js 10 50000
@ -642,3 +647,6 @@ with a semicolon (;).
Example:: Example::
$ run foo.js; tail foo.js $ 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. | | script | string | Filename of script to execute. |
| hostname | string | Hostname of the <code>target server</code> on which to execute the script. | | 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. | | 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:** **Returns:**

@ -30,7 +30,7 @@ True if the specified script is running on the target server, and false otherwis
RAM cost: 0.1 GB 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 ## Example

@ -4,7 +4,7 @@
## NS.kill() method ## 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:** **Signature:**
@ -24,13 +24,13 @@ kill(filename: string, hostname?: string, ...args: ScriptArg[]): boolean;
boolean boolean
True if the script is successfully killed, and false otherwise. True if the scripts were successfully killed, and false otherwise.
## Remarks ## Remarks
RAM cost: 0.5 GB 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 ## 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. | | [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. | | [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(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. | | [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. | | [ls(host, substring)](./bitburner.ns.ls.md) | List files on a server. |
| [moveTail(x, y, pid)](./bitburner.ns.movetail.md) | Move a tail window. | | [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. | | 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. | | 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:** **Returns:**

@ -15,6 +15,7 @@ interface RunOptions
| Property | Modifiers | Type | Description | | 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> | | [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 | | [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 | | [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 { BlackOperationNames } from "../Bladeburner/data/BlackOperationNames";
import { isClassWork } from "../Work/ClassWork"; import { isClassWork } from "../Work/ClassWork";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import { workerScripts } from "../Netscript/WorkerScripts";
import type { PlayerObject } from "../PersonObjects/Player/PlayerObject"; import type { PlayerObject } from "../PersonObjects/Player/PlayerObject";
@ -284,13 +285,7 @@ export const achievements: Record<string, Achievement> = {
RUNNING_SCRIPTS_1000: { RUNNING_SCRIPTS_1000: {
...achievementData["RUNNING_SCRIPTS_1000"], ...achievementData["RUNNING_SCRIPTS_1000"],
Icon: "run1000", Icon: "run1000",
Condition: (): boolean => { Condition: (): boolean => workerScripts.size >= 1000,
let running = 0;
for (const s of GetAllServers()) {
running += s.runningScripts.length;
}
return running >= 1000;
},
}, },
DRAIN_SERVER: { DRAIN_SERVER: {
...achievementData["DRAIN_SERVER"], ...achievementData["DRAIN_SERVER"],

@ -3,7 +3,6 @@ import { CONSTANTS } from "../Constants";
import { IHacknetNode } from "./IHacknetNode"; import { IHacknetNode } from "./IHacknetNode";
import { BaseServer } from "../Server/BaseServer"; import { BaseServer } from "../Server/BaseServer";
import { RunningScript } from "../Script/RunningScript";
import { HacknetServerConstants } from "./data/Constants"; import { HacknetServerConstants } from "./data/Constants";
import { import {
calculateHashGainRate, calculateHashGainRate,
@ -15,7 +14,7 @@ import {
import { createRandomIp } from "../utils/IPAddress"; 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"; import { Player } from "@player";
interface IConstructorParams { interface IConstructorParams {
@ -113,14 +112,6 @@ export class HacknetServer extends BaseServer implements IHacknetNode {
return true; 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 { updateRamUsed(ram: number): void {
super.updateRamUsed(ram); super.updateRamUsed(ram);
this.updateHashRate(Player.mults.hacknet_node_money); 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 // Serialize the current object to a JSON save state
toJSON(): IReviverValue { toJSON(): IReviverValue {
return Generic_toJSON("HacknetServer", this); return this.toJSONBase("HacknetServer", includedKeys);
} }
// Initializes a HacknetServer Object from a JSON save state // Initializes a HacknetServer Object from a JSON save state
static fromJSON(value: IReviverValue): HacknetServer { 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; constructorsForReviver.HacknetServer = HacknetServer;

@ -5,11 +5,10 @@ import { NSFull } from "../NetscriptFunctions";
* Netscript functions and arguments for that script. * Netscript functions and arguments for that script.
*/ */
export class Environment { 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; stopFlag = false;
/** The currently running function */ /** The currently running function */
runningFn = ""; runningFn = "";
/** Environment variables (currently only Netscript functions) */ /** Environment variables (currently only Netscript functions) */

@ -1,5 +1,6 @@
import { NetscriptContext } from "./APIWrapper"; import { NetscriptContext } from "./APIWrapper";
import { WorkerScript } from "./WorkerScript"; import { WorkerScript } from "./WorkerScript";
import { killWorkerScript } from "./killWorkerScript";
import { GetAllServers, GetServer } from "../Server/AllServers"; import { GetAllServers, GetServer } from "../Server/AllServers";
import { Player } from "@player"; import { Player } from "@player";
import { ScriptDeath } from "./ScriptDeath"; import { ScriptDeath } from "./ScriptDeath";
@ -26,7 +27,7 @@ import { GangMemberTask } from "../Gang/GangMemberTask";
import { RunningScript } from "../Script/RunningScript"; import { RunningScript } from "../Script/RunningScript";
import { toNative } from "../NetscriptFunctions/toNative"; import { toNative } from "../NetscriptFunctions/toNative";
import { ScriptIdentifier } from "./ScriptIdentifier"; import { ScriptIdentifier } from "./ScriptIdentifier";
import { findRunningScript, findRunningScriptByPid } from "../Script/ScriptHelpers"; import { findRunningScripts, findRunningScriptByPid } from "../Script/ScriptHelpers";
import { arrayToString } from "../utils/helpers/arrayToString"; import { arrayToString } from "../utils/helpers/arrayToString";
import { HacknetServer } from "../Hacknet/HacknetServer"; import { HacknetServer } from "../Hacknet/HacknetServer";
import { BaseServer } from "../Server/BaseServer"; import { BaseServer } from "../Server/BaseServer";
@ -35,6 +36,8 @@ import { checkEnum } from "../utils/helpers/enum";
import { RamCostConstants } from "./RamCostGenerator"; import { RamCostConstants } from "./RamCostGenerator";
import { isPositiveInteger, PositiveInteger, Unknownify } from "../types"; import { isPositiveInteger, PositiveInteger, Unknownify } from "../types";
import { Engine } from "../engine"; import { Engine } from "../engine";
import { resolveFilePath, FilePath } from "../Paths/FilePath";
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
export const helpers = { export const helpers = {
string, string,
@ -61,8 +64,10 @@ export const helpers = {
gangMember, gangMember,
gangTask, gangTask,
log, log,
filePath,
scriptPath,
getRunningScript, getRunningScript,
getRunningScriptByArgs, getRunningScriptsByArgs,
getCannotFindRunningScriptErrorMessage, getCannotFindRunningScriptErrorMessage,
createPublicRunningScript, createPublicRunningScript,
failOnHacknetServer, failOnHacknetServer,
@ -73,6 +78,7 @@ export interface CompleteRunOptions {
threads: PositiveInteger; threads: PositiveInteger;
temporary: boolean; temporary: boolean;
ramOverride?: number; ramOverride?: number;
preventDuplicates: boolean;
} }
export function assertMember<T extends string>( export function assertMember<T extends string>(
@ -186,6 +192,7 @@ function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRun
const result: CompleteRunOptions = { const result: CompleteRunOptions = {
threads: 1 as PositiveInteger, threads: 1 as PositiveInteger,
temporary: false, temporary: false,
preventDuplicates: false,
}; };
function checkThreads(threads: unknown, argName: string) { function checkThreads(threads: unknown, argName: string) {
if (threads !== null && threads !== undefined) { if (threads !== null && threads !== undefined) {
@ -200,6 +207,7 @@ function runOptions(ctx: NetscriptContext, threadOrOption: unknown): CompleteRun
const options = threadOrOption as Unknownify<CompleteRunOptions>; const options = threadOrOption as Unknownify<CompleteRunOptions>;
checkThreads(options.threads, "RunOptions.threads"); checkThreads(options.threads, "RunOptions.threads");
result.temporary = !!options.temporary; result.temporary = !!options.temporary;
result.preventDuplicates = !!options.preventDuplicates;
if (options.ramOverride !== undefined && options.ramOverride !== null) { if (options.ramOverride !== undefined && options.ramOverride !== null) {
result.ramOverride = number(ctx, "RunOptions.ramOverride", options.ramOverride); result.ramOverride = number(ctx, "RunOptions.ramOverride", options.ramOverride);
if (result.ramOverride < RamCostConstants.Base) { if (result.ramOverride < RamCostConstants.Base) {
@ -348,10 +356,8 @@ function checkEnvFlags(ctx: NetscriptContext): void {
throw new ScriptDeath(ws); throw new ScriptDeath(ws);
} }
if (ws.env.runningFn && ctx.function !== "asleep") { 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."); log(ctx, () => "Failed to run due to failed concurrency check.");
throw makeRuntimeErrorMsg( const err = makeRuntimeErrorMsg(
ctx, ctx,
`Concurrent calls to Netscript functions are not allowed! `Concurrent calls to Netscript functions are not allowed!
Did you forget to await hack(), grow(), or some other 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}`, Currently running: ${ws.env.runningFn} tried to run: ${ctx.function}`,
"CONCURRENCY", "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); ws.dynamicRamUsage = Math.min(ws.dynamicRamUsage + ramCost, RamCostConstants.Max);
if (ws.dynamicRamUsage > 1.01 * ws.scriptRef.ramUsage) { if (ws.dynamicRamUsage > 1.01 * ws.scriptRef.ramUsage) {
log(ctx, () => "Insufficient static ram available."); log(ctx, () => "Insufficient static ram available.");
ws.env.stopFlag = true; const err = makeRuntimeErrorMsg(
throw makeRuntimeErrorMsg(
ctx, ctx,
`Dynamic RAM usage calculated to be greater than RAM allocation. `Dynamic RAM usage calculated to be greater than RAM allocation.
This is probably because you somehow circumvented the static RAM calculation. This is probably because you somehow circumvented the static RAM calculation.
@ -410,6 +417,8 @@ function updateDynamicRam(ctx: NetscriptContext, ramCost: number): void {
Sorry :(`, Sorry :(`,
"RAM USAGE", "RAM USAGE",
); );
killWorkerScript(ws);
throw err;
} }
} }
@ -651,22 +660,35 @@ function log(ctx: NetscriptContext, message: () => string) {
ctx.workerScript.log(ctx.functionPath, message); 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. * If the 'fn' argument is not specified, this returns the current RunningScript.
* @param fn - Filename of script * @param fn - Filename of script
* @param hostname - Hostname/ip of the server on which the script resides * @param hostname - Hostname/ip of the server on which the script resides
* @param scriptArgs - Running script's arguments * @param scriptArgs - Running script's arguments
* @returns Running script identified by the parameters, or null if no such script * @returns Running scripts identified by the parameters, or empty if no such script
* exists, or the current running script if the first argument 'fn' * exists, or only the current running script if the first argument 'fn'
* is not specified. * is not specified.
*/ */
function getRunningScriptByArgs( export function getRunningScriptsByArgs(
ctx: NetscriptContext, ctx: NetscriptContext,
fn: string, fn: string,
hostname: string, hostname: string,
scriptArgs: ScriptArg[], scriptArgs: ScriptArg[],
): RunningScript | null { ): Map<number, RunningScript> | null {
if (!Array.isArray(scriptArgs)) { if (!Array.isArray(scriptArgs)) {
throw helpers.makeRuntimeErrorMsg( throw helpers.makeRuntimeErrorMsg(
ctx, ctx,
@ -675,18 +697,14 @@ function getRunningScriptByArgs(
); );
} }
if (fn != null && typeof fn === "string") { const path = scriptPath(ctx, "filename", fn);
// Get Logs of another script // Lookup server to scope search
if (hostname == null) { if (hostname == null) {
hostname = ctx.workerScript.hostname; hostname = ctx.workerScript.hostname;
} }
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
return findRunningScript(fn, scriptArgs, server); return findRunningScripts(path, scriptArgs, server);
}
// If no arguments are specified, return the current RunningScript
return ctx.workerScript.scriptRef;
} }
function getRunningScriptByPid(pid: number): RunningScript | null { function getRunningScriptByPid(pid: number): RunningScript | null {
@ -701,7 +719,9 @@ function getRunningScript(ctx: NetscriptContext, ident: ScriptIdentifier): Runni
if (typeof ident === "number") { if (typeof ident === "number") {
return getRunningScriptByPid(ident); return getRunningScriptByPid(ident);
} else { } 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; delay: number | null = null;
/** Holds the Promise reject() function while the script is "blocked" by an async op */ /** 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 */ /** Stores names of all functions that have logging disabled */
disableLogs: Record<string, boolean> = {}; disableLogs: Record<string, boolean> = {};
@ -79,7 +79,7 @@ export class WorkerScript {
hostname: string; hostname: string;
/** Function called when the script ends. */ /** Function called when the script ends. */
atExit?: () => void; atExit: (() => void) | undefined = undefined;
constructor(runningScriptObj: RunningScript, pid: number, nsFuncsGenerator?: (ws: WorkerScript) => NSFull) { constructor(runningScriptObj: RunningScript, pid: number, nsFuncsGenerator?: (ws: WorkerScript) => NSFull) {
this.name = runningScriptObj.filename; this.name = runningScriptObj.filename;

@ -7,47 +7,24 @@ import { WorkerScript } from "./WorkerScript";
import { workerScripts } from "./WorkerScripts"; import { workerScripts } from "./WorkerScripts";
import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter"; import { WorkerScriptStartStopEventEmitter } from "./WorkerScriptStartStopEventEmitter";
import { RunningScript } from "../Script/RunningScript";
import { GetServer } from "../Server/AllServers"; import { GetServer } from "../Server/AllServers";
import { AddRecentScript } from "./RecentScripts"; import { AddRecentScript } from "./RecentScripts";
import { ITutorial } from "../InteractiveTutorial"; import { ITutorial } from "../InteractiveTutorial";
import { AlertEvents } from "../ui/React/AlertManager"; import { AlertEvents } from "../ui/React/AlertManager";
import { handleUnknownError } from "./NetscriptHelpers"; import { handleUnknownError } from "./NetscriptHelpers";
import { roundToTwo } from "../utils/helpers/roundToTwo";
export type killScriptParams = WorkerScript | number | { runningScript: RunningScript; hostname: string }; export function killWorkerScript(ws: WorkerScript): boolean {
export function killWorkerScript(params: killScriptParams): boolean {
if (ITutorial.isRunning) { if (ITutorial.isRunning) {
AlertEvents.emit("Processes cannot be killed during the tutorial."); AlertEvents.emit("Processes cannot be killed during the tutorial.");
return false; 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); stopAndCleanUpWorkerScript(ws);
return true; return true;
} }
}
return false; export function killWorkerScriptByPid(pid: number): boolean {
}
}
function killWorkerScriptByPid(pid: number): boolean {
const ws = workerScripts.get(pid); const ws = workerScripts.get(pid);
if (ws instanceof WorkerScript) { if (ws instanceof WorkerScript) {
stopAndCleanUpWorkerScript(ws); stopAndCleanUpWorkerScript(ws);
@ -58,6 +35,10 @@ function killWorkerScriptByPid(pid: number): boolean {
} }
function stopAndCleanUpWorkerScript(ws: WorkerScript): void { 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 //Clean up any ongoing netscriptDelay
if (ws.delay) clearTimeout(ws.delay); if (ws.delay) clearTimeout(ws.delay);
ws.delayReject?.(new ScriptDeath(ws)); ws.delayReject?.(new ScriptDeath(ws));
@ -65,7 +46,6 @@ function stopAndCleanUpWorkerScript(ws: WorkerScript): void {
if (typeof ws.atExit === "function") { if (typeof ws.atExit === "function") {
try { try {
ws.env.stopFlag = false;
const atExit = ws.atExit; const atExit = ws.atExit;
ws.atExit = undefined; ws.atExit = undefined;
atExit(); atExit();
@ -95,21 +75,21 @@ function removeWorkerScript(workerScript: WorkerScript): void {
} }
// Delete the RunningScript object from that server // Delete the RunningScript object from that server
for (let i = 0; i < server.runningScripts.length; ++i) { const rs = workerScript.scriptRef;
const runningScript = server.runningScripts[i]; const byPid = server.runningScriptMap.get(rs.scriptKey);
if (runningScript === workerScript.scriptRef) { if (!byPid) {
server.runningScripts.splice(i, 1); console.error(`Couldn't find runningScriptMap for key ${rs.scriptKey}`);
break; } 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); workerScripts.delete(workerScript.pid);
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);
AddRecentScript(workerScript); AddRecentScript(workerScript);
WorkerScriptStartStopEventEmitter.emit(); WorkerScriptStartStopEventEmitter.emit();
} }

@ -1,6 +1,5 @@
import $ from "jquery"; import $ from "jquery";
import { vsprintf, sprintf } from "sprintf-js"; import { vsprintf, sprintf } from "sprintf-js";
import { WorkerScriptStartStopEventEmitter } from "./Netscript/WorkerScriptStartStopEventEmitter";
import { BitNodeMultipliers, IBitNodeMultipliers } from "./BitNode/BitNodeMultipliers"; import { BitNodeMultipliers, IBitNodeMultipliers } from "./BitNode/BitNodeMultipliers";
import { CONSTANTS } from "./Constants"; import { CONSTANTS } from "./Constants";
import { import {
@ -35,7 +34,7 @@ import {
import { Server } from "./Server/Server"; import { Server } from "./Server/Server";
import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing"; import { influenceStockThroughServerGrow } from "./StockMarket/PlayerInfluencing";
import { runScriptFromScript } from "./NetscriptWorker"; import { runScriptFromScript } from "./NetscriptWorker";
import { killWorkerScript } from "./Netscript/killWorkerScript"; import { killWorkerScript, killWorkerScriptByPid } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts"; import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
import { helpers, assertObjectType } from "./Netscript/NetscriptHelpers"; import { helpers, assertObjectType } from "./Netscript/NetscriptHelpers";
@ -71,6 +70,7 @@ import { NetscriptSingularity } from "./NetscriptFunctions/Singularity";
import { dialogBoxCreate } from "./ui/React/DialogBox"; import { dialogBoxCreate } from "./ui/React/DialogBox";
import { SnackbarEvents, ToastVariant } from "./ui/React/Snackbar"; import { SnackbarEvents, ToastVariant } from "./ui/React/Snackbar";
import { checkEnum } from "./utils/helpers/enum"; import { checkEnum } from "./utils/helpers/enum";
import { matchScriptPathExact } from "./utils/helpers/scriptKey";
import { Flags } from "./NetscriptFunctions/Flags"; import { Flags } from "./NetscriptFunctions/Flags";
import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence"; import { calculateIntelligenceBonus } from "./PersonObjects/formulas/intelligence";
@ -82,12 +82,12 @@ import { ScriptDeath } from "./Netscript/ScriptDeath";
import { getBitNodeMultipliers } from "./BitNode/BitNode"; import { getBitNodeMultipliers } from "./BitNode/BitNode";
import { assert, arrayAssert, stringAssert, objectAssert } from "./utils/helpers/typeAssertion"; import { assert, arrayAssert, stringAssert, objectAssert } from "./utils/helpers/typeAssertion";
import { CityName, JobName, CrimeType, GymType, LocationName, UniversityClassType } from "./Enums"; import { CityName, JobName, CrimeType, GymType, LocationName, UniversityClassType } from "./Enums";
import { cloneDeep } from "lodash"; import { cloneDeep, escapeRegExp } from "lodash";
import { FactionWorkType } from "./Enums"; import { FactionWorkType } from "./Enums";
import numeral from "numeral"; import numeral from "numeral";
import { clearPort, peekPort, portHandle, readPort, tryWritePort, writePort } from "./NetscriptPort"; import { clearPort, peekPort, portHandle, readPort, tryWritePort, writePort } from "./NetscriptPort";
import { FilePath, resolveFilePath } from "./Paths/FilePath"; import { FilePath } from "./Paths/FilePath";
import { hasScriptExtension, resolveScriptFilePath } from "./Paths/ScriptFilePath"; import { hasScriptExtension } from "./Paths/ScriptFilePath";
import { hasTextExtension } from "./Paths/TextFilePath"; import { hasTextExtension } from "./Paths/TextFilePath";
import { ContentFilePath } from "./Paths/ContentFile"; import { ContentFilePath } from "./Paths/ContentFile";
import { LiteratureName } from "./Literature/data/LiteratureNames"; import { LiteratureName } from "./Literature/data/LiteratureNames";
@ -691,8 +691,7 @@ export const ns: InternalAPI<NSFull> = {
run: run:
(ctx) => (ctx) =>
(_scriptname, _thread_or_opt = 1, ..._args) => { (_scriptname, _thread_or_opt = 1, ..._args) => {
const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); const path = helpers.scriptPath(ctx, "scriptname", _scriptname);
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`);
const runOpts = helpers.runOptions(ctx, _thread_or_opt); const runOpts = helpers.runOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args); const args = helpers.scriptArgs(ctx, _args);
const scriptServer = ctx.workerScript.getServer(); const scriptServer = ctx.workerScript.getServer();
@ -702,8 +701,7 @@ export const ns: InternalAPI<NSFull> = {
exec: exec:
(ctx) => (ctx) =>
(_scriptname, _hostname, _thread_or_opt = 1, ..._args) => { (_scriptname, _hostname, _thread_or_opt = 1, ..._args) => {
const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); const path = helpers.scriptPath(ctx, "scriptname", _scriptname);
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`);
const hostname = helpers.string(ctx, "hostname", _hostname); const hostname = helpers.string(ctx, "hostname", _hostname);
const runOpts = helpers.runOptions(ctx, _thread_or_opt); const runOpts = helpers.runOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args); const args = helpers.scriptArgs(ctx, _args);
@ -713,8 +711,7 @@ export const ns: InternalAPI<NSFull> = {
spawn: spawn:
(ctx) => (ctx) =>
(_scriptname, _thread_or_opt = 1, ..._args) => { (_scriptname, _thread_or_opt = 1, ..._args) => {
const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); const path = helpers.scriptPath(ctx, "scriptname", _scriptname);
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `${path} is not a valid script file path.`);
const runOpts = helpers.runOptions(ctx, _thread_or_opt); const runOpts = helpers.runOptions(ctx, _thread_or_opt);
const args = helpers.scriptArgs(ctx, _args); const args = helpers.scriptArgs(ctx, _args);
const spawnDelay = 10; const spawnDelay = 10;
@ -741,21 +738,23 @@ export const ns: InternalAPI<NSFull> = {
const killByPid = typeof ident === "number"; const killByPid = typeof ident === "number";
if (killByPid) { if (killByPid) {
// Kill by pid // Kill by pid
res = killWorkerScript(ident); res = killWorkerScriptByPid(ident);
} else { } else {
// Kill by filename/hostname // Kill by filename/hostname
if (scriptID === undefined) { if (scriptID === undefined) {
throw helpers.makeRuntimeErrorMsg(ctx, "Usage: kill(scriptname, server, [arg1], [arg2]...)"); throw helpers.makeRuntimeErrorMsg(ctx, "Usage: kill(scriptname, server, [arg1], [arg2]...)");
} }
const server = helpers.getServer(ctx, ident.hostname); const byPid = helpers.getRunningScriptsByArgs(ctx, ident.scriptname, ident.hostname, ident.args);
const runningScriptObj = helpers.getRunningScriptByArgs(ctx, ident.scriptname, ident.hostname, ident.args); if (byPid === null) {
if (runningScriptObj == null) {
helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(ident)); helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(ident));
return false; return false;
} }
res = killWorkerScript({ runningScript: runningScriptObj, hostname: server.hostname }); res = true;
for (const pid of byPid.keys()) {
res &&= killWorkerScriptByPid(pid);
}
} }
if (res) { if (res) {
@ -771,7 +770,7 @@ export const ns: InternalAPI<NSFull> = {
} else { } else {
helpers.log( helpers.log(
ctx, ctx,
() => `No such script '${scriptID}' on '${hostname}' with args: ${arrayToString(scriptArgs)}`, () => `Internal error killing '${scriptID}' on '${hostname}' with args: ${arrayToString(scriptArgs)}`,
); );
} }
return false; return false;
@ -786,12 +785,13 @@ export const ns: InternalAPI<NSFull> = {
let scriptsKilled = 0; let scriptsKilled = 0;
for (let i = server.runningScripts.length - 1; i >= 0; --i) { for (const byPid of server.runningScriptMap.values()) {
if (safetyguard === true && server.runningScripts[i].pid == ctx.workerScript.pid) continue; for (const pid of byPid.keys()) {
killWorkerScript({ runningScript: server.runningScripts[i], hostname: server.hostname }); if (safetyguard === true && pid == ctx.workerScript.pid) continue;
killWorkerScriptByPid(pid);
++scriptsKilled; ++scriptsKilled;
} }
WorkerScriptStartStopEventEmitter.emit(); }
helpers.log( helpers.log(
ctx, ctx,
() => `Killing all scripts on '${server.hostname}'. May take a few minutes for the scripts to die.`, () => `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 destServer = helpers.getServer(ctx, destination);
const sourceServer = helpers.getServer(ctx, source); const sourceServer = helpers.getServer(ctx, source);
const files = Array.isArray(_files) ? _files : [_files]; const files = Array.isArray(_files) ? _files : [_files];
const lits: (FilePath & LiteratureName)[] = []; const lits: FilePath[] = [];
const contentFiles: ContentFilePath[] = []; const contentFiles: ContentFilePath[] = [];
//First loop through filenames to find all errors before moving anything. //First loop through filenames to find all errors before moving anything.
for (const file of files) { for (const file of files) {
// Not a string const path = helpers.filePath(ctx, "files", file);
if (typeof file !== "string") { if (hasScriptExtension(path) || hasTextExtension(path)) {
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}`);
contentFiles.push(path); contentFiles.push(path);
continue; continue;
} }
if (!file.endsWith(".lit")) { if (!path.endsWith(".lit")) {
throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files."); throw helpers.makeRuntimeErrorMsg(ctx, "Only works for scripts, .lit and .txt files.");
} }
const sanitizedPath = resolveFilePath(file, ctx.workerScript.name); lits.push(path);
if (!sanitizedPath || !checkEnum(LiteratureName, sanitizedPath)) {
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid filename: '${file}'`);
}
lits.push(sanitizedPath);
} }
let noFailures = true; let noFailures = true;
@ -864,7 +855,8 @@ export const ns: InternalAPI<NSFull> = {
continue; 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}'.`); helpers.log(ctx, () => `File '${litFilePath}' copied over to '${destServer?.hostname}'.`);
continue; continue;
} }
@ -898,7 +890,8 @@ export const ns: InternalAPI<NSFull> = {
const hostname = helpers.string(ctx, "hostname", _hostname); const hostname = helpers.string(ctx, "hostname", _hostname);
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
const processes: ProcessInfo[] = []; const processes: ProcessInfo[] = [];
for (const script of server.runningScripts) { for (const byPid of server.runningScriptMap.values()) {
for (const script of byPid.values()) {
processes.push({ processes.push({
filename: script.filename, filename: script.filename,
threads: script.threads, threads: script.threads,
@ -907,6 +900,7 @@ export const ns: InternalAPI<NSFull> = {
temporary: script.temporary, temporary: script.temporary,
}); });
} }
}
return processes; return processes;
}, },
hasRootAccess: (ctx) => (_hostname) => { hasRootAccess: (ctx) => (_hostname) => {
@ -1267,7 +1261,7 @@ export const ns: InternalAPI<NSFull> = {
} }
// Delete all scripts running on server // 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.`); helpers.log(ctx, () => `Cannot delete server '${hostname}' because it still has scripts running.`);
return false; return false;
} }
@ -1325,12 +1319,10 @@ export const ns: InternalAPI<NSFull> = {
return writePort(portNumber, data); return writePort(portNumber, data);
}, },
write: (ctx) => (_filename, _data, _mode) => { 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 data = helpers.string(ctx, "data", _data ?? "");
const mode = helpers.string(ctx, "mode", _mode ?? "a"); 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); const server = helpers.getServer(ctx, ctx.workerScript.hostname);
if (hasScriptExtension(filepath)) { if (hasScriptExtension(filepath)) {
@ -1369,8 +1361,8 @@ export const ns: InternalAPI<NSFull> = {
return readPort(portNumber); return readPort(portNumber);
}, },
read: (ctx) => (_filename) => { read: (ctx) => (_filename) => {
const path = resolveFilePath(helpers.string(ctx, "filename", _filename), ctx.workerScript.name); const path = helpers.filePath(ctx, "filename", _filename);
if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) return ""; if (!hasScriptExtension(path) && !hasTextExtension(path)) return "";
const server = ctx.workerScript.getServer(); const server = ctx.workerScript.getServer();
return server.getContentFile(path)?.content ?? ""; return server.getContentFile(path)?.content ?? "";
}, },
@ -1379,8 +1371,8 @@ export const ns: InternalAPI<NSFull> = {
return peekPort(portNumber); return peekPort(portNumber);
}, },
clear: (ctx) => (_file) => { clear: (ctx) => (_file) => {
const path = resolveFilePath(helpers.string(ctx, "file", _file), ctx.workerScript.name); const path = helpers.filePath(ctx, "file", _file);
if (!path || (!hasScriptExtension(path) && !hasTextExtension(path))) { if (!hasScriptExtension(path) && !hasTextExtension(path)) {
throw helpers.makeRuntimeErrorMsg(ctx, `Invalid file path or extension: ${_file}`); throw helpers.makeRuntimeErrorMsg(ctx, `Invalid file path or extension: ${_file}`);
} }
const server = ctx.workerScript.getServer(); const server = ctx.workerScript.getServer();
@ -1398,7 +1390,7 @@ export const ns: InternalAPI<NSFull> = {
return portHandle(portNumber); return portHandle(portNumber);
}, },
rm: (ctx) => (_fn, _hostname) => { 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 hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname);
const s = helpers.getServer(ctx, hostname); const s = helpers.getServer(ctx, hostname);
if (!filepath) { if (!filepath) {
@ -1414,34 +1406,30 @@ export const ns: InternalAPI<NSFull> = {
return status.res; return status.res;
}, },
scriptRunning: (ctx) => (_scriptname, _hostname) => { 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 hostname = helpers.string(ctx, "hostname", _hostname);
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
for (let i = 0; i < server.runningScripts.length; ++i) { return server.isRunning(scriptname);
if (server.runningScripts[i].filename == scriptname) {
return true;
}
}
return false;
}, },
scriptKill: (ctx) => (_scriptname, _hostname) => { 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 hostname = helpers.string(ctx, "hostname", _hostname);
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
let suc = false; let suc = false;
for (let i = 0; i < server.runningScripts.length; i++) {
if (server.runningScripts[i].filename == scriptname) { const pattern = matchScriptPathExact(escapeRegExp(path));
killWorkerScript({ runningScript: server.runningScripts[i], hostname: server.hostname }); for (const [key, byPid] of server.runningScriptMap) {
if (!pattern.test(key)) continue;
suc = true; suc = true;
i--; for (const pid of byPid.keys()) {
killWorkerScriptByPid(pid);
} }
} }
return suc; return suc;
}, },
getScriptName: (ctx) => () => ctx.workerScript.name, getScriptName: (ctx) => () => ctx.workerScript.name,
getScriptRam: (ctx) => (_scriptname, _hostname) => { getScriptRam: (ctx) => (_scriptname, _hostname) => {
const path = resolveScriptFilePath(helpers.string(ctx, "scriptname", _scriptname), ctx.workerScript.name); const path = helpers.scriptPath(ctx, "scriptname", _scriptname);
if (!path) throw helpers.makeRuntimeErrorMsg(ctx, `Could not parse file path ${_scriptname}`);
const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname); const hostname = helpers.string(ctx, "hostname", _hostname ?? ctx.workerScript.hostname);
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
const script = server.scripts.get(path); const script = server.scripts.get(path);
@ -1640,7 +1628,7 @@ export const ns: InternalAPI<NSFull> = {
}, },
wget: (ctx) => async (_url, _target, _hostname) => { wget: (ctx) => async (_url, _target, _hostname) => {
const url = helpers.string(ctx, "url", _url); 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 hostname = _hostname ? helpers.string(ctx, "hostname", _hostname) : ctx.workerScript.hostname;
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
if (!target || (!hasTextExtension(target) && !hasScriptExtension(target))) { if (!target || (!hasTextExtension(target) && !hasScriptExtension(target))) {
@ -1725,10 +1713,8 @@ export const ns: InternalAPI<NSFull> = {
mv: (ctx) => (_host, _source, _destination) => { mv: (ctx) => (_host, _source, _destination) => {
const hostname = helpers.string(ctx, "host", _host); const hostname = helpers.string(ctx, "host", _host);
const server = helpers.getServer(ctx, hostname); const server = helpers.getServer(ctx, hostname);
const sourcePath = resolveFilePath(helpers.string(ctx, "source", _source)); const sourcePath = helpers.filePath(ctx, "source", _source);
if (!sourcePath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid source filename: '${_source}'`); const destinationPath = helpers.filePath(ctx, "destination", _destination);
const destinationPath = resolveFilePath(helpers.string(ctx, "destination", _destination), sourcePath);
if (!destinationPath) throw helpers.makeRuntimeErrorMsg(ctx, `Invalid destination filename: '${destinationPath}'`);
if ( if (
(!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) || (!hasTextExtension(sourcePath) && !hasScriptExtension(sourcePath)) ||

@ -31,7 +31,7 @@ import { parse } from "acorn";
import { simple as walksimple } from "acorn-walk"; import { simple as walksimple } from "acorn-walk";
import { Terminal } from "./Terminal"; import { Terminal } from "./Terminal";
import { ScriptArg } from "@nsdefs"; import { ScriptArg } from "@nsdefs";
import { handleUnknownError, CompleteRunOptions } from "./Netscript/NetscriptHelpers"; import { handleUnknownError, CompleteRunOptions, getRunningScriptsByArgs } from "./Netscript/NetscriptHelpers";
import { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath"; import { resolveScriptFilePath, ScriptFilePath } from "./Paths/ScriptFilePath";
import { root } from "./Paths/Directory"; import { root } from "./Paths/Directory";
@ -39,14 +39,10 @@ export const NetscriptPorts: Map<PortNumber, Port> = new Map();
export function prestigeWorkerScripts(): void { export function prestigeWorkerScripts(): void {
for (const ws of workerScripts.values()) { for (const ws of workerScripts.values()) {
ws.env.stopFlag = true;
killWorkerScript(ws); killWorkerScript(ws);
} }
NetscriptPorts.clear(); NetscriptPorts.clear();
WorkerScriptStartStopEventEmitter.emit();
workerScripts.clear();
} }
async function startNetscript2Script(workerScript: WorkerScript): Promise<void> { async function startNetscript2Script(workerScript: WorkerScript): Promise<void> {
@ -360,18 +356,15 @@ export function loadAllRunningScripts(): void {
// Reset each server's RAM usage to 0 // Reset each server's RAM usage to 0
server.ramUsed = 0; server.ramUsed = 0;
if (skipScriptLoad) { const rsList = server.savedScripts;
server.savedScripts = undefined;
if (skipScriptLoad || !rsList) {
// Start game with no scripts // Start game with no scripts
server.runningScripts.length = 0;
continue; continue;
} }
// Using backwards index iteration to avoid complications when removing elements during iteration. for (const runningScript of rsList) {
for (let i = server.runningScripts.length - 1; i >= 0; i--) { startWorkerScript(runningScript, server);
const runningScript = server.runningScripts[i];
const success = createAndAddWorkerScript(runningScript, server);
scriptCalculateOfflineProduction(runningScript); 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. // 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}'`); workerScript.log(caller, () => `'${scriptname}' is already running on '${host.hostname}'`);
return 0; return 0;
} }

@ -1,6 +1,6 @@
import { allContentFiles } from "./ContentFile"; import { allContentFiles } from "./ContentFile";
import type { BaseServer } from "../Server/BaseServer"; 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 / /** 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" => "../" */ * 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 { PositiveInteger } from "../types";
import { getKeyList } from "../utils/helpers/getKeyList"; import { getKeyList } from "../utils/helpers/getKeyList";
import { ScriptFilePath } from "../Paths/ScriptFilePath"; import { ScriptFilePath } from "../Paths/ScriptFilePath";
import { ScriptKey, scriptKey } from "../utils/helpers/scriptKey";
export class RunningScript { export class RunningScript {
// Script arguments // Script arguments
@ -61,6 +62,9 @@ export class RunningScript {
// hostname of the server on which this script is running // hostname of the server on which this script is running
server = ""; server = "";
// Cached key for ByArgs lookups
scriptKey: ScriptKey = "";
// Number of threads that this script is running with // Number of threads that this script is running with
threads = 1 as PositiveInteger; threads = 1 as PositiveInteger;
@ -75,6 +79,7 @@ export class RunningScript {
if (!ramUsage) throw new Error("Must provide a ramUsage for RunningScript initialization."); if (!ramUsage) throw new Error("Must provide a ramUsage for RunningScript initialization.");
this.filename = script.filename; this.filename = script.filename;
this.args = args; this.args = args;
this.scriptKey = scriptKey(this.filename, args);
this.server = script.server; this.server = script.server;
this.ramUsage = ramUsage; this.ramUsage = ramUsage;
this.dependencies = script.dependencies; this.dependencies = script.dependencies;

@ -7,7 +7,9 @@ import { processSingleServerGrowth } from "../Server/ServerHelpers";
import { GetServer } from "../Server/AllServers"; import { GetServer } from "../Server/AllServers";
import { formatPercent } from "../ui/formatNumber"; import { formatPercent } from "../ui/formatNumber";
import { workerScripts } from "../Netscript/WorkerScripts"; 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 { export function scriptCalculateOfflineProduction(runningScript: RunningScript): void {
//The Player object stores the last update time from when we were online //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 //Returns a RunningScript map containing scripts matching the filename and
//designated server, and false otherwise //arguments on the designated server, or null if none were found
export function findRunningScript( export function findRunningScripts(
filename: string, path: ScriptFilePath,
args: (string | number | boolean)[], args: (string | number | boolean)[],
server: BaseServer, server: BaseServer,
): RunningScript | null { ): Map<number, RunningScript> | null {
for (let i = 0; i < server.runningScripts.length; ++i) { return server.runningScriptMap.get(scriptKey(path, args)) ?? null;
if (server.runningScripts[i].filename === filename && compareArrays(server.runningScripts[i].args, args)) {
return server.runningScripts[i];
}
}
return null;
} }
//Returns a RunningScript object matching the pid on the //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. * Must be greater-or-equal to the base RAM cost. Defaults to the statically calculated cost.
*/ */
ramOverride?: number; 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 */ /** @public */
@ -5500,7 +5505,7 @@ export interface NS {
* ``` * ```
* @param script - Filename of script to run. * @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 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. * @returns Returns the PID of a successfully started script, and 0 otherwise.
*/ */
run(script: string, threadOrOptions?: number | RunOptions, ...args: (string | number | boolean)[]): number; 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 script - Filename of script to execute.
* @param hostname - Hostname of the `target server` on which to execute the script. * @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 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. * @returns Returns the PID of a successfully started script, and 0 otherwise.
*/ */
exec( exec(
@ -5595,11 +5600,11 @@ export interface NS {
kill(pid: number): boolean; 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 * @remarks
* RAM cost: 0.5 GB * 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}. * To instead kill a script using its PID, see {@link NS.(kill:1) | the other ns.kill entry}.
* *
* @example * @example
@ -5616,7 +5621,7 @@ export interface NS {
* @param filename - Filename of the script to kill. * @param filename - Filename of the script to kill.
* @param hostname - Hostname where the script to kill is running. Defaults to the current server. * @param hostname - Hostname where the script to kill is running. Defaults to the current server.
* @param args - Arguments of the script to kill. * @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; 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. * 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. * 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 * @example
* ```js * ```js

@ -10,10 +10,9 @@ import { getRandomInt } from "../utils/helpers/getRandomInt";
import { Reviver } from "../utils/JSONReviver"; import { Reviver } from "../utils/JSONReviver";
import { SpecialServers } from "./data/SpecialServers"; import { SpecialServers } from "./data/SpecialServers";
import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers"; import { BitNodeMultipliers } from "../BitNode/BitNodeMultipliers";
import "../Script/RunningScript"; // For reviver side-effect
import { IPAddress, isIPAddress } from "../Types/strings"; 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 * Map of all Servers that exist in the game
@ -207,17 +206,6 @@ function excludeReplacer(key: string, value: any): any {
return value; 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 { 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 { ScriptFilePath, hasScriptExtension } from "../Paths/ScriptFilePath";
import { TextFilePath, hasTextExtension } from "../Paths/TextFilePath"; 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 { createRandomIp } from "../utils/IPAddress";
import { compareArrays } from "../utils/helpers/compareArrays";
import { ScriptArg } from "../Netscript/ScriptArg";
import { JSONMap } from "../Types/Jsonable"; import { JSONMap } from "../Types/Jsonable";
import { IPAddress, ServerName } from "../Types/strings"; import { IPAddress, ServerName } from "../Types/strings";
import { FilePath } from "../Paths/FilePath"; import { FilePath } from "../Paths/FilePath";
@ -19,6 +19,10 @@ import { ProgramFilePath, hasProgramExtension } from "../Paths/ProgramFilePath";
import { MessageFilename } from "src/Message/MessageHelpers"; import { MessageFilename } from "src/Message/MessageHelpers";
import { LiteratureName } from "src/Literature/data/LiteratureNames"; import { LiteratureName } from "src/Literature/data/LiteratureNames";
import { CompletedProgramName } from "src/Programs/Programs"; 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 { interface IConstructorParams {
adminRights?: boolean; adminRights?: boolean;
@ -64,7 +68,7 @@ export abstract class BaseServer implements IServer {
maxRam = 0; maxRam = 0;
// Message files AND Literature files on this Server // 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. // Name of company/faction/etc. that this server belongs to.
// Optional, not applicable to all Servers // Optional, not applicable to all Servers
@ -77,8 +81,12 @@ export abstract class BaseServer implements IServer {
// RAM (GB) used. i.e. unavailable RAM // RAM (GB) used. i.e. unavailable RAM
ramUsed = 0; ramUsed = 0;
// RunningScript files on this server // RunningScript files on this server. Keyed first by name/args, then by PID.
runningScripts: RunningScript[] = []; 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 // Script files on this Server
scripts: JSONMap<ScriptFilePath, Script> = new JSONMap(); scripts: JSONMap<ScriptFilePath, Script> = new JSONMap();
@ -138,23 +146,16 @@ export abstract class BaseServer implements IServer {
return null; 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. */ /** Get a TextFile or Script depending on the input path type. */
getContentFile(path: ContentFilePath): ContentFile | null { getContentFile(path: ContentFilePath): ContentFile | null {
return (hasTextExtension(path) ? this.textFiles.get(path) : this.scripts.get(path)) ?? 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 */ /** Returns boolean indicating whether the given script is running on this server */
isRunning(fn: string): boolean { isRunning(path: ScriptFilePath): boolean {
for (const runningScriptObj of this.runningScripts) { const pattern = matchScriptPathExact(lodash.escapeRegExp(path));
if (runningScriptObj.filename === fn) { for (const k of this.runningScriptMap.keys()) {
if (pattern.test(k)) {
return true; return true;
} }
} }
@ -216,7 +217,12 @@ export abstract class BaseServer implements IServer {
* be run. * be run.
*/ */
runScript(script: RunningScript): void { 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 { setMaxRam(ram: number): void {
@ -279,4 +285,36 @@ export abstract class BaseServer implements IServer {
if (hasTextExtension(path)) return this.writeToTextFile(path, content); if (hasTextExtension(path)) return this.writeToTextFile(path, content);
return this.writeToScriptFile(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 { createRandomString } from "../utils/helpers/createRandomString";
import { createRandomIp } from "../utils/IPAddress"; 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"; import { IPAddress, ServerName } from "../Types/strings";
export interface IConstructorParams { export interface IConstructorParams {
@ -147,13 +147,14 @@ export class Server extends BaseServer {
/** Serialize the current object to a JSON save state */ /** Serialize the current object to a JSON save state */
toJSON(): IReviverValue { toJSON(): IReviverValue {
return Generic_toJSON("Server", this); return this.toJSONBase("Server", includedKeys);
} }
// Initializes a Server Object from a JSON save state // Initializes a Server Object from a JSON save state
static fromJSON(value: IReviverValue): Server { 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; constructorsForReviver.Server = Server;

@ -252,7 +252,6 @@ export function prestigeHomeComputer(homeComp: Server): void {
const hasBitflume = homeComp.programs.includes(CompletedProgramName.bitFlume); const hasBitflume = homeComp.programs.includes(CompletedProgramName.bitFlume);
homeComp.programs.length = 0; //Remove programs homeComp.programs.length = 0; //Remove programs
homeComp.runningScripts = [];
homeComp.serversOnNetwork = []; homeComp.serversOnNetwork = [];
homeComp.isConnectedTo = true; homeComp.isConnectedTo = true;
homeComp.ramUsed = 0; homeComp.ramUsed = 0;
@ -263,6 +262,9 @@ export function prestigeHomeComputer(homeComp: Server): void {
homeComp.messages.length = 0; //Remove .lit and .msg files homeComp.messages.length = 0; //Remove .lit and .msg files
homeComp.messages.push(LiteratureName.HackersStartingHandbook); 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 // 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 { dialogBoxCreate } from "../ui/React/DialogBox";
import { isPowerOfTwo } from "../utils/helpers/isPowerOfTwo"; import { isPowerOfTwo } from "../utils/helpers/isPowerOfTwo";
import { WorkerScript } from "../Netscript/WorkerScript";
import { workerScripts } from "../Netscript/WorkerScripts"; import { workerScripts } from "../Netscript/WorkerScripts";
// Returns the cost of purchasing a server with the given RAM // 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(); const home = Player.getHomeComputer();
home.serversOnNetwork = replace(home.serversOnNetwork, hostname, newName); home.serversOnNetwork = replace(home.serversOnNetwork, hostname, newName);
server.serversOnNetwork = replace(server.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.scripts.forEach((r) => (r.server = newName));
server.hostname = newName; server.hostname = newName;
workerScripts.forEach((w) => {
if (w.hostname === hostname) w.hostname = newName;
});
renameServer(hostname, newName); renameServer(hostname, newName);
}; };

@ -1,6 +1,6 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { findRunningScript } from "../../Script/ScriptHelpers"; import { findRunningScripts } from "../../Script/ScriptHelpers";
import { hasScriptExtension, validScriptExtensions } from "../../Paths/ScriptFilePath"; import { hasScriptExtension, validScriptExtensions } from "../../Paths/ScriptFilePath";
export function check(args: (string | number | boolean)[], server: BaseServer): void { 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 // Check that the script is running on this machine
const runningScript = findRunningScript(scriptName, args.slice(1), server); const runningScripts = findRunningScripts(scriptName, args.slice(1), server);
if (runningScript == null) return Terminal.error(`No script named ${scriptName} is running on the server`); if (runningScripts === null) {
runningScript.displayLog(); 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 { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { findRunningScripts } from "../../Script/ScriptHelpers";
import { killWorkerScript } from "../../Netscript/killWorkerScript"; import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
import { hasScriptExtension } from "../../Paths/ScriptFilePath"; import { hasScriptExtension } from "../../Paths/ScriptFilePath";
import type { BaseServer } from "../../Server/BaseServer";
export function kill(args: (string | number | boolean)[], server: BaseServer): void { export function kill(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length < 1) { try {
return Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]..."); 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") { if (typeof args[0] === "number") {
const pid = args[0]; 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); return;
if (runningScript == null) return Terminal.error("No such script is running. Nothing to kill"); }
killWorkerScript(runningScript.pid); const path = Terminal.getFilepath(args[0]);
Terminal.print(`Killing ${path}`); 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 { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { killWorkerScript } from "../../Netscript/killWorkerScript"; import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
import { WorkerScriptStartStopEventEmitter } from "../../Netscript/WorkerScriptStartStopEventEmitter";
export function killall(_args: (string | number | boolean)[], server: BaseServer): void { export function killall(_args: (string | number | boolean)[], server: BaseServer): void {
Terminal.print("Killing all running scripts"); Terminal.print("Killing all running scripts");
for (const runningScript of server.runningScripts) killWorkerScript(runningScript.pid); for (const byPid of server.runningScriptMap.values()) {
WorkerScriptStartStopEventEmitter.emit(); for (const runningScript of byPid.values()) {
killWorkerScriptByPid(runningScript.pid);
}
}
} }

@ -1,5 +1,6 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { matchScriptPathUnanchored } from "../../utils/helpers/scriptKey";
import * as libarg from "arg"; import * as libarg from "arg";
export function ps(args: (string | number | boolean)[], server: BaseServer): void { 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]"); Terminal.error("Incorrect usage of ps command. Usage: ps [-g, --grep pattern]");
return; return;
} }
const pattern = flags["--grep"]; let pattern = flags["--grep"];
if (pattern) { if (!pattern) {
const re = new RegExp(pattern.toString()); pattern = ".*"; // Match anything
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();
} }
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); Terminal.print(res);
} }
} }

@ -28,11 +28,6 @@ export function runScript(path: ScriptFilePath, commandArgs: (string | number |
// Todo: Switch out arg for something with typescript support // Todo: Switch out arg for something with typescript support
const args = flags["_"] as ScriptArg[]; 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); const singleRamUsage = script.getRamUsage(server.scripts);
if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script."); if (!singleRamUsage) return Terminal.error("Error while calculating ram usage for this script.");

@ -1,66 +1,38 @@
import { Terminal } from "../../Terminal"; import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer"; import { BaseServer } from "../../Server/BaseServer";
import { findRunningScriptByPid } from "../../Script/ScriptHelpers"; import { findRunningScripts, findRunningScriptByPid } from "../../Script/ScriptHelpers";
import { compareArrays } from "../../utils/helpers/compareArrays";
import { LogBoxEvents } from "../../ui/React/LogBoxManager"; import { LogBoxEvents } from "../../ui/React/LogBoxManager";
import { hasScriptExtension } from "../../Paths/ScriptFilePath"; import { hasScriptExtension } from "../../Paths/ScriptFilePath";
export function tail(commandArray: (string | number | boolean)[], server: BaseServer): void { export function tail(commandArray: (string | number | boolean)[], server: BaseServer): void {
try {
if (commandArray.length < 1) { if (commandArray.length < 1) {
return Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]..."); Terminal.error("Incorrect number of arguments. Usage: tail [script] [arg1] [arg2]...");
} } else if (typeof commandArray[0] === "string") {
if (typeof commandArray[0] === "number") { const [rawName, ...args] = commandArray;
const runningScript = findRunningScriptByPid(commandArray[0], server); const path = Terminal.getFilepath(rawName);
if (!runningScript) return Terminal.error(`No script with PID ${commandArray[0]} is running on the server`); if (!path) return Terminal.error(`Invalid filename: ${rawName}`);
LogBoxEvents.emit(runningScript);
return;
}
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.`); if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Tail can only be used on scripts.`);
// Get script arguments const candidates = findRunningScripts(path, args, server);
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;
}
// if there's no candidate then we just don't know. // 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); Terminal.print(headers);
const currRunningScripts = server.runningScripts; const currRunningScripts = server.runningScriptMap;
// Iterate through scripts on current server // Iterate through scripts on current server
for (let i = 0; i < currRunningScripts.length; i++) { for (const byPid of currRunningScripts.values()) {
const script = currRunningScripts[i]; for (const script of byPid.values()) {
// Calculate name padding // Calculate name padding
const numSpacesScript = Math.max(0, scriptWidth - script.filename.length); const numSpacesScript = Math.max(0, scriptWidth - script.filename.length);
const spacesScript = " ".repeat(numSpacesScript); const spacesScript = " ".repeat(numSpacesScript);
@ -52,3 +51,4 @@ export function top(args: (string | number | boolean)[], server: BaseServer): vo
Terminal.print(entry); Terminal.print(entry);
} }
} }
}

@ -17,6 +17,8 @@ import { Settings } from "../../Settings/Settings";
import { TablePaginationActionsAll } from "../React/TablePaginationActionsAll"; import { TablePaginationActionsAll } from "../React/TablePaginationActionsAll";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import { useRerender } from "../React/hooks"; 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 // Map of server hostname -> all workerscripts on that server for all active scripts
interface IServerData { interface IServerData {
@ -36,7 +38,13 @@ export function ServerAccordions(props: IProps): React.ReactElement {
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(Settings.ActiveScriptsServerPageSize); 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 => { const handleChangePage = (event: unknown, newPage: number): void => {
setPage(newPage); setPage(newPage);
@ -73,11 +81,16 @@ export function ServerAccordions(props: IProps): React.ReactElement {
if (data !== undefined) data.workerScripts.push(ws); if (data !== undefined) data.workerScripts.push(ws);
} }
const filtered = Object.values(serverToScriptMap).filter( // Match filter in the scriptname part of the key
(data) => const pattern = matchScriptPathUnanchored(lodash.escapeRegExp(filter));
data && const filtered = Object.values(serverToScriptMap).filter((data) => {
(data.server.hostname.includes(filter) || data.server.runningScripts.find((s) => s.filename.includes(filter))), 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)); useEffect(() => WorkerScriptStartStopEventEmitter.subscribe(rerender));

@ -24,7 +24,7 @@ import Collapse from "@mui/material/Collapse";
import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore"; import ExpandMore from "@mui/icons-material/ExpandMore";
import { killWorkerScript } from "../../Netscript/killWorkerScript"; import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
import { WorkerScript } from "../../Netscript/WorkerScript"; import { WorkerScript } from "../../Netscript/WorkerScript";
import { dialogBoxCreate } from "../React/DialogBox"; import { dialogBoxCreate } from "../React/DialogBox";
@ -53,7 +53,7 @@ export function WorkerScriptAccordion(props: IProps): React.ReactElement {
function logClickHandler(): void { function logClickHandler(): void {
LogBoxEvents.emit(scriptRef); LogBoxEvents.emit(scriptRef);
} }
const killScript = killWorkerScript.bind(null, { runningScript: scriptRef, hostname: scriptRef.server }); const killScript = killWorkerScriptByPid.bind(null, scriptRef.pid);
function killScriptClickHandler(): void { function killScriptClickHandler(): void {
if (killScript()) dialogBoxCreate("Killing script"); if (killScript()) dialogBoxCreate("Killing script");

@ -170,7 +170,7 @@ export function GameRoot(): React.ReactElement {
function killAllScripts(): void { function killAllScripts(): void {
for (const server of GetAllServers()) { for (const server of GetAllServers()) {
server.runningScripts = []; server.runningScriptMap.clear();
} }
saveObject.saveGame(); saveObject.saveGame();
setTimeout(() => htmlLocation.reload(), 2000); setTimeout(() => htmlLocation.reload(), 2000);

@ -1,7 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { EventEmitter } from "../../utils/EventEmitter"; import { EventEmitter } from "../../utils/EventEmitter";
import { RunningScript } from "../../Script/RunningScript"; import { RunningScript } from "../../Script/RunningScript";
import { killWorkerScript } from "../../Netscript/killWorkerScript"; import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
@ -14,7 +14,7 @@ import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import { workerScripts } from "../../Netscript/WorkerScripts"; import { workerScripts } from "../../Netscript/WorkerScripts";
import { startWorkerScript } from "../../NetscriptWorker"; import { startWorkerScript } from "../../NetscriptWorker";
import { GetServer } from "../../Server/AllServers"; import { GetServer } from "../../Server/AllServers";
import { findRunningScript } from "../../Script/ScriptHelpers"; import { findRunningScriptByPid } from "../../Script/ScriptHelpers";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { ANSIITypography } from "./ANSIITypography"; import { ANSIITypography } from "./ANSIITypography";
@ -160,23 +160,6 @@ function LogWindow(props: IProps): React.ReactElement {
setSize([size.width, size.height]); 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 setPosition = ({ x, y }: LogBoxPositionData) => {
const node = rootRef?.current; const node = rootRef?.current;
if (!node) return; if (!node) return;
@ -213,13 +196,13 @@ function LogWindow(props: IProps): React.ReactElement {
}, []); }, []);
function kill(): void { function kill(): void {
killWorkerScript({ runningScript: script, hostname: script.server }); killWorkerScriptByPid(script.pid);
} }
function run(): void { function run(): void {
const server = GetServer(script.server); const server = GetServer(script.server);
if (server === null) return; if (server === null) return;
const s = findRunningScript(script.filename, script.args, server); const s = findRunningScriptByPid(script.pid, server);
if (s === null) { if (s === null) {
const baseScript = server.scripts.get(script.filename); const baseScript = server.scripts.get(script.filename);
if (!baseScript) { if (!baseScript) {

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

@ -3,7 +3,5 @@
* @param decimal A decimal value to trim to two places. * @param decimal A decimal value to trim to two places.
*/ */
export function roundToTwo(decimal: number): number { export function roundToTwo(decimal: number): number {
const leftShift: number = Math.round(parseFloat(`${decimal}e+2`)); return Math.round(decimal * 100) / 100;
return +`${leftShift}e-2`;
} }

@ -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(); openV2Modal();
for (const server of GetAllServers()) { for (const server of GetAllServers()) {
server.runningScripts = []; server.runningScriptMap = new Map();
} }
saveObject.exportGame(); saveObject.exportGame();
}; };

@ -103,9 +103,12 @@ test.each([
alerted, alerted,
new Promise((resolve) => { new Promise((resolve) => {
eventDelete = WorkerScriptStartStopEventEmitter.subscribe(() => { eventDelete = WorkerScriptStartStopEventEmitter.subscribe(() => {
if (!server.runningScripts.includes(runningScript)) { for (const byPid of server.runningScriptMap.values()) {
resolve(null); for (const rs of byPid.values()) {
if (rs === runningScript) return;
} }
}
resolve(null);
}); });
}), }),
]); ]);

@ -1,6 +1,9 @@
import "../../src/Player"; import "../../src/Player";
import { loadAllServers, saveAllServers } from "../../src/Server/AllServers"; import { loadAllServers, saveAllServers } from "../../src/Server/AllServers";
import { loadAllRunningScripts } from "../../src/NetscriptWorker";
jest.useFakeTimers();
// Direct tests of loading and saving. // Direct tests of loading and saving.
// Tests here should try to be comprehensive (cover as much stuff as possible) // Tests here should try to be comprehensive (cover as much stuff as possible)
@ -46,6 +49,7 @@ function loadStandardServers() {
"pid": 3, "pid": 3,
"ramUsage": 1.6, "ramUsage": 1.6,
"server": "home", "server": "home",
"scriptKey": "script.js*[]",
"temporary": true, "temporary": true,
"dependencies": [ "dependencies": [
{ {
@ -71,6 +75,7 @@ function loadStandardServers() {
"pid": 2, "pid": 2,
"ramUsage": 1.6, "ramUsage": 1.6,
"server": "home", "server": "home",
"scriptKey": "script.js*[]",
"dependencies": [ "dependencies": [
{ {
"filename": "script.js", "filename": "script.js",
@ -81,7 +86,11 @@ function loadStandardServers() {
} }
} }
], ],
"scripts": [ "scripts": {
"ctor": "JSONMap",
"data": [
[
"script.js",
{ {
"ctor": "Script", "ctor": "Script",
"data": { "data": {
@ -107,7 +116,9 @@ function loadStandardServers() {
] ]
} }
} }
], ]
]
},
"serversOnNetwork": [ "serversOnNetwork": [
"n00dles" "n00dles"
], ],
@ -131,12 +142,13 @@ function loadStandardServers() {
} }
} }
}`); // Fix confused highlighting ` }`); // Fix confused highlighting `
loadAllRunningScripts();
} }
test("load/saveAllServers", () => { test("load/saveAllServers", () => {
// Feed a JSON object through loadAllServers/saveAllServers. // Feed a JSON object through loadAllServers/saveAllServers.
// The object is a pruned set of servers that was extracted from a real (dev) game. // The object is a pruned set of servers that was extracted from a real (dev) game.
jest.setSystemTime(123456789000);
loadStandardServers(); loadStandardServers();
// Re-stringify with indenting for nicer diffs // Re-stringify with indenting for nicer diffs

@ -21,28 +21,11 @@ exports[`load/saveAllServers 1`] = `
"programs": [ "programs": [
"NUKE.exe" "NUKE.exe"
], ],
"ramUsed": 1.6, "scripts": {
"runningScripts": [ "ctor": "JSONMap",
{ "data": [
"ctor": "RunningScript", [
"data": { "script.js",
"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", "ctor": "Script",
"data": { "data": {
@ -51,7 +34,9 @@ exports[`load/saveAllServers 1`] = `
"server": "home" "server": "home"
} }
} }
], ]
]
},
"serversOnNetwork": [ "serversOnNetwork": [
"n00dles" "n00dles"
], ],
@ -72,7 +57,28 @@ exports[`load/saveAllServers 1`] = `
"numOpenPortsRequired": 5, "numOpenPortsRequired": 5,
"openPortCount": 0, "openPortCount": 0,
"requiredHackingSkill": 1, "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": { "n00dles": {
@ -90,8 +96,6 @@ exports[`load/saveAllServers 1`] = `
"messages": [], "messages": [],
"organizationName": "Noodle Bar", "organizationName": "Noodle Bar",
"programs": [], "programs": [],
"ramUsed": 0,
"runningScripts": [],
"scripts": { "scripts": {
"ctor": "JSONMap", "ctor": "JSONMap",
"data": [] "data": []
@ -116,7 +120,8 @@ exports[`load/saveAllServers 1`] = `
"numOpenPortsRequired": 0, "numOpenPortsRequired": 0,
"openPortCount": 0, "openPortCount": 0,
"requiredHackingSkill": 1, "requiredHackingSkill": 1,
"serverGrowth": 3000 "serverGrowth": 3000,
"runningScripts": []
} }
} }
}" }"
@ -143,9 +148,11 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"programs": [ "programs": [
"NUKE.exe" "NUKE.exe"
], ],
"ramUsed": 1.6, "scripts": {
"runningScripts": [], "ctor": "JSONMap",
"scripts": [ "data": [
[
"script.js",
{ {
"ctor": "Script", "ctor": "Script",
"data": { "data": {
@ -154,7 +161,9 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"server": "home" "server": "home"
} }
} }
], ]
]
},
"serversOnNetwork": [ "serversOnNetwork": [
"n00dles" "n00dles"
], ],
@ -175,7 +184,8 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"numOpenPortsRequired": 5, "numOpenPortsRequired": 5,
"openPortCount": 0, "openPortCount": 0,
"requiredHackingSkill": 1, "requiredHackingSkill": 1,
"serverGrowth": 1 "serverGrowth": 1,
"runningScripts": []
} }
}, },
"n00dles": { "n00dles": {
@ -193,8 +203,6 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"messages": [], "messages": [],
"organizationName": "Noodle Bar", "organizationName": "Noodle Bar",
"programs": [], "programs": [],
"ramUsed": 0,
"runningScripts": [],
"scripts": { "scripts": {
"ctor": "JSONMap", "ctor": "JSONMap",
"data": [] "data": []
@ -219,7 +227,8 @@ exports[`load/saveAllServers pruning RunningScripts 1`] = `
"numOpenPortsRequired": 0, "numOpenPortsRequired": 0,
"openPortCount": 0, "openPortCount": 0,
"requiredHackingSkill": 1, "requiredHackingSkill": 1,
"serverGrowth": 3000 "serverGrowth": 3000,
"runningScripts": []
} }
} }
}" }"