Skip to content

Allow complete control over stdio #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ in upstream
, "prelude"
, "unsafe-coerce"
]
with node-child-process.version = "v11.0.0"
with node-child-process.version = "v11.1.0"
with node-child-process.dependencies =
[ "exceptions"
, "node-event-emitter"
Expand Down
1 change: 1 addition & 0 deletions spago.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
, "either"
, "exceptions"
, "foldable-traversable"
, "foreign"
, "foreign-object"
, "functions"
, "integers"
Expand Down
2 changes: 2 additions & 0 deletions src/Node/Library/Execa.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export function setTimeoutImpl(timeout, cb) {
return t.unref ? t : { unref: () => {} };
}

const undefinedVal = undefined;
export { undefinedVal as undefined };
397 changes: 309 additions & 88 deletions src/Node/Library/Execa.purs

Large diffs are not rendered by default.

26 changes: 16 additions & 10 deletions test/Test/Node/Library/Execa.purs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@ import Prelude

import Data.Maybe (Maybe(..))
import Data.Time.Duration (Milliseconds(..))
import Data.Traversable (for_)
import Effect.Class (liftEffect)
import Effect.Exception as Exception
import Node.Buffer as Buffer
import Node.ChildProcess as CP
import Node.ChildProcess.Types (Exit(..), fromKillSignal', intSignal, stringSignal)
import Node.Encoding (Encoding(..))
import Node.Library.Execa (execa, execaCommand, execaCommandSync, execaSync)
import Node.Library.HumanSignals (signals)
import Node.Path as Path
import Node.UnsafeChildProcess.Safe as SafeCP
import Test.Node.Library.Utils (isWindows, itNix)
import Test.Spec (Spec, describe, it)
import Test.Spec.Assertions (fail, shouldEqual)
import Test.Spec.Assertions as Assertions
import Test.Spec.Assertions.String (shouldContain)

spec :: Spec Unit
spec = describe "execa" do
itNix "`echo test` should fail due to a Node.js bug" do
spawned <- execa "echo" [] identity
spawned.stdin.writeUtf8End "test"
for_ spawned.stdin \s -> s.writeUtf8End "test"
result <- spawned.getResult
case result.stdinError of
Nothing -> fail "Expected EPIPE error"
Expand All @@ -36,7 +38,7 @@ spec = describe "execa" do
_ -> fail result.message
itNix "input is buffer" do
spawned <- execa "cat" [ "-" ] identity
spawned.stdin.writeUtf8End "test"
for_ spawned.stdin \s -> s.writeUtf8End "test"
result <- spawned.getResult
case result.exit of
Normally 0 -> result.stdout `shouldEqual` "test"
Expand All @@ -49,7 +51,7 @@ spec = describe "execa" do
describe "using sleep files" do
let
shellCmd = if isWindows then "pwsh" else "sh"
sleepFile = Path.concat [ "test", "fixtures", "sleep." <> if isWindows then "cmd" else "sh" ]
sleepFile = Path.concat [ "test", "fixtures", "sleep." <> if isWindows then "ps1" else "sh" ]
describe "kill works" do
it "basic cancel produces error" do
spawned <- execa shellCmd [ sleepFile, "1" ] identity
Expand Down Expand Up @@ -80,12 +82,16 @@ spec = describe "execa" do
result <- spawned.getResult
case result.exit of
Normally 64 | isWindows -> do
sig <- liftEffect $ CP.signalCode spawned.childProcess
sig `shouldEqual` (Just "SIGTERM")
result.timedOut `shouldEqual` true
sig <- liftEffect $ SafeCP.signalCode spawned.childProcess
when (sig /= (Just "SIGTERM")) do
Assertions.fail $ "Didn't get expected kill signal. Result was\n" <> show result
unless (result.timedOut) do
Assertions.fail $ "Result didn't indicate time out. Result was\n" <> show result
BySignal sig -> do
sig `shouldEqual` (stringSignal "SIGTERM")
result.timedOut `shouldEqual` true
when (sig /= (stringSignal "SIGTERM")) do
Assertions.fail $ "Didn't get expected kill signal. Result was\n" <> show result
unless (result.timedOut) do
Assertions.fail $ "Result didn't indicate time out. Result was\n" <> show result
_ ->
fail $ "Timeout should work: " <> show result
describe "execaSync" do
Expand All @@ -111,7 +117,7 @@ spec = describe "execa" do
_ -> fail result.message
itNix "input is buffer" do
spawned <- execaCommand "cat -" identity
spawned.stdin.writeUtf8End "test"
for_ spawned.stdin \s -> s.writeUtf8End "test"
result <- spawned.getResult
case result.exit of
Normally 0 -> result.stdout `shouldEqual` "test"
Expand Down
2 changes: 0 additions & 2 deletions test/fixtures/outErr.cmd

This file was deleted.

4 changes: 0 additions & 4 deletions test/fixtures/outErr.sh

This file was deleted.

File renamed without changes.
11 changes: 11 additions & 0 deletions test/ipc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# ipc

This folder contains a small script for seeing when `ipc` stdio value can be used.

Conclusions:

| Function | Result |
| - | - |
| `spawn` | works |
| `spawnSync` | runtime error |
| `fork` | works |
28 changes: 28 additions & 0 deletions test/ipc/child.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import process from "node:process";

console.log("Script started");

const tc = (msg, f) => {
try {
f();
console.log(`${msg}: Success`);
} catch (e) {
console.log(`${msg}: Failure - ${e}`);
}
};

process.on("message", (msg) => {
console.log(`Child got parent message: ${msg}`);
});

tc("Child -> Parent Message", () => {
process.send("Hello from child");
});

setTimeout(() => {
tc("Child disconnect from parent", () => {
process.disconnect();
});

console.log("Script finished");
}, 100);
26 changes: 26 additions & 0 deletions test/ipc/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@


Spawn - IPC usage: Success
Parent receives message: Success
Parent -> Child: Success
Script started
Child -> Parent Message: Success
Parent received message: Hello from child
Child got parent message: Hello from Parent
Child disconnect from parent: Success
Script finished


SpawnSync - IPC usage: Failure - Error [ERR_IPC_SYNC_FORK]: IPC cannot be used with synchronous forks


Fork - IPC usage: Success
Parent receives message: Success
Parent -> Child: Success
Script started
Child -> Parent Message: Success
Parent received message: Hello from child
Child got parent message: Hello from Parent
Child disconnect from parent: Success
Script finished
Finished
52 changes: 52 additions & 0 deletions test/ipc/parent.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import child_process from "node:child_process";

const stdio = [ "ignore", "inherit", "inherit", "ipc"];

const tc = (msg, f) => {
try {
f();
console.log(`${msg}: Success`);
} catch (e) {
console.log(`${msg}: Failure - ${e}`);
}
};

const go = (idx) => {
var child;
if (idx === 1) {
tc("\n\nSpawn - IPC usage", () => {
child = child_process.spawn("node", ["child.mjs"], { stdio });
});
} else if (idx === 2) {
tc("\n\nSpawnSync - IPC usage", () => {
child = child_process.spawnSync("node", ["child.mjs"], { stdio });
});
} else if (idx === 3) {
tc("\n\nFork - IPC usage", () => {
child = child_process.fork("child.mjs", { stdio });
});
}
if (!child) {
if (idx !== 4) {
go(idx+1);
} else {
console.log("Finished");
}
} else {
tc("Parent receives message", () => {
child.on("message", (msg) => {
console.log(`Parent received message: ${msg}`);
});
});
child.on("spawn", () => {
tc("Parent -> Child", () => {
child.send("Hello from Parent");
});
});
child.on("exit", () => {
go(idx + 1);
});
}
};

go(1);
17 changes: 17 additions & 0 deletions test/stdio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# stdio

This folder contains a small script for seeing what is needed to terminate a child process with the different `stdio` options. It also helps verify when a `stream` will have a value and when it will be `null`.

Conclusions:

| stdio | Stream | Read | Write | Program terminates when |
| - | - | - | - | - |
| `inherit` | `stdin` | Error | Error | `Ctrl+D` pressed |
| `ignore` | `stdin` | Error | Error | Event Loop finishes |
| `pipe` | `stdin` | Error | Success | `stdin.end()` called |
| `inherit` | `stdout` | Error | Error | Event Loop finishes |
| `ignore` | `stdout` | Error | Error | Event Loop finishes |
| `pipe` | `stdout` | Success | Error | Event Loop finishes |
| `inherit` | `stderr` | Error | Error | Event Loop finishes |
| `ignore` | `stderr` | Error | Error | Event Loop finishes |
| `pipe` | `stderr` | Success | Error | Event Loop finishes |
22 changes: 22 additions & 0 deletions test/stdio/child.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import process from "node:process";

console.log("Script started");
process.stdin.once("data", (str) => {
console.log(`Got data from stdin: ${str}`);
});

const ms = 100;

setTimeout(() => {
console.log("log 1");
setTimeout(() => {
console.error("error 1");
setTimeout(() => {
console.log("log 2");
setTimeout(() => {
console.error("error 2");
console.log("Script terminated");
}, ms);
}, ms);
}, ms);
}, ms);
Loading