concurrent-output 1.7.9 → 1.10.21
raw patch · 13 files changed
Files
- CHANGELOG +189/−0
- LICENSE +1/−1
- System/Console/Concurrent.hs +3/−3
- System/Console/Concurrent/Internal.hs +45/−84
- System/Console/Regions.hs +147/−42
- System/Process/Concurrent.hs +2/−14
- TODO +53/−0
- Utility/Data.hs +0/−19
- Utility/Exception.hs +1/−3
- Utility/Monad.hs +0/−51
- concurrent-output.cabal +15/−14
- demo2.hs +4/−9
- stmdemo.hs +31/−11
CHANGELOG view
@@ -1,3 +1,192 @@+concurrent-output (1.10.21) unstable; urgency=medium++ * Allow ansi-terminal-1.1++ -- Joey Hess <id@joeyh.name> Mon, 29 Apr 2024 10:15:55 -0400++concurrent-output (1.10.20) unstable; urgency=medium++ * Allow text-2.1.++ -- Joey Hess <id@joeyh.name> Thu, 12 Oct 2023 15:46:36 -0400++concurrent-output (1.10.19) unstable; urgency=medium++ * Support building for WASM.++ -- Joey Hess <id@joeyh.name> Wed, 30 Aug 2023 16:03:03 -0400++concurrent-output (1.10.18) unstable; urgency=medium++ * Avoid some build warnings on Windows.+ * Allow ansi-terminal-1.0++ -- Joey Hess <id@joeyh.name> Mon, 15 May 2023 16:32:50 -0400++concurrent-output (1.10.17) unstable; urgency=medium++ * Fix ansi-terminal dependency to only include >= 0.6.2,+ which is needed for hSupportsANSI.++ -- Joey Hess <id@joeyh.name> Tue, 10 Jan 2023 15:40:11 -0400++concurrent-output (1.10.16) unstable; urgency=medium++ * Allow transformers-0.6+ (Thanks, Janus Troelsen)++ -- Joey Hess <id@joeyh.name> Fri, 20 May 2022 19:31:56 -0400++concurrent-output (1.10.15) unstable; urgency=medium++ * Allow text-2.0++ -- Joey Hess <id@joeyh.name> Fri, 04 Feb 2022 15:32:58 -0400++concurrent-output (1.10.14) unstable; urgency=medium++ * Allow ansi-terminal-0.11.1.++ -- Joey Hess <id@joeyh.name> Wed, 24 Nov 2021 09:18:20 -0400++concurrent-output (1.10.13) unstable; urgency=medium++ * outputConcurrent and errorConcurrent are now safe to call from a thread+ that receives async exceptions.+ * Fix compilation with GHC head.+ (Thanks, Ellie Hermaszewska)++ -- Joey Hess <id@joeyh.name> Mon, 22 Nov 2021 11:19:21 -0400++concurrent-output (1.10.12) unstable; urgency=medium++ * Bugfix: createProcessConcurrent would sometimes send the process's+ output to the console when it was supposed to be piped to a handle.+ (1.7.5 introduced this bug)+ * System.Console.Regions should be safe to use in a thread that+ receives async exceptions; displayConsoleRegions and withConsoleRegion+ have been made async exception safe.+ (However, System.Console.Concurrent is not async exception safe.)+ * Improve documentation about when withConcurrentOutput should be used.+ * Remove some unused code.+ * Allow building with older ansi-terminal versions, not only 0.9+.++ -- Joey Hess <id@joeyh.name> Wed, 10 Jun 2020 17:22:39 -0400++concurrent-output (1.10.11) unstable; urgency=medium++ * Improve documentation of how to use ANSI color codes.+ * Allow ansi-terminal-0.10.++ -- Joey Hess <id@joeyh.name> Sun, 06 Oct 2019 15:14:42 -0400++concurrent-output (1.10.10) unstable; urgency=medium++ * Allow ansi-terminal-0.9.+ * Fix a build warning.++ -- Joey Hess <id@joeyh.name> Sun, 12 May 2019 16:29:18 -0400++concurrent-output (1.10.9) unstable; urgency=medium++ * waitDisplayChange would deadlock if the STM action passed to it did not+ cause any changes to the display. That is now safe to do.++ -- Joey Hess <id@joeyh.name> Mon, 19 Nov 2018 18:40:55 -0400++concurrent-output (1.10.8) unstable; urgency=medium++ * Fix bug in waitDisplayChange that could result in an STM deadlock+ exception.++ -- Joey Hess <id@joeyh.name> Mon, 19 Nov 2018 17:01:12 -0400++concurrent-output (1.10.7) unstable; urgency=medium++ * Allow stm-2.5.++ -- Joey Hess <id@joeyh.name> Sat, 22 Sep 2018 17:14:59 -0400++concurrent-output (1.10.6) unstable; urgency=medium++ * Relax unix bound for ghc 8.6.++ -- Joey Hess <id@joeyh.name> Mon, 02 Jul 2018 10:58:59 -0400++concurrent-output (1.10.5) unstable; urgency=medium++ * Allow exceptions-0.10.++ -- Joey Hess <id@joeyh.name> Tue, 13 Mar 2018 11:50:39 -0400++concurrent-output (1.10.4) unstable; urgency=medium++ * Allow exceptions-0.9.++ -- Joey Hess <id@joeyh.name> Mon, 26 Feb 2018 11:45:08 -0400++concurrent-output (1.10.3) unstable; urgency=medium++ * Allow async-2.2.++ -- Joey Hess <id@joeyh.name> Mon, 05 Feb 2018 11:07:27 -0400++concurrent-output (1.10.2) unstable; urgency=medium++ * Allow ansi-terminal-0.8.++ -- Joey Hess <id@joeyh.name> Thu, 25 Jan 2018 11:51:43 -0400++concurrent-output (1.10.1) unstable; urgency=medium++ * Allow ansi-terminal-0.7.++ -- Joey Hess <id@joeyh.name> Sat, 25 Nov 2017 17:11:50 -0400++concurrent-output (1.10.0) unstable; urgency=medium++ * Simplified by removing workaround for waitForProcess race+ condition (https://github.com/haskell/process/issues/46).+ * Depends on process-1.6.0.0 which fixed that race.+ * ConcurrentProcessHandle is now a type alias for ProcessHandle,+ and waitForProcessConcurrent simply calls waitForProcess.+ These are now only provided to avoid breaking backwards+ compatability.++ -- Joey Hess <id@joeyh.name> Fri, 19 May 2017 11:54:49 -0400++concurrent-output (1.9.2) unstable; urgency=medium++ * Allow process-1.6.0.0.++ -- Joey Hess <id@joeyh.name> Fri, 19 May 2017 11:36:35 -0400++concurrent-output (1.9.1) unstable; urgency=medium++ * Documentation fix: createProcessConcurrent has been available on+ Windows since 1.7.5 but the docs said not.+ * When all regions are closed, flush stdout so that the display gets+ updated, which didn't happen before.++ -- Joey Hess <id@joeyh.name> Tue, 16 May 2017 16:49:43 -0400++concurrent-output (1.9.0) unstable; urgency=medium++ * Replaced displayUpdateNotifier with a simpler and safer+ waitDisplayChange interface.++ -- Joey Hess <id@joeyh.name> Fri, 12 May 2017 17:17:09 -0400++concurrent-output (1.8.0) unstable; urgency=medium++ * Added displayUpdateNotifier, which can be used to wait for+ changes to console regions to be displayed.+ (stmdemo has an example of using that to temporarily shut down the+ region based display to run a bash prompt, and restore the region+ display later.)++ -- Joey Hess <id@joeyh.name> Fri, 12 May 2017 16:27:41 -0400+ concurrent-output (1.7.9) unstable; urgency=medium * Allow lazy text to be used as an Outputable value, and as
LICENSE view
@@ -1,4 +1,4 @@-Copyright © 2015 Joey Hess <id@joeyh.name>+Copyright © 2015-2017 Joey Hess <id@joeyh.name> Copyright © 2009 Joachim Breitner Redistribution and use in source and binary forms, with or without
System/Console/Concurrent.hs view
@@ -7,7 +7,7 @@ -- > import Control.Concurrent.Async -- > import System.Console.Concurrent -- >--- > main = withConcurrentOutput $+-- > main = withConcurrentOutput $ do -- > outputConcurrent "washed the car\n" -- > `concurrently` -- > outputConcurrent "walked the dog\n"@@ -20,12 +20,12 @@ Outputable(..), outputConcurrent, errorConcurrent,- ConcurrentProcessHandle, createProcessConcurrent,- waitForProcessConcurrent, createProcessForeground, flushConcurrentOutput, lockOutput,+ ConcurrentProcessHandle,+ waitForProcessConcurrent, -- * Low level access to the output buffer OutputBuffer, StdHandle(..),
System/Console/Concurrent/Internal.hs view
@@ -40,8 +40,6 @@ , outputBuffer :: TMVar OutputBuffer , errorBuffer :: TMVar OutputBuffer , outputThreads :: TMVar Integer- , processWaiters :: TMVar [Async ()]- , waitForProcessLock :: TMVar () } data Lock = Locked@@ -54,8 +52,6 @@ <*> newTMVarIO (OutputBuffer []) <*> newTMVarIO (OutputBuffer []) <*> newTMVarIO 0- <*> newTMVarIO []- <*> newEmptyTMVarIO -- | Holds a lock while performing an action. This allows the action to -- perform its own output to the console, without using functions from this@@ -106,7 +102,8 @@ dropOutputLock = withLock $ void . takeTMVar -- | Use this around any actions that use `outputConcurrent`--- or `createProcessConcurrent`+-- or `createProcessConcurrent`, unless +-- `System.Console.Regions.displayConsoleRegions` is being used. -- -- This is necessary to ensure that buffered concurrent output actually -- gets displayed before the program exits.@@ -147,15 +144,20 @@ -- | Displays a value to stdout. ----- No newline is appended to the value, so if you want a newline, be sure--- to include it yourself.--- -- Uses locking to ensure that the whole output occurs atomically -- even when other threads are concurrently generating output. --+-- No newline is appended to the value, so if you want a newline, be sure+-- to include it yourself.+-- -- When something else is writing to the console at the same time, this does -- not block. It buffers the value, so it will be displayed once the other -- writer is done.+--+-- When outputConcurrent is used within a call to+-- `System.Console.Regions.displayConsoleRegions`, the output is displayed+-- above the currently open console regions. Only lines ending in a newline+-- are displayed in this case (it uses `waitCompleteLines`). outputConcurrent :: Outputable v => v -> IO () outputConcurrent = outputConcurrent' StdOut @@ -166,7 +168,12 @@ errorConcurrent = outputConcurrent' StdErr outputConcurrent' :: Outputable v => StdHandle -> v -> IO ()-outputConcurrent' stdh v = bracket setup cleanup go+outputConcurrent' stdh v = do+ -- Use a worker thread. This is so any async exception that+ -- is thrown to the current thread does not affect+ -- tryTakeOutputLock, which is not async exception safe.+ worker <- async $ bracket setup cleanup go+ wait worker where setup = tryTakeOutputLock cleanup False = return ()@@ -181,69 +188,13 @@ h = toHandle stdh bv = bufferFor stdh -newtype ConcurrentProcessHandle = ConcurrentProcessHandle P.ProcessHandle--toConcurrentProcessHandle :: (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) -> (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)-toConcurrentProcessHandle (i, o, e, h) = (i, o, e, ConcurrentProcessHandle h)+-- | This alias is provided to avoid breaking backwards compatibility.+type ConcurrentProcessHandle = P.ProcessHandle --- | Use this to wait for processes started with --- `createProcessConcurrent` and `createProcessForeground`, and get their--- exit status.------ Note that such processes are actually automatically waited for--- internally, so not calling this explicitly will not result--- in zombie processes. This behavior differs from `P.waitForProcess`+-- | Same as `P.waitForProcess`; provided to avoid breaking backwards+-- compatibility. waitForProcessConcurrent :: ConcurrentProcessHandle -> IO ExitCode-waitForProcessConcurrent (ConcurrentProcessHandle h) = - bracket lock unlock checkexit- where- lck = waitForProcessLock globalOutputHandle- lock = atomically $ tryPutTMVar lck ()- unlock True = atomically $ takeTMVar lck- unlock False = return ()- checkexit locked = maybe (waitsome locked) return- =<< P.getProcessExitCode h- waitsome True = do- let v = processWaiters globalOutputHandle- l <- atomically $ readTMVar v- if null l- -- Avoid waitAny [] which blocks forever- then P.waitForProcess h- else do- -- Wait for any of the running- -- processes to exit. It may or may not- -- be the one corresponding to the- -- ProcessHandle. If it is,- -- getProcessExitCode will succeed.- void $ tryIO $ waitAny l- checkexit True- waitsome False = do- -- Another thread took the lck first. Wait for that thread to- -- wait for one of the running processes to exit.- atomically $ do- putTMVar lck ()- takeTMVar lck- checkexit False---- Registers an action that waits for a process to exit,--- adding it to the processWaiters list, and removing it once the action--- completes.-asyncProcessWaiter :: IO () -> IO ()-asyncProcessWaiter waitaction = do- regdone <- newEmptyTMVarIO- waiter <- async $ do- self <- atomically (takeTMVar regdone)- waitaction `finally` unregister self- register waiter regdone- where- v = processWaiters globalOutputHandle- register waiter regdone = atomically $ do- l <- takeTMVar v- putTMVar v (waiter:l)- putTMVar regdone waiter- unregister waiter = atomically $ do- l <- takeTMVar v- putTMVar v (filter (/= waiter) l)+waitForProcessConcurrent = P.waitForProcess -- | Wrapper around `System.Process.createProcess` that prevents -- multiple processes that are running concurrently from writing@@ -262,8 +213,10 @@ -- redirected to a buffer. The buffered output will be displayed as soon -- as the output lock becomes free. ----- Currently only available on Unix systems, not Windows.-createProcessConcurrent :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle) +-- Note that the the process is waited for by a background thread,+-- so unlike createProcess, neglecting to call waitForProcess will not+-- result in zombie processess.+createProcessConcurrent :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) createProcessConcurrent p | willOutput (P.std_out p) || willOutput (P.std_err p) = ifM tryTakeOutputLock@@ -272,31 +225,34 @@ ) | otherwise = do r@(_, _, _, h) <- P.createProcess p- asyncProcessWaiter $- void $ tryIO $ P.waitForProcess h- return (toConcurrentProcessHandle r)+ _ <- async $ void $ tryIO $ P.waitForProcess h+ return r -- | Wrapper around `System.Process.createProcess` that makes sure a process -- is run in the foreground, with direct access to stdout and stderr. -- Useful when eg, running an interactive process.-createProcessForeground :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)+--+-- Note that the the process is waited for by a background thread,+-- so unlike createProcess, neglecting to call waitForProcess will not+-- result in zombie processess.+createProcessForeground :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) createProcessForeground p = do takeOutputLock fgProcess p -fgProcess :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)+fgProcess :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) fgProcess p = do r@(_, _, _, h) <- P.createProcess p `onException` dropOutputLock registerOutputThread -- Wait for the process to exit and drop the lock.- asyncProcessWaiter $ do+ _ <- async $ do void $ tryIO $ P.waitForProcess h unregisterOutputThread dropOutputLock- return (toConcurrentProcessHandle r)+ return r -bgProcess :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)+bgProcess :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) bgProcess p = do let p' = p { P.std_out = rediroutput (P.std_out p)@@ -311,15 +267,20 @@ , mungeret (P.std_err p) stderr_h , h )- asyncProcessWaiter $ void $ tryIO $ P.waitForProcess h- outbuf <- setupOutputBuffer StdOut stdout_h- errbuf <- setupOutputBuffer StdErr stderr_h+ -- Wait for the process for symmetry with fgProcess,+ -- which does the same.+ _ <- async $ void $ tryIO $ P.waitForProcess h+ outbuf <- setupOutputBuffer StdOut (mungebuf (P.std_out p) stdout_h)+ errbuf <- setupOutputBuffer StdErr (mungebuf (P.std_err p) stderr_h) void $ async $ bufferWriter [outbuf, errbuf]- return (toConcurrentProcessHandle r)+ return r where rediroutput ss | willOutput ss = P.CreatePipe | otherwise = ss+ mungebuf ss mh+ | willOutput ss = mh+ | otherwise = Nothing mungeret ss mh | willOutput ss = Nothing | otherwise = mh
System/Console/Regions.hs view
@@ -9,7 +9,9 @@ -- License: BSD-2-clause -- -- Console regions are displayed near the bottom of the console, and can be--- updated concurrently by threads. Any other output displayed using+-- updated concurrently by threads. +--+-- Any other output lines displayed using -- `outputConcurrent` and `createProcessConcurrent` -- will scroll up above the open console regions. --@@ -108,6 +110,7 @@ consoleWidth, consoleHeight, regionList,+ waitDisplayChange, ) where import Data.Monoid@@ -123,17 +126,19 @@ import Control.Concurrent.STM.TSem import Control.Concurrent.Async import System.Console.ANSI-import qualified System.Console.Terminal.Size as Console import System.IO import System.IO.Unsafe (unsafePerformIO) import Text.Read-import Data.List+import Data.List (intercalate, nubBy)+import Control.Applicative+import Prelude+#ifdef VERSION_terminal_size+import qualified System.Console.Terminal.Size as Console #ifndef mingw32_HOST_OS import System.Posix.Signals import System.Posix.Signals.Exts #endif-import Control.Applicative-import Prelude+#endif import System.Console.Concurrent import Utility.Monad@@ -142,6 +147,7 @@ -- | Controls how a region is laid out in the console. -- -- Here's an annotated example of how the console layout works.+-- Each sequence of the same letter represents a distinct region. -- -- > scrolling...... -- > scrolling......@@ -178,22 +184,26 @@ regionList :: TMVar [ConsoleRegion] regionList = unsafePerformIO newEmptyTMVarIO --- | On Unix systems, this TVar is automatically updated when the--- terminal is resized. On Windows, it is only initialized on program start--- with the current terminal size.+data ConsoleSize = ConsoleSize+ { _consoleHeight :: Int+ , _consoleWidth :: Int+ }+ {-# NOINLINE consoleSize #-}-consoleSize :: TVar (Console.Window Int)-consoleSize = unsafePerformIO $ newTVarIO $ - Console.Window { Console.width = 80, Console.height = 25}+consoleSize :: TVar ConsoleSize+consoleSize = unsafePerformIO $ newTVarIO $+ ConsoleSize { _consoleWidth = 80, _consoleHeight = 25} type Width = Int -- | Gets the width of the console. -- -- On Unix, this is automatically updated when the terminal is resized.--- On Windows, it is only initialized on program start.+-- On Windows, it is determined at start. On WASM,+-- the console width is hard coded to 80 since WASI does not provide a way+-- to determine it. consoleWidth :: STM Int-consoleWidth = munge . Console.width <$> readTVar consoleSize+consoleWidth = munge . _consoleWidth <$> readTVar consoleSize where #ifndef mingw32_HOST_OS munge = id@@ -204,11 +214,15 @@ #endif -- | Get the height of the console.+--+-- On Unix, this is automatically updated when the terminal is resized.+-- On Windows, it is determined at start. On WASM,+-- the console heigth is hard coded to 25 since WASI does not provide a way+-- to determine it. consoleHeight :: STM Int-consoleHeight = Console.height <$> readTVar consoleSize+consoleHeight = _consoleHeight <$> readTVar consoleSize --- | The RegionList TMVar is left empty when `displayConsoleRegions`--- is not running.+-- | Check if `displayConsoleRegions` is running. regionDisplayEnabled :: IO Bool regionDisplayEnabled = atomically $ not <$> isEmptyTMVar regionList @@ -256,10 +270,26 @@ -- necessary. -- -- The value can include ANSI SGR escape sequences for changing--- the colors etc of all or part of a region.+-- the colors of all or part of a region. For this to display properly,+-- a reset escape sequence must be included to get the color back+-- to default. System.Console.ANSI makes it easy to construct such+-- values. For example:+--+-- > import System.Console.ANSI+-- > +-- > setConsoleRegion region +-- > ( "hello "+-- > <> setSGRCode [SetColor Foreground Vivid Red] +-- > <> "Mars" +-- > <> setSGRCode [Reset]+-- > <> "!"+-- > ) -- -- Other ANSI escape sequences, especially those doing cursor -- movement, will mess up the layouts of regions. Caveat emptor.+--+-- ANSI SGR escape sequences that span multiple lines do not currently+-- display as you might hope. (Patches would be accepted.) setConsoleRegion :: (ToRegionContent v, LiftRegion m) => ConsoleRegion -> v -> m () setConsoleRegion r v = liftRegion $ modifyRegion r $ const $ pure $ toRegionContent v@@ -293,7 +323,9 @@ -- | Runs the action with a new console region, closing the region when -- the action finishes or on exception. withConsoleRegion :: (MonadIO m, MonadMask m) => RegionLayout -> (ConsoleRegion -> m a) -> m a-withConsoleRegion ly = bracketIO (openConsoleRegion ly) (closeConsoleRegion)+withConsoleRegion ly = bracketIO+ (openConsoleRegion ly)+ (uninterruptibleMask_ . closeConsoleRegion) -- | Opens a new console region. openConsoleRegion :: LiftRegion m => RegionLayout -> m ConsoleRegion@@ -402,7 +434,9 @@ -- Note that this uses `lockOutput`, so it takes over all output to the -- console while the passed IO action is running. As well as displaying -- the console regions, this handles display of anything buffered by--- `outputConcurrent` and `createProcessConcurrent`.+-- `outputConcurrent` and `createProcessConcurrent`. So,+-- `withConcurrentOutput` and `flushConcurrentOutput` should not be run+-- while this is in use, and will block. -- -- When standard output is not an ANSI capable terminal, -- console regions are not displayed.@@ -412,17 +446,17 @@ , lockOutput $ bracket setup cleanup (const a) ) where- setup = liftIO $ do+ setup = liftIO $ uninterruptibleMask $ \unmask -> do atomically $ putTMVar regionList [] endsignal <- atomically $ do s <- newTSem 1 waitTSem s return s isterm <- liftIO $ hSupportsANSI stdout- when isterm trackConsoleWidth- da <- async $ displayThread isterm endsignal+ when isterm (unmask trackConsoleWidth)+ da <- async $ unmask $ displayThread isterm endsignal return (isterm, da, endsignal)- cleanup (isterm, da, endsignal) = liftIO $ do+ cleanup (isterm, da, endsignal) = liftIO $ uninterruptibleMask_ $ do atomically $ signalTSem endsignal void $ wait da void $ atomically $ takeTMVar regionList@@ -430,48 +464,107 @@ installResizeHandler Nothing trackConsoleWidth :: IO ()+#ifdef VERSION_terminal_size trackConsoleWidth = do- let getwidth = maybe noop (atomically . writeTVar consoleSize)+ let getsz = maybe noop (atomically . writeTVar consoleSize . conv) =<< Console.size- getwidth- installResizeHandler (Just getwidth)+ getsz+ installResizeHandler (Just getsz)+ where+ conv wsz = ConsoleSize+ { _consoleWidth = Console.width wsz+ , _consoleHeight = Console.height wsz+ }+#else+trackConsoleWidth = return ()+#endif data DisplayChange- = BufferChange (StdHandle, OutputBuffer)+ = BufferChange BufferSnapshot | RegionChange RegionSnapshot+ | RegionListChange RegionSnapshot | TerminalResize Width- | EndSignal ()+ | Shutdown+ | DisplayChangeBarrier Barrier +type BufferSnapshot = (StdHandle, OutputBuffer) type RegionSnapshot = ([ConsoleRegion], [R], [[Text]])+type Barrier = Integer +-- | This is a broadcast TChan, which gets a DisplayChange written to it+-- after the display has been updated. It can be used to wait for something+-- to be displayed.+{-# NOINLINE displayUpdateNotifier #-}+displayUpdateNotifier :: TChan DisplayChange+displayUpdateNotifier = unsafePerformIO $ newBroadcastTChanIO++{-# NOINLINE displayChangeBarrier #-}+displayChangeBarrier :: TVar Barrier+displayChangeBarrier = unsafePerformIO $ newTVarIO 0++-- | Runs a STM action, and waits for the display to be fully updated+-- with any changes that action makes to the displayed regions.+waitDisplayChange :: STM a -> IO a+waitDisplayChange a = do+ c <- atomically $ dupTChan displayUpdateNotifier+ bv <- newEmptyTMVarIO+ _ <- setbarrier bv `concurrently` waitchange c bv+ snd <$> atomically (readTMVar bv)+ where+ setbarrier bv = atomically $ do+ !b <- succ <$> readTVar displayChangeBarrier+ r <- a+ writeTVar displayChangeBarrier b+ putTMVar bv (b, r)+ waitchange c bv = do+ change <- atomically $ readTChan c+ -- this blocks until the STM action has run, and the+ -- barrier is set.+ b <- fst <$> atomically (readTMVar bv)+ case change of+ DisplayChangeBarrier b' | b' >= b -> return ()+ _ -> waitchange c bv+ displayThread :: Bool -> TSem -> IO () displayThread isterm endsignal = do origwidth <- atomically consoleWidth- go ([], [], []) origwidth+ origbarrier <- atomically (readTVar displayChangeBarrier)+ go ([], [], []) origwidth origbarrier where- go origsnapshot@(orighandles, origregions, origlines) origwidth = do+ go origsnapshot@(orighandles, origregions, origlines) origwidth origbarrier = do let waitwidthchange = do w <- consoleWidth if w == origwidth then retry else return w+ let waitbarrierchange = do+ b <- readTVar displayChangeBarrier+ if b /= origbarrier+ then return b+ else retry let waitanychange = (RegionChange <$> regionWaiter origsnapshot origwidth) `orElse`- (RegionChange <$> regionListWaiter origsnapshot)+ (RegionListChange <$> regionListWaiter origsnapshot) `orElse` (BufferChange <$> outputBufferWaiterSTM waitCompleteLines) `orElse` (TerminalResize <$> waitwidthchange) `orElse`- (EndSignal <$> waitTSem endsignal)+ (waitTSem endsignal >> pure Shutdown)+ `orElse`+ -- Must come last, so the changes above are+ -- processed before barriers.+ (DisplayChangeBarrier <$> waitbarrierchange) (change, height) <- atomically $ (,) <$> waitanychange <*> consoleHeight let onscreen = take (height - 1) . concat- case change of- RegionChange snapshot@(_, _, newlines) -> do- when isterm $ do- changedLines (onscreen origlines) (onscreen newlines)- go snapshot origwidth+ let update snapshot@(_, _, newlines) = do+ when isterm $+ changedLines (onscreen origlines) (onscreen newlines)+ return $ go snapshot origwidth origbarrier+ next <- case change of+ RegionChange snapshot -> update snapshot+ RegionListChange snapshot -> update snapshot BufferChange (h, buf) -> do -- Note that even when every available line -- is dedicated to visible regions, the@@ -481,13 +574,19 @@ let origlines' = onscreen origlines inAreaAbove isterm (length origlines') origlines' $ emitOutputBuffer h buf- go origsnapshot origwidth+ return $ go origsnapshot origwidth origbarrier TerminalResize newwidth -> do newlines <- atomically (mapM (resizeRegion newwidth) orighandles) when isterm $ do resizeRecovery (onscreen newlines)- go (orighandles, origregions, newlines) newwidth- EndSignal () -> return ()+ return $ go (orighandles, origregions, newlines) newwidth origbarrier+ Shutdown ->+ return $ return ()+ DisplayChangeBarrier b ->+ return $ go origsnapshot origwidth b+ hFlush stdout+ atomically $ writeTChan displayUpdateNotifier change+ next readRegions :: [ConsoleRegion] -> STM [R] readRegions = mapM (\(ConsoleRegion h) -> readTVar h)@@ -576,7 +675,6 @@ #endif cursorDown (sum (map (snd . fst) l')) setCursorColumn 0- hFlush stdout where l' = changeOffsets l 1 [] @@ -608,7 +706,6 @@ when isterm $ do setCursorColumn 0 -- just in case the output lacked a newline displayLines (reverse ls)- hFlush stdout displayLines :: [Text] -> IO () displayLines = mapM_ $ \l -> do@@ -617,11 +714,15 @@ installResizeHandler :: Maybe (IO ()) -> IO () #ifndef mingw32_HOST_OS+#ifdef VERSION_terminal_size installResizeHandler h = void $ installHandler windowChange (maybe Default Catch h) Nothing #else installResizeHandler _ = return () #endif+#else+installResizeHandler _ = return ()+#endif calcRegionLines :: R -> Width -> STM [Text] calcRegionLines r width = do@@ -694,6 +795,8 @@ endSGR :: Char endSGR = 'm' +#ifndef mingw32_HOST_OS+ -- | Finds the least expensive output to make a console that was displaying -- the old line display the new line. Cursor starts at far left. --@@ -829,3 +932,5 @@ | x >= 90 && x <= 97 = Just (Foreground, x) | x >= 100 && x <= 107 = Just (Background, x) | otherwise = Nothing++#endif
System/Process/Concurrent.hs view
@@ -9,26 +9,14 @@ module System.Process.Concurrent where import System.Console.Concurrent-import System.Console.Concurrent.Internal (ConcurrentProcessHandle(..)) import System.Process hiding (createProcess, waitForProcess) import System.IO import System.Exit -- | Calls `createProcessConcurrent`------ You should use the waitForProcess in this module on the resulting--- ProcessHandle. Using System.Process.waitForProcess instead can have--- mildly unexpected results. createProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)-createProcess p = do- (i, o, e, ConcurrentProcessHandle h) <- createProcessConcurrent p- return (i, o, e, h)+createProcess = createProcessConcurrent -- | Calls `waitForProcessConcurrent`------ You should only use this on a ProcessHandle obtained by calling--- createProcess from this module. Using this with a ProcessHandle--- obtained from System.Process.createProcess etc will have extremely--- unexpected results; it can wait a very long time before returning. waitForProcess :: ProcessHandle -> IO ExitCode-waitForProcess = waitForProcessConcurrent . ConcurrentProcessHandle+waitForProcess = waitForProcessConcurrent
TODO view
@@ -0,0 +1,53 @@+* System.Console.Regions handling of outputConcurrent and errorConcurrent+ when the output lacks a newline is surprising. The output may never be+ displayed. A better approach:++ When output arrives that lacks a newline, allocate a region for it+ and buffer it in that region, so it displays immediately and more+ output will go to the same region. When a newline arrives, close the+ region and scroll up the completed line the same way any completed region+ is displayed.++* Parts of System.Console.Concurrent may not be async exception safe.++ If a thread is running an action from that module and an async exception+ is sent to it, it may result in a deadlock or other problem.++ (System.Console.Regions has been made safe,+ also outputConcurrent and errorConcurrent.)++ Particularly problematic is takeOutputLock', which takes the lock+ and then outputs buffers to the console. If the emitOutputBuffer+ calls are interrupted by async exception, it will be left locked+ and the buffered output is also lost. But masking them is not good+ because emitOutputBuffer could run for a long time in some situations.++ fgProcess and bgProcess also do stuff with registerOutputThread/unregisterOutputThread+ that may not be async exception safe. ++ And createProcessForeground uses takeOutputLock but then calls fgProcess,+ which could be interrupted (during its call to registerOutputThread)+ before it starts the async thread that drops the lock.++ One approach to all this might be to fork off a worker thread,+ which will thus be immune to any async exception directed at the calling+ thread.++* Calling setConsoleRegion with something that throws an error+ will cause no further display updates to happen.+ + The exception in the thunk will crash the displayThread,+ and that is not waited on until the action passed to displayConsoleRegions+ finishes, so the exception is deferred to that point. Of course if the+ action never finishes, that hides the exception.++ It does seem this should be improved, by catching the crashing+ displayThread and ideally propigating the exception immediately.+ But displayConsoleRegions would need to use a different class than+ the current MonadIO to be able to do that. Eg, MonadBaseControl+ so lifted-async can be used, or a class that lets the action be+ canceled.++ Or it could be dealt with by having setConsoleRegion force the thunk+ before passing it over to the displayThread. But the instance+ ToRegionContent (STM Text) seems to prevent doing that.
− Utility/Data.hs
@@ -1,19 +0,0 @@-{- utilities for simple data types- -- - Copyright 2013 Joey Hess <id@joeyh.name>- -- - License: BSD-2-clause- -}--{-# OPTIONS_GHC -fno-warn-tabs #-}--module Utility.Data where--{- First item in the list that is not Nothing. -}-firstJust :: Eq a => [Maybe a] -> Maybe a-firstJust ms = case dropWhile (== Nothing) ms of- [] -> Nothing- (md:_) -> md--eitherToMaybe :: Either a b -> Maybe b-eitherToMaybe = either (const Nothing) Just
Utility/Exception.hs view
@@ -31,8 +31,6 @@ import System.IO.Error (isDoesNotExistError, ioeGetErrorType) import GHC.IO.Exception (IOErrorType(..)) -import Utility.Data- {- Catches IO errors and returns a Bool -} catchBoolIO :: MonadCatch m => m Bool -> m Bool catchBoolIO = catchDefaultIO False@@ -86,7 +84,7 @@ tryWhenExists :: MonadCatch m => m a -> m (Maybe a) tryWhenExists a = do v <- tryJust (guard . isDoesNotExistError) a- return (eitherToMaybe v)+ return (either (const Nothing) Just v) {- Catches only exceptions caused by hardware faults. - Ie, disk IO error. -}
Utility/Monad.hs view
@@ -9,62 +9,11 @@ module Utility.Monad where -import Data.Maybe-import Control.Monad--{- Return the first value from a list, if any, satisfying the given- - predicate -}-firstM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)-firstM _ [] = return Nothing-firstM p (x:xs) = ifM (p x) (return $ Just x , firstM p xs)--{- Runs the action on values from the list until it succeeds, returning- - its result. -}-getM :: Monad m => (a -> m (Maybe b)) -> [a] -> m (Maybe b)-getM _ [] = return Nothing-getM p (x:xs) = maybe (getM p xs) (return . Just) =<< p x--{- Returns true if any value in the list satisfies the predicate,- - stopping once one is found. -}-anyM :: Monad m => (a -> m Bool) -> [a] -> m Bool-anyM p = liftM isJust . firstM p--allM :: Monad m => (a -> m Bool) -> [a] -> m Bool-allM _ [] = return True-allM p (x:xs) = p x <&&> allM p xs--{- Runs an action on values from a list until it succeeds. -}-untilTrue :: Monad m => [a] -> (a -> m Bool) -> m Bool-untilTrue = flip anyM- {- if with a monadic conditional. -} ifM :: Monad m => m Bool -> (m a, m a) -> m a ifM cond (thenclause, elseclause) = do c <- cond if c then thenclause else elseclause--{- short-circuiting monadic || -}-(<||>) :: Monad m => m Bool -> m Bool -> m Bool-ma <||> mb = ifM ma ( return True , mb )--{- short-circuiting monadic && -}-(<&&>) :: Monad m => m Bool -> m Bool -> m Bool-ma <&&> mb = ifM ma ( mb , return False )--{- Same fixity as && and || -}-infixr 3 <&&>-infixr 2 <||>--{- Runs an action, passing its value to an observer before returning it. -}-observe :: Monad m => (a -> m b) -> m a -> m a-observe observer a = do- r <- a- _ <- observer r- return r--{- b `after` a runs first a, then b, and returns the value of a -}-after :: Monad m => m b -> m a -> m a-after = observe . const {- do nothing -} noop :: Monad m => m ()
concurrent-output.cabal view
@@ -1,14 +1,14 @@ Name: concurrent-output-Version: 1.7.9-Cabal-Version: >= 1.8+Version: 1.10.21+Cabal-Version: >= 1.10 License: BSD2 Maintainer: Joey Hess <id@joeyh.name> Author: Joey Hess, Joachim Breitner Stability: Stable-Copyright: 2015 Joey Hess, 2009 Joachim Breitner+Copyright: 2015-2021 Joey Hess, 2009 Joachim Breitner License-File: LICENSE Build-Type: Simple-Category: User Interfaces+Category: User Interfaces, Concurrency Synopsis: Ungarble output from several threads or commands Description: Lets multiple threads and external processes concurrently output to the@@ -29,17 +29,17 @@ stmdemo.hs Library+ Default-Language: Haskell2010 GHC-Options: -Wall -fno-warn-tabs -O2 Build-Depends: base (>= 4.6), base < 5- , text (>= 0.11.0 && < 1.3.0)- , async (>= 2.0 && < 2.2)- , stm (>= 2.0 && < 2.5)- , process (>= 1.1.0 && < 1.5.0)+ , text (>= 0.11.0 && < 2.2)+ , async (>= 2.0 && < 2.3)+ , stm (>= 2.0 && < 2.6)+ , process (>= 1.6.0 && < 1.7.0) , directory (>= 1.2.0 && < 1.4.0)- , transformers (>= 0.3.0 && < 0.6.0)- , exceptions (>= 0.6.0 && < 0.9.0)- , ansi-terminal (>= 0.6.0 && < 0.7.0)- , terminal-size (>= 0.3.0 && < 0.4.0)+ , transformers (>= 0.3.0 && < 0.7.0)+ , exceptions (>= 0.6.0 && < 0.11.0)+ , ansi-terminal (>= 0.6.2 && < 1.2.0) Exposed-Modules: System.Console.Concurrent System.Console.Concurrent.Internal@@ -47,11 +47,12 @@ System.Process.Concurrent Other-Modules: Utility.Monad- Utility.Data Utility.Exception if (! os(Windows))- Build-Depends: unix (>= 2.7.0 && < 2.8.0)+ Build-Depends: unix (>= 2.7.0 && < 2.9.0)+ if (! arch(wasm32))+ Build-Depends: terminal-size (>= 0.3.0 && < 0.4.0) source-repository head type: git
demo2.hs view
@@ -1,21 +1,16 @@ import Control.Concurrent.Async import Control.Concurrent-import System.Console.Concurrent import System.Console.Regions-import System.Console.Terminal.Size-import qualified Data.Text as T-import Control.Concurrent.STM-import Control.Applicative-import Data.Time.Clock import Control.Monad -main = displayConsoleRegions $ mapConcurrently id+main :: IO ()+main = displayConsoleRegions $ void $ mapConcurrently id [ spinner 100 1 "Pinwheels!!" setConsoleRegion "/-\\|" (withtitle 1) , spinner 100 1 "Bubbles!!!!" setConsoleRegion ".oOo." (withtitle 1) , spinner 100 1 "Dots......!" appendConsoleRegion "." (const (take 3)) , spinner 30 2 "KleisiFish?" setConsoleRegion " <=< <=< " (withtitle 10)- , spinner 10 9 "Countdowns!" setConsoleRegion- (reverse [1..10])+ , spinner 9 9 "Countdowns!" setConsoleRegion+ (reverse ([1..10] :: [Int])) (\t n -> t ++ show (head n)) ] where
stmdemo.hs view
@@ -7,15 +7,23 @@ import Data.Time.Clock import Control.Monad import Data.Monoid+import System.Process main :: IO () main = void $ displayConsoleRegions $ do+ void titleRegion ir <- infoRegion cr <- clockRegion rr <- rulerRegion- growingDots+ growingDots `concurrently` runBash mapM_ closeConsoleRegion [ir, cr] +titleRegion :: IO ConsoleRegion+titleRegion = do+ r <- openConsoleRegion Linear+ setConsoleRegion r "STM demo!"+ return r+ infoRegion :: IO ConsoleRegion infoRegion = do r <- openConsoleRegion Linear@@ -48,24 +56,36 @@ rightAlign r return r +rulerRegion :: IO ConsoleRegion+rulerRegion = do+ r <- openConsoleRegion Linear+ setConsoleRegion r $ do+ width <- consoleWidth+ return $ T.pack $ take width nums+ return r+ where+ nums = cycle $ concatMap show [0..9]+ rightAlign :: ConsoleRegion -> STM () rightAlign r = tuneDisplay r $ \t -> do w <- consoleWidth return (T.replicate (w - T.length t) (T.singleton ' ') <> t) +growingDots :: IO () growingDots = withConsoleRegion Linear $ \r -> do atomically $ rightAlign r width <- atomically consoleWidth- replicateM width $ do+ void $ replicateM width $ do appendConsoleRegion r "." threadDelay (100000) -rulerRegion :: IO ConsoleRegion-rulerRegion = do- r <- openConsoleRegion Linear- setConsoleRegion r $ do- width <- consoleWidth- return $ T.pack $ take width nums- return r- where- nums = cycle $ concatMap show [0..9]+runBash :: IO ()+runBash = do+ -- Wait for growingDots to display some.+ threadDelay 1000000+ -- Temporarily clear whatever console regions are open.+ rs <- waitDisplayChange $ swapTMVar regionList []+ putStrLn "We interrupt this demo to run a shell prompt. exit to continue!"+ callCommand "bash"+ -- Restore the console regions.+ void $ atomically $ swapTMVar regionList rs