packages feed

core-program 0.6.7.1 → 0.6.8.0

raw patch · 3 files changed

+130/−3 lines, 3 filesdep +processPVP ok

version bump matches the API change (PVP)

Dependencies added: process

API changes (from Hackage documentation)

+ Core.Program.Execute: callProcess :: [Rope] -> Program τ ExitCode
+ Core.Program.Threads: ThreadCancelled :: Terminator
+ Core.Program.Threads: data Terminator
+ Core.Program.Threads: instance GHC.Exception.Type.Exception Core.Program.Threads.Terminator
+ Core.Program.Threads: instance GHC.Show.Show Core.Program.Threads.Terminator

Files

core-program.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           core-program-version:        0.6.7.1+version:        0.6.8.0 synopsis:       Opinionated Haskell Interoperability description:    A library to help build command-line programs, both tools and                 longer-running daemons.@@ -68,6 +68,7 @@     , hourglass     , mtl     , prettyprinter >=1.6.2+    , process     , safe-exceptions     , stm     , template-haskell >=2.14 && <3
lib/Core/Program/Execute.hs view
@@ -93,6 +93,7 @@        -- * Running processes     , readProcess+    , callProcess     , execProcess_        -- * Internals@@ -140,6 +141,7 @@ import Control.Exception qualified as Base (throwIO) import Control.Exception.Safe qualified as Safe     ( catch+    , onException     , throw     ) import Control.Monad@@ -178,8 +180,17 @@     ( findExecutable     ) import System.Exit (ExitCode (..))+import System.IO qualified as Base (IOMode (ReadMode), hClose, openFile) import System.Posix.Internals (hostIsThreaded) import System.Posix.Process qualified as Posix (executeFile, exitImmediately)+import System.Process qualified as Base+    ( CreateProcess (std_err, std_in, std_out)+    , StdStream (Inherit, UseHandle)+    , createProcess+    , proc+    , terminateProcess+    , waitForProcess+    ) import System.Process.Typed qualified as Typed (nullStream, proc, readProcess, setStdin) import Prelude hiding (log) @@ -783,6 +794,79 @@                 -- does not return                 _ <- Posix.executeFile cmd' True args' Nothing                 pure ()++{- |+Execute an external child process and wait for it to finish. The command is+specified first and and subsequent arguments as elements of the list. This+helper then logs the command being executed to the debug output, which can be+useful when you're trying to find out what exactly what program is being+invoked.++The output of the child process (its @stdout@) will go to the terminal console+independently of your parent process's output. If your Haskell program does+anything concurrently then anything it 'Core.Program.Logging.write's will be+interleaved and probably make a mess of the child's output. So don't do that.++See the similar 'readProcess' for an action which executes an external program+but which returns its output.++If the thread invoking 'callProcess' receives an interrupting asynchronous+exception then it will terminate the child, waiting for it to exit.++(this wraps __typed-process__'s 'System.Process.Typed.runProcess' but follows+the naming convention of the underlying 'System.Process.callProcess' code from+__process__.)++@since 0.6.8+-}+callProcess :: [Rope] -> Program τ ExitCode+callProcess [] = error "No command provided"+callProcess (cmd : args) = do+    let cmd' = fromRope cmd+    let args' = fmap fromRope args+    let task1 = Base.proc cmd' args'++    let command = mconcat (List.intersperse (singletonRope ' ') (cmd : args))+    debug "command" command++    probe <- liftIO $ do+        findExecutable cmd'++    case probe of+        Nothing -> do+            Safe.throw (CommandNotFound cmd)+        Just _ -> do+            liftIO $ do+                i <- Base.openFile "/dev/null" Base.ReadMode++                let task2 =+                        task1+                            { Base.std_in = Base.UseHandle i+                            , Base.std_out = Base.Inherit+                            , Base.std_err = Base.Inherit+                            }++                (_, _, _, p) <- Base.createProcess task2++                Safe.onException+                    ( do+                        exit <- Base.waitForProcess p+                        Base.hClose i+                        pure exit+                    )+                    ( do+                        --+                        -- To avoid defunct zombie processes, you have to+                        -- wait() on the process and read its exit code. In+                        -- normal circumstances this happens because we are+                        -- _waiting_ but in abnormal circumstances where we+                        -- are forcing the child, we have to wait for the OS+                        -- to give us an exit code.+                        --+                        Base.terminateProcess p+                        _ <- Base.waitForProcess p+                        Base.hClose i+                    )  {- | Reset the start time (used to calculate durations shown in event- and
lib/Core/Program/Threads.hs view
@@ -44,10 +44,11 @@       -- * Internals     , Thread     , unThread+    , Terminator (..)     ) where  import Control.Concurrent (ThreadId, forkIO, killThread)-import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, putMVar, readMVar)+import Control.Concurrent.MVar (MVar, newEmptyMVar, newMVar, putMVar, readMVar, tryPutMVar) import Control.Concurrent.STM (atomically) import Control.Concurrent.STM.TVar (modifyTVar', newTVarIO, readTVarIO) import Control.Exception.Safe qualified as Safe (catch, finally, onException, throw)@@ -354,6 +355,14 @@ {- | Cancel a thread. +Be careful when using this. If you are planning cancel a worker thread then+the main thread that is 'waitThread'ing on it will /throw an exception/,+specifically 'ThreadCancelled' (unless something else has already thrown an+exception in which case /that/ will be thrown instead). In this scenario you+will need to 'Core.Program.Exceptions.catch' around your waiting function+otherwise the uncaught exception will continue to unwind your execution stack+and probably end your program.+ (this wraps __base__\'s 'Control.Concurrent.killThread'. The underlying mechanism used is to throw the 'GHC.Conc.ThreadKilled' exception to the other thread. That exception is asynchronous, so will not be trapped by a@@ -365,7 +374,40 @@ cancelThread :: Thread α -> Program τ () cancelThread thread = do     liftIO $ do-        killThread (threadPointerOf thread)+        --+        -- There are some curiosities about what happens here. Someone+        -- waitThread'ing on a Thread is blocked on reading the outcome MVar.+        -- so to break that wait we put the Left value in. If the thread was+        -- already dead this has no effect, but if not, then this will+        -- initiate it rapidly being killing off.+        --+        let outcome = threadOutcomeOf thread+        result <- tryPutMVar outcome (Left (toException ThreadCancelled))+        case result of+            False -> do+                pure ()+            True -> do+                killThread (threadPointerOf thread)++{- |+When a thread is aborted with 'cancelThread' this value is used to mark a+failed computation inside the 'Thread'. Although it is not the mechanism used+internally to kill the computation, it /is/ the exception that is subsequently+rethrown from 'waitThread' if you are waiting on that thread to finish,+allowing you to 'Core.Program.Exceptions.catch' the case of a thread being+cancelled if necessary.++This is mostly here to differentiate from 'Control.Exception.ThreadKilled',+giving you some knowledge as to whether it was your explicit 'cancelThread'+that ended the thread, or something else. You need to handle it either way,+but sometimes you want to know the difference.++@since 0.6.8+-}+data Terminator = ThreadCancelled+    deriving (Show)++instance Exception Terminator  {- | Fork two threads and wait for both to finish. The return value is the pair of