module Sos.Job ( Job(jobEvent, jobCommands) , JobResult(..) , ShellCommand , newJob , runJob , restartJob , unrestartJob , shouldRestartJob ) where import Sos.FileEvent import Sos.Utils import Control.Applicative import Control.Concurrent.STM import Control.Exception import Control.Monad import Data.List.NonEmpty (NonEmpty) import Data.Monoid import System.Exit import System.Process import Text.Printf import qualified Data.List.NonEmpty as NonEmpty type ShellCommand = String data JobResult = JobSuccess | JobFailure -- | A 'Job' is an interruptible list of shell commands to run. data Job = Job { jobEvent :: FileEvent -- ^ Event that triggered this job. , jobCommands :: NonEmpty ShellCommand -- ^ The list of shell commands to run. , jobRestart :: TMVar () -- ^ A TMVar that, when written to, indicates this job should be -- immediately canceled and restarted. } newJob :: FileEvent -> NonEmpty ShellCommand -> STM Job newJob event cmds = do tmvar <- newEmptyTMVar pure (Job event cmds tmvar) restartJob :: Job -> STM () restartJob job = void (tryPutTMVar (jobRestart job) ()) -- | Clear any previous restart "ping"s that were sent to this job while it was -- sitting in the job queue (not at the front). unrestartJob :: Job -> STM () unrestartJob = void . tryTakeTMVar . jobRestart -- | An STM action that returns when this job should be restarted, and retries -- otherwise. shouldRestartJob :: Job -> STM () shouldRestartJob = readTMVar . jobRestart -- | Run a Job's list of shell commands sequentially. If a command returns -- ExitFailure, or an exception is thrown, don't run the rest (but also don't -- propagate the exception). Return whether or not all commands completed -- successfully. runJob :: Job -> IO JobResult runJob (NonEmpty.toList . jobCommands -> cmds0) = go 1 cmds0 where go :: Int -> [ShellCommand] -> IO JobResult go _ [] = pure JobSuccess go n (cmd:cmds) = do putStrLn (magenta (printf "[%d/%d] " n (length cmds0)) <> cmd) let acquire :: IO ProcessHandle acquire = do (_, _, _, ph) <- createProcess (shell cmd) pure ph try (bracket acquire terminateProcess waitForProcess) >>= \case Left (ex :: SomeException) -> do -- We expect to get ThreadKilled exceptions when we get canceled and -- restarted. Any other exception would be bizarre; just print it and -- move on. case fromException ex of Just ThreadKilled -> case length cmds0 of -- If this was a one-command job, just print that it's been -- canceled. Otherwise, print a little graphic showing how much -- of the job was completed before being restarted 1 -> putStrLn (yellow ("Restarting job: " ++ cmd)) _ -> do let (xs, ys) = splitAt (n-1) cmds0 putStrLn (yellow "Restarting job:") mapM_ (putStrLn . yellow . printf "[✓] %s") xs mapM_ (putStrLn . yellow . printf "[ ] %s") ys _ -> putStrLn (red ("Exception: " ++ show ex)) pure JobFailure Right ExitSuccess -> do putStrLn (green "Success ✓") go (n+1) cmds Right (ExitFailure c) -> do putStrLn (red (printf "Failure ✗ (%d)" c)) pure JobFailure