Skip to content

Commit 8e031fe

Browse files
Fix remaining issues (#60)
* Update tests to Aff * Add stdin write test * Fix exit handler's string value * Update module import to include 'node:' * Fix path of stdin test * Add missing FFI * Drop `ipc` on sync fns (error); inline safeStdio * Fix `fromKillSignalImpl` FFI arg order * Update tests * Add changelog entry
1 parent 4d27b66 commit 8e031fe

File tree

7 files changed

+144
-75
lines changed

7 files changed

+144
-75
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ Bugfixes:
1212

1313
Other improvements:
1414

15+
## [v12.0.0](https://github.com/purescript-node/purescript-node-child-process/releases/tag/v12.0.0) - 2023-07-26
16+
17+
Breaking changes:
18+
- Removed `safeStdio` (#60 by @JordanMartinez)
19+
20+
Turns out this isn't safe for `*Sync` functions. AFAIK, this isn't documented
21+
in Node docs.
22+
23+
Bugfixes:
24+
- Fixed `exitH`'s String value for listener (#60 by @JordanMartinez)
25+
- Added missing FFI for `execSync'` (#60 by @JordanMartinez)
26+
- Fixed `fromKillSignal`'s FFI's arg order (#60 by @JordanMartinez)
27+
28+
Other improvements:
29+
- Update tests to actually throw if invalid state occurs (#60 by @JordanMartinez)
30+
1531
## [v11.0.0](https://github.com/purescript-node/purescript-node-child-process/releases/tag/v11.0.0) - 2023-07-25
1632

1733
Breaking changes:

src/Node/ChildProcess.purs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ module Node.ChildProcess
8686

8787
import Prelude
8888

89-
import Data.Maybe (Maybe(..), fromMaybe, maybe)
89+
import Data.Maybe (Maybe(..), fromMaybe)
9090
import Data.Nullable (Nullable, toMaybe, toNullable)
9191
import Data.Posix (Pid, Gid, Uid)
9292
import Data.Posix.Signal (Signal)
@@ -97,12 +97,11 @@ import Effect.Uncurried (EffectFn2)
9797
import Foreign (Foreign)
9898
import Foreign.Object (Object)
9999
import Node.Buffer (Buffer)
100-
import Node.ChildProcess.Types (Exit(..), Handle, KillSignal, Shell, StdIO, UnsafeChildProcess)
100+
import Node.ChildProcess.Types (Exit(..), Handle, KillSignal, Shell, StdIO, UnsafeChildProcess, ipc, pipe)
101101
import Node.Errors.SystemError (SystemError)
102102
import Node.EventEmitter (EventEmitter, EventHandle)
103103
import Node.EventEmitter.UtilTypes (EventHandle0, EventHandle1)
104104
import Node.Stream (Readable, Writable)
105-
import Node.UnsafeChildProcess.Safe (safeStdio)
106105
import Node.UnsafeChildProcess.Safe as SafeCP
107106
import Node.UnsafeChildProcess.Unsafe (unsafeSOBToBuffer)
108107
import Node.UnsafeChildProcess.Unsafe as UnsafeCP
@@ -288,7 +287,7 @@ spawnSync' command args buildOpts = (UnsafeCP.spawnSync' command args opts) <#>
288287
}
289288
where
290289
opts =
291-
{ stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio
290+
{ stdio: [ pipe, pipe, pipe ] <> fromMaybe [] o.appendStdio
292291
, encoding: "buffer"
293292
, cwd: fromMaybe undefined o.cwd
294293
, input: fromMaybe undefined o.input
@@ -328,7 +327,7 @@ spawn
328327
:: String
329328
-> Array String
330329
-> Effect ChildProcess
331-
spawn cmd args = coerce $ UnsafeCP.spawn' cmd args { stdio: safeStdio }
330+
spawn cmd args = coerce $ UnsafeCP.spawn cmd args
332331

333332
-- | - `cwd` <string> | <URL> Current working directory of the child process.
334333
-- | - `env` <Object> Environment key-value pairs. Default: process.env.
@@ -367,7 +366,7 @@ spawn'
367366
spawn' cmd args buildOpts = coerce $ UnsafeCP.spawn' cmd args opts
368367
where
369368
opts =
370-
{ stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio
369+
{ stdio: [ pipe, pipe, pipe, ipc ] <> fromMaybe [] o.appendStdio
371370
, cwd: fromMaybe undefined o.cwd
372371
, env: fromMaybe undefined o.env
373372
, argv0: fromMaybe undefined o.argv0
@@ -452,7 +451,7 @@ execSync' cmd buildOpts = do
452451
, windowsHide: Nothing
453452
}
454453
opts =
455-
{ stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio
454+
{ stdio: [ pipe, pipe, pipe ] <> fromMaybe [] o.appendStdio
456455
, encoding: "buffer"
457456
, cwd: fromMaybe undefined o.cwd
458457
, input: fromMaybe undefined o.input
@@ -550,7 +549,7 @@ execFileSync
550549
-> Array String
551550
-> Effect Buffer
552551
execFileSync file args =
553-
map unsafeSOBToBuffer $ UnsafeCP.execFileSync' file args { stdio: safeStdio, encoding: "buffer" }
552+
map unsafeSOBToBuffer $ UnsafeCP.execFileSync' file args { encoding: "buffer" }
554553

555554
-- | - `cwd` <string> | <URL> Current working directory of the child process.
556555
-- | - `input` <string> | <Buffer> | <TypedArray> | <DataView> The value which will be passed as stdin to the spawned process. Supplying this value will override stdio[0].
@@ -585,7 +584,7 @@ execFileSync' file args buildOpts =
585584
map unsafeSOBToBuffer $ UnsafeCP.execFileSync' file args opts
586585
where
587586
opts =
588-
{ stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio
587+
{ stdio: [ pipe, pipe, pipe ] <> fromMaybe [] o.appendStdio
589588
, encoding: "buffer"
590589
, cwd: fromMaybe undefined o.cwd
591590
, input: fromMaybe undefined o.input
@@ -685,7 +684,7 @@ fork
685684
:: String
686685
-> Array String
687686
-> Effect ChildProcess
688-
fork modulePath args = coerce $ UnsafeCP.fork' modulePath args { stdio: safeStdio }
687+
fork modulePath args = coerce $ UnsafeCP.fork' modulePath args { stdio: [ pipe, pipe, pipe, ipc ] }
689688

690689
-- | - `cwd` <string> | <URL> Current working directory of the child process.
691690
-- | - `detached` <boolean> Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached).
@@ -724,7 +723,7 @@ fork'
724723
fork' modulePath args buildOpts = coerce $ UnsafeCP.fork' modulePath args opts
725724
where
726725
opts =
727-
{ stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio
726+
{ stdio: [ pipe, pipe, pipe, ipc ] <> fromMaybe [] o.appendStdio
728727
, cwd: fromMaybe undefined o.cwd
729728
, detached: fromMaybe undefined o.detached
730729
, env: fromMaybe undefined o.env

src/Node/ChildProcess/Types.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
export const showKillSignal = (ks) => ks + "";
22
export const showShell = (shell) => shell + "";
3-
export const fromKillSignalImpl = (left, right, sig) => {
3+
export const fromKillSignalImpl = (fromInt, fromStr, sig) => {
44
const ty = typeof sig;
5-
if (ty === "number") return right(sig | 0);
6-
if (ty === "string") return left(sig);
5+
if (ty === "number") return fromInt(sig | 0);
6+
if (ty === "string") return fromStr(sig);
77
throw new Error("Impossible. Got kill signal that was neither int nor string: " + sig);
88
};

src/Node/UnsafeChildProcess/Safe.purs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ module Node.UnsafeChildProcess.Safe
2424
, spawnFile
2525
, spawnArgs
2626
, stdio
27-
, safeStdio
2827
) where
2928

3029
import Prelude
@@ -37,7 +36,7 @@ import Data.Posix.Signal as Signal
3736
import Effect (Effect)
3837
import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2, runEffectFn1, runEffectFn2)
3938
import Foreign (Foreign)
40-
import Node.ChildProcess.Types (Exit(..), Handle, KillSignal, StdIO, UnsafeChildProcess, intSignal, ipc, pipe, stringSignal)
39+
import Node.ChildProcess.Types (Exit(..), Handle, KillSignal, StdIO, UnsafeChildProcess, intSignal, stringSignal)
4140
import Node.Errors.SystemError (SystemError)
4241
import Node.EventEmitter (EventEmitter, EventHandle(..))
4342
import Node.EventEmitter.UtilTypes (EventHandle0, EventHandle1)
@@ -61,7 +60,7 @@ errorH :: EventHandle1 UnsafeChildProcess SystemError
6160
errorH = EventHandle "error" mkEffectFn1
6261

6362
exitH :: EventHandle UnsafeChildProcess (Exit -> Effect Unit) (EffectFn2 (Nullable Int) (Nullable KillSignal) Unit)
64-
exitH = EventHandle "exitH" \cb -> mkEffectFn2 \code signal ->
63+
exitH = EventHandle "exit" \cb -> mkEffectFn2 \code signal ->
6564
case toMaybe code, toMaybe signal of
6665
Just c, _ -> cb $ Normally c
6766
_, Just s -> cb $ BySignal s
@@ -149,10 +148,3 @@ foreign import spawnArgs :: UnsafeChildProcess -> Array String
149148
foreign import spawnFile :: UnsafeChildProcess -> String
150149

151150
foreign import stdio :: UnsafeChildProcess -> Array StdIO
152-
153-
-- | Safe default configuration for an UnsafeChildProcess.
154-
-- | `[ pipe, pipe, pipe, ipc ]`.
155-
-- | Creates a new stream for `stdin`, `stdout`, and `stderr`
156-
-- | Also adds an IPC channel, even if it's not used.
157-
safeStdio :: Array StdIO
158-
safeStdio = [ pipe, pipe, pipe, ipc ]

src/Node/UnsafeChildProcess/Unsafe.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ export {
1010
spawn as spawnImpl,
1111
spawn as spawnOptsImpl,
1212
execSync as execSyncImpl,
13+
execSync as execSyncOptsImpl,
1314
execFileSync as execFileSyncImpl,
1415
execFileSync as execFileSyncOptsImpl,
1516
spawnSync as spawnSyncImpl,
1617
spawnSync as spawnSyncOptsImpl,
1718
fork as forkImpl,
1819
fork as forkOptsImpl,
19-
} from "child_process";
20+
} from "node:child_process";
2021

2122
export const unsafeStdin = (cp) => cp.stdin;
2223
export const unsafeStdout = (cp) => cp.stdout;

test/Main.purs

Lines changed: 105 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,76 +2,131 @@ module Test.Main where
22

33
import Prelude
44

5-
import Data.Either (hush)
5+
import Data.Either (Either(..), hush)
66
import Data.Maybe (Maybe(..))
77
import Data.Posix.Signal (Signal(..))
88
import Data.Posix.Signal as Signal
99
import Effect (Effect)
10-
import Effect.Console (log)
10+
import Effect.Aff (Aff, effectCanceler, launchAff_, makeAff, nonCanceler)
11+
import Effect.Class (liftEffect)
12+
import Effect.Class.Console (log)
13+
import Effect.Exception (throw, throwException)
1114
import Node.Buffer as Buffer
12-
import Node.ChildProcess (errorH, exec', execSync', exitH, kill, spawn, stdout)
15+
import Node.ChildProcess (exec', execSync', kill, spawn, stdin)
16+
import Node.ChildProcess as CP
17+
import Node.ChildProcess.Aff (waitSpawned)
1318
import Node.ChildProcess.Types (Exit(..), fromKillSignal)
1419
import Node.Encoding (Encoding(..))
1520
import Node.Encoding as NE
16-
import Node.Errors.SystemError (code)
17-
import Node.EventEmitter (on_)
18-
import Node.Stream (dataH)
21+
import Node.EventEmitter (EventHandle, once, once_)
22+
import Node.Stream as Stream
23+
import Unsafe.Coerce (unsafeCoerce)
1924

2025
main :: Effect Unit
21-
main = do
22-
log "spawns processes ok"
26+
main = launchAff_ do
27+
writingToStdinWorks
2328
spawnLs
29+
nonExistentExecutable
30+
noEffectsTooEarly
31+
killsProcess
32+
execLs
33+
execSyncEcho "some value"
2434

25-
log "emits an error if executable does not exist"
26-
nonExistentExecutable $ do
27-
log "nonexistent executable: all good."
35+
until
36+
:: forall emitter psCb jsCb a
37+
. emitter
38+
-> EventHandle emitter psCb jsCb
39+
-> ((a -> Effect Unit) -> psCb)
40+
-> Aff a
41+
until ee event cb = makeAff \done -> do
42+
rm <- ee # once event (cb (done <<< Right))
43+
pure $ effectCanceler rm
2844

29-
log "doesn't perform effects too early"
30-
spawn "ls" [ "-la" ] >>= \ls -> do
31-
let _ = kill ls
32-
ls # on_ exitH \exit ->
33-
case exit of
34-
Normally 0 ->
35-
log "All good!"
36-
_ -> do
37-
log ("Bad exit: expected `Normally 0`, got: " <> show exit)
45+
writingToStdinWorks :: Aff Unit
46+
writingToStdinWorks = do
47+
log "\nwriting to stdin works"
48+
sp <- liftEffect $ spawn "sh" [ "./test/sleep.sh" ]
49+
liftEffect do
50+
(stdin sp) # once_ Stream.errorH \err -> do
51+
log "Error in stdin"
52+
throwException $ unsafeCoerce err
53+
buf <- Buffer.fromString "helllo" UTF8
54+
void $ Stream.write (stdin sp) buf
55+
sp # once_ CP.errorH \err -> do
56+
log "Error in child process"
57+
throwException $ unsafeCoerce err
58+
exit <- until sp CP.closeH \completeAff -> \exit ->
59+
completeAff exit
60+
log $ "spawn sleep done " <> show exit
3861

39-
log "kills processes"
40-
spawn "ls" [ "-la" ] >>= \ls -> do
41-
_ <- kill ls
42-
ls # on_ exitH \exit ->
43-
case exit of
44-
BySignal s | Just SIGTERM <- Signal.fromString =<< (hush $ fromKillSignal s) ->
45-
log "All good!"
46-
_ -> do
47-
log ("Bad exit: expected `BySignal SIGTERM`, got: " <> show exit)
62+
spawnLs :: Aff Unit
63+
spawnLs = do
64+
log "\nspawns processes ok"
65+
ls <- liftEffect $ spawn "ls" [ "-la" ]
66+
res <- waitSpawned ls
67+
case res of
68+
Right pid -> log $ "ls successfully spawned with PID: " <> show pid
69+
Left err -> liftEffect $ throwException $ unsafeCoerce err
70+
exit <- until ls CP.closeH \complete -> \exit -> complete exit
71+
case exit of
72+
Normally 0 -> log $ "ls exited with 0"
73+
Normally i -> liftEffect $ throw $ "ls had non-zero exit: " <> show i
74+
BySignal sig -> liftEffect $ throw $ "ls exited with sig: " <> show sig
4875

49-
log "exec"
50-
execLs
76+
nonExistentExecutable :: Aff Unit
77+
nonExistentExecutable = do
78+
log "\nemits an error if executable does not exist"
79+
ch <- liftEffect $ spawn "this-does-not-exist" []
80+
res <- waitSpawned ch
81+
case res of
82+
Left _ -> log "nonexistent executable: all good."
83+
Right pid -> liftEffect $ throw $ "nonexistent executable started with PID: " <> show pid
5184

52-
spawnLs :: Effect Unit
53-
spawnLs = do
54-
ls <- spawn "ls" [ "-la" ]
55-
ls # on_ exitH \exit ->
56-
log $ "ls exited: " <> show exit
57-
(stdout ls) # on_ dataH (Buffer.toString UTF8 >=> log)
85+
noEffectsTooEarly :: Aff Unit
86+
noEffectsTooEarly = do
87+
log "\ndoesn't perform effects too early"
88+
ls <- liftEffect $ spawn "ls" [ "-la" ]
89+
let _ = kill ls
90+
exit <- until ls CP.exitH \complete -> \exit -> complete exit
91+
case exit of
92+
Normally 0 ->
93+
log "All good!"
94+
_ ->
95+
liftEffect $ throw $ "Bad exit: expected `Normally 0`, got: " <> show exit
5896

59-
nonExistentExecutable :: Effect Unit -> Effect Unit
60-
nonExistentExecutable done = do
61-
ch <- spawn "this-does-not-exist" []
62-
ch # on_ errorH \err ->
63-
log (code err) *> done
97+
killsProcess :: Aff Unit
98+
killsProcess = do
99+
log "\nkills processes"
100+
ls <- liftEffect $ spawn "ls" [ "-la" ]
101+
_ <- liftEffect $ kill ls
102+
exit <- until ls CP.exitH \complete -> \exit -> complete exit
103+
case exit of
104+
BySignal s | Just SIGTERM <- Signal.fromString =<< (hush $ fromKillSignal s) ->
105+
log "All good!"
106+
_ -> do
107+
liftEffect $ throw $ "Bad exit: expected `BySignal SIGTERM`, got: " <> show exit
64108

65-
execLs :: Effect Unit
109+
execLs :: Aff Unit
66110
execLs = do
67-
-- returned ChildProcess is ignored here
68-
_ <- exec' "ls >&2" identity \r ->
69-
log "redirected to stderr:" *> (Buffer.toString UTF8 r.stderr >>= log)
70-
pure unit
111+
log "\nexec"
112+
r <- makeAff \done -> do
113+
-- returned ChildProcess is ignored here
114+
void $ exec' "ls >&2" identity (done <<< Right)
115+
pure nonCanceler
116+
stdout' <- liftEffect $ Buffer.toString UTF8 r.stdout
117+
stderr' <- liftEffect $ Buffer.toString UTF8 r.stderr
118+
when (stdout' /= "") do
119+
liftEffect $ throw $ "stdout should be redirected to stderr but had content: " <> show stdout'
120+
when (stderr' == "") do
121+
liftEffect $ throw $ "stderr should have content but was empty"
122+
log "stdout was successfully redirected to stderr"
71123

72-
execSyncEcho :: String -> Effect Unit
73-
execSyncEcho str = do
124+
execSyncEcho :: String -> Aff Unit
125+
execSyncEcho str = liftEffect do
126+
log "\nexecSyncEcho"
74127
buf <- Buffer.fromString str UTF8
75128
resBuf <- execSync' "cat" (_ { input = Just buf })
76129
res <- Buffer.toString NE.UTF8 resBuf
77-
log res
130+
when (str /= res) do
131+
throw $ "cat did not output its input. \nGot: " <> show res <> "\nExpected: " <> show str
132+
log "cat successfully re-outputted its input"

test/sleep.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env sh
2+
3+
sleep 2
4+
echo "$1"
5+
6+
echo "Done"

0 commit comments

Comments
 (0)