使用concurrent-output同时运行多个进程,输出到不同的控制台区域。

huangapple go评论55阅读模式
英文:

Run several processes concurrently outputting to different console regions using concurrent-output

问题

I'd like to run several processes in parallel while avoiding interleaving output. Using ConsoleRegion from the concurrent-output package seems to be a good candidate for that, but I don't see a way to start a process and have all output in a console region.

Is it possible to redirect the output of a process to a console region with concurrent-output? Or is there some equivalent that allows me to do that?

As mentioned in the comments, there is createProcessConcurrent, but that doesn't quite do what I'm trying to do. Given the following code:

import Control.Concurrent
import Control.Concurrent.Async
import Control.Monad
import Data.Functor
import System.Console.ANSI
import System.Console.Concurrent
import System.Console.Regions
import System.Process

run :: IO ()
run = void $ displayConsoleRegions $ do
  mapConcurrently download [1 .. 10]
    `concurrently` createProcessConcurrent (shell "foo.sh")
    `concurrently` createProcessConcurrent (shell "bar.sh")

download :: Int -> IO ()
download n = withConsoleRegion Linear $ \r -> do
  setConsoleRegion r basemsg
  go n r
  where
    basemsg = "Download " ++ show n
    go c r
      | c < 1 = finishConsoleRegion r (basemsg ++ " done!")
      | otherwise = do
          threadDelay 1000000
          appendConsoleRegion r " ... "
          go (c - 1) r

And the scripts

echo "foo"
sleep 1
echo "foo"
sleep 1
echo "foo"
sleep 1
echo "bar"
sleep 1
echo "bar"
sleep 1
echo "bar"
sleep 1
echo "bar"
sleep 1
echo "bar"
sleep 1

This is adapted from the docs of concurrent-output. The scripts are just simulating some longer running scrips with regular output. What this code does is to print the download part regularly (like intended and described in the docs), but it shows the output from the scripts as a whole (which is fine) only after the script has finished (which is not fine).
Instead, I'd like to have a ConsoleRegion for each script where the output appears as soon as the script emits it.

Context: I'm trying to build a command line utility similar to the git-run npm package, but running the command for several repositories in parallel.

英文:

I'd like to run several processes in parallel while avoiding interleaving output. Using ConsoleRegion from the concurrent-output package seems to be a good candidate for that, but I don't see a way to start a process and have all output in a console region.

Is it possible to redirect the output of a process to a console region with concurrent-output? Or is there some equivalent that allows me to do that?

As mentioned in the comments, there is createProcessConcurrent, but that doesn't quite do what I'm trying to do. Given the following code:

import Control.Concurrent
import Control.Concurrent.Async
import Control.Monad
import Data.Functor
import System.Console.ANSI
import System.Console.Concurrent
import System.Console.Regions
import System.Process

run :: IO ()
run = void $ displayConsoleRegions $ do
  mapConcurrently download [1 .. 10]
    `concurrently` createProcessConcurrent (shell &quot;foo.sh&quot;)
    `concurrently` createProcessConcurrent (shell &quot;bar.sh&quot;)

download :: Int -&gt; IO ()
download n = withConsoleRegion Linear $ \r -&gt; do
  setConsoleRegion r basemsg
  go n r
  where
    basemsg = &quot;Download &quot; ++ show n
    go c r
      | c &lt; 1 = finishConsoleRegion r (basemsg ++ &quot; done!&quot;)
      | otherwise = do
          threadDelay 1000000
          appendConsoleRegion r &quot; ... &quot;
          go (c - 1) r

And the scripts

echo &quot;foo&quot;
sleep 1
echo &quot;foo&quot;
sleep 1
echo &quot;foo&quot;
sleep 1
echo &quot;bar&quot;
sleep 1
echo &quot;bar&quot;
sleep 1
echo &quot;bar&quot;
sleep 1
echo &quot;bar&quot;
sleep 1
echo &quot;bar&quot;
sleep 1

This is adapted from the docs of concurrent-output. The scripts are just simulating some longer running scrips with regular output. What this code does is to print the download part regularly (like intended and described in the docs), but it shows the output from the scripts as a whole (which is fine) only after the script has finished (which is not fine).
Instead, I'd like to have a ConsoleRegion for each script where the output appears as soon as the script emits it.

Context: I'm trying to build a command line utility similar to the git-run npm package, but running the command for several repositories in parallel

答案1

得分: 0

以下是翻译好的部分:

似乎没有内置支持将进程的输出重定向到区域,但仍然可以在无需修改库的情况下添加对此的支持。这里是一个我制作的粗糙版本,通过复制`createProcessConcurrently`的实现并修改它,直接将输出输出到一个区域而不是缓冲区。出于简单起见,我还将调用阻塞,这样我们就不必担心何时关闭区域等。

```haskell

import Control.Concurrent.Async
import Control.Concurrent
import System.Console.Concurrent
import System.Console.Regions
import System.Process as P
import System.IO (Handle, hClose)
import Control.Monad (void)
import Control.Exception (try, IOException)
import qualified Data.Text.IO as T
import qualified Data.Text as T
import System.Exit (ExitCode)

main = displayConsoleRegions $ do
    mapConcurrently download [20,40..100]
        `concurrently` createProcessWithNewRegion Linear (shell "echo hello 1; sleep 1; echo world 1")
        `concurrently` createProcessWithNewRegion Linear (shell "echo hello 2; sleep 3; echo world 2")
        `concurrently` createProcessWithNewRegion Linear (shell "echo hello 3; sleep 5; echo world 3")

-- | 创建一个新的区域并在其中运行进程。等待进程完成
createProcessWithNewRegion :: RegionLayout
    -> CreateProcess
    -> IO (Either IOException ExitCode)
createProcessWithNewRegion layout = withConsoleRegion layout . bgProcessRegion

-- | 阻塞:创建一个进程并将标准输出和标准错误重定向到一个区域
-- 当进程完成时,关闭区域并保留输出
bgProcessRegion :: CreateProcess -> ConsoleRegion -> IO (Either IOException ExitCode)
bgProcessRegion p reg = do
    let p' = p
            { P.std_out = P.CreatePipe
            , P.std_err = P.CreatePipe
            }
    (stdin_h, stdout_h, stderr_h, h) <- P.createProcess p'
    let r = (stdin_h, Nothing, Nothing, h)

    -- 将标准输出和标准错误发送到区域
    -- 等待两个句柄都关闭后再继续
    setupOutputBuffer StdOut reg stdout_h
      `concurrently_` setupOutputBuffer StdErr reg stderr_h

    -- 如果我们不明确告诉它保存输出,
    -- 进程完成后它将被清除
    finalText <- getConsoleRegion reg
    finishConsoleRegion reg finalText

    -- 等待进程终止
    tryIO (P.waitForProcess h)

-- | 一旦到达,将所有文本从句柄发送到区域
setupOutputBuffer :: StdHandle -> ConsoleRegion -> Maybe Handle -> IO ()
setupOutputBuffer h reg Nothing = pure ()
setupOutputBuffer h reg (Just fromh) = go
  where
    go = do
        t <- T.hGetChunk fromh
        if T.null t
            then do
                hClose fromh
            else do
                appendConsoleRegion reg t
                go

{- 专门用于IO错误的尝试 -}
tryIO :: IO a -> IO (Either IOException a)
tryIO = try

download :: Int -> IO ()
download n = withConsoleRegion Linear $ \r -> do
   setConsoleRegion r basemsg
   go n r
  where
   basemsg = "Download " ++ show n
   go c r
       | c < 1 = finishConsoleRegion r (basemsg ++ " done!")
       | otherwise = do
           threadDelay 100000
           appendConsoleRegion r "."
           go (c-1) r
英文:

There seems to be no built in support for redirecting the output of a process to a region, but it's still possible to add support for it without needing to modify the library. Here's a crude version that I made by copying the implementation of createProcessConcurrently and modifying it to output directly to a region instead of buffering. I also made the call blocking for the sake of simplicity, so we don't have to worry about when to close the region etc.


import Control.Concurrent.Async
import Control.Concurrent
import System.Console.Concurrent
import System.Console.Regions
import System.Process as P
import System.IO (Handle, hClose)
import Control.Monad (void)
import Control.Exception (try, IOException)
import qualified Data.Text.IO as T
import qualified Data.Text as T
import System.Exit (ExitCode)

main = displayConsoleRegions $ do
    mapConcurrently download [20,40..100]
        `concurrently` createProcessWithNewRegion Linear (shell &quot;echo hello 1; sleep 1; echo world 1&quot;)
        `concurrently` createProcessWithNewRegion Linear (shell &quot;echo hello 2; sleep 3; echo world 2&quot;)
        `concurrently` createProcessWithNewRegion Linear (shell &quot;echo hello 3; sleep 5; echo world 3&quot;)

-- | Create a new region and run a process in it. Wait until the process is complete
createProcessWithNewRegion :: RegionLayout
    -&gt; CreateProcess
    -&gt; IO (Either IOException ExitCode)
createProcessWithNewRegion layout = withConsoleRegion layout . bgProcessRegion

-- | Blocking: Create a process and redirect both stdout and stderr to a region
-- when the process is completed, close the region and preserve the output
bgProcessRegion :: CreateProcess -&gt; ConsoleRegion -&gt; IO (Either IOException ExitCode)
bgProcessRegion p reg = do
    let p&#39; = p
            { P.std_out = P.CreatePipe
            , P.std_err = P.CreatePipe
            }
    (stdin_h, stdout_h, stderr_h, h) &lt;- P.createProcess p&#39;
    let r = ( stdin_h , Nothing , Nothing , h)

    -- Send both stdout and stderr to the region
    -- wait until both handles are closed before continuing
    setupOutputBuffer StdOut reg stdout_h
      `concurrently_` setupOutputBuffer StdErr reg stderr_h

    -- If we don&#39;t explicitly tell it to save the output,
    -- it will be cleared when the process is done
    finalText &lt;- getConsoleRegion reg
    finishConsoleRegion reg finalText

    -- Wait until the process is terminated
    tryIO (P.waitForProcess h)

-- | Send all text from a handle to a region as soon as it arrives
setupOutputBuffer :: StdHandle -&gt; ConsoleRegion -&gt; Maybe Handle -&gt; IO ()
setupOutputBuffer h reg Nothing = pure ()
setupOutputBuffer h reg (Just fromh) = go
  where
    go = do
        t &lt;- T.hGetChunk fromh
        if T.null t
            then do
                hClose fromh
            else do
                appendConsoleRegion reg t
                go


{- try specialized for IO errors only -}
tryIO :: IO a -&gt; IO (Either IOException a)
tryIO = try


download :: Int -&gt; IO ()
download n = withConsoleRegion Linear $ \r -&gt; do
   setConsoleRegion r basemsg
   go n r
  where
   basemsg = &quot;Download &quot; ++ show n
   go c r
       | c &lt; 1 = finishConsoleRegion r (basemsg ++ &quot; done!&quot;)
       | otherwise = do
           threadDelay 100000
           appendConsoleRegion r &quot;.&quot;
           go (c-1) r

huangapple
  • 本文由 发表于 2023年6月19日 19:46:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/76506306.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定