Skip to content

Commit cc7c581

Browse files
committed
Make SpawnOptions a bit safer
- Add a sum type for the `stdio` option - Use Maybe instead of `undefined`.
1 parent 7e9400a commit cc7c581

File tree

4 files changed

+124
-29
lines changed

4 files changed

+124
-29
lines changed

bower.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"purescript-node-streams": "~0.2.0",
99
"purescript-foreign": "~0.7.2",
1010
"purescript-maps": "~0.5.3",
11-
"purescript-nullable": "~0.2.1"
11+
"purescript-nullable": "~0.2.1",
12+
"purescript-unsafe-coerce": "~0.1.0",
13+
"purescript-node-fs": "~0.9.2"
1214
},
1315
"devDependencies": {
1416
"purescript-console": "~0.1.1"

docs/Node/ChildProcess.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ process won't necessarily kill it.
8989
#### `SpawnOptions`
9090

9191
``` purescript
92-
type SpawnOptions = { cwd :: String, stdio :: Array String, env :: Nullable (StrMap String), detached :: Boolean, uid :: Int, gid :: Int }
92+
type SpawnOptions = { cwd :: Maybe String, stdio :: Array (Maybe StdIOBehaviour), env :: Maybe (StrMap String), detached :: Boolean, uid :: Maybe Int, gid :: Maybe Int }
9393
```
9494

9595
#### `onExit`
@@ -140,4 +140,25 @@ defaultSpawnOptions :: SpawnOptions
140140
type ChildProcessError = { code :: String, errno :: String, syscall :: String }
141141
```
142142

143+
An error which occurred inside a child process.
144+
145+
#### `StdIOBehaviour`
146+
147+
``` purescript
148+
data StdIOBehaviour
149+
```
150+
151+
Behaviour for standard IO streams (eg, standard input, standard output) of
152+
a child process.
153+
154+
* `Pipe`: creates a pipe between the child and parent process, which can
155+
then be accessed as a `Stream` via the `stdin`, `stdout`, or `stderr`
156+
functions.
157+
* `Ignore`: ignore this stream. This will cause Node to open /dev/null and
158+
connect it to the stream.
159+
* `ShareStream`: Connect the supplied stream to the corresponding file
160+
descriptor in the child.
161+
* `ShareFD`: Connect the supplied file descriptor (which should be open
162+
in the parent) to the corresponding file descriptor in the child.
163+
143164

src/Node/ChildProcess.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// module Node.ChildProcess
44
/* eslint-env node*/
55

6-
exports.spawn = function spawn(command) {
6+
exports.spawnImpl = function spawnImpl(command) {
77
return function(args) {
88
return function(opts) {
99
return function() {
@@ -70,4 +70,5 @@ exports.onError = function onError(cp){
7070
};
7171
};
7272

73+
exports["undefined"] = undefined;
7374
exports.process = process;

src/Node/ChildProcess.purs

Lines changed: 97 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module Node.ChildProcess
1818
, onError
1919
, spawn
2020
, SpawnOptions()
21+
, StdIOBehaviour()
2122
, defaultSpawnOptions
2223
) where
2324

@@ -28,11 +29,13 @@ import Control.Monad.Eff (Eff())
2829
import Data.StrMap (StrMap())
2930
import Data.Function (Fn2(), runFn2)
3031
import Data.Nullable (Nullable(), toNullable)
31-
import Data.Maybe (Maybe(..))
32+
import Data.Maybe (Maybe(..), fromMaybe)
3233
import Data.Foreign (Foreign())
34+
import Unsafe.Coerce (unsafeCoerce)
3335

36+
import Node.FS as FS
3437
import Node.Buffer (Buffer())
35-
import Node.Stream (Readable(), Writable())
38+
import Node.Stream (Readable(), Writable(), Stream())
3639
import Node.ChildProcess.Signal (Signal(..))
3740

3841
-- | A handle for inter-process communication (IPC).
@@ -95,53 +98,121 @@ kill :: forall eff. Signal -> ChildProcess -> Eff (cp :: CHILD_PROCESS | eff) Bo
9598
kill sig (ChildProcess cp) = pure (cp.kill sig)
9699

97100
type SpawnOptions =
98-
{ cwd :: String
99-
, stdio :: Array String
100-
, env :: Nullable (StrMap String)
101+
{ cwd :: Maybe String
102+
, stdio :: Array (Maybe StdIOBehaviour)
103+
, env :: Maybe (StrMap String)
101104
, detached :: Boolean
102-
, uid :: Int
103-
, gid :: Int
105+
, uid :: Maybe Int
106+
, gid :: Maybe Int
104107
}
105108

106109
onExit :: forall eff. ChildProcess -> (Maybe Int -> Maybe Signal -> Eff eff Unit) -> Eff eff Unit
107110
onExit = mkOnExit Nothing Just Signal
108111

112+
foreign import mkOnExit :: forall a eff.
113+
Maybe a -> (a -> Maybe a) -> (String -> Signal) ->
114+
ChildProcess -> (Maybe Int -> Maybe Signal -> Eff eff Unit) -> Eff eff Unit
115+
109116
onClose :: forall eff. ChildProcess -> (Maybe Int -> Maybe Signal -> Eff eff Unit) -> Eff eff Unit
110117
onClose = mkOnClose Nothing Just Signal
111118

119+
foreign import mkOnClose :: forall a eff.
120+
Maybe a -> (a -> Maybe a) -> (String -> Signal) ->
121+
ChildProcess -> (Maybe Int -> Maybe Signal -> Eff eff Unit) -> Eff eff Unit
122+
112123
onMessage :: forall eff. ChildProcess -> (Foreign -> Maybe Handle -> Eff eff Unit) -> Eff eff Unit
113124
onMessage = mkOnMessage Nothing Just
114125

126+
foreign import mkOnMessage :: forall a eff.
127+
Maybe a -> (a -> Maybe a) ->
128+
ChildProcess -> (Foreign -> Maybe Handle -> Eff eff Unit) -> Eff eff Unit
129+
115130
foreign import onDisconnect :: forall eff. ChildProcess -> Eff eff Unit -> Eff eff Unit
116131
foreign import onError :: forall eff. ChildProcess -> (ChildProcessError -> Eff eff Unit) -> Eff eff Unit
117132

118-
foreign import spawn :: forall eff. String -> Array String -> SpawnOptions -> Eff (cp :: CHILD_PROCESS | eff) ChildProcess
133+
spawn :: forall eff. String -> Array String -> SpawnOptions -> Eff (cp :: CHILD_PROCESS | eff) ChildProcess
134+
spawn cmd args opts = spawnImpl cmd args (convertOpts opts)
135+
where
136+
convertOpts opts =
137+
{ cwd: fromMaybe undefined opts.cwd
138+
, stdio: toActualStdIOOptions opts.stdio
139+
, env: toNullable opts.env
140+
, detached: opts.detached
141+
, uid: fromMaybe undefined opts.uid
142+
, gid: fromMaybe undefined opts.gid
143+
}
144+
145+
foreign import spawnImpl :: forall opts eff. String -> Array String -> { | opts } -> Eff (cp :: CHILD_PROCESS | eff) ChildProcess
146+
147+
-- There's gotta be a better way.
148+
foreign import undefined :: forall a. a
119149

120150
defaultSpawnOptions :: SpawnOptions
121151
defaultSpawnOptions =
122-
{ cwd: undefined
123-
, stdio: ["pipe", "pipe", "pipe"]
124-
, env: toNullable Nothing
152+
{ cwd: Nothing
153+
, stdio: pipe
154+
, env: Nothing
125155
, detached: false
126-
, uid: undefined
127-
, gid: undefined
156+
, uid: Nothing
157+
, gid: Nothing
128158
}
129159

130-
type ChildProcessError =
160+
-- | An error which occurred inside a child process.
161+
type ChildProcessError =
131162
{ code :: String
132163
, errno :: String
133164
, syscall :: String
134165
}
135166

136-
foreign import mkOnExit :: forall a eff.
137-
Maybe a -> (a -> Maybe a) -> (String -> Signal) ->
138-
ChildProcess -> (Maybe Int -> Maybe Signal -> Eff eff Unit) -> Eff eff Unit
139-
foreign import mkOnMessage :: forall a eff.
140-
Maybe a -> (a -> Maybe a) ->
141-
ChildProcess -> (Foreign -> Maybe Handle -> Eff eff Unit) -> Eff eff Unit
142-
foreign import mkOnClose :: forall a eff.
143-
Maybe a -> (a -> Maybe a) -> (String -> Signal) ->
144-
ChildProcess -> (Maybe Int -> Maybe Signal -> Eff eff Unit) -> Eff eff Unit
145-
146-
-- There's gotta be a better way.
147-
foreign import undefined :: forall a. a
167+
-- | Behaviour for standard IO streams (eg, standard input, standard output) of
168+
-- | a child process.
169+
-- |
170+
-- | * `Pipe`: creates a pipe between the child and parent process, which can
171+
-- | then be accessed as a `Stream` via the `stdin`, `stdout`, or `stderr`
172+
-- | functions.
173+
-- | * `Ignore`: ignore this stream. This will cause Node to open /dev/null and
174+
-- | connect it to the stream.
175+
-- | * `ShareStream`: Connect the supplied stream to the corresponding file
176+
-- | descriptor in the child.
177+
-- | * `ShareFD`: Connect the supplied file descriptor (which should be open
178+
-- | in the parent) to the corresponding file descriptor in the child.
179+
data StdIOBehaviour
180+
= Pipe
181+
| Ignore
182+
| ShareStream (forall r eff a. Stream r eff a)
183+
| ShareFD FS.FileDescriptor
184+
185+
-- | Create pipes for each of the three standard IO streams.
186+
pipe :: Array (Maybe StdIOBehaviour)
187+
pipe = map Just [Pipe, Pipe, Pipe]
188+
189+
-- | Share stdin with stdin, stdout with stdout, and stderr with stderr.
190+
inherit :: Array (Maybe StdIOBehaviour)
191+
inherit = map Just
192+
[ ShareStream process.stdin
193+
, ShareStream process.stdout
194+
, ShareStream process.stderr
195+
]
196+
197+
foreign import process :: forall props. { | props }
198+
199+
-- | Ignore all streams.
200+
ignore :: Array (Maybe StdIOBehaviour)
201+
ignore = map Just [Ignore, Ignore, Ignore]
202+
203+
foreign import data ActualStdIOBehaviour :: *
204+
205+
toActualStdIOBehaviour :: StdIOBehaviour -> ActualStdIOBehaviour
206+
toActualStdIOBehaviour b = case b of
207+
Pipe -> c "pipe"
208+
Ignore -> c "ignore"
209+
ShareFD x -> c x
210+
ShareStream stream -> c stream
211+
where
212+
c :: forall a. a -> ActualStdIOBehaviour
213+
c = unsafeCoerce
214+
215+
type ActualStdIOOptions = Array (Nullable ActualStdIOBehaviour)
216+
217+
toActualStdIOOptions :: Array (Maybe StdIOBehaviour) -> ActualStdIOOptions
218+
toActualStdIOOptions = map (toNullable <<< map toActualStdIOBehaviour)

0 commit comments

Comments
 (0)