packages feed

concurrent-output 1.1.0 → 1.2.0

raw patch · 11 files changed

+938/−627 lines, 11 filesdep −bytestringdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies removed: bytestring

Dependency ranges changed: base

API changes (from Hackage documentation)

- System.Console.Concurrent: instance GHC.Classes.Eq System.Console.Concurrent.AtEnd
- System.Console.Concurrent: instance GHC.Classes.Eq System.Console.Concurrent.OutputBuffer
- System.Console.Concurrent: instance GHC.Classes.Eq System.Console.Concurrent.OutputBufferedActivity
- System.Console.Concurrent: instance System.Console.Concurrent.Outputable Data.ByteString.Internal.ByteString
- System.Console.Concurrent: instance System.Console.Concurrent.Outputable Data.Text.Internal.Text
- System.Console.Concurrent: instance System.Console.Concurrent.Outputable GHC.Base.String
+ System.Console.Concurrent: createProcessForeground :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)
+ System.Console.Concurrent: data ConcurrentProcessHandle
+ System.Console.Concurrent.Internal: AtEnd :: AtEnd
+ System.Console.Concurrent.Internal: BufSig :: BufSig
+ System.Console.Concurrent.Internal: ConcurrentProcessHandle :: ProcessHandle -> ConcurrentProcessHandle
+ System.Console.Concurrent.Internal: InTempFile :: FilePath -> Bool -> OutputBufferedActivity
+ System.Console.Concurrent.Internal: Locked :: Lock
+ System.Console.Concurrent.Internal: Output :: Text -> OutputBufferedActivity
+ System.Console.Concurrent.Internal: OutputBuffer :: [OutputBufferedActivity] -> OutputBuffer
+ System.Console.Concurrent.Internal: OutputHandle :: TMVar Lock -> TMVar OutputBuffer -> TMVar OutputBuffer -> TMVar Integer -> TMVar [Async ()] -> TMVar () -> OutputHandle
+ System.Console.Concurrent.Internal: StdErr :: StdHandle
+ System.Console.Concurrent.Internal: StdOut :: StdHandle
+ System.Console.Concurrent.Internal: [endsInNewLine] :: OutputBufferedActivity -> Bool
+ System.Console.Concurrent.Internal: [errorBuffer] :: OutputHandle -> TMVar OutputBuffer
+ System.Console.Concurrent.Internal: [outputBuffer] :: OutputHandle -> TMVar OutputBuffer
+ System.Console.Concurrent.Internal: [outputLock] :: OutputHandle -> TMVar Lock
+ System.Console.Concurrent.Internal: [outputThreads] :: OutputHandle -> TMVar Integer
+ System.Console.Concurrent.Internal: [processWaiters] :: OutputHandle -> TMVar [Async ()]
+ System.Console.Concurrent.Internal: [tempFile] :: OutputBufferedActivity -> FilePath
+ System.Console.Concurrent.Internal: [waitForProcessLock] :: OutputHandle -> TMVar ()
+ System.Console.Concurrent.Internal: addOutputBuffer :: OutputBufferedActivity -> OutputBuffer -> IO OutputBuffer
+ System.Console.Concurrent.Internal: asyncProcessWaiter :: IO () -> IO ()
+ System.Console.Concurrent.Internal: bgProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)
+ System.Console.Concurrent.Internal: bufferFor :: StdHandle -> TMVar OutputBuffer
+ System.Console.Concurrent.Internal: bufferOutputSTM :: Outputable v => StdHandle -> v -> STM ()
+ System.Console.Concurrent.Internal: bufferOutputSTM' :: StdHandle -> OutputBuffer -> STM ()
+ System.Console.Concurrent.Internal: bufferWriter :: [(StdHandle, MVar OutputBuffer, TMVar BufSig, TMVar AtEnd)] -> IO ()
+ System.Console.Concurrent.Internal: class Outputable v
+ System.Console.Concurrent.Internal: createProcessConcurrent :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)
+ System.Console.Concurrent.Internal: createProcessForeground :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)
+ System.Console.Concurrent.Internal: data AtEnd
+ System.Console.Concurrent.Internal: data BufSig
+ System.Console.Concurrent.Internal: data Lock
+ System.Console.Concurrent.Internal: data OutputBuffer
+ System.Console.Concurrent.Internal: data OutputBufferedActivity
+ System.Console.Concurrent.Internal: data OutputHandle
+ System.Console.Concurrent.Internal: data StdHandle
+ System.Console.Concurrent.Internal: dropOutputLock :: IO ()
+ System.Console.Concurrent.Internal: emitOutputBuffer :: StdHandle -> OutputBuffer -> IO ()
+ System.Console.Concurrent.Internal: endsNewLine :: Text -> Bool
+ System.Console.Concurrent.Internal: fgProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)
+ System.Console.Concurrent.Internal: flushConcurrentOutput :: IO ()
+ System.Console.Concurrent.Internal: globalOutputHandle :: OutputHandle
+ System.Console.Concurrent.Internal: instance GHC.Classes.Eq System.Console.Concurrent.Internal.AtEnd
+ System.Console.Concurrent.Internal: instance GHC.Classes.Eq System.Console.Concurrent.Internal.OutputBuffer
+ System.Console.Concurrent.Internal: instance GHC.Classes.Eq System.Console.Concurrent.Internal.OutputBufferedActivity
+ System.Console.Concurrent.Internal: instance System.Console.Concurrent.Internal.Outputable Data.Text.Internal.Text
+ System.Console.Concurrent.Internal: instance System.Console.Concurrent.Internal.Outputable GHC.Base.String
+ System.Console.Concurrent.Internal: lockOutput :: (MonadIO m, MonadMask m) => m a -> m a
+ System.Console.Concurrent.Internal: newtype ConcurrentProcessHandle
+ System.Console.Concurrent.Internal: outputBufferWaiterSTM :: (OutputBuffer -> (OutputBuffer, OutputBuffer)) -> STM [(StdHandle, OutputBuffer)]
+ System.Console.Concurrent.Internal: outputConcurrent :: Outputable v => v -> IO ()
+ System.Console.Concurrent.Internal: outputDrainer :: StdStream -> Handle -> MVar OutputBuffer -> TMVar BufSig -> TMVar AtEnd -> IO ()
+ System.Console.Concurrent.Internal: registerOutputThread :: IO ()
+ System.Console.Concurrent.Internal: setupOutputBuffer :: StdHandle -> Handle -> StdStream -> Handle -> IO (StdHandle, MVar OutputBuffer, TMVar BufSig, TMVar AtEnd)
+ System.Console.Concurrent.Internal: takeOutputLock :: IO ()
+ System.Console.Concurrent.Internal: takeOutputLock' :: Bool -> IO Bool
+ System.Console.Concurrent.Internal: toConcurrentProcessHandle :: (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle) -> (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)
+ System.Console.Concurrent.Internal: toHandle :: StdHandle -> Handle
+ System.Console.Concurrent.Internal: toOutput :: Outputable v => v -> Text
+ System.Console.Concurrent.Internal: tryTakeOutputLock :: IO Bool
+ System.Console.Concurrent.Internal: unregisterOutputThread :: IO ()
+ System.Console.Concurrent.Internal: waitAnyBuffer :: OutputBuffer -> (OutputBuffer, OutputBuffer)
+ System.Console.Concurrent.Internal: waitCompleteLines :: OutputBuffer -> (OutputBuffer, OutputBuffer)
+ System.Console.Concurrent.Internal: waitForProcessConcurrent :: ConcurrentProcessHandle -> IO ExitCode
+ System.Console.Concurrent.Internal: willOutput :: StdStream -> Bool
+ System.Console.Concurrent.Internal: withConcurrentOutput :: (MonadIO m, MonadMask m) => m a -> m a
+ System.Console.Concurrent.Internal: withLock :: (TMVar Lock -> STM a) -> IO a
+ System.Console.Regions: instance GHC.Classes.Eq System.Console.Regions.LineUpdate
+ System.Console.Regions: instance GHC.Show.Show System.Console.Regions.LineUpdate
+ System.Process.Concurrent: createProcess :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
+ System.Process.Concurrent: waitForProcess :: ProcessHandle -> IO ExitCode
- System.Console.Concurrent: createProcessConcurrent :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
+ System.Console.Concurrent: createProcessConcurrent :: CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)
- System.Console.Concurrent: toOutput :: Outputable v => v -> ByteString
+ System.Console.Concurrent: toOutput :: Outputable v => v -> Text
- System.Console.Concurrent: waitForProcessConcurrent :: ProcessHandle -> IO ExitCode
+ System.Console.Concurrent: waitForProcessConcurrent :: ConcurrentProcessHandle -> IO ExitCode

Files

CHANGELOG view
@@ -1,3 +1,18 @@+concurrent-output (1.2.0) unstable; urgency=medium++  * Avoid crash when not all of a program's output is consumed, as+    happens when eg, piping to head(1).+  * Use text, and not bytestring internally.+  * Added createProcessForeground, useful for running commands like vim.+  * Fix race that sometimes caused processes to run in background mode+    even though no other foreground process was still running.+  * Concurrent process functions now use ConcurrentProcessHandle+    instead of ProcessHandle.+  * Multi-line regions now supported.+  * Optimize region update, avoiding outputting characters already on-screen.++ -- Joey Hess <id@joeyh.name>  Mon, 02 Nov 2015 23:25:40 -0400+ concurrent-output (1.1.0) unstable; urgency=medium    * Renamed module.
System/Console/Concurrent.hs view
@@ -1,7 +1,5 @@-{-# LANGUAGE BangPatterns, TypeSynonymInstances, FlexibleInstances, TupleSections #-}- -- | --- Copyright: 2013 Joey Hess <id@joeyh.name>+-- Copyright: 2015 Joey Hess <id@joeyh.name> -- License: BSD-2-clause --  -- Concurrent output handling.@@ -21,8 +19,10 @@ 	withConcurrentOutput, 	Outputable(..), 	outputConcurrent,+	ConcurrentProcessHandle, 	createProcessConcurrent, 	waitForProcessConcurrent,+	createProcessForeground, 	flushConcurrentOutput, 	lockOutput, 	-- * Low level access to the output buffer@@ -35,451 +35,5 @@ 	emitOutputBuffer, ) where -import System.IO-import System.Posix.IO-import System.Directory-import System.Exit-import Control.Monad-import Control.Monad.IO.Class (liftIO, MonadIO)-import Control.Applicative-import System.IO.Unsafe (unsafePerformIO)-import Control.Concurrent-import Control.Concurrent.STM-import Control.Concurrent.Async-import Data.Maybe-import Data.List-import Data.Monoid-import Data.Char-import qualified System.Process as P-import qualified Data.ByteString as B-import qualified Data.Text as T-import Data.Text.Encoding (encodeUtf8)--import Utility.Monad-import Utility.Exception--data OutputHandle = OutputHandle-	{ outputLock :: TMVar Lock-	, outputBuffer :: TMVar OutputBuffer-	, errorBuffer :: TMVar OutputBuffer-	, outputThreads :: TMVar Integer-	}--data Lock = Locked---- | A shared global variable for the OutputHandle.-{-# NOINLINE globalOutputHandle #-}-globalOutputHandle :: OutputHandle-globalOutputHandle = unsafePerformIO $ OutputHandle-	<$> newEmptyTMVarIO-	<*> newTMVarIO (OutputBuffer [])-	<*> newTMVarIO (OutputBuffer [])-	<*> newTMVarIO 0---- | Holds a lock while performing an action. This allows the action to--- perform its own output to the console, without using functions from this--- module.------ While this is running, other threads that try to lockOutput will block.--- Any calls to `outputConcurrent` and `createProcessConcurrent` will not--- block, but the output will be buffered and displayed only once the--- action is done.-lockOutput :: (MonadIO m, MonadMask m) => m a -> m a-lockOutput = bracket_ (liftIO takeOutputLock) (liftIO dropOutputLock)---- | Blocks until we have the output lock.-takeOutputLock :: IO ()-takeOutputLock = void $ takeOutputLock' True---- | Tries to take the output lock, without blocking.-tryTakeOutputLock :: IO Bool-tryTakeOutputLock = takeOutputLock' False--withLock :: (TMVar Lock -> STM a) -> IO a-withLock a = atomically $ a (outputLock globalOutputHandle)--takeOutputLock' :: Bool -> IO Bool-takeOutputLock' block = do-	locked <- withLock $ \l -> do-		v <- tryTakeTMVar l-		case v of-			Just Locked-				| block -> retry-				| otherwise -> do-					-- Restore value we took.-					putTMVar l Locked-					return False-			Nothing -> do-				putTMVar l Locked-				return True-	when locked $ do-		(outbuf, errbuf) <- atomically $ (,)-			<$> swapTMVar (outputBuffer globalOutputHandle) (OutputBuffer [])-			<*> swapTMVar (errorBuffer globalOutputHandle) (OutputBuffer [])-		emitOutputBuffer StdOut outbuf-		emitOutputBuffer StdErr errbuf-	return locked---- | Only safe to call after taking the output lock.-dropOutputLock :: IO ()-dropOutputLock = withLock $ void . takeTMVar---- | Use this around any actions that use `outputConcurrent`--- or `createProcessConcurrent`------ This is necessary to ensure that buffered concurrent output actually--- gets displayed before the program exits.-withConcurrentOutput :: (MonadIO m, MonadMask m) => m a -> m a-withConcurrentOutput a = a `finally` liftIO flushConcurrentOutput---- | Blocks until any processes started by `createProcessConcurrent` have--- finished, and any buffered output is displayed. ------ `withConcurrentOutput` calls this at the end; you can call it anytime--- you want to flush output.-flushConcurrentOutput :: IO ()-flushConcurrentOutput = do-	-- Wait for all outputThreads to finish.-	let v = outputThreads globalOutputHandle-	atomically $ do-		r <- takeTMVar v-		if r <= 0-			then putTMVar v r-			else retry-	-- Take output lock to ensure that nothing else is currently-	-- generating output, and flush any buffered output.-	lockOutput $ return ()---- | Values that can be output.-class Outputable v where-	toOutput :: v -> B.ByteString--instance Outputable B.ByteString where-	toOutput = id--instance Outputable T.Text where-	toOutput = encodeUtf8--instance Outputable String where-	toOutput = toOutput . T.pack---- | 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.------ 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.-outputConcurrent :: Outputable v => v -> IO ()-outputConcurrent v = bracket setup cleanup go-  where-	setup = tryTakeOutputLock-	cleanup False = return ()-	cleanup True = dropOutputLock-	go True = do-		B.hPut stdout (toOutput v)-		hFlush stdout-	go False = do-		let bv = outputBuffer globalOutputHandle-		oldbuf <- atomically $ takeTMVar bv-		newbuf <- addOutputBuffer (Output (toOutput v)) oldbuf-		atomically $ putTMVar bv newbuf---- | This must be used to wait for processes started with --- `createProcessConcurrent`.------ This is necessary because `System.Process.waitForProcess` has a--- race condition when two threads check the same process. If the race--- is triggered, one thread will successfully wait, but the other--- throws a DoesNotExist exception.-waitForProcessConcurrent :: P.ProcessHandle -> IO ExitCode-waitForProcessConcurrent h = do-	v <- tryWhenExists (P.waitForProcess h)-	case v of-		Just r -> return r-		Nothing -> maybe (waitForProcessConcurrent h) return =<< P.getProcessExitCode h---- | Wrapper around `System.Process.createProcess` that prevents --- multiple processes that are running concurrently from writing--- to stdout/stderr at the same time.------ If the process does not output to stdout or stderr, it's run--- by createProcess entirely as usual. Only processes that can generate--- output are handled specially:------ A process is allowed to write to stdout and stderr in the usual--- way, assuming it can successfully take the output lock.------ When the output lock is held (ie, by another concurrent process,--- or because `outputConcurrent` is being called at the same time),--- the process is instead run with its stdout and stderr--- redirected to a buffer. The buffered output will be displayed as soon--- as the output lock becomes free, or after the command has finished.-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-			( firstprocess-			, concurrentprocess-			)-	| otherwise = P.createProcess p-  where-	rediroutput ss h-		| willOutput ss = P.UseHandle h-		| otherwise = ss--	firstprocess = do-		r@(_, _, _, h) <- P.createProcess p-			`onException` dropOutputLock-		-- Wait for the process to exit and drop the lock.-		void $ async $ do-			void $ tryIO $ waitForProcessConcurrent h-			dropOutputLock-		return r-	-	concurrentprocess = do-		(toouth, fromouth) <- pipe-		(toerrh, fromerrh) <- pipe-		let p' = p-			{ P.std_out = rediroutput (P.std_out p) toouth-			, P.std_err = rediroutput (P.std_err p) toerrh-			}-		registerOutputThread-		r <- P.createProcess p'-			`onException` unregisterOutputThread-		outbuf <- setupOutputBuffer StdOut toouth (P.std_out p) fromouth-		errbuf <- setupOutputBuffer StdErr toerrh (P.std_err p) fromerrh-		void $ async $ bufferWriter [outbuf, errbuf]-		return r--	pipe = do-		(from, to) <- createPipe-		(,) <$> fdToHandle to <*> fdToHandle from--willOutput :: P.StdStream -> Bool-willOutput P.Inherit = True-willOutput _ = False---- | Buffered output.-data OutputBuffer = OutputBuffer [OutputBufferedActivity]-	deriving (Eq)--data StdHandle = StdOut | StdErr--toHandle :: StdHandle -> Handle-toHandle StdOut = stdout-toHandle StdErr = stderr--bufferFor :: StdHandle -> TMVar OutputBuffer-bufferFor StdOut = outputBuffer globalOutputHandle-bufferFor StdErr = errorBuffer globalOutputHandle--data OutputBufferedActivity-	= Output B.ByteString-	| InTempFile-		{ tempFile :: FilePath-		, endsInNewLine :: Bool-		}-	deriving (Eq)--data AtEnd = AtEnd-	deriving Eq--data BufSig = BufSig--setupOutputBuffer :: StdHandle -> Handle -> P.StdStream -> Handle -> IO (StdHandle, MVar OutputBuffer, TMVar BufSig, TMVar AtEnd)-setupOutputBuffer h toh ss fromh = do-	hClose toh-	buf <- newMVar (OutputBuffer [])-	bufsig <- atomically newEmptyTMVar-	bufend <- atomically newEmptyTMVar-	void $ async $ outputDrainer ss fromh buf bufsig bufend-	return (h, buf, bufsig, bufend)---- Drain output from the handle, and buffer it.-outputDrainer :: P.StdStream -> Handle -> MVar OutputBuffer -> TMVar BufSig -> TMVar AtEnd -> IO ()-outputDrainer ss fromh buf bufsig bufend-	| willOutput ss = go-	| otherwise = atend-  where-	go = do-		v <- tryIO $ B.hGetSome fromh 1048576-		case v of-			Right b | not (B.null b) -> do-				modifyMVar_ buf $ addOutputBuffer (Output b)-				changed-				go-			_ -> atend-	atend = do-		atomically $ putTMVar bufend AtEnd-		hClose fromh-	changed = atomically $ do-		void $ tryTakeTMVar bufsig-		putTMVar bufsig BufSig--registerOutputThread :: IO ()-registerOutputThread = do-	hFlush stdout-	let v = outputThreads globalOutputHandle-	atomically $ putTMVar v . succ =<< takeTMVar v-	-unregisterOutputThread :: IO ()-unregisterOutputThread = do-	hFlush stdout-	let v = outputThreads globalOutputHandle-	atomically $ putTMVar v . pred =<< takeTMVar v---- Wait to lock output, and once we can, display everything --- that's put into the buffers, until the end.------ If end is reached before lock is taken, instead add the command's--- buffers to the global outputBuffer and errorBuffer.-bufferWriter :: [(StdHandle, MVar OutputBuffer, TMVar BufSig, TMVar AtEnd)] -> IO ()-bufferWriter ts = do-	activitysig <- atomically newEmptyTMVar-	worker1 <- async $ lockOutput $-		ifM (atomically $ tryPutTMVar activitysig ())-			( void $ mapConcurrently displaybuf ts-			, noop -- buffers already moved to global-			)-	worker2 <- async $ void $ globalbuf activitysig-	void $ async $ do-		void $ waitCatch worker1-		void $ waitCatch worker2-		unregisterOutputThread-  where-	displaybuf v@(outh, buf, bufsig, bufend) = do-		change <- atomically $-			(Right <$> takeTMVar bufsig)-				`orElse`-			(Left <$> takeTMVar bufend)-		l <- takeMVar buf-		putMVar buf (OutputBuffer [])-		emitOutputBuffer outh l-		case change of-			Right BufSig -> displaybuf v-			Left AtEnd -> return ()-	globalbuf activitysig = do-		ok <- atomically $ do-			-- signal we're going to handle it-			-- (returns false if the displaybuf already did)-			ok <- tryPutTMVar activitysig ()-			-- wait for end of all buffers-			when ok $-				mapM_ (\(_outh, _buf, _bufsig, bufend) -> takeTMVar bufend) ts-			return ok-		when ok $ do-			-- add all of the command's buffered output to the-			-- global output buffer, atomically-			bs <- forM ts $ \(outh, buf, _bufsig, _bufend) ->-				(outh,) <$> takeMVar buf-			atomically $-				forM_ bs $ \(outh, b) -> -					bufferOutputSTM' outh b---- Adds a value to the OutputBuffer. When adding Output to a Handle,--- it's cheaper to combine it with any already buffered Output to that--- same Handle.------ When the total buffered Output exceeds 1 mb in size, it's moved out of--- memory, to a temp file. This should only happen rarely, but is done to--- avoid some verbose process unexpectedly causing excessive memory use.-addOutputBuffer :: OutputBufferedActivity -> OutputBuffer -> IO OutputBuffer-addOutputBuffer (Output b) (OutputBuffer buf)-	| B.length b' <= 1048576 = return $ OutputBuffer (Output b' : other)-	| otherwise = do-		tmpdir <- getTemporaryDirectory-		(tmp, h) <- openTempFile tmpdir "output.tmp"-		let !endnl = endsNewLine b'-		let i = InTempFile-			{ tempFile = tmp-			, endsInNewLine = endnl-			}-		B.hPut h b'-		hClose h-		return $ OutputBuffer (i : other)-  where-	!b' = B.concat (mapMaybe getOutput this) <> b-	!(this, other) = partition isOutput buf-	isOutput v = case v of-		Output _ -> True-		_ -> False-	getOutput v = case v of-		Output b'' -> Just b''-		_ -> Nothing-addOutputBuffer v (OutputBuffer buf) = return $ OutputBuffer (v:buf)---- | Adds a value to the output buffer for later display.------ Note that buffering large quantities of data this way will keep it--- resident in memory until it can be displayed. While `outputConcurrent`--- uses temp files if the buffer gets too big, this STM function cannot do--- so.-bufferOutputSTM :: Outputable v => StdHandle -> v -> STM ()-bufferOutputSTM h v = bufferOutputSTM' h (OutputBuffer [Output (toOutput v)])--bufferOutputSTM' :: StdHandle -> OutputBuffer -> STM ()-bufferOutputSTM' h (OutputBuffer newbuf) = do-	(OutputBuffer buf) <- takeTMVar bv-	putTMVar bv (OutputBuffer (newbuf ++ buf))-  where-	bv = bufferFor h---- | A STM action that waits for some buffered output to become--- available, and returns it.------ The function can select a subset of output when only some is desired;--- the fst part is returned and the snd is left in the buffer.------ This will prevent it from being displayed in the usual way, so you'll--- need to use `emitOutputBuffer` to display it yourself.-outputBufferWaiterSTM :: (OutputBuffer -> (OutputBuffer, OutputBuffer)) -> STM [(StdHandle, OutputBuffer)]-outputBufferWaiterSTM selector = do-	bs <- forM hs $ \h -> do-		let bv = bufferFor h-		(selected, rest) <- selector <$> takeTMVar bv-		putTMVar bv rest-		return selected-	if all (== OutputBuffer []) bs-		then retry-		else do-			return (zip hs bs)-  where-	hs = [StdOut, StdErr]--waitAnyBuffer :: OutputBuffer -> (OutputBuffer, OutputBuffer)-waitAnyBuffer b = (b, OutputBuffer [])---- | Use with `outputBufferWaiterSTM` to make it only return buffered--- output that ends with a newline. Anything buffered without a newline--- is left in the buffer.-waitCompleteLines :: OutputBuffer -> (OutputBuffer, OutputBuffer)-waitCompleteLines (OutputBuffer l) = -	let (selected, rest) = span completeline l-	in (OutputBuffer selected, OutputBuffer rest)-  where-	completeline (v@(InTempFile {})) = endsInNewLine v-	completeline (Output b) = endsNewLine b--endsNewLine :: B.ByteString -> Bool-endsNewLine b = not (B.null b) && B.last b == fromIntegral (ord '\n')+import System.Console.Concurrent.Internal --- | Emits the content of the OutputBuffer to the Handle------ If you use this, you should use `lockOutput` to ensure you're the only--- thread writing to the console.-emitOutputBuffer :: StdHandle -> OutputBuffer -> IO ()-emitOutputBuffer stdh (OutputBuffer l) = -	forM_ (reverse l) $ \ba -> case ba of-		Output b -> do-			B.hPut outh b-			hFlush outh-		InTempFile tmp _ -> do-			B.hPut outh =<< B.readFile tmp-			void $ tryWhenExists $ removeFile tmp-  where-	outh = toHandle stdh
+ System/Console/Concurrent/Internal.hs view
@@ -0,0 +1,522 @@+{-# LANGUAGE BangPatterns, TypeSynonymInstances, FlexibleInstances, TupleSections #-}++-- | +-- Copyright: 2015 Joey Hess <id@joeyh.name>+-- License: BSD-2-clause+-- +-- Concurrent output handling, internals.+--+-- May change at any time.++module System.Console.Concurrent.Internal where++import System.IO+import System.Posix.IO+import System.Directory+import System.Exit+import Control.Monad+import Control.Monad.IO.Class (liftIO, MonadIO)+import Control.Applicative+import System.IO.Unsafe (unsafePerformIO)+import Control.Concurrent+import Control.Concurrent.STM+import Control.Concurrent.Async+import Data.Maybe+import Data.List+import Data.Monoid+import qualified System.Process as P+import qualified Data.Text as T+import qualified Data.Text.IO as T++import Utility.Monad+import Utility.Exception++data OutputHandle = OutputHandle+	{ outputLock :: TMVar Lock+	, outputBuffer :: TMVar OutputBuffer+	, errorBuffer :: TMVar OutputBuffer+	, outputThreads :: TMVar Integer+	, processWaiters :: TMVar [Async ()]+	, waitForProcessLock :: TMVar ()+	}++data Lock = Locked++-- | A shared global variable for the OutputHandle.+{-# NOINLINE globalOutputHandle #-}+globalOutputHandle :: OutputHandle+globalOutputHandle = unsafePerformIO $ OutputHandle+	<$> newEmptyTMVarIO+	<*> 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+-- module.+--+-- While this is running, other threads that try to lockOutput will block.+-- Any calls to `outputConcurrent` and `createProcessConcurrent` will not+-- block, but the output will be buffered and displayed only once the+-- action is done.+lockOutput :: (MonadIO m, MonadMask m) => m a -> m a+lockOutput = bracket_ (liftIO takeOutputLock) (liftIO dropOutputLock)++-- | Blocks until we have the output lock.+takeOutputLock :: IO ()+takeOutputLock = void $ takeOutputLock' True++-- | Tries to take the output lock, without blocking.+tryTakeOutputLock :: IO Bool+tryTakeOutputLock = takeOutputLock' False++withLock :: (TMVar Lock -> STM a) -> IO a+withLock a = atomically $ a (outputLock globalOutputHandle)++takeOutputLock' :: Bool -> IO Bool+takeOutputLock' block = do+	locked <- withLock $ \l -> do+		v <- tryTakeTMVar l+		case v of+			Just Locked+				| block -> retry+				| otherwise -> do+					-- Restore value we took.+					putTMVar l Locked+					return False+			Nothing -> do+				putTMVar l Locked+				return True+	when locked $ do+		(outbuf, errbuf) <- atomically $ (,)+			<$> swapTMVar (outputBuffer globalOutputHandle) (OutputBuffer [])+			<*> swapTMVar (errorBuffer globalOutputHandle) (OutputBuffer [])+		emitOutputBuffer StdOut outbuf+		emitOutputBuffer StdErr errbuf+	return locked++-- | Only safe to call after taking the output lock.+dropOutputLock :: IO ()+dropOutputLock = withLock $ void . takeTMVar++-- | Use this around any actions that use `outputConcurrent`+-- or `createProcessConcurrent`+--+-- This is necessary to ensure that buffered concurrent output actually+-- gets displayed before the program exits.+withConcurrentOutput :: (MonadIO m, MonadMask m) => m a -> m a+withConcurrentOutput a = a `finally` liftIO flushConcurrentOutput++-- | Blocks until any processes started by `createProcessConcurrent` have+-- finished, and any buffered output is displayed. +--+-- `withConcurrentOutput` calls this at the end; you can call it anytime+-- you want to flush output.+flushConcurrentOutput :: IO ()+flushConcurrentOutput = do+	-- Wait for all outputThreads to finish.+	let v = outputThreads globalOutputHandle+	atomically $ do+		r <- takeTMVar v+		if r <= 0+			then putTMVar v r+			else retry+	-- Take output lock to ensure that nothing else is currently+	-- generating output, and flush any buffered output.+	lockOutput $ return ()++-- | Values that can be output.+class Outputable v where+	toOutput :: v -> T.Text++instance Outputable T.Text where+	toOutput = id++instance Outputable String where+	toOutput = toOutput . T.pack++-- | 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.+--+-- 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.+outputConcurrent :: Outputable v => v -> IO ()+outputConcurrent v = bracket setup cleanup go+  where+	setup = tryTakeOutputLock+	cleanup False = return ()+	cleanup True = dropOutputLock+	go True = do+		T.hPutStr stdout (toOutput v)+		hFlush stdout+	go False = do+		let bv = outputBuffer globalOutputHandle+		oldbuf <- atomically $ takeTMVar bv+		newbuf <- addOutputBuffer (Output (toOutput v)) oldbuf+		atomically $ putTMVar bv newbuf++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)++-- | 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`+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)++-- | Wrapper around `System.Process.createProcess` that prevents +-- multiple processes that are running concurrently from writing+-- to stdout/stderr at the same time.+--+-- If the process does not output to stdout or stderr, it's run+-- by createProcess entirely as usual. Only processes that can generate+-- output are handled specially:+--+-- A process is allowed to write to stdout and stderr in the usual+-- way, assuming it can successfully take the output lock.+--+-- When the output lock is held (ie, by another concurrent process,+-- or because `outputConcurrent` is being called at the same time),+-- the process is instead run with its stdout and stderr+-- redirected to a buffer. The buffered output will be displayed as soon+-- as the output lock becomes free.+createProcessConcurrent :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle) +createProcessConcurrent p+	| willOutput (P.std_out p) || willOutput (P.std_err p) =+		ifM tryTakeOutputLock+			( fgProcess p+			, bgProcess p+			)+	| otherwise = do+		r@(_, _, _, h) <- P.createProcess p+		asyncProcessWaiter $+			void $ tryIO $ P.waitForProcess h+		return (toConcurrentProcessHandle 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)+createProcessForeground p = do+	takeOutputLock+	fgProcess p++fgProcess :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)+fgProcess p = do+	r@(_, _, _, h) <- P.createProcess p+		`onException` dropOutputLock+	-- Wait for the process to exit and drop the lock.+	asyncProcessWaiter $ do+		void $ tryIO $ P.waitForProcess h+		dropOutputLock+	return (toConcurrentProcessHandle r)++bgProcess :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, ConcurrentProcessHandle)+bgProcess p = do+	(toouth, fromouth) <- pipe+	(toerrh, fromerrh) <- pipe+	let p' = p+		{ P.std_out = rediroutput (P.std_out p) toouth+		, P.std_err = rediroutput (P.std_err p) toerrh+		}+	registerOutputThread+	r@(_, _, _, h) <- P.createProcess p'+		`onException` unregisterOutputThread+	asyncProcessWaiter $ void $ tryIO $ P.waitForProcess h+	outbuf <- setupOutputBuffer StdOut toouth (P.std_out p) fromouth+	errbuf <- setupOutputBuffer StdErr toerrh (P.std_err p) fromerrh+	void $ async $ bufferWriter [outbuf, errbuf]+	return (toConcurrentProcessHandle r)+  where+	pipe = do+		(from, to) <- createPipe+		(,) <$> fdToHandle to <*> fdToHandle from+	rediroutput ss h+		| willOutput ss = P.UseHandle h+		| otherwise = ss++willOutput :: P.StdStream -> Bool+willOutput P.Inherit = True+willOutput _ = False++-- | Buffered output.+data OutputBuffer = OutputBuffer [OutputBufferedActivity]+	deriving (Eq)++data StdHandle = StdOut | StdErr++toHandle :: StdHandle -> Handle+toHandle StdOut = stdout+toHandle StdErr = stderr++bufferFor :: StdHandle -> TMVar OutputBuffer+bufferFor StdOut = outputBuffer globalOutputHandle+bufferFor StdErr = errorBuffer globalOutputHandle++data OutputBufferedActivity+	= Output T.Text+	| InTempFile+		{ tempFile :: FilePath+		, endsInNewLine :: Bool+		}+	deriving (Eq)++data AtEnd = AtEnd+	deriving Eq++data BufSig = BufSig++setupOutputBuffer :: StdHandle -> Handle -> P.StdStream -> Handle -> IO (StdHandle, MVar OutputBuffer, TMVar BufSig, TMVar AtEnd)+setupOutputBuffer h toh ss fromh = do+	hClose toh+	buf <- newMVar (OutputBuffer [])+	bufsig <- atomically newEmptyTMVar+	bufend <- atomically newEmptyTMVar+	void $ async $ outputDrainer ss fromh buf bufsig bufend+	return (h, buf, bufsig, bufend)++-- Drain output from the handle, and buffer it.+outputDrainer :: P.StdStream -> Handle -> MVar OutputBuffer -> TMVar BufSig -> TMVar AtEnd -> IO ()+outputDrainer ss fromh buf bufsig bufend+	| willOutput ss = go+	| otherwise = atend+  where+	go = do+		t <- T.hGetChunk fromh+		if T.null t+			then atend+			else do+				modifyMVar_ buf $ addOutputBuffer (Output t)+				changed+				go+	atend = do+		atomically $ putTMVar bufend AtEnd+		hClose fromh+	changed = atomically $ do+		void $ tryTakeTMVar bufsig+		putTMVar bufsig BufSig++registerOutputThread :: IO ()+registerOutputThread = do+	let v = outputThreads globalOutputHandle+	atomically $ putTMVar v . succ =<< takeTMVar v+	+unregisterOutputThread :: IO ()+unregisterOutputThread = do+	let v = outputThreads globalOutputHandle+	atomically $ putTMVar v . pred =<< takeTMVar v++-- Wait to lock output, and once we can, display everything +-- that's put into the buffers, until the end.+--+-- If end is reached before lock is taken, instead add the command's+-- buffers to the global outputBuffer and errorBuffer.+bufferWriter :: [(StdHandle, MVar OutputBuffer, TMVar BufSig, TMVar AtEnd)] -> IO ()+bufferWriter ts = do+	activitysig <- atomically newEmptyTMVar+	worker1 <- async $ lockOutput $+		ifM (atomically $ tryPutTMVar activitysig ())+			( void $ mapConcurrently displaybuf ts+			, noop -- buffers already moved to global+			)+	worker2 <- async $ void $ globalbuf activitysig+	void $ async $ do+		void $ waitCatch worker1+		void $ waitCatch worker2+		unregisterOutputThread+  where+	displaybuf v@(outh, buf, bufsig, bufend) = do+		change <- atomically $+			(Right <$> takeTMVar bufsig)+				`orElse`+			(Left <$> takeTMVar bufend)+		l <- takeMVar buf+		putMVar buf (OutputBuffer [])+		emitOutputBuffer outh l+		case change of+			Right BufSig -> displaybuf v+			Left AtEnd -> return ()+	globalbuf activitysig = do+		ok <- atomically $ do+			-- signal we're going to handle it+			-- (returns false if the displaybuf already did)+			ok <- tryPutTMVar activitysig ()+			-- wait for end of all buffers+			when ok $+				mapM_ (\(_outh, _buf, _bufsig, bufend) -> takeTMVar bufend) ts+			return ok+		when ok $ do+			-- add all of the command's buffered output to the+			-- global output buffer, atomically+			bs <- forM ts $ \(outh, buf, _bufsig, _bufend) ->+				(outh,) <$> takeMVar buf+			atomically $+				forM_ bs $ \(outh, b) -> +					bufferOutputSTM' outh b++-- Adds a value to the OutputBuffer. When adding Output to a Handle,+-- it's cheaper to combine it with any already buffered Output to that+-- same Handle.+--+-- When the total buffered Output exceeds 1 mb in size, it's moved out of+-- memory, to a temp file. This should only happen rarely, but is done to+-- avoid some verbose process unexpectedly causing excessive memory use.+addOutputBuffer :: OutputBufferedActivity -> OutputBuffer -> IO OutputBuffer+addOutputBuffer (Output t) (OutputBuffer buf)+	| T.length t' <= 1048576 = return $ OutputBuffer (Output t' : other)+	| otherwise = do+		tmpdir <- getTemporaryDirectory+		(tmp, h) <- openTempFile tmpdir "output.tmp"+		let !endnl = endsNewLine t'+		let i = InTempFile+			{ tempFile = tmp+			, endsInNewLine = endnl+			}+		T.hPutStr h t'+		hClose h+		return $ OutputBuffer (i : other)+  where+	!t' = T.concat (mapMaybe getOutput this) <> t+	!(this, other) = partition isOutput buf+	isOutput v = case v of+		Output _ -> True+		_ -> False+	getOutput v = case v of+		Output t'' -> Just t''+		_ -> Nothing+addOutputBuffer v (OutputBuffer buf) = return $ OutputBuffer (v:buf)++-- | Adds a value to the output buffer for later display.+--+-- Note that buffering large quantities of data this way will keep it+-- resident in memory until it can be displayed. While `outputConcurrent`+-- uses temp files if the buffer gets too big, this STM function cannot do+-- so.+bufferOutputSTM :: Outputable v => StdHandle -> v -> STM ()+bufferOutputSTM h v = bufferOutputSTM' h (OutputBuffer [Output (toOutput v)])++bufferOutputSTM' :: StdHandle -> OutputBuffer -> STM ()+bufferOutputSTM' h (OutputBuffer newbuf) = do+	(OutputBuffer buf) <- takeTMVar bv+	putTMVar bv (OutputBuffer (newbuf ++ buf))+  where+	bv = bufferFor h++-- | A STM action that waits for some buffered output to become+-- available, and returns it.+--+-- The function can select a subset of output when only some is desired;+-- the fst part is returned and the snd is left in the buffer.+--+-- This will prevent it from being displayed in the usual way, so you'll+-- need to use `emitOutputBuffer` to display it yourself.+outputBufferWaiterSTM :: (OutputBuffer -> (OutputBuffer, OutputBuffer)) -> STM [(StdHandle, OutputBuffer)]+outputBufferWaiterSTM selector = do+	bs <- forM hs $ \h -> do+		let bv = bufferFor h+		(selected, rest) <- selector <$> takeTMVar bv+		putTMVar bv rest+		return selected+	if all (== OutputBuffer []) bs+		then retry+		else do+			return (zip hs bs)+  where+	hs = [StdOut, StdErr]++waitAnyBuffer :: OutputBuffer -> (OutputBuffer, OutputBuffer)+waitAnyBuffer b = (b, OutputBuffer [])++-- | Use with `outputBufferWaiterSTM` to make it only return buffered+-- output that ends with a newline. Anything buffered without a newline+-- is left in the buffer.+waitCompleteLines :: OutputBuffer -> (OutputBuffer, OutputBuffer)+waitCompleteLines (OutputBuffer l) = +	let (selected, rest) = span completeline l+	in (OutputBuffer selected, OutputBuffer rest)+  where+	completeline (v@(InTempFile {})) = endsInNewLine v+	completeline (Output b) = endsNewLine b++endsNewLine :: T.Text -> Bool+endsNewLine t = not (T.null t) && T.last t == '\n'++-- | Emits the content of the OutputBuffer to the Handle+--+-- If you use this, you should use `lockOutput` to ensure you're the only+-- thread writing to the console.+emitOutputBuffer :: StdHandle -> OutputBuffer -> IO ()+emitOutputBuffer stdh (OutputBuffer l) = +	forM_ (reverse l) $ \ba -> case ba of+		Output t -> emit t+		InTempFile tmp _ -> do+			emit =<< T.readFile tmp+			void $ tryWhenExists $ removeFile tmp+  where+	outh = toHandle stdh+	emit t = void $ tryIO $ do+		T.hPutStr outh t+		hFlush outh
System/Console/Regions.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE BangPatterns, TupleSections #-}  -- | --- Copyright: 2013 Joey Hess <id@joeyh.name>+-- Copyright: 2015 Joey Hess <id@joeyh.name> -- License: BSD-2-clause --  -- Console regions are displayed near the bottom of the console, and can be@@ -15,9 +15,12 @@ -- > import Control.Concurrent -- > import System.Console.Concurrent -- > import System.Console.Regions+-- > import System.Process -- >  -- > main = displayConsoleRegions $ do--- > 	mapConcurrently download [1..5] `concurrently` mapM_ message [1..10]+-- > 	mapConcurrently download [1..5]+-- >		`concurrently` mapM_ message [1..10]+-- >		`concurrently` createProcessConcurrent (proc "echo" ["hello world"]) -- >  -- > message :: Int -> IO () -- > message n = do@@ -40,6 +43,7 @@ -- Will display like this: -- -- > Message 1+-- > hello world -- > Message 2 -- > Download 1 ... -- > Download 2 ...@@ -49,6 +53,7 @@ -- the console will update like this: -- -- > Message 1+-- > hello world -- > Message 2 -- > Download 1 done! -- > Message 3@@ -83,7 +88,8 @@ import Data.Maybe import Data.String import Data.Char-import qualified Data.ByteString as B+import qualified Data.Text as T+import qualified Data.Text.IO as T import Control.Monad import Control.Applicative import Control.Monad.IO.Class (liftIO, MonadIO)@@ -96,6 +102,8 @@ import System.IO.Unsafe (unsafePerformIO) import System.Posix.Signals import System.Posix.Signals.Exts+import Text.Read+import Data.List  import System.Console.Concurrent import Utility.Monad@@ -121,13 +129,9 @@ newtype ConsoleRegionHandle = ConsoleRegionHandle (TVar Region) 	deriving (Eq) -type Width = Int-type Height = Int- data Region = Region-	{ regionContent :: B.ByteString-	, regionHeight :: (Width -> Height)-	-- ^ Function from console width to the height of this region+	{ regionContent :: T.Text+	, regionLines :: [T.Text] -- cache 	, regionLayout :: RegionLayout 	, regionChildren :: Maybe [ConsoleRegionHandle] 	}@@ -143,6 +147,13 @@ regionList :: RegionList regionList = unsafePerformIO newEmptyTMVarIO +type Width = Int++-- | A shared global console width.+{-# NOINLINE consoleWidth #-}+consoleWidth :: TVar Width+consoleWidth = unsafePerformIO (newTVarIO 80)+ -- | Updates the list of regions. The list is ordered from the bottom of -- the screen up. Reordering it will change the order in which regions are -- displayed. It's also fine to remove, duplicate, or add new regions to the@@ -157,25 +168,24 @@ regionDisplayEnabled = atomically $ not <$> isEmptyTMVar regionList  -- | Sets the value to display within a console region.-{- TODO--- It's fine for the value to be longer than the terminal is wide.--- Regions are laid out according to the size of their contents,--- and expand to multiple lines if necessary. ----- The value of a `Linear` region can contain newlines ('\n').--- And it's ok to include ANSI escape sequences for changing colors,--- or setting the terminal title. However, ANSI cursor movement sequences--- will mess up the  layouts of regions, '\r' is not handled, and--- other control characters or ANSI codes may confuse the region--- display. Caveat emptor.--}+-- It's fine for the value to be longer than the terminal is wide,+-- or to include newlines ('\n'). Regions expand to multiple lines as+-- necessary.+--+-- The value can include ANSI SGR escape sequences for changing+-- the colors etc of all or part of a region.+-- +-- Other ANSI escape sequences, especially those doing cursor+-- movement, will mess up the layouts of regions. Caveat emptor. setConsoleRegion :: Outputable v => ConsoleRegionHandle -> v -> IO () setConsoleRegion h = atomically . setConsoleRegionSTM h  setConsoleRegionSTM :: Outputable v => ConsoleRegionHandle -> v -> STM () setConsoleRegionSTM (ConsoleRegionHandle tv) v = do 	r <- readTVar tv-	writeTVar tv (modifyRegion r (const (toOutput v)))+	width <- readTVar consoleWidth+	writeTVar tv (modifyRegion r width (const (toOutput v))) 	case regionLayout r of 		Linear -> return () 		InLine p -> refreshParent p@@ -188,16 +198,24 @@ appendConsoleRegionSTM :: Outputable v => ConsoleRegionHandle -> v -> STM () appendConsoleRegionSTM (ConsoleRegionHandle tv) v = do 	r <- readTVar tv-	writeTVar tv (modifyRegion r (<> toOutput v))+	width <- readTVar consoleWidth+	writeTVar tv (modifyRegion r width (<> toOutput v)) 	case regionLayout r of 		Linear -> return () 		InLine p -> refreshParent p -modifyRegion :: Region -> (B.ByteString -> B.ByteString) -> Region-modifyRegion r f = r { regionContent = c, regionHeight = calcRegionHeight c }+modifyRegion :: Region -> Width -> (T.Text -> T.Text) -> Region+modifyRegion r width f = r { regionContent = c, regionLines = calcRegionLines c width }   where 	!c = f (regionContent r) +resizeRegion :: Width -> ConsoleRegionHandle -> STM Region+resizeRegion width (ConsoleRegionHandle tv) = do+	r <- readTVar tv+	let !r' = modifyRegion r width id+	writeTVar tv r'+	return r'+ -- | Runs the action with a new console region, closing the region when -- the action finishes or on exception. withConsoleRegion :: (MonadIO m, MonadMask m) => RegionLayout -> (ConsoleRegionHandle -> m a) -> m a@@ -214,9 +232,10 @@ -- >	replicateM 3 (openConsoleRegionSTM Linear) openConsoleRegionSTM :: RegionLayout -> STM ConsoleRegionHandle openConsoleRegionSTM ly = do+	width <- readTVar consoleWidth 	let r = Region 		{ regionContent = mempty-		, regionHeight = calcRegionHeight mempty+		, regionLines = calcRegionLines mempty width 		, regionLayout = ly 		, regionChildren = Nothing 		}@@ -275,15 +294,20 @@ 	refreshParent parent  refreshParent :: ConsoleRegionHandle -> STM ()-refreshParent parent@(ConsoleRegionHandle pv) = do+refreshParent (ConsoleRegionHandle pv) = do 	p <- readTVar pv+	width <- readTVar consoleWidth 	case regionChildren p of 		Nothing -> return () 		Just l -> do 			cs <- forM l $ \child@(ConsoleRegionHandle cv) -> do 				refreshParent child 				regionContent <$> readTVar cv-			let p' = p { regionContent = mconcat cs }+			let !c = mconcat cs+			let p' = p+				{ regionContent = c+				, regionLines = calcRegionLines c width+				} 			writeTVar pv p'  -- | Handles all display for the other functions in this module.@@ -308,10 +332,8 @@ 			waitTSem s 			return s 		isterm <- liftIO $ hSupportsANSI stdout-		cwidth <- atomically newEmptyTMVar-		when isterm $-			trackConsoleWidth cwidth-		da <- async $ displayThread isterm cwidth endsignal+		when isterm trackConsoleWidth+		da <- async $ displayThread isterm endsignal 		return (isterm, da, endsignal) 	cleanup (isterm, da, endsignal) = liftIO $ do 		atomically $ signalTSem endsignal@@ -320,36 +342,35 @@ 		when isterm $ 			installResizeHandler Nothing -trackConsoleWidth :: TMVar Width -> IO ()-trackConsoleWidth cwidth = do+trackConsoleWidth :: IO ()+trackConsoleWidth = do 	let getwidth = do 		v <- Console.size 		case v of 			Nothing -> return () 			Just (Console.Window _height width) ->-				atomically $ void $ do-					void $ tryTakeTMVar cwidth-					putTMVar cwidth width+				atomically $ void $+					swapTVar consoleWidth width 	getwidth-	installResizeHandler $ Just getwidth+	installResizeHandler (Just getwidth)  data DisplayChange 	= BufferChange [(StdHandle, OutputBuffer)] 	| RegionChange RegionSnapshot-	| TerminalResize (Maybe Width)+	| TerminalResize Width 	| EndSignal () -type RegionSnapshot = ([ConsoleRegionHandle], [Region], Maybe Width)+type RegionSnapshot = ([ConsoleRegionHandle], [Region]) -displayThread :: Bool -> TMVar Width -> TSem -> IO ()-displayThread isterm cwidth endsignal = do-	origwidth <- atomically $ tryReadTMVar cwidth-	go ([], [], origwidth)+displayThread :: Bool -> TSem -> IO ()+displayThread isterm endsignal = do+	origwidth <- atomically $ readTVar consoleWidth+	go ([], []) origwidth   where-	go origsnapshot@(orighandles, origregions, origwidth) = do+	go origsnapshot@(orighandles, origregions) origwidth = do 		let waitwidthchange = do-			v <- tryReadTMVar cwidth-			if v == origwidth then retry else return v+			w <- readTVar consoleWidth+			if w == origwidth then retry else return w 		change <- atomically $ 			(RegionChange <$> regionWaiter origsnapshot) 				`orElse`@@ -361,180 +382,332 @@ 				`orElse` 			(EndSignal <$> waitTSem endsignal) 		case change of-			RegionChange snapshot@(_, regions, _) -> do+			RegionChange snapshot@(_, newregions) -> do 				when isterm $-					changedRegions origregions regions-				go snapshot+					changedLines+						(getlines origregions)+						(getlines newregions)+				go snapshot origwidth 			BufferChange buffers -> do-				inAreaAbove isterm origregions $+				inAreaAbove isterm (length $ getlines origregions) (getlines origregions) $ 					mapM_ (uncurry emitOutputBuffer) buffers-				go origsnapshot-			TerminalResize width -> do-				when isterm $-					-- force redraw of all regions-					inAreaAbove isterm origregions $+				go origsnapshot origwidth+			TerminalResize newwidth -> do+				newregions <- atomically $+					mapM (resizeRegion newwidth) orighandles+				when isterm $ do+					-- A resize can leave the cursor+					-- in a fairly undefined location,+					-- if it occurred while a screen+					-- draw was in progress. Move the+					-- cursor to top of screen, clear+					-- entire screen, and redisplay.+					setCursorPosition 0 0+					inAreaAbove isterm 0 (getlines newregions) $ 						return ()-				go (orighandles, origregions, width)+				go (orighandles, newregions) newwidth 			EndSignal () -> return ()+	getlines = concatMap (reverse . regionLines)  readRegions :: [ConsoleRegionHandle] -> STM [Region] readRegions = mapM (\(ConsoleRegionHandle h) -> readTVar h)  -- Wait for any changes to the region list, eg adding or removing a handle. regionListWaiter :: RegionSnapshot -> STM RegionSnapshot-regionListWaiter (orighandles, _origregions, origwidth) = do+regionListWaiter (orighandles, _origregions) = do 	handles <- readTMVar regionList 	if handles == orighandles 		then retry-		else (handles,,origwidth) <$> readRegions handles+		else do+			rs <- readRegions handles+			return (handles, rs) 		 -- Wait for any changes to any of the regions currently in the region list. regionWaiter :: RegionSnapshot -> STM RegionSnapshot-regionWaiter (orighandles, origregions, origwidth) = do+regionWaiter (orighandles, origregions) = do 	rs <- readRegions orighandles 	if rs == origregions 		then retry-		else return (orighandles, rs, origwidth)+		else return (orighandles, rs)  -- This is not an optimal screen update like curses can do, but it's--- pretty efficient, most of the time! The only particularly--- expensive part is removing a region, which typically reorders--- the regions and so requires redrawing them all.-changedRegions :: [Region] -> [Region] -> IO ()-changedRegions origregions regions+-- pretty efficient, most of the time!+changedLines :: [T.Text] -> [T.Text] -> IO ()+changedLines origlines newlines 	| delta == 0 = do-		-- The total number of regions is unchanged, so update+		-- The total number of lines is unchanged, so update 		-- whichever ones have changed, and leave the rest as-is.-		diffUpdate origregions regions+		diffUpdate origlines newlines 	| delta > 0 = do-		-- Added more regions, so output each, with a-		-- newline, thus scrolling the old regions up+		-- Added more lines, so output each, with a+		-- newline, thus scrolling the old lines up 		-- the screen. (We can do this, because the cursor-		-- is left below the first region.)-		let newregions = reverse (take delta regions)-		displayRegions newregions-		hFlush stdout-		-- Some existing regions may have also changed..-		let scrolledregions = newregions ++ origregions-		diffUpdate scrolledregions regions+		-- is left below the first line.)+		let addedlines = reverse (take delta newlines)+		displayLines addedlines+		-- Some existing lines may have also changed..+		let scrolledlines = addedlines ++ origlines+		diffUpdate scrolledlines newlines 	| otherwise = do-		-- Some regions were removed. Move up that many lines,-		-- clearing each line, and update any changed regions.+		-- Some lines were removed. Move up that many lines,+		-- clearing each line, and update any changed lines. 		replicateM_ (abs delta) $ do 			cursorUpLine 1 			clearLine-		diffUpdate (drop (abs delta) origregions) regions+		diffUpdate (drop (abs delta) origlines) newlines   where-	delta = length regions - length origregions+	delta = length newlines - length origlines --- TODO Rather than writing the whole text of a region, find--- a more efficient update, reusing parts of the old content of the region.-diffUpdate :: [Region] -> [Region] -> IO ()-diffUpdate origregions regions = updateRegions (zip regions changed)+diffUpdate :: [T.Text] -> [T.Text] -> IO ()+diffUpdate old new = updateLines (zip (zip new changed) old)   where-	changed = map (uncurry (/=)) (zip regions origregions) ++ repeat True+	changed = map (uncurry (/=)) (zip new old) ++ repeat True --- Displays regions that are paired with True, and skips over the rest.--- Cursor is assumed to be just below the line of the first region at the+changeOffsets :: [((r, Bool), r)] -> Int -> [((r, Int), r)] -> [((r, Int), r)]+changeOffsets [] _ c = reverse c+changeOffsets (((new, changed), old):rs) n c+	| changed = changeOffsets rs 1 (((new, n), old):c)+	| otherwise = changeOffsets rs (succ n) c++-- Displays lines that are paired with True, and skips over the rest.+-- Cursor is assumed to be just below the first line at the -- beginning, and is put back there at the end.-updateRegions :: [(Region, Bool)] -> IO ()-updateRegions l+updateLines :: [((T.Text, Bool), T.Text)] -> IO ()+updateLines l 	| null l' = noop 	| otherwise = do-		forM_ l' $ \(r, offset) -> do+		forM_ l' $ \((newt, offset), oldt) -> do 			cursorUpLine offset-			clearLine-			B.hPut stdout (regionContent r)-			hFlush stdout-		cursorDownLine (sum (map snd l'))+			T.hPutStr stdout $ +				genLineUpdate $ calcLineUpdate oldt newt+		cursorDownLine (sum (map (snd . fst) l')) 		setCursorColumn 0 		hFlush stdout   where 	l' = changeOffsets l 1 [] -changeOffsets :: [(r, Bool)] -> Int -> [(r, Int)] -> [(r, Int)]-changeOffsets [] _ c = reverse c-changeOffsets ((r, changed):rs) n c-	| changed = changeOffsets rs 1 ((r, n):c)-	| otherwise = changeOffsets rs (succ n) c---- Move cursor up before the regions, performs some output there,--- which will scroll down and overwrite the regions, so --- redraws all the regions below.-inAreaAbove :: Bool -> [Region] -> IO () -> IO ()-inAreaAbove isterm regions outputter = do+-- Move cursor up before the lines, performs some output there,+-- which will scroll down and overwrite the lines, so +-- redraws all the lines below.+inAreaAbove :: Bool -> Int -> [T.Text] -> IO () -> IO ()+inAreaAbove isterm numlines ls outputter = do 	when isterm $ do-		unless (null regions) $-			cursorUpLine (length regions)+		unless (numlines < 1) $+			cursorUpLine $ numlines 		clearFromCursorToScreenEnd 	outputter 	when isterm $ do 		setCursorColumn 0 -- just in case the output lacked a newline-		displayRegions (reverse regions)+		displayLines (reverse ls) 	hFlush stdout -displayRegions :: [Region] -> IO ()-displayRegions = mapM_ $ \r -> do-	B.hPut stdout (regionContent r)+displayLines :: [T.Text] -> IO ()+displayLines = mapM_ $ \l -> do+	T.hPutStr stdout l 	putChar '\n'  installResizeHandler :: Maybe (IO ()) -> IO () installResizeHandler h = void $ 	installHandler windowChange (maybe Default Catch h) Nothing -calcRegionHeight :: B.ByteString -> Width -> Height-calcRegionHeight b width = sum $ map (calcLineHeight width) (B.split wnl b)+-- | Splits a Text into the lines it would display using when output onto+-- a console with a given width, starting from the first column.+--+-- ANSI SGR sequences are handled specially, so that color, etc settings+-- work despite the lines being split up, and the lines can be output+-- indepedently. For example, "foooREDbar bazRESET" when split into lines+-- becomes ["fooREDbarRESET", "RED bazRESET"]+calcRegionLines :: T.Text -> Width -> [T.Text]+calcRegionLines t width+	| width < 1 || T.null t = [t] -- even an empty text is 1 line high+	| otherwise = calcRegionLines' width [] [] 0 1 (T.length t) t++calcRegionLines' :: Int -> [T.Text] -> [T.Text] -> Int -> Int -> Int -> T.Text -> [T.Text]+calcRegionLines' width collectedlines collectedSGR i displaysize len t+	| i >= len = if i > 0+		then reverse (finishline t)+		else reverse collectedlines+	| t1 == '\n' = calcRegionLines' width (finishline $ T.init currline)+		[] 0 1 (T.length rest) (contSGR rest)+	-- ANSI escape sequences do not take up space on screen.+	| t1 == '\ESC' && i+1 < len = case T.index t (i+1) of+		'[' -> skipansi endCSI True+		']' -> skipansi endOSC False+		_ -> calcRegionLines' width collectedlines collectedSGR (i+1) displaysize len t+	-- Control characters do not take up space on screen.+	| isControl t1 = calcRegionLines' width collectedlines collectedSGR (i+1) displaysize len t+	| displaysize >= width = calcRegionLines' width (finishline currline)+		[] 0 1 (T.length rest) (contSGR rest)+	| otherwise = calcRegionLines' width collectedlines collectedSGR (i+1) (displaysize+1) len t   where-	wnl = fromIntegral (ord '\n')+	t1 = T.index t i+	(currline, rest) = T.splitAt (i+1) t --- Calculate the height a line would occupy if output onto a console--- with a given width, starting from the first column.+	skipansi toend isCSI = case T.findIndex toend (T.drop (i+2) t) of+		Just csiend -> calcRegionLines' width collectedlines +			(addSGR (csiend+2)) (i+2+csiend) (displaysize-1) len t+		Nothing -> reverse (finishline t)+	  where+		addSGR csiend+			| not isCSI = collectedSGR+			| ansicode == resetSGR = []+			| not (T.null ansicode) && T.last ansicode == endSGR =+				ansicode : collectedSGR+			| otherwise = collectedSGR+		  where+			ansicode = T.take (csiend + 1) (T.drop i t)+	finishline l = closeSGR l : collectedlines+	-- Close any open SGR codes at end of line+	closeSGR l+		| null collectedSGR = l+		| otherwise = l <> resetSGR+	-- Continue any open SGR codes from previous line+	contSGR l = mconcat (reverse collectedSGR) <> l++resetSGR :: T.Text+resetSGR = T.pack (setSGRCode [Reset])++endCSI :: Char -> Bool+endCSI c = let o = ord c in o >= 64 && o < 127++endOSC :: Char -> Bool+endOSC c = c == '\BEL'++endSGR :: Char+endSGR = 'm'++-- | Finds the least expensive output to make a console that was displaying+-- the old line display the new line. Cursor starts at far left. ----- Note that this counts each byte, is not aware of multibyte characters,--- so will over-estimate a bit for those.+-- Basically, loop through and find spans where the old and new line are+-- the same. Generate cursorForwardCode ANSI sequences to skip over those+-- spans, unless such a sequence would be longer than the span it's skipping. ----- Also an issue are ANSI escape sequences for eg, setting colors or the--- title. This is handled by stripping out common ANSI escape sequences--- and control characters.-calcLineHeight :: Width -> B.ByteString -> Height-calcLineHeight width b-	| width < 1 || B.null b = 1 -- even an empty line is 1 line high-	| otherwise =-		let (q,r) = (B.length b - countInvisibleBytes b) `quotRem` width-		in q + if r > 0 then 1 else 0+-- Since ANSI sequences can be present in the line, need to take them+-- into account. Generally, each of the sequences in new has to be included,+-- even if old contained the same sequence:+--+-- > old: GREENfoofoofooREDbarbarbarRESETbaz+-- > new: GREENfoofoofooREDxarbarbaxRESETbaz+-- > ret: GREEN-------->REDx------>yRESET+--+-- (The first GREEN does not effect any output text, so it can be elided.)+-- +-- Also, despite old having the same second span as new, in the same+-- location, that span has to be re-emitted because its color changed:+-- +-- > old: GREENfoofooREDbarbarbarbarbar+-- > new: GREENfoofoofooTANbarbarbar+-- > ret: GREEN----->fooTANbarbarbarCLEARREST+--+-- Also note above that the sequence has to clear the rest of the line,+-- since the new line is shorter than the old.+calcLineUpdate :: T.Text -> T.Text -> [LineUpdate]+calcLineUpdate old new = +	reverse $ go+		(advanceLine old [] [])+		(advanceLine new [] [])+  where+	go (Just _, _, _, _) (Nothing, _, past, _) = ClearToEnd : past+	go (Nothing, _, _, _) (Nothing, _, past, _) = past+	go (Nothing, _, _, _) (Just n, ns, past, _) =+		Display ns : Display (T.singleton n) : past+	go (Just o, os, _, oinvis) (Just n, ns, past, ninvis)+		| o == n && oinvis == ninvis = go+			(advanceLine os [] oinvis)+			(advanceLine ns (Skip [o] : past) ninvis)+		| otherwise = go+			(advanceLine os [] oinvis)+			(advanceLine ns (Display (T.singleton n) : past) ninvis) --- ANSI sequences and control characters.-countInvisibleBytes :: B.ByteString -> Int-countInvisibleBytes = go 0 . breakesc+type Past = [LineUpdate]+type Invis = [LineUpdate]++-- Find next character of t that is not a ANSI escape sequence+-- or control char. Any such passed on the way to the character+-- are prepended to past, and added to invis.+--+-- resetSGR is handled specially; it causes all SGRs to be removed from+-- invis, It's still prepended to past.+advanceLine :: T.Text -> Past -> Invis -> (Maybe Char, T.Text, Past, Invis)+advanceLine t past invis+	| T.null t = (Nothing, T.empty, past, invis)+	| otherwise = case T.head t of+		'\ESC' -> case T.drop 1 t of+			t' | T.null t' -> advanceLine (T.drop 1 t)+				(Skip "\ESC":past) (Skip "\ESC":invis)+			   | otherwise -> case T.head t' of+			   	'[' -> skipansi endCSI+				']' -> skipansi endOSC+				c -> (Just c, T.drop 2 t, Skip "\ESC":past, Skip "\ESC":invis)+		c | isControl c -> advanceLine (T.drop 1 t) (Skip [c]:past) (Skip [c]:invis)+		  | otherwise -> (Just c, T.drop 1 t, past, invis)   where-	go c (beforeesc, b)-		| B.length b <= 1 = B.length b + c'-		| headis csi (B.drop 1 b) = countseq breakcsi-		| headis osc (B.drop 1 b) = countseq breakosc-		-- found an ESC but apparently not in an ANSI sequence;-		-- count it as 1 invisible control character-		| otherwise = go (c'+1) (breakesc (B.drop 1 b))+	skipansi toend = case T.findIndex toend (T.drop 2 t) of+		Just csiend -> +			let sgr = SGR (T.take (csiend+3) t)+			in advanceLine (T.drop (csiend+3) t)+				(sgr:past) (addsgr sgr invis)+		Nothing -> (Nothing, T.empty, past, invis)+	addsgr (SGR sgrt) l+		| sgrt == resetSGR = filter (not . isSGR) l+	addsgr s l = s:l++data LineUpdate = Display T.Text | Skip [Char] | SGR T.Text | ClearToEnd+	deriving (Eq, Show)++isSGR :: LineUpdate -> Bool+isSGR (SGR _) = True+isSGR _ = False++genLineUpdate :: [LineUpdate] -> T.Text+genLineUpdate l = T.concat $ map tot (optimiseLineUpdate l)+  where+	tot (Display t) = t+	tot (Skip s)+		-- length (cursorForwardCode 1) == 4 so there's no point+		-- generating that for a skip of less than 5.+		| len < 5 = T.pack s+		| otherwise = T.pack (cursorForwardCode len) 	  where-		c' = c + countControlChars beforeesc-		countseq breaker =-			let (inseq, b') = breaker (B.drop 2 b)-			-- add 1 for the ESC and one for the head char-			-- that introduced the sequence, plus the length-			-- of the rest of the sequence, plus 1 for -			-- the end char of the sequence.-			in go (c'+1+1+B.length inseq+1) (breakesc (B.drop 1 b'))+		len = length s+	tot (SGR t) = t+	tot ClearToEnd = T.pack clearFromCursorToLineEndCode -	esc = fromIntegral (ord '\ESC')-	bel = fromIntegral (ord '\BEL')-	breakesc = B.break (== esc)-	headis c b = B.head b ==c-	csi = fromIntegral (ord '[') -- Control Sequence Introducer-	csiend c = c >= 64 && c < 127-	breakcsi = B.break csiend-	osc = fromIntegral (ord ']') -- Operating system command-	breakosc = B.break (== bel)+optimiseLineUpdate :: [LineUpdate] -> [LineUpdate]+optimiseLineUpdate = go []+  where+	-- elide trailing Skips+	go (Skip _:rest) [] = go rest []+	-- elide SGRs at the end of the line, except for the reset SGR+	go (SGR t:rest) [] | t /= resetSGR = go rest []+	go c [] = reverse c+	-- combine adjacent SGRs and Skips+	go c (SGR t1:Skip s:SGR t2:rest) = tryharder c (SGR (combineSGR t1 t2):Skip s:rest)+	go c (Skip s:Skip s':rest) = tryharder c (Skip (s++s'):rest)+	go c (SGR t1:SGR t2:rest) = tryharder c (SGR (combineSGR t1 t2):rest)+	go c (v:rest) = go (v:c) rest+	tryharder c l = go [] (reverse c ++ l) -countControlChars :: B.ByteString -> Int-countControlChars = length . filter iscontrol8 . B.unpack+-- Parse and combine 2 ANSI SGR sequences into one.+combineSGR :: T.Text -> T.Text -> T.Text+combineSGR a b = case combineSGRCodes (codes a) (codes b) of+	Nothing -> a <> b+	Just cs -> T.pack $ "\ESC[" ++ intercalate ";" (map show cs) ++ "m"   where-	iscontrol8 c = c < 32 || c == 127 -- del+	codes = map (readMaybe . T.unpack) .+		T.split (== ';') . T.drop 2 . T.init++-- Prefers values from the second sequence when there's a conflict with+-- values from the first sequence.+combineSGRCodes :: [Maybe Int] -> [Maybe Int] -> Maybe [Int]+combineSGRCodes as bs =+	map snd . nubBy (\a b -> fst a == fst b) <$> mapM range (reverse bs ++ reverse as)+  where+	range Nothing = Nothing+	range (Just x)+		| x >= 30 && x <= 37 = Just (Foreground, x)+		| x >= 40 && x <= 47 = Just (Background, x)+		| x >= 90 && x <= 97 = Just (Foreground, x)+		| x >= 100 && x <= 107 = Just (Background, x)+		| otherwise = Nothing
+ System/Process/Concurrent.hs view
@@ -0,0 +1,34 @@+-- | +-- Copyright: 2015 Joey Hess <id@joeyh.name>+-- License: BSD-2-clause+-- +-- The functions exported by this module are intended to be drop-in+-- replacements for those from System.Process, when converting a whole+-- program to use System.Console.Concurrent.++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)++-- | 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
TODO view
@@ -1,5 +1,4 @@-Optimize line update, avoiding outputting characters already on-screen (aka-what curses does, but this is portable to even windows, so no curses. Anway,-should be under 20 line function.)--Make layout take region width into account.+It would be nice to have regions whose content was determined by running a+STM Text action. Such regions should update whenever the values that action+accessed change. Could be used for such things as spreadsheets.+I have this mostly done in the stmregions git branch, but not quite working.
aptdemo.hs view
@@ -4,11 +4,12 @@ import Control.Concurrent import System.Console.Concurrent import System.Console.Regions+import System.Console.ANSI  main = displayConsoleRegions $ do 	mapConcurrently downline-		[ ["foo", "bar", "baz"]-		, ["pony", "mango", "very_large"]+		[ [dl "pony", growingdots, dl "mango"]+		, [dl "foo", dl "bar", dl "very large"] 		] 		`concurrently` mapM_ message [1..20] @@ -17,14 +18,24 @@ 	outputConcurrent ("Updated blah blah #" ++ show n ++ "\n")  downline cs = withConsoleRegion Linear $ \r -> -	mapConcurrently (kid r) (reverse cs)+	mapConcurrently (\a -> a r) (reverse cs)++dl c parent = withConsoleRegion (InLine parent) (go 0)   where-	kid parent c = withConsoleRegion (InLine parent) (go c 0)-	go c n r+	go n r 		| n <= 100 = do 			setConsoleRegion r $-				"[" ++ c ++ " " ++ show n ++ "%] "+				"[" ++ setSGRCode [SetColor Foreground Vivid Green] ++ c ++ setSGRCode [Reset] ++ " " ++ show n ++ "%] " 			threadDelay (25000 * length c)-			go c (n+1) r+			go (n+1) r 		| otherwise = finishConsoleRegion r $ 			"Downloaded " ++ c ++ ".deb"++growingdots parent = withConsoleRegion (InLine parent) (go 0)+  where+	go n r+		| n <= 250 = do+			setConsoleRegion r ("[" ++ setSGRCode [SetColor Foreground Vivid Blue] ++ replicate n '.' ++ setSGRCode [Reset] ++ "] ")+			threadDelay (100000)+			go (n+1) r+		| otherwise = return ()
concurrent-output.cabal view
@@ -1,5 +1,5 @@ Name: concurrent-output-Version: 1.1.0+Version: 1.2.0 Cabal-Version: >= 1.8 License: BSD2 Maintainer: Joey Hess <id@joeyh.name>@@ -31,8 +31,7 @@  Library   GHC-Options: -Wall -fno-warn-tabs -O2-  Build-Depends: base (>= 4.5), base < 5-    , bytestring (>= 0.10.0 && < 0.11)+  Build-Depends: base (>= 4.6), base < 5     , text (>= 1.2.0 && < 1.3.0)     , async (>= 2.0 && < 2.1)     , stm (>= 2.0 && < 2.5)@@ -45,7 +44,9 @@     , terminal-size (>= 0.3.0 && < 0.4.0)   Exposed-Modules:     System.Console.Concurrent+    System.Console.Concurrent.Internal     System.Console.Regions+    System.Process.Concurrent   Other-Modules:     Utility.Monad     Utility.Data
demo.hs view
@@ -34,7 +34,7 @@ 	threadDelay 1000000 	(Nothing, Nothing, Nothing, p) <- createProcessConcurrent (proc "ls" ["-C"]) 	outputConcurrent "started running ls >>>"-	_ <- waitForProcess p+	_ <- waitForProcessConcurrent p 	outputConcurrent "<<< ls is done!\n" 	return () 	
demo2.hs view
@@ -6,7 +6,7 @@ main = displayConsoleRegions $ mapConcurrently id 	[ spinner 100 1 "Pinwheels!!" setConsoleRegion "/-\\|" (withtitle 1) 	, spinner 100 1 "Bubbles!!!!" setConsoleRegion ".oOo." (withtitle 1)-	, spinner  40 2 "Dots......!" appendConsoleRegion "."  (const (take 1))+	, spinner 100 1 "Dots......!" appendConsoleRegion "."  (const (take 1)) 	, spinner  30 2 "KleisiFish?" setConsoleRegion "  <=<   <=<  " (withtitle 10) 	, spinner  10 9 "Countdowns!" setConsoleRegion 		(reverse [1..10])
demo3.hs view
@@ -3,10 +3,12 @@ import System.Process  main = withConcurrentOutput $-	outputConcurrent "washed the car\n"+	outputConcurrent "hello world\n" 		`concurrently` 	createProcessConcurrent (proc "ls" []) 		`concurrently`-	createProcessConcurrent (proc "sh" ["-c", "echo \"hi from echo!\" >&2"])+	createProcessConcurrent (proc "who" []) 		`concurrently`-	outputConcurrent "walked the dog\n"+	createProcessForeground (proc "vim" [])+		`concurrently`+	outputConcurrent "hello world again\n"