aboutsummaryrefslogtreecommitdiff
path: root/src/Text/Pandoc/Process.hs
blob: 80be531c14355c53217ca9a6ba691ee95ac875ca (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
{-# LANGUAGE NoImplicitPrelude #-}
{-
Copyright (C) 2013-2019 John MacFarlane <jgm@berkeley.edu>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
-}

{- |
   Module      : Text.Pandoc.Process
   Copyright   : Copyright (C) 2013-2019 John MacFarlane
   License     : GNU GPL, version 2 or above

   Maintainer  : John MacFarlane <jgm@berkeley.edu>
   Stability   : alpha
   Portability : portable

ByteString variant of 'readProcessWithExitCode'.
-}
module Text.Pandoc.Process (pipeProcess)
where
import Prelude
import Control.Concurrent (MVar, forkIO, killThread, newEmptyMVar, putMVar,
                           takeMVar)
import Control.Exception (SomeException (..))
import qualified Control.Exception as E
import Control.Monad (unless)
import Control.DeepSeq (rnf)
import qualified Data.ByteString.Lazy as BL
import Foreign.C (Errno (Errno), ePIPE)
import GHC.IO.Exception (IOErrorType(..), IOException(..))
import System.Exit (ExitCode (..))
import System.IO (hClose)
import System.Process

{- |
Version of 'System.Process.readProcessWithExitCode' that uses lazy bytestrings
instead of strings and allows setting environment variables.

@readProcessWithExitCode@ creates an external process, reads its
standard output strictly, waits until the process
terminates, and then returns the 'ExitCode' of the process
and the standard output.  stderr is inherited from the parent.

If an asynchronous exception is thrown to the thread executing
@readProcessWithExitCode@, the forked process will be terminated and
@readProcessWithExitCode@ will wait (block) until the process has been
terminated.

This function was adapted from @readProcessWithExitCode@ of module
System.Process, package process-1.6.3.0. The original code is BSD
licensed and © University of Glasgow 2004-2008.
-}
pipeProcess
    :: Maybe [(String, String)] -- ^ environment variables
    -> FilePath                 -- ^ Filename of the executable (see 'proc' for details)
    -> [String]                 -- ^ any arguments
    -> BL.ByteString            -- ^ standard input
    -> IO (ExitCode,BL.ByteString) -- ^ exitcode, stdout
pipeProcess mbenv cmd args input = do
    let cp_opts = (proc cmd args)
                  { env     = mbenv
                  , std_in  = CreatePipe
                  , std_out = CreatePipe
                  , std_err = Inherit
                  }
    withCreateProcess cp_opts $
      \mbInh mbOuth _ pid -> do
        let (inh, outh) =
             case (mbInh, mbOuth) of
                  (Just i, Just o) -> (i, o)
                  (Nothing, _)     -> error "withCreateProcess no inh"
                  (_, Nothing)     -> error "withCreateProcess no outh"

        out <- BL.hGetContents outh

        -- fork off threads to start consuming stdout & stderr
        withForkWait (E.evaluate $ rnf out) $ \waitOut -> do

          -- now write any input
          unless (BL.null input) $
            ignoreSigPipe $ BL.hPutStr inh input
          -- hClose performs implicit hFlush, and thus may trigger a SIGPIPE
          ignoreSigPipe $ hClose inh

          -- wait on the output
          waitOut

          hClose outh

        -- wait on the process
        ex <- waitForProcess pid

        return (ex, out)

-- | Fork a thread while doing something else, but kill it if there's an
-- exception.
--
-- This is important in the cases above because we want to kill the thread
-- that is holding the Handle lock, because when we clean up the process we
-- try to close that handle, which could otherwise deadlock.
--
-- This function was copied verbatim from module System.Process of package
-- process-1.6.3.0.
withForkWait :: IO () -> (IO () ->  IO a) -> IO a
withForkWait async body = do
  waitVar <- newEmptyMVar :: IO (MVar (Either SomeException ()))
  E.mask $ \restore -> do
    tid <- forkIO $ E.try (restore async) >>= putMVar waitVar
    let wait = takeMVar waitVar >>= either E.throwIO return
    restore (body wait) `E.onException` killThread tid

-- This function was copied verbatim from module System.Process of package
-- process-1.6.3.0.
ignoreSigPipe :: IO () -> IO ()
ignoreSigPipe = E.handle $ \e ->
  case e of
    IOError { ioe_type  = ResourceVanished
            , ioe_errno = Just ioe }
      | Errno ioe == ePIPE -> return ()
    _ -> E.throwIO e