16

関数内で外部プログラムを呼び出します。ここで、外部プログラムだけでなく、この関数をタイムアウトしたいと思います。しかし、関数がタイムアウトした後も、外部プログラムは計算が完了するまでコンピューター上で実行され続けます(私はdebianを使用しています)。その後、そのスレッドはメインプログラムまでメインプログラムのサブスレッドとしてプロセステーブルに残ります。終了します。

これが私がやりたいことを説明する2つの最小限の例です。1つ目はunsafePerformIOを使用し、2つ目は完全にIOモナドにあります。私は実際には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

ブラケットはここで役立つかもしれませんが、私は本当に方法がわかりません。

編集:私はジョン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 ()

これで私の問題は解決します。生成されたプロセスのすべての子プロセスと適切なタイミングでそれを強制終了します。

敬具。

4

1 に答える 1

9

編集:生成されたプロセスの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を取得したので、必要に応じてシステムコールまたはシェルアウトを使用して強制終了できます。そのルートに行く場合は、Haskell実行可能ファイルが同じプロセスグループにないことに注意してください。

/ end edit

この振る舞いは、ある意味賢明なようです。のドキュメントは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) )
于 2012-01-11T15:48:29.783 に答える