From 4678ed07713589e7add0670419bc0ae7dea2385d Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 09:02:36 -0700 Subject: [PATCH 01/14] Add unsafe API --- CHANGELOG.md | 3 +- src/Node/ChildProcess.purs | 12 +- src/Node/ChildProcess/Types.purs | 79 +++++ src/Node/ChildProcess/Unsafe.js | 29 ++ src/Node/ChildProcess/Unsafe.purs | 473 ++++++++++++++++++++++++++++++ test/Main.purs | 3 +- 6 files changed, 586 insertions(+), 13 deletions(-) create mode 100644 src/Node/ChildProcess/Types.purs create mode 100644 src/Node/ChildProcess/Unsafe.js create mode 100644 src/Node/ChildProcess/Unsafe.purs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b61503..77dd56d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ 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) - +- Moved `Exit` type from `Node.ChildProces` to ``Node.ChildProces.Types` (#46 by @JordanMartinez) New features: - Added event handler for `spawn` event (#43 by @JordanMartinez) @@ -36,6 +36,7 @@ New features: - signalCode - spawnArgs - spawnFile +- Added unsafe, uncurried API of all ChildProcess-creating functions (#46 by @JordanMartinez) Bugfixes: diff --git a/src/Node/ChildProcess.purs b/src/Node/ChildProcess.purs index 3e90a4b..3f4dd08 100644 --- a/src/Node/ChildProcess.purs +++ b/src/Node/ChildProcess.purs @@ -34,7 +34,6 @@ module Node.ChildProcess , killed , signalCode , send - , Exit(..) , spawn , SpawnOptions , defaultSpawnOptions @@ -68,6 +67,7 @@ import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2, runEffe import Foreign (Foreign) import Foreign.Object (Object) import Node.Buffer (Buffer) +import Node.ChildProcess.Types (Exit(..)) import Node.Encoding (Encoding, encodingToNode) import Node.Errors.SystemError (SystemError) import Node.EventEmitter (EventEmitter, EventHandle(..)) @@ -229,16 +229,6 @@ foreign import spawnFile :: ChildProcess -> String mkEffect :: forall a. (Unit -> a) -> Effect a mkEffect = unsafeCoerce --- | Specifies how a child process exited; normally (with an exit code), or --- | due to a signal. -data Exit - = Normally Int - | BySignal Signal - -instance showExit :: Show Exit where - show (Normally x) = "Normally " <> show x - show (BySignal sig) = "BySignal " <> show sig - -- | 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 -- | not throw an error. Instead, the `ChildProcess` will be created anyway, diff --git a/src/Node/ChildProcess/Types.purs b/src/Node/ChildProcess/Types.purs new file mode 100644 index 0000000..348f8fb --- /dev/null +++ b/src/Node/ChildProcess/Types.purs @@ -0,0 +1,79 @@ +module Node.ChildProcess.Types where + +import Prelude + +import Data.Nullable (Nullable, null) +import Data.Posix.Signal (Signal) +import Node.EventEmitter (EventEmitter) +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 + +toEventEmitter :: UnsafeChildProcess -> EventEmitter +toEventEmitter = unsafeCoerce + +-- | 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 Signal + +instance showExit :: Show Exit where + show (Normally x) = "Normally " <> show x + show (BySignal sig) = "BySignal " <> show sig diff --git a/src/Node/ChildProcess/Unsafe.js b/src/Node/ChildProcess/Unsafe.js new file mode 100644 index 0000000..e2c00e0 --- /dev/null +++ b/src/Node/ChildProcess/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 stdin = (cp) => cp.stdin; +export const stdout = (cp) => cp.stdout; +export const stderr = (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/ChildProcess/Unsafe.purs b/src/Node/ChildProcess/Unsafe.purs new file mode 100644 index 0000000..b8f465a --- /dev/null +++ b/src/Node/ChildProcess/Unsafe.purs @@ -0,0 +1,473 @@ +-- | 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. `ExecSyncOptions`) are well-typed. +module Node.UnsafeChildProcess.Unsafe + ( unsafeSOBToString + , unsafeSOBToBuffer + , unsafeStdin + , unsafeStdout + , unsafeStderr + , execSync + , ExecSyncOptions + , execSyncOpts + , exec + , ExecOptions + , execOpts + , execCb + , execOptsCb + , execFileSync + , ExecFileSyncOptions + , execFileSync' + , execFile + , ExecFileOptions + , execFileOpts + , execFileCb + , execFileOptsCb + , SpawnSyncResult + , spawnSync + , SpawnSyncOptions + , spawnSync' + , spawn + , SpawnOptions + , spawn' + , fork + , ForkOptions + , fork' + , unsafeSend + , SendOptions + , 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 (Handle) +import Node.ChildProcess.Types (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 ExecSyncOptions = + ( 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 + ) + +execSyncOpts + :: forall r trash + . Row.Union r trash ExecSyncOptions + => String + -> { | r } + -> Effect StringOrBuffer +execSyncOpts 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 ExecOptions = + ( 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 ExecOptions + => String + -> { | r } + -> Effect UnsafeChildProcess +execOpts command opts = runEffectFn2 execOptsImpl command opts + +foreign import execOptsImpl :: forall r. EffectFn2 (String) ({ | r }) (UnsafeChildProcess) + +execCb :: String -> (SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) -> Effect UnsafeChildProcess +execCb command cb = runEffectFn2 execCbImpl command $ mkEffectFn3 cb + +foreign import execCbImpl :: EffectFn2 (String) (EffectFn3 SystemError StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) + +execOptsCb + :: forall r trash + . Row.Union r trash ExecOptions + => String + -> { | r } + -> (SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) + -> Effect UnsafeChildProcess +execOptsCb command opts cb = runEffectFn3 execOptsCbImpl command opts $ mkEffectFn3 cb + +foreign import execOptsCbImpl :: forall r. EffectFn3 (String) ({ | r }) (EffectFn3 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 ExecFileSyncOptions = + ( 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 ExecFileSyncOptions + => 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 ExecFileOptions = + ( 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 ExecFileOptions + => 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 ExecFileOptions + => String + -> Array String + -> { | r } + -> (SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) + -> Effect UnsafeChildProcess +execFileOptsCb file args opts cb = runEffectFn4 execFileOptsCbImpl file args opts $ mkEffectFn3 cb + +foreign import execFileOptsCbImpl :: forall r. EffectFn4 (String) (Array String) ({ | r }) (EffectFn3 SystemError StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) + +type SpawnSyncResult = + { pid :: Pid + , output :: Array Foreign + , stdout :: StringOrBuffer + , stderr :: StringOrBuffer + , status :: Nullable Int + , signal :: Nullable String + , error :: SystemError + } + +spawnSync :: String -> Array String -> Effect SpawnSyncResult +spawnSync command args = runEffectFn2 spawnSyncImpl command args + +foreign import spawnSyncImpl :: EffectFn2 (String) (Array String) (SpawnSyncResult) + +-- | - `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 SpawnSyncOptions = + ( 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 SpawnSyncOptions + => String + -> Array String + -> { | r } + -> Effect SpawnSyncResult +spawnSync' command args opts = runEffectFn3 spawnSyncOptsImpl command args opts + +foreign import spawnSyncOptsImpl :: forall r. EffectFn3 (String) (Array String) ({ | r }) (SpawnSyncResult) + +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 SpawnOptions = + ( 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 SpawnOptions + => 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) + +type ForkOptions = + ( 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 ForkOptions + => 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 :: Object Foreign -> Nullable Handle -> UnsafeChildProcess -> Effect Boolean +unsafeSend msg handle cp = runEffectFn3 sendImpl cp msg handle + +foreign import sendImpl :: EffectFn3 (UnsafeChildProcess) (Object Foreign) (Nullable Handle) (Boolean) + +type SendOptions = + ( keepAlive :: Boolean + ) + +-- | Unsafe because child process must be a Node child process and an IPC channel must exist. +unsafeSendOpts + :: forall r trash + . Row.Union r trash SendOptions + => Object Foreign + -> Nullable Handle + -> { | r } + -> UnsafeChildProcess + -> Effect Boolean +unsafeSendOpts msg handle opts cp = runEffectFn4 sendOptsImpl cp msg handle opts + +foreign import sendOptsImpl :: forall r. EffectFn4 (UnsafeChildProcess) (Object Foreign) (Nullable Handle) ({ | r }) (Boolean) + +-- | Unsafe because child process must be a Node child process and an IPC channel must exist. +unsafeSendCb :: Object Foreign -> 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 :: EffectFn4 (UnsafeChildProcess) (Object Foreign) (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 + . Row.Union r trash SendOptions + => Object Foreign + -> 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 r. EffectFn5 (UnsafeChildProcess) (Object Foreign) (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..909ca0c 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -7,7 +7,8 @@ import Data.Posix.Signal (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.ChildProcess (defaultExecOptions, defaultExecSyncOptions, defaultSpawnOptions, errorH, exec, execSync, exitH, kill, spawn, stdout) +import Node.ChildProcess.Types (Exit(..)) import Node.Encoding (Encoding(UTF8)) import Node.Encoding as NE import Node.Errors.SystemError (code) From 2a02930bdaf8705bacd2a91e548d7f52fa84555f Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 09:04:23 -0700 Subject: [PATCH 02/14] Move Handle into types --- CHANGELOG.md | 5 ++++- src/Node/ChildProcess.purs | 8 ++------ src/Node/ChildProcess/Types.purs | 3 +++ src/Node/ChildProcess/Unsafe.purs | 3 +-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77dd56d..2585c9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,10 @@ 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) -- Moved `Exit` type from `Node.ChildProces` to ``Node.ChildProces.Types` (#46 by @JordanMartinez) +- Moved types from `Node.ChildProces` to ``Node.ChildProces.Types` (#46 by @JordanMartinez) + + - `Exit(Normally, BySignal)` + - `Handle` New features: - Added event handler for `spawn` event (#43 by @JordanMartinez) diff --git a/src/Node/ChildProcess.purs b/src/Node/ChildProcess.purs index 3f4dd08..71aa192 100644 --- a/src/Node/ChildProcess.purs +++ b/src/Node/ChildProcess.purs @@ -12,8 +12,7 @@ -- | forms the basis for this module and has in-depth documentation about -- | runtime behaviour. module Node.ChildProcess - ( Handle - , ChildProcess + ( ChildProcess , toEventEmitter , closeH , disconnectH @@ -67,7 +66,7 @@ import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2, runEffe import Foreign (Foreign) import Foreign.Object (Object) import Node.Buffer (Buffer) -import Node.ChildProcess.Types (Exit(..)) +import Node.ChildProcess.Types (Exit(..), Handle) import Node.Encoding (Encoding, encodingToNode) import Node.Errors.SystemError (SystemError) import Node.EventEmitter (EventEmitter, EventHandle(..)) @@ -77,9 +76,6 @@ import Node.Stream (Readable, Stream, Writable) import Partial.Unsafe (unsafeCrashWith) 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. -- | diff --git a/src/Node/ChildProcess/Types.purs b/src/Node/ChildProcess/Types.purs index 348f8fb..5835f17 100644 --- a/src/Node/ChildProcess/Types.purs +++ b/src/Node/ChildProcess/Types.purs @@ -18,6 +18,9 @@ foreign import data UnsafeChildProcess :: Type toEventEmitter :: UnsafeChildProcess -> EventEmitter toEventEmitter = unsafeCoerce +-- | 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 diff --git a/src/Node/ChildProcess/Unsafe.purs b/src/Node/ChildProcess/Unsafe.purs index b8f465a..cb648e4 100644 --- a/src/Node/ChildProcess/Unsafe.purs +++ b/src/Node/ChildProcess/Unsafe.purs @@ -61,8 +61,7 @@ import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, EffectFn5, import Foreign (Foreign) import Foreign.Object (Object) import Node.Buffer (Buffer) -import Node.ChildProcess (Handle) -import Node.ChildProcess.Types (KillSignal, Shell, StdIO, StringOrBuffer, UnsafeChildProcess) +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 From cd23ec78626e71bb870a227436b803d255dcc5bd Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 09:20:24 -0700 Subject: [PATCH 03/14] Move Unsafe to new namespace --- src/Node/{ChildProcess => UnsafeChildProcess}/Unsafe.js | 0 src/Node/{ChildProcess => UnsafeChildProcess}/Unsafe.purs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/Node/{ChildProcess => UnsafeChildProcess}/Unsafe.js (100%) rename src/Node/{ChildProcess => UnsafeChildProcess}/Unsafe.purs (100%) diff --git a/src/Node/ChildProcess/Unsafe.js b/src/Node/UnsafeChildProcess/Unsafe.js similarity index 100% rename from src/Node/ChildProcess/Unsafe.js rename to src/Node/UnsafeChildProcess/Unsafe.js diff --git a/src/Node/ChildProcess/Unsafe.purs b/src/Node/UnsafeChildProcess/Unsafe.purs similarity index 100% rename from src/Node/ChildProcess/Unsafe.purs rename to src/Node/UnsafeChildProcess/Unsafe.purs From 49f327fbf2a44eba107d59afcf477bce27a6a437 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 09:21:37 -0700 Subject: [PATCH 04/14] Add Safe API; reimplement CP using it --- src/Node/ChildProcess.js | 11 --- src/Node/ChildProcess.purs | 80 ++++++---------- src/Node/ChildProcess/Types.purs | 4 - src/Node/UnsafeChildProcess/Safe.js | 10 ++ src/Node/UnsafeChildProcess/Safe.purs | 129 ++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 66 deletions(-) create mode 100644 src/Node/UnsafeChildProcess/Safe.js create mode 100644 src/Node/UnsafeChildProcess/Safe.purs diff --git a/src/Node/ChildProcess.js b/src/Node/ChildProcess.js index d621449..e5a11ca 100644 --- a/src/Node/ChildProcess.js +++ b/src/Node/ChildProcess.js @@ -9,17 +9,6 @@ export function unsafeFromNullable(msg) { }; } -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); } diff --git a/src/Node/ChildProcess.purs b/src/Node/ChildProcess.purs index 71aa192..7a2b47b 100644 --- a/src/Node/ChildProcess.purs +++ b/src/Node/ChildProcess.purs @@ -32,6 +32,8 @@ module Node.ChildProcess , killSignal , killed , signalCode + , spawnFile + , spawnArgs , send , spawn , SpawnOptions @@ -59,21 +61,20 @@ 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 Effect (Effect) import Effect.Exception as Exception -import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2, runEffectFn1, runEffectFn2) +import Effect.Uncurried (EffectFn2) import Foreign (Foreign) import Foreign.Object (Object) import Node.Buffer (Buffer) -import Node.ChildProcess.Types (Exit(..), Handle) +import Node.ChildProcess.Types (Exit, Handle, UnsafeChildProcess) import Node.Encoding (Encoding, encodingToNode) 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 Partial.Unsafe (unsafeCrashWith) +import Node.UnsafeChildProcess.Safe as SafeCP import Unsafe.Coerce (unsafeCoerce) -- | Opaque type returned by `spawn`, `fork` and `exec`. @@ -83,33 +84,28 @@ import Unsafe.Coerce (unsafeCoerce) newtype ChildProcess = ChildProcess ChildProcessRec toEventEmitter :: ChildProcess -> EventEmitter -toEventEmitter = unsafeCoerce +toEventEmitter = toUnsafeChildProcess >>> SafeCP.toEventEmitter + +toUnsafeChildProcess :: ChildProcess -> UnsafeChildProcess +toUnsafeChildProcess = unsafeCoerce 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 +spawnH = unsafeCoerce SafeCP.spawnH runChildProcess :: ChildProcess -> ChildProcessRec runChildProcess (ChildProcess r) = r @@ -120,11 +116,7 @@ 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 } -- | The standard input stream of a child process. Note that this is only @@ -153,21 +145,15 @@ foreign import unsafeFromNullable :: forall a. String -> Nullable a -> a -- | 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 :: 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) +exitCode = unsafeCoerce SafeCP.exitCode -- | Send messages to the (`nodejs`) child process. -- | @@ -183,19 +169,13 @@ send msg handle (ChildProcess cp) = mkEffect \_ -> runFn2 cp.send msg handle -- | 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), @@ -206,21 +186,19 @@ 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 - -foreign import signalCodeImpl :: EffectFn1 (ChildProcess) (Nullable String) - -foreign import killedImpl :: EffectFn1 (ChildProcess) (Boolean) +signalCode = unsafeCoerce SafeCP.signalCode -foreign import spawnArgs :: ChildProcess -> Array String +spawnArgs :: ChildProcess -> Array String +spawnArgs = unsafeCoerce SafeCP.spawnArgs -foreign import spawnFile :: ChildProcess -> String +spawnFile :: ChildProcess -> String +spawnFile = unsafeCoerce SafeCP.spawnFile mkEffect :: forall a. (Unit -> a) -> Effect a mkEffect = unsafeCoerce diff --git a/src/Node/ChildProcess/Types.purs b/src/Node/ChildProcess/Types.purs index 5835f17..62abc92 100644 --- a/src/Node/ChildProcess/Types.purs +++ b/src/Node/ChildProcess/Types.purs @@ -4,7 +4,6 @@ import Prelude import Data.Nullable (Nullable, null) import Data.Posix.Signal (Signal) -import Node.EventEmitter (EventEmitter) import Node.FS (FileDescriptor) import Node.Stream (Stream) import Unsafe.Coerce (unsafeCoerce) @@ -15,9 +14,6 @@ import Unsafe.Coerce (unsafeCoerce) -- | (e.g. `stdio`, `fork`) exist. foreign import data UnsafeChildProcess :: Type -toEventEmitter :: UnsafeChildProcess -> EventEmitter -toEventEmitter = unsafeCoerce - -- | A handle for inter-process communication (IPC). foreign import data Handle :: Type 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..315ad1d --- /dev/null +++ b/src/Node/UnsafeChildProcess/Safe.purs @@ -0,0 +1,129 @@ +-- | 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 + ) 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, UnsafeChildProcess) +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 >>= 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 + +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 >>= 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 + +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 From 7a27fa8a269a741c8f828ba05d3a85843d206b33 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 09:22:25 -0700 Subject: [PATCH 05/14] Relocate send --- src/Node/ChildProcess.purs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Node/ChildProcess.purs b/src/Node/ChildProcess.purs index 7a2b47b..de021a4 100644 --- a/src/Node/ChildProcess.purs +++ b/src/Node/ChildProcess.purs @@ -155,18 +155,6 @@ connected = unsafeCoerce SafeCP.connected exitCode :: ChildProcess -> Effect (Maybe Int) exitCode = unsafeCoerce SafeCP.exitCode --- | 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 - -- | Closes the IPC channel between parent and child. disconnect :: ChildProcess -> Effect Unit disconnect = unsafeCoerce SafeCP.disconnect @@ -191,6 +179,18 @@ killSignal = unsafeCoerce SafeCP.killSignal killed :: ChildProcess -> Effect Boolean killed = unsafeCoerce SafeCP.killed +-- | 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 + signalCode :: ChildProcess -> Effect (Maybe String) signalCode = unsafeCoerce SafeCP.signalCode From bfed53e7781b5d427e1fc4816f02d34868823a1e Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 09:31:45 -0700 Subject: [PATCH 06/14] Add Js prefix to all Js options --- src/Node/UnsafeChildProcess/Unsafe.purs | 68 ++++++++++++------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/Node/UnsafeChildProcess/Unsafe.purs b/src/Node/UnsafeChildProcess/Unsafe.purs index cb648e4..4a1db53 100644 --- a/src/Node/UnsafeChildProcess/Unsafe.purs +++ b/src/Node/UnsafeChildProcess/Unsafe.purs @@ -7,7 +7,7 @@ -- | 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. `ExecSyncOptions`) are well-typed. +-- | All type aliases for options (e.g. `JsExecSyncOptions`) are well-typed. module Node.UnsafeChildProcess.Unsafe ( unsafeSOBToString , unsafeSOBToBuffer @@ -15,33 +15,33 @@ module Node.UnsafeChildProcess.Unsafe , unsafeStdout , unsafeStderr , execSync - , ExecSyncOptions + , JsExecSyncOptions , execSyncOpts , exec - , ExecOptions + , JsExecOptions , execOpts , execCb , execOptsCb , execFileSync - , ExecFileSyncOptions + , JsExecFileSyncOptions , execFileSync' , execFile - , ExecFileOptions + , JsExecFileOptions , execFileOpts , execFileCb , execFileOptsCb - , SpawnSyncResult + , JsSpawnSyncResult , spawnSync - , SpawnSyncOptions + , JsSpawnSyncOptions , spawnSync' , spawn - , SpawnOptions + , JsSpawnOptions , spawn' , fork - , ForkOptions + , JsForkOptions , fork' , unsafeSend - , SendOptions + , JsSendOptions , unsafeSendOpts , unsafeSendCb , unsafeSendOptsCb @@ -101,7 +101,7 @@ foreign import execSyncImpl :: EffectFn1 (String) (StringOrBuffer) -- | - `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 = +type JsExecSyncOptions = ( cwd :: String , input :: Buffer , stdio :: Array StdIO @@ -118,7 +118,7 @@ type ExecSyncOptions = execSyncOpts :: forall r trash - . Row.Union r trash ExecSyncOptions + . Row.Union r trash JsExecSyncOptions => String -> { | r } -> Effect StringOrBuffer @@ -141,7 +141,7 @@ foreign import execImpl :: EffectFn1 (String) (UnsafeChildProcess) -- | - `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 = +type JsExecOptions = ( cwd :: String , env :: Object String , encoding :: String @@ -156,7 +156,7 @@ type ExecOptions = execOpts :: forall r trash - . Row.Union r trash ExecOptions + . Row.Union r trash JsExecOptions => String -> { | r } -> Effect UnsafeChildProcess @@ -171,7 +171,7 @@ foreign import execCbImpl :: EffectFn2 (String) (EffectFn3 SystemError StringOrB execOptsCb :: forall r trash - . Row.Union r trash ExecOptions + . Row.Union r trash JsExecOptions => String -> { | r } -> (SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) @@ -197,7 +197,7 @@ foreign import execFileSyncImpl :: EffectFn2 (String) (Array String) (StringOrBu -- | - `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 ExecFileSyncOptions = +type JsExecFileSyncOptions = ( cwd :: String , input :: Buffer , stdio :: Array StdIO @@ -214,7 +214,7 @@ type ExecFileSyncOptions = execFileSync' :: forall r trash - . Row.Union r trash ExecFileSyncOptions + . Row.Union r trash JsExecFileSyncOptions => String -> Array String -> { | r } @@ -239,7 +239,7 @@ foreign import execFileImpl :: EffectFn2 (String) (Array String) (UnsafeChildPro -- | - `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 = +type JsExecFileOptions = ( cwd :: String , env :: Object String , encoding :: String @@ -255,7 +255,7 @@ type ExecFileOptions = execFileOpts :: forall r trash - . Row.Union r trash ExecFileOptions + . Row.Union r trash JsExecFileOptions => String -> Array String -> { | r } @@ -271,7 +271,7 @@ foreign import execFileCbImpl :: EffectFn3 (String) (Array String) (EffectFn3 Sy execFileOptsCb :: forall r trash - . Row.Union r trash ExecFileOptions + . Row.Union r trash JsExecFileOptions => String -> Array String -> { | r } @@ -281,7 +281,7 @@ execFileOptsCb file args opts cb = runEffectFn4 execFileOptsCbImpl file args opt foreign import execFileOptsCbImpl :: forall r. EffectFn4 (String) (Array String) ({ | r }) (EffectFn3 SystemError StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) -type SpawnSyncResult = +type JsSpawnSyncResult = { pid :: Pid , output :: Array Foreign , stdout :: StringOrBuffer @@ -291,10 +291,10 @@ type SpawnSyncResult = , error :: SystemError } -spawnSync :: String -> Array String -> Effect SpawnSyncResult +spawnSync :: String -> Array String -> Effect JsSpawnSyncResult spawnSync command args = runEffectFn2 spawnSyncImpl command args -foreign import spawnSyncImpl :: EffectFn2 (String) (Array String) (SpawnSyncResult) +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]. @@ -310,7 +310,7 @@ foreign import spawnSyncImpl :: EffectFn2 (String) (Array String) (SpawnSyncResu -- | - `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 = +type JsSpawnSyncOptions = ( cwd :: String , input :: Buffer , argv0 :: String @@ -329,14 +329,14 @@ type SpawnSyncOptions = spawnSync' :: forall r trash - . Row.Union r trash SpawnSyncOptions + . Row.Union r trash JsSpawnSyncOptions => String -> Array String -> { | r } - -> Effect SpawnSyncResult + -> Effect JsSpawnSyncResult spawnSync' command args opts = runEffectFn3 spawnSyncOptsImpl command args opts -foreign import spawnSyncOptsImpl :: forall r. EffectFn3 (String) (Array String) ({ | r }) (SpawnSyncResult) +foreign import spawnSyncOptsImpl :: forall r. EffectFn3 (String) (Array String) ({ | r }) (JsSpawnSyncResult) spawn :: String -> Array String -> Effect UnsafeChildProcess spawn command args = runEffectFn2 spawnImpl command args @@ -357,7 +357,7 @@ foreign import spawnImpl :: EffectFn2 (String) (Array String) (UnsafeChildProces -- | - `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 = +type JsSpawnOptions = ( cwd :: String , env :: Object String , argv0 :: String @@ -375,7 +375,7 @@ type SpawnOptions = spawn' :: forall r trash - . Row.Union r trash SpawnOptions + . Row.Union r trash JsSpawnOptions => String -> Array String -> { | r } @@ -389,7 +389,7 @@ fork modulePath args = runEffectFn2 forkImpl modulePath args foreign import forkImpl :: EffectFn2 (String) (Array String) (UnsafeChildProcess) -type ForkOptions = +type JsForkOptions = ( cwd :: String , detached :: Boolean , env :: Object String @@ -407,7 +407,7 @@ type ForkOptions = fork' :: forall r trash - . Row.Union r trash ForkOptions + . Row.Union r trash JsForkOptions => String -> Array String -> { | r } @@ -422,14 +422,14 @@ unsafeSend msg handle cp = runEffectFn3 sendImpl cp msg handle foreign import sendImpl :: EffectFn3 (UnsafeChildProcess) (Object Foreign) (Nullable Handle) (Boolean) -type SendOptions = +type JsSendOptions = ( keepAlive :: Boolean ) -- | Unsafe because child process must be a Node child process and an IPC channel must exist. unsafeSendOpts :: forall r trash - . Row.Union r trash SendOptions + . Row.Union r trash JsSendOptions => Object Foreign -> Nullable Handle -> { | r } @@ -448,7 +448,7 @@ foreign import sendCbImpl :: EffectFn4 (UnsafeChildProcess) (Object Foreign) (Nu -- | Unsafe because child process must be a Node child process and an IPC channel must exist. unsafeSendOptsCb :: forall r trash - . Row.Union r trash SendOptions + . Row.Union r trash JsSendOptions => Object Foreign -> Nullable Handle -> { | r } From 21ab7a23f46137fbd798623f6558b2997f0831f6 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 09:31:57 -0700 Subject: [PATCH 07/14] Rename execSyncOpts to execSync' --- src/Node/UnsafeChildProcess/Unsafe.purs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/UnsafeChildProcess/Unsafe.purs b/src/Node/UnsafeChildProcess/Unsafe.purs index 4a1db53..0087a5f 100644 --- a/src/Node/UnsafeChildProcess/Unsafe.purs +++ b/src/Node/UnsafeChildProcess/Unsafe.purs @@ -16,7 +16,7 @@ module Node.UnsafeChildProcess.Unsafe , unsafeStderr , execSync , JsExecSyncOptions - , execSyncOpts + , execSync' , exec , JsExecOptions , execOpts @@ -116,13 +116,13 @@ type JsExecSyncOptions = , windowsHide :: Boolean ) -execSyncOpts +execSync' :: forall r trash . Row.Union r trash JsExecSyncOptions => String -> { | r } -> Effect StringOrBuffer -execSyncOpts command opts = runEffectFn2 execSyncOptsImpl command opts +execSync' command opts = runEffectFn2 execSyncOptsImpl command opts foreign import execSyncOptsImpl :: forall r. EffectFn2 (String) ({ | r }) (StringOrBuffer) From 66eb12bc3427c5e85d5c017c36ca589de53dd64d Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 12:10:45 -0700 Subject: [PATCH 08/14] Reimplement ChildProcess bindings via Unsafe/Safe APIs --- CHANGELOG.md | 28 +- src/Node/ChildProcess.js | 50 -- src/Node/ChildProcess.purs | 765 +++++++++++++++--------- src/Node/ChildProcess/Types.purs | 3 +- src/Node/UnsafeChildProcess/Safe.purs | 14 +- src/Node/UnsafeChildProcess/Unsafe.purs | 43 +- test/Main.purs | 22 +- 7 files changed, 563 insertions(+), 362 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2585c9f..a28cb69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,10 +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) -- Moved types from `Node.ChildProces` to ``Node.ChildProces.Types` (#46 by @JordanMartinez) +- Breaking changes made to the `Exit` type (#46 by @JordanMartinez) - - `Exit(Normally, BySignal)` - - `Handle` + - 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) @@ -40,6 +60,7 @@ New features: - spawnArgs - spawnFile - Added unsafe, uncurried API of all ChildProcess-creating functions (#46 by @JordanMartinez) +- Added safe variant of `spawnSync`/`spawnSync'` (#46 by @JordanMartinez) Bugfixes: @@ -48,6 +69,7 @@ Other improvements: - 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) +- 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 e5a11ca..b63f84e 100644 --- a/src/Node/ChildProcess.js +++ b/src/Node/ChildProcess.js @@ -1,52 +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 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 de021a4..4562b55 100644 --- a/src/Node/ChildProcess.purs +++ b/src/Node/ChildProcess.purs @@ -4,13 +4,33 @@ -- | 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 ( ChildProcess , toEventEmitter @@ -34,60 +54,68 @@ module Node.ChildProcess , signalCode , spawnFile , spawnArgs - , send + , 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.Time.Duration (Milliseconds) import Effect (Effect) -import Effect.Exception as Exception +import Effect.Exception (Error) import Effect.Uncurried (EffectFn2) import Foreign (Foreign) import Foreign.Object (Object) import Node.Buffer (Buffer) -import Node.ChildProcess.Types (Exit, Handle, UnsafeChildProcess) -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.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) -- | 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 = toUnsafeChildProcess >>> SafeCP.toEventEmitter toUnsafeChildProcess :: ChildProcess -> UnsafeChildProcess -toUnsafeChildProcess = unsafeCoerce +toUnsafeChildProcess (ChildProcess p) = p closeH :: EventHandle ChildProcess (Exit -> Effect Unit) (EffectFn2 (Nullable Int) (Nullable String) Unit) closeH = unsafeCoerce SafeCP.closeH @@ -107,40 +135,23 @@ messageH = unsafeCoerce SafeCP.messageH spawnH :: EventHandle0 ChildProcess spawnH = unsafeCoerce SafeCP.spawnH -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 ()) - , send :: forall r. Fn2 { | r } Handle Boolean - } +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". 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". 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". 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 -- | exited, another process may have taken the same ID, so be careful! @@ -179,18 +190,6 @@ killSignal = unsafeCoerce SafeCP.killSignal killed :: ChildProcess -> Effect Boolean killed = unsafeCoerce SafeCP.killed --- | 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 - signalCode :: ChildProcess -> Effect (Maybe String) signalCode = unsafeCoerce SafeCP.signalCode @@ -200,8 +199,96 @@ spawnArgs = unsafeCoerce SafeCP.spawnArgs spawnFile :: ChildProcess -> String spawnFile = unsafeCoerce SafeCP.spawnFile -mkEffect :: forall a. (Unit -> a) -> Effect a -mkEffect = unsafeCoerce +type SpawnSyncResult = + { pid :: Pid + , output :: Array Foreign + , stdout :: Buffer + , stderr :: Buffer + , exitStatus :: Exit + , error :: Maybe SystemError + } + +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 + } + +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 + } + +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 + } + + 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 @@ -210,51 +297,131 @@ mkEffect = unsafeCoerce 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 } + +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, @@ -263,112 +430,68 @@ defaultSpawnOptions = -- | -- | 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 +exec :: String -> Effect ChildProcess +exec command = coerce $ UnsafeCP.execOpts command { encoding: "buffer" } --- | 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 - -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 +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. @@ -376,117 +499,211 @@ foreign import execSyncImpl 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" } + +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 - } +-- | 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" } -type ExecSyncOptions = +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 } --- | 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 +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 + } + +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) -toActualStdIOOptions :: Array (Maybe StdIOBehaviour) -> ActualStdIOOptions -toActualStdIOOptions = map (toNullable <<< map toActualStdIOBehaviour) +type SendOptions = + { keepAlive :: Maybe Boolean + } + +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 index 62abc92..505638d 100644 --- a/src/Node/ChildProcess/Types.purs +++ b/src/Node/ChildProcess/Types.purs @@ -3,7 +3,6 @@ module Node.ChildProcess.Types where import Prelude import Data.Nullable (Nullable, null) -import Data.Posix.Signal (Signal) import Node.FS (FileDescriptor) import Node.Stream (Stream) import Unsafe.Coerce (unsafeCoerce) @@ -71,7 +70,7 @@ foreign import data StringOrBuffer :: Type -- | due to a signal. data Exit = Normally Int - | BySignal Signal + | BySignal String instance showExit :: Show Exit where show (Normally x) = "Normally " <> show x diff --git a/src/Node/UnsafeChildProcess/Safe.purs b/src/Node/UnsafeChildProcess/Safe.purs index 315ad1d..29d5678 100644 --- a/src/Node/UnsafeChildProcess/Safe.purs +++ b/src/Node/UnsafeChildProcess/Safe.purs @@ -20,6 +20,7 @@ module Node.UnsafeChildProcess.Safe , signalCode , spawnFile , spawnArgs + , safeStdio ) where import Prelude @@ -32,7 +33,7 @@ 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, UnsafeChildProcess) +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) @@ -44,7 +45,7 @@ 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 >>= Signal.fromString of + 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 @@ -57,7 +58,7 @@ 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 >>= Signal.fromString of + 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 @@ -127,3 +128,10 @@ 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.purs b/src/Node/UnsafeChildProcess/Unsafe.purs index 0087a5f..983b432 100644 --- a/src/Node/UnsafeChildProcess/Unsafe.purs +++ b/src/Node/UnsafeChildProcess/Unsafe.purs @@ -164,21 +164,23 @@ execOpts command opts = runEffectFn2 execOptsImpl command opts foreign import execOptsImpl :: forall r. EffectFn2 (String) ({ | r }) (UnsafeChildProcess) -execCb :: String -> (SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) -> Effect UnsafeChildProcess -execCb command cb = runEffectFn2 execCbImpl command $ mkEffectFn3 cb +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 SystemError StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) +foreign import execCbImpl :: EffectFn2 (String) (EffectFn3 (Nullable SystemError) StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) execOptsCb :: forall r trash . Row.Union r trash JsExecOptions => String -> { | r } - -> (SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) + -> (Maybe SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) -> Effect UnsafeChildProcess -execOptsCb command opts cb = runEffectFn3 execOptsCbImpl command opts $ mkEffectFn3 cb +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 SystemError StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) +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 @@ -275,11 +277,12 @@ execFileOptsCb => String -> Array String -> { | r } - -> (SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) + -> (Maybe SystemError -> StringOrBuffer -> StringOrBuffer -> Effect Unit) -> Effect UnsafeChildProcess -execFileOptsCb file args opts cb = runEffectFn4 execFileOptsCbImpl file args opts $ mkEffectFn3 cb +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 SystemError StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) +foreign import execFileOptsCbImpl :: forall r. EffectFn4 (String) (Array String) ({ | r }) (EffectFn3 (Nullable SystemError) StringOrBuffer StringOrBuffer Unit) (UnsafeChildProcess) type JsSpawnSyncResult = { pid :: Pid @@ -288,7 +291,7 @@ type JsSpawnSyncResult = , stderr :: StringOrBuffer , status :: Nullable Int , signal :: Nullable String - , error :: SystemError + , error :: Nullable SystemError } spawnSync :: String -> Array String -> Effect JsSpawnSyncResult @@ -417,10 +420,10 @@ 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 :: Object Foreign -> Nullable Handle -> UnsafeChildProcess -> Effect Boolean +unsafeSend :: forall messageRows. { | messageRows } -> Nullable Handle -> UnsafeChildProcess -> Effect Boolean unsafeSend msg handle cp = runEffectFn3 sendImpl cp msg handle -foreign import sendImpl :: EffectFn3 (UnsafeChildProcess) (Object Foreign) (Nullable Handle) (Boolean) +foreign import sendImpl :: forall messageRows. EffectFn3 (UnsafeChildProcess) ({ | messageRows }) (Nullable Handle) (Boolean) type JsSendOptions = ( keepAlive :: Boolean @@ -428,28 +431,28 @@ type JsSendOptions = -- | Unsafe because child process must be a Node child process and an IPC channel must exist. unsafeSendOpts - :: forall r trash + :: forall r trash messageRows . Row.Union r trash JsSendOptions - => Object Foreign + => { | messageRows } -> Nullable Handle -> { | r } -> UnsafeChildProcess -> Effect Boolean unsafeSendOpts msg handle opts cp = runEffectFn4 sendOptsImpl cp msg handle opts -foreign import sendOptsImpl :: forall r. EffectFn4 (UnsafeChildProcess) (Object Foreign) (Nullable Handle) ({ | r }) (Boolean) +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 :: Object Foreign -> Nullable Handle -> (Maybe Error -> Effect Unit) -> UnsafeChildProcess -> Effect Boolean +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 :: EffectFn4 (UnsafeChildProcess) (Object Foreign) (Nullable Handle) (EffectFn1 (Nullable Error) Unit) (Boolean) +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 + :: forall r trash messageRows . Row.Union r trash JsSendOptions - => Object Foreign + => { | messageRows } -> Nullable Handle -> { | r } -> (Maybe Error -> Effect Unit) @@ -457,7 +460,7 @@ unsafeSendOptsCb -> Effect Boolean unsafeSendOptsCb msg handle opts cb cp = runEffectFn5 sendOptsCbImpl cp msg handle opts $ mkEffectFn1 \err -> cb $ toMaybe err -foreign import sendOptsCbImpl :: forall r. EffectFn5 (UnsafeChildProcess) (Object Foreign) (Nullable Handle) ({ | r }) (EffectFn1 (Nullable Error) Unit) (Boolean) +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 diff --git a/test/Main.purs b/test/Main.purs index 909ca0c..a27749f 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -4,12 +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 (defaultExecOptions, defaultExecSyncOptions, defaultSpawnOptions, errorH, exec, execSync, exitH, kill, spawn, stdout) +import Node.ChildProcess (errorH, exec', execSync', exitH, kill, spawn, stdout) import Node.ChildProcess.Types (Exit(..)) -import Node.Encoding (Encoding(UTF8)) +import Node.Encoding (Encoding(..)) import Node.Encoding as NE import Node.Errors.SystemError (code) import Node.EventEmitter (on_) @@ -25,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 @@ -35,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) @@ -49,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 -> - log "redirected to stderr:" *> (Buffer.toString UTF8 r.stderr >>= log) + _ <- exec' "ls >&2" identity \_ _ stderr' -> + log "redirected to stderr:" *> (Buffer.toString UTF8 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 From 755c8afbe1969b8d1c9f0caa28a83141e2d411d3 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 12:14:19 -0700 Subject: [PATCH 09/14] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a28cb69..4637f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ 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 From 70aaf440a2cd6efb3d3656f4080d7f247d0acb76 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 13:28:42 -0700 Subject: [PATCH 10/14] Update usage of exec' --- test/Main.purs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Main.purs b/test/Main.purs index a27749f..5b71f49 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -64,8 +64,8 @@ nonExistentExecutable done = do execLs :: Effect Unit execLs = do -- returned ChildProcess is ignored here - _ <- exec' "ls >&2" identity \_ _ stderr' -> - log "redirected to stderr:" *> (Buffer.toString UTF8 stderr' >>= log) + _ <- exec' "ls >&2" identity \r -> + log "redirected to stderr:" *> (Buffer.toString UTF8 r.stderr >>= log) pure unit execSyncEcho :: String -> Effect Unit From 8e4dcb36e57bcf4e3285a011de3110c695e39bb7 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 13:29:11 -0700 Subject: [PATCH 11/14] Update FFI name to match PS one --- src/Node/UnsafeChildProcess/Unsafe.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Node/UnsafeChildProcess/Unsafe.js b/src/Node/UnsafeChildProcess/Unsafe.js index e2c00e0..ba39b8a 100644 --- a/src/Node/UnsafeChildProcess/Unsafe.js +++ b/src/Node/UnsafeChildProcess/Unsafe.js @@ -18,9 +18,9 @@ export { fork as forkOptsImpl, } from "child_process"; -export const stdin = (cp) => cp.stdin; -export const stdout = (cp) => cp.stdout; -export const stderr = (cp) => cp.stderr; +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); From 38d6a964d8f6b4b2ae87e2aafc14c60ae244bffc Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 13:35:01 -0700 Subject: [PATCH 12/14] Add missing docs on options --- src/Node/ChildProcess.purs | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/Node/ChildProcess.purs b/src/Node/ChildProcess.purs index 4562b55..e8fd2a9 100644 --- a/src/Node/ChildProcess.purs +++ b/src/Node/ChildProcess.purs @@ -199,6 +199,9 @@ spawnArgs = unsafeCoerce SafeCP.spawnArgs spawnFile :: ChildProcess -> String spawnFile = unsafeCoerce SafeCP.spawnFile +-- | 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 @@ -224,6 +227,18 @@ spawnSync command args = (UnsafeCP.spawnSync command args) <#> \r -> , error: toMaybe r.error } +-- | - `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 @@ -300,6 +315,19 @@ spawn -> Effect ChildProcess 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) @@ -503,6 +531,16 @@ execFileSync 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 @@ -561,6 +599,16 @@ execFile -> 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 , env :: Maybe (Object String) @@ -618,6 +666,19 @@ fork -> 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 @@ -684,6 +745,7 @@ send -> 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 } From 7e2daf585eb22b681a1952a591f3c654545ace59 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 13:35:17 -0700 Subject: [PATCH 13/14] Add more missing options docs --- src/Node/UnsafeChildProcess/Unsafe.purs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Node/UnsafeChildProcess/Unsafe.purs b/src/Node/UnsafeChildProcess/Unsafe.purs index 983b432..51772cf 100644 --- a/src/Node/UnsafeChildProcess/Unsafe.purs +++ b/src/Node/UnsafeChildProcess/Unsafe.purs @@ -392,6 +392,20 @@ 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 @@ -425,6 +439,7 @@ 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 ) From 9eaa21caa4b4f9a32a85110f78009a422834e2ed Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Thu, 20 Jul 2023 14:05:09 -0700 Subject: [PATCH 14/14] Tweak docs one more time --- src/Node/ChildProcess.purs | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Node/ChildProcess.purs b/src/Node/ChildProcess.purs index e8fd2a9..fc902f2 100644 --- a/src/Node/ChildProcess.purs +++ b/src/Node/ChildProcess.purs @@ -138,22 +138,20 @@ 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 = 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 = 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 = 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 = unsafeCoerce SafeCP.pid @@ -384,7 +382,7 @@ spawn' cmd args buildOpts = coerce $ UnsafeCP.spawn' cmd args opts -- | 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. +-- | Returns: The `stdout` from the command. execSync :: String -> Effect Buffer @@ -453,8 +451,7 @@ execSync' cmd buildOpts = do -- | 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). @@ -489,6 +486,13 @@ type ExecOptions = , shell :: Maybe Shell } +-- | 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 -> (ExecOptions -> ExecOptions) @@ -523,7 +527,7 @@ exec' command buildOpts cb = coerce $ UnsafeCP.execOptsCb command opts \err sout -- | 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