英文:
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 "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
答案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 "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")
-- | Create a new region and run a process in it. Wait until the process is complete
createProcessWithNewRegion :: RegionLayout
-> CreateProcess
-> 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 -> 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)
-- 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't explicitly tell it to save the output,
-- it will be cleared when the process is done
finalText <- 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 -> 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
{- try specialized for IO errors only -}
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
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论