【问题标题】:Haskell: How to timeout a function that runs an external commandHaskell:如何使运行外部命令的函数超时
【发布时间】:2012-02-07 21:43:33
【问题描述】:

我在函数内部调用外部程序。现在我想超时这个功能,而不仅仅是外部程序。但是在函数超时后,外部程序仍在我的计算机上运行(我正在使用debian),直到它完成计算,之后它的线程仍然作为我的主程序的子线程保留在进程表中,直到主程序终止。

这里有两个最小的例子来说明我想要做什么。第一个使用 unsafePerformIO,第二个完全在 IO monad 中。我并不真正依赖 unsafePerformIO,但如果可能的话,我想保留它。无论有没有它,都会出现所描述的问题。

使用 unsafePerformIO

module Main where

import System.Timeout
import Criterion.Measurement
import System.IO.Unsafe
import System.Process

main = do
    x <- time $ timeoutP (1 * 1000000) $ mytest 2
    y <- getLine
    putStrLn $ show x ++ y

timeoutP :: Int -> a -> IO (Maybe a)
timeoutP t fun = timeout t $ return $! fun

mytest :: Int -> String
mytest n =
  let
    x = runOnExternalProgram $ n * 1000
  in
    x ++ ". Indeed."

runOnExternalProgram :: Int -> String
runOnExternalProgram n = unsafePerformIO $ do
    -- convert the input to a parameter of the external program
    let x = show $ n + 12
    -- run the external program
    -- (here i use "sleep" to indicate a slow computation)
    answer <- readProcess "sleep" [x] ""
    -- convert the output as needed
    let verboseAnswer = "External program answered: " ++ answer
    return verboseAnswer

没有 unsafePerformIO

module Main where

import System.Timeout
import Criterion.Measurement
import System.IO.Unsafe
import System.Process

main = do
    x <- time $ timeout (1 * 1000000) $ mytest 2
    y <- getLine
    putStrLn $ show x ++ y

mytest :: Int -> IO String
mytest n = do
    x <- runOnExternalProgram $ n * 1000
    return $ x ++ ". Indeed."

runOnExternalProgram :: Int -> IO String
runOnExternalProgram n = do
    -- convert the input to a parameter for the external program:
    let x = show $ n + 12
    -- run the external program
    -- (here i use "sleep" to indicate a slow computation):
    answer <- readProcess "sleep" [x] ""
    -- convert the output as needed:
    let verboseAnswer = "External program answered: " ++ answer
    return verboseAnswer

也许括号在这里可以提供帮助,但我真的不知道如何。

编辑:我采用了 John L 的回答。现在我正在使用以下内容:

import Control.Concurrent
import Control.Exception
import System.Exit
import System.IO
import System.IO.Error
import System.Posix.Signals
import System.Process
import System.Process.Internals

safeCreateProcess :: String -> [String] -> StdStream -> StdStream -> StdStream
                  -> ( ( Maybe Handle
                       , Maybe Handle
                       , Maybe Handle
                       , ProcessHandle
                       ) -> IO a )
                  -> IO a
safeCreateProcess prog args streamIn streamOut streamErr fun = bracket
    ( do
        h <- createProcess (proc prog args) 
                 { std_in  = streamIn
                 , std_out = streamOut
                 , std_err = streamErr
                 , create_group = True }
        return h
    )
-- "interruptProcessGroupOf" is in the new System.Process. Since some
-- programs return funny exit codes i implemented a "terminateProcessGroupOf".
--    (\(_, _, _, ph) -> interruptProcessGroupOf ph >> waitForProcess ph)
    (\(_, _, _, ph) -> terminateProcessGroup ph >> waitForProcess ph)
    fun
{-# NOINLINE safeCreateProcess #-}

safeReadProcess :: String -> [String] -> String -> IO String
safeReadProcess prog args str =
    safeCreateProcess prog args CreatePipe CreatePipe Inherit
      (\(Just inh, Just outh, _, ph) -> do
        hPutStr inh str
        hClose inh
        -- fork a thread to consume output
        output <- hGetContents outh
        outMVar <- newEmptyMVar
        forkIO $ evaluate (length output) >> putMVar outMVar ()
        -- wait on output
        takeMVar outMVar
        hClose outh
        return output
-- The following would be great, if some programs did not return funny
-- exit codes!
--            ex <- waitForProcess ph
--            case ex of
--                ExitSuccess -> return output
--                ExitFailure r ->
--                    fail ("spawned process " ++ prog ++ " exit: " ++ show r)
      )

terminateProcessGroup :: ProcessHandle -> IO ()
terminateProcessGroup ph = do
    let (ProcessHandle pmvar) = ph
    ph_ <- readMVar pmvar
    case ph_ of
        OpenHandle pid -> do  -- pid is a POSIX pid
            signalProcessGroup 15 pid
        otherwise -> return ()

这解决了我的问题。它会在正确的时间杀死衍生进程的所有子进程。

亲切的问候。

【问题讨论】:

标签: haskell timeout external-process


【解决方案1】:

编辑:可以获得生成进程的 pid。您可以使用如下代码执行此操作:

-- highly non-portable, and liable to change between versions
import System.Process.Internals

-- from the finalizer of the bracketed function
-- `ph` is a ProcessHandle as returned by createProcess
  (\(_,_,_,ph) -> do
    let (ProcessHandle pmvar) = ph
    ph_ <- takeMVar pmvar
    case ph_ of
      OpenHandle pid -> do  -- pid is a POSIX pid
        ... -- do stuff
        putMVar pmvar ph_

如果您终止该进程,而不是将打开的 ph_ 放入 mvar,您应该创建一个适当的 ClosedHandle 并将其放回原处。这段代码执行屏蔽很重要(括号会为您执行此操作)。

现在您有了一个 POSIX id,您可以根据需要使用系统调用或 shell out 来杀死。如果你走这条路,请注意你的 Haskell 可执行文件不在同一个进程组中。

/结束编辑

这种行为似乎有点明智。 timeout 的文档声称它根本不适用于非 Haskell 代码,实际上我看不出它有任何通用方法。发生的事情是readProcess 产生了一个新进程,但在等待该进程的输出时超时。似乎readProcess 在异常中止时不会终止生成的进程。这可能是readProcess 中的错误,也可能是设计使然。

作为一种解决方法,我认为您需要自己实现其中的一些。 timeout 通过在生成的线程中引发异步异常来工作。如果您将 runOnExternalProgram 包装在异常处理程序中,您将获得所需的行为。

这里的关键函数是新的runOnExternalProgram,它是您原来的函数和readProcess 的组合。创建一个新的readProcess 会更好(更模块化、更可重用、更可维护),它会在引发异常时终止生成的进程,但我将把它留作练习。

module Main where

import System.Timeout
import Criterion.Measurement
import System.IO.Unsafe
import System.Process
import Control.Exception
import System.IO
import System.IO.Error
import GHC.IO.Exception
import System.Exit
import Control.Concurrent.MVar
import Control.Concurrent

main = do
    x <- time $ timeoutP (1 * 1000000) $ mytest 2
    y <- getLine
    putStrLn $ show x ++ y

timeoutP :: Int -> IO a -> IO (Maybe a)
timeoutP t fun = timeout t $ fun

mytest :: Int -> IO String
mytest n = do
  x <- runOnExternalProgram $ n * 1000
  return $ x ++ ". Indeed."

runOnExternalProgram :: Int -> IO String
runOnExternalProgram n = 
    -- convert the input to a parameter of the external program
    let x = show $ n + 12
    in bracketOnError
        (createProcess (proc "sleep" [x]){std_in = CreatePipe
                                         ,std_out = CreatePipe
                                         ,std_err = Inherit})
        (\(Just inh, Just outh, _, pid) -> terminateProcess pid >> waitForProcess pid)

        (\(Just inh, Just outh, _, pid) -> do
          -- fork a thread to consume output
          output <- hGetContents outh
          outMVar <- newEmptyMVar
          forkIO $ evaluate (length output) >> putMVar outMVar ()

          -- no input in this case
          hClose inh

          -- wait on output
          takeMVar outMVar
          hClose outh

          -- wait for process
          ex <- waitForProcess pid

          case ex of
            ExitSuccess -> do
              -- convert the output as needed
              let verboseAnswer = "External program answered: " ++ output
              return verboseAnswer
            ExitFailure r ->
              ioError (mkIOError OtherError ("spawned process exit: " ++ show r) Nothing Nothing) )

【讨论】:

  • 您必须通过使用Control.Exception.bracket 或手动使用mask 来改进这一点。我可以在createProcess 之后和handle 之前杀死上面的代码并导致代码行为错误。
  • @John L:感谢您对“异步异常”的建议。您的方法在超时后停止执行外部程序,但仍然留下子线程,因为“terminateProcess”之后没有“waitForProcess”。除此之外,它几乎是我在我的问题中编辑的“括号”方法的复制(并且遇到了同样的问题)。我相信“括号”方法不会受到 Chris Kuklewicz 指出的问题的影响。干杯。
  • @ChrisKuklewicz:当然你应该使用bracket 是正确的。现在修好了。我实际上使用了bracketOnError,因为如果主体正常返回,则该进程应该已经终止了。
  • @André:但是僵尸现在进入了!也解决了这个问题。
猜你喜欢
  • 2020-10-31
  • 2012-03-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多