packages feed

buildbox 2.1.7.1 → 2.1.8.1

raw patch · 9 files changed

+168/−169 lines, 9 filesdep +exceptionsdep +temporarydep −randomPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: exceptions, temporary

Dependencies removed: random

API changes (from Hackage documentation)

- BuildBox.Build: [buildStateId] :: BuildState -> Integer
- BuildBox: catch :: Build a -> (BuildError -> Build a) -> Build a
+ BuildBox: catch :: MonadCatch m => forall a e. Exception e => m a -> (e -> m a) -> m a
- BuildBox: throw :: BuildError -> Build a
+ BuildBox: throw :: (MonadThrow m, Exception e) => e -> m a
- BuildBox: type Build a = ErrorT BuildError (StateT BuildState IO) a
+ BuildBox: type Build a = StateT BuildState IO a
- BuildBox.Build: BuildState :: Maybe Handle -> Integer -> Integer -> FilePath -> BuildState
+ BuildBox.Build: BuildState :: Maybe Handle -> Integer -> FilePath -> BuildState
- BuildBox.Build: buildStateDefault :: Integer -> FilePath -> BuildState
+ BuildBox.Build: buildStateDefault :: FilePath -> BuildState
- BuildBox.Build: catch :: Build a -> (BuildError -> Build a) -> Build a
+ BuildBox.Build: catch :: MonadCatch m => forall a e. Exception e => m a -> (e -> m a) -> m a
- BuildBox.Build: throw :: BuildError -> Build a
+ BuildBox.Build: throw :: (MonadThrow m, Exception e) => e -> m a
- BuildBox.Build: type Build a = ErrorT BuildError (StateT BuildState IO) a
+ BuildBox.Build: type Build a = StateT BuildState IO a

Files

BuildBox/Build.hs view
@@ -36,19 +36,25 @@ import BuildBox.Build.BuildState import BuildBox.Build.BuildError import Control.Monad.State+import Control.Monad.Catch import System.IO import Prelude  +-- | Alias for 'throwM' from Control.Monad.Catch.+throw :: (MonadThrow m, Exception e) => e -> m a+throw = throwM++ -- | Log a system command to the handle in our `BuildConfig`, if any. logSystem :: String -> Build () logSystem cmd  = do   mHandle <- gets buildStateLogSystem         case mHandle of          Nothing        -> return ()-         Just handle    -          -> do io $ hPutStr   handle "buildbox system: "-                io $ hPutStrLn handle cmd+         Just h    +          -> do io $ hPutStr   h "buildbox system: "+                io $ hPutStrLn h cmd                 return ()  
BuildBox/Build/Base.hs view
@@ -4,16 +4,15 @@ import BuildBox.Pretty import BuildBox.Build.BuildError import BuildBox.Build.BuildState-import Control.Monad.Error+import Control.Monad.Catch import Control.Monad.State-import Control.Exception        (try) import System.IO-import System.Random import System.Directory + -- | The builder monad encapsulates and IO action that can fail with an error,  --   and also read some global configuration info.-type Build a    = ErrorT BuildError (StateT BuildState IO) a+type Build a    = StateT BuildState IO a   -- Build ------------------------------------------------------------------------------------------@@ -21,26 +20,24 @@ --   temporary files (like \"/tmp\") runBuild :: FilePath -> Build a -> IO (Either BuildError a) runBuild scratchDir build- = do   uid     <- getUniqueId-        let s   = buildStateDefault uid scratchDir-        evalStateT (runErrorT build) s+ = do   let s   = buildStateDefault scratchDir+        try $ evalStateT build s   -- | Like 'runBuild`, but report whether it succeeded to the console. --   If it succeeded then return Just the result, else Nothing. runBuildPrint :: FilePath -> Build a -> IO (Maybe a) runBuildPrint scratchDir build- = do   uid     <- getUniqueId-        let s   = buildStateDefault uid scratchDir+ = do   let s   = buildStateDefault scratchDir         runBuildPrintWithState s build   -- | Like `runBuild` but also takes a `BuildState`. runBuildWithState :: BuildState -> Build a -> IO (Maybe a) runBuildWithState s build- = do   result  <- evalStateT (runErrorT build) s+ = do   result  <- try $ evalStateT build s         case result of-         Left err+         Left (err :: BuildError)           -> do putStrLn $ render $ ppr err                 return $ Nothing                 @@ -51,9 +48,9 @@ -- | Like `runBuildPrint` but also takes a `BuildState`. runBuildPrintWithState :: BuildState -> Build a -> IO (Maybe a) runBuildPrintWithState s build- = do   result  <- evalStateT (runErrorT build) s+ = do   result  <- try $ evalStateT build s         case result of-         Left err+         Left (err :: BuildError)           -> do putStrLn "\nBuild failed"                 putStr   "  due to "                 putStrLn $ render $ ppr err@@ -70,29 +67,7 @@ successfully f  = f >> return ()  --- | Get a unique(ish) id for this process.---   The random seeds the global generator with the cpu time in psecs, which should be good enough.-getUniqueId :: IO Integer-getUniqueId-        = randomRIO (0, 1000000000)     - -- Errors -------------------------------------------------------------------------------------------- | Throw an error in the build monad.-throw :: BuildError -> Build a-throw   = throwError----- | Run a build command, catching any exceptions it sends.-catch :: Build a -> (BuildError -> Build a) -> Build a-catch build handle- = do   s            <- get-        (result, s') <- io $ runStateT (runErrorT build) s-        case result of-         Left err -> handle err-         Right x  -          -> do put s'-                return x- -- | Throw a needs error saying we needs the given file. --   A catcher could then usefully create the file, or defer the compuation until it has been  --   created.@@ -103,7 +78,7 @@                  if isFile || isDir          then return ()-         else throw $ ErrorNeeds filePath+         else throwM $ ErrorNeeds filePath   -- Utils ------------------------------------------------------------------------------------------@@ -116,7 +91,7 @@         result  <- liftIO $ try x                  case result of-         Left err       -> throw $ ErrorIOError err+         Left  err      -> throwM $ ErrorIOError err          Right res      -> return res  
BuildBox/Build/BuildError.hs view
@@ -5,8 +5,9 @@         (BuildError(..)) where import BuildBox.Pretty+import Control.Monad.Catch+import Data.Typeable import System.Exit-import Control.Monad.Error import BuildBox.Data.Log                (Log) import qualified BuildBox.Data.Log      as Log @@ -33,10 +34,11 @@         -- | A build command needs the following file to continue.         --   This can be used for writing make-like bots.         | ErrorNeeds FilePath+        deriving Typeable++instance Exception BuildError          -instance Error BuildError where- strMsg s = ErrorOther s  instance Pretty BuildError where  ppr err
BuildBox/Build/BuildState.hs view
@@ -12,12 +12,7 @@         = BuildState         { -- | Log all system commands executed to this file handle.           buildStateLogSystem   :: Maybe Handle-        -          -- | Uniqueish id for this build process.-          --   On POSIX we'd use the PID, but that doesn't work on Windows.-          --   The id is initialised by the Haskell random number generator on startup.-        , buildStateId          :: Integer-        +                           -- | Sequence number for generating fresh file names.         , buildStateSeq         :: Integer          @@ -26,11 +21,10 @@   -- | The default build config.-buildStateDefault :: Integer -> FilePath -> BuildState-buildStateDefault uniqId scratchDir +buildStateDefault :: FilePath -> BuildState+buildStateDefault scratchDir          = BuildState         { buildStateLogSystem   = Nothing-        , buildStateId          = uniqId         , buildStateSeq         = 0          , buildStateScratchDir  = scratchDir } 
BuildBox/Build/Testable.hs view
@@ -11,13 +11,14 @@ where import BuildBox.Build.Base       import BuildBox.Build.BuildError-import Control.Monad.Error+import Control.Monad.Catch   -- | Some testable property. class Testable prop where   test :: prop -> Build Bool + -- | Testable properties are checkable. --   If the check returns false then throw an error. check :: (Show prop, Testable prop) => prop -> Build ()@@ -25,7 +26,7 @@  = do   result  <- test prop         if result          then return ()-         else throwError $ ErrorCheckFailed True prop+         else throwM $ ErrorCheckFailed True prop   -- | Testable properties are checkable.@@ -34,7 +35,7 @@ checkFalse prop  = do   result  <- test prop         if result-         then throwError $ ErrorCheckFailed False prop+         then throwM $ ErrorCheckFailed False prop          else return ()          
BuildBox/Command/Darcs.hs view
@@ -83,7 +83,10 @@       in       DarcsPatch         {-          darcsTimestamp = readTime defaultTimeLocale "%a %b %e %H:%M:%S %Z %Y" (unwords time)+          darcsTimestamp = parseTimeOrError True +                                defaultTimeLocale+                                "%a %b %e %H:%M:%S %Z %Y"+                                (unwords time)         , darcsAuthor    = unwords author         , darcsComment   = Seq.drop 1 p         }
BuildBox/Command/File.hs view
@@ -12,9 +12,12 @@ where import BuildBox.Build import System.Directory-import Control.Exception+-- import Control.Exception import Control.Monad.State+import Control.Monad.Catch import System.Info+import qualified System.IO.Temp         as System+import qualified System.IO              as System  -- | Properties of the file system we can test for. data PropFile@@ -103,55 +106,34 @@ -- | Create a temp file, pass it to some command, then delete the file after the command finishes. withTempFile :: (FilePath -> Build a) -> Build a withTempFile build- = do   fileName        <- newTempFile--        -- run the real command-        result  <- build fileName--        -- cleanup-        io $ removeFile fileName--        return result----- | Allocate a new temporary file name-newTempFile :: Build FilePath-newTempFile  = do   buildDir        <- gets buildStateScratchDir-        buildId         <- gets buildStateId         buildSeq        <- gets buildStateSeq -        -- Increment the sequence number.-        modify $ \s -> s { buildStateSeq = buildStateSeq s + 1 }--        -- Ensure the build directory exists, or canonicalizePath will fail-        ensureDir buildDir--        -- Build the file name we'll try to use.-        -- We need to account for a blank scratch directory, otherwise there is-        -- no way to use the CD as a scratch on Windows.-        let fileName     = (if (null buildDir) then "" else (buildDir ++ "/"))-                ++ "buildbox-" ++ show buildId ++ "-" ++ show buildSeq-                                                -- TODO: normalise path--        -- If it already exists then something has gone badly wrong.-        --   Maybe the unique Id for the process wasn't as unique as we thought.-        exists          <- io $ doesFileExist fileName-        when exists-         $ error "buildbox: panic, supposedly fresh file already exists."--        -- Touch the file for good measure.-        --   If the unique id wasn't then we want to detect this.-        io $ writeFile fileName ""+        -- File name template.+        let sTemplate   = "buildbox-" ++ show buildSeq ++ ".tmp" -        io $ canonicalizePath fileName+        System.withTempFile +                buildDir sTemplate+                (\  fileName h +                 -> do  io $ System.hClose h+                        build fileName)   -- | Atomically write a file by first writing it to a tmp file then renaming it. --   This prevents concurrent processes from reading half-written files. atomicWriteFile :: FilePath -> String -> Build () atomicWriteFile filePath str- = do   tmp     <- newTempFile+ = do   buildDir        <- gets buildStateScratchDir+        buildSeq        <- gets buildStateSeq++        -- File name template.+        let sTemplate   = "buildbox-" ++ show buildSeq ++ ".tmp"++        -- Create a new temp file.+        (tmp, h)        <- io $ System.openBinaryTempFile buildDir sTemplate+        io $ System.hClose h++        -- Write the data to the temp file.         io $ writeFile tmp str         e <- io $ try $ renameFile tmp filePath 
BuildBox/Control/Gang.hs view
@@ -16,18 +16,25 @@         , waitForGangState) where import Control.Concurrent+import Control.Exception import Data.IORef import qualified Data.Set       as Set import Data.Set                 (Set) + -- Gang ----------------------------------------------------------------------- -- | Abstract gang of threads. data Gang         = Gang          { gangThreads           :: Int++        -- | Number of worker threads currently waiting.         , gangThreadsAvailable  :: QSemN+         , gangState             :: IORef GangState+         , gangActionsRunning    :: IORef Int +         , gangThreadsRunning    :: IORef (Set ThreadId) }  @@ -60,20 +67,37 @@ -- | Block until all actions have finished executing, --   or the gang is killed. joinGang :: Gang -> IO ()-joinGang gang- = do   state   <- readIORef (gangState gang)+joinGang !gang+ = do   +        -- Wait for all the threads to become available.+        waitQSemN (gangThreadsAvailable gang)+                  (gangThreads gang)++        -- See what state the gang is now in.+        state   <- readIORef (gangState gang)+         if state == GangFinished || state == GangKilled+         -- The gang is done.          then return ()++         -- Hmm. We're holding all the threads but the gang is still+         -- running. Maybe the controller hasn't started the first+         -- one yet. Just put them all back and try again.+         -- We delay for a moment to allow the controller to run.          else do                 threadDelay 1000++                signalQSemN (gangThreadsAvailable gang)+                            (gangThreads gang)+                 joinGang gang   -- | Block until already started actions have completed, but don't start any more. --   Gang state changes to `GangFlushing`. flushGang :: Gang -> IO ()-flushGang gang- = do   writeIORef (gangState gang) GangFlushing+flushGang !gang+ = do   atomicWriteIORef (gangState gang) GangFlushing         waitForGangState gang GangFinished  @@ -81,34 +105,60 @@ --   but no more will be started until a `resumeGang` command is issued. --   Gang state changes to `GangPaused`. pauseGang :: Gang -> IO ()-pauseGang gang-        = writeIORef (gangState gang) GangPaused+pauseGang !gang+ = do   -- Set the gang to paused mode.+        -- This will prevent any new threads from being started.+        atomicWriteIORef (gangState gang) GangPaused   -- | Resume a paused gang, which allows it to continue starting new actions.---   Gang state changes to `GangRunning`.+--   If the gang was paused it now changes to `GangRunning`,+--   otherwise nothing happens. resumeGang :: Gang -> IO ()-resumeGang gang-        = writeIORef (gangState gang) GangRunning+resumeGang !gang+ = atomicModifyIORef' (gangState gang) $ \state + -> do  case state of +         GangPaused     -> (GangRunning, ())+         _              -> (state, ())   -- | Kill all the threads in a gang. --   Gang stage changes to `GangKilled`. killGang :: Gang -> IO ()-killGang gang- = do   writeIORef (gangState gang) GangKilled+killGang !gang+ = do   +        atomicWriteIORef (gangState gang) GangKilled+         tids    <- readIORef (gangThreadsRunning gang)          mapM_ killThread $ Set.toList tids +        -- Signal that all the threads are available.+        --+        -- NOTE: There is a race here where a thread might have terminated+        -- cleanly and already bumped the QSemN just before were to add +        -- to it, but we've killed the gang anyway so nothing more will+        -- be run.+        signalQSemN (gangThreadsAvailable gang) (length tids) + -- | Block until the gang is in the given state. waitForGangState :: Gang -> GangState -> IO ()-waitForGangState gang waitState- = do   state   <- readIORef (gangState gang)+waitForGangState !gang !waitState+ = do   +        -- Wait for all the threads to become available.+        waitQSemN (gangThreadsAvailable gang)+                  (gangThreads gang)++        state   <- readIORef (gangState gang)+         if state == waitState          then return ()          else do                 threadDelay 1000++                signalQSemN (gangThreadsAvailable gang)+                            (gangThreads gang)+                 waitForGangState gang waitState  @@ -117,13 +167,13 @@ --   Gang state starts as `GangRunning` then transitions to `GangFinished`. --   To block until all the actions are finished use `joinGang`. forkGangActions-        :: Int                  -- ^ Number of worker threads in the gang \/ maximum number-                                --   of actions to execute concurrenty.-        -> [IO ()]              -- ^ Actions to run. They are started in-order, but may finish-                                --   out-of-order depending on the run time of the individual action.+        :: Int          -- ^ Number of worker threads in the gang \/ maximum number+                        --   of actions to execute concurrenty.+        -> [IO ()]      -- ^ Actions to run. They are started in-order, but may finish+                        --   out-of-order depending on the run time of the individual action.         -> IO Gang -forkGangActions threads actions+forkGangActions !threads !actions  = do   semThreads              <- newQSemN threads         refState                <- newIORef GangRunning         refActionsRunning       <- newIORef 0@@ -142,70 +192,52 @@  -- | Run actions on a gang. gangLoop :: Gang -> [IO ()] -> IO ()-gangLoop gang []- = do   -- Wait for all the threads to finish.-        waitQSemN -                (gangThreadsAvailable gang) -                (gangThreads gang)-                -        -- Signal that the gang is finished running actions.++gangLoop !gang []+ = do   -- Signal that the gang is finished running actions.         writeIORef (gangState gang) GangFinished +gangLoop !gang actions@(action:actionsRest)+ = do   +        -- Wait for a worker thread to become available.+        waitQSemN  (gangThreadsAvailable gang) 1+        +        -- See what state the gang is in.+        state   <- readIORef (gangState gang) -gangLoop gang actions@(action:actionsRest)- = do   state   <- readIORef (gangState gang)         case state of          GangRunning -          -> do -- Wait for a worker thread to become available.-                waitQSemN (gangThreadsAvailable gang) 1-                gangLoop_withWorker gang action actionsRest--         GangPaused-          -> do threadDelay 1000-                gangLoop gang actions-                        -         GangFlushing-          -> do actionsRunning  <- readIORef (gangActionsRunning gang)-                if actionsRunning == 0-                 then   writeIORef (gangState gang) GangFinished-                 else do        -                        threadDelay 1000-                        gangLoop gang []--         GangFinished   -> return ()-         GangKilled     -> return ()---- we have an available worker-gangLoop_withWorker :: Gang -> IO () -> [IO ()] -> IO ()-gangLoop_withWorker gang action actionsRest- = do   -- See if we're supposed to be starting actions or not.-        state   <- readIORef (gangState gang)-        case state of-         GangRunning-          -> do -- fork off the first action-                tid <- forkOS $ do-                        -- run the action (and wait for it to complete)-                        action+          -> do -- Fork off the next action.+                -- We need to use the 'finally' to release the thread+                -- in the case that the action throws an exception.+                tid     <- forkOS +                        $  finally action+                        $  do   -- Remove our ThreadId from the set of+                                -- running ThreadIds.+                                tid     <- myThreadId+                                atomicModifyIORef' (gangThreadsRunning gang)+                                        (\tids -> (Set.delete tid tids, ())) -                        -- signal that a new worker is available-                        signalQSemN (gangThreadsAvailable gang) 1-                        -                        -- remove our ThreadId from the set of running ThreadIds.-                        tid     <- myThreadId-                        atomicModifyIORef (gangThreadsRunning gang)-                                (\tids -> (Set.delete tid tids, ()))+                                -- Signal that the worker is now available.+                                signalQSemN (gangThreadsAvailable gang) 1                          -- Add the ThreadId of the freshly forked thread to the set                 -- of running ThreadIds. We'll need this set if we want to kill                 -- the gang.-                atomicModifyIORef (gangThreadsRunning gang)+                atomicModifyIORef' (gangThreadsRunning gang)                         (\tids -> (Set.insert tid tids, ()))         -                -- handle the rest of the actions.+                -- Handle the rest of the actions.                 gangLoop gang actionsRest -         -- someone issued flush or pause command while we-         -- were waiting for a worker, so don't start next action.-         _ -> do-                signalQSemN (gangThreadsAvailable gang) 1-                gangLoop gang (action:actionsRest)+         -- TODO: avoid spinning on pause.+         GangPaused+          -> do signalQSemN (gangThreadsAvailable gang) 1+                threadDelay 1000+                gangLoop gang actions+                        +         GangFlushing   -> return ()+         GangFinished   -> return ()+         GangKilled     -> return ()++
buildbox.cabal view
@@ -1,5 +1,5 @@ Name:                buildbox-Version:             2.1.7.1+Version:             2.1.8.1 License:             BSD3 License-file:        LICENSE Author:              Ben Lippmeier@@ -29,7 +29,8 @@         mtl            >= 2.2 && < 2.3,         old-locale     >= 1.0 && < 1.1,         process        >= 1.2 && < 1.3,-        random         >= 1.0 && < 1.2,+        temporary      >= 1.2 && < 1.3,+        exceptions     >= 0.8 && < 0.9,         pretty         >= 1.1 && < 1.2,         stm            >= 2.4 && < 2.5,         text           >= 1.2 && < 1.3@@ -71,3 +72,6 @@    Extensions:         ExistentialQuantification+        ScopedTypeVariables+        TypeSynonymInstances+        BangPatterns