PIPE: Add pipe support for passing data into and out of terminal commands (#2395)

This commit is contained in:
Michael Ficocelli
2026-02-22 12:18:23 -07:00
committed by GitHub
parent 4a22e16058
commit 92b8b58588
68 changed files with 2430 additions and 480 deletions

View File

@@ -0,0 +1,23 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [NS](./bitburner.ns.md) &gt; [getStdin](./bitburner.ns.getstdin.md)
## NS.getStdin() method
Retrieves the NetscriptPort handle used to get input piped to the script. Examples:
If a script was run with data piped into it via the terminal: `echo input1 | run myScript.js`
then `ns.getStdin().read()` inside `myScript.js` would return `"input1"`<!-- -->.
If more data is added later (for example, if one script's terminal is piped to another script), then the script can read that data from `ns.getStdin()` as well. `await ns.getStdin().nextPortWrite()` can be used to wait until new data is available to read.
**Signature:**
```typescript
getStdin(): NetscriptPort | null;
```
**Returns:**
[NetscriptPort](./bitburner.netscriptport.md) \| null

View File

@@ -1051,6 +1051,23 @@ Get the used RAM on a server.
Share power has a multiplicative effect on rep/second while doing work for a faction. Share power increases incrementally for every thread of share running on your server network, but at a sharply decreasing rate.
</td></tr>
<tr><td>
[getStdin()](./bitburner.ns.getstdin.md)
</td><td>
Retrieves the NetscriptPort handle used to get input piped to the script. Examples:
If a script was run with data piped into it via the terminal: `echo input1 | run myScript.js`
then `ns.getStdin().read()` inside `myScript.js` would return `"input1"`<!-- -->.
If more data is added later (for example, if one script's terminal is piped to another script), then the script can read that data from `ns.getStdin()` as well. `await ns.getStdin().nextPortWrite()` can be used to wait until new data is available to read.
</td></tr>
<tr><td>

View File

@@ -7,6 +7,7 @@ import { SpecialServers } from "../Server/data/SpecialServers";
import { Money } from "../ui/React/Money";
import { DarkWebItem } from "./DarkWebItem";
import { isCreateProgramWork } from "../Work/CreateProgramWork";
import type { StdIO } from "../Terminal/StdIO/StdIO";
import { CompletedProgramName } from "@enums";
import { getDarkscapeNavigator } from "../DarkNet/effects/effects";
@@ -14,7 +15,7 @@ import { getDarkscapeNavigator } from "../DarkNet/effects/effects";
export function checkIfConnectedToDarkweb(): void {
const server = Player.getCurrentServer();
if (server !== null && SpecialServers.DarkWeb == server.hostname) {
Terminal.print(
Terminal.printAndBypassPipes(
"You are now connected to the dark web. From the dark web you can purchase illegal items. " +
"Use the 'buy -l' command to display a list of all the items you can buy. Use 'buy [item-name]' " +
"to purchase an item. Use 'buy -a' to purchase all unowned items. You can use the 'buy' command anywhere, " +
@@ -23,7 +24,7 @@ export function checkIfConnectedToDarkweb(): void {
}
}
export function listAllDarkwebItems(): void {
export function listAllDarkwebItems(stdIO: StdIO): void {
for (const key of Object.keys(DarkWebItems) as (keyof typeof DarkWebItems)[]) {
const item = DarkWebItems[key];
@@ -37,11 +38,12 @@ export function listAllDarkwebItems(): void {
<>
<span>{item.program}</span> - <span>{cost}</span> - <span>{item.description}</span>
</>,
stdIO,
);
}
}
export function buyDarkwebItem(itemName: string): void {
export function buyDarkwebItem(itemName: string, stdIO: StdIO): void {
itemName = itemName.toLowerCase();
// find the program that matches, if any
@@ -56,19 +58,19 @@ export function buyDarkwebItem(itemName: string): void {
// return if invalid
if (item === null) {
Terminal.error("Unrecognized item: " + itemName);
Terminal.error("Unrecognized item: " + itemName, stdIO);
return;
}
// return if the player already has it.
if (Player.hasProgram(item.program)) {
Terminal.print("You already have the " + item.program + " program");
Terminal.print("You already have the " + item.program + " program", stdIO);
return;
}
// return if the player doesn't have enough money
if (Player.money < item.price) {
Terminal.error("Not enough money to purchase " + item.program);
Terminal.error("Not enough money to purchase " + item.program, stdIO);
return;
}
@@ -83,6 +85,7 @@ export function buyDarkwebItem(itemName: string): void {
Terminal.print(
"You have purchased the " + item.program + " program. The new program can be found on your home computer.",
stdIO,
);
if (item.program === CompletedProgramName.darkscape) {
@@ -90,7 +93,7 @@ export function buyDarkwebItem(itemName: string): void {
}
}
export function buyAllDarkwebItems(): void {
export function buyAllDarkwebItems(stdIO: StdIO): void {
const itemsToBuy: DarkWebItem[] = [];
for (const key of Object.keys(DarkWebItems) as (keyof typeof DarkWebItems)[]) {
@@ -98,21 +101,21 @@ export function buyAllDarkwebItems(): void {
if (!Player.hasProgram(item.program)) {
itemsToBuy.push(item);
if (item.price > Player.money) {
Terminal.error("Need " + formatMoney(item.price - Player.money) + " more to purchase " + item.program);
Terminal.error("Need " + formatMoney(item.price - Player.money) + " more to purchase " + item.program, stdIO);
return;
} else {
buyDarkwebItem(item.program);
buyDarkwebItem(item.program, stdIO);
}
}
}
if (itemsToBuy.length === 0) {
Terminal.print("All available programs have been purchased already.");
Terminal.print("All available programs have been purchased already.", stdIO);
return;
}
if (itemsToBuy.length > 0) {
Terminal.print("All programs have been purchased.");
Terminal.print("All programs have been purchased.", stdIO);
return;
}
}

View File

@@ -28,6 +28,9 @@
## Advanced Mechanics
- [Hacking algorithms](programming/hackingalgorithms.md)
- [IPvGO](programming/go_algorithms.md)
- [Darkweb Network](programming/darknet.md)
- [Terminal Pipes and Redirects](programming/terminal_pipes_and_redirects.md)
- [List of factions and their requirements](advanced/faction_list.md)
- [Offline scripts and bonus time](advanced/offlineandbonustime.md)
- [BitNodes](advanced/bitnodes.md)
@@ -42,8 +45,6 @@
- [Sleeves](advanced/sleeves.md)
- [Grafting](advanced/grafting.md)
- [Stanek's Gift](advanced/stanek.md)
- [IPvGO](programming/go_algorithms.md)
- [Darkweb Network](programming/darknet.md)
## Resources

View File

@@ -0,0 +1,60 @@
# Terminal Pipes and Redirects - WIP
The output of commands and scripts, that normally would be logged to the terminal, can instead be redirected and sent to another location.
For example, `echo` logs whatever input it is given.
```
[home /]> echo test123
test123
```
However, its output can instead be sent to a file using the output redirect `>` :
```
[home /]> echo test123 >> newFile.txt
```
After this, `newFile.txt` will be created (if it didn't exist) and will contain `test123`
### Accessing stdin via script
```js
/** @param {NS} ns */
async function read(ns) {
const stdin = ns.getStdin();
if (stdin.empty()) {
await stdin.nextWrite();
}
return stdin.read();
}
```
### Creating your own command line utilities
`cut.js` using `read()` from the snippet above
```js
/** @param {NS} ns */
export async function main(ns) {
if (!ns.getStdin()) {
ns.tprint("ERROR: No piped input given");
return;
}
// The '-c' flag expects a range of characters like 2-4
// Other flags, such as '-b' bytes and '-d' delimeter, are left as an excercise for the reader
const flags = ns.flags([["c", "0"]]);
const charCountRange = flags.c.split("-");
const startCharCount = Number(charCountRange[0]?.trim());
const endCharCount = Number(charCountRange[1]?.trim() ?? startCharCount);
let data = await read(ns);
while (data != null) {
// slice the characters from the input data to specified range, and print them (aka send to stdout)
// tprintf is used to avoid printing the script's filename and line number before the message
ns.tprintf("%s", data.slice(startCharCount - 1, endCharCount));
data = await read(ns);
}
}
```

View File

@@ -65,7 +65,8 @@ import file62 from "./doc/en/programming/go_algorithms.md?raw";
import file63 from "./doc/en/programming/hackingalgorithms.md?raw";
import file64 from "./doc/en/programming/learn.md?raw";
import file65 from "./doc/en/programming/remote_api.md?raw";
import file66 from "./doc/en/programming/typescript_react.md?raw";
import file66 from "./doc/en/programming/terminal_pipes_and_redirects.md?raw";
import file67 from "./doc/en/programming/typescript_react.md?raw";
import nsDoc_bitburner__valueof_md from "../../markdown/bitburner._valueof.md?raw";
import nsDoc_bitburner_activefragment_highestcharge_md from "../../markdown/bitburner.activefragment.highestcharge.md?raw";
@@ -1047,6 +1048,7 @@ import nsDoc_bitburner_ns_getserverrequiredhackinglevel_md from "../../markdown/
import nsDoc_bitburner_ns_getserversecuritylevel_md from "../../markdown/bitburner.ns.getserversecuritylevel.md?raw";
import nsDoc_bitburner_ns_getserverusedram_md from "../../markdown/bitburner.ns.getserverusedram.md?raw";
import nsDoc_bitburner_ns_getsharepower_md from "../../markdown/bitburner.ns.getsharepower.md?raw";
import nsDoc_bitburner_ns_getstdin_md from "../../markdown/bitburner.ns.getstdin.md?raw";
import nsDoc_bitburner_ns_gettotalscriptexpgain_md from "../../markdown/bitburner.ns.gettotalscriptexpgain.md?raw";
import nsDoc_bitburner_ns_gettotalscriptincome_md from "../../markdown/bitburner.ns.gettotalscriptincome.md?raw";
import nsDoc_bitburner_ns_getweakentime_md from "../../markdown/bitburner.ns.getweakentime.md?raw";
@@ -1656,7 +1658,8 @@ AllPages["en/programming/go_algorithms.md"] = file62;
AllPages["en/programming/hackingalgorithms.md"] = file63;
AllPages["en/programming/learn.md"] = file64;
AllPages["en/programming/remote_api.md"] = file65;
AllPages["en/programming/typescript_react.md"] = file66;
AllPages["en/programming/terminal_pipes_and_redirects.md"] = file66;
AllPages["en/programming/typescript_react.md"] = file67;
AllPages["nsDoc/bitburner._valueof.md"] = nsDoc_bitburner__valueof_md;
AllPages["nsDoc/bitburner.activefragment.highestcharge.md"] = nsDoc_bitburner_activefragment_highestcharge_md;
@@ -2638,6 +2641,7 @@ AllPages["nsDoc/bitburner.ns.getserverrequiredhackinglevel.md"] = nsDoc_bitburne
AllPages["nsDoc/bitburner.ns.getserversecuritylevel.md"] = nsDoc_bitburner_ns_getserversecuritylevel_md;
AllPages["nsDoc/bitburner.ns.getserverusedram.md"] = nsDoc_bitburner_ns_getserverusedram_md;
AllPages["nsDoc/bitburner.ns.getsharepower.md"] = nsDoc_bitburner_ns_getsharepower_md;
AllPages["nsDoc/bitburner.ns.getstdin.md"] = nsDoc_bitburner_ns_getstdin_md;
AllPages["nsDoc/bitburner.ns.gettotalscriptexpgain.md"] = nsDoc_bitburner_ns_gettotalscriptexpgain_md;
AllPages["nsDoc/bitburner.ns.gettotalscriptincome.md"] = nsDoc_bitburner_ns_gettotalscriptincome_md;
AllPages["nsDoc/bitburner.ns.getweakentime.md"] = nsDoc_bitburner_ns_getweakentime_md;

View File

@@ -664,6 +664,7 @@ export const RamCosts: RamCostTree<NSFull> = {
tprintRaw: 0,
printRaw: 0,
dynamicImport: 0,
getStdin: 0,
formulas: {
mockServer: 0,

View File

@@ -112,6 +112,7 @@ import { NetscriptFormat } from "./NetscriptFunctions/Format";
import { checkDarknetServer } from "./DarkNet/effects/offlineServerHandling";
import { DarknetServer } from "./Server/DarknetServer";
import { FragmentTypeEnum } from "./CotMG/FragmentType";
import { PortHandle } from "./NetscriptPort";
import { exampleDarknetServerData, ResponseCodeEnum } from "./DarkNet/Enums";
import { renderToStaticMarkup } from "react-dom/server";
import { Literatures } from "./Literature/Literatures";
@@ -427,47 +428,49 @@ export const ns: InternalAPI<NSFull> = {
throw helpers.errorMessage(ctx, "Takes at least 1 argument.");
}
const str = helpers.argsToString(args);
const stdOut = ctx.workerScript.scriptRef.terminalStdOut;
if (str.startsWith("ERROR") || str.startsWith("FAIL")) {
Terminal.error(`${ctx.workerScript.name}: ${str}`);
return;
}
if (str.startsWith("SUCCESS")) {
Terminal.success(`${ctx.workerScript.name}: ${str}`);
Terminal.success(`${ctx.workerScript.name}: ${str}`, stdOut);
return;
}
if (str.startsWith("WARN")) {
Terminal.warn(`${ctx.workerScript.name}: ${str}`);
Terminal.warn(`${ctx.workerScript.name}: ${str}`, stdOut);
return;
}
if (str.startsWith("INFO")) {
Terminal.info(`${ctx.workerScript.name}: ${str}`);
Terminal.info(`${ctx.workerScript.name}: ${str}`, stdOut);
return;
}
Terminal.print(`${ctx.workerScript.name}: ${str}`);
Terminal.print(`${ctx.workerScript.name}: ${str}`, stdOut);
},
tprintf:
(ctx) =>
(_format, ...args) => {
const format = helpers.string(ctx, "format", _format);
const str = vsprintf(format, args);
const stdOut = ctx.workerScript.scriptRef.terminalStdOut;
if (str.startsWith("ERROR") || str.startsWith("FAIL")) {
Terminal.error(`${str}`);
return;
}
if (str.startsWith("SUCCESS")) {
Terminal.success(`${str}`);
Terminal.success(`${str}`, stdOut);
return;
}
if (str.startsWith("WARN")) {
Terminal.warn(`${str}`);
Terminal.warn(`${str}`, stdOut);
return;
}
if (str.startsWith("INFO")) {
Terminal.info(`${str}`);
Terminal.info(`${str}`, stdOut);
return;
}
Terminal.print(`${str}`);
Terminal.print(`${str}`, stdOut);
},
clearLog: (ctx) => () => {
ctx.workerScript.scriptRef.clearLog();
@@ -1507,8 +1510,8 @@ export const ns: InternalAPI<NSFull> = {
const name = helpers.string(ctx, "name", _name);
return getRamCost(name.split("."), true);
},
tprintRaw: () => (value) => {
Terminal.printRaw(wrapUserNode(value));
tprintRaw: (ctx) => (value) => {
Terminal.printRaw(wrapUserNode(value), ctx.workerScript.scriptRef.terminalStdOut);
},
printRaw: (ctx) => (value) => {
ctx.workerScript.print(wrapUserNode(value));
@@ -1524,6 +1527,10 @@ export const ns: InternalAPI<NSFull> = {
//Script **must** be a script at this point
return compile(script as Script, server.scripts);
},
getStdin: (ctx) => () => {
const stdinHandle = ctx.workerScript.scriptRef.stdin?.handle;
return stdinHandle ? new PortHandle(stdinHandle.n) : null;
},
flags: Flags,
heart: { break: () => () => Player.karma },
...NetscriptExtra(),

View File

@@ -468,7 +468,7 @@ export function NetscriptSingularity(): InternalAPI<ISingularity> {
helpers.checkSingularityAccess(ctx);
const filename = helpers.string(ctx, "filename", _filename);
const server = Player.getCurrentServer();
cat([filename], server);
cat([filename], server, ctx.workerScript.scriptRef.terminalStdOut);
},
connect: (ctx) => (_host?) => {
helpers.checkSingularityAccess(ctx);

View File

@@ -1,6 +1,7 @@
import type { CompletedProgramName } from "@enums";
import { ProgramFilePath, asProgramFilePath } from "../Paths/ProgramFilePath";
import { BaseServer } from "../Server/BaseServer";
import type { StdIO } from "../Terminal/StdIO/StdIO";
export interface IProgramCreate {
level: number;
@@ -11,14 +12,14 @@ export interface IProgramCreate {
interface ProgramConstructorParams {
name: CompletedProgramName;
create: IProgramCreate | null;
run: (args: string[], server: BaseServer) => void;
run: (args: string[], server: BaseServer, stdIO: StdIO) => void;
nsMethod?: string;
}
export class Program {
name: ProgramFilePath & CompletedProgramName;
create: IProgramCreate | null;
run: (args: string[], server: BaseServer) => void;
run: (args: string[], server: BaseServer, stdIO: StdIO) => void;
nsMethod?: string;
constructor({ name, create, run, nsMethod }: ProgramConstructorParams) {

View File

@@ -16,6 +16,7 @@ import { Page } from "../ui/Router";
import { knowAboutBitverse } from "../BitNode/BitNodeUtils";
import { handleStormSeed } from "../DarkNet/effects/webstorm";
import { clampNumber } from "../utils/helpers/clampNumber";
import type { StdIO } from "../Terminal/StdIO/StdIO";
function requireHackingLevel(lvl: number) {
return function () {
@@ -33,7 +34,7 @@ function bitFlumeRequirements() {
};
}
function warnIfNonArgProgramIsRunWithArgs(name: CompletedProgramName, args: string[]): void {
function warnIfNonArgProgramIsRunWithArgs(name: CompletedProgramName, args: string[], stdIO: StdIO): void {
if (args.length === 0) {
return;
}
@@ -41,6 +42,7 @@ function warnIfNonArgProgramIsRunWithArgs(name: CompletedProgramName, args: stri
`You are running ${name} with arguments, but ${name} does not accept arguments. These arguments will be ignored. ` +
`${name} only affects the server ('${Player.currentServer}') that you are connecting via the terminal. ` +
"If you want to pass the target's hostname as an argument, you have to use the respective NS API.",
stdIO,
);
}
@@ -54,25 +56,25 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(1),
time: CONSTANTS.MillisecondsPerFiveMinutes,
},
run: (args: string[], server: BaseServer): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.nuke, args);
run: (args: string[], server: BaseServer, stdIO: StdIO): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.nuke, args, stdIO);
if (!(server instanceof Server)) {
Terminal.error("Cannot nuke this kind of server.");
Terminal.error("Cannot nuke this kind of server.", stdIO);
return;
}
if (server.hasAdminRights) {
Terminal.print("You already have root access to this computer. There is no reason to run NUKE.exe");
Terminal.print("You can now run scripts on this server.");
Terminal.print("You already have root access to this computer. There is no reason to run NUKE.exe", stdIO);
Terminal.print("You can now run scripts on this server.", stdIO);
return;
}
if (server.openPortCount >= server.numOpenPortsRequired) {
server.hasAdminRights = true;
Terminal.print("NUKE successful! Gained root access to " + server.hostname);
Terminal.print("You can now run scripts on this server.");
Terminal.print("NUKE successful! Gained root access to " + server.hostname, stdIO);
Terminal.print("You can now run scripts on this server.", stdIO);
return;
}
Terminal.print("NUKE unsuccessful. Not enough ports have been opened");
Terminal.print("NUKE unsuccessful. Not enough ports have been opened", stdIO);
},
}),
[CompletedProgramName.bruteSsh]: new Program({
@@ -84,19 +86,19 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(50),
time: CONSTANTS.MillisecondsPerFiveMinutes * 2,
},
run: (args: string[], server: BaseServer): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.bruteSsh, args);
run: (args: string[], server: BaseServer, stdIO: StdIO): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.bruteSsh, args, stdIO);
if (!(server instanceof Server)) {
Terminal.error("Cannot run BruteSSH.exe on this kind of server.");
Terminal.error("Cannot run BruteSSH.exe on this kind of server.", stdIO);
return;
}
if (server.sshPortOpen) {
Terminal.print("SSH Port (22) is already open!");
Terminal.print("SSH Port (22) is already open!", stdIO);
return;
}
server.sshPortOpen = true;
Terminal.print("Opened SSH Port(22)!");
Terminal.print("Opened SSH Port(22)!", stdIO);
server.openPortCount++;
},
}),
@@ -109,19 +111,19 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(100),
time: CONSTANTS.MillisecondsPerHalfHour,
},
run: (args: string[], server: BaseServer): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.ftpCrack, args);
run: (args: string[], server: BaseServer, stdIO: StdIO): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.ftpCrack, args, stdIO);
if (!(server instanceof Server)) {
Terminal.error("Cannot run FTPCrack.exe on this kind of server.");
Terminal.error("Cannot run FTPCrack.exe on this kind of server.", stdIO);
return;
}
if (server.ftpPortOpen) {
Terminal.print("FTP Port (21) is already open!");
Terminal.print("FTP Port (21) is already open!", stdIO);
return;
}
server.ftpPortOpen = true;
Terminal.print("Opened FTP Port (21)!");
Terminal.print("Opened FTP Port (21)!", stdIO);
server.openPortCount++;
},
}),
@@ -134,19 +136,19 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(250),
time: CONSTANTS.MillisecondsPer2Hours,
},
run: (args: string[], server: BaseServer): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.relaySmtp, args);
run: (args: string[], server: BaseServer, stdIO: StdIO): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.relaySmtp, args, stdIO);
if (!(server instanceof Server)) {
Terminal.error("Cannot run relaySMTP.exe on this kind of server.");
Terminal.error("Cannot run relaySMTP.exe on this kind of server.", stdIO);
return;
}
if (server.smtpPortOpen) {
Terminal.print("SMTP Port (25) is already open!");
Terminal.print("SMTP Port (25) is already open!", stdIO);
return;
}
server.smtpPortOpen = true;
Terminal.print("Opened SMTP Port (25)!");
Terminal.print("Opened SMTP Port (25)!", stdIO);
server.openPortCount++;
},
}),
@@ -159,19 +161,19 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(500),
time: CONSTANTS.MillisecondsPer4Hours,
},
run: (args: string[], server: BaseServer): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.httpWorm, args);
run: (args: string[], server: BaseServer, stdIO: StdIO): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.httpWorm, args, stdIO);
if (!(server instanceof Server)) {
Terminal.error("Cannot run HTTPWorm.exe on this kind of server.");
Terminal.error("Cannot run HTTPWorm.exe on this kind of server.", stdIO);
return;
}
if (server.httpPortOpen) {
Terminal.print("HTTP Port (80) is already open!");
Terminal.print("HTTP Port (80) is already open!", stdIO);
return;
}
server.httpPortOpen = true;
Terminal.print("Opened HTTP Port (80)!");
Terminal.print("Opened HTTP Port (80)!", stdIO);
server.openPortCount++;
},
}),
@@ -184,19 +186,19 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(750),
time: CONSTANTS.MillisecondsPer8Hours,
},
run: (args: string[], server: BaseServer): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.sqlInject, args);
run: (args: string[], server: BaseServer, stdIO: StdIO): void => {
warnIfNonArgProgramIsRunWithArgs(CompletedProgramName.sqlInject, args, stdIO);
if (!(server instanceof Server)) {
Terminal.error("Cannot run SQLInject.exe on this kind of server.");
Terminal.error("Cannot run SQLInject.exe on this kind of server.", stdIO);
return;
}
if (server.sqlPortOpen) {
Terminal.print("SQL Port (1433) is already open!");
Terminal.print("SQL Port (1433) is already open!", stdIO);
return;
}
server.sqlPortOpen = true;
Terminal.print("Opened SQL Port (1433)!");
Terminal.print("Opened SQL Port (1433)!", stdIO);
server.openPortCount++;
},
}),
@@ -208,9 +210,9 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(75),
time: CONSTANTS.MillisecondsPerQuarterHour,
},
run: (): void => {
Terminal.print("This executable cannot be run.");
Terminal.print("DeepscanV1.exe lets you run 'scan-analyze' with a depth up to 5.");
run: (__, ___, stdIO: StdIO): void => {
Terminal.print("This executable cannot be run.", stdIO);
Terminal.print("DeepscanV1.exe lets you run 'scan-analyze' with a depth up to 5.", stdIO);
},
}),
[CompletedProgramName.deepScan2]: new Program({
@@ -221,9 +223,9 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(400),
time: CONSTANTS.MillisecondsPer2Hours,
},
run: (): void => {
Terminal.print("This executable cannot be run.");
Terminal.print("DeepscanV2.exe lets you run 'scan-analyze' with a depth up to 10.");
run: (__, ___, stdIO: StdIO): void => {
Terminal.print("This executable cannot be run.", stdIO);
Terminal.print("DeepscanV2.exe lets you run 'scan-analyze' with a depth up to 10.", stdIO);
},
}),
[CompletedProgramName.serverProfiler]: new Program({
@@ -235,44 +237,47 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(75),
time: CONSTANTS.MillisecondsPerHalfHour,
},
run: (args: string[]): void => {
run: (args: string[], __, stdIO: StdIO): void => {
if (args.length !== 1) {
Terminal.error("Must pass a server hostname or IP as an argument for ServerProfiler.exe");
Terminal.error("Must pass a server hostname or IP as an argument for ServerProfiler.exe", stdIO);
return;
}
const targetServer = GetServer(args[0]);
if (targetServer == null) {
Terminal.error("Invalid server IP/hostname");
Terminal.error("Invalid server IP/hostname", stdIO);
return;
}
if (!(targetServer instanceof Server)) {
Terminal.error(`ServerProfiler.exe can only be run on normal servers.`);
Terminal.error(`ServerProfiler.exe can only be run on normal servers.`, stdIO);
return;
}
Terminal.print(targetServer.hostname + ":");
Terminal.print("Server base security level: " + targetServer.baseDifficulty);
Terminal.print("Server current security level: " + targetServer.hackDifficulty);
Terminal.print("Server growth rate: " + targetServer.serverGrowth);
Terminal.print(targetServer.hostname + ":", stdIO);
Terminal.print("Server base security level: " + targetServer.baseDifficulty, stdIO);
Terminal.print("Server current security level: " + targetServer.hackDifficulty, stdIO);
Terminal.print("Server growth rate: " + targetServer.serverGrowth, stdIO);
Terminal.print(
`Netscript hack() execution time: ${convertTimeMsToTimeElapsedString(
calculateHackingTime(targetServer, Player) * 1000,
true,
)}`,
stdIO,
);
Terminal.print(
`Netscript grow() execution time: ${convertTimeMsToTimeElapsedString(
calculateGrowTime(targetServer, Player) * 1000,
true,
)}`,
stdIO,
);
Terminal.print(
`Netscript weaken() execution time: ${convertTimeMsToTimeElapsedString(
calculateWeakenTime(targetServer, Player) * 1000,
true,
)}`,
stdIO,
);
},
}),
@@ -284,10 +289,10 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(25),
time: CONSTANTS.MillisecondsPerQuarterHour,
},
run: (): void => {
Terminal.print("This executable cannot be run.");
Terminal.print("AutoLink.exe lets you automatically connect to other servers when using 'scan-analyze'.");
Terminal.print("When using scan-analyze, click on a server's hostname to connect to it.");
run: (__, ___, stdIO: StdIO): void => {
Terminal.print("This executable cannot be run.", stdIO);
Terminal.print("AutoLink.exe lets you automatically connect to other servers when using 'scan-analyze'.", stdIO);
Terminal.print("When using scan-analyze, click on a server's hostname to connect to it.", stdIO);
},
}),
[CompletedProgramName.formulas]: new Program({
@@ -298,9 +303,9 @@ export const Programs: Record<CompletedProgramName, Program> = {
req: requireHackingLevel(1000),
time: CONSTANTS.MillisecondsPer4Hours,
},
run: (): void => {
Terminal.print("This executable cannot be run.");
Terminal.print("Formulas.exe lets you use the formulas API.");
run: (__, ___, stdIO: StdIO): void => {
Terminal.print("This executable cannot be run.", stdIO);
Terminal.print("Formulas.exe lets you use the formulas API.", stdIO);
},
}),
[CompletedProgramName.bitFlume]: new Program({
@@ -324,43 +329,45 @@ export const Programs: Record<CompletedProgramName, Program> = {
[CompletedProgramName.flight]: new Program({
name: CompletedProgramName.flight,
create: null,
run: (): void => {
run: (__, ___, stdIO: StdIO): void => {
const numAugReq = currentNodeMults.DaedalusAugsRequirement;
const fulfilled =
Player.augmentations.length >= numAugReq && Player.money >= 1e11 && Player.skills.hacking >= 2500;
if (!fulfilled) {
if (Player.augmentations.length >= numAugReq) {
Terminal.print(`[x] Augmentations: ${Player.augmentations.length} / ${numAugReq}`);
Terminal.print(`[x] Augmentations: ${Player.augmentations.length} / ${numAugReq}`, stdIO);
} else {
Terminal.print(`[ ] Augmentations: ${Player.augmentations.length} / ${numAugReq}`);
Terminal.print(`[ ] Augmentations: ${Player.augmentations.length} / ${numAugReq}`, stdIO);
}
if (Player.money >= 1e11) {
Terminal.print(`[x] Money: ${formatMoney(Player.money)} / ${formatMoney(1e11)}`);
Terminal.print(`[x] Money: ${formatMoney(Player.money)} / ${formatMoney(1e11)}`, stdIO);
} else {
Terminal.print(`[ ] Money: ${formatMoney(Player.money)} / ${formatMoney(1e11)}`);
Terminal.print(`[ ] Money: ${formatMoney(Player.money)} / ${formatMoney(1e11)}`, stdIO);
}
if (Player.skills.hacking >= 2500) {
Terminal.print(`[x] Hacking skill: ${Player.skills.hacking} / 2500`);
Terminal.print(`[x] Hacking skill: ${Player.skills.hacking} / 2500`, stdIO);
} else {
Terminal.print(`[ ] Hacking skill: ${Player.skills.hacking} / 2500`);
Terminal.print(`[ ] Hacking skill: ${Player.skills.hacking} / 2500`, stdIO);
}
return;
}
Terminal.print("We will contact you.");
Terminal.print(`-- ${FactionName.Daedalus} --`);
Terminal.print("We will contact you.", stdIO);
Terminal.print(`-- ${FactionName.Daedalus} --`, stdIO);
},
}),
[CompletedProgramName.darkscape]: new Program({
name: CompletedProgramName.darkscape,
create: null,
run: (): void => {
Terminal.print("This program gives access to the dark net.");
run: (__, ___, stdIO: StdIO): void => {
Terminal.print("This program gives access to the dark net.", stdIO);
Terminal.print(
"The dark net is an unstable, constantly shifting network of servers that are only connected to the normal network through the darkweb server.",
stdIO,
);
Terminal.print(
"This network can be accessed using the `ns.dnet` api functions, or the DarkNet UI on the left-hand panel.",
stdIO,
);
},
}),
@@ -368,8 +375,8 @@ export const Programs: Record<CompletedProgramName, Program> = {
name: CompletedProgramName.stormSeed,
nsMethod: "dnet.unleashStormSeed",
create: null,
run: (): void => {
Terminal.print("You can feel a storm approaching...");
run: (__, ___, stdIO: StdIO): void => {
Terminal.print("You can feel a storm approaching...", stdIO);
const connectedServer = Player.getCurrentServer();
handleStormSeed(connectedServer);
},

View File

@@ -19,6 +19,10 @@ import { ScriptKey, scriptKey } from "../utils/helpers/scriptKey";
import type { LogBoxProperties } from "../ui/React/LogBoxManager";
import { StdIO } from "../Terminal/StdIO/StdIO";
import { IOStream } from "../Terminal/StdIO/IOStream";
import { getTerminalStdIO } from "../Terminal/StdIO/RedirectIO";
export class RunningScript {
// Script arguments
args: ScriptArg[] = [];
@@ -70,9 +74,17 @@ export class RunningScript {
// Cached key for ByArgs lookups. Will be overwritten by a correct ScriptKey in fromJSON or constructor
scriptKey = "" as ScriptKey;
stdin: IOStream | null = null;
// Access to properties of the tail window. Can be used to get/set size, position, etc.
tailProps = null as LogBoxProperties | null;
// Configuration for piping the script's tail output
tailStdOut: StdIO | null = null;
// Configuration for piping the script's terminal output
terminalStdOut: StdIO = getTerminalStdIO(null);
// The title, as shown in the script's log box. Defaults to the name + args,
// but can be changed by the user. If it is set to a React element (only by the user),
// that will not be persisted, and will be restored to default on load.
@@ -111,14 +123,16 @@ export class RunningScript {
this.logs.push(logEntry);
this.logUpd = true;
this.tailStdOut?.write?.(logEntry);
}
displayLog(): void {
displayLog(stdIO: StdIO): void {
for (const log of this.logs) {
if (typeof log === "string") {
Terminal.print(log);
Terminal.print(log, stdIO);
} else {
Terminal.printRaw(log);
Terminal.printRaw(log, stdIO);
}
}
}

View File

@@ -8991,6 +8991,21 @@ export interface NS {
*/
dynamicImport(path: string): Promise<any>;
/**
* Retrieves the NetscriptPort handle used to get input piped to the script.
* Examples:
*
* If a script was run with data piped into it via the terminal:
* `echo input1 | run myScript.js`
*
* then `ns.getStdin().read()` inside `myScript.js` would return `"input1"`.
*
* If more data is added later (for example, if one script's terminal is piped to another script),
* then the script can read that data from `ns.getStdin()` as well.
* `await ns.getStdin().nextPortWrite()` can be used to wait until new data is available to read.
*/
getStdin(): NetscriptPort | null;
enums: NSEnums;
}

View File

@@ -5,7 +5,7 @@ export const TerminalHelpText: string[] = [
" analyze Get information about the current machine ",
" backdoor Install a backdoor on the current machine ",
" buy [-l/-a/program] Purchase a program through the Dark Web",
" cat [file] Display a .msg, .lit, or text file",
" cat [file]... Display a .msg, .lit, or text file, or concatenate multiple together",
" cd [dir] Change to a new directory",
" changelog Display changelog",
" check [script] [args...] Print a script's logs to Terminal",
@@ -14,6 +14,7 @@ export const TerminalHelpText: string[] = [
" connect [hostname] Connects to a remote server",
" cp [src] [dest] Copy a file",
" download [script/text file] Downloads scripts or text files to your computer",
" echo [string] Print the specified string to the terminal.",
" expr [math expression] Evaluate a mathematical expression",
" free Check the machine's memory (RAM) usage",
" grep [opts]... pattern [file]... Search for PATTERN (string/regular expression) in each FILE and print results to terminal",
@@ -140,7 +141,7 @@ export const HelpTexts: Record<string, string[]> = {
" ",
],
cat: [
"Usage: cat [file name]",
"Usage: cat [file name]...",
" ",
"Display message (.msg), literature (.lit), script (.js, .jsx, .ts, .tsx), or text (.txt, .json, .css) files. Examples:",
" ",
@@ -150,6 +151,24 @@ export const HelpTexts: Record<string, string[]> = {
" ",
" cat servers.txt",
" ",
"Can be used to concatenate multiple files and/or piped input together. Examples:",
" ",
" cat j1.msg foo.lit",
" ",
" cat servers.txt scripts/hack.js logs.txt",
" ",
"If a hyphen (-) is provided as an argument, cat will read from stdin at that location.",
"If not provided, any stdin will be placed at the end or the concatenated output.",
" ",
"This example pipes 'some text' in between the contents of file1.txt and file2.txt, and writes the result to the terminal:",
" ",
" echo some text | cat file1.txt - file2.txt",
" ",
"The output of cat can be redirected (as can all commands that log text).",
"For example, this duplicates the contents of file1.js into file3.js:",
" ",
" cat file1.js > file3.js",
" ",
],
cd: [
"Usage: cd [dir]",
@@ -215,6 +234,15 @@ export const HelpTexts: Record<string, string[]> = {
"Download all text files: download *.txt",
" ",
],
echo: [
"Usage: echo [string]",
" ",
"Print the specified string to the terminal. This command is mostly useful for piping",
" ",
"Example: echo 'Text To Store In File' > newFile.txt",
" ",
"Example: echo 'Text To Search In' | grep To",
],
expr: [
"Usage: expr [mathematical expression]",
" ",

View File

@@ -1,11 +1,15 @@
import { trimQuotes } from "../utils/helpers/string";
import { substituteAliases } from "../Alias";
import { Terminal } from "../Terminal";
// Helper function to parse individual arguments into number/boolean/string as appropriate
function parseArg(arg: string): string | number | boolean {
if (arg === "true") return true;
if (arg === "false") return false;
const argAsNumber = Number(arg);
if (!isNaN(argAsNumber)) return argAsNumber;
if (arg === "$!") {
return Terminal.pidOfLastScriptRun ?? -1;
}
return trimQuotes(arg);
}

View File

@@ -0,0 +1,55 @@
import { NetscriptPort } from "@nsdefs";
import { PortHandle } from "../../NetscriptPort";
import { getNextStdinHandle } from "./utils";
export class IOStream implements NetscriptPort {
isClosed: boolean = false;
handle: PortHandle = getNextStdinHandle();
close(): void {
this.write(null);
}
write(value: any): unknown {
if (this.isClosed) {
return;
}
if (value === null) {
this.isClosed = true;
}
return this.handle.write(value);
}
tryWrite(value: any): boolean {
if (this.isClosed) {
return false;
}
this.write(value);
return true;
}
clear(): void {
this.handle.clear();
}
empty(): boolean {
return this.handle.empty();
}
full(): boolean {
return this.handle.full();
}
nextWrite(): Promise<void> {
return this.handle.nextWrite();
}
peek(): unknown {
return this.handle.peek();
}
read(): unknown {
return this.handle.read();
}
}

View File

@@ -0,0 +1,227 @@
import { parseCommand } from "../Parser";
import { IOStream } from "./IOStream";
import { StdIO } from "./StdIO";
import { Terminal } from "../../Terminal";
import { hasTextExtension } from "../../Paths/TextFilePath";
import { hasScriptExtension, resolveScriptFilePath } from "../../Paths/ScriptFilePath";
import { Player } from "@player";
import { Settings } from "../../Settings/Settings";
import { Args, isPipeSymbol, PipeSymbols, stringify } from "./utils";
import { sleep } from "../../utils/Utility";
export async function parseRedirectedCommands(commandString: string) {
const parsed = parseCommand(commandString);
const commandSets = findCommandsSplitByRedirects(parsed);
if (commandSets.length <= 1) {
return Terminal.executeCommand(commandString, getTerminalStdIO(null));
}
const stdIOChain = buildStdIOChain(commandSets.length);
const openPipes: Promise<void>[] = [];
let longRunningCommandUsed = false;
for (let i = 0; i < commandSets.length; i++) {
const commandSet = commandSets[i];
const stdIO = stdIOChain[i];
handleCommand(stdIO, commandSet);
longRunningCommandUsed ||= isLongRunningCommand(commandSet);
openPipes.push(longRunningCommandUsed ? sleep(0) : waitUntilClosed(stdIO));
}
// Allow the IO chain to pass data through its async iterators
await Promise.all(openPipes);
return true;
}
export function handleCommand(stdIO: StdIO, commandStrings: Args[]) {
const pipeSymbol = isPipeSymbol(commandStrings[0]) ? `${commandStrings[0]}` : null;
const command = `${pipeSymbol ? commandStrings[1] : commandStrings[0]}`;
const args = pipeSymbol ? commandStrings.slice(2) : commandStrings.slice(1);
if (!command) {
return handleIoError(stdIO, `Invalid command string: no command found after output redirect ${pipeSymbol}.`);
}
// Pipe to file
if (command && (hasTextExtension(command) || (hasScriptExtension(command) && pipeSymbol !== PipeSymbols.Pipe))) {
return handlePipeToFile(command, pipeSymbol, stdIO);
}
// > and >> are invalid pipes for commands that are not piping to files
if (pipeSymbol === PipeSymbols.OutputRedirection || pipeSymbol === PipeSymbols.AppendOutputRedirection) {
return handleIoError(
stdIO,
`Invalid pipe symbol '${pipeSymbol}' for command: ${command}. > and >> can only be used to pipe into files.`,
);
}
const commandArgs = args.map((arg) => (`${arg}`.includes(" ") ? `"${arg}"` : `${arg}`));
const commandString = [command, ...commandArgs].join(" ");
Terminal.executeCommand(commandString, stdIO);
}
export function buildStdIOChain(length: number, initialStdIO: StdIO | null = null): StdIO[] {
const stdIOs: StdIO[] = [];
let priorStdIO = initialStdIO;
for (let i = 0; i < length; i++) {
const newStdIO = new StdIO(priorStdIO?.stdout ?? null);
stdIOs.push(newStdIO);
priorStdIO = newStdIO;
}
stdIOs[stdIOs.length - 1].stdout = null; // Last StdIO writes to terminal
return stdIOs;
}
export function findCommandsSplitByRedirects(commands: Args[]) {
const result: Args[][] = [];
let currentCommand: Args[] = [];
for (const token of commands) {
if (isPipeSymbol(token)) {
result.push(currentCommand);
currentCommand = [token];
} else {
currentCommand.push(token);
}
}
result.push(currentCommand);
for (const [index, commandGroup] of result.entries()) {
if (index !== 1 && commandGroup[0] === PipeSymbols.InputRedirection) {
handleIoError(
getTerminalStdIO(),
`Error in pipe command: Invalid pipe command. Only the first command in a pipe chain can have input redirection '<'.`,
);
return [];
}
}
// If the second command starts with an input redirection, convert it to a simple pipe.
if (result[1]?.[0] === PipeSymbols.InputRedirection) {
const inputRedirectCommand = result.splice(1, 1)[0];
result.unshift(["cat", ...inputRedirectCommand.slice(1)]);
result[1].unshift(PipeSymbols.Pipe);
}
return result;
}
export function getTerminalStdIO(stdin: IOStream | null = null) {
return new StdIO(stdin, null);
}
function handlePipeToFile(fileName: string, pipeType: string | null, stdIO: StdIO) {
if (!pipeType) {
return handleIoError(stdIO, `Invalid command string: no pipe symbol found for piping to file ${fileName}.`);
}
if (pipeType !== PipeSymbols.OutputRedirection && pipeType !== PipeSymbols.AppendOutputRedirection) {
return handleIoError(
stdIO,
`Invalid pipe symbol '${pipeType}' for piping to file ${fileName}. Only > and >> are allowed.`,
);
}
// No output from writing to files
stdIO.stdout?.close();
if (hasTextExtension(fileName)) {
writeToTextFile(fileName, pipeType, stdIO);
} else if (hasScriptExtension(fileName)) {
writeToScriptFile(fileName, pipeType, stdIO);
} else {
return handleIoError(stdIO, `Invalid file extension for piping to file: ${fileName}`);
}
}
function writeToTextFile(filename: string, pipeType: string, stdIO: StdIO) {
const filePath = Terminal.getFilepath(filename);
if (!filePath || !hasTextExtension(filePath)) {
return handleIoError(stdIO, `Invalid file path provided: ${filename}`);
}
if (!Terminal.getFile(filePath)) {
Player.getCurrentServer().writeToTextFile(filePath, "");
}
const file = Terminal.getTextFile(filePath);
const overwrite = pipeType === PipeSymbols.OutputRedirection;
if (!file) {
return handleIoError(stdIO, `Failed to create text file for piping output: ${filePath}`);
}
if (file?.content && overwrite) {
file.content = "";
}
void callOnRead(stdIO, (data: unknown) => {
const currentFile = Terminal.getTextFile(filePath);
if (!currentFile) {
return;
}
const output = stringify(data);
currentFile.content = concatenateFileContents(currentFile.content, output);
});
}
function writeToScriptFile(filename: string, pipeType: string, stdIO: StdIO): void {
const scriptPath = Terminal.getFilepath(filename);
if (!scriptPath || !hasScriptExtension(scriptPath)) {
return handleIoError(stdIO, `Invalid file path provided: ${filename}`);
}
const overwrite = pipeType === PipeSymbols.OutputRedirection;
void callOnRead(stdIO, (data: unknown) => {
if (!Terminal.getScript(scriptPath)) {
Player.getCurrentServer().writeToScriptFile(scriptPath, "");
}
const file = Terminal.getScript(scriptPath);
if (!file) {
return handleIoError(stdIO, `Failed to create script file for piping output: ${scriptPath}`);
}
if (file?.content && overwrite) {
return handleIoError(
stdIO,
`Overwriting non-empty script files is forbidden. Attempted to overwrite ${scriptPath}`,
);
}
const output = stringify(data);
file.content = concatenateFileContents(file.content, output);
});
}
export async function callOnRead(stdIO: StdIO, callback: (data: unknown, stdIO: StdIO) => Promise<void> | void) {
for await (const data of stdIO.read()) {
const streamIsCleared = stdIO.stdin?.deref()?.isClosed && stdIO.stdin?.deref()?.empty();
if (data === null || streamIsCleared) {
return;
}
await callback(data, stdIO);
}
}
function handleIoError(stdIO: StdIO, error: string) {
Terminal.error(error, stdIO);
}
function isLongRunningCommand(commandSet: Args[]) {
const pipeSymbol = isPipeSymbol(commandSet[0]) ? `${commandSet[0]}` : null;
const command = `${pipeSymbol ? commandSet[1] : commandSet[0]}`;
return ["wget", "tail", "run"].includes(command) || !!resolveScriptFilePath(command);
}
function concatenateFileContents(content: string, newContent: string): string {
const concatenatedContent = content + (content ? "\n" : "") + newContent;
const splitLines = concatenatedContent.split("\n");
if (splitLines.length > Settings.MaxTerminalCapacity * 5) {
const truncatedFileContent = splitLines.slice(-Settings.MaxTerminalCapacity * 5).join("\n");
return `(File truncated at ${Settings.MaxTerminalCapacity * 5} lines)\n${truncatedFileContent}`;
}
return concatenatedContent;
}
async function waitUntilClosed(stdio: StdIO): Promise<void> {
while (stdio.stdout && !stdio.stdout?.isClosed) {
await stdio.stdout.nextWrite();
}
}

View File

@@ -0,0 +1,81 @@
import { IOStream } from "./IOStream";
import { Terminal } from "../../Terminal";
import { Output, RawOutput, Link } from "../OutputTypes";
import { stringify } from "./utils";
let remaining = 0;
const registerStdIOInstance = (stdIO: StdIO) => {
const id = `StdIO-${Math.random().toString(16).slice(2)}`;
StdIORegistry.register(stdIO, id);
remaining++;
console.debug(`Created StdIO instance ${id}. Instances remaining: ${remaining}`);
};
const StdIORegistry = new FinalizationRegistry((name: string) => {
remaining--;
console.debug(`StdIO instance ${name} has been garbage collected. Remaining instances: ${remaining}`);
});
export class StdIO {
stdin: WeakRef<IOStream> | null = null;
stdout: IOStream | null;
constructor(stdin: IOStream | null, stdout: IOStream | null = new IOStream()) {
if (stdin) {
this.stdin = new WeakRef(stdin);
}
this.stdout = stdout;
registerStdIOInstance(this);
}
// Async iterator to read from stdin
async *[Symbol.asyncIterator]() {
const stdin = this.stdin?.deref();
if (!stdin || (stdin.isClosed && stdin.empty())) {
return;
}
while (!stdin.isClosed || !stdin.empty()) {
if (stdin.empty() && !stdin.isClosed) {
await stdin.nextWrite();
}
yield stdin.read();
}
}
// Read from stdin via the async iterator
read() {
return this[Symbol.asyncIterator]();
}
getAllCurrentStdin(includeNewlines = true): string {
const stdin = this.stdin?.deref();
if (!stdin) {
return "";
}
const inputs: string[] = [];
while (!stdin.empty()) {
const input = stdin.read();
if (input === null) {
break;
}
inputs.push(stringify(input));
}
return inputs.map((i) => `${i}${includeNewlines ? "\n" : ""}`).join("");
}
write(data: unknown): unknown {
if (this.stdout) {
return this.stdout.write(stringify(data, true));
}
// If there is no stdout, write to the terminal
if (data instanceof Output || data instanceof Link || data instanceof RawOutput) {
return Terminal.terminalOutput(data);
}
Terminal.printAndBypassPipes(stringify(data));
}
close(): void {
this.stdout?.close();
this.stdin?.deref()?.close();
}
}

View File

@@ -0,0 +1,67 @@
import React, { isValidElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { Link, Output, RawOutput } from "../OutputTypes";
import { ANSI_ESCAPE } from "../../ui/React/ANSIITypography";
import { PortHandle, PortNumber } from "../../NetscriptPort";
import { parseCommand } from "../Parser";
export type Args = string | number | boolean;
export const PipeSymbols = {
Pipe: "|",
OutputRedirection: ">",
AppendOutputRedirection: ">>",
InputRedirection: "<",
} as const;
export function isPipeSymbol(symbol: string | number | boolean): boolean {
return Object.keys(PipeSymbols).some((key) => PipeSymbols[key as keyof typeof PipeSymbols] === symbol);
}
export function stringify(s: unknown, stripAnsiEscape = false): string {
if (s == null) {
return "";
} else if (s instanceof Output) {
return clean(s.text, stripAnsiEscape);
} else if (s instanceof Link) {
return `${s.dashes} ${s.hostname}`;
} else if (s instanceof RawOutput) {
// TODO: test
return stringifyReactElement(s.raw);
} else if (isValidElement(s)) {
return stringifyReactElement(s);
} else if (s instanceof HTMLElement) {
return s.innerText;
} else if (typeof s === "string" || typeof s === "number" || typeof s === "boolean") {
return clean(s.toString(), stripAnsiEscape);
} else {
return clean(JSON.stringify(s), stripAnsiEscape);
}
}
export function stringifyReactElement(element: React.ReactNode): string {
const markup = renderToStaticMarkup(<>{element}</>);
const div = document.createElement("div");
div.innerHTML = markup.replaceAll(">", "> ").replaceAll("<br/>", "\n");
return (div.innerText ?? div.textContent ?? "").trim();
}
export function getCommandAfterLastPipe(commandString: string): string {
const parsedCommands = parseCommand(commandString);
const lastPipeIndex = parsedCommands.findLastIndex(isPipeSymbol);
if (lastPipeIndex === -1) {
return commandString;
}
return parsedCommands.slice(lastPipeIndex + 1).join(" ");
}
function clean(str: string, stripAnsiEscape: boolean) {
return stripAnsiEscape ? str.replaceAll(ANSI_ESCAPE, "") : str;
}
let nextStdinPort = -1e7;
export function getNextStdinHandle(): PortHandle {
// port numbers for pipes are negative numbers to avoid collisions with standard player ns ports
return new PortHandle(nextStdinPort-- as PortNumber);
}

View File

@@ -44,6 +44,7 @@ import { check } from "./commands/check";
import { connect } from "./commands/connect";
import { cp } from "./commands/cp";
import { download } from "./commands/download";
import { echo } from "./commands/echo";
import { expr } from "./commands/expr";
import { free } from "./commands/free";
import { grep } from "./commands/grep";
@@ -86,10 +87,15 @@ import { hasTextExtension } from "../Paths/TextFilePath";
import { ContractFilePath } from "../Paths/ContractFilePath";
import { ServerConstants } from "../Server/data/Constants";
import { isIPAddress } from "../Types/strings";
import { StdIO } from "./StdIO/StdIO";
import { getTerminalStdIO, parseRedirectedCommands } from "./StdIO/RedirectIO";
import { getRewardFromCache } from "../DarkNet/effects/cacheFiles";
import { DarknetServer } from "../Server/DarknetServer";
export const TerminalCommands: Record<string, (args: (string | number | boolean)[], server: BaseServer) => void> = {
export const TerminalCommands: Record<
string,
(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO) => void
> = {
"scan-analyze": scananalyze,
alias: alias,
analyze: analyze,
@@ -104,6 +110,7 @@ export const TerminalCommands: Record<string, (args: (string | number | boolean)
connect: connect,
cp: cp,
download: download,
echo: echo,
expr: expr,
free: free,
grep: grep,
@@ -139,6 +146,7 @@ export const TerminalCommands: Record<string, (args: (string | number | boolean)
export class Terminal {
// Flags to determine whether the player is currently running a hack or an analyze
action: TTimer | null = null;
actionStdIO: StdIO | null = null;
commandHistory: string[] = [];
commandHistoryIndex = 0;
@@ -153,13 +161,16 @@ export class Terminal {
// Path of current directory
currDir = "" as Directory;
// PID of the script run as part of the last executed command, if any
pidOfLastScriptRun: number | null = null;
process(cycles: number): void {
if (this.action === null) return;
this.action.timeLeft -= (CONSTANTS.MilliPerCycle * cycles) / 1000;
if (this.action.timeLeft < 0.01) this.finishAction(false);
}
append(item: Output | Link | RawOutput): void {
terminalOutput(item: Output | Link | RawOutput): void {
this.outputHistory.push(item);
if (this.outputHistory.length > Settings.MaxTerminalCapacity) {
this.outputHistory.splice(0, this.outputHistory.length - Settings.MaxTerminalCapacity);
@@ -167,31 +178,36 @@ export class Terminal {
TerminalEvents.emit();
}
print(s: string): void {
this.append(new Output(s, "primary"));
print(s: string, stdIO: StdIO = getTerminalStdIO(null)): void {
stdIO.write(s);
}
printRaw(node: React.ReactNode): void {
this.append(new RawOutput(node));
printRaw(node: React.ReactNode, stdIO: StdIO = getTerminalStdIO(null)): void {
stdIO.write(new RawOutput(node));
}
error(s: string): void {
this.append(new Output(s, "error"));
printAndBypassPipes(s: string): void {
this.terminalOutput(new Output(s, "primary"));
}
success(s: string): void {
this.append(new Output(s, "success"));
error(s: string, stdIO: StdIO | null = null): void {
stdIO?.close();
this.terminalOutput(new Output(s, "error"));
}
info(s: string): void {
this.append(new Output(s, "info"));
success(s: string, stdIO: StdIO = getTerminalStdIO(null)): void {
stdIO.write(new Output(s, "success"));
}
warn(s: string): void {
this.append(new Output(s, "warn"));
info(s: string, stdIO: StdIO = getTerminalStdIO(null)): void {
stdIO.write(new Output(s, "info"));
}
startHack(): void {
warn(s: string, stdIO: StdIO = getTerminalStdIO(null)): void {
stdIO.write(new Output(s, "warn"));
}
startHack(stdIO: StdIO): void {
// Hacking through Terminal should be faster than hacking through a script
const server = Player.getCurrentServer();
if (server instanceof HacknetServer) {
@@ -199,48 +215,49 @@ export class Terminal {
return;
}
if (!(server instanceof Server)) throw new Error("server should be normal server");
this.startAction(calculateHackingTime(server, Player) / 4, "h", server);
this.startAction(calculateHackingTime(server, Player) / 4, "h", stdIO, server);
}
startGrow(): void {
startGrow(stdIO: StdIO): void {
const server = Player.getCurrentServer();
if (server instanceof HacknetServer) {
this.error("Cannot grow this kind of server");
return;
}
if (!(server instanceof Server)) throw new Error("server should be normal server");
this.startAction(calculateGrowTime(server, Player) / 16, "g", server);
this.startAction(calculateGrowTime(server, Player) / 16, "g", stdIO, server);
}
startWeaken(): void {
startWeaken(stdIO: StdIO): void {
const server = Player.getCurrentServer();
if (server instanceof HacknetServer) {
this.error("Cannot weaken this kind of server");
return;
}
if (!(server instanceof Server)) throw new Error("server should be normal server");
this.startAction(calculateWeakenTime(server, Player) / 16, "w", server);
this.startAction(calculateWeakenTime(server, Player) / 16, "w", stdIO, server);
}
startBackdoor(): void {
startBackdoor(stdIO: StdIO): void {
// Backdoor should take the same amount of time as hack
const server = Player.getCurrentServer();
if (server instanceof HacknetServer) {
this.error("Cannot backdoor this kind of server");
this.error("Cannot backdoor this kind of server", stdIO);
return;
}
if (!(server instanceof Server || server instanceof DarknetServer))
throw new Error("server should be normal server");
this.startAction(calculateHackingTime(server, Player) / 4, "b", server);
this.startAction(calculateHackingTime(server, Player) / 4, "b", stdIO, server);
}
startAnalyze(): void {
this.print("Analyzing system...");
startAnalyze(stdIO: StdIO): void {
this.print("Analyzing system...", stdIO);
const server = Player.getCurrentServer();
this.startAction(1, "a", server);
this.startAction(1, "a", stdIO, server);
}
startAction(n: number, action: "h" | "b" | "a" | "g" | "w" | "c", server?: BaseServer): void {
startAction(n: number, action: "h" | "b" | "a" | "g" | "w" | "c", stdIO: StdIO, server?: BaseServer): void {
this.action = new TTimer(n, action, server);
this.actionStdIO = stdIO;
}
// Complete the hack/analyze command
@@ -252,6 +269,9 @@ export class Terminal {
return;
}
if (!(server instanceof Server)) throw new Error("server should be normal server");
if (!this.actionStdIO) {
throw new Error("Missing stdIO for hack action");
}
// Calculate whether hack was successful
const hackChance = calculateHackingChance(server, Player);
@@ -303,25 +323,36 @@ export class Terminal {
`Hack successful on '${server.hostname}'! Gained ${formatMoney(moneyGained, true)} and ${formatExp(
expGainedOnSuccess,
)} hacking exp`,
this.actionStdIO,
);
this.print(
`Security increased on '${server.hostname}' from ${formatSecurity(oldSec)} to ${formatSecurity(newSec)}`,
this.actionStdIO,
);
} else {
// Failure
Player.gainHackingExp(expGainedOnFailure);
this.print(`Failed to hack '${server.hostname}'. Gained ${formatExp(expGainedOnFailure)} hacking exp`);
this.print(
`Failed to hack '${server.hostname}'. Gained ${formatExp(expGainedOnFailure)} hacking exp`,
this.actionStdIO,
);
}
this.actionStdIO.close();
this.actionStdIO = null;
}
finishGrow(server: BaseServer, cancelled = false): void {
if (cancelled) return;
if (server instanceof HacknetServer) {
this.error("Cannot grow this kind of server");
this.error("Cannot grow this kind of server", this.actionStdIO);
return;
}
if (!(server instanceof Server)) throw new Error("server should be normal server");
if (!this.actionStdIO) {
throw new Error("Missing stdIO for grow action");
}
const expGain = calculateHackingExpGain(server, Player);
const oldSec = server.hackDifficulty;
const growth = processSingleServerGrowth(server, 25, server.cpuCores);
@@ -332,20 +363,27 @@ export class Terminal {
`Available money on '${server.hostname}' grown by ${formatPercent(growth - 1, 6)}. Gained ${formatExp(
expGain,
)} hacking exp.`,
this.actionStdIO,
);
this.print(
`Security increased on '${server.hostname}' from ${formatSecurity(oldSec)} to ${formatSecurity(newSec)}`,
this.actionStdIO,
);
this.actionStdIO.close();
this.actionStdIO = null;
}
finishWeaken(server: BaseServer, cancelled = false): void {
if (cancelled) return;
if (server instanceof HacknetServer) {
this.error("Cannot weaken this kind of server");
this.error("Cannot weaken this kind of server", this.actionStdIO);
return;
}
if (!(server instanceof Server)) throw new Error("server should be normal server");
if (!this.actionStdIO) {
throw new Error("Missing stdIO for weaken action");
}
const expGain = calculateHackingExpGain(server, Player);
const oldSec = server.hackDifficulty;
const weakenAmt = getWeakenEffect(1, server.cpuCores);
@@ -358,17 +396,24 @@ export class Terminal {
oldSec,
)} to ${formatSecurity(newSec)} (min: ${formatSecurity(server.minDifficulty)})` +
` and Gained ${formatExp(expGain)} hacking exp.`,
this.actionStdIO,
);
this.actionStdIO.close();
this.actionStdIO = null;
}
finishBackdoor(server: BaseServer, cancelled = false): void {
if (!cancelled) {
if (server instanceof HacknetServer) {
this.error("Cannot hack this kind of server");
this.error("Cannot hack this kind of server", this.actionStdIO);
return;
}
if (!(server instanceof Server || server instanceof DarknetServer))
throw new Error("server should be normal server");
if (!this.actionStdIO) {
throw new Error("Missing stdIO for backdoor action");
}
server.backdoorInstalled = true;
if (SpecialServers.WorldDaemon === server.hostname) {
if (Player.bitNodeN == null) {
@@ -381,51 +426,65 @@ export class Terminal {
Engine.Counters.checkFactionInvitations = 0;
Engine.checkCounters();
this.print(`Backdoor on '${server.hostname}' successful!`);
this.print(`Backdoor on '${server.hostname}' successful!`, this.actionStdIO);
this.actionStdIO.close();
this.actionStdIO = null;
}
}
finishAnalyze(currServ: BaseServer, cancelled = false): void {
if (!cancelled) {
if (!this.actionStdIO) {
throw new Error("Missing stdIO for analyze action");
}
const isHacknet = currServ instanceof HacknetServer;
this.print(currServ.hostname + ": ");
this.print(currServ.hostname + ": ", this.actionStdIO);
const org = currServ.organizationName;
this.print("Organization name: " + (!isHacknet ? org : "player"));
this.print("Organization name: " + (!isHacknet ? org : "player"), this.actionStdIO);
const hasAdminRights = (!isHacknet && currServ.hasAdminRights) || isHacknet;
this.print("Root Access: " + (hasAdminRights ? "YES" : "NO"));
this.print("Root Access: " + (hasAdminRights ? "YES" : "NO"), this.actionStdIO);
const canRunScripts = hasAdminRights && currServ.maxRam > 0;
this.print("Can run scripts on this host: " + (canRunScripts ? "YES" : "NO"));
this.print("RAM: " + formatRam(currServ.maxRam));
this.print("Can run scripts on this host: " + (canRunScripts ? "YES" : "NO"), this.actionStdIO);
this.print("RAM: " + formatRam(currServ.maxRam), this.actionStdIO);
if (currServ instanceof DarknetServer && currServ.blockedRam) {
this.print("RAM blocked by owner: " + formatRam(currServ.blockedRam));
this.print("Stasis link: " + (currServ.hasStasisLink ? "YES" : "NO"));
this.print("Backdoor: " + (currServ.backdoorInstalled ? "YES" : "NO"));
this.print("RAM blocked by owner: " + formatRam(currServ.blockedRam), this.actionStdIO);
this.print("Stasis link: " + (currServ.hasStasisLink ? "YES" : "NO"), this.actionStdIO);
this.print("Backdoor: " + (currServ.backdoorInstalled ? "YES" : "NO"), this.actionStdIO);
}
if (currServ instanceof Server) {
this.print("Backdoor: " + (currServ.backdoorInstalled ? "YES" : "NO"));
this.print("Backdoor: " + (currServ.backdoorInstalled ? "YES" : "NO"), this.actionStdIO);
const hackingSkill = currServ.requiredHackingSkill;
this.print("Required hacking skill for hack() and backdoor: " + (!isHacknet ? hackingSkill : "N/A"));
this.print(
"Required hacking skill for hack() and backdoor: " + (!isHacknet ? hackingSkill : "N/A"),
this.actionStdIO,
);
const security = currServ.hackDifficulty;
this.print("Server security level: " + (!isHacknet ? formatSecurity(security) : "N/A"));
this.print("Server security level: " + (!isHacknet ? formatSecurity(security) : "N/A"), this.actionStdIO);
const hackingChance = calculateHackingChance(currServ, Player);
this.print("Chance to hack: " + (!isHacknet ? formatPercent(hackingChance) : "N/A"));
this.print("Chance to hack: " + (!isHacknet ? formatPercent(hackingChance) : "N/A"), this.actionStdIO);
const hackingTime = calculateHackingTime(currServ, Player) * 1000;
this.print("Time to hack: " + (!isHacknet ? convertTimeMsToTimeElapsedString(hackingTime, true) : "N/A"));
this.print(
"Time to hack: " + (!isHacknet ? convertTimeMsToTimeElapsedString(hackingTime, true) : "N/A"),
this.actionStdIO,
);
}
this.print(
`Total money available on server: ${
currServ instanceof Server ? formatMoney(currServ.moneyAvailable, true) : "N/A"
}`,
this.actionStdIO,
);
if (currServ instanceof Server) {
const numPort = currServ.numOpenPortsRequired;
this.print("Required number of open ports for NUKE: " + (!isHacknet ? numPort : "N/A"));
this.print("SSH port: " + (currServ.sshPortOpen ? "Open" : "Closed"));
this.print("FTP port: " + (currServ.ftpPortOpen ? "Open" : "Closed"));
this.print("SMTP port: " + (currServ.smtpPortOpen ? "Open" : "Closed"));
this.print("HTTP port: " + (currServ.httpPortOpen ? "Open" : "Closed"));
this.print("SQL port: " + (currServ.sqlPortOpen ? "Open" : "Closed"));
this.print("Required number of open ports for NUKE: " + (!isHacknet ? numPort : "N/A"), this.actionStdIO);
this.print("SSH port: " + (currServ.sshPortOpen ? "Open" : "Closed"), this.actionStdIO);
this.print("FTP port: " + (currServ.ftpPortOpen ? "Open" : "Closed"), this.actionStdIO);
this.print("SMTP port: " + (currServ.smtpPortOpen ? "Open" : "Closed"), this.actionStdIO);
this.print("HTTP port: " + (currServ.httpPortOpen ? "Open" : "Closed"), this.actionStdIO);
this.print("SQL port: " + (currServ.sqlPortOpen ? "Open" : "Closed"), this.actionStdIO);
}
this.actionStdIO.close();
this.actionStdIO = null;
}
}
@@ -436,8 +495,11 @@ export class Terminal {
}
if (!this.action.server) throw new Error("Missing action target server");
if (!this.actionStdIO) {
throw new Error("Missing stdIO for action");
}
this.print(this.getProgressText());
this.print(this.getProgressText(), this.actionStdIO);
if (this.action.action === "h") {
this.finishHack(this.action.server, cancelled);
} else if (this.action.action === "g") {
@@ -459,9 +521,11 @@ export class Terminal {
}
if (cancelled) {
this.print("Cancelled");
this.print("Cancelled", this.actionStdIO);
}
this.action = null;
this.actionStdIO.close();
this.actionStdIO = null;
TerminalEvents.emit();
}
@@ -524,16 +588,16 @@ export class Terminal {
TerminalEvents.emit();
}
async runContract(contractPath: ContractFilePath): Promise<void> {
async runContract(contractPath: ContractFilePath, stdIO: StdIO): Promise<void> {
// There's already an opened contract
if (this.contractOpen) {
return this.error("There's already a Coding Contract in Progress");
return this.error("There's already a Coding Contract in Progress", stdIO);
}
const server = Player.getCurrentServer();
const contract = server.getContract(contractPath);
if (!contract) {
return this.error("No such contract");
return this.error("No such contract", stdIO);
}
this.contractOpen = true;
@@ -545,14 +609,14 @@ export class Terminal {
// Check if the contract still exists by the time the promise is fulfilled
if (postPromptServer?.getContract(contractPath) == null) {
this.contractOpen = false;
return this.error("Contract no longer exists (Was it solved by a script?)");
return this.error("Contract no longer exists (Was it solved by a script?)", stdIO);
}
switch (promptResult.result) {
case CodingContractResult.Success:
if (contract.reward !== null) {
const reward = Player.gainCodingContractReward(contract.reward, contract.getDifficulty());
this.print(`Contract SUCCESS - ${reward}`);
this.print(`Contract SUCCESS - ${reward}`, stdIO);
}
server.removeContract(contract);
break;
@@ -561,23 +625,24 @@ export class Terminal {
`Contract FAILED - ${
promptResult.message ?? `The answer is not in the right format for contract '${contract.type}'`
}`,
stdIO,
);
break;
case CodingContractResult.Failure:
++contract.tries;
if (contract.tries >= contract.getMaxNumTries()) {
this.error("Contract FAILED - Contract is now self-destructing");
this.error("Contract FAILED - Contract is now self-destructing", stdIO);
const solution = contract.getAnswer();
if (solution !== null) {
this.error(`Coding Contract solution was: ${solution}`);
this.error(`Coding Contract solution was: ${solution}`, stdIO);
}
server.removeContract(contract);
} else {
this.error(`Contract FAILED - ${contract.getMaxNumTries() - contract.tries} tries remaining`);
this.error(`Contract FAILED - ${contract.getMaxNumTries() - contract.tries} tries remaining`, stdIO);
}
break;
case CodingContractResult.Cancelled:
this.print("Contract cancelled");
this.print("Contract cancelled", stdIO);
break;
default: {
const __: never = promptResult.result;
@@ -586,7 +651,7 @@ export class Terminal {
this.contractOpen = false;
}
executeScanAnalyzeCommand(depth = 1, all = false): void {
executeScanAnalyzeCommand(depth = 1, all = false, stdIO: StdIO): void {
interface Node {
hostname: string;
children: Node[];
@@ -618,13 +683,13 @@ export class Terminal {
const root = makeNode();
const printOutput = (node: Node, prefix = [" "], last = true) => {
const printOutput = (node: Node, stdIO: StdIO, prefix = [" "], last = true) => {
const titlePrefix = prefix.slice(0, prefix.length - 1).join("") + (last ? "┗ " : "┣ ");
const infoPrefix = prefix.join("") + (node.children.length > 0 ? "┃ " : " ");
if (Player.hasProgram(CompletedProgramName.autoLink)) {
this.append(new Link(titlePrefix, node.hostname));
this.printRaw(new Link(titlePrefix, node.hostname), stdIO);
} else {
this.print(titlePrefix + node.hostname + "\n");
this.print(titlePrefix + node.hostname + "\n", stdIO);
}
const server = GetServer(node.hostname);
@@ -633,18 +698,24 @@ export class Terminal {
if (server instanceof Server) {
this.print(
`${infoPrefix}Root Access: ${hasRoot}, Required hacking skill: ${server.requiredHackingSkill}` + "\n",
stdIO,
);
this.print(`${infoPrefix}Number of open ports required to NUKE: ${server.numOpenPortsRequired}` + "\n");
this.print(`${infoPrefix}Number of open ports required to NUKE: ${server.numOpenPortsRequired}` + "\n", stdIO);
} else {
this.print(`${infoPrefix}Root Access: ${hasRoot}` + "\n");
this.print(`${infoPrefix}Root Access: ${hasRoot}` + "\n", stdIO);
}
this.print(`${infoPrefix}RAM: ${formatRam(server.maxRam)}` + "\n");
this.print(`${infoPrefix}RAM: ${formatRam(server.maxRam)}` + "\n", stdIO);
node.children.forEach((n, i) =>
printOutput(n, [...prefix, i === node.children.length - 1 ? " " : "┃ "], i === node.children.length - 1),
printOutput(
n,
stdIO,
[...prefix, i === node.children.length - 1 ? " " : "┃ "],
i === node.children.length - 1,
),
);
};
printOutput(root);
printOutput(root, stdIO);
}
connectToServer(hostname: string, singularity = false): void {
@@ -658,14 +729,14 @@ export class Terminal {
server.isConnectedTo = true;
this.setcwd(root);
if (!singularity) {
this.print("Connected to " + `${isIPAddress(hostname) ? server.ip : server.hostname}`);
this.printAndBypassPipes("Connected to " + `${isIPAddress(hostname) ? server.ip : server.hostname}`);
if (Player.getCurrentServer().hostname === "darkweb") {
checkIfConnectedToDarkweb(); // Posts a 'help' message if connecting to dark web
}
}
}
executeCommands(commands: string): void {
async executeCommands(commands: string): Promise<void> {
// Handle Terminal History - multiple commands should be saved as one
if (this.commandHistory[this.commandHistory.length - 1] != commands) {
this.commandHistory.push(commands);
@@ -676,7 +747,9 @@ export class Terminal {
}
this.commandHistoryIndex = this.commandHistory.length;
const allCommands = parseCommands(commands);
for (const command of allCommands) this.executeCommand(command);
for (const command of allCommands) {
await parseRedirectedCommands(command);
}
}
clear(): void {
@@ -690,8 +763,9 @@ export class Terminal {
this.clear();
}
executeCommand(command: string): void {
if (this.action !== null) return this.error(`Cannot execute command (${command}) while an action is in progress`);
executeCommand(command: string, stdIO: StdIO): void {
if (this.action !== null)
return this.error(`Cannot execute command (${command}) while an action is in progress`, stdIO);
const commandArray = parseCommand(command);
if (!commandArray.length) return;
@@ -844,9 +918,9 @@ export class Terminal {
/* Command parser */
const commandName = commandArray[0];
if (typeof commandName !== "string") return this.error(`${commandName} is not a valid command.`);
if (typeof commandName !== "string") return this.error(`${commandName} is not a valid command.`, stdIO);
// run by path command
if (isBasicFilePath(commandName)) return run(commandArray, currentServer);
if (isBasicFilePath(commandName)) return run(commandArray, currentServer, stdIO);
// Aside from the run-by-path command, we don't need the first entry once we've stored it in commandName.
commandArray.shift();
@@ -855,10 +929,18 @@ export class Terminal {
if (!f) {
const similarCommands = findSimilarCommands(commandName);
const didYouMeanString = similarCommands.length ? ` Did you mean: ${similarCommands.join(" or ")}?` : "";
return this.error(`Command ${commandName} not found.${didYouMeanString}`);
return this.error(`Command ${commandName} not found.${didYouMeanString}`, stdIO);
}
f(commandArray, currentServer);
f(commandArray, currentServer, stdIO);
if (commandName.toLowerCase() !== "run") {
this.pidOfLastScriptRun = null;
}
if (!this.action && !["wget", "run", "cat", "grep", "tail"].includes(commandName.toLowerCase())) {
stdIO.close();
}
}
getProgressText(): string {

View File

@@ -1,28 +1,30 @@
import { Terminal } from "../../Terminal";
import { parseAliasDeclaration, printAliases } from "../../Alias";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function alias(args: (string | number | boolean)[]): void {
export function alias(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length === 0) {
printAliases();
return;
}
if (args[0] === "--all") {
Terminal.error(`--all is reserved for removal`);
Terminal.error(`--all is reserved for removal`, stdIO);
return;
}
if (args.length === 1) {
if (parseAliasDeclaration(args[0] + "")) {
Terminal.print(`Set alias ${args[0]}`);
Terminal.printAndBypassPipes(`Set alias ${args[0]}`);
return;
}
}
if (args.length === 2) {
if (args[0] === "-g") {
if (parseAliasDeclaration(args[1] + "", true)) {
Terminal.print(`Set global alias ${args[1]}`);
Terminal.printAndBypassPipes(`Set global alias ${args[1]}`);
return;
}
}
}
Terminal.error('Incorrect usage of alias command. Usage: alias [-g] [aliasname="value"]');
Terminal.error('Incorrect usage of alias command. Usage: alias [-g] [aliasname="value"]', stdIO);
}

View File

@@ -1,9 +1,11 @@
import { Terminal } from "../../Terminal";
import { StdIO } from "../StdIO/StdIO";
import { BaseServer } from "../../Server/BaseServer";
export function analyze(args: (string | number | boolean)[]): void {
export function analyze(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) {
Terminal.error("Incorrect usage of analyze command. Usage: analyze");
Terminal.error("Incorrect usage of analyze command. Usage: analyze", stdIO);
return;
}
Terminal.startAnalyze();
Terminal.startAnalyze(stdIO);
}

View File

@@ -2,31 +2,34 @@ import { Terminal } from "../../Terminal";
import { Player } from "@player";
import { BaseServer } from "../../Server/BaseServer";
import { Server } from "../../Server/Server";
import { StdIO } from "../StdIO/StdIO";
import { DarknetServer } from "../../Server/DarknetServer";
export function backdoor(args: (string | number | boolean)[], server: BaseServer): void {
export function backdoor(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) {
Terminal.error("Incorrect usage of backdoor command. Usage: backdoor");
Terminal.error("Incorrect usage of backdoor command. Usage: backdoor", stdIO);
return;
}
if (!(server instanceof Server) && !(server instanceof DarknetServer)) {
Terminal.error("Can only install a backdoor on normal servers");
Terminal.error("Can only install a backdoor on normal servers", stdIO);
return;
}
if (server.purchasedByPlayer) {
Terminal.error(
"Cannot install a backdoor on your own machines! You are currently connected to your home PC or one of your cloud servers.",
stdIO,
);
return;
}
if (!server.hasAdminRights) {
Terminal.error("You do not have admin rights for this machine!");
Terminal.error("You do not have admin rights for this machine!", stdIO);
return;
}
if (server.requiredHackingSkill && server.requiredHackingSkill > Player.skills.hacking) {
Terminal.error(
"Your hacking skill is not high enough to install a backdoor on this machine. Try analyzing the machine to determine the required hacking skill.",
stdIO,
);
return;
}
@@ -34,8 +37,9 @@ export function backdoor(args: (string | number | boolean)[], server: BaseServer
if (server.backdoorInstalled) {
Terminal.warn(
`You have already installed a backdoor on this server. You can check the "Backdoor" status via the "analyze" command.`,
stdIO,
);
}
Terminal.startBackdoor();
Terminal.startBackdoor(stdIO);
}

View File

@@ -1,23 +1,26 @@
import { Terminal } from "../../Terminal";
import { Player } from "@player";
import { listAllDarkwebItems, buyAllDarkwebItems, buyDarkwebItem } from "../../DarkWeb/DarkWeb";
import { StdIO } from "../StdIO/StdIO";
import { BaseServer } from "../../Server/BaseServer";
export function buy(args: (string | number | boolean)[]): void {
export function buy(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (!Player.hasTorRouter()) {
Terminal.error(
`You need to be able to connect to the Dark Web to use the "buy" command. (Maybe there's a TOR router you can buy somewhere)`,
stdIO,
);
return;
}
if (args.length != 1) {
Terminal.print("Incorrect number of arguments. Usage: ");
Terminal.print("buy -l");
Terminal.print("buy -a");
Terminal.print("buy [item name]");
Terminal.print("Incorrect number of arguments. Usage: ", stdIO);
Terminal.print("buy -l", stdIO);
Terminal.print("buy -a", stdIO);
Terminal.print("buy [item name]", stdIO);
return;
}
const arg = args[0] + "";
if (arg == "-l" || arg == "-1" || arg == "--list") listAllDarkwebItems();
else if (arg == "-a" || arg == "--all") buyAllDarkwebItems();
else buyDarkwebItem(arg);
if (arg == "-l" || arg == "-1" || arg == "--list") listAllDarkwebItems(stdIO);
else if (arg == "-a" || arg == "--all") buyAllDarkwebItems(stdIO);
else buyDarkwebItem(arg, stdIO);
}

View File

@@ -1,36 +1,144 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { showMessage } from "../../Message/MessageHelpers";
import { showLiterature } from "../../Literature/LiteratureHelpers";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { Messages, showMessage } from "../../Message/MessageHelpers";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
import { hasTextExtension } from "../../Paths/TextFilePath";
import { isMember } from "../../utils/EnumHelper";
import { StdIO } from "../StdIO/StdIO";
import { Literatures } from "../../Literature/Literatures";
import { LiteratureName, MessageFilename } from "@enums";
import { callOnRead } from "../StdIO/RedirectIO";
import { stringify } from "../StdIO/utils";
import { showLiterature } from "../../Literature/LiteratureHelpers";
import { dialogBoxCreate } from "../../ui/React/DialogBox";
export function cat(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 1) return Terminal.error("Incorrect usage of cat command. Usage: cat [file]");
export function cat(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
const initialStdIn = stdIO.getAllCurrentStdin(false);
const stdin = stdIO.stdin?.deref();
const stdinIsClosed = !stdin || (stdin.isClosed && stdin.empty());
const hasStdOut = !!stdIO.stdout;
const relative_filename = args[0] + "";
const path = Terminal.getFilepath(relative_filename);
if (!path) return Terminal.error(`Invalid filename: ${relative_filename}`);
if (args.length === 0 && initialStdIn.length === 0 && stdinIsClosed) {
return Terminal.error(
`Incorrect use of cat command: No files specified, and no stdin provided. Try "cat [filename]"`,
stdIO,
);
}
if (!validateFilenames(args, server, stdIO)) {
return;
}
// If only a single file is being catted, and no stdin/stdout redirects are being used, show the file dialog
if (args.length === 1 && args[0] !== "-" && !initialStdIn.length && stdinIsClosed && !hasStdOut) {
return showFileContentDialog(String(args[0]), server, stdIO);
}
const output = concatenateFileContents(args, server, initialStdIn);
stdIO.write(output);
if (stdinIsClosed) {
stdIO.close();
} else {
void callOnRead(stdIO, (data: unknown, stdInOut) => {
stdInOut.write(stringify(data));
});
}
}
export function concatenateFileContents(
filenames: (string | number | boolean)[],
server: BaseServer,
initialStdin: string,
): string {
let result = "";
for (const arg of filenames) {
const filename = String(arg);
if (filename === "-") {
result += initialStdin;
} else {
result += getFileContents(filename, server);
}
}
if (!filenames.find((arg) => arg === "-")) {
// If stdin location is not specified, append it to the end by default
result += initialStdin;
}
return result;
}
export function getFileContents(filename: string, server: BaseServer): string {
const path = Terminal.getFilepath(filename);
if (!path) return "";
if (hasScriptExtension(path) || hasTextExtension(path)) {
const file = server.getContentFile(path);
if (!file) return Terminal.error(`No file at path ${path}`);
if (!file) return "";
return file.content ?? "";
}
if (isMember("MessageFilename", path) && server.messages.includes(path)) {
return stringify(Messages[path as MessageFilename].msg) + "\n";
}
if (isMember("LiteratureName", path) && server.messages.includes(path)) {
const lit = Literatures[path as LiteratureName];
return `${lit.title}\n\n${stringify(lit.text)}\n`;
}
return "";
}
function showFileContentDialog(filename: string, server: BaseServer, stdIO: StdIO) {
const path = Terminal.getFilepath(filename);
if (!path) return Terminal.error(`Invalid filename: ${filename}`, stdIO);
if (hasScriptExtension(path) || hasTextExtension(path)) {
const file = server.getContentFile(path);
if (!file) return Terminal.error(`No file at path ${path}`, stdIO);
return dialogBoxCreate(`${file.filename}\n\n${file.content}`);
}
if (!path.endsWith(".msg") && !path.endsWith(".lit")) {
return Terminal.error(
"Invalid file extension. Filename must end with .msg, .lit, a script extension (.js, .jsx, .ts, .tsx) or a text extension (.txt, .json, .css)",
);
if (isMember("MessageFilename", path) && server.messages.includes(path)) {
return showMessage(path);
}
// Message
if (isMember("MessageFilename", path)) {
if (server.messages.includes(path)) return showMessage(path);
if (isMember("LiteratureName", path) && server.messages.includes(path)) {
return showLiterature(path);
}
if (isMember("LiteratureName", path)) {
if (server.messages.includes(path)) return showLiterature(path);
}
Terminal.error(`No file at path ${path}`);
}
export function validateFilenames(filenames: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): boolean {
for (const filename of filenames) {
if (filename === "-") continue;
if (typeof filename !== "string") {
Terminal.error(`Invalid filename: ${filename}`, stdIO);
return false;
}
const path = Terminal.getFilepath(filename);
if (!path) {
Terminal.error(`Invalid filename: ${filename}`, stdIO);
return false;
}
if (hasScriptExtension(path) || hasTextExtension(path)) {
const file = server.getContentFile(path);
if (!file) {
Terminal.error(`No file at path ${path}`, stdIO);
return false;
}
} else if (path.endsWith(".msg")) {
if (!isMember("MessageFilename", path) || !server.messages.includes(path)) {
Terminal.error(`No file at path ${path}`, stdIO);
return false;
}
} else if (path.endsWith(".lit")) {
if (!isMember("LiteratureName", path) || !server.messages.includes(path)) {
Terminal.error(`No file at path ${path}`, stdIO);
return false;
}
} else {
Terminal.error(
"Invalid file extension. Filename must end with .msg, .lit, a script extension (.js, .jsx, .ts, .tsx) or a text extension (.txt, .json, .css)",
stdIO,
);
return false;
}
}
return true;
}

View File

@@ -1,14 +1,16 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { directoryExistsOnServer, resolveDirectory } from "../../Paths/Directory";
import { StdIO } from "../StdIO/StdIO";
export function cd(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length > 1) return Terminal.error("Incorrect number of arguments. Usage: cd [dir]");
export function cd(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length > 1) return Terminal.error("Incorrect number of arguments. Usage: cd [dir]", stdIO);
// If no arg was provided, just use "/".
const userInput = String(args[0] ?? "/");
const targetDir = resolveDirectory(userInput, Terminal.currDir);
// Explicitly checking null due to root being ""
if (targetDir === null) return Terminal.error(`Could not resolve directory ${userInput}`);
if (!directoryExistsOnServer(targetDir, server)) return Terminal.error(`Directory ${targetDir} does not exist.`);
if (targetDir === null) return Terminal.error(`Could not resolve directory ${userInput}`, stdIO);
if (!directoryExistsOnServer(targetDir, server))
return Terminal.error(`Directory ${targetDir} does not exist.`, stdIO);
Terminal.setcwd(targetDir);
}

View File

@@ -2,28 +2,29 @@ import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { findRunningScripts } from "../../Script/ScriptHelpers";
import { hasScriptExtension, validScriptExtensions } from "../../Paths/ScriptFilePath";
import { StdIO } from "../StdIO/StdIO";
export function check(args: (string | number | boolean)[], server: BaseServer): void {
export function check(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length < 1) {
Terminal.error(`Incorrect number of arguments. Usage: check [script] [arg1] [arg2]...`);
Terminal.error(`Incorrect number of arguments. Usage: check [script] [arg1] [arg2]...`, stdIO);
} else {
const scriptName = Terminal.getFilepath(args[0] + "");
if (!scriptName) return Terminal.error(`Invalid filename: ${args[0]}`);
if (!scriptName) return Terminal.error(`Invalid filename: ${args[0]}`, stdIO);
// Can only tail script files
if (!hasScriptExtension(scriptName)) {
return Terminal.error(`check: File extension must be one of ${validScriptExtensions.join(", ")})`);
return Terminal.error(`check: File extension must be one of ${validScriptExtensions.join(", ")})`, stdIO);
}
// Check that the script is running on this machine
const runningScripts = findRunningScripts(scriptName, args.slice(1), server);
if (runningScripts === null) {
Terminal.error(`No script named ${scriptName} is running on the server`);
Terminal.error(`No script named ${scriptName} is running on the server`, stdIO);
return;
}
const next = runningScripts.values().next();
if (!next.done) {
next.value.displayLog();
next.value.displayLog(stdIO);
}
}
}

View File

@@ -3,11 +3,12 @@ import { BaseServer } from "../../Server/BaseServer";
import { getServerOnNetwork } from "../../Server/ServerHelpers";
import { GetServer } from "../../Server/AllServers";
import { exceptionAlert } from "../../utils/helpers/exceptionAlert";
import { StdIO } from "../StdIO/StdIO";
export function connect(args: (string | number | boolean)[], server: BaseServer): void {
export function connect(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
// Disconnect from current server in Terminal and connect to new one
if (args.length !== 1) {
Terminal.error("Incorrect usage of connect command. Usage: connect [hostname]");
Terminal.error("Incorrect usage of connect command. Usage: connect [hostname]", stdIO);
return;
}
@@ -15,7 +16,7 @@ export function connect(args: (string | number | boolean)[], server: BaseServer)
const target = GetServer(hostname);
if (target === null) {
Terminal.error(`Invalid hostname: '${hostname}'`);
Terminal.error(`Invalid hostname: '${hostname}'`, stdIO);
return;
}
@@ -47,5 +48,6 @@ export function connect(args: (string | number | boolean)[], server: BaseServer)
Terminal.error(
`Cannot directly connect to ${hostname}. Make sure the server is backdoored or adjacent to your current server`,
stdIO,
);
}

View File

@@ -3,19 +3,20 @@ import { BaseServer } from "../../Server/BaseServer";
import { combinePath, getFilenameOnly } from "../../Paths/FilePath";
import { hasTextExtension } from "../../Paths/TextFilePath";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
import { StdIO } from "../StdIO/StdIO";
export function cp(args: (string | number | boolean)[], server: BaseServer): void {
export function cp(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 2) {
return Terminal.error("Incorrect usage of cp command. Usage: cp [source filename] [destination]");
return Terminal.error("Incorrect usage of cp command. Usage: cp [source filename] [destination]", stdIO);
}
// Find the source file
const sourceFilePath = Terminal.getFilepath(String(args[0]));
if (!sourceFilePath) return Terminal.error(`Invalid source filename ${args[0]}`);
if (!sourceFilePath) return Terminal.error(`Invalid source filename ${args[0]}`, stdIO);
if (!hasTextExtension(sourceFilePath) && !hasScriptExtension(sourceFilePath)) {
return Terminal.error("cp: Can only be performed on script and text files");
return Terminal.error("cp: Can only be performed on script and text files", stdIO);
}
const source = server.getContentFile(sourceFilePath);
if (!source) return Terminal.error(`File not found: ${sourceFilePath}`);
if (!source) return Terminal.error(`File not found: ${sourceFilePath}`, stdIO);
// Determine the destination file path.
const destinationInput = String(args[1]);
@@ -23,14 +24,15 @@ export function cp(args: (string | number | boolean)[], server: BaseServer): voi
let destFilePath = Terminal.getFilepath(destinationInput);
if (!destFilePath) {
const destDirectory = Terminal.getDirectory(destinationInput);
if (!destDirectory) return Terminal.error(`Could not resolve ${destinationInput} as a FilePath or Directory`);
if (!destDirectory)
return Terminal.error(`Could not resolve ${destinationInput} as a FilePath or Directory`, stdIO);
destFilePath = combinePath(destDirectory, getFilenameOnly(sourceFilePath));
}
if (!hasTextExtension(destFilePath) && !hasScriptExtension(destFilePath)) {
return Terminal.error(`cp: Can only copy to script and text files (${destFilePath} is invalid destination)`);
return Terminal.error(`cp: Can only copy to script and text files (${destFilePath} is invalid destination)`, stdIO);
}
const result = server.writeToContentFile(destFilePath, source.content);
Terminal.print(`File ${sourceFilePath} copied to ${destFilePath}`);
if (result.overwritten) Terminal.warn(`${destFilePath} was overwritten.`);
Terminal.print(`File ${sourceFilePath} copied to ${destFilePath}`, stdIO);
if (result.overwritten) Terminal.warn(`${destFilePath} was overwritten.`, stdIO);
}

View File

@@ -0,0 +1,7 @@
import { StdIO } from "../StdIO/StdIO";
import { BaseServer } from "../../Server/BaseServer";
export function echo(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
stdIO.write(args.join(" "));
stdIO.close();
}

View File

@@ -1,8 +1,10 @@
import { Terminal } from "../../Terminal";
import { StdIO } from "../StdIO/StdIO";
import { BaseServer } from "../../Server/BaseServer";
export function expr(args: (string | number | boolean)[]): void {
export function expr(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length === 0) {
Terminal.error("Incorrect usage of expr command. Usage: expr [math expression]");
Terminal.error("Incorrect usage of expr command. Usage: expr [math expression]", stdIO);
return;
}
const expr = args.join("");
@@ -13,8 +15,8 @@ export function expr(args: (string | number | boolean)[]): void {
try {
result = String(eval?.(sanitizedExpr));
} catch (e) {
Terminal.error(`Could not evaluate expression: ${sanitizedExpr}. Error: ${e}.`);
Terminal.error(`Could not evaluate expression: ${sanitizedExpr}. Error: ${e}.`, stdIO);
return;
}
Terminal.print(result);
Terminal.print(result, stdIO);
}

View File

@@ -1,10 +1,11 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { formatPercent, formatRam } from "../../ui/formatNumber";
import { StdIO } from "../StdIO/StdIO";
export function free(args: (string | number | boolean)[], server: BaseServer): void {
export function free(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) {
Terminal.error("Incorrect usage of free command. Usage: free");
Terminal.error("Incorrect usage of free command. Usage: free", stdIO);
return;
}
const ram = formatRam(server.maxRam);
@@ -13,9 +14,10 @@ export function free(args: (string | number | boolean)[], server: BaseServer): v
const maxLength = Math.max(ram.length, Math.max(used.length, avail.length));
const usedPercent = formatPercent(server.ramUsed / server.maxRam);
Terminal.print(`Total: ${" ".repeat(maxLength - ram.length)}${ram}`);
Terminal.print(`Total: ${" ".repeat(maxLength - ram.length)}${ram}`, stdIO);
Terminal.print(
`Used: ${" ".repeat(maxLength - used.length)}${used}` + (server.maxRam > 0 ? ` (${usedPercent})` : ""),
stdIO,
);
Terminal.print(`Available: ${" ".repeat(maxLength - avail.length)}${avail}`);
Terminal.print(`Available: ${" ".repeat(maxLength - avail.length)}${avail}`, stdIO);
}

View File

@@ -1,13 +1,15 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { hasTextExtension } from "../../Paths/TextFilePath";
import { ContentFile, ContentFilePath, allContentFiles } from "../../Paths/ContentFile";
import { ContentFilePath, allContentFiles } from "../../Paths/ContentFile";
import { Settings } from "../../Settings/Settings";
import { help } from "../commands/help";
import { Output } from "../OutputTypes";
import { pluralize } from "../../utils/I18nUtils";
type LineParser = (options: Options, filename: string, line: string, i: number) => ParsedLine;
import { StdIO } from "../StdIO/StdIO";
import { callOnRead } from "../StdIO/RedirectIO";
import { stringify } from "../StdIO/utils";
import { getFileContents, validateFilenames } from "./cat";
const RED: string = "\x1b[31m";
const DEFAULT: string = "\x1b[0m";
@@ -19,8 +21,7 @@ const WHITE: string = "\x1b[37m";
const ERR = {
noArgs: "grep argument error. Usage: grep [OPTION]... PATTERN [FILE]... [-O] [OUTPUT FILE] [-m -B/A/C] [NUM]",
noSearchArg:
"grep argument error: At least one FILE argument must be passed, or pass -*/--search-all to search all files on server",
noSearchArg: `grep argument error: At least one FILE argument must be passed, or pass -*/--search-all to search all files on server, or pipe input into grep e.g. "echo test | grep t"`,
badArgs: (args: string[]) => "grep argument error: Invalid argument(s): " + args.join(", "),
badParameter: (option: string, arg: string) =>
`grep argument error: Incorrect ${option} argument "${arg}". Must be a number. OPTIONS with additional parameters (-O, -m, -B/A/C) must be separated from other options`,
@@ -30,6 +31,7 @@ const ERR = {
`grep file output failed: Invalid output file "${path}". Output file path must be a valid text file. (.txt, .json, .css)`,
truncated: () =>
`\n${YELLOW}Terminal output truncated to ${Settings.MaxTerminalCapacity} lines (Max terminal capacity)`,
tooManyInputs: () => `grep argument error. Cannot use both redirected input and terminal search simultaneously.`,
} as const;
type ArgStrings = {
@@ -289,7 +291,7 @@ class Results {
return this;
}
getVerboseInfo(files: ContentFile[], pattern: string | RegExp, options: Options): string {
getVerboseInfo(files: DataToSearch[], pattern: string | RegExp, options: Options): string {
if (!options.isVerbose) return "";
const totalLines = this.results.length;
const matchCount = Math.abs((options.isInvertMatch ? totalLines : 0) - this.numMatches);
@@ -311,28 +313,12 @@ class Results {
}
}
function getServerFiles(server: BaseServer): [ContentFile[], string[]] {
function getServerFiles(server: BaseServer) {
const files = [];
for (const tuple of allContentFiles(server)) {
files.push(tuple[1]);
}
return [files, []];
}
function getArgFiles(args: string[]): [ContentFile[], string[]] {
const notFiles = [];
const files = [];
for (const arg of args) {
const file = hasTextExtension(arg) ? Terminal.getTextFile(arg) : Terminal.getScript(arg);
if (!file) {
notFiles.push(arg);
} else {
files.push(file);
}
}
return [files, notFiles];
return files.map((file) => ({ filename: file.filename, content: file.content }));
}
function parseLine(pattern: string | RegExp, options: Options, filename: string, line: string, i: number): ParsedLine {
@@ -349,49 +335,32 @@ function parseLine(pattern: string | RegExp, options: Options, filename: string,
return { lines, filename, isMatched, isPrint: false, isFileSep: false };
}
function parseFile(lineParser: LineParser, options: Options, file: ContentFile, i: number): ParsedLine[] {
const parseLineFn = lineParser.bind(null, options, file.filename);
const editedContent: ParsedLine[] = file.content.split("\n").map(parseLineFn);
const hasMatch = editedContent.some((line) => line.isMatched);
const isPrintFileSep = options.hasContextFlag && hasMatch && i !== 0;
const fileSeparator: ParsedLine = {
lines: { prettyLine: `${CYAN}--${DEFAULT}`, rawLine: "--" },
isPrint: true,
isMatched: false,
isFileSep: true,
filename: "",
};
return isPrintFileSep ? [fileSeparator, ...editedContent] : editedContent;
}
function writeToTerminal(
prettyResult: string[],
options: Options,
results: Results,
files: ContentFile[],
files: DataToSearch[],
pattern: string | RegExp,
stdIO: StdIO,
): void {
const printResult = prettyResult.slice(0, Math.min(prettyResult.length, Settings.MaxTerminalCapacity)); // limit printing to terminal
const verboseInfo = results.getVerboseInfo(files, pattern, options);
const truncateInfo = prettyResult.length !== printResult.length ? ERR.truncated() : "";
if (results.areEdited) Terminal.print(printResult.join("\n") + truncateInfo);
if (options.isVerbose) Terminal.print(verboseInfo);
if (results.areEdited) stdIO.write(printResult.join("\n") + truncateInfo);
if (options.isVerbose) stdIO.write(verboseInfo);
}
function checkOutFile(outFileStr: string, options: Options, server: BaseServer): ContentFilePath | null {
function checkOutFile(outFileStr: string, options: Options, server: BaseServer, stdIO: StdIO): ContentFilePath | null {
if (!outFileStr) {
return null;
}
const outFilePath = Terminal.getFilepath(outFileStr);
if (!outFilePath || !hasTextExtension(outFilePath)) {
Terminal.error(ERR.badOutFile(outFileStr));
Terminal.error(ERR.badOutFile(outFileStr), stdIO);
return null;
}
if (!options.isOverWrite && server.textFiles.has(outFilePath)) {
Terminal.error(ERR.outFileExists(outFileStr));
Terminal.error(ERR.outFileExists(outFileStr), stdIO);
return null;
}
return outFilePath;
@@ -401,44 +370,105 @@ function grabTerminal(): string[] {
return Terminal.outputHistory.map((line) => (line as Output).text ?? "");
}
export function grep(args: (string | number | boolean)[], server: BaseServer): void {
if (!args.length) return Terminal.error(ERR.noArgs);
export function grep(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
const stdin = stdIO.stdin?.deref();
const noStdinProvided = !stdin || (stdin.isClosed && stdin.empty());
if (!args.length && noStdinProvided) return Terminal.error(ERR.noArgs, stdIO);
const [otherArgs, options, params] = new Args(args).splitOptsAndArgs();
if (options.isHelp) return help(["grep"]);
if (options.isHelp) return help(["grep"], server, stdIO);
options.hasContextFlag = !!params.context || !!params.preContext || !!params.postContext;
const nContext = Math.max(Number(params.preContext), Number(params.context), Number(params.postContext));
const nLimit = Number(params.maxMatches);
if (options.hasContextFlag && (!nContext || isNaN(Number(params.context))))
return Terminal.error(ERR.badParameter("context", params.context));
return Terminal.error(ERR.badParameter("context", params.context), stdIO);
if (params.maxMatches && (!nLimit || isNaN(Number(params.maxMatches))))
return Terminal.error(ERR.badParameter("limit", params.maxMatches));
return Terminal.error(ERR.badParameter("limit", params.maxMatches), stdIO);
const [files, notFiles] = options.isSearchAll ? getServerFiles(server) : getArgFiles(otherArgs.slice(1));
const stdinContent = stdIO.getAllCurrentStdin();
if (notFiles.length) return Terminal.error(ERR.badArgs(notFiles));
if (!options.isPipeIn && !options.isSearchAll && !files.length) return Terminal.error(ERR.noSearchArg);
if (!options.isPipeIn && !options.isSearchAll && !otherArgs.length && noStdinProvided)
return Terminal.error(ERR.noSearchArg, stdIO);
if (options.isPipeIn && stdinContent.length) return Terminal.error(ERR.tooManyInputs(), stdIO);
options.isMultiFile = files.length > 1;
const outFilePath = checkOutFile(params.outfile, options, server);
options.isMultiFile = otherArgs.length > 1;
const outFilePath = checkOutFile(params.outfile, options, server, stdIO);
if (params.outfile && !outFilePath) return; // associated errors are printed in checkOutFile
try {
const pattern = options.isRegExpr ? new RegExp(otherArgs[0], "g") : otherArgs[0];
const lineParser = parseLine.bind(null, pattern);
const termParser = lineParser.bind(null, options, "Terminal");
const fileParser = parseFile.bind(null, lineParser, options);
const contentToMatch = options.isPipeIn ? grabTerminal().map(termParser) : files.flatMap(fileParser);
const results = new Results(contentToMatch, options, params);
const [rawResult, prettyResult] = results.capMatches(nLimit).addContext(nContext).splitAndFilter();
if (!validateFilenames(otherArgs.slice(1), server, stdIO)) return;
const fileContent: DataToSearch[] = options.isSearchAll
? getServerFiles(server)
: getDataToGrep(otherArgs.slice(1), server, stdinContent);
if (options.isPipeIn) files.length = 0;
if (!options.isQuiet) writeToTerminal(prettyResult, options, results, files, pattern);
if (params.outfile && outFilePath) server.writeToContentFile(outFilePath, rawResult.join("\n"));
try {
applyFilters(options, params, otherArgs, fileContent, server, stdIO);
} catch (error) {
console.error(error);
Terminal.error(`grep processing error: ${error}`);
Terminal.error(`grep processing error: ${error}`, stdIO);
}
void callOnRead(stdIO, (data: unknown, stdInOut: StdIO) => {
const content = { filename: "stdin", content: stringify(data) };
applyFilters(options, params, otherArgs, [content], server, stdInOut);
});
}
function applyFilters(
options: Options,
params: Parameters,
otherArgs: string[],
fileContent: DataToSearch[],
server: BaseServer,
stdIO: StdIO,
) {
const nContext = Math.max(Number(params.preContext), Number(params.context), Number(params.postContext));
const nLimit = Number(params.maxMatches);
const outFilePath = checkOutFile(params.outfile, options, server, stdIO);
const pattern = options.isRegExpr ? new RegExp(otherArgs[0], "g") : otherArgs[0];
const contentToMatch = getContentToMatch(pattern, options, fileContent);
const results = new Results(contentToMatch, options, params);
const [rawResult, prettyResult] = results.capMatches(nLimit).addContext(nContext).splitAndFilter();
if (!options.isQuiet) writeToTerminal(prettyResult, options, results, fileContent, pattern, stdIO);
if (params.outfile && outFilePath) server.writeToContentFile(outFilePath, rawResult.join("\n"));
}
function getContentToMatch(pattern: RegExp | string, options: Options, fileContent: DataToSearch[]): ParsedLine[] {
if (options.isPipeIn) {
return grabTerminal().map((terminalOutput, i) => parseLine(pattern, options, "Terminal", terminalOutput, i));
}
return fileContent.flatMap((fileContent) =>
fileContent.content.split("\n").map((line, i) => parseLine(pattern, options, fileContent.filename, line, i)),
);
}
function getDataToGrep(filenames: string[], server: BaseServer, stdinContent: string): DataToSearch[] {
const dataToGrep: DataToSearch[] = [];
const stdinToGrep = {
filename: "stdin",
content: stdinContent,
};
for (const filename of filenames) {
if (filename === "-") {
dataToGrep.push(stdinToGrep);
} else {
dataToGrep.push({
filename,
content: getFileContents(filename, server),
});
}
}
// If stdin location is not explicitly specified, append it to the end
if (!filenames.includes("-") && stdinContent.length) {
dataToGrep.push(stdinToGrep);
}
return dataToGrep;
}
type DataToSearch = {
filename: string;
content: string;
};

View File

@@ -1,12 +1,13 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function grow(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 0) return Terminal.error("Incorrect usage of grow command. Usage: grow");
export function grow(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) return Terminal.error("Incorrect usage of grow command. Usage: grow", stdIO);
if (server.purchasedByPlayer) return Terminal.error("Cannot grow your own machines!");
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!");
if (server.purchasedByPlayer) return Terminal.error("Cannot grow your own machines!", stdIO);
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!", stdIO);
// Grow does not require meeting the hacking level, but undefined requiredHackingSkill indicates the wrong type of server.
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot grow this server.");
Terminal.startGrow();
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot grow this server.", stdIO);
Terminal.startGrow(stdIO);
}

View File

@@ -1,17 +1,19 @@
import { Terminal } from "../../Terminal";
import { Player } from "@player";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function hack(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 0) return Terminal.error("Incorrect usage of hack command. Usage: hack");
if (server.purchasedByPlayer) return Terminal.error("Cannot hack your own machines!");
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!");
export function hack(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) return Terminal.error("Incorrect usage of hack command. Usage: hack", stdIO);
if (server.purchasedByPlayer) return Terminal.error("Cannot hack your own machines!", stdIO);
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!", stdIO);
// Acts as a functional check that the server is hackable. Hacknet servers should already be filtered out anyway by purchasedByPlayer
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot hack this server.");
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot hack this server.", stdIO);
if (server.requiredHackingSkill > Player.skills.hacking) {
return Terminal.error(
"Your hacking skill is not high enough to hack this machine. Try analyzing the machine to determine the required hacking skill",
stdIO,
);
}
Terminal.startHack();
Terminal.startHack(stdIO);
}

View File

@@ -1,20 +1,22 @@
import { Terminal } from "../../Terminal";
import { TerminalHelpText, HelpTexts } from "../HelpText";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function help(args: (string | number | boolean)[]): void {
export function help(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0 && args.length !== 1) {
Terminal.error("Incorrect usage of help command. Usage: help");
Terminal.error("Incorrect usage of help command. Usage: help", stdIO);
return;
}
if (args.length === 0) {
TerminalHelpText.forEach((line) => Terminal.print(line));
TerminalHelpText.forEach((line) => Terminal.print(line, stdIO));
} else {
const cmd = args[0] + "";
const txt = HelpTexts[cmd];
if (txt == null) {
Terminal.error("No help topics match '" + cmd + "'");
Terminal.error("No help topics match '" + cmd + "'", stdIO);
return;
}
txt.forEach((t) => Terminal.print(t));
txt.forEach((t) => Terminal.print(t, stdIO));
}
}

View File

@@ -1,10 +1,12 @@
import { Terminal } from "../../Terminal";
import { Player } from "@player";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function history(args: (string | number | boolean)[]): void {
export function history(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length === 0) {
Terminal.commandHistory.forEach((command, index) => {
Terminal.print(`${index.toString().padStart(2)} ${command}`);
Terminal.print(`${index.toString().padStart(2)} ${command}`, stdIO);
});
return;
}
@@ -14,6 +16,6 @@ export function history(args: (string | number | boolean)[]): void {
Terminal.commandHistory = [];
Terminal.commandHistoryIndex = 1;
} else {
Terminal.error("Incorrect usage of history command. usage: history [-c]");
Terminal.error("Incorrect usage of history command. usage: history [-c]", stdIO);
}
}

View File

@@ -1,10 +1,11 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function hostname(args: (string | number | boolean)[], server: BaseServer): void {
export function hostname(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) {
Terminal.error("Incorrect usage of hostname command. Usage: hostname");
Terminal.error("Incorrect usage of hostname command. Usage: hostname", stdIO);
return;
}
Terminal.print(server.hostname);
Terminal.print(server.hostname, stdIO);
}

View File

@@ -1,10 +1,11 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function ipaddr(args: (string | number | boolean)[], server: BaseServer): void {
export function ipaddr(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) {
Terminal.error("Incorrect usage of hostname command. Usage: ipaddr");
Terminal.error("Incorrect usage of hostname command. Usage: ipaddr", stdIO);
return;
}
Terminal.print(server.ip);
Terminal.print(server.ip, stdIO);
}

View File

@@ -4,11 +4,12 @@ import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
import type { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function kill(args: (string | number | boolean)[], server: BaseServer): void {
export function kill(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
try {
if (args.length < 1 || typeof args[0] === "boolean") {
Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]...");
Terminal.error("Incorrect usage of kill command. Usage: kill [pid] or kill [scriptname] [arg1] [arg2]...", stdIO);
return;
}
@@ -17,35 +18,36 @@ export function kill(args: (string | number | boolean)[], server: BaseServer): v
const pid = args[0];
const res = killWorkerScriptByPid(pid);
if (res) {
Terminal.print(`Killing script with PID ${pid}`);
Terminal.print(`Killing script with PID ${pid}`, stdIO);
} else {
Terminal.error(`Failed to kill script with PID ${pid}. No such script is running`);
Terminal.error(`Failed to kill script with PID ${pid}. No such script is running`, stdIO);
}
return;
}
const path = Terminal.getFilepath(args[0]);
if (!path) return Terminal.error(`Invalid filename: ${args[0]}`);
if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Kill can only be used on scripts.`);
if (!path) return Terminal.error(`Invalid filename: ${args[0]}`, stdIO);
if (!hasScriptExtension(path))
return Terminal.error(`Invalid file extension. Kill can only be used on scripts.`, stdIO);
const runningScripts = findRunningScripts(path, args.slice(1), server);
if (runningScripts === null) {
Terminal.error("No such script is running. Nothing to kill");
Terminal.error("No such script is running. Nothing to kill", stdIO);
return;
}
let killed = 0;
for (const pid of runningScripts.keys()) {
killed++;
if (killed < 5) {
Terminal.print(`Killing ${path} with pid ${pid}`);
Terminal.print(`Killing ${path} with pid ${pid}`, stdIO);
}
killWorkerScriptByPid(pid);
}
if (killed >= 5) {
Terminal.print(`... killed ${killed} instances total`);
Terminal.print(`... killed ${killed} instances total`, stdIO);
}
} catch (error) {
console.error(error);
Terminal.error(String(error));
Terminal.error(String(error), stdIO);
}
}

View File

@@ -1,9 +1,10 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
import { StdIO } from "../StdIO/StdIO";
export function killall(_args: (string | number | boolean)[], server: BaseServer): void {
Terminal.print("Killing all running scripts");
export function killall(_args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
Terminal.print("Killing all running scripts", stdIO);
for (const byPid of server.runningScriptMap.values()) {
for (const runningScript of byPid.values()) {
killWorkerScriptByPid(runningScript.pid);

View File

@@ -26,10 +26,11 @@ import {
import { isMember } from "../../utils/EnumHelper";
import { Settings } from "../../Settings/Settings";
import { formatBytes, formatRam } from "../../ui/formatNumber";
import { StdIO } from "../StdIO/StdIO";
import { DarknetServer } from "../../Server/DarknetServer";
import type { CacheFilePath } from "../../Paths/CacheFilePath";
export function ls(args: (string | number | boolean)[], server: BaseServer): void {
export function ls(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
enum FileType {
Folder,
Message,
@@ -76,7 +77,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
const numArgs = args.length;
function incorrectUsage(): void {
Terminal.error("Incorrect usage of ls command. Usage: ls [dir] [-l] [-h] [-g, --grep pattern]");
Terminal.error("Incorrect usage of ls command. Usage: ls [dir] [-l] [-h] [-g, --grep pattern]", stdIO);
}
if (numArgs > 5) {
@@ -262,7 +263,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
})();
function onClick(): void {
if (!server.isConnectedTo) {
return Terminal.error(`File is not on this server, connect to ${server.hostname} and try again`);
return Terminal.error(`File is not on this server, connect to ${server.hostname} and try again`, stdIO);
}
// Message and lit files are always in root, no need to combine path with base directory
if (isMember("MessageFilename", props.path)) {
@@ -327,6 +328,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
>
{nameElement}
</LongListItem>,
stdIO,
);
}
} else {
@@ -335,7 +337,7 @@ export function ls(args: (string | number | boolean)[], server: BaseServer): voi
return React.cloneElement(nameElement, { key: segmentPath.toString() });
});
const colSize = Math.ceil(Math.max(...segments.map((segment) => segment.length)) * 0.7) + "em";
Terminal.printRaw(<SegmentGrid colSize={colSize}>{segmentElements}</SegmentGrid>);
Terminal.printRaw(<SegmentGrid colSize={colSize}>{segmentElements}</SegmentGrid>, stdIO);
}
}

View File

@@ -1,6 +1,7 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function lscpu(_args: (string | number | boolean)[], server: BaseServer): void {
Terminal.print(server.cpuCores + " Core(s)");
export function lscpu(_args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
Terminal.print(server.cpuCores + " Core(s)", stdIO);
}

View File

@@ -2,11 +2,12 @@ import { Terminal } from "../../Terminal";
import { formatRam } from "../../ui/formatNumber";
import { Settings } from "../../Settings/Settings";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function mem(args: (string | number | boolean)[], server: BaseServer): void {
export function mem(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
try {
if (args.length !== 1 && args.length !== 3) {
Terminal.error("Incorrect usage of mem command. usage: mem [scriptname] [-t] [number threads]");
Terminal.error("Incorrect usage of mem command. usage: mem [scriptname] [-t] [number threads]", stdIO);
return;
}
@@ -15,36 +16,36 @@ export function mem(args: (string | number | boolean)[], server: BaseServer): vo
if (args.length === 3 && args[1] === "-t") {
numThreads = Math.round(parseInt(args[2] + ""));
if (isNaN(numThreads) || numThreads < 1) {
Terminal.error("Invalid number of threads specified. Number of threads must be greater than 1");
Terminal.error("Invalid number of threads specified. Number of threads must be greater than 1", stdIO);
return;
}
}
const script = Terminal.getScript(scriptName);
if (script == null) {
Terminal.error("mem failed. No such script exists!");
Terminal.error("mem failed. No such script exists!", stdIO);
return;
}
const singleRamUsage = script.getRamUsage(server.scripts);
if (!singleRamUsage) return Terminal.error(`Could not calculate ram usage for ${scriptName}`);
if (!singleRamUsage) return Terminal.error(`Could not calculate ram usage for ${scriptName}`, stdIO);
const ramUsage = singleRamUsage * numThreads;
Terminal.print(`This script requires ${formatRam(ramUsage)} of RAM to run for ${numThreads} thread(s)`);
Terminal.print(`This script requires ${formatRam(ramUsage)} of RAM to run for ${numThreads} thread(s)`, stdIO);
const verboseEntries = script.ramUsageEntries.sort((a, b) => b.cost - a.cost) ?? [];
const padding = Settings.UseIEC60027_2 ? 9 : 8;
for (const entry of verboseEntries) {
Terminal.print(`${formatRam(entry.cost * numThreads).padStart(padding)} | ${entry.name} (${entry.type})`);
Terminal.print(`${formatRam(entry.cost * numThreads).padStart(padding)} | ${entry.name} (${entry.type})`, stdIO);
}
if (ramUsage > 0 && verboseEntries.length === 0) {
// Let's warn the user that he might need to save his script again to generate the detailed entries
Terminal.warn("You might have to open & save this script to see the detailed RAM usage information.");
Terminal.warn("You might have to open & save this script to see the detailed RAM usage information.", stdIO);
}
} catch (error) {
console.error(error);
Terminal.error(String(error));
Terminal.error(String(error), stdIO);
}
}

View File

@@ -2,8 +2,9 @@ import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { matchScriptPathUnanchored } from "../../utils/helpers/scriptKey";
import libarg from "arg";
import { StdIO } from "../StdIO/StdIO";
export function ps(args: (string | number | boolean)[], server: BaseServer): void {
export function ps(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
let flags: {
"--grep": string;
};
@@ -18,7 +19,7 @@ export function ps(args: (string | number | boolean)[], server: BaseServer): voi
);
} catch (e) {
// catch passing only -g / --grep with no string to use as the search
Terminal.error("Incorrect usage of ps command. Usage: ps [-g, --grep pattern]");
Terminal.error("Incorrect usage of ps command. Usage: ps [-g, --grep pattern]", stdIO);
return;
}
let pattern = flags["--grep"];
@@ -30,7 +31,7 @@ export function ps(args: (string | number | boolean)[], server: BaseServer): voi
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, stdIO);
}
}
}

View File

@@ -5,8 +5,9 @@ import { getAllDirectories, type Directory } from "../../Paths/Directory";
import type { ProgramFilePath } from "../../Paths/ProgramFilePath";
import type { IReturnStatus } from "../../types";
import type { FilePath } from "../../Paths/FilePath";
import { StdIO } from "../StdIO/StdIO";
export function rm(args: (string | number | boolean)[], server: BaseServer): void {
export function rm(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
const errors = {
arg: (reason: string) => `Incorrect usage of rm command. ${reason}. Usage: rm [OPTION]... [FILE]...`,
dirsProvided: (name: string) =>
@@ -19,7 +20,7 @@ export function rm(args: (string | number | boolean)[], server: BaseServer): voi
"You are trying to delete all files within the root directory. If this is intentional, use the --no-preserve-root flag",
} as const;
if (args.length === 0) return Terminal.error(errors["arg"]("No arguments provided"));
if (args.length === 0) return Terminal.error(errors["arg"]("No arguments provided"), stdIO);
const recursive = args.includes("-r") || args.includes("-R") || args.includes("--recursive") || args.includes("-rf");
const force = args.includes("-f") || args.includes("--force") || args.includes("-rf");
@@ -33,8 +34,8 @@ export function rm(args: (string | number | boolean)[], server: BaseServer): voi
typeof arg === "string" && (!arg.startsWith("-") || (index - 1 >= 0 && array[index - 1] === "--"));
const targets = args.filter(isTargetString);
if (targets.length === 0) return Terminal.error(errors["arg"]("No targets provided"));
if (!ignoreSpecialRoot && targets.includes("/")) return Terminal.error(errors["rootDeletion"]());
if (targets.length === 0) return Terminal.error(errors["arg"]("No targets provided"), stdIO);
if (!ignoreSpecialRoot && targets.includes("/")) return Terminal.error(errors["rootDeletion"](), stdIO);
const directories: Directory[] = [];
const files: FilePath[] = [];
@@ -62,7 +63,7 @@ export function rm(args: (string | number | boolean)[], server: BaseServer): voi
const fileExists = file !== null && allFiles.has(file);
if (fileDir === null) return Terminal.error(errors.invalidFile(target));
if (fileDir === null) return Terminal.error(errors.invalidFile(target), stdIO);
const dirExists = allDirs.has(fileDir);
if (file === null || dirExists) {
// If file === null, it means we specified a trailing-slash directory/,
@@ -82,11 +83,11 @@ export function rm(args: (string | number | boolean)[], server: BaseServer): voi
continue;
} else {
// Only exists as a directory (maybe).
return Terminal.error(errors.dirsProvided(target));
return Terminal.error(errors.dirsProvided(target), stdIO);
}
}
if (!dirExists && !force) {
return Terminal.error(errors.noSuchDir(target));
return Terminal.error(errors.noSuchDir(target), stdIO);
}
// If we pass -f and pass a non-existing directory, we will add it
// here and then it will match no files, producing no errors. This
@@ -96,7 +97,7 @@ export function rm(args: (string | number | boolean)[], server: BaseServer): voi
}
if (!force && !allFiles.has(file)) {
// With -f, we ignore file-not-found and try to delete everything at the end.
return Terminal.error(errors.noSuchFile(target));
return Terminal.error(errors.noSuchFile(target), stdIO);
}
files.push(file);
}
@@ -120,9 +121,9 @@ export function rm(args: (string | number | boolean)[], server: BaseServer): voi
for (const report of reports) {
if (report.result.res) {
Terminal.success(`Deleted: ${report.target}`);
Terminal.success(`Deleted: ${report.target}`, stdIO);
} else {
Terminal.error(errors.deleteFailed(report.target, report.result.msg));
Terminal.error(errors.deleteFailed(report.target, report.result.msg), stdIO);
}
}
};

View File

@@ -5,30 +5,33 @@ import { runProgram } from "./runProgram";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
import { hasContractExtension } from "../../Paths/ContractFilePath";
import { hasProgramExtension } from "../../Paths/ProgramFilePath";
import { StdIO } from "../StdIO/StdIO";
import { hasCacheExtension } from "../../Paths/CacheFilePath";
export function run(args: (string | number | boolean)[], server: BaseServer): void {
export function run(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
// Run a program or a script
const arg = args.shift();
if (!arg)
return Terminal.error(
"Usage: run [program/script] [-t num_threads] [--tail] [--ram-override ram_in_GBs] [--temporary] [args...]",
stdIO,
);
const path = Terminal.getFilepath(String(arg));
if (!path) return Terminal.error(`${arg} is not a valid filepath.`);
if (!path) return Terminal.error(`${arg} is not a valid filepath.`, stdIO);
if (hasScriptExtension(path)) {
return runScript(path, args, server);
runScript(path, args, server, stdIO);
return;
} else if (hasContractExtension(path)) {
Terminal.runContract(path).catch((error) => {
Terminal.runContract(path, stdIO).catch((error) => {
console.error(error);
Terminal.error(`Cannot run contract ${path} on ${server.hostname}. Error: ${error}.`);
Terminal.error(`Cannot run contract ${path} on ${server.hostname}. Error: ${error}.`, stdIO);
});
return;
} else if (hasProgramExtension(path)) {
return runProgram(path, args, server);
return runProgram(path, args, server, stdIO);
} else if (hasCacheExtension(path)) {
return Terminal.startAction(4, "c", server);
return Terminal.startAction(4, "c", stdIO, server);
}
Terminal.error(`Invalid file extension. Only .js, .jsx, .ts, .tsx, .cct, and .exe files can be run.`);
Terminal.error(`Invalid file extension. Only .js, .jsx, .ts, .tsx, .cct, and .exe files can be run.`, stdIO);
}

View File

@@ -4,8 +4,14 @@ import { BaseServer } from "../../Server/BaseServer";
import { Programs } from "../../Programs/Programs";
import { ProgramFilePath } from "../../Paths/ProgramFilePath";
import { getRecordKeys } from "../../Types/Record";
import { StdIO } from "../StdIO/StdIO";
export function runProgram(path: ProgramFilePath, args: (string | number | boolean)[], server: BaseServer): void {
export function runProgram(
path: ProgramFilePath,
args: (string | number | boolean)[],
server: BaseServer,
stdIO: StdIO,
): void {
// Check if you have the program on your computer. If you do, execute it, otherwise
// display an error message
const programLowered = path.toLowerCase();
@@ -16,8 +22,9 @@ export function runProgram(path: ProgramFilePath, args: (string | number | boole
if (!realProgramName || (!Player.hasProgram(realProgramName) && !programPresentOnServer)) {
Terminal.error(
`No such (js, jsx, ts, tsx, script, cct, or exe) file! (Only finished programs that exist on your home computer or scripts on ${server.hostname} can be run)`,
stdIO,
);
return;
}
Programs[realProgramName].run(args.map(String), server);
Programs[realProgramName].run(args.map(String), server, stdIO);
}

View File

@@ -10,12 +10,15 @@ import { sendDeprecationNotice } from "./common/deprecation";
import { roundToTwo } from "../../utils/helpers/roundToTwo";
import { RamCostConstants } from "../../Netscript/RamCostGenerator";
import { pluralize } from "../../utils/I18nUtils";
import { RunningScript } from "../../Script/RunningScript";
import { StdIO } from "../StdIO/StdIO";
export function runScript(
scriptPath: ScriptFilePath,
commandArgs: (string | number | boolean)[],
server: BaseServer,
): void {
stdIO: StdIO,
): RunningScript | undefined {
if (isLegacyScript(scriptPath)) {
sendDeprecationNotice();
return;
@@ -35,18 +38,20 @@ export function runScript(
argv: commandArgs,
});
} catch (error) {
Terminal.error(`Invalid arguments. ${error}.`);
Terminal.error(`Invalid arguments. ${error}.`, stdIO);
return;
}
const tailFlag = flags["--tail"] === true;
const numThreads = parseFloat(flags["-t"] ?? 1);
const ramOverride = flags["--ram-override"] != null ? roundToTwo(parseFloat(flags["--ram-override"])) : undefined;
if (!isPositiveInteger(numThreads)) {
return Terminal.error("Invalid number of threads specified. Number of threads must be an integer greater than 0");
Terminal.error("Invalid number of threads specified. Number of threads must be an integer greater than 0", stdIO);
return;
}
if (ramOverride != null && (isNaN(ramOverride) || ramOverride < RamCostConstants.Base)) {
Terminal.error(
`Invalid ram override specified. Ram override must be a number greater than ${RamCostConstants.Base}`,
stdIO,
);
return;
}
@@ -62,7 +67,7 @@ export function runScript(
args,
);
if (!result.success) {
Terminal.error(result.message);
Terminal.error(result.message, stdIO);
return;
}
@@ -72,11 +77,11 @@ export function runScript(
const success = startWorkerScript(runningScript, server);
if (!success) {
Terminal.error(`Failed to start script`);
Terminal.error(`Failed to start script`, stdIO);
return;
}
Terminal.print(
Terminal.printAndBypassPipes(
`Running script with ${pluralize(numThreads, "thread")}, pid ${runningScript.pid} and args: ${JSON.stringify(
args,
)}.`,
@@ -84,5 +89,17 @@ export function runScript(
if (tailFlag) {
LogBoxEvents.emit(runningScript);
}
return;
Terminal.pidOfLastScriptRun = runningScript.pid;
// Bind stdio to script
runningScript.stdin = stdIO.stdin?.deref() ?? null;
runningScript.terminalStdOut = stdIO;
// scripts interacting with terminal pipes are temporary, to avoid orphaned or partial pipelines on start
if (runningScript.stdin || stdIO.stdout) {
runningScript.temporary = true;
}
return runningScript;
}

View File

@@ -2,10 +2,11 @@ import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { getServerOnNetwork } from "../../Server/ServerHelpers";
import { IPAddress } from "../../Types/strings";
import { StdIO } from "../StdIO/StdIO";
export function scan(args: (string | number | boolean)[], currServ: BaseServer): void {
export function scan(args: (string | number | boolean)[], currServ: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) {
Terminal.error("Incorrect usage of scan command. Usage: scan");
Terminal.error("Incorrect usage of scan command. Usage: scan", stdIO);
return;
}
// Displays available network connections using TCP
@@ -32,6 +33,6 @@ export function scan(args: (string | number | boolean)[], currServ: BaseServer):
entry += server.ip;
entry += " ".repeat(maxIP - server.ip.length + 1);
entry += server.hasRoot;
Terminal.print(entry);
Terminal.print(entry, stdIO);
}
}

View File

@@ -1,14 +1,16 @@
import { Player } from "@player";
import { CompletedProgramName } from "@enums";
import { Terminal } from "../../Terminal";
import { StdIO } from "../StdIO/StdIO";
import { BaseServer } from "../../Server/BaseServer";
export function scananalyze(args: (string | number | boolean)[]): void {
export function scananalyze(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length === 0) {
Terminal.executeScanAnalyzeCommand();
Terminal.executeScanAnalyzeCommand(1, false, stdIO);
} else {
// # of args must be 2 or 3
if (args.length > 2) {
Terminal.error("Incorrect usage of scan-analyze command. usage: scan-analyze [depth]");
Terminal.error("Incorrect usage of scan-analyze command. usage: scan-analyze [depth]", stdIO);
return;
}
let all = false;
@@ -19,19 +21,19 @@ export function scananalyze(args: (string | number | boolean)[]): void {
const depth = parseInt(args[0] + "");
if (isNaN(depth) || depth < 0) {
return Terminal.error("Incorrect usage of scan-analyze command. depth argument must be positive numeric");
return Terminal.error("Incorrect usage of scan-analyze command. depth argument must be positive numeric", stdIO);
}
if (
depth > 3 &&
!Player.hasProgram(CompletedProgramName.deepScan1) &&
!Player.hasProgram(CompletedProgramName.deepScan2)
) {
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 3");
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 3", stdIO);
} else if (depth > 5 && !Player.hasProgram(CompletedProgramName.deepScan2)) {
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 5");
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 5", stdIO);
} else if (depth > 10) {
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 10");
return Terminal.error("You cannot scan-analyze with that high of a depth. Maximum depth is 10", stdIO);
}
Terminal.executeScanAnalyzeCommand(depth, all);
Terminal.executeScanAnalyzeCommand(depth, all, stdIO);
}
}

View File

@@ -6,16 +6,17 @@ import { hasTextExtension } from "../../Paths/TextFilePath";
import { isMember } from "../../utils/EnumHelper";
import { LiteratureName } from "@enums";
import { ContentFile } from "../../Paths/ContentFile";
import { StdIO } from "../StdIO/StdIO";
export function scp(args: (string | number | boolean)[], server: BaseServer): void {
export function scp(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length < 2) {
return Terminal.error("Incorrect usage of scp command. Usage: scp [source filename] [destination hostname]");
return Terminal.error("Incorrect usage of scp command. Usage: scp [source filename] [destination hostname]", stdIO);
}
// Validate destination server
const destHostname = String(args.pop());
const destServer = GetReachableServer(destHostname);
if (!destServer) return Terminal.error(`Invalid destination server: ${destHostname}`);
if (!destServer) return Terminal.error(`Invalid destination server: ${destHostname}`, stdIO);
// Validate filepaths
const filenames = args.map(String);
@@ -24,11 +25,11 @@ export function scp(args: (string | number | boolean)[], server: BaseServer): vo
// File validation loop, handle all errors before copying any files
for (const filename of filenames) {
const path = Terminal.getFilepath(filename);
if (!path) return Terminal.error(`Invalid file path: ${filename}`);
if (!path) return Terminal.error(`Invalid file path: ${filename}`, stdIO);
// Validate .lit files
if (path.endsWith(".lit")) {
if (!isMember("LiteratureName", path) || !server.messages.includes(path)) {
return Terminal.error(`scp failed: ${path} does not exist on server ${server.hostname}`);
return Terminal.error(`scp failed: ${path} does not exist on server ${server.hostname}`, stdIO);
}
files.push(path);
continue;
@@ -37,10 +38,12 @@ export function scp(args: (string | number | boolean)[], server: BaseServer): vo
if (!hasScriptExtension(path) && !hasTextExtension(path)) {
return Terminal.error(
`scp failed: ${path} has invalid extension. scp only works for scripts (.js, .jsx, .ts, .tsx), text files (.txt, .json, .css), and literature files (.lit)`,
stdIO,
);
}
const sourceContentFile = server.getContentFile(path);
if (!sourceContentFile) return Terminal.error(`scp failed: ${path} does not exist on server ${server.hostname}`);
if (!sourceContentFile)
return Terminal.error(`scp failed: ${path} does not exist on server ${server.hostname}`, stdIO);
files.push(sourceContentFile);
}
@@ -49,18 +52,18 @@ export function scp(args: (string | number | boolean)[], server: BaseServer): vo
// Lit files, entire "file" is just the name
if (isMember("LiteratureName", file)) {
if (destServer.messages.includes(file)) {
Terminal.print(`${file} was already on ${destHostname}, file skipped`);
Terminal.print(`${file} was already on ${destHostname}, file skipped`, stdIO);
continue;
}
destServer.messages.push(file);
Terminal.print(`${file} copied to ${destHostname}`);
Terminal.print(`${file} copied to ${destHostname}`, stdIO);
continue;
}
// Content files (script and txt)
const { filename, content } = file;
const { overwritten } = destServer.writeToContentFile(filename, content);
if (overwritten) Terminal.warn(`${filename} already existed on ${destHostname} and was overwritten`);
else Terminal.print(`${filename} copied to ${destHostname}`);
if (overwritten) Terminal.warn(`${filename} already existed on ${destHostname} and was overwritten`, stdIO);
else Terminal.print(`${filename} copied to ${destHostname}`, stdIO);
}
}

View File

@@ -1,15 +1,16 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function sudov(args: (string | number | boolean)[], server: BaseServer): void {
export function sudov(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) {
Terminal.error("Incorrect number of arguments. Usage: sudov");
Terminal.error("Incorrect number of arguments. Usage: sudov", stdIO);
return;
}
if (server.hasAdminRights) {
Terminal.print("You have ROOT access to this machine");
Terminal.print("You have ROOT access to this machine", stdIO);
} else {
Terminal.print("You do NOT have root access to this machine");
Terminal.print("You do NOT have root access to this machine", stdIO);
}
}

View File

@@ -1,10 +1,14 @@
import { escapeRegExp } from "lodash";
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { findRunningScripts, findRunningScriptByPid } from "../../Script/ScriptHelpers";
import { LogBoxEvents } from "../../ui/React/LogBoxManager";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
import { hasScriptExtension, ScriptFilePath } from "../../Paths/ScriptFilePath";
import { RunningScript } from "../../Script/RunningScript";
import { matchScriptPathExact } from "../../utils/helpers/scriptKey";
import { StdIO } from "../StdIO/StdIO";
export function tail(commandArray: (string | number | boolean)[], server: BaseServer): void {
export function tail(commandArray: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
try {
if (commandArray.length < 1) {
Terminal.error("Incorrect number of arguments. Usage: tail [pid] or tail [scriptname] [arg1] [arg2]...");
@@ -14,18 +18,32 @@ export function tail(commandArray: (string | number | boolean)[], server: BaseSe
if (!path) return Terminal.error(`Invalid filename: ${rawName}`);
if (!hasScriptExtension(path)) return Terminal.error(`Invalid file extension. Tail can only be used on scripts.`);
// Only select from name match if there is no ambiguity and no argument filter specified
const scriptsMatchingName = commandArray.length === 1 ? findRunningScriptsByFilename(path, server) : null;
const scriptMatchingName = scriptsMatchingName?.size === 1 ? scriptsMatchingName.values().next() : null;
// Check for exact matches with specified arguments
const candidates = findRunningScripts(path, args, server);
if (candidates === null && (scriptsMatchingName?.size ?? 0) > 1) {
Terminal.error(
`Multiple scripts named ${path} are running on the server. ` +
`Specify arguments to pick which script to tail.`,
);
return;
}
// if there's no candidate then we just don't know.
if (candidates === null) {
if (candidates === null && scriptMatchingName === 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).
const next = candidates.values().next();
if (!next.done) {
LogBoxEvents.emit(next.value);
const next = scriptMatchingName ?? candidates?.values().next();
if (next && !next.done) {
handleTail(next.value, stdIO);
}
} else if (typeof commandArray[0] === "number") {
const runningScript = findRunningScriptByPid(commandArray[0]);
@@ -33,10 +51,34 @@ export function tail(commandArray: (string | number | boolean)[], server: BaseSe
Terminal.error(`No script with PID ${commandArray[0]} is running`);
return;
}
LogBoxEvents.emit(runningScript);
handleTail(runningScript, stdIO);
}
} catch (error) {
console.error(error);
Terminal.error(String(error));
}
}
function handleTail(script: RunningScript, stdIO: StdIO): void {
if (!stdIO.stdout) {
return LogBoxEvents.emit(script);
}
script.tailStdOut = stdIO;
script.logs.forEach((log) => {
script.tailStdOut?.write?.(log);
});
}
function findRunningScriptsByFilename(path: ScriptFilePath, server: BaseServer): Map<number, RunningScript> | null {
const result = new Map<number, RunningScript>();
const pattern = matchScriptPathExact(escapeRegExp(path));
for (const [key, runningScriptMap] of server.runningScriptMap.entries()) {
if (pattern.test(key)) {
for (const [pid, runningScript] of runningScriptMap.entries()) {
result.set(pid, runningScript);
}
}
}
return result.size > 0 ? result : null;
}

View File

@@ -1,10 +1,11 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { formatRam } from "../../ui/formatNumber";
import { StdIO } from "../StdIO/StdIO";
export function top(args: (string | number | boolean)[], server: BaseServer): void {
export function top(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) {
Terminal.error("Incorrect usage of top command. Usage: top");
Terminal.error("Incorrect usage of top command. Usage: top", stdIO);
return;
}
@@ -24,7 +25,7 @@ export function top(args: (string | number | boolean)[], server: BaseServer): vo
const headers = `${scriptTxt}${spacesAfterScriptTxt}${pidTxt}${spacesAfterPidTxt}${threadsTxt}${spacesAfterThreadsTxt}${ramTxt}`;
Terminal.print(headers);
Terminal.print(headers, stdIO);
const currRunningScripts = server.runningScriptMap;
// Iterate through scripts on current server
@@ -48,7 +49,7 @@ export function top(args: (string | number | boolean)[], server: BaseServer): vo
const entry = [script.filename, spacesScript, script.pid, spacesPid, script.threads, spacesThread, ramUsage].join(
"",
);
Terminal.print(entry);
Terminal.print(entry, stdIO);
}
}
}

View File

@@ -1,12 +1,13 @@
import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { StdIO } from "../StdIO/StdIO";
export function weaken(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 0) return Terminal.error("Incorrect usage of weaken command. Usage: weaken");
export function weaken(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length !== 0) return Terminal.error("Incorrect usage of weaken command. Usage: weaken", stdIO);
if (server.purchasedByPlayer) return Terminal.error("Cannot weaken your own machines!");
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!");
if (server.purchasedByPlayer) return Terminal.error("Cannot weaken your own machines!", stdIO);
if (!server.hasAdminRights) return Terminal.error("You do not have admin rights for this machine!", stdIO);
// Weaken does not require meeting the hacking level, but undefined requiredHackingSkill indicates the wrong type of server.
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot weaken this server.");
Terminal.startWeaken();
if (server.requiredHackingSkill === undefined) return Terminal.error("Cannot weaken this server.", stdIO);
Terminal.startWeaken(stdIO);
}

View File

@@ -2,34 +2,56 @@ import { Terminal } from "../../Terminal";
import { BaseServer } from "../../Server/BaseServer";
import { hasScriptExtension } from "../../Paths/ScriptFilePath";
import { hasTextExtension } from "../../Paths/TextFilePath";
import { StdIO } from "../StdIO/StdIO";
export function wget(args: (string | number | boolean)[], server: BaseServer): void {
if (args.length !== 2 || typeof args[0] !== "string" || typeof args[1] !== "string") {
Terminal.error("Incorrect usage of wget command. Usage: wget [url] [target file]");
// TODO-FICO: unit tests
export function wget(args: (string | number | boolean)[], server: BaseServer, stdIO: StdIO): void {
if (args.length === 2 && stdIO.stdout) {
Terminal.error(
"Incorrect use of wget command. Either specify a destination file or redirect the output with a pipe, not both.",
);
return;
}
const [source, fileName] = args;
const argCountIsValid = (args.length === 1 && stdIO.stdout) || args.length === 2;
const arg1IsValid = typeof source === "string";
const arg2IsValid = (args.length === 1 && stdIO.stdout) || typeof fileName === "string";
if (!argCountIsValid || !arg1IsValid || !arg2IsValid) {
Terminal.error("Incorrect usage of wget command. Usage: wget [url] [target file]", stdIO);
return;
}
const target = Terminal.getFilepath(`${fileName}`);
if (args.length === 2 && (!target || (!hasScriptExtension(target) && !hasTextExtension(target)))) {
Terminal.error(`wget failed: Invalid target file. Target file must be a script file or a text file.`, stdIO);
return;
}
const target = Terminal.getFilepath(args[1]);
if (!target || (!hasScriptExtension(target) && !hasTextExtension(target))) {
Terminal.error(`wget failed: Invalid target file. Target file must be a script file or a text file.`);
return;
}
fetch(args[0])
fetch(source)
.then(async (response) => {
if (response.status !== 200) {
Terminal.error(`wget failed. HTTP code: ${response.status}.`);
Terminal.error(`wget failed. HTTP code: ${response.status}.`, stdIO);
return;
}
const writeResult = server.writeToContentFile(target, await response.text());
if (writeResult.overwritten) {
Terminal.print(`wget successfully retrieved content and overwrote ${target}`);
} else {
Terminal.print(`wget successfully retrieved content to new file ${target}`);
const content = await response.text();
if (stdIO.stdout) {
Terminal.printAndBypassPipes(`wget successfully retrieved content`);
stdIO.write(content);
stdIO.close();
return;
}
if (target && (hasScriptExtension(target) || hasTextExtension(target))) {
const writeResult = server.writeToContentFile(target, content);
if (writeResult.overwritten) {
Terminal.print(`wget successfully retrieved content and overwrote ${target}`);
} else {
Terminal.print(`wget successfully retrieved content to new file ${target}`);
}
}
})
.catch((reason) => {
// Check the comment in wget of src\NetscriptFunctions.ts to see why we use Object.getOwnPropertyNames.
Terminal.error(`wget failed: ${JSON.stringify(reason, Object.getOwnPropertyNames(reason))}`);
Terminal.error(`wget failed: ${JSON.stringify(reason, Object.getOwnPropertyNames(reason))}`, stdIO);
});
}

View File

@@ -16,13 +16,19 @@ import { Terminal } from "../Terminal";
import { parseUnknownError } from "../utils/ErrorHelper";
import { DarknetServer } from "../Server/DarknetServer";
import { CompletedProgramName } from "@enums";
import { getCommandAfterLastPipe } from "./StdIO/utils";
/** Suggest all completion possibilities for the last argument in the last command being typed
* @param terminalText The current full text entered in the terminal
* @param baseDir The current working directory.
* @returns Array of possible string replacements for the current text being autocompleted.
*/
export async function getTabCompletionPossibilities(terminalText: string, baseDir = root): Promise<string[]> {
export async function getTabCompletionPossibilities(fullTerminalText: string, baseDir = root): Promise<string[]> {
// Get the text in the terminal after the most recent pipe character
const terminalText = getCommandAfterLastPipe(fullTerminalText);
// True if there is a pipe in the terminal text
const isInPipe = fullTerminalText !== terminalText;
// Get the current command text
const currentText = /[^ ]*$/.exec(terminalText)?.[0] ?? "";
// Remove the current text from the commands string
@@ -75,9 +81,10 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi
function addGeneric({ iterable, usePathing, ignoreCurrent }: AddAllGenericOptions) {
const requiredStart = usePathing ? pathingRequiredMatch : requiredMatch;
for (const member of iterable) {
if (ignoreCurrent && member.length <= requiredStart.length) continue;
const itemToAdd = usePathing ? relativeDir + member.substring(baseDir.length) : member;
if ((ignoreCurrent && member.length <= requiredStart.length) || possibilities.includes(itemToAdd)) continue;
if (member.toLowerCase().startsWith(requiredStart)) {
possibilities.push(usePathing ? relativeDir + member.substring(baseDir.length) : member);
possibilities.push(itemToAdd);
}
}
}
@@ -163,7 +170,6 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi
addCodingContracts();
}
}
switch (commandArray[0]) {
case "buy":
addDarkwebItems();
@@ -267,6 +273,14 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi
if (options) {
addGeneric({ iterable: options, usePathing: false });
}
} else {
// Add script names if you are in a command - scripts can be run by name
addScripts();
// Include text files if the command is part of a pipe
if (isInPipe) {
addTextFiles();
}
}
return possibilities;
}

View File

@@ -231,11 +231,11 @@ export function TerminalInput(): React.ReactElement {
if (event.key === KEY.ENTER) {
event.preventDefault();
const command = searchResults.length ? searchResults[searchResultsIndex] : value;
Terminal.print(`[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> ${command}`);
Terminal.printAndBypassPipes(`[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> ${command}`);
if (command) {
Terminal.executeCommands(command);
saveValue("");
resetSearch();
await Terminal.executeCommands(command);
}
return;
}

View File

@@ -7,7 +7,7 @@ import { Settings } from "../../Settings/Settings";
// This particular eslint-disable is correct.
// In this super specific weird case we in fact do want a regex on an ANSII character.
// eslint-disable-next-line no-control-regex
const ANSI_ESCAPE = new RegExp("\u{001b}\\[(?<code>.*?)m", "ug");
export const ANSI_ESCAPE = new RegExp("\u{001b}\\[(?<code>.*?)m", "ug");
const useStyles = makeStyles()((theme: Theme) => ({
success: {

View File

@@ -8,6 +8,7 @@ import { resolveTextFilePath } from "../../Paths/TextFilePath";
import { dialogBoxCreate as dialogBoxCreateOriginal } from "../../ui/React/DialogBox";
import { Terminal } from "../../Terminal";
import { pluralize } from "../I18nUtils";
import { getTerminalStdIO } from "../../Terminal/StdIO/RedirectIO";
// Temporary until fixing alerts manager to store alerts outside of react scope
const dialogBoxCreate = (text: string) =>
@@ -171,8 +172,12 @@ export function showAPIBreaks(version: string, { additionalText, apiBreakingChan
textFileName,
`API BREAK INFO FOR ${version}\n\n${details.map((detail) => detail.text).join("\n\n\n\n")}`,
);
Terminal.warn(`AN API BREAK FROM VERSION ${version} MAY HAVE AFFECTED SOME OF YOUR SCRIPTS.`);
Terminal.warn(`INFORMATION ABOUT THIS POTENTIAL IMPACT HAS BEEN LOGGED IN ${textFileName} ON YOUR HOME COMPUTER.`);
const stdIO = getTerminalStdIO();
Terminal.warn(`AN API BREAK FROM VERSION ${version} MAY HAVE AFFECTED SOME OF YOUR SCRIPTS.`, stdIO);
Terminal.warn(
`INFORMATION ABOUT THIS POTENTIAL IMPACT HAS BEEN LOGGED IN ${textFileName} ON YOUR HOME COMPUTER.`,
stdIO,
);
dialogBoxCreate(
`SOME OF YOUR SCRIPTS HAVE POTENTIALLY BEEN IMPACTED BY AN API BREAK, DUE TO CHANGES IN VERSION ${version}\n\n` +
"The following dialog boxes will provide details of the potential impact to your scripts.\n" +
@@ -197,7 +202,9 @@ export function showAPIBreaks(version: string, { additionalText, apiBreakingChan
(detail.apiBreakInfo.brokenAPIs.length > 0
? `\n\nWe found ${pluralize(detail.totalDetectedLines, "affected line")}.`
: ""),
stdIO,
);
++warningIndex;
}
stdIO.close();
}

View File

@@ -14,6 +14,7 @@ import { WorkerScript } from "../../../src/Netscript/WorkerScript";
import { NetscriptFunctions } from "../../../src/NetscriptFunctions";
import type { PositiveInteger } from "../../../src/types";
import { ErrorState } from "../../../src/ErrorHandling/ErrorState";
import { getTerminalStdIO } from "../../../src/Terminal/StdIO/RedirectIO";
fixDoImportIssue();
@@ -36,7 +37,7 @@ async function expectErrorWhenRunningScript(
for (const script of scripts) {
Player.getHomeComputer().writeToScriptFile(script.filePath, script.code);
}
runScript(testScriptPath, [], Player.getHomeComputer());
runScript(testScriptPath, [], Player.getHomeComputer(), getTerminalStdIO());
const workerScript = workerScripts.get(1);
if (!workerScript) {
throw new Error(`Invalid worker script`);
@@ -145,7 +146,7 @@ describe("runScript and runScriptFromScript", () => {
ns.print(server.hostname);
}`,
);
runScript(testScriptPath, [], Player.getHomeComputer());
runScript(testScriptPath, [], Player.getHomeComputer(), getTerminalStdIO());
const workerScript = workerScripts.get(1);
if (!workerScript) {
throw new Error(`Invalid worker script`);
@@ -160,7 +161,7 @@ describe("runScript and runScriptFromScript", () => {
});
describe("Failure", () => {
test("Script does not exist", () => {
runScript(testScriptPath, [], Player.getHomeComputer());
runScript(testScriptPath, [], Player.getHomeComputer(), getTerminalStdIO());
expect((Terminal.outputHistory[1] as { text: string }).text).toContain(
`Script ${testScriptPath} does not exist on home`,
);
@@ -172,7 +173,7 @@ describe("runScript and runScriptFromScript", () => {
`export async function main(ns) {
}`,
);
runScript(testScriptPath, [], server);
runScript(testScriptPath, [], server, getTerminalStdIO());
expect((Terminal.outputHistory[1] as { text: string }).text).toContain(
`You do not have root access on ${server.hostname}`,
);
@@ -184,7 +185,7 @@ describe("runScript and runScriptFromScript", () => {
{
}`,
);
runScript(testScriptPath, [], Player.getHomeComputer());
runScript(testScriptPath, [], Player.getHomeComputer(), getTerminalStdIO());
expect((Terminal.outputHistory[1] as { text: string }).text).toContain(
`Cannot calculate RAM usage of ${testScriptPath}`,
);
@@ -196,7 +197,7 @@ describe("runScript and runScriptFromScript", () => {
ns.ramOverride(1024);
}`,
);
runScript(testScriptPath, [], Player.getHomeComputer());
runScript(testScriptPath, [], Player.getHomeComputer(), getTerminalStdIO());
expect((Terminal.outputHistory[1] as { text: string }).text).toContain("This script requires 1.02TB of RAM");
});
test("Thrown error in main function", async () => {

View File

@@ -59,6 +59,12 @@ function loadStandardServers() {
"ramUsage": 1.6,
"server": "home",
"scriptKey": "script.js*[]",
"stdin": null,
"tailStdOut": null,
"terminalStdOut": {
"stdin": null,
"stdout": null
},
"temporary": true,
"dependencies": [
{
@@ -85,6 +91,12 @@ function loadStandardServers() {
"ramUsage": 1.6,
"server": "home",
"scriptKey": "script.js*[]",
"stdin": null,
"tailStdOut": null,
"terminalStdOut": {
"stdin": null,
"stdout": null
},
"title": "Awesome Script",
"dependencies": [
{

View File

@@ -0,0 +1,494 @@
import { Terminal } from "../../../src/Terminal";
import { GetServer, prestigeAllServers } from "../../../src/Server/AllServers";
import { Player } from "@player";
import { type TextFilePath } from "../../../src/Paths/TextFilePath";
import { type ScriptFilePath } from "../../../src/Paths/ScriptFilePath";
import { LiteratureName, MessageFilename } from "@enums";
import { fixDoImportIssue, initGameEnvironment } from "../Utilities";
import { runScript } from "../../../src/Terminal/commands/runScript";
import { getTerminalStdIO } from "../../../src/Terminal/StdIO/RedirectIO";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
fixDoImportIssue();
initGameEnvironment();
describe("Terminal Pipes", () => {
beforeEach(() => {
prestigeAllServers();
Player.init();
Terminal.outputHistory = [];
GetServer(Player.currentServer)?.textFiles.clear();
GetServer(Player.currentServer)?.scripts.clear();
});
describe("piping to files", () => {
it("should handle piping to a file", async () => {
const fileName = "output.txt";
const command = `echo 'Hello World' > ${fileName}`;
await Terminal.executeCommands(command);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(fileName as TextFilePath)?.text;
expect(JSON.stringify(Terminal.outputHistory)).toBe("[]");
expect(fileContent).toBe("Hello World");
});
it("should reject invalid text filenames", async () => {
const invalidFileName = 'a".txt';
const command = `echo 'Hello World' > ${invalidFileName}`;
await Terminal.executeCommands(command);
const mostRecentOutput = Terminal.outputHistory[Terminal.outputHistory.length - 1];
expect(mostRecentOutput?.text).toBe(`Invalid file path provided: ${invalidFileName}`);
});
it("should reject invalid script filenames", async () => {
const invalidFileName = 'a".js';
const command = `echo 'Hello World' > ${invalidFileName}`;
await Terminal.executeCommands(command);
const mostRecentOutput = Terminal.outputHistory[Terminal.outputHistory.length - 1];
expect(mostRecentOutput?.text).toBe(`Invalid file path provided: ${invalidFileName}`);
});
it("should append to a file when using >> operator", async () => {
const fileName = "output.txt";
const commandString = `echo first line >> ${fileName}; echo second line >> ${fileName}`;
await Terminal.executeCommands(commandString);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(fileName as TextFilePath)?.text;
expect(JSON.stringify(Terminal.outputHistory)).toBe("[]");
expect(fileContent).toBe("first line\nsecond line");
});
it("should overwrite a file when using > operator", async () => {
const fileName = "output.txt";
const commandString = `echo first line > ${fileName}; echo second line > ${fileName}`;
await Terminal.executeCommands(commandString);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(fileName as TextFilePath)?.text;
expect(fileContent).toBe("second line");
});
it("should only overwrite file contents once per > pipe", async () => {
// Add file to server with content
const outputFileName = "scriptOutput9.txt" as TextFilePath;
const startingData = "startingData";
const commandString = `echo ${startingData} > ${outputFileName}`;
await Terminal.executeCommands(commandString);
const scriptName = "testScript.js" as ScriptFilePath;
const scriptContent = `export async function main(ns) { ns.tprint(ns.args); await ns.sleep(100); ns.tprint(ns.args); }`;
// Add script to server
await Terminal.executeCommands(`echo '${scriptContent}' > ${scriptName}`);
// Pass arguments to script via pipe
const command = `run ${scriptName} test1 > ${outputFileName}`;
await Terminal.executeCommands(command);
await sleep(200);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(outputFileName)?.text;
expect(Terminal.outputHistory.length).toBe(1);
expect(fileContent).toContain(`${scriptName}: ["test1"]\n${scriptName}: ["test1"]`);
expect(fileContent).not.toContain(startingData);
});
it("should only overwrite file contents once per > pipe when arguments are piped in", async () => {
// Add file to server with content
const outputFileName = "scriptOutput8.txt" as TextFilePath;
const startingData = "startingData";
const commandString = `echo ${startingData} > ${outputFileName}`;
await Terminal.executeCommands(commandString);
const scriptName = "testScript.js" as ScriptFilePath;
const scriptContent = `export async function main(ns) { ns.tprint(ns.getStdin().read()); await ns.sleep(100); ns.tprint(ns.getStdin().read()); }`;
// Add script to server
await Terminal.executeCommands(`echo '${scriptContent}' > ${scriptName}`);
// Pass arguments to script via pipe
const command = `echo test1 test2 | ${scriptName} > ${outputFileName}`;
await Terminal.executeCommands(command);
await sleep(200);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(outputFileName)?.text;
expect(Terminal.outputHistory.length).toBe(1);
expect(fileContent).toContain(`${scriptName}: test1 test2\n${scriptName}: null`);
expect(fileContent).not.toContain(startingData);
});
it("should not permit overwriting a script file with content", async () => {
const fileName = "output.js";
const commandString = `echo 'Hello World' > ${fileName}; echo 'Malicious Content' > ${fileName}`;
await Terminal.executeCommands(commandString);
const server = GetServer(Player.currentServer);
const fileContent = server?.scripts?.get(fileName as ScriptFilePath)?.content;
expect(fileContent).toContain("Hello World");
});
});
describe("piping multiple inputs", () => {
it("should handle multiple commands with distinct pipes", async () => {
const fileName1 = "output.txt";
const fileName2 = "output2.txt";
const commandString = `echo test > ${fileName1}; echo test2 > ${fileName2}`;
await Terminal.executeCommands(commandString);
expect(JSON.stringify(Terminal.outputHistory)).toBe("[]");
const server = GetServer(Player.currentServer);
const fileContent1 = server?.textFiles?.get(fileName1 as TextFilePath)?.text;
expect(fileContent1).toBe("test");
const fileContent2 = server?.textFiles?.get(fileName2 as TextFilePath)?.text;
expect(fileContent2).toBe("test2");
});
it("passes all piped inputs to the output command", async () => {
await Terminal.executeCommands("echo 1337 > file1.txt");
const command = "cat file1.txt > file2.txt";
await Terminal.executeCommands(command);
await sleep(100);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get("file2.txt" as TextFilePath)?.text;
expect(fileContent).toBe("1337");
});
});
describe("cat and echo with pipes", () => {
it("should pipe cat file contents to specified output", async () => {
const fileName = "test4.txt";
const fileContent = "This is a test file.";
await Terminal.executeCommands(`echo '${fileContent}' > ${fileName}`);
await Terminal.executeCommands(`cat '${fileName}' | cat`);
const server = GetServer(Player.currentServer);
const newFileContent = server?.textFiles?.get(fileName as TextFilePath)?.text;
expect(newFileContent).toBe(fileContent);
const lastOutput = Terminal.outputHistory[Terminal.outputHistory.length - 1];
expect(lastOutput.text).toContain(fileContent);
});
it("should pipe cat .lit file contents to specified output", async () => {
const fileName = "test.txt";
const server = GetServer(Player.currentServer);
server?.messages.push(LiteratureName.HackersStartingHandbook);
await Terminal.executeCommands(`cat ${LiteratureName.HackersStartingHandbook} > ${fileName}`);
await Terminal.executeCommands(`cat ${fileName} | cat `);
const newFileContent = server?.textFiles?.get(fileName as TextFilePath)?.text;
expect(newFileContent).toContain("hacking is the most profitable way to earn money and progress");
const lastOutput = Terminal.outputHistory[Terminal.outputHistory.length - 1];
expect(lastOutput.text).toContain("hacking is the most profitable way to earn money and progress");
});
it("should pipe cat message file contents to specified output", async () => {
const fileName = "test3.txt";
const server = GetServer(Player.currentServer);
server?.messages.push(MessageFilename.TruthGazer);
await Terminal.executeCommands(`cat ${MessageFilename.TruthGazer} > ${fileName}`);
await Terminal.executeCommands(`cat ${fileName} | cat `);
const newFileContent = server?.textFiles?.get(fileName as TextFilePath)?.text;
expect(newFileContent).toContain("__ESCAP3__");
const lastOutput = Terminal.outputHistory[Terminal.outputHistory.length - 1];
expect(lastOutput.text).toContain("__ESCAP3__");
});
});
describe("piping to and from scripts", () => {
it("should handle piping to a script file, and passing arguments into a script to run", async () => {
const scriptName = "testScript2.js" as ScriptFilePath;
const scriptContent = `export function main(ns) { ns.tprint('Input received: ', ns.getStdin().peek()); }`;
// Add script to server
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
const content = GetServer(Player.currentServer)?.scripts.get(scriptName)?.content;
expect(content).toBe(scriptContent);
// Pass arguments to script via pipe
const command = `echo 'data' | run ${scriptName}`;
await Terminal.executeCommands(command);
await sleep(100);
expect(Terminal.outputHistory[0]?.text).toContain(`Running script with 1 thread`);
expect(Terminal.outputHistory[1]?.text).toEqual(`${scriptName}: Input received: data`);
});
it("should piping content out of a script", async () => {
const outputFileName = "scriptOutput4.txt" as TextFilePath;
const scriptName = "testScript.js" as ScriptFilePath;
const scriptContent = `export async function main(ns) { ns.tprint('Input received: ', ns.getStdin().peek()); }`;
// Add script to server
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
const content = GetServer(Player.currentServer)?.scripts.get(scriptName)?.content;
expect(content).toBe(scriptContent);
// Pass arguments to script via pipe
const command = `echo 'data' | ${scriptName} > ${outputFileName}`;
await Terminal.executeCommands(command);
await sleep(200);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(outputFileName)?.text;
expect(Terminal.outputHistory.length).toBe(1);
expect(fileContent).toContain(`Input received: data`);
});
it("should pipe content out of a script when the run command is used", async () => {
const outputFileName = "scriptOutput3.txt" as TextFilePath;
const scriptName = "testScript.js" as ScriptFilePath;
const scriptContent = `export function main(ns) { ns.tprint('Args received: ', ns.args); }`;
// Add script to server
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
const content = GetServer(Player.currentServer)?.scripts.get(scriptName)?.content;
expect(content).toBe(scriptContent);
// Pass arguments to script via pipe
const command = `run ${scriptName} test1 arguments > ${outputFileName}`;
await Terminal.executeCommands(command);
await sleep(200);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(outputFileName)?.text;
expect(Terminal.outputHistory.length).toBe(1);
expect(fileContent).toContain(`Args received: ["test1","arguments"]`);
});
it("should correctly pipe each script's async output to its specified location", async () => {
// Add file to server with content
const outputFileName = "scriptOutput.txt" as TextFilePath;
const outputFileName2 = "scriptOutput2.txt" as TextFilePath;
const scriptName = "testScript.js" as ScriptFilePath;
const scriptContent = `export async function main(ns) { ns.tprint(ns.args); await ns.sleep(100); ns.tprint(ns.args); }`;
// Add script to server
await Terminal.executeCommands(`echo '${scriptContent}' > ${scriptName}`);
// Pass arguments to script via pipe
const command = `run ${scriptName} test1 test2 > ${outputFileName}; run ${scriptName} test3 test4 > ${outputFileName2}`;
await Terminal.executeCommands(command);
await sleep(300);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(outputFileName)?.text;
const fileContent2 = server?.textFiles?.get(outputFileName2)?.text;
expect(Terminal.outputHistory.length).toBe(2);
expect(fileContent).toContain(`${scriptName}: ["test1","test2"]\n${scriptName}: ["test1","test2"]`);
expect(fileContent2).toContain(`${scriptName}: ["test3","test4"]\n${scriptName}: ["test3","test4"]`);
});
it("should correctly pipe a script's async output to a specified destination script", async () => {
// Add file to server with content
const outputFileName = "scriptOutput.txt" as TextFilePath;
const scriptName = "testScript.js" as ScriptFilePath;
const scriptName2 = "testScript2.js" as ScriptFilePath;
const scriptContent = `export async function main(ns) { ns.tprint(ns.getStdin().peek()); await ns.sleep(80); ns.tprint(ns.getStdin().peek()); }`;
const scriptContent2 = `export async function main(ns) { ns.tprint(ns.getStdin().read()); await ns.sleep(200); ns.tprint(ns.getStdin().read()); ns.tprint(ns.getStdin().read()); }`;
// Add script to server
await Terminal.executeCommands(
`echo '${scriptContent}' > ${scriptName}; echo '${scriptContent2}' > ${scriptName2}`,
);
// Pass arguments to script via pipe
const command = `echo 1 | ${scriptName} | ${scriptName2} > ${outputFileName}`;
await Terminal.executeCommands(command);
await sleep(300);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(outputFileName)?.text;
expect(Terminal.outputHistory.length).toBe(2);
expect(fileContent).toContain(`${scriptName2}: ${scriptName}: 1\n${scriptName2}: NULL PORT DATA`);
});
it("should correctly pipe each script's async output to its specified destination script", async () => {
// Add file to server with content
const outputFileName = "scriptOutput.txt" as TextFilePath;
const outputFileName2 = "scriptOutput2.txt" as TextFilePath;
const scriptName = "testScript.js" as ScriptFilePath;
const scriptName2 = "testScript2.js" as ScriptFilePath;
const scriptName3 = "testScript3.js" as ScriptFilePath;
const scriptName4 = "testScript4.js" as ScriptFilePath;
const scriptContent = `export async function main(ns) { ns.tprint(ns.getStdin().peek()); await ns.sleep(80); ns.tprint(ns.getStdin().peek()); }`;
const scriptContent2 = `export async function main(ns) { ns.tprint(ns.getStdin().read()); await ns.sleep(200); ns.tprint(ns.getStdin().read()); ns.tprint(ns.getStdin().read()); }`;
// Add script to server
await Terminal.executeCommands(
`echo '${scriptContent}' > ${scriptName}; echo '${scriptContent2}' > ${scriptName2}`,
);
await Terminal.executeCommands(`cat ${scriptName} > ${scriptName3}; cat ${scriptName2} > ${scriptName4};`);
// Pass arguments to script via pipe
const command = `echo 1 | ${scriptName} | ${scriptName2} > ${outputFileName}; echo 2 | ${scriptName3} | ${scriptName4} > ${outputFileName2}`;
await Terminal.executeCommands(command);
await sleep(300);
const server = GetServer(Player.currentServer);
const fileContent = server?.textFiles?.get(outputFileName)?.text;
const fileContent2 = server?.textFiles?.get(outputFileName2)?.text;
expect(Terminal.outputHistory.length).toBe(4);
expect(fileContent).toContain(`${scriptName2}: ${scriptName}: 1\n${scriptName2}: NULL PORT DATA`);
expect(fileContent2).toContain(`${scriptName4}: ${scriptName3}: 2\n${scriptName4}: NULL PORT DATA`);
});
});
describe("input redirection", () => {
it("should use file contents as input stream if input redirection < is used", async () => {
const fileContent = "File input data";
const fileName = "inputFile.txt";
await Terminal.executeCommands(`echo '${fileContent}' > ${fileName}`);
const fileContentOnServer = GetServer(Player.currentServer)?.textFiles?.get(fileName as TextFilePath)?.text;
expect(fileContentOnServer).toBe(fileContent);
const commandString = `cat < ${fileName} | cat `;
await Terminal.executeCommands(commandString);
const lastOutput = Terminal.outputHistory[Terminal.outputHistory.length - 1];
expect(lastOutput?.text).toBe(fileContent);
});
it("should return an error if input redirection file does not exist", async () => {
const fileName = "nonExistentFile.txt";
const commandString = `cat < ${fileName}`;
await Terminal.executeCommands(commandString);
const lastOutput = Terminal.outputHistory[Terminal.outputHistory.length - 2];
expect(lastOutput?.text).toBe(`No file at path ${fileName}`);
});
it("should return an error if the input redirection is not the first pipe in the chain", async () => {
await Terminal.executeCommands(`echo 'Some data' | cat < inputFile.txt`);
const error = Terminal.outputHistory[0];
expect(error?.text).toBe(
`Error in pipe command: Invalid pipe command. Only the first command in a pipe chain can have input redirection '<'.`,
);
});
});
it("should handle piping content to cat", async () => {
const testContent = "This is a test.";
const commandString = `echo "${testContent}" | cat`;
await Terminal.executeCommands(commandString);
await sleep(50);
expect(Terminal.outputHistory.length).toBe(1);
expect(Terminal.outputHistory[0].text).toContain(testContent);
});
it("should replace $! with the PID of the last script run", async () => {
const scriptName = "testScript.js" as ScriptFilePath;
const scriptContent = `export async function main(ns) { ns.print('Script is running'); await ns.sleep(100); }`;
const server = GetServer(Player.currentServer);
// Add script to server
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
await sleep(50);
// Run the script to set PipeState.pidOfLastScriptRun
const runningScript = runScript(scriptName, [], server, getTerminalStdIO());
const expectedPid = runningScript?.pid;
await sleep(200);
const command = `echo $! > pidOutput.txt`;
await Terminal.executeCommands(command);
await sleep(50);
const fileContent = server?.textFiles?.get("pidOutput.txt" as TextFilePath)?.text;
expect(Number(fileContent)).toBe(expectedPid);
});
it("should replace $! with -1 if the prior command was not a run", async () => {
const scriptName = "testScript.js" as ScriptFilePath;
const scriptContent = `export async function main(ns) { ns.print("Script is running"); await ns.sleep(100); }`;
const server = GetServer(Player.currentServer);
// Add script to server
await Terminal.executeCommands(`echo '${scriptContent}' > ${scriptName}`);
await sleep(50);
// Run the script to set PipeState.pidOfLastScriptRun
await Terminal.executeCommands(`run ${scriptName}`);
await sleep(200);
await Terminal.executeCommands(`echo "Not a run command"`);
const command = `echo $! > pidOutput.txt`;
await Terminal.executeCommands(command);
const fileContent = server?.textFiles?.get("pidOutput.txt" as TextFilePath)?.text;
expect(Number(fileContent)).toBe(-1);
});
it("should pipe the tail output of scripts to stdout when specified with $!", async () => {
const scriptContent = `export async function main(ns) {ns.print('foo');await ns.sleep(50);ns.print('test2');}`;
const scriptName = "testScript.jsx" as ScriptFilePath;
const tailOutputFileName = "tailOutput.txt" as TextFilePath;
// Add script to server
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
const fileContent = GetServer(Player.currentServer)?.scripts?.get(scriptName)?.content;
expect(fileContent).toBe(scriptContent);
await Terminal.executeCommands(`run ${scriptName}; tail $! > ${tailOutputFileName}`);
await sleep(200);
const outputFileContent = GetServer(Player.currentServer)?.textFiles?.get(tailOutputFileName)?.text;
expect(outputFileContent).toContain("foo\nsleep: Sleeping for 0.050 seconds.\ntest2");
});
it("should pipe the tail output of scripts to stdout", async () => {
const scriptContent = `export async function main(ns) {ns.print('foo');await ns.sleep(50);ns.print('test2');}`;
const scriptName = "testScript.jsx" as ScriptFilePath;
const tailOutputFileName = "tailOutput.txt" as TextFilePath;
// Add script to server
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
const fileContent = GetServer(Player.currentServer)?.scripts?.get(scriptName)?.content;
expect(fileContent).toBe(scriptContent);
await Terminal.executeCommands(`run ${scriptName}; tail ${scriptName} > ${tailOutputFileName}`);
await sleep(200);
const outputFileContent = GetServer(Player.currentServer)?.textFiles?.get(tailOutputFileName)?.text;
expect(outputFileContent).toContain("foo\nsleep: Sleeping for 0.050 seconds.\ntest2");
});
});

View File

@@ -0,0 +1,207 @@
import { IOStream } from "../../../src/Terminal/StdIO/IOStream";
import {
findCommandsSplitByRedirects,
getTerminalStdIO,
handleCommand,
parseRedirectedCommands,
} from "../../../src/Terminal/StdIO/RedirectIO";
import { Terminal } from "../../../src/Terminal";
import { fixDoImportIssue, initGameEnvironment } from "../Utilities";
import { GetServer, prestigeAllServers } from "../../../src/Server/AllServers";
import { Player } from "@player";
import { StdIO } from "../../../src/Terminal/StdIO/StdIO";
import { TextFilePath } from "../../../src/Paths/TextFilePath";
import { ScriptFilePath } from "../../../src/Paths/ScriptFilePath";
import { Output } from "../../../src/Terminal/OutputTypes";
import { ANSI_ESCAPE } from "../../../src/ui/React/ANSIITypography";
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
fixDoImportIssue();
initGameEnvironment();
describe("RedirectIOTests", () => {
beforeEach(() => {
prestigeAllServers();
Player.init();
Terminal.outputHistory = [];
GetServer(Player.currentServer)?.textFiles.clear();
GetServer(Player.currentServer)?.scripts.clear();
});
it("should redirect output to the terminal correctly from a terminal StdIO", async () => {
const data = "Hello, Terminal!";
const terminalIO = getTerminalStdIO(null);
terminalIO.write(data);
await sleep(50);
expect(Terminal.outputHistory.length).toBe(1);
expect(Terminal.outputHistory[0].text).toContain(data);
});
it("findCommandsSplitByRedirects should split commands by pipes", () => {
const commandString = "echo Hello > file.txt >> anotherFile.txt | echo World";
const parsedCommands = commandString.split(" ");
const result = findCommandsSplitByRedirects(parsedCommands);
expect(result[0]).toEqual(["echo", "Hello"]);
expect(result[1]).toEqual([">", "file.txt"]);
expect(result[2]).toEqual([">>", "anotherFile.txt"]);
expect(result[3]).toEqual(["|", "echo", "World"]);
expect(result.length).toBe(4);
});
describe("handleCommand", () => {
it("should handle echo command passing its args to stdout", async () => {
const commandString = "echo Hello, World";
const stdIO = new StdIO(null);
handleCommand(stdIO, commandString.split(" "));
await sleep(50);
expect(stdIO.stdout.empty()).toBe(false);
const output = stdIO.stdout.read();
expect(output).toBe("Hello, World");
});
it("should handle writing stdin contents to files", async () => {
const filename = "output.txt";
const commandString = `> ${filename}`;
const stdin = new IOStream();
const stdIO = new StdIO(stdin);
void handleCommand(stdIO, commandString.split(" "));
stdin.write("File content line 1");
stdin.write("File content line 2");
await sleep(50);
const server = GetServer(Player.currentServer);
const file = server?.textFiles.get(filename as TextFilePath);
expect(file).toBeDefined();
expect(file?.content).toBe("File content line 1\nFile content line 2");
});
});
describe("parseRedirectedCommands", () => {
it("should append echo output redirected to a file", async () => {
const filename = "appendOutput.txt";
const commandString = `echo First Line >> ${filename} | echo Second Line >> ${filename}`;
await parseRedirectedCommands(commandString);
const server = GetServer(Player.currentServer);
const file = server?.textFiles.get(filename as TextFilePath);
expect(file).toBeDefined();
expect(file?.content).toBe("First Line\nSecond Line");
});
it("should prevent overwriting non-empty script files", async () => {
const filename = "scriptOutput.js";
const commandString = `echo Hello > ${filename} | echo World > ${filename}`;
await parseRedirectedCommands(commandString);
const server = GetServer(Player.currentServer);
const file = server?.scripts.get(filename as ScriptFilePath);
expect(file).toBeDefined();
expect(file?.content).toBe("Hello");
});
});
describe("stdout from scripts", () => {
it("should redirect tprint output from a running script to a file", async () => {
const scriptName = "testScript.js";
const filename = "scriptLog.txt";
const scriptContent = `export function main(ns) { ns.tprint('Logging to file' ); }`;
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
const currentScripts = GetServer(Player.currentServer)?.scripts;
const script = currentScripts?.get(scriptName as ScriptFilePath);
expect(script?.content).toBe(scriptContent);
await Terminal.executeCommands(`run ${scriptName} >> ${filename}`);
await sleep(50);
const server = GetServer(Player.currentServer);
const file = server?.textFiles.get(filename as TextFilePath);
expect(file?.content).toBe("testScript.js: Logging to file");
});
});
describe("stdin to scripts", () => {
it("should provide stdin input to a running script", async () => {
const scriptName = "inputScript.js";
const scriptContent = `export async function main(ns) {
const stdIn = await ns.getStdin();
if (stdIn?.empty()) {
ns.tprint('No input received yet');
await stdIn.nextWrite();
}
const input = stdIn?.read();
ns.tprint('Received input: ' + input);
}`;
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
const inputData = "Hello from stdin!";
await Terminal.executeCommands(`echo "${inputData}" | run ${scriptName}`);
await sleep(50);
console.log(Terminal.outputHistory);
const outputLog: Output[] = Terminal.outputHistory.filter(isOutput);
const outputText: Output = outputLog.find((entry: Output) => entry.text?.includes("Received input:"));
expect(outputText?.text).toEqual(`${scriptName}: Received input: ${inputData}`);
});
it("should provide stdin input from a script to a running script", async () => {
const scriptName = "inputScript.js";
const scriptContent = `export async function main(ns) {
const stdIn = await ns.getStdin();
if (stdIn?.empty()) {
ns.tprint('No input received yet');
await stdIn.nextWrite();
}
const input = stdIn?.read();
ns.tprint('Received input: ' + input);
}`;
await Terminal.executeCommands(`echo "${scriptContent}" > ${scriptName}`);
const inputData = "Hello from stdin!";
await Terminal.executeCommands(`echo "${inputData}" | ${scriptName} | run ${scriptName}`);
await sleep(50);
console.log(Terminal.outputHistory);
const outputLog: Output[] = Terminal.outputHistory.filter(isOutput);
const outputText: Output = outputLog.find((entry: Output) => entry.text?.includes("Received input:"));
expect(outputText?.text).toEqual(`${scriptName}: Received input: ${scriptName}: Received input: ${inputData}`);
});
});
describe("cat and grep with redirected IO", () => {
it("should be able to read files to the terminal", async () => {
const filename = "appendOutput.txt";
const setupCommandString = `echo First Line >> ${filename} | echo Second Line >> ${filename}`;
await parseRedirectedCommands(setupCommandString);
await parseRedirectedCommands(`echo 1 | cat ${filename}`);
expect(Terminal.outputHistory.length).toBe(1);
expect(Terminal.outputHistory[0].text).toBe("First Line\nSecond Line1");
});
it("should be able to grep files read by cat", async () => {
const filename = "appendOutput.txt";
const setupCommandString = `echo First Line >> ${filename} | echo Second Line >> ${filename}`;
await parseRedirectedCommands(setupCommandString);
await parseRedirectedCommands(`cat ${filename} | grep Second`);
expect(Terminal.outputHistory.length).toBe(1);
const log = Terminal.outputHistory[0];
if (!isOutput(log)) throw new Error("Expected output to be of type Output");
expect(log.text.replaceAll(ANSI_ESCAPE, "")).toBe("Second Line");
});
});
});
function isOutput(entry: unknown): entry is Output {
return !!entry && typeof entry === "object" && entry instanceof Output;
}

View File

@@ -0,0 +1,142 @@
import { cat } from "../../../src/Terminal/commands/cat";
import { GetServerOrThrow, prestigeAllServers } from "../../../src/Server/AllServers";
import { Player } from "@player";
import { Terminal } from "../../../src/Terminal";
import { StdIO } from "../../../src/Terminal/StdIO/StdIO";
import { IOStream } from "../../../src/Terminal/StdIO/IOStream";
import { TextFile } from "../../../src/TextFile";
import { TextFilePath } from "../../../src/Paths/TextFilePath";
import { LiteratureName, MessageFilename } from "@enums";
import { Literatures } from "../../../src/Literature/Literatures";
import { stringifyReactElement } from "../../../src/Terminal/StdIO/utils";
import { Messages } from "../../../src/Message/MessageHelpers";
const fileName = "example.txt" as TextFilePath;
const fileName2 = "example2.txt" as TextFilePath;
const fileContent1 = "This is an example text file.";
const fileContent2 = "This is another example text file.";
describe("cat command", () => {
beforeEach(() => {
prestigeAllServers();
Player.init();
Terminal.outputHistory = [];
const server = GetServerOrThrow(Player.currentServer);
server.textFiles.clear();
server.scripts.clear();
server.messages.length = 0; //Remove .lit and .msg files
server.messages.push(LiteratureName.HackersStartingHandbook);
server.messages.push(MessageFilename.Jumper0);
const file = new TextFile(fileName, fileContent1);
server.textFiles.set(fileName, file);
const file2 = new TextFile(fileName2, fileContent2);
server.textFiles.set(fileName2, file2);
});
it("should retrieve file contents and pass to stdout", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdOut = new IOStream();
const stdIO = new StdIO(null, stdOut);
cat([fileName, fileName2], server, stdIO);
const output = stdOut.read();
expect(output).toBe(`${fileContent1}${fileContent2}`);
});
it("should read from stdin when '-' is provided as an argument", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdinStuff = "\nInput from stdin line 1";
const stdIn = new IOStream();
stdIn.write(stdinStuff);
const stdOut = new IOStream();
const stdIO = new StdIO(stdIn, stdOut);
cat([fileName, "-", fileName2], server, stdIO);
const output = stdOut.read();
expect(output).toBe(`${fileContent1}${stdinStuff}${fileContent2}`);
});
it("should read from stdin and concat it last when '-' is not provided as an argument", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdIn = new IOStream();
stdIn.write("Input from stdin line 1");
const stdOut = new IOStream();
const stdIO = new StdIO(stdIn, stdOut);
cat([fileName, fileName2], server, stdIO);
const output = stdOut.read();
expect(output).toBe(`${fileContent1}${fileContent2}Input from stdin line 1`);
});
it("should be able to read .lit files", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdOut = new IOStream();
const stdIO = new StdIO(null, stdOut);
cat([`${LiteratureName.HackersStartingHandbook}`], server, stdIO);
const output = stdOut.read();
const bodyText = stringifyReactElement(Literatures[LiteratureName.HackersStartingHandbook].text);
const expectedOutput = `${Literatures[LiteratureName.HackersStartingHandbook].title}\n\n${bodyText}\n`;
expect(output).toBe(expectedOutput);
expect(output).toContain("When starting out, hacking is the most profitable way to earn money and progress.");
});
it("should be able to read msg files", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdOut = new IOStream();
const stdIO = new StdIO(null, stdOut);
cat([`${MessageFilename.Jumper0}`], server, stdIO);
const output = stdOut.read();
const text = Messages[MessageFilename.Jumper0].msg + "\n";
expect(output).toBe(text);
});
it("should be able to concatenate lit and msg files", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdOut = new IOStream();
const stdIO = new StdIO(null, stdOut);
cat([`${LiteratureName.HackersStartingHandbook}`, `${MessageFilename.Jumper0}`], server, stdIO);
const output = stdOut.read();
const bodyText = stringifyReactElement(Literatures[LiteratureName.HackersStartingHandbook].text);
const expectedLitOutput = `${Literatures[LiteratureName.HackersStartingHandbook].title}\n\n${bodyText}\n`;
const expectedMsgOutput = Messages[MessageFilename.Jumper0].msg + "\n";
const expectedOutput = `${expectedLitOutput}${expectedMsgOutput}`;
expect(output).toBe(expectedOutput);
});
it("should be able to concatenate lit and msg files with stdin", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdIn = new IOStream();
stdIn.write("Input from stdin line 1");
const stdOut = new IOStream();
const stdIO = new StdIO(stdIn, stdOut);
cat([`${LiteratureName.HackersStartingHandbook}`, "-", `${MessageFilename.Jumper0}`], server, stdIO);
const output = stdOut.read();
const bodyText = stringifyReactElement(Literatures[LiteratureName.HackersStartingHandbook].text);
const expectedLitOutput = `${Literatures[LiteratureName.HackersStartingHandbook].title}\n\n${bodyText}\n`;
const expectedMsgOutput = Messages[MessageFilename.Jumper0].msg + "\n";
const expectedOutput = `${expectedLitOutput}Input from stdin line 1${expectedMsgOutput}`;
expect(output).toBe(expectedOutput);
});
});

View File

@@ -0,0 +1,85 @@
import { GetServerOrThrow, prestigeAllServers } from "../../../src/Server/AllServers";
import { Player } from "@player";
import { Terminal } from "../../../src/Terminal";
import { StdIO } from "../../../src/Terminal/StdIO/StdIO";
import { IOStream } from "../../../src/Terminal/StdIO/IOStream";
import { TextFile } from "../../../src/TextFile";
import { TextFilePath } from "../../../src/Paths/TextFilePath";
import { grep } from "../../../src/Terminal/commands/grep";
import { ScriptFilePath } from "../../../src/Paths/ScriptFilePath";
import { Script } from "../../../src/Script/Script";
import { stringify } from "../../../src/Terminal/StdIO/utils";
const fileName = "example.txt" as TextFilePath;
const fileName2 = "example2.txt" as TextFilePath;
const fileContent1 = "This is an example text file.\nThis is line 2 of file 1";
const fileContent2 = "This is another example text file.\nThis is line 2 of file 2";
describe("grep command", () => {
beforeEach(() => {
prestigeAllServers();
Player.init();
Terminal.outputHistory = [];
const server = GetServerOrThrow(Player.currentServer);
server.textFiles.clear();
server.scripts.clear();
const file = new TextFile(fileName, fileContent1);
server.textFiles.set(fileName, file);
const file2 = new TextFile(fileName2, fileContent2);
server.textFiles.set(fileName2, file2);
});
it("should retrieve lines matching the pattern from the specified text file", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdOut = new IOStream();
const stdIO = new StdIO(null, stdOut);
grep(["line 2", fileName], server, stdIO);
const output = stdOut.read();
expect(Terminal.outputHistory).toEqual([]);
expect(output).toBe(`example.txt:This is line 2 of file 1`);
});
it("should retrieve lines matching the pattern from the specified script file", () => {
const server = GetServerOrThrow(Player.currentServer);
const scriptFileName = "script.js" as ScriptFilePath;
const scriptContent = "console.log('Hello World');\n// This is line 2 of the script";
const scriptFile = new Script(scriptFileName, scriptContent, server.hostname);
server.scripts.set(scriptFileName, scriptFile);
const stdOut = new IOStream();
const stdIO = new StdIO(null, stdOut);
grep(["line 2", scriptFileName], server, stdIO);
const output = stdOut.read();
expect(Terminal.outputHistory).toEqual([]);
expect(output).toBe(`script.js:// This is line 2 of the script`);
});
it("should retrieve lines matching the pattern from stdin", () => {
const server = GetServerOrThrow(Player.currentServer);
const stdIn = new IOStream();
stdIn.write("First line from stdin\nThis is line 2 from stdin\nThird line from stdin");
stdIn.close();
const stdOut = new IOStream();
const stdIO = new StdIO(stdIn, stdOut);
grep(["line 2"], server, stdIO);
const output = stdOut.read();
expect(Terminal.outputHistory).toEqual([]);
expect(output).toBe(`This is line 2 from stdin`);
});
it("should grep input piped from cat", async () => {
await Terminal.executeCommands(`cat ${fileName} ${fileName2} | grep "line 2"`);
const lastOutput = Terminal.outputHistory[Terminal.outputHistory.length - 1];
// Output from cat will not have filenames, and will not add additional newlines between file contents
expect(stringify(lastOutput.text, true)).toBe(
`This is line 2 of file 1This is another example text file.\nThis is line 2 of file 2`,
);
});
});

View File

@@ -82,6 +82,12 @@ exports[`load/saveAllServers 1`] = `
"ramUsage": 1.6,
"server": "home",
"scriptKey": "script.js*[]",
"stdin": null,
"tailStdOut": null,
"terminalStdOut": {
"stdin": null,
"stdout": null
},
"title": "Awesome Script",
"threads": 1,
"temporary": false