diff --git a/markdown/bitburner.ns.getstdin.md b/markdown/bitburner.ns.getstdin.md
new file mode 100644
index 000000000..b02eead1c
--- /dev/null
+++ b/markdown/bitburner.ns.getstdin.md
@@ -0,0 +1,23 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [NS](./bitburner.ns.md) > [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
+
diff --git a/markdown/bitburner.ns.md b/markdown/bitburner.ns.md
index 43c291e42..9cb2627fb 100644
--- a/markdown/bitburner.ns.md
+++ b/markdown/bitburner.ns.md
@@ -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.
+
+
diff --git a/src/DarkWeb/DarkWeb.tsx b/src/DarkWeb/DarkWeb.tsx
index c213fbb9c..815f6bdf2 100644
--- a/src/DarkWeb/DarkWeb.tsx
+++ b/src/DarkWeb/DarkWeb.tsx
@@ -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 {
<>
{item.program} - {cost} - {item.description}
>,
+ 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;
}
}
diff --git a/src/Documentation/doc/en/index.md b/src/Documentation/doc/en/index.md
index 0994d34d9..7da8c94c4 100644
--- a/src/Documentation/doc/en/index.md
+++ b/src/Documentation/doc/en/index.md
@@ -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
diff --git a/src/Documentation/doc/en/programming/terminal_pipes_and_redirects.md b/src/Documentation/doc/en/programming/terminal_pipes_and_redirects.md
new file mode 100644
index 000000000..0d12924ec
--- /dev/null
+++ b/src/Documentation/doc/en/programming/terminal_pipes_and_redirects.md
@@ -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);
+ }
+}
+```
diff --git a/src/Documentation/pages.ts b/src/Documentation/pages.ts
index 1772241a0..e6bf61c88 100644
--- a/src/Documentation/pages.ts
+++ b/src/Documentation/pages.ts
@@ -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;
diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts
index cb3441d5f..03db7d7b9 100644
--- a/src/Netscript/RamCostGenerator.ts
+++ b/src/Netscript/RamCostGenerator.ts
@@ -664,6 +664,7 @@ export const RamCosts: RamCostTree = {
tprintRaw: 0,
printRaw: 0,
dynamicImport: 0,
+ getStdin: 0,
formulas: {
mockServer: 0,
diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts
index 19bc82d5c..c0e344235 100644
--- a/src/NetscriptFunctions.ts
+++ b/src/NetscriptFunctions.ts
@@ -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 = {
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 = {
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 = {
//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(),
diff --git a/src/NetscriptFunctions/Singularity.ts b/src/NetscriptFunctions/Singularity.ts
index 9b5731b1d..493c3e47b 100644
--- a/src/NetscriptFunctions/Singularity.ts
+++ b/src/NetscriptFunctions/Singularity.ts
@@ -468,7 +468,7 @@ export function NetscriptSingularity(): InternalAPI {
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);
diff --git a/src/Programs/Program.ts b/src/Programs/Program.ts
index 14cc53826..e6ea2a00d 100644
--- a/src/Programs/Program.ts
+++ b/src/Programs/Program.ts
@@ -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) {
diff --git a/src/Programs/Programs.ts b/src/Programs/Programs.ts
index 289728386..259f764f5 100644
--- a/src/Programs/Programs.ts
+++ b/src/Programs/Programs.ts
@@ -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 = {
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 = {
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 = {
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 = {
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 = {
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 = {
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 = {
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 = {
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 = {
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 = {
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 = {
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.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 = {
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);
},
diff --git a/src/Script/RunningScript.ts b/src/Script/RunningScript.ts
index 34b736127..4a2ba48b8 100644
--- a/src/Script/RunningScript.ts
+++ b/src/Script/RunningScript.ts
@@ -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);
}
}
}
diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts
index 92bb582e7..96482f4ee 100644
--- a/src/ScriptEditor/NetscriptDefinitions.d.ts
+++ b/src/ScriptEditor/NetscriptDefinitions.d.ts
@@ -8991,6 +8991,21 @@ export interface NS {
*/
dynamicImport(path: string): Promise;
+ /**
+ * 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;
}
diff --git a/src/Terminal/HelpText.ts b/src/Terminal/HelpText.ts
index 64239cc27..0c366b4ef 100644
--- a/src/Terminal/HelpText.ts
+++ b/src/Terminal/HelpText.ts
@@ -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 = {
" ",
],
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 = {
" ",
" 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 = {
"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]",
" ",
diff --git a/src/Terminal/Parser.ts b/src/Terminal/Parser.ts
index 27774c2de..27d687eee 100644
--- a/src/Terminal/Parser.ts
+++ b/src/Terminal/Parser.ts
@@ -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);
}
diff --git a/src/Terminal/StdIO/IOStream.ts b/src/Terminal/StdIO/IOStream.ts
new file mode 100644
index 000000000..7edcd2a2e
--- /dev/null
+++ b/src/Terminal/StdIO/IOStream.ts
@@ -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 {
+ return this.handle.nextWrite();
+ }
+
+ peek(): unknown {
+ return this.handle.peek();
+ }
+
+ read(): unknown {
+ return this.handle.read();
+ }
+}
diff --git a/src/Terminal/StdIO/RedirectIO.ts b/src/Terminal/StdIO/RedirectIO.ts
new file mode 100644
index 000000000..cdc386215
--- /dev/null
+++ b/src/Terminal/StdIO/RedirectIO.ts
@@ -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[] = [];
+ 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) {
+ 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 {
+ while (stdio.stdout && !stdio.stdout?.isClosed) {
+ await stdio.stdout.nextWrite();
+ }
+}
diff --git a/src/Terminal/StdIO/StdIO.ts b/src/Terminal/StdIO/StdIO.ts
new file mode 100644
index 000000000..04a3684df
--- /dev/null
+++ b/src/Terminal/StdIO/StdIO.ts
@@ -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 | 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();
+ }
+}
diff --git a/src/Terminal/StdIO/utils.tsx b/src/Terminal/StdIO/utils.tsx
new file mode 100644
index 000000000..1ccd18eb7
--- /dev/null
+++ b/src/Terminal/StdIO/utils.tsx
@@ -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(" ", "\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);
+}
diff --git a/src/Terminal/Terminal.ts b/src/Terminal/Terminal.ts
index 91a5765e7..121b4903d 100644
--- a/src/Terminal/Terminal.ts
+++ b/src/Terminal/Terminal.ts
@@ -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 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 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 {
+ async runContract(contractPath: ContractFilePath, stdIO: StdIO): Promise {
// 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 {
// 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 {
diff --git a/src/Terminal/commands/alias.ts b/src/Terminal/commands/alias.ts
index 74ba60861..40cf0628e 100644
--- a/src/Terminal/commands/alias.ts
+++ b/src/Terminal/commands/alias.ts
@@ -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);
}
diff --git a/src/Terminal/commands/analyze.ts b/src/Terminal/commands/analyze.ts
index 674b1db76..efdae4d2a 100644
--- a/src/Terminal/commands/analyze.ts
+++ b/src/Terminal/commands/analyze.ts
@@ -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);
}
diff --git a/src/Terminal/commands/backdoor.ts b/src/Terminal/commands/backdoor.ts
index 5edf59200..83af232d2 100644
--- a/src/Terminal/commands/backdoor.ts
+++ b/src/Terminal/commands/backdoor.ts
@@ -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);
}
diff --git a/src/Terminal/commands/buy.ts b/src/Terminal/commands/buy.ts
index d8682cb0f..d6bb35b7a 100644
--- a/src/Terminal/commands/buy.ts
+++ b/src/Terminal/commands/buy.ts
@@ -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);
}
diff --git a/src/Terminal/commands/cat.ts b/src/Terminal/commands/cat.ts
index ce4c53ca6..657560bc5 100644
--- a/src/Terminal/commands/cat.ts
+++ b/src/Terminal/commands/cat.ts
@@ -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;
}
diff --git a/src/Terminal/commands/cd.ts b/src/Terminal/commands/cd.ts
index 3dd182452..466f9fb00 100644
--- a/src/Terminal/commands/cd.ts
+++ b/src/Terminal/commands/cd.ts
@@ -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);
}
diff --git a/src/Terminal/commands/check.ts b/src/Terminal/commands/check.ts
index e79858249..75d7df4be 100644
--- a/src/Terminal/commands/check.ts
+++ b/src/Terminal/commands/check.ts
@@ -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);
}
}
}
diff --git a/src/Terminal/commands/connect.ts b/src/Terminal/commands/connect.ts
index e561171bc..bb576620c 100644
--- a/src/Terminal/commands/connect.ts
+++ b/src/Terminal/commands/connect.ts
@@ -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,
);
}
diff --git a/src/Terminal/commands/cp.ts b/src/Terminal/commands/cp.ts
index ade51e545..7394e182d 100644
--- a/src/Terminal/commands/cp.ts
+++ b/src/Terminal/commands/cp.ts
@@ -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);
}
diff --git a/src/Terminal/commands/echo.ts b/src/Terminal/commands/echo.ts
new file mode 100644
index 000000000..cdbfc4df2
--- /dev/null
+++ b/src/Terminal/commands/echo.ts
@@ -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();
+}
diff --git a/src/Terminal/commands/expr.ts b/src/Terminal/commands/expr.ts
index f38bb5115..24f2dea42 100644
--- a/src/Terminal/commands/expr.ts
+++ b/src/Terminal/commands/expr.ts
@@ -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);
}
diff --git a/src/Terminal/commands/free.ts b/src/Terminal/commands/free.ts
index 968f1da85..982dddbc8 100644
--- a/src/Terminal/commands/free.ts
+++ b/src/Terminal/commands/free.ts
@@ -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);
}
diff --git a/src/Terminal/commands/grep.ts b/src/Terminal/commands/grep.ts
index 9c4598ee1..ad7825201 100644
--- a/src/Terminal/commands/grep.ts
+++ b/src/Terminal/commands/grep.ts
@@ -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;
+};
diff --git a/src/Terminal/commands/grow.ts b/src/Terminal/commands/grow.ts
index 280457315..0d1eedd1a 100644
--- a/src/Terminal/commands/grow.ts
+++ b/src/Terminal/commands/grow.ts
@@ -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);
}
diff --git a/src/Terminal/commands/hack.ts b/src/Terminal/commands/hack.ts
index 46df900a5..5dd870b13 100644
--- a/src/Terminal/commands/hack.ts
+++ b/src/Terminal/commands/hack.ts
@@ -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);
}
diff --git a/src/Terminal/commands/help.ts b/src/Terminal/commands/help.ts
index 6bdf2a0d5..992e2c583 100644
--- a/src/Terminal/commands/help.ts
+++ b/src/Terminal/commands/help.ts
@@ -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));
}
}
diff --git a/src/Terminal/commands/history.ts b/src/Terminal/commands/history.ts
index c7a9064fe..8a0827560 100644
--- a/src/Terminal/commands/history.ts
+++ b/src/Terminal/commands/history.ts
@@ -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);
}
}
diff --git a/src/Terminal/commands/hostname.ts b/src/Terminal/commands/hostname.ts
index 767763808..6374f5cf8 100644
--- a/src/Terminal/commands/hostname.ts
+++ b/src/Terminal/commands/hostname.ts
@@ -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);
}
diff --git a/src/Terminal/commands/ipaddr.ts b/src/Terminal/commands/ipaddr.ts
index abe9edb1d..d3a7b471a 100644
--- a/src/Terminal/commands/ipaddr.ts
+++ b/src/Terminal/commands/ipaddr.ts
@@ -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);
}
diff --git a/src/Terminal/commands/kill.ts b/src/Terminal/commands/kill.ts
index 6639f14f9..44bf52d82 100644
--- a/src/Terminal/commands/kill.ts
+++ b/src/Terminal/commands/kill.ts
@@ -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);
}
}
diff --git a/src/Terminal/commands/killall.ts b/src/Terminal/commands/killall.ts
index 1ce2a69d0..6a8a0cf28 100644
--- a/src/Terminal/commands/killall.ts
+++ b/src/Terminal/commands/killall.ts
@@ -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);
diff --git a/src/Terminal/commands/ls.tsx b/src/Terminal/commands/ls.tsx
index 8cfbada89..b5101c006 100644
--- a/src/Terminal/commands/ls.tsx
+++ b/src/Terminal/commands/ls.tsx
@@ -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}
,
+ 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({segmentElements});
+ Terminal.printRaw({segmentElements}, stdIO);
}
}
diff --git a/src/Terminal/commands/lscpu.ts b/src/Terminal/commands/lscpu.ts
index 096793249..17fc6c862 100644
--- a/src/Terminal/commands/lscpu.ts
+++ b/src/Terminal/commands/lscpu.ts
@@ -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);
}
diff --git a/src/Terminal/commands/mem.ts b/src/Terminal/commands/mem.ts
index df56e6768..43e63f2e2 100644
--- a/src/Terminal/commands/mem.ts
+++ b/src/Terminal/commands/mem.ts
@@ -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);
}
}
diff --git a/src/Terminal/commands/ps.ts b/src/Terminal/commands/ps.ts
index 260621918..c94cb80ec 100644
--- a/src/Terminal/commands/ps.ts
+++ b/src/Terminal/commands/ps.ts
@@ -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);
}
}
}
diff --git a/src/Terminal/commands/rm.ts b/src/Terminal/commands/rm.ts
index db4f59090..6c450dbd1 100644
--- a/src/Terminal/commands/rm.ts
+++ b/src/Terminal/commands/rm.ts
@@ -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);
}
}
};
diff --git a/src/Terminal/commands/run.ts b/src/Terminal/commands/run.ts
index f8c9169fe..73dd2e583 100644
--- a/src/Terminal/commands/run.ts
+++ b/src/Terminal/commands/run.ts
@@ -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);
}
diff --git a/src/Terminal/commands/runProgram.ts b/src/Terminal/commands/runProgram.ts
index 140e8d8fc..bcca7b9d4 100644
--- a/src/Terminal/commands/runProgram.ts
+++ b/src/Terminal/commands/runProgram.ts
@@ -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);
}
diff --git a/src/Terminal/commands/runScript.ts b/src/Terminal/commands/runScript.ts
index 7b6f37e14..55d805e2a 100644
--- a/src/Terminal/commands/runScript.ts
+++ b/src/Terminal/commands/runScript.ts
@@ -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;
}
diff --git a/src/Terminal/commands/scan.ts b/src/Terminal/commands/scan.ts
index 7a4b257b1..45477a4b2 100644
--- a/src/Terminal/commands/scan.ts
+++ b/src/Terminal/commands/scan.ts
@@ -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);
}
}
diff --git a/src/Terminal/commands/scananalyze.ts b/src/Terminal/commands/scananalyze.ts
index cb126eba9..375a8c7ad 100644
--- a/src/Terminal/commands/scananalyze.ts
+++ b/src/Terminal/commands/scananalyze.ts
@@ -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);
}
}
diff --git a/src/Terminal/commands/scp.ts b/src/Terminal/commands/scp.ts
index f8e75144c..23425411f 100644
--- a/src/Terminal/commands/scp.ts
+++ b/src/Terminal/commands/scp.ts
@@ -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);
}
}
diff --git a/src/Terminal/commands/sudov.ts b/src/Terminal/commands/sudov.ts
index 9fac032c9..6390d3b26 100644
--- a/src/Terminal/commands/sudov.ts
+++ b/src/Terminal/commands/sudov.ts
@@ -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);
}
}
diff --git a/src/Terminal/commands/tail.ts b/src/Terminal/commands/tail.ts
index 9c68a54ec..d708954dc 100644
--- a/src/Terminal/commands/tail.ts
+++ b/src/Terminal/commands/tail.ts
@@ -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 | null {
+ const result = new Map();
+ 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;
+}
diff --git a/src/Terminal/commands/top.ts b/src/Terminal/commands/top.ts
index 7c6a0263a..cc22906cc 100644
--- a/src/Terminal/commands/top.ts
+++ b/src/Terminal/commands/top.ts
@@ -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);
}
}
}
diff --git a/src/Terminal/commands/weaken.ts b/src/Terminal/commands/weaken.ts
index 4de82a491..0b0ad45ed 100644
--- a/src/Terminal/commands/weaken.ts
+++ b/src/Terminal/commands/weaken.ts
@@ -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);
}
diff --git a/src/Terminal/commands/wget.ts b/src/Terminal/commands/wget.ts
index a7482b654..5c02330cf 100644
--- a/src/Terminal/commands/wget.ts
+++ b/src/Terminal/commands/wget.ts
@@ -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);
});
}
diff --git a/src/Terminal/getTabCompletionPossibilities.ts b/src/Terminal/getTabCompletionPossibilities.ts
index c5dbcf065..279785299 100644
--- a/src/Terminal/getTabCompletionPossibilities.ts
+++ b/src/Terminal/getTabCompletionPossibilities.ts
@@ -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 {
+export async function getTabCompletionPossibilities(fullTerminalText: string, baseDir = root): Promise {
+ // 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;
}
diff --git a/src/Terminal/ui/TerminalInput.tsx b/src/Terminal/ui/TerminalInput.tsx
index d4db28522..d92436bdd 100644
--- a/src/Terminal/ui/TerminalInput.tsx
+++ b/src/Terminal/ui/TerminalInput.tsx
@@ -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;
}
diff --git a/src/ui/React/ANSIITypography.tsx b/src/ui/React/ANSIITypography.tsx
index 6955f365a..73419bfe3 100644
--- a/src/ui/React/ANSIITypography.tsx
+++ b/src/ui/React/ANSIITypography.tsx
@@ -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}\\[(?.*?)m", "ug");
+export const ANSI_ESCAPE = new RegExp("\u{001b}\\[(?.*?)m", "ug");
const useStyles = makeStyles()((theme: Theme) => ({
success: {
diff --git a/src/utils/APIBreaks/APIBreak.ts b/src/utils/APIBreaks/APIBreak.ts
index d152dbb6a..dfd64e349 100644
--- a/src/utils/APIBreaks/APIBreak.ts
+++ b/src/utils/APIBreaks/APIBreak.ts
@@ -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();
}
diff --git a/test/jest/Netscript/RunScript.test.ts b/test/jest/Netscript/RunScript.test.ts
index 715674d3c..c787afefc 100644
--- a/test/jest/Netscript/RunScript.test.ts
+++ b/test/jest/Netscript/RunScript.test.ts
@@ -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 () => {
diff --git a/test/jest/Save.test.ts b/test/jest/Save.test.ts
index 638f8e403..a98c68186 100644
--- a/test/jest/Save.test.ts
+++ b/test/jest/Save.test.ts
@@ -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": [
{
diff --git a/test/jest/Terminal/Pipes.test.ts b/test/jest/Terminal/Pipes.test.ts
new file mode 100644
index 000000000..591c25d27
--- /dev/null
+++ b/test/jest/Terminal/Pipes.test.ts
@@ -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");
+ });
+});
diff --git a/test/jest/Terminal/RedirectIO.test.ts b/test/jest/Terminal/RedirectIO.test.ts
new file mode 100644
index 000000000..cab52c56a
--- /dev/null
+++ b/test/jest/Terminal/RedirectIO.test.ts
@@ -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;
+}
diff --git a/test/jest/Terminal/cat.test.ts b/test/jest/Terminal/cat.test.ts
new file mode 100644
index 000000000..af3a55930
--- /dev/null
+++ b/test/jest/Terminal/cat.test.ts
@@ -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);
+ });
+});
diff --git a/test/jest/Terminal/grep.test.ts b/test/jest/Terminal/grep.test.ts
new file mode 100644
index 000000000..9c2774557
--- /dev/null
+++ b/test/jest/Terminal/grep.test.ts
@@ -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`,
+ );
+ });
+});
diff --git a/test/jest/__snapshots__/Save.test.ts.snap b/test/jest/__snapshots__/Save.test.ts.snap
index 6d0cd5375..29afc0a0d 100644
--- a/test/jest/__snapshots__/Save.test.ts.snap
+++ b/test/jest/__snapshots__/Save.test.ts.snap
@@ -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
|