
I've put together a simple test case for a rather annoying problem. I've got a program that drives other programs. For example, it can drive `cat`: :; Simple cat a-file When the file is a little bit greater than 135060 bytes, this program fails to produce any output at all -- I need to use ^C to get my console back. I have a similar program that encounters the same issue a little after 139192 bytes. The inconsistency is one baffling feature of this bug. If I remove the `hClose`, the example program just hangs, no matter the size of the input. It is entirely possible that I have overlooked some subtle feature of pipe semantics -- indeed, I hope that is the case. -- _jsn import Data.ByteString.Lazy import System.Process import System.Environment import System.IO (hClose) import Prelude hiding (writeFile, readFile) main = do exe:file:_ <- getArgs bytes <- readFile file foo <- simple exe bytes [] writeFile (file ++ ".foo") foo -- Manufactures a simple stream handler from a command line utility. simple :: String -> ByteString -> [String] -> IO ByteString simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing hPut i bytes s <- hGetContents o hClose i return s

On 2008 Nov 10, at 16:29, Jason Dusek wrote:
I've put together a simple test case for a rather annoying problem. I've got a program that drives other programs. For example, it can drive `cat`:
:; Simple cat a-file
When the file is a little bit greater than 135060 bytes, this program fails to produce any output at all -- I need to use ^C to get my console back.
If you are feeding it input and collecting output, you need to forkIO one or both. In particular, the sequence feed input read output waitForProcess will deadlock if at any point the input or output pipe fills: one side will be blocked on write() waiting for the other side to read() from the pipe, while the read() side is blocked waiting for write() on the other pipe, which won't be read() because that's the first side. (There are other variants of this, but that's the general form: processes blocked each waiting for the other side to do something.)
If I remove the `hClose`, the example program just hangs, no matter the size of the input.
That's another symptom of it, yes, made worse because the pipe close is now implicit and the other side won't stop read()ing until the pipe is close()d by the first side.
simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing hPut i bytes s <- hGetContents o hClose i return s
Yep, that's your problem. forkIO the hPut. -- brandon s. allbery [solaris,freebsd,perl,pugs,haskell] allbery@kf8nh.com system administrator [openafs,heimdal,too many hats] allbery@ece.cmu.edu electrical and computer engineering, carnegie mellon university KF8NH

simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing hPut i bytes s <- hGetContents o hClose i return s
Yep, that's your problem. forkIO the hPut.
Maybe I didn't do enough here -- just wrapping in `forkIO` does not seem to actually help. -- _jsn import Data.ByteString.Lazy import System.Process import System.Environment import System.IO (hClose) import Control.Concurrent import Prelude hiding (writeFile, readFile) main = do exe:file:_ <- getArgs bytes <- readFile file foo <- simple exe bytes [] writeFile (file ++ ".foo") foo -- Manufactures a simple stream handler from a command line utility. simple :: String -> ByteString -> [String] -> IO ByteString simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing forkIO $ hPut i bytes s <- hGetContents o return s

This does not work either. It should cover all the bases, right? Fork off input, pull things from ouput as they are ready, stop when we reach end of file. If you remove the line `print partial`, the program loops forever; if you keep it, the program stops right there. -- _jsn import Data.ByteString.Lazy hiding (putStrLn) import System.Process import System.Environment import System.IO (putStrLn, hClose, hWaitForInput) import System.IO.Error import Control.Concurrent import Prelude hiding (writeFile, readFile) main = do exe:file:_ <- getArgs bytes <- readFile file foo <- simple exe bytes [] writeFile (file ++ ".foo") foo -- Manufactures a simple stream handler from a command line utility. simple :: String -> ByteString -> [String] -> IO ByteString simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing pushAndPull i o bytes pushAndPull i o bytes = do putStrLn "Working with:" print bytes forkIO $ hPut i bytes putStrLn "forked" readUntilDone empty where readUntilDone soFar = do (const $ return soFar) `hctac` do putStrLn "hctac" hWaitForInput o 0 -- Wait as long as it takes. putStrLn "waited" partial <- hGetContents o putStrLn "contents:" print partial readUntilDone $ append soFar partial where hctac = flip catch

On 2008 Nov 10, at 19:04, Jason Dusek wrote:
simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing hPut i bytes s <- hGetContents o hClose i return s
Yep, that's your problem. forkIO the hPut.
Maybe I didn't do enough here -- just wrapping in `forkIO` does not seem to actually help.
*sigh* I hate the ghc runtime... it works in ghci, or compiled with - threaded. Otherwise you still get the deadlock because it only switches threads under limited circumstances (garbage collections?) which isn't nearly often enough. -- brandon s. allbery [solaris,freebsd,perl,pugs,haskell] allbery@kf8nh.com system administrator [openafs,heimdal,too many hats] allbery@ece.cmu.edu electrical and computer engineering, carnegie mellon university KF8NH

That was just me being absent-minded -- I have threaded in my Cabal file but was not using it on the command line for my little test script. Thank you for calling it to my attention. -- _jsn

Brandon S. Allbery KF8NH wrote:
On 2008 Nov 10, at 19:04, Jason Dusek wrote:
simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing hPut i bytes s <- hGetContents o hClose i return s
Yep, that's your problem. forkIO the hPut.
Maybe I didn't do enough here -- just wrapping in `forkIO` does not seem to actually help.
*sigh* I hate the ghc runtime... it works in ghci, or compiled with -threaded.
Would you hate it less if -threaded were the default?
Otherwise you still get the deadlock because it only switches threads under limited circumstances (garbage collections?)
No, the issue is that without real OS threads, a foreign call can't be pre-empted (pretty obvious when you think about it). waitForProcess ends up making a blocking foreign call - non-obvious, but at least it's documented. Cheers, Simon

On 2008 Nov 27, at 8:51, Simon Marlow wrote:
Brandon S. Allbery KF8NH wrote:
On 2008 Nov 10, at 19:04, Jason Dusek wrote:
simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing hPut i bytes s <- hGetContents o hClose i return s
Yep, that's your problem. forkIO the hPut.
Maybe I didn't do enough here -- just wrapping in `forkIO` does not seem to actually help. *sigh* I hate the ghc runtime... it works in ghci, or compiled with -threaded.
Would you hate it less if -threaded were the default?
Otherwise you still get the deadlock because it only switches threads under limited circumstances (garbage collections?)
No, the issue is that without real OS threads, a foreign call can't be pre-empted (pretty obvious when you think about it). waitForProcess ends up making a blocking foreign call - non-obvious, but at least it's documented.
The way this is usually handled in the non-threaded case is to either use SIGCHLD or non-blocking waitpid() so that "green" threads can continue running. I'm a little surprised this wasn't done. -- brandon s. allbery [solaris,freebsd,perl,pugs,haskell] allbery@kf8nh.com system administrator [openafs,heimdal,too many hats] allbery@ece.cmu.edu electrical and computer engineering, carnegie mellon university KF8NH

On Thu, 2008-11-27 at 11:38 -0500, Brandon S. Allbery KF8NH wrote:
On 2008 Nov 27, at 8:51, Simon Marlow wrote:
No, the issue is that without real OS threads, a foreign call can't be pre-empted (pretty obvious when you think about it). waitForProcess ends up making a blocking foreign call - non-obvious, but at least it's documented.
The way this is usually handled in the non-threaded case is to either use SIGCHLD or non-blocking waitpid() so that "green" threads can continue running. I'm a little surprised this wasn't done.
Yes, we've discussed this in detail a few months back. We even have a partial implementation. However it stalled on needing a better signals API which we have not managed to get through the standardisation process. Unfortunately there is no non-blocking (non-polling) waitpid() and the global (process-scope) nature of signals is a pain. Duncan

Quoth Duncan Coutts

Donn Cave wrote:
Quoth Duncan Coutts
: | On Thu, 2008-11-27 at 11:38 -0500, Brandon S. Allbery KF8NH wrote: | |> The way this is usually handled in the non-threaded case is to either |> use SIGCHLD or non-blocking waitpid() so that "green" threads can |> continue running. I'm a little surprised this wasn't done. | | Yes, we've discussed this in detail a few months back. We even have a | partial implementation. However it stalled on needing a better signals | API which we have not managed to get through the standardisation | process. | | Unfortunately there is no non-blocking (non-polling) waitpid() and the | global (process-scope) nature of signals is a pain. SIGCHLD can be a pain in its own unusual way. Once you have a SIGCHLD handler, process exits will interrupt "long" I/O, so every such read(), recv() or whatever must now check for EINTR and restart. Even though the authors of GHC go to great lengths to convert all I/O to non-blocking anyway, this will still apply to external library functions that are beyond GHC's reach. So it's a strategy I would use only if I were kind of desperate.
We already have this issue since GHC's runtime uses a SIGVTALRM timer signal for context switching and profiling. Indeed, it did cause trouble with the editline library which doesn't test for EINTR in one or two places, and we had to work around it by temporarily disabling the timer. Still, it's standard practice to test for EINTR and all library code should do it. Cheers, Simon

"Jason Dusek"
simple exe bytes args = do (i, o, e, p) <- runInteractiveProcess exe args Nothing Nothing hPut i bytes s <- hGetContents o hClose i return s
Yep, that's your problem. forkIO the hPut.
Maybe I didn't do enough here -- just wrapping in `forkIO` does not seem to actually help.
What GHC version are you using? Bugs #1780, #1936, and #2155 might be relevant. -- Daniel Franke df@dfranke.us http://www.dfranke.us |----| =|\ \\\\ || * | -|-\--------- Man is free at the instant he wants to be. -----| =| \ /// --Voltaire

I am using 6.8.3 -- it is almost exciting to realize how close I came to 'impossible' instead of 'annoying'. Living on the edge with UNIX IO! -- _jsn
participants (7)
-
Brandon S. Allbery KF8NH
-
Daniel Franke
-
Donn Cave
-
Duncan Coutts
-
Jason Dusek
-
Krzysztof Skrzętnicki
-
Simon Marlow