3

I need to run a process, do something while it is running, and finally terminate it. The process in question writes things to standard output that I would like to retain. Unfortunately, it seems that the process dies before I can connect and extract its last words. Having scarce experience with asynchronous programming, I am having a difficulty finding a nice solution. It would be fortunate if I can accomplish this task within the framework of RIO.Process, although I am prepared to step outside of it if it cannot be avoided. (Note that RIO employs an unusual way of invoking external processes via a callback system.)

Below is a highly simplified runnable example of what I am trying to achieve.

Here is an emulation of the program to be run:
(Put it in a file called x.sh and say chmod +x x.sh to make it executable.)

#!/bin/sh

trap 'echo "Terminating..."; exit 0' TERM

echo "Initialization complete."

while true; do sleep 1; done

Here is my code:
(Put it in a file called X.hs and compile with ghc -package rio X.hs.)

{-#   language   NoImplicitPrelude   #-}
{-#   language   BlockArguments      #-}
{-#   language   OverloadedStrings   #-}

module Main where

import RIO
import RIO.Process
import Data.Text.IO (hGetContents, hGetLine)

main :: IO ()
main = runSimpleApp do
    proc "./x.sh" [ ]
        \processConfig -> withProcessWait_ (setStdout createPipe processConfig)
            \processHandle -> bracket_
                (initialize processHandle)
                (terminate processHandle)
                (return ())

initialize :: (HasProcessContext env, HasLogFunc env) => Process () Handle () -> RIO env ()
initialize processHandle = do
    x <- liftIO $ hGetLine (getStdout processHandle)
    if x == "Initialization complete." then return () else error "This should not happen."

terminate :: HasLogFunc env => Process () Handle () -> RIO env ()
terminate processHandle = do
    log' <- async $ liftIO $ hGetContents (getStdout processHandle)
    stopProcess processHandle
    log <- wait log'
    logInfo $ display log

Here is what happens:

% ./X
X: fd:3: hGetBuffering: illegal operation (handle is closed)

x.sh is saying something, but I cannot hear.

What is the right way to manage this?

Ignat Insarov
  • 4,660
  • 18
  • 37

1 Answers1

1

From the documentation for stopProcess:

Close a process and release any resources acquired. This will ensure terminateProcess is called, wait for the process to actually exit, and then close out resources allocated for the streams. In the event of any cleanup exceptions being thrown this will throw an exception.

(emphasis mine) You don't want stopProcess to do that before you read the output. You just want terminateProcess. withProcessWait_ will take care of the rest of it. Unfortuntately, you do have to step outside of RIO to do that, with import System.Process (terminateProcess) and then liftIO $ terminateProcess (unsafeProcessHandle processHandle).

Side notes: You're kind of misusing bracket_. Since the "middle" of your bracket_ is a no-op, and especially now that the beginning and end aren't actually acquiring or releasing any resources, it's kind of pointless. Also, instead of using async at all, you can just read the output normally after terminating the process, since the output that a process already produced doesn't just disappear when it's terminated.

Here's your code with all of the above fixed:

{-#   language   NoImplicitPrelude   #-}
{-#   language   BlockArguments      #-}
{-#   language   OverloadedStrings   #-}

module Main where

import RIO
import RIO.Process
import Data.Text.IO (hGetContents, hGetLine)
import System.Process (terminateProcess)

main :: IO ()
main = runSimpleApp do
    proc "./x.sh" [ ]
        \processConfig -> withProcessWait_ (setStdout createPipe processConfig)
            \processHandle -> do
                initialize processHandle
                terminate processHandle

initialize :: (HasProcessContext env, HasLogFunc env) => Process () Handle () -> RIO env ()
initialize processHandle = do
    x <- liftIO $ hGetLine (getStdout processHandle)
    if x == "Initialization complete." then return () else error "This should not happen."

terminate :: HasLogFunc env => Process () Handle () -> RIO env ()
terminate processHandle = do
    liftIO $ terminateProcess (unsafeProcessHandle processHandle)
    log <- liftIO $ hGetContents (getStdout processHandle)
    logInfo $ display log
  • I also read this. I tried to follow the source code, but it is indirect. It appears that `stopProcess` extracts an IO action from `ProcessHandle` and executes it, and I have no idea what this action does. If it frees some resources, it means I should free these resources manually to avoid leaks. This is why I am cautions about invoking `terminateProcess` and calling it a day. – Ignat Insarov Jan 04 '20 at 17:30
  • @IgnatInsarov Like I said, "`withProcessWait_` will take care of the rest of it." Since you're using it, you don't need to worry about using `stopProcess` manually to avoid leaks. – Joseph Sible-Reinstate Monica Jan 04 '20 at 17:31
  • How are you so sure? – Ignat Insarov Jan 04 '20 at 17:31
  • @IgnatInsarov By reading [the source code](https://hackage.haskell.org/package/typed-process-0.2.6.0/docs/src/System.Process.Typed.html#withProcessWait_). It will call `stopProcess` itself once your code is done. – Joseph Sible-Reinstate Monica Jan 04 '20 at 17:33
  • `bracket` is there because this is what I will be using in the actual code, and I wanted to keep the structure of my example in exact correspondence with it, so as not to accidentally alter the meaning. – Ignat Insarov Jan 04 '20 at 17:52
  • For one, you can put threadDelay inside the bracket to add some realistic delay. In the actual code, I do check for a message from the process that it has started successfully, so the delay that we need will be there. Turns out my example is too short! Possibly you could edit this information into your answer? I edited my question to incorporate this detail. Also, I gather that `terminateProcess` will leave the standard output handle intact, but could you spell it out as the reason why `async` is not required? – Ignat Insarov Jan 04 '20 at 18:24
  • @IgnatInsarov Answer updated to avoid `threadDelay`. You still don't need `bracket_` in what you posted, though. – Joseph Sible-Reinstate Monica Jan 04 '20 at 18:32