diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b61503..4637f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,30 @@ Breaking changes: - Update `pid` type signature to return `Maybe Pid` rather than `Pid` (#44 by @JordanMartinez) - Update `kill` returned value from `Effect Unit` to `Effect Boolean` (#44 by @JordanMartinez) - Migrate `Error` to `node-os`' `SystemError` (#45 by @JordanMartinez) +- Breaking changes made to the `Exit` type (#46 by @JordanMartinez) + - Moved from `Node.ChildProces` to `Node.ChildProces.Types` + - Changed the `BySignal`'s constructor's arg type from `Signal` to `String` +- Breaking changes made to the `Handle` type (#46 by @JordanMartinez) + + - Moved from `Node.ChildProces` to `Node.ChildProces.Types` +- Converted `defaultOptions { override = Just 1}` pattern to `(_ { override = Just 1})` (#46 by @JordanMartinez) + + Before: + ```purs + spawn "foo" [ "bar" ] (defaultSpawnOptions { someOption = Just overrideValue }) + spawn "foo" [ "bar" ] defaultSpawnOptions + ``` + + After: + ```purs + spawn "foo" [ "bar" ] (_ { someOption = Just overrideValue }) + spawn "foo" [ "bar" ] identity + ``` +- Restrict end-user's ability to configure `stdio` to only those appended to `safeStdio` (#46 by @JordanMartinez) + + See the module docs for `Node.ChildProcess`. +- All `ChildProcess`-creating functions have been updated to support no args and all args variants (#46 by @JordanMartinez) New features: - Added event handler for `spawn` event (#43 by @JordanMartinez) @@ -36,6 +59,8 @@ New features: - signalCode - spawnArgs - spawnFile +- Added unsafe, uncurried API of all ChildProcess-creating functions (#46 by @JordanMartinez) +- Added safe variant of `spawnSync`/`spawnSync'` (#46 by @JordanMartinez) Bugfixes: @@ -43,7 +68,8 @@ Other improvements: - Bumped CI's node version to `lts/*` (#41 by @JordanMartinez) - Updated CI `actions/checkout` and `actions/setup-nodee` to `v3` (#41 by @JordanMartinez) - Format codebase & enforce formatting in CI via purs-tidy (#42 by @JordanMartinez) -- Migrate more FFI to uncurried functions (#44 by @JordanMartinez) +- Migrate FFI to uncurried functions (#44, #46 by @JordanMartinez) +- Updated recommended module alias in docs (#46 by @JordanMartinez) ## [v9.0.0](https://github.com/purescript-node/purescript-node-child-process/releases/tag/v9.0.0) - 2022-04-29 diff --git a/src/Node/ChildProcess.js b/src/Node/ChildProcess.js index d621449..b63f84e 100644 --- a/src/Node/ChildProcess.js +++ b/src/Node/ChildProcess.js @@ -1,63 +1,2 @@ -/* eslint-env node*/ - -import { spawn, exec, execFile, execSync, execFileSync, fork as cp_fork } from "child_process"; - -export function unsafeFromNullable(msg) { - return x => { - if (x === null) throw new Error(msg); - return x; - }; -} - -export const connectedImpl = (cp) => cp.connected; -export const disconnectImpl = (cp) => cp.disconnect(); -export const exitCodeImpl = (cp) => cp.exitCode; -export const pidImpl = (cp) => cp.pid; -export const killImpl = (cp) => cp.kill(); -export const killStrImpl = (cp, str) => cp.kill(str); -export const killedImpl = (cp) => cp.killed; -export const signalCodeImpl = (cp) => cp.signalCode; -export const spawnArgs = (cp) => cp.spawnArgs; -export const spawnFile = (cp) => cp.spawnFile; - -export function spawnImpl(command) { - return args => opts => () => spawn(command, args, opts); -} - -export function execImpl(command) { - return opts => callback => () => exec( - command, - opts, - (err, stdout, stderr) => { - callback(err)(stdout)(stderr)(); - } - ); -} - -export const execFileImpl = function execImpl(command) { - return args => opts => callback => () => execFile( - command, - args, - opts, - (err, stdout, stderr) => { - callback(err)(stdout)(stderr)(); - } - ); -}; - -export function execSyncImpl(command) { - return opts => () => execSync(command, opts); -} - -export function execFileSyncImpl(command) { - return args => opts => () => execFileSync(command, args, opts); -} - -export function fork(cmd) { - return args => () => cp_fork(cmd, args); -} - const _undefined = undefined; export { _undefined as undefined }; -import process from "process"; -export { process }; diff --git a/src/Node/ChildProcess.purs b/src/Node/ChildProcess.purs index 3e90a4b..fc902f2 100644 --- a/src/Node/ChildProcess.purs +++ b/src/Node/ChildProcess.purs @@ -4,16 +4,35 @@ -- | It is intended to be imported qualified, as follows: -- | -- | ```purescript --- | import Node.ChildProcess (ChildProcess, CHILD_PROCESS) -- | import Node.ChildProcess as ChildProcess +-- | -- or... +-- | import Node.ChildProcess as CP -- | ``` -- | -- | The [Node.js documentation](https://nodejs.org/api/child_process.html) -- | forms the basis for this module and has in-depth documentation about -- | runtime behaviour. +-- | +-- | ## Meaning of `appendStdio` +-- | +-- | By default, `ChildProcess` uses `safeStdio` for its `stdio` option. However, +-- | Node allows one to pass in additional values besides the typical 3 (i.e. `stdin`, `stdout`, `stderr`) +-- | and the IPC channel that might be used (i.e. `ipc`). Thus, `appendStdio` is an option +-- | defined in this library that doesn't exist in the Node docs. +-- | It exists to allow the end-user to append additional values to the `safeStdio` value +-- | used here. For example, +-- | +-- | ``` +-- | spawn' file args (_ { appendStdio = Just [ fileDescriptor8, pipe, pipe ]}) +-- | ``` +-- | +-- | would end up calling `spawn` with the following `stdio`: +-- | ``` +-- | -- i.e. `safeStdio <> [ fileDescriptor8, pipe, pipe ]` +-- | [pipe, pipe, pipe, ipc, fileDescriptor8, pipe, pipe] +-- | ``` module Node.ChildProcess - ( Handle - , ChildProcess + ( ChildProcess , toEventEmitter , closeH , disconnectH @@ -33,173 +52,127 @@ module Node.ChildProcess , killSignal , killed , signalCode - , send - , Exit(..) + , spawnFile + , spawnArgs + , spawnSync + , SpawnSyncOptions + , spawnSync' , spawn , SpawnOptions - , defaultSpawnOptions + , spawn' + , execSync + , ExecSyncOptions + , execSync' , exec - , execFile - , ExecOptions , ExecResult - , defaultExecOptions - , execSync + , ExecOptions + , exec' , execFileSync - , ExecSyncOptions - , defaultExecSyncOptions + , ExecFileSyncOptions + , execFileSync' + , execFile + , ExecFileOptions + , execFile' , fork - , StdIOBehaviour(..) - , pipe - , inherit - , ignore + , fork' + , send + , send' ) where import Prelude -import Data.Function.Uncurried (Fn2, runFn2) import Data.Maybe (Maybe(..), fromMaybe, maybe) import Data.Nullable (Nullable, toMaybe, toNullable) import Data.Posix (Pid, Gid, Uid) import Data.Posix.Signal (Signal) -import Data.Posix.Signal as Signal +import Data.Time.Duration (Milliseconds) import Effect (Effect) -import Effect.Exception as Exception -import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2, runEffectFn1, runEffectFn2) +import Effect.Exception (Error) +import Effect.Uncurried (EffectFn2) import Foreign (Foreign) import Foreign.Object (Object) import Node.Buffer (Buffer) -import Node.Encoding (Encoding, encodingToNode) +import Node.ChildProcess.Types (Exit(..), Handle, KillSignal, Shell, StdIO, UnsafeChildProcess) import Node.Errors.SystemError (SystemError) -import Node.EventEmitter (EventEmitter, EventHandle(..)) +import Node.EventEmitter (EventEmitter, EventHandle) import Node.EventEmitter.UtilTypes (EventHandle0, EventHandle1) -import Node.FS as FS -import Node.Stream (Readable, Stream, Writable) +import Node.Stream (Readable, Writable) +import Node.UnsafeChildProcess.Safe (safeStdio) +import Node.UnsafeChildProcess.Safe as SafeCP +import Node.UnsafeChildProcess.Unsafe (unsafeSOBToBuffer) +import Node.UnsafeChildProcess.Unsafe as UnsafeCP import Partial.Unsafe (unsafeCrashWith) +import Safe.Coerce (coerce) import Unsafe.Coerce (unsafeCoerce) --- | A handle for inter-process communication (IPC). -foreign import data Handle :: Type - -- | Opaque type returned by `spawn`, `fork` and `exec`. -- | Needed as input for most methods in this module. -- | -- | `ChildProcess` extends `EventEmitter` -newtype ChildProcess = ChildProcess ChildProcessRec +newtype ChildProcess = ChildProcess UnsafeChildProcess toEventEmitter :: ChildProcess -> EventEmitter -toEventEmitter = unsafeCoerce +toEventEmitter = toUnsafeChildProcess >>> SafeCP.toEventEmitter + +toUnsafeChildProcess :: ChildProcess -> UnsafeChildProcess +toUnsafeChildProcess (ChildProcess p) = p closeH :: EventHandle ChildProcess (Exit -> Effect Unit) (EffectFn2 (Nullable Int) (Nullable String) Unit) -closeH = EventHandle "close" \cb -> mkEffectFn2 \code signal -> - case toMaybe code, toMaybe signal >>= Signal.fromString of - Just c, _ -> cb $ Normally c - _, Just s -> cb $ BySignal s - _, _ -> unsafeCrashWith $ "Impossible. 'close' event did not get an exit code or kill signal: " <> show code <> "; " <> show signal +closeH = unsafeCoerce SafeCP.closeH disconnectH :: EventHandle0 ChildProcess -disconnectH = EventHandle "disconnect" identity +disconnectH = unsafeCoerce SafeCP.disconnectH errorH :: EventHandle1 ChildProcess SystemError -errorH = EventHandle "error" mkEffectFn1 +errorH = unsafeCoerce SafeCP.errorH exitH :: EventHandle ChildProcess (Exit -> Effect Unit) (EffectFn2 (Nullable Int) (Nullable String) Unit) -exitH = EventHandle "exitH" \cb -> mkEffectFn2 \code signal -> - case toMaybe code, toMaybe signal >>= Signal.fromString of - Just c, _ -> cb $ Normally c - _, Just s -> cb $ BySignal s - _, _ -> unsafeCrashWith $ "Impossible. 'exit' event did not get an exit code or kill signal: " <> show code <> "; " <> show signal +exitH = unsafeCoerce SafeCP.exitH messageH :: EventHandle ChildProcess (Foreign -> Maybe Handle -> Effect Unit) (EffectFn2 Foreign (Nullable Handle) Unit) -messageH = EventHandle "message" \cb -> mkEffectFn2 \a b -> cb a $ toMaybe b +messageH = unsafeCoerce SafeCP.messageH spawnH :: EventHandle0 ChildProcess -spawnH = EventHandle "spawn" identity - -runChildProcess :: ChildProcess -> ChildProcessRec -runChildProcess (ChildProcess r) = r - --- | Note: some of these types are lies, and so it is unsafe to access some of --- | these record fields directly. -type ChildProcessRec = - { stdin :: Nullable (Writable ()) - , stdout :: Nullable (Readable ()) - , stderr :: Nullable (Readable ()) - , pid :: Pid - , connected :: Boolean - , kill :: String -> Unit - , send :: forall r. Fn2 { | r } Handle Boolean - , disconnect :: Effect Unit - } +spawnH = unsafeCoerce SafeCP.spawnH + +unsafeFromNull :: forall a. Nullable a -> a +unsafeFromNull = unsafeCoerce --- | The standard input stream of a child process. Note that this is only --- | available if the process was spawned with the stdin option set to "pipe". +-- | The standard input stream of a child process. stdin :: ChildProcess -> Writable () -stdin = unsafeFromNullable (missingStream "stdin") <<< _.stdin <<< runChildProcess +stdin = toUnsafeChildProcess >>> UnsafeCP.unsafeStdin >>> unsafeFromNull --- | The standard output stream of a child process. Note that this is only --- | available if the process was spawned with the stdout option set to "pipe". +-- | The standard output stream of a child process. stdout :: ChildProcess -> Readable () -stdout = unsafeFromNullable (missingStream "stdout") <<< _.stdout <<< runChildProcess +stdout = toUnsafeChildProcess >>> UnsafeCP.unsafeStdout >>> unsafeFromNull --- | The standard error stream of a child process. Note that this is only --- | available if the process was spawned with the stderr option set to "pipe". +-- | The standard error stream of a child process. stderr :: ChildProcess -> Readable () -stderr = unsafeFromNullable (missingStream "stderr") <<< _.stderr <<< runChildProcess - -missingStream :: String -> String -missingStream str = - "Node.ChildProcess: stream not available: " <> str <> "\nThis is probably " - <> "because you passed something other than Pipe to the stdio option when " - <> "you spawned it." - -foreign import unsafeFromNullable :: forall a. String -> Nullable a -> a +stderr = toUnsafeChildProcess >>> UnsafeCP.unsafeStderr >>> unsafeFromNull --- | The process ID of a child process. Note that if the process has already +-- | The process ID of a child process. This will be `Nothing` until +-- | the process has spawned. Note that if the process has already -- | exited, another process may have taken the same ID, so be careful! pid :: ChildProcess -> Effect (Maybe Pid) -pid cp = map toMaybe $ runEffectFn1 pidImpl cp - -foreign import pidImpl :: EffectFn1 (ChildProcess) (Nullable Pid) +pid = unsafeCoerce SafeCP.pid -- | Indicates whether it is still possible to send and receive -- | messages from the child process. connected :: ChildProcess -> Effect Boolean -connected cp = runEffectFn1 connectedImpl cp - -foreign import connectedImpl :: EffectFn1 (ChildProcess) (Boolean) +connected = unsafeCoerce SafeCP.connected exitCode :: ChildProcess -> Effect (Maybe Int) -exitCode cp = map toMaybe $ runEffectFn1 exitCodeImpl cp - -foreign import exitCodeImpl :: EffectFn1 (ChildProcess) (Nullable Int) - --- | Send messages to the (`nodejs`) child process. --- | --- | See the [node documentation](https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback) --- | for in-depth documentation. -send - :: forall props - . { | props } - -> Handle - -> ChildProcess - -> Effect Boolean -send msg handle (ChildProcess cp) = mkEffect \_ -> runFn2 cp.send msg handle +exitCode = unsafeCoerce SafeCP.exitCode -- | Closes the IPC channel between parent and child. disconnect :: ChildProcess -> Effect Unit -disconnect cp = runEffectFn1 disconnectImpl cp - -foreign import disconnectImpl :: EffectFn1 (ChildProcess) (Unit) +disconnect = unsafeCoerce SafeCP.disconnect kill :: ChildProcess -> Effect Boolean -kill cp = runEffectFn1 killImpl cp - -foreign import killImpl :: EffectFn1 (ChildProcess) (Boolean) +kill = unsafeCoerce SafeCP.kill kill' :: String -> ChildProcess -> Effect Boolean -kill' sig cp = runEffectFn2 killStrImpl cp sig - -foreign import killStrImpl :: EffectFn2 (ChildProcess) (String) (Boolean) +kill' = unsafeCoerce SafeCP.kill' -- | Send a signal to a child process. In the same way as the -- | [unix kill(2) system call](https://linux.die.net/man/2/kill), @@ -210,34 +183,125 @@ foreign import killStrImpl :: EffectFn2 (ChildProcess) (String) (Boolean) -- | The child process might emit an `"error"` event if the signal -- | could not be delivered. killSignal :: Signal -> ChildProcess -> Effect Boolean -killSignal sig cp = kill' (Signal.toString sig) cp +killSignal = unsafeCoerce SafeCP.killSignal killed :: ChildProcess -> Effect Boolean -killed cp = runEffectFn1 killedImpl cp +killed = unsafeCoerce SafeCP.killed signalCode :: ChildProcess -> Effect (Maybe String) -signalCode cp = map toMaybe $ runEffectFn1 signalCodeImpl cp +signalCode = unsafeCoerce SafeCP.signalCode -foreign import signalCodeImpl :: EffectFn1 (ChildProcess) (Nullable String) +spawnArgs :: ChildProcess -> Array String +spawnArgs = unsafeCoerce SafeCP.spawnArgs -foreign import killedImpl :: EffectFn1 (ChildProcess) (Boolean) +spawnFile :: ChildProcess -> String +spawnFile = unsafeCoerce SafeCP.spawnFile -foreign import spawnArgs :: ChildProcess -> Array String +-- | Note: `exitStatus` combines the `status` and `signal` fields +-- | from the value normally returned by `spawnSync` into one value +-- | since only one of them can be non-null at the end. +type SpawnSyncResult = + { pid :: Pid + , output :: Array Foreign + , stdout :: Buffer + , stderr :: Buffer + , exitStatus :: Exit + , error :: Maybe SystemError + } -foreign import spawnFile :: ChildProcess -> String +spawnSync + :: String + -> Array String + -> Effect SpawnSyncResult +spawnSync command args = (UnsafeCP.spawnSync command args) <#> \r -> + { pid: r.pid + , output: r.output + , stdout: unsafeSOBToBuffer r.stdout + , stderr: unsafeSOBToBuffer r.stderr + , exitStatus: case toMaybe r.status, toMaybe r.signal of + Just c, _ -> Normally c + _, Just s -> BySignal s + _, _ -> unsafeCrashWith $ "Impossible: `spawnSync` child process neither exited nor was killed." + , error: toMaybe r.error + } -mkEffect :: forall a. (Unit -> a) -> Effect a -mkEffect = unsafeCoerce +-- | - `cwd` | Current working directory of the child process. +-- | - `input` | | | The value which will be passed as stdin to the spawned process. Supplying this value will override stdio[0]. +-- | - `argv0` Explicitly set the value of argv[0] sent to the child process. This will be set to command if not specified. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed. Default: 'SIGTERM'. +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +-- | - `windowsVerbatimArguments` No quoting or escaping of arguments is done on Windows. Ignored on Unix. This is set to true automatically when shell is specified and is CMD. Default: false. +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +type SpawnSyncOptions = + { cwd :: Maybe String + , input :: Maybe Buffer + , appendStdio :: Maybe (Array StdIO) + , argv0 :: Maybe String + , env :: Maybe (Object String) + , uid :: Maybe Uid + , gid :: Maybe Gid + , timeout :: Maybe Milliseconds + , killSignal :: Maybe KillSignal + , maxBuffer :: Maybe Number + , shell :: Maybe Shell + , windowsVerbatimArguments :: Maybe Boolean + , windowsHide :: Maybe Boolean + } --- | Specifies how a child process exited; normally (with an exit code), or --- | due to a signal. -data Exit - = Normally Int - | BySignal Signal +spawnSync' + :: String + -> Array String + -> (SpawnSyncOptions -> SpawnSyncOptions) + -> Effect SpawnSyncResult +spawnSync' command args buildOpts = (UnsafeCP.spawnSync' command args opts) <#> \r -> + { pid: r.pid + , output: r.output + , stdout: unsafeSOBToBuffer r.stdout + , stderr: unsafeSOBToBuffer r.stderr + , exitStatus: case toMaybe r.status, toMaybe r.signal of + Just c, _ -> Normally c + _, Just s -> BySignal s + _, _ -> unsafeCrashWith $ "Impossible: `spawnSync` child process neither exited nor was killed." + , error: toMaybe r.error + } + where + opts = + { stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio + , encoding: "buffer" + , cwd: fromMaybe undefined o.cwd + , input: fromMaybe undefined o.input + , argv0: fromMaybe undefined o.argv0 + , env: fromMaybe undefined o.env + , uid: fromMaybe undefined o.uid + , gid: fromMaybe undefined o.gid + , timeout: fromMaybe undefined o.timeout + , killSignal: fromMaybe undefined o.killSignal + , maxBuffer: fromMaybe undefined o.maxBuffer + , shell: fromMaybe undefined o.shell + , windowsVerbatimArguments: fromMaybe undefined o.windowsVerbatimArguments + , windowsHide: fromMaybe undefined o.windowsHide + } -instance showExit :: Show Exit where - show (Normally x) = "Normally " <> show x - show (BySignal sig) = "BySignal " <> show sig + o = buildOpts + { cwd: Nothing + , input: Nothing + , appendStdio: Nothing + , argv0: Nothing + , env: Nothing + , uid: Nothing + , gid: Nothing + , timeout: Nothing + , killSignal: Nothing + , maxBuffer: Nothing + , shell: Nothing + , windowsVerbatimArguments: Nothing + , windowsHide: Nothing + } -- | Spawn a child process. Note that, in the event that a child process could -- | not be spawned (for example, if the executable was not found) this will @@ -246,283 +310,466 @@ instance showExit :: Show Exit where spawn :: String -> Array String - -> SpawnOptions -> Effect ChildProcess -spawn cmd args = spawnImpl cmd args <<< convertOpts - where - convertOpts opts = - { cwd: fromMaybe undefined opts.cwd - , stdio: toActualStdIOOptions opts.stdio - , env: toNullable opts.env - , detached: opts.detached - , uid: fromMaybe undefined opts.uid - , gid: fromMaybe undefined opts.gid - } +spawn cmd args = coerce $ UnsafeCP.spawn' cmd args { stdio: safeStdio } + +-- | - `cwd` | Current working directory of the child process. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `argv0` Explicitly set the value of argv[0] sent to the child process. This will be set to command if not specified. +-- | - `detached` Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached). +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `serialization` Specify the kind of serialization used for sending messages between processes. Possible values are 'json' and 'advanced'. See Advanced serialization for more details. Default: 'json'. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +-- | - `windowsVerbatimArguments` No quoting or escaping of arguments is done on Windows. Ignored on Unix. This is set to true automatically when shell is specified and is CMD. Default: false. +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +-- | - `signal` allows aborting the child process using an AbortSignal. +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed by timeout or abort signal. Default: 'SIGTERM'. +type SpawnOptions = + { cwd :: Maybe String + , env :: Maybe (Object String) + , argv0 :: Maybe String + , appendStdio :: Maybe (Array StdIO) + , detached :: Maybe Boolean + , uid :: Maybe Uid + , gid :: Maybe Gid + , serialization :: Maybe String + , shell :: Maybe Shell + , windowsVerbatimArguments :: Maybe Boolean + , windowsHide :: Maybe Boolean + , timeout :: Maybe Number + , killSignal :: Maybe KillSignal + } -foreign import spawnImpl - :: forall opts - . String +spawn' + :: String -> Array String - -> { | opts } + -> (SpawnOptions -> SpawnOptions) -> Effect ChildProcess +spawn' cmd args buildOpts = coerce $ UnsafeCP.spawn' cmd args opts + where + opts = + { stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio + , cwd: fromMaybe undefined o.cwd + , env: fromMaybe undefined o.env + , argv0: fromMaybe undefined o.argv0 + , detached: fromMaybe undefined o.detached + , uid: fromMaybe undefined o.uid + , gid: fromMaybe undefined o.gid + , serialization: fromMaybe undefined o.serialization + , shell: fromMaybe undefined o.shell + , windowsVerbatimArguments: fromMaybe undefined o.windowsVerbatimArguments + , windowsHide: fromMaybe undefined o.windowsHide + , timeout: fromMaybe undefined o.timeout + , killSignal: fromMaybe undefined o.killSignal + } + o = buildOpts + { cwd: Nothing + , env: Nothing + , argv0: Nothing + , appendStdio: Nothing + , detached: Nothing + , uid: Nothing + , gid: Nothing + , serialization: Nothing + , shell: Nothing + , windowsVerbatimArguments: Nothing + , windowsHide: Nothing + , timeout: Nothing + , killSignal: Nothing + } --- There's gotta be a better way. -foreign import undefined :: forall a. a - --- | Configuration of `spawn`. Fields set to `Nothing` will use --- | the node defaults. -type SpawnOptions = +-- | Generally identical to `exec`, with the exception that +-- | the method will not return until the child process has fully closed. +-- | Returns: The `stdout` from the command. +execSync + :: String + -> Effect Buffer +execSync cmd = map unsafeSOBToBuffer $ UnsafeCP.execSync cmd + +-- | - `cwd` | Current working directory of the child process. +-- | - `input` | | | The value which will be passed as stdin to the spawned process. Supplying this value will override stdio[0]. +-- | - `stdio` | Child's stdio configuration. stderr by default will be output to the parent process' stderr unless stdio is specified. Default: 'pipe'. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `shell` Shell to execute the command with. See Shell requirements and Default Windows shell. Default: '/bin/sh' on Unix, process.env.ComSpec on Windows. +-- | - `uid` Sets the user identity of the process. (See setuid(2)). +-- | - `gid` Sets the group identity of the process. (See setgid(2)). +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed. Default: 'SIGTERM'. +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `encoding` The encoding used for all stdio inputs and outputs. Default: 'buffer'. +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +type ExecSyncOptions = { cwd :: Maybe String - , stdio :: Array (Maybe StdIOBehaviour) + , input :: Maybe Buffer + , appendStdio :: Maybe (Array StdIO) , env :: Maybe (Object String) - , detached :: Boolean + , shell :: Maybe String , uid :: Maybe Uid , gid :: Maybe Gid + , timeout :: Maybe Milliseconds + , killSignal :: Maybe KillSignal + , maxBuffer :: Maybe Number + , windowsHide :: Maybe Boolean } --- | A default set of `SpawnOptions`. Everything is set to `Nothing`, --- | `detached` is `false` and `stdio` is `ChildProcess.pipe`. -defaultSpawnOptions :: SpawnOptions -defaultSpawnOptions = - { cwd: Nothing - , stdio: pipe - , env: Nothing - , detached: false - , uid: Nothing - , gid: Nothing - } +execSync' + :: String + -> (ExecSyncOptions -> ExecSyncOptions) + -> Effect Buffer +execSync' cmd buildOpts = do + map unsafeSOBToBuffer $ UnsafeCP.execSync' cmd opts + where + o = buildOpts + { cwd: Nothing + , input: Nothing + , appendStdio: Nothing + , env: Nothing + , shell: Nothing + , uid: Nothing + , gid: Nothing + , timeout: Nothing + , killSignal: Nothing + , maxBuffer: Nothing + , windowsHide: Nothing + } + opts = + { stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio + , encoding: "buffer" + , cwd: fromMaybe undefined o.cwd + , input: fromMaybe undefined o.input + , env: fromMaybe undefined o.env + , shell: fromMaybe undefined o.shell + , uid: fromMaybe undefined o.uid + , gid: fromMaybe undefined o.gid + , timeout: fromMaybe undefined o.timeout + , killSignal: fromMaybe undefined o.killSignal + , maxBuffer: fromMaybe undefined o.maxBuffer + , windowsHide: fromMaybe undefined o.windowsHide + } -- | Similar to `spawn`, except that this variant will: -- | * run the given command with the shell, --- | * buffer output, and wait until the process has exited before calling the --- | callback. +-- | * buffer output, and wait until the process has exited. -- | -- | Note that the child process will be killed if the amount of output exceeds -- | a certain threshold (the default is defined by Node.js). -exec - :: String - -> ExecOptions - -> (ExecResult -> Effect Unit) - -> Effect ChildProcess -exec cmd opts callback = - execImpl cmd (convertExecOptions opts) \err stdout' stderr' -> - callback - { error: toMaybe err - , stdout: stdout' - , stderr: stderr' - } - -foreign import execImpl - :: String - -> ActualExecOptions - -> (Nullable Exception.Error -> Buffer -> Buffer -> Effect Unit) - -> Effect ChildProcess - --- | Like `exec`, except instead of using a shell, it passes the arguments --- | directly to the specified command. -execFile - :: String - -> Array String - -> ExecOptions - -> (ExecResult -> Effect Unit) - -> Effect ChildProcess -execFile cmd args opts callback = - execFileImpl cmd args (convertExecOptions opts) \err stdout' stderr' -> - callback - { error: toMaybe err - , stdout: stdout' - , stderr: stderr' - } - -foreign import execFileImpl - :: String - -> Array String - -> ActualExecOptions - -> (Nullable Exception.Error -> Buffer -> Buffer -> Effect Unit) - -> Effect ChildProcess +exec :: String -> Effect ChildProcess +exec command = coerce $ UnsafeCP.execOpts command { encoding: "buffer" } -foreign import data ActualExecOptions :: Type - -convertExecOptions :: ExecOptions -> ActualExecOptions -convertExecOptions opts = unsafeCoerce - { cwd: fromMaybe undefined opts.cwd - , env: fromMaybe undefined opts.env - , encoding: maybe undefined encodingToNode opts.encoding - , shell: fromMaybe undefined opts.shell - , timeout: fromMaybe undefined opts.timeout - , maxBuffer: fromMaybe undefined opts.maxBuffer - , killSignal: fromMaybe undefined opts.killSignal - , uid: fromMaybe undefined opts.uid - , gid: fromMaybe undefined opts.gid +-- | The combined output of a process called with `exec`. +type ExecResult = + { stdout :: Buffer + , stderr :: Buffer + , error :: Maybe SystemError } --- | Configuration of `exec`. Fields set to `Nothing` --- | will use the node defaults. +-- | - `cwd` | Current working directory of the child process. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `timeout` Default: 0 +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `killSignal` | Default: 'SIGTERM' +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). type ExecOptions = { cwd :: Maybe String , env :: Maybe (Object String) - , encoding :: Maybe Encoding - , shell :: Maybe String , timeout :: Maybe Number - , maxBuffer :: Maybe Int - , killSignal :: Maybe Signal + , maxBuffer :: Maybe Number + , killSignal :: Maybe KillSignal , uid :: Maybe Uid , gid :: Maybe Gid + , windowsHide :: Maybe Boolean + , shell :: Maybe Shell } --- | A default set of `ExecOptions`. Everything is set to `Nothing`. -defaultExecOptions :: ExecOptions -defaultExecOptions = - { cwd: Nothing - , env: Nothing - , encoding: Nothing - , shell: Nothing - , timeout: Nothing - , maxBuffer: Nothing - , killSignal: Nothing - , uid: Nothing - , gid: Nothing - } - --- | The combined output of a process calld with `exec`. -type ExecResult = - { stderr :: Buffer - , stdout :: Buffer - , error :: Maybe Exception.Error - } - --- | Generally identical to `exec`, with the exception that --- | the method will not return until the child process has fully closed. --- | Returns: The stdout from the command. -execSync - :: String - -> ExecSyncOptions - -> Effect Buffer -execSync cmd opts = - execSyncImpl cmd (convertExecSyncOptions opts) - -foreign import execSyncImpl +-- | Similar to `spawn`, except that this variant will: +-- | * run the given command with the shell, +-- | * buffer output, and wait until the process has exited before calling the +-- | callback. +-- | +-- | Note that the child process will be killed if the amount of output exceeds +-- | a certain threshold (the default is defined by Node.js). +exec' :: String - -> ActualExecSyncOptions - -> Effect Buffer + -> (ExecOptions -> ExecOptions) + -> (ExecResult -> Effect Unit) + -> Effect ChildProcess +exec' command buildOpts cb = coerce $ UnsafeCP.execOptsCb command opts \err sout serr -> + cb { stdout: unsafeSOBToBuffer sout, stderr: unsafeSOBToBuffer serr, error: err } + where + opts = + { encoding: "buffer" + , cwd: fromMaybe undefined o.cwd + , env: fromMaybe undefined o.env + , timeout: fromMaybe undefined o.timeout + , maxBuffer: fromMaybe undefined o.maxBuffer + , killSignal: fromMaybe undefined o.killSignal + , uid: fromMaybe undefined o.uid + , gid: fromMaybe undefined o.gid + , windowsHide: fromMaybe undefined o.windowsHide + , shell: fromMaybe undefined o.shell + } + o = buildOpts + { cwd: Nothing + , env: Nothing + , timeout: Nothing + , maxBuffer: Nothing + , killSignal: Nothing + , uid: Nothing + , gid: Nothing + , windowsHide: Nothing + , shell: Nothing + } -- | Generally identical to `execFile`, with the exception that -- | the method will not return until the child process has fully closed. --- | Returns: The stdout from the command. +-- | Returns: The `stdout` from the command. execFileSync :: String -> Array String - -> ExecSyncOptions -> Effect Buffer -execFileSync cmd args opts = - execFileSyncImpl cmd args (convertExecSyncOptions opts) +execFileSync file args = + map unsafeSOBToBuffer $ UnsafeCP.execFileSync' file args { stdio: safeStdio, encoding: "buffer" } + +-- | - `cwd` | Current working directory of the child process. +-- | - `input` | | | The value which will be passed as stdin to the spawned process. Supplying this value will override stdio[0]. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed. Default: 'SIGTERM'. +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +type ExecFileSyncOptions = + { cwd :: Maybe String + , input :: Maybe Buffer + , appendStdio :: Maybe (Array StdIO) + , env :: Maybe (Object String) + , uid :: Maybe Uid + , gid :: Maybe Gid + , timeout :: Maybe Milliseconds + , killSignal :: Maybe KillSignal + , maxBuffer :: Maybe Number + , windowsHide :: Maybe Boolean + , shell :: Maybe Shell + } -foreign import execFileSyncImpl +execFileSync' :: String -> Array String - -> ActualExecSyncOptions + -> (ExecFileSyncOptions -> ExecFileSyncOptions) -> Effect Buffer +execFileSync' file args buildOpts = + map unsafeSOBToBuffer $ UnsafeCP.execFileSync' file args opts + where + opts = + { stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio + , encoding: "buffer" + , cwd: fromMaybe undefined o.cwd + , input: fromMaybe undefined o.input + , env: fromMaybe undefined o.env + , uid: fromMaybe undefined o.uid + , gid: fromMaybe undefined o.gid + , timeout: fromMaybe undefined o.timeout + , killSignal: fromMaybe undefined o.killSignal + , maxBuffer: fromMaybe undefined o.maxBuffer + , windowsHide: fromMaybe undefined o.windowsHide + , shell: fromMaybe undefined o.shell + } + o = buildOpts + { cwd: Nothing + , input: Nothing + , appendStdio: Nothing + , env: Nothing + , uid: Nothing + , gid: Nothing + , timeout: Nothing + , killSignal: Nothing + , maxBuffer: Nothing + , windowsHide: Nothing + , shell: Nothing + } -foreign import data ActualExecSyncOptions :: Type - -convertExecSyncOptions :: ExecSyncOptions -> ActualExecSyncOptions -convertExecSyncOptions opts = unsafeCoerce - { cwd: fromMaybe undefined opts.cwd - , input: fromMaybe undefined opts.input - , stdio: toActualStdIOOptions opts.stdio - , env: fromMaybe undefined opts.env - , timeout: fromMaybe undefined opts.timeout - , maxBuffer: fromMaybe undefined opts.maxBuffer - , killSignal: fromMaybe undefined opts.killSignal - , uid: fromMaybe undefined opts.uid - , gid: fromMaybe undefined opts.gid - } - -type ExecSyncOptions = +-- | Like `exec`, except instead of using a shell, it passes the arguments +-- | directly to the specified command. +execFile + :: String + -> Array String + -> Effect ChildProcess +execFile cmd args = coerce $ UnsafeCP.execFileOpts cmd args { encoding: "buffer" } + +-- | - `cwd` | Current working directory of the child process. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `timeout` Default: 0 +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `killSignal` | Default: 'SIGTERM' +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +-- | - `windowsVerbatimArguments` No quoting or escaping of arguments is done on Windows. Ignored on Unix. Default: false. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +type ExecFileOptions = { cwd :: Maybe String - , input :: Maybe String - , stdio :: Array (Maybe StdIOBehaviour) , env :: Maybe (Object String) , timeout :: Maybe Number - , maxBuffer :: Maybe Int - , killSignal :: Maybe Signal + , maxBuffer :: Maybe Number + , killSignal :: Maybe KillSignal , uid :: Maybe Uid , gid :: Maybe Gid + , windowsHide :: Maybe Boolean + , windowsVerbatimArguments :: Maybe Boolean + , shell :: Maybe Shell } -defaultExecSyncOptions :: ExecSyncOptions -defaultExecSyncOptions = - { cwd: Nothing - , input: Nothing - , stdio: pipe - , env: Nothing - , timeout: Nothing - , maxBuffer: Nothing - , killSignal: Nothing - , uid: Nothing - , gid: Nothing - } +execFile' + :: String + -> Array String + -> (ExecFileOptions -> ExecFileOptions) + -> (ExecResult -> Effect Unit) + -> Effect ChildProcess +execFile' cmd args buildOpts cb = coerce $ UnsafeCP.execFileOptsCb cmd args opts \err sout serr -> + cb { stdout: unsafeSOBToBuffer sout, stderr: unsafeSOBToBuffer serr, error: err } + where + opts = + { cwd: fromMaybe undefined o.cwd + , env: fromMaybe undefined o.env + , encoding: "buffer" + , timeout: fromMaybe undefined o.timeout + , maxBuffer: fromMaybe undefined o.maxBuffer + , killSignal: fromMaybe undefined o.killSignal + , uid: fromMaybe undefined o.uid + , gid: fromMaybe undefined o.gid + , windowsHide: fromMaybe undefined o.windowsHide + , windowsVerbatimArguments: fromMaybe undefined o.windowsVerbatimArguments + , shell: fromMaybe undefined o.shell + } + o = buildOpts + { cwd: Nothing + , env: Nothing + , timeout: Nothing + , maxBuffer: Nothing + , killSignal: Nothing + , uid: Nothing + , gid: Nothing + , windowsHide: Nothing + , windowsVerbatimArguments: Nothing + , shell: Nothing + } -- | A special case of `spawn` for creating Node.js child processes. The first -- | argument is the module to be run, and the second is the argv (command line -- | arguments). -foreign import fork +fork :: String -> Array String -> Effect ChildProcess +fork modulePath args = coerce $ UnsafeCP.fork' modulePath args { stdio: safeStdio } + +-- | - `cwd` | Current working directory of the child process. +-- | - `detached` Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached). +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `execPath` Executable used to create the child process. +-- | - `execArgv` List of string arguments passed to the executable. Default: process.execArgv. +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `serialization` Specify the kind of serialization used for sending messages between processes. Possible values are 'json' and 'advanced'. See Advanced serialization for more details. Default: 'json'. +-- | - `signal` Allows closing the child process using an AbortSignal. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed by timeout or abort signal. Default: 'SIGTERM'. +-- | - `silent` If true, stdin, stdout, and stderr of the child will be piped to the parent, otherwise they will be inherited from the parent, see the 'pipe' and 'inherit' options for child_process.spawn()'s stdio for more details. Default: false. +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `windowsVerbatimArguments` No quoting or escaping of arguments is done on Windows. Ignored on Unix. Default: false. +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +type ForkOptions = + { cwd :: Maybe String + , detached :: Maybe Boolean + , appendStdio :: Maybe (Array StdIO) + , env :: Maybe (Object String) + , execPath :: Maybe String + , execArgv :: Maybe (Array String) + , gid :: Maybe Gid + , serialization :: Maybe String + , killSignal :: Maybe KillSignal + , silent :: Maybe Boolean + , uid :: Maybe Uid + , windowsVerbatimArguments :: Maybe Boolean + , timeout :: Maybe Milliseconds + } --- | Behaviour for standard IO streams (eg, standard input, standard output) of --- | a child process. --- | --- | * `Pipe`: creates a pipe between the child and parent process, which can --- | then be accessed as a `Stream` via the `stdin`, `stdout`, or `stderr` --- | functions. --- | * `Ignore`: ignore this stream. This will cause Node to open /dev/null and --- | connect it to the stream. --- | * `ShareStream`: Connect the supplied stream to the corresponding file --- | descriptor in the child. --- | * `ShareFD`: Connect the supplied file descriptor (which should be open --- | in the parent) to the corresponding file descriptor in the child. -data StdIOBehaviour - = Pipe - | Ignore - | ShareStream (forall r. Stream r) - | ShareFD FS.FileDescriptor - --- | Create pipes for each of the three standard IO streams. -pipe :: Array (Maybe StdIOBehaviour) -pipe = map Just [ Pipe, Pipe, Pipe ] - --- | Share `stdin` with `stdin`, `stdout` with `stdout`, --- | and `stderr` with `stderr`. -inherit :: Array (Maybe StdIOBehaviour) -inherit = map Just - [ ShareStream process.stdin - , ShareStream process.stdout - , ShareStream process.stderr - ] - -foreign import process :: forall props. { | props } - --- | Ignore all streams. -ignore :: Array (Maybe StdIOBehaviour) -ignore = map Just [ Ignore, Ignore, Ignore ] - --- Helpers - -foreign import data ActualStdIOBehaviour :: Type - -toActualStdIOBehaviour :: StdIOBehaviour -> ActualStdIOBehaviour -toActualStdIOBehaviour b = case b of - Pipe -> c "pipe" - Ignore -> c "ignore" - ShareFD x -> c x - ShareStream stream -> c stream +fork' + :: String + -> Array String + -> (ForkOptions -> ForkOptions) + -> Effect ChildProcess +fork' modulePath args buildOpts = coerce $ UnsafeCP.fork' modulePath args opts where - c :: forall a. a -> ActualStdIOBehaviour - c = unsafeCoerce + opts = + { stdio: maybe safeStdio (\rest -> safeStdio <> rest) o.appendStdio + , cwd: fromMaybe undefined o.cwd + , detached: fromMaybe undefined o.detached + , env: fromMaybe undefined o.env + , execPath: fromMaybe undefined o.execPath + , execArgv: fromMaybe undefined o.execArgv + , gid: fromMaybe undefined o.gid + , serialization: fromMaybe undefined o.serialization + , killSignal: fromMaybe undefined o.killSignal + , silent: fromMaybe undefined o.silent + , uid: fromMaybe undefined o.uid + , windowsVerbatimArguments: fromMaybe undefined o.windowsVerbatimArguments + , timeout: fromMaybe undefined o.timeout + } + o = buildOpts + { cwd: Nothing + , detached: Nothing + , appendStdio: Nothing + , env: Nothing + , execPath: Nothing + , execArgv: Nothing + , gid: Nothing + , serialization: Nothing + , killSignal: Nothing + , silent: Nothing + , uid: Nothing + , windowsVerbatimArguments: Nothing + , timeout: Nothing + } -type ActualStdIOOptions = Array (Nullable ActualStdIOBehaviour) +-- | Send messages to the (`nodejs`) child process. +-- | +-- | See the [node documentation](https://nodejs.org/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback) +-- | for in-depth documentation. +send + :: forall props + . { | props } + -> Maybe Handle + -> ChildProcess + -> Effect Boolean +send msg handle cp = UnsafeCP.unsafeSend msg (toNullable handle) (coerce cp) + +-- | - `keepAlive` A value that can be used when passing instances of `net.Socket` as the `Handle`. When true, the socket is kept open in the sending process. Default: false. +type SendOptions = + { keepAlive :: Maybe Boolean + } -toActualStdIOOptions :: Array (Maybe StdIOBehaviour) -> ActualStdIOOptions -toActualStdIOOptions = map (toNullable <<< map toActualStdIOBehaviour) +send' + :: forall props + . { | props } + -> Maybe Handle + -> (SendOptions -> SendOptions) + -> (Maybe Error -> Effect Unit) + -> ChildProcess + -> Effect Boolean +send' msg handle buildOpts cb cp = + UnsafeCP.unsafeSendOptsCb msg (toNullable handle) opts cb (coerce cp) + where + opts = + { keepAlive: fromMaybe undefined o.keepAlive } + o = buildOpts + { keepAlive: Nothing + } + +-- Unfortunately, there's not be a better way... +foreign import undefined :: forall a. a diff --git a/src/Node/ChildProcess/Types.purs b/src/Node/ChildProcess/Types.purs new file mode 100644 index 0000000..505638d --- /dev/null +++ b/src/Node/ChildProcess/Types.purs @@ -0,0 +1,77 @@ +module Node.ChildProcess.Types where + +import Prelude + +import Data.Nullable (Nullable, null) +import Node.FS (FileDescriptor) +import Node.Stream (Stream) +import Unsafe.Coerce (unsafeCoerce) + +-- | A child process with no guarantees about whether or not +-- | properties or methods (e.g. `stdin`, `send`) that depend on +-- | options or which function used to start the child process +-- | (e.g. `stdio`, `fork`) exist. +foreign import data UnsafeChildProcess :: Type + +-- | A handle for inter-process communication (IPC). +foreign import data Handle :: Type + +-- | See https://nodejs.org/docs/latest-v18.x/api/child_process.html#optionsstdio +foreign import data StdIO :: Type + +pipe :: StdIO +pipe = unsafeCoerce "pipe" + +ignore :: StdIO +ignore = unsafeCoerce "ignore" + +overlapped :: StdIO +overlapped = unsafeCoerce "overlapped" + +ipc :: StdIO +ipc = unsafeCoerce "ipc" + +inherit :: StdIO +inherit = unsafeCoerce "inherit" + +shareStream :: forall r. Stream r -> StdIO +shareStream = unsafeCoerce + +fileDescriptor :: Int -> StdIO +fileDescriptor = unsafeCoerce + +fileDescriptor' :: FileDescriptor -> StdIO +fileDescriptor' = unsafeCoerce + +defaultStdIO :: StdIO +defaultStdIO = unsafeCoerce (null :: Nullable String) + +foreign import data KillSignal :: Type + +intSignal :: Int -> KillSignal +intSignal = unsafeCoerce + +stringSignal :: String -> KillSignal +stringSignal = unsafeCoerce + +foreign import data Shell :: Type + +enableShell :: Shell +enableShell = unsafeCoerce true + +customShell :: String -> Shell +customShell = unsafeCoerce + +-- | Indicates value is either a String or a Buffer depending on +-- | what options were used. +foreign import data StringOrBuffer :: Type + +-- | Specifies how a child process exited; normally (with an exit code), or +-- | due to a signal. +data Exit + = Normally Int + | BySignal String + +instance showExit :: Show Exit where + show (Normally x) = "Normally " <> show x + show (BySignal sig) = "BySignal " <> show sig diff --git a/src/Node/UnsafeChildProcess/Safe.js b/src/Node/UnsafeChildProcess/Safe.js new file mode 100644 index 0000000..6cea788 --- /dev/null +++ b/src/Node/UnsafeChildProcess/Safe.js @@ -0,0 +1,10 @@ +export const connectedImpl = (cp) => cp.connected; +export const disconnectImpl = (cp) => cp.disconnect(); +export const exitCodeImpl = (cp) => cp.exitCode; +export const pidImpl = (cp) => cp.pid; +export const killImpl = (cp) => cp.kill(); +export const killStrImpl = (cp, str) => cp.kill(str); +export const killedImpl = (cp) => cp.killed; +export const signalCodeImpl = (cp) => cp.signalCode; +export const spawnArgs = (cp) => cp.spawnArgs; +export const spawnFile = (cp) => cp.spawnFile; diff --git a/src/Node/UnsafeChildProcess/Safe.purs b/src/Node/UnsafeChildProcess/Safe.purs new file mode 100644 index 0000000..29d5678 --- /dev/null +++ b/src/Node/UnsafeChildProcess/Safe.purs @@ -0,0 +1,137 @@ +-- | All API below is safe (i.e. does not crash if called) +-- | and independent from options or which +-- | function was used to start the `ChildProcess`. +module Node.UnsafeChildProcess.Safe + ( toEventEmitter + , closeH + , disconnectH + , errorH + , exitH + , messageH + , spawnH + , pid + , connected + , disconnect + , exitCode + , kill + , kill' + , killSignal + , killed + , signalCode + , spawnFile + , spawnArgs + , safeStdio + ) where + +import Prelude + +import Data.Maybe (Maybe(..)) +import Data.Nullable (Nullable, toMaybe) +import Data.Posix (Pid) +import Data.Posix.Signal (Signal) +import Data.Posix.Signal as Signal +import Effect (Effect) +import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2, runEffectFn1, runEffectFn2) +import Foreign (Foreign) +import Node.ChildProcess.Types (Exit(..), Handle, StdIO, UnsafeChildProcess, ipc, pipe) +import Node.Errors.SystemError (SystemError) +import Node.EventEmitter (EventEmitter, EventHandle(..)) +import Node.EventEmitter.UtilTypes (EventHandle0, EventHandle1) +import Partial.Unsafe (unsafeCrashWith) +import Unsafe.Coerce (unsafeCoerce) + +toEventEmitter :: UnsafeChildProcess -> EventEmitter +toEventEmitter = unsafeCoerce + +closeH :: EventHandle UnsafeChildProcess (Exit -> Effect Unit) (EffectFn2 (Nullable Int) (Nullable String) Unit) +closeH = EventHandle "close" \cb -> mkEffectFn2 \code signal -> + case toMaybe code, toMaybe signal of + Just c, _ -> cb $ Normally c + _, Just s -> cb $ BySignal s + _, _ -> unsafeCrashWith $ "Impossible. 'close' event did not get an exit code or kill signal: " <> show code <> "; " <> show signal + +disconnectH :: EventHandle0 UnsafeChildProcess +disconnectH = EventHandle "disconnect" identity + +errorH :: EventHandle1 UnsafeChildProcess SystemError +errorH = EventHandle "error" mkEffectFn1 + +exitH :: EventHandle UnsafeChildProcess (Exit -> Effect Unit) (EffectFn2 (Nullable Int) (Nullable String) Unit) +exitH = EventHandle "exitH" \cb -> mkEffectFn2 \code signal -> + case toMaybe code, toMaybe signal of + Just c, _ -> cb $ Normally c + _, Just s -> cb $ BySignal s + _, _ -> unsafeCrashWith $ "Impossible. 'exit' event did not get an exit code or kill signal: " <> show code <> "; " <> show signal + +messageH :: EventHandle UnsafeChildProcess (Foreign -> Maybe Handle -> Effect Unit) (EffectFn2 Foreign (Nullable Handle) Unit) +messageH = EventHandle "message" \cb -> mkEffectFn2 \a b -> cb a $ toMaybe b + +spawnH :: EventHandle0 UnsafeChildProcess +spawnH = EventHandle "spawn" identity + +-- | The process ID of a child process. Note that if the process has already +-- | exited, another process may have taken the same ID, so be careful! +pid :: UnsafeChildProcess -> Effect (Maybe Pid) +pid cp = map toMaybe $ runEffectFn1 pidImpl cp + +foreign import pidImpl :: EffectFn1 (UnsafeChildProcess) (Nullable Pid) + +-- | Indicates whether it is still possible to send and receive +-- | messages from the child process. +connected :: UnsafeChildProcess -> Effect Boolean +connected cp = runEffectFn1 connectedImpl cp + +foreign import connectedImpl :: EffectFn1 (UnsafeChildProcess) (Boolean) + +exitCode :: UnsafeChildProcess -> Effect (Maybe Int) +exitCode cp = map toMaybe $ runEffectFn1 exitCodeImpl cp + +foreign import exitCodeImpl :: EffectFn1 (UnsafeChildProcess) (Nullable Int) + +-- | Closes the IPC channel between parent and child. +disconnect :: UnsafeChildProcess -> Effect Unit +disconnect cp = runEffectFn1 disconnectImpl cp + +foreign import disconnectImpl :: EffectFn1 (UnsafeChildProcess) (Unit) + +kill :: UnsafeChildProcess -> Effect Boolean +kill cp = runEffectFn1 killImpl cp + +foreign import killImpl :: EffectFn1 (UnsafeChildProcess) (Boolean) + +kill' :: String -> UnsafeChildProcess -> Effect Boolean +kill' sig cp = runEffectFn2 killStrImpl cp sig + +foreign import killStrImpl :: EffectFn2 (UnsafeChildProcess) (String) (Boolean) + +-- | Send a signal to a child process. In the same way as the +-- | [unix kill(2) system call](https://linux.die.net/man/2/kill), +-- | sending a signal to a child process won't necessarily kill it. +-- | +-- | The resulting effects of this function depend on the process +-- | and the signal. They can vary from system to system. +-- | The child process might emit an `"error"` event if the signal +-- | could not be delivered. +killSignal :: Signal -> UnsafeChildProcess -> Effect Boolean +killSignal sig cp = kill' (Signal.toString sig) cp + +killed :: UnsafeChildProcess -> Effect Boolean +killed cp = runEffectFn1 killedImpl cp + +foreign import killedImpl :: EffectFn1 (UnsafeChildProcess) (Boolean) + +signalCode :: UnsafeChildProcess -> Effect (Maybe String) +signalCode cp = map toMaybe $ runEffectFn1 signalCodeImpl cp + +foreign import signalCodeImpl :: EffectFn1 (UnsafeChildProcess) (Nullable String) + +foreign import spawnArgs :: UnsafeChildProcess -> Array String + +foreign import spawnFile :: UnsafeChildProcess -> String + +-- | Safe default configuration for an UnsafeChildProcess. +-- | `[ pipe, pipe, pipe, ipc ]`. +-- | Creates a new stream for `stdin`, `stdout`, and `stderr` +-- | Also adds an IPC channel, even if it's not used. +safeStdio :: Array StdIO +safeStdio = [ pipe, pipe, pipe, ipc ] diff --git a/src/Node/UnsafeChildProcess/Unsafe.js b/src/Node/UnsafeChildProcess/Unsafe.js new file mode 100644 index 0000000..ba39b8a --- /dev/null +++ b/src/Node/UnsafeChildProcess/Unsafe.js @@ -0,0 +1,29 @@ +export { + exec as execImpl, + exec as execOptsImpl, + exec as execCbImpl, + exec as execOptsCbImpl, + execFile as execFileImpl, + execFile as execFileOptsImpl, + execFile as execFileCbImpl, + execFile as execFileOptsCbImpl, + spawn as spawnImpl, + spawn as spawnOptsImpl, + execSync as execSyncImpl, + execFileSync as execFileSyncImpl, + execFileSync as execFileSyncOptsImpl, + spawnSync as spawnSyncImpl, + spawnSync as spawnSyncOptsImpl, + fork as forkImpl, + fork as forkOptsImpl, +} from "child_process"; + +export const unsafeStdin = (cp) => cp.stdin; +export const unsafeStdout = (cp) => cp.stdout; +export const unsafeStderr = (cp) => cp.stderr; +export const unsafeChannelRefImpl = (cp) => cp.channel.ref(); +export const unsafeChannelUnrefImpl = (cp) => cp.channel.unref(); +export const sendImpl = (cp, msg, handle) => cp.send(msg, handle); +export const sendOptsImpl = (cp, msg, handle, opts) => cp.send(msg, handle, opts); +export const sendCbImpl = (cp, msg, handle, cb) => cp.send(msg, handle, cb); +export const sendOptsCbImpl = (cp, msg, handle, opts, cb) => cp.send(msg, handle, opts, cb); diff --git a/src/Node/UnsafeChildProcess/Unsafe.purs b/src/Node/UnsafeChildProcess/Unsafe.purs new file mode 100644 index 0000000..51772cf --- /dev/null +++ b/src/Node/UnsafeChildProcess/Unsafe.purs @@ -0,0 +1,490 @@ +-- | Exposes low-level functions for ChildProcess +-- | where JavaScript values, rather than PureScript ones, +-- | are expected. +-- | +-- | All functions prefixed with `unsafe` indicate why they can be unsafe +-- | (i.e. produce a crash a runtime). All other functions +-- | are unsafe because their options (or default ones if not specified) +-- | can affect whether the `unsafe*` values/methods exist. +-- | +-- | All type aliases for options (e.g. `JsExecSyncOptions`) are well-typed. +module Node.UnsafeChildProcess.Unsafe + ( unsafeSOBToString + , unsafeSOBToBuffer + , unsafeStdin + , unsafeStdout + , unsafeStderr + , execSync + , JsExecSyncOptions + , execSync' + , exec + , JsExecOptions + , execOpts + , execCb + , execOptsCb + , execFileSync + , JsExecFileSyncOptions + , execFileSync' + , execFile + , JsExecFileOptions + , execFileOpts + , execFileCb + , execFileOptsCb + , JsSpawnSyncResult + , spawnSync + , JsSpawnSyncOptions + , spawnSync' + , spawn + , JsSpawnOptions + , spawn' + , fork + , JsForkOptions + , fork' + , unsafeSend + , JsSendOptions + , unsafeSendOpts + , unsafeSendCb + , unsafeSendOptsCb + , unsafeChannelRef + , unsafeChannelUnref + ) where + +import Prelude + +import Data.Maybe (Maybe) +import Data.Nullable (Nullable, toMaybe) +import Data.Posix (Gid, Pid, Uid) +import Data.Time.Duration (Milliseconds) +import Effect (Effect) +import Effect.Exception (Error) +import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, EffectFn5, mkEffectFn1, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3, runEffectFn4, runEffectFn5) +import Foreign (Foreign) +import Foreign.Object (Object) +import Node.Buffer (Buffer) +import Node.ChildProcess.Types (Handle, KillSignal, Shell, StdIO, StringOrBuffer, UnsafeChildProcess) +import Node.Errors.SystemError (SystemError) +import Node.Stream (Readable, Writable) +import Prim.Row as Row +import Unsafe.Coerce (unsafeCoerce) + +-- | Same as `unsafeCoerce`. No runtime checking is done to ensure +-- | the value is a `String`. +unsafeSOBToString :: StringOrBuffer -> String +unsafeSOBToString = unsafeCoerce + +-- | Same as `unsafeCoerce`. No runtime checking is done to ensure +-- | the value is a `Buffer`. +unsafeSOBToBuffer :: StringOrBuffer -> Buffer +unsafeSOBToBuffer = unsafeCoerce + +-- | Unsafe because it depends on what value was passed in via `stdio[0]` +foreign import unsafeStdin :: UnsafeChildProcess -> Nullable (Writable ()) +-- | Unsafe because it depends on what value was passed in via `stdio[1]` +foreign import unsafeStdout :: UnsafeChildProcess -> Nullable (Readable ()) +-- | Unsafe because it depends on what value was passed in via `stdio[2]` +foreign import unsafeStderr :: UnsafeChildProcess -> Nullable (Readable ()) + +execSync :: String -> Effect StringOrBuffer +execSync command = runEffectFn1 execSyncImpl command + +foreign import execSyncImpl :: EffectFn1 (String) (StringOrBuffer) + +-- | - `cwd` | Current working directory of the child process. +-- | - `input` | | | The value which will be passed as stdin to the spawned process. Supplying this value will override stdio[0]. +-- | - `stdio` | Child's stdio configuration. stderr by default will be output to the parent process' stderr unless stdio is specified. Default: 'pipe'. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `shell` Shell to execute the command with. See Shell requirements and Default Windows shell. Default: '/bin/sh' on Unix, process.env.ComSpec on Windows. +-- | - `uid` Sets the user identity of the process. (See setuid(2)). +-- | - `gid` Sets the group identity of the process. (See setgid(2)). +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed. Default: 'SIGTERM'. +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `encoding` The encoding used for all stdio inputs and outputs. Default: 'buffer'. +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +type JsExecSyncOptions = + ( cwd :: String + , input :: Buffer + , stdio :: Array StdIO + , env :: Object String + , shell :: String + , uid :: Uid + , gid :: Gid + , timeout :: Milliseconds + , killSignal :: KillSignal + , maxBuffer :: Number + , encoding :: String + , windowsHide :: Boolean + ) + +execSync' + :: forall r trash + . Row.Union r trash JsExecSyncOptions + => String + -> { | r } + -> Effect StringOrBuffer +execSync' command opts = runEffectFn2 execSyncOptsImpl command opts + +foreign import execSyncOptsImpl :: forall r. EffectFn2 (String) ({ | r }) (StringOrBuffer) + +exec :: String -> Effect UnsafeChildProcess +exec command = runEffectFn1 execImpl command + +foreign import execImpl :: EffectFn1 (String) (UnsafeChildProcess) + +-- | - `cwd` | Current working directory of the child process. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `encoding` Default: 'utf8' +-- | - `timeout` Default: 0 +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `killSignal` | Default: 'SIGTERM' +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +type JsExecOptions = + ( cwd :: String + , env :: Object String + , encoding :: String + , timeout :: Number + , maxBuffer :: Number + , killSignal :: KillSignal + , uid :: Uid + , gid :: Gid + , windowsHide :: Boolean + , shell :: Shell + ) + +execOpts + :: forall r trash + . Row.Union r trash JsExecOptions + => String + -> { | r } + -> Effect UnsafeChildProcess +execOpts command opts = runEffectFn2 execOptsImpl command opts + +foreign import execOptsImpl :: forall r. EffectFn2 (String) ({ | r }) (UnsafeChildProcess) + +execCb :: String -> (Maybe SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) -> Effect UnsafeChildProcess +execCb command cb = runEffectFn2 execCbImpl command $ mkEffectFn3 \err sout serr -> + cb (toMaybe err) sout serr + +foreign import execCbImpl :: EffectFn2 (String) (EffectFn3 (Nullable SystemError) StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) + +execOptsCb + :: forall r trash + . Row.Union r trash JsExecOptions + => String + -> { | r } + -> (Maybe SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) + -> Effect UnsafeChildProcess +execOptsCb command opts cb = runEffectFn3 execOptsCbImpl command opts $ mkEffectFn3 \err sout serr -> + cb (toMaybe err) sout serr + +foreign import execOptsCbImpl :: forall r. EffectFn3 (String) ({ | r }) (EffectFn3 (Nullable SystemError) StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) + +execFileSync :: String -> Array String -> Effect StringOrBuffer +execFileSync file args = runEffectFn2 execFileSyncImpl file args + +foreign import execFileSyncImpl :: EffectFn2 (String) (Array String) (StringOrBuffer) + +-- | - `cwd` | Current working directory of the child process. +-- | - `input` | | | The value which will be passed as stdin to the spawned process. Supplying this value will override stdio[0]. +-- | - `stdio` | Child's stdio configuration. stderr by default will be output to the parent process' stderr unless stdio is specified. Default: 'pipe'. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed. Default: 'SIGTERM'. +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `encoding` The encoding used for all stdio inputs and outputs. Default: 'buffer'. +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +type JsExecFileSyncOptions = + ( cwd :: String + , input :: Buffer + , stdio :: Array StdIO + , env :: Object String + , uid :: Uid + , gid :: Gid + , timeout :: Milliseconds + , killSignal :: KillSignal + , maxBuffer :: Number + , encoding :: String + , windowsHide :: Boolean + , shell :: Shell + ) + +execFileSync' + :: forall r trash + . Row.Union r trash JsExecFileSyncOptions + => String + -> Array String + -> { | r } + -> Effect StringOrBuffer +execFileSync' file args options = runEffectFn3 execFileSyncOptsImpl file args options + +foreign import execFileSyncOptsImpl :: forall r. EffectFn3 (String) (Array String) ({ | r }) (StringOrBuffer) + +execFile :: String -> Array String -> Effect UnsafeChildProcess +execFile file args = runEffectFn2 execFileImpl file args + +foreign import execFileImpl :: EffectFn2 (String) (Array String) (UnsafeChildProcess) + +-- | - `cwd` | Current working directory of the child process. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `encoding` Default: 'utf8' +-- | - `timeout` Default: 0 +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `killSignal` | Default: 'SIGTERM' +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +-- | - `windowsVerbatimArguments` No quoting or escaping of arguments is done on Windows. Ignored on Unix. Default: false. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +type JsExecFileOptions = + ( cwd :: String + , env :: Object String + , encoding :: String + , timeout :: Number + , maxBuffer :: Number + , killSignal :: KillSignal + , uid :: Uid + , gid :: Gid + , windowsHide :: Boolean + , windowsVerbatimArguments :: Boolean + , shell :: Shell + ) + +execFileOpts + :: forall r trash + . Row.Union r trash JsExecFileOptions + => String + -> Array String + -> { | r } + -> Effect UnsafeChildProcess +execFileOpts file args opts = runEffectFn3 execFileOptsImpl file args opts + +foreign import execFileOptsImpl :: forall r. EffectFn3 (String) (Array String) ({ | r }) (UnsafeChildProcess) + +execFileCb :: String -> Array String -> (SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) -> Effect UnsafeChildProcess +execFileCb file args cb = runEffectFn3 execFileCbImpl file args $ mkEffectFn3 cb + +foreign import execFileCbImpl :: EffectFn3 (String) (Array String) (EffectFn3 SystemError StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) + +execFileOptsCb + :: forall r trash + . Row.Union r trash JsExecFileOptions + => String + -> Array String + -> { | r } + -> (Maybe SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) + -> Effect UnsafeChildProcess +execFileOptsCb file args opts cb = runEffectFn4 execFileOptsCbImpl file args opts $ mkEffectFn3 \err sout serr -> + cb (toMaybe err) sout serr + +foreign import execFileOptsCbImpl :: forall r. EffectFn4 (String) (Array String) ({ | r }) (EffectFn3 (Nullable SystemError) StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) + +type JsSpawnSyncResult = + { pid :: Pid + , output :: Array Foreign + , stdout :: StringOrBuffer + , stderr :: StringOrBuffer + , status :: Nullable Int + , signal :: Nullable String + , error :: Nullable SystemError + } + +spawnSync :: String -> Array String -> Effect JsSpawnSyncResult +spawnSync command args = runEffectFn2 spawnSyncImpl command args + +foreign import spawnSyncImpl :: EffectFn2 (String) (Array String) (JsSpawnSyncResult) + +-- | - `cwd` | Current working directory of the child process. +-- | - `input` | | | The value which will be passed as stdin to the spawned process. Supplying this value will override stdio[0]. +-- | - `argv0` Explicitly set the value of argv[0] sent to the child process. This will be set to command if not specified. +-- | - `stdio` | Child's stdio configuration. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed. Default: 'SIGTERM'. +-- | - `maxBuffer` Largest amount of data in bytes allowed on stdout or stderr. If exceeded, the child process is terminated and any output is truncated. See caveat at maxBuffer and Unicode. Default: 1024 * 1024. +-- | - `encoding` The encoding used for all stdio inputs and outputs. Default: 'buffer'. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +-- | - `windowsVerbatimArguments` No quoting or escaping of arguments is done on Windows. Ignored on Unix. This is set to true automatically when shell is specified and is CMD. Default: false. +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +type JsSpawnSyncOptions = + ( cwd :: String + , input :: Buffer + , argv0 :: String + , stdio :: Array StdIO + , env :: Object String + , uid :: Uid + , gid :: Gid + , timeout :: Milliseconds + , killSignal :: KillSignal + , maxBuffer :: Number + , encoding :: String + , shell :: Shell + , windowsVerbatimArguments :: Boolean + , windowsHide :: Boolean + ) + +spawnSync' + :: forall r trash + . Row.Union r trash JsSpawnSyncOptions + => String + -> Array String + -> { | r } + -> Effect JsSpawnSyncResult +spawnSync' command args opts = runEffectFn3 spawnSyncOptsImpl command args opts + +foreign import spawnSyncOptsImpl :: forall r. EffectFn3 (String) (Array String) ({ | r }) (JsSpawnSyncResult) + +spawn :: String -> Array String -> Effect UnsafeChildProcess +spawn command args = runEffectFn2 spawnImpl command args + +foreign import spawnImpl :: EffectFn2 (String) (Array String) (UnsafeChildProcess) + +-- | - `cwd` | Current working directory of the child process. +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `argv0` Explicitly set the value of argv[0] sent to the child process. This will be set to command if not specified. +-- | - `stdio` | Child's stdio configuration (see options.stdio). +-- | - `detached` Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached). +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `serialization` Specify the kind of serialization used for sending messages between processes. Possible values are 'json' and 'advanced'. See Advanced serialization for more details. Default: 'json'. +-- | - `shell` | If true, runs command inside of a shell. Uses '/bin/sh' on Unix, and process.env.ComSpec on Windows. A different shell can be specified as a string. See Shell requirements and Default Windows shell. Default: false (no shell). +-- | - `windowsVerbatimArguments` No quoting or escaping of arguments is done on Windows. Ignored on Unix. This is set to true automatically when shell is specified and is CMD. Default: false. +-- | - `windowsHide` Hide the subprocess console window that would normally be created on Windows systems. Default: false. +-- | - `signal` allows aborting the child process using an AbortSignal. +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed by timeout or abort signal. Default: 'SIGTERM'. +type JsSpawnOptions = + ( cwd :: String + , env :: Object String + , argv0 :: String + , stdio :: Array StdIO + , detached :: Boolean + , uid :: Uid + , gid :: Gid + , serialization :: String + , shell :: Shell + , windowsVerbatimArguments :: Boolean + , windowsHide :: Boolean + , timeout :: Number + , killSignal :: KillSignal + ) + +spawn' + :: forall r trash + . Row.Union r trash JsSpawnOptions + => String + -> Array String + -> { | r } + -> Effect UnsafeChildProcess +spawn' command args opts = runEffectFn3 spawnOptsImpl command args opts + +foreign import spawnOptsImpl :: forall r. EffectFn3 (String) (Array String) ({ | r }) (UnsafeChildProcess) + +fork :: String -> Array String -> Effect UnsafeChildProcess +fork modulePath args = runEffectFn2 forkImpl modulePath args + +foreign import forkImpl :: EffectFn2 (String) (Array String) (UnsafeChildProcess) + +-- | - `cwd` | Current working directory of the child process. +-- | - `detached` Prepare child to run independently of its parent process. Specific behavior depends on the platform, see options.detached). +-- | - `env` Environment key-value pairs. Default: process.env. +-- | - `execPath` Executable used to create the child process. +-- | - `execArgv` List of string arguments passed to the executable. Default: process.execArgv. +-- | - `gid` Sets the group identity of the process (see setgid(2)). +-- | - `serialization` Specify the kind of serialization used for sending messages between processes. Possible values are 'json' and 'advanced'. See Advanced serialization for more details. Default: 'json'. +-- | - `signal` Allows closing the child process using an AbortSignal. +-- | - `killSignal` | The signal value to be used when the spawned process will be killed by timeout or abort signal. Default: 'SIGTERM'. +-- | - `silent` If true, stdin, stdout, and stderr of the child will be piped to the parent, otherwise they will be inherited from the parent, see the 'pipe' and 'inherit' options for child_process.spawn()'s stdio for more details. Default: false. +-- | - `stdio` | See child_process.spawn()'s stdio. When this option is provided, it overrides silent. If the array variant is used, it must contain exactly one item with value 'ipc' or an error will be thrown. For instance [0, 1, 2, 'ipc']. +-- | - `uid` Sets the user identity of the process (see setuid(2)). +-- | - `windowsVerbatimArguments` No quoting or escaping of arguments is done on Windows. Ignored on Unix. Default: false. +-- | - `timeout` In milliseconds the maximum amount of time the process is allowed to run. Default: undefined. +type JsForkOptions = + ( cwd :: String + , detached :: Boolean + , env :: Object String + , execPath :: String + , execArgv :: Array String + , gid :: Gid + , serialization :: String + , killSignal :: KillSignal + , silent :: Boolean + , stdio :: Array StdIO + , uid :: Uid + , windowsVerbatimArguments :: Boolean + , timeout :: Milliseconds + ) + +fork' + :: forall r trash + . Row.Union r trash JsForkOptions + => String + -> Array String + -> { | r } + -> Effect UnsafeChildProcess +fork' modulePath args opts = runEffectFn3 forkOptsImpl modulePath args opts + +foreign import forkOptsImpl :: forall r. EffectFn3 (String) (Array String) { | r } (UnsafeChildProcess) + +-- | Unsafe because child process must be a Node child process and an IPC channel must exist. +unsafeSend :: forall messageRows. { | messageRows } -> Nullable Handle -> UnsafeChildProcess -> Effect Boolean +unsafeSend msg handle cp = runEffectFn3 sendImpl cp msg handle + +foreign import sendImpl :: forall messageRows. EffectFn3 (UnsafeChildProcess) ({ | messageRows }) (Nullable Handle) (Boolean) + +-- | - `keepAlive` A value that can be used when passing instances of `net.Socket` as the `Handle`. When true, the socket is kept open in the sending process. Default: false. +type JsSendOptions = + ( keepAlive :: Boolean + ) + +-- | Unsafe because child process must be a Node child process and an IPC channel must exist. +unsafeSendOpts + :: forall r trash messageRows + . Row.Union r trash JsSendOptions + => { | messageRows } + -> Nullable Handle + -> { | r } + -> UnsafeChildProcess + -> Effect Boolean +unsafeSendOpts msg handle opts cp = runEffectFn4 sendOptsImpl cp msg handle opts + +foreign import sendOptsImpl :: forall messageRows r. EffectFn4 (UnsafeChildProcess) ({ | messageRows }) (Nullable Handle) ({ | r }) (Boolean) + +-- | Unsafe because child process must be a Node child process and an IPC channel must exist. +unsafeSendCb :: forall messageRows. { | messageRows } -> Nullable Handle -> (Maybe Error -> Effect Unit) -> UnsafeChildProcess -> Effect Boolean +unsafeSendCb msg handle cb cp = runEffectFn4 sendCbImpl cp msg handle $ mkEffectFn1 \err -> cb $ toMaybe err + +foreign import sendCbImpl :: forall messageRows. EffectFn4 (UnsafeChildProcess) ({ | messageRows }) (Nullable Handle) (EffectFn1 (Nullable Error) Unit) (Boolean) + +-- | Unsafe because child process must be a Node child process and an IPC channel must exist. +unsafeSendOptsCb + :: forall r trash messageRows + . Row.Union r trash JsSendOptions + => { | messageRows } + -> Nullable Handle + -> { | r } + -> (Maybe Error -> Effect Unit) + -> UnsafeChildProcess + -> Effect Boolean +unsafeSendOptsCb msg handle opts cb cp = runEffectFn5 sendOptsCbImpl cp msg handle opts $ mkEffectFn1 \err -> cb $ toMaybe err + +foreign import sendOptsCbImpl :: forall messageRows r. EffectFn5 (UnsafeChildProcess) ({ | messageRows }) (Nullable Handle) ({ | r }) (EffectFn1 (Nullable Error) Unit) (Boolean) + +-- | Unsafe because it depends on whether an IPC channel exists. +unsafeChannelRef :: UnsafeChildProcess -> Effect Unit +unsafeChannelRef cp = runEffectFn1 unsafeChannelRefImpl cp + +foreign import unsafeChannelRefImpl :: EffectFn1 (UnsafeChildProcess) (Unit) + +-- | Unsafe because it depends on whether an IPC channel exists. +unsafeChannelUnref :: UnsafeChildProcess -> Effect Unit +unsafeChannelUnref cp = runEffectFn1 unsafeChannelUnrefImpl cp + +foreign import unsafeChannelUnrefImpl :: EffectFn1 (UnsafeChildProcess) (Unit) diff --git a/test/Main.purs b/test/Main.purs index a61c408..5b71f49 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -4,11 +4,13 @@ import Prelude import Data.Maybe (Maybe(..)) import Data.Posix.Signal (Signal(..)) +import Data.Posix.Signal as Signal import Effect (Effect) import Effect.Console (log) import Node.Buffer as Buffer -import Node.ChildProcess (Exit(..), defaultExecOptions, defaultExecSyncOptions, defaultSpawnOptions, errorH, exec, execSync, exitH, kill, spawn, stdout) -import Node.Encoding (Encoding(UTF8)) +import Node.ChildProcess (errorH, exec', execSync', exitH, kill, spawn, stdout) +import Node.ChildProcess.Types (Exit(..)) +import Node.Encoding (Encoding(..)) import Node.Encoding as NE import Node.Errors.SystemError (code) import Node.EventEmitter (on_) @@ -24,7 +26,7 @@ main = do log "nonexistent executable: all good." log "doesn't perform effects too early" - spawn "ls" [ "-la" ] defaultSpawnOptions >>= \ls -> do + spawn "ls" [ "-la" ] >>= \ls -> do let _ = kill ls ls # on_ exitH \exit -> case exit of @@ -34,11 +36,11 @@ main = do log ("Bad exit: expected `Normally 0`, got: " <> show exit) log "kills processes" - spawn "ls" [ "-la" ] defaultSpawnOptions >>= \ls -> do + spawn "ls" [ "-la" ] >>= \ls -> do _ <- kill ls ls # on_ exitH \exit -> case exit of - BySignal SIGTERM -> + BySignal s | Just SIGTERM <- Signal.fromString s -> log "All good!" _ -> do log ("Bad exit: expected `BySignal SIGTERM`, got: " <> show exit) @@ -48,26 +50,27 @@ main = do spawnLs :: Effect Unit spawnLs = do - ls <- spawn "ls" [ "-la" ] defaultSpawnOptions + ls <- spawn "ls" [ "-la" ] ls # on_ exitH \exit -> log $ "ls exited: " <> show exit (stdout ls) # on_ dataH (Buffer.toString UTF8 >=> log) nonExistentExecutable :: Effect Unit -> Effect Unit nonExistentExecutable done = do - ch <- spawn "this-does-not-exist" [] defaultSpawnOptions + ch <- spawn "this-does-not-exist" [] ch # on_ errorH \err -> log (code err) *> done execLs :: Effect Unit execLs = do -- returned ChildProcess is ignored here - _ <- exec "ls >&2" defaultExecOptions \r -> + _ <- exec' "ls >&2" identity \r -> log "redirected to stderr:" *> (Buffer.toString UTF8 r.stderr >>= log) pure unit execSyncEcho :: String -> Effect Unit execSyncEcho str = do - resBuf <- execSync "cat" (defaultExecSyncOptions { input = Just str }) + buf <- Buffer.fromString str UTF8 + resBuf <- execSync' "cat" (_ { input = Just buf }) res <- Buffer.toString NE.UTF8 resBuf log res