3
votes

I am new to both purescript and haskell (and even javascript and node), so I am stumped in trying to figure out how to save off the output of a child process in purescript. I am using the purescript-node-childprocess and purescript-node-fs modules. Basically, the problem I am having is this:

import Node.ChildProcess (CHILD_PROCESS, SpawnOptions, defaultSpawnOptions, Exit(..), spawn, onExit, stdout)
import Node.Stream (onData)

type CPEffect = forall e
              .  Eff (cp :: CHILD_PROCESS
                     , console :: CONSOLE
                     , err :: EXCEPTION
                     , buffer :: BUFFER | e
                     ) Unit

-- | Basically a wrapper around the spawn command.
-- | Takes a command, an array of arguments, and a record of 
-- | options to pass to spawn.
launch :: String -> Array String -> SpawnOptions -> CPEffect
launch cmd args opts = do
  cmd' <- spawn cmd args opts
  onExit cmd' defaultExitHdlr
  -- My problem is with onData due to its return type 
  onData (stdout cmd') -- what do I put as the callback handler arg?
  log $ "done with " <> cmd

As shown in my comment above, the problem is the onData function which comes from the Node.Stream module. The problem is in the 2nd arg and return type:

onData :: forall w eff
        . Readable w (err :: EXCEPTION | eff)
       -> (Buffer -> Eff (err :: EXCEPTION | eff) Unit)
       -> Eff (err :: EXCEPTION | eff) Unit

Since the return is an Eff which returns Unit, how can I save off the child process output? The 2nd argument is a function which takes a Buffer and also returns the same type. Indeed, it is this function which is getting the data from the Readable (which is the stdout of the childprocess). In other words, the first argument is the stdout stream from node of the child process, and the 2nd arg is a callback handler which will fill in the buffer from the stdout stream.

But since callback handler returns Unit, I don't see how I can accumulate the output from the child process. I am still in the process of learning Monad Transformers, so is this a solution? Can I create a Writer monad that somehow wraps this?

2

2 Answers

4
votes

If I was going to do this in purescript without using the FFI, I would use a Ref, and in the onData callback, read the ref value, make a new string by appending the new data to the ref value, and then update the ref value with this new string.

WriterT is indeed useful for this kind of thing although I probably would not use it in this particular case, because it could be a bit awkward to work out how to get the types to line up, since the callback takes Eff, not WriterT something Eff.

Another option, which I used to solve this exact issue in Pulp, is to use the npm package concat-stream. In fact you might well find Pulp useful as a real world example of using these libraries - I think the modules Pulp.Exec and Pulp.System.Stream are the ones you will be interested in.

2
votes

In pscid I'm using an STRef to concatenate the output of a process and print its results to the console when the process exits. I've copied the relevant code so that you have an example:

execCommand
  ∷ ∀ e
  . String
  → String
  → Eff (cp ∷ CHILD_PROCESS, console ∷ CONSOLE | e) Unit
execCommand name command =
   catchLog (name <> " threw an exception") $
    runST do
      let cmd = unsafePartial fromJust (uncons (split (Pattern " ") command))
      output ← newSTRef ""
      log ("Running: \"" <> command <> "\"")
      cp ← spawn cmd.head cmd.tail defaultSpawnOptions

      let stout = stdout cp
          sterr = stderr cp

      onDataString stout UTF8 \s →
        modifySTRef output (_ <> s) $> unit

      onDataString sterr UTF8 \s →
        modifySTRef output (_ <> s) $> unit

      onExit cp \e → case e of
        Normally 0 → logColored Green (name <> " successful!")
        Normally code → do
          log =<< readSTRef output
          logColored Red (name <> " errored with code: " <> show code)
        BySignal _       → pure unit