├── .gitignore ├── LICENSE ├── README.md ├── Setup.hs ├── logody.cabal ├── logody_logo.png ├── package.yaml ├── src └── Main.hs ├── stack.yaml ├── stack.yaml.lock └── test ├── processes.yaml └── test.bash /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Author name here (c) 2018 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Author name here nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # logody 2 | 3 | Just like docker compose but with normal programs. 4 | 5 | ![](https://github.com/sordina/logody/raw/master/logody_logo.png) 6 | 7 | Aggregates process output harmoniously. 8 | 9 | Demo: 10 | 11 | ## Disclaimer 12 | 13 | This was implemented on a Friday afternoon and there may have been beer involved. 14 | 15 | Yes, "logdog" is a much better name. [Unfortunately it's already taken.](https://getlogdog.com/) 16 | 17 | Before you send an issue about your program not listing output, check the buffering settings. 18 | Some usual suspects include `grep --line-buffered`, `sed -l`. Remember that GNU and BSD 19 | arguments are often different! 20 | 21 | ## Features 22 | 23 | * Run multiple processes in parallel 24 | * Safely interleve logs 25 | * Indicate process name on log-line 26 | * Indicate handle on log-line (stdout, stderr) 27 | * Indicate exit-codes on log-line after termination 28 | * Aligns process-names and outputs as columns 29 | * Supply shell strings 30 | * Supply executables and lists of arguments 31 | * Configure processes to restart on success 32 | * Configure processes to restart on failure 33 | * Specify subset of configuration processes via arguments 34 | * Convenient arguments-only mode for simple concurrent processes 35 | * YAML configuration file format (feature... or bug?) 36 | * Optionally strips non-printable characters from output 37 | * Kill all processes easily with Ctrl-C 38 | 39 | ## Installing 40 | 41 | This is a Haskell codebase. 42 | 43 | [You can install logody with stack.](https://docs.haskellstack.org/en/stable/README/) 44 | 45 | ## Operation 46 | 47 | Two modes of operation: 48 | 49 | * Shell Arguments 50 | * Configuration 51 | 52 | ### Shell Arguments 53 | 54 | If you don't supply any config input, then the arguments are interpreted as shell strings. 55 | 56 | These will be named "process_N" sequentially and run until completion or failure. 57 | 58 | ### Usage 59 | 60 | Usage: logody [OPTIONS] [SHELL]* < CONFIG_FILE 61 | 62 | echo -n | logody SHELL* 63 | or... 64 | logody [NAME]* < CONFIG_FILE 65 | 66 | Options: 67 | 68 | -h | --help Print help and usage information. 69 | -v | --version Print version information. 70 | -n | --no-config Don't read any configuration, just accept shell string arguments. 71 | -f | --file Specify config file. '-' for STDIN. 72 | 73 | ### Configuration 74 | 75 | If you supply configuration input, then your process list will be defined in the config file format. 76 | 77 | Any arguments provided will be used to filter the process list. 78 | 79 | #### Config Example 80 | 81 | ``` 82 | --- 83 | osname: 84 | process: uname 85 | args: 86 | - "-a" 87 | 88 | echo: 89 | shell: "echo bar && sleep 1 && exit 1" 90 | resume: 91 | - fail 92 | 93 | testscript: 94 | process: ./test/test.bash 95 | sanitise: False 96 | resume: 97 | - succeed 98 | - fail 99 | ``` 100 | 101 | Output: 102 | 103 | ``` 104 | $ logody < test/processes.yaml 105 | testscript | Starting Process {"name":"testscript","resumption":{"succeed":true,"failure":true},"runner":{"tag":"Program","contents":["./test/test.bash",[]]}} 106 | osname | Starting Process {"name":"osname","resumption":{"succeed":false,"failure":false},"runner":{"tag":"Program","contents":["uname",["-a"]]}} 107 | echo | Starting Process {"name":"echo","resumption":{"succeed":false,"failure":true},"runner":{"tag":"Shell","contents":"echo bar && sleep 1 && exit 1"}} 108 | osname | stdout -> Darwin host.local ... 109 | echo | stdout -> bar 110 | osname | Exited Successfully 111 | testscript | stdout -> test.bash 1 112 | testscript | stdout -> test.bash 2 113 | testscript | stdout -> test.bash 3 114 | testscript | stdout -> test.bash 4 115 | echo | Failure -> Failed with code 1 116 | echo | Restarting process after failure with exit code 1 117 | echo | stdout -> bar 118 | testscript | stdout -> test.bash 5 119 | testscript | stdout -> test.bash 6 120 | testscript | stdout -> test.bash 7 121 | echo | Failure -> Failed with code 1 122 | echo | Restarting process after failure with exit code 1 123 | echo | stdout -> bar 124 | testscript | stdout -> test.bash 8 125 | testscript | stdout -> test.bash 9 126 | testscript | stdout -> test.bash 10 127 | echo | Failure -> Failed with code 1 128 | echo | Restarting process after failure with exit code 1 129 | echo | stdout -> bar 130 | testscript | Exited Successfully 131 | testscript | Restarting process after success 132 | testscript | stdout -> test.bash 1 133 | ^C 134 | ``` 135 | 136 | 137 | ## Bugs 138 | 139 | * No known bugs! 140 | 141 | 142 | ## TODO 143 | 144 | * Support other config formats: JSON, Dhall 145 | * Color Lines 146 | * Implement all of the applicable Docker-Compose features 147 | -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /logody.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 1.12 2 | 3 | -- This file has been generated from package.yaml by hpack version 0.31.1. 4 | -- 5 | -- see: https://github.com/sol/hpack 6 | -- 7 | -- hash: e9b343513f16a29e5d5faa6e6ec861a22c39cf1d55a21c8dc9b058ce7f16d3ac 8 | 9 | name: logody 10 | version: 0.1.0.0 11 | category: Web 12 | homepage: https://github.com/githubuser/logody#readme 13 | author: Author name here 14 | maintainer: example@example.com 15 | copyright: 2018 Author name here 16 | license: BSD3 17 | license-file: LICENSE 18 | build-type: Simple 19 | extra-source-files: 20 | README.md 21 | 22 | executable logody 23 | main-is: Main.hs 24 | hs-source-dirs: 25 | src 26 | ghc-options: -threaded -rtsopts -with-rtsopts=-N 27 | build-depends: 28 | aeson 29 | , async 30 | , base >=4.7 && <5 31 | , bytestring 32 | , containers 33 | , directory 34 | , process 35 | , yaml 36 | other-modules: 37 | Paths_logody 38 | default-language: Haskell2010 39 | -------------------------------------------------------------------------------- /logody_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sordina/logody/07e793cbe1a5170e7235ff3ca38cd52a275d9c72/logody_logo.png -------------------------------------------------------------------------------- /package.yaml: -------------------------------------------------------------------------------- 1 | name: logody 2 | version: 0.1.0.0 3 | #synopsis: 4 | #description: 5 | homepage: https://github.com/githubuser/logody#readme 6 | license: BSD3 7 | author: Author name here 8 | maintainer: example@example.com 9 | copyright: 2018 Author name here 10 | category: Web 11 | extra-source-files: 12 | - README.md 13 | 14 | dependencies: 15 | - base >= 4.7 && < 5 16 | - yaml 17 | - aeson 18 | - async 19 | - containers 20 | - process 21 | - bytestring 22 | - directory 23 | 24 | executables: 25 | logody: 26 | source-dirs: src 27 | main: Main.hs 28 | ghc-options: 29 | - -threaded 30 | - -rtsopts 31 | - -with-rtsopts=-N 32 | 33 | -------------------------------------------------------------------------------- /src/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DeriveGeneric #-} 2 | {-# LANGUAGE ScopedTypeVariables #-} 3 | 4 | module Main where 5 | 6 | import Prelude hiding (log) 7 | 8 | import Data.Yaml (ToJSON, FromJSON, ParseException(AesonException), decodeEither') 9 | import GHC.Generics 10 | import Control.Monad 11 | import Data.Maybe 12 | import Data.Aeson (encode) 13 | import Data.ByteString.Lazy.Char8 (unpack) 14 | import Data.ByteString.Char8 (pack) 15 | import Data.Either 16 | import Data.Char 17 | import System.IO 18 | import System.Directory 19 | import System.Exit 20 | import System.Environment 21 | import Data.Map (Map, toAscList) 22 | import Control.Concurrent.Async 23 | import qualified System.Process as P 24 | import qualified Control.Concurrent.Chan as C 25 | 26 | -- Types and Data 27 | 28 | data ProcessConfigItem = PCI 29 | { process :: Maybe String 30 | , shell :: Maybe String 31 | , sanitise :: Maybe Bool 32 | , resume :: Maybe [ String ] 33 | , args :: Maybe [ String ] 34 | } 35 | deriving (Show, Generic) 36 | 37 | data Process = P 38 | { name :: String 39 | , runner :: Runner 40 | , sane :: Bool 41 | , resumption :: Resume 42 | } 43 | deriving (Show, Generic) 44 | 45 | data Runner 46 | = Program String [String] 47 | | Shell String 48 | deriving (Eq, Show, Generic) 49 | 50 | data Resume = Resume 51 | { succeed :: Bool 52 | , failure :: Bool 53 | } 54 | deriving (Show, Generic) 55 | 56 | type ChanM a = C.Chan (Maybe a) 57 | 58 | type Logger = Process -> String -> IO () 59 | 60 | -- Instances 61 | 62 | instance FromJSON ProcessConfigItem 63 | instance ToJSON Process 64 | instance ToJSON Runner 65 | instance ToJSON Resume 66 | 67 | -- Main 68 | 69 | main :: IO () 70 | main = getArgs >>= processArguments 71 | 72 | processArguments :: [String] -> IO () 73 | processArguments ("-h":_) = help 74 | processArguments ("--help":_) = help 75 | processArguments ("-v":_) = version 76 | processArguments ("--version":_) = version 77 | processArguments ("-n": ps) = processConf ps "" 78 | processArguments ("--no-config": ps) = processConf ps "" 79 | processArguments ("-f" : "-": ps) = file "STDIN" >> getContents >>= processConf ps 80 | processArguments ("--file" : "-": ps) = file "STDIN" >> getContents >>= processConf ps 81 | processArguments ("-f" : f : ps) = file f >> readFile f >>= processConf ps 82 | processArguments ("--file" : f : ps) = file f >> readFile f >>= processConf ps 83 | processArguments ps = do 84 | let conf = "logody.yaml" 85 | fe <- doesFileExist conf 86 | if fe 87 | then file conf >> readFile conf >>= processConf ps 88 | else file "STDIN" >> getContents >>= processConf ps 89 | 90 | file :: String -> IO () 91 | file s = hPutStrLn stderr $ "Reading Configuration file " ++ s ++ "..." 92 | 93 | processConf :: [String] -> String -> IO () 94 | processConf ps conf 95 | = decodeProcesses conf ps 96 | >>= either print sallyForth 97 | 98 | test :: IO () 99 | test = readFile "./test/processes.yaml" >>= processConf [] 100 | 101 | version :: IO () 102 | version = putStrLn "0.2" 103 | 104 | help :: IO () 105 | help = do 106 | putStrLn "Usage: logody [SHELL]* < CONFIG_FILE" 107 | putStrLn "" 108 | putStrLn " echo -n | logody SHELL*" 109 | putStrLn " or..." 110 | putStrLn " logody [OPTIONS] [NAME]* < CONFIG_FILE" 111 | putStrLn "" 112 | putStrLn "Options:" 113 | putStrLn "" 114 | putStrLn " -h | --help Print help and usage information." 115 | putStrLn " -v | --version Print version information." 116 | putStrLn " -n | --no-config Don't read any configuration, just accept shell string arguments." 117 | putStrLn " -f | --file Specify config file. '-' for STDIN." 118 | putStrLn "" 119 | putStrLn "WARNING: logody will attempt to read logody.yaml" 120 | putStrLn " failing that it will read from STDIN." 121 | putStrLn " echo an empty string as input to skip configuration." 122 | putStrLn "" 123 | putStrLn "Config Format Example:" 124 | putStrLn "" 125 | putStrLn " ---" 126 | putStrLn " pluckProcesses:" 127 | putStrLn " process: uname" 128 | putStrLn " args:" 129 | putStrLn " - \"-a\"" 130 | putStrLn " " 131 | putStrLn " bar_:" 132 | putStrLn " shell: \"echo bar && sleep 1 && exit 1\"" 133 | putStrLn " resume:" 134 | putStrLn " - fail" 135 | putStrLn " " 136 | putStrLn " baz__:" 137 | putStrLn " process: ./test/test.bash" 138 | putStrLn " sanitise: false" 139 | putStrLn " resume:" 140 | putStrLn " - succeed" 141 | putStrLn " - fail" 142 | 143 | -- Config File Parsing 144 | 145 | decodeProcessConfig :: String -> Either ParseException (Map String ProcessConfigItem) 146 | decodeProcessConfig = decodeEither' . pack 147 | 148 | makeShell :: Int -> String -> Process 149 | makeShell i p = P ("process_" ++ show i) (Shell p) (True) (Resume False False) 150 | 151 | decodeProcesses :: String -> [String] -> IO ( Either [ParseException] [Process]) 152 | decodeProcesses conf ps = do 153 | case (ps, conf) 154 | of ([], "") -> return $ crash "pass a config into STDIN, or specify shell arguments" 155 | (_, "") -> return $ Right $ zipWith makeShell [0..] ps 156 | ([], _) -> do 157 | let pc = mapLeft (:[]) $ decodeProcessConfig conf 158 | return (pc >>= catEithers . map makeProcess . toAscList) 159 | (_, _) -> do 160 | let pc = mapLeft (:[]) $ decodeProcessConfig conf 161 | return (pc >>= pluckProcesses ps . catEithers . map makeProcess . toAscList) 162 | 163 | pluckProcesses :: [String] -> Either a [Process] -> Either a [Process] 164 | pluckProcesses ps = fmap (filter (flip elem ps . name)) 165 | 166 | mapLeft :: (a -> c) -> Either a b -> Either c b 167 | mapLeft f (Left a) = Left (f a) 168 | mapLeft _f (Right b) = Right b 169 | 170 | -- Process Construction 171 | 172 | makeProcess :: (String, ProcessConfigItem) -> Either [ParseException] Process 173 | makeProcess (_, PCI Nothing Nothing _ _ _) = crash "specify process or a shell" 174 | makeProcess (_, PCI (Just _) (Just _) _ _ _) = crash "specify EITHER a process or a shell" 175 | makeProcess (_, PCI Nothing (Just _) _ _ (Just (_:_))) = crash "shell commands do NOT take arguments" 176 | makeProcess (n, PCI (Just p) Nothing c r Nothing) = P n (Program p []) (sanity c) <$> (makeResume r) 177 | makeProcess (n, PCI (Just p) Nothing c r (Just a)) = P n (Program p a) (sanity c) <$> (makeResume r) 178 | makeProcess (n, PCI Nothing (Just s) c r Nothing) = P n (Shell s) (sanity c) <$> (makeResume r) 179 | makeProcess (n, PCI Nothing (Just s) c r (Just [])) = P n (Shell s) (sanity c) <$> (makeResume r) 180 | 181 | sanity :: Maybe Bool -> Bool 182 | sanity = fromMaybe True 183 | 184 | crash :: String -> Either [ParseException] b 185 | crash s = Left [ AesonException s ] 186 | 187 | makeResume :: Maybe [String] -> Either [ParseException] Resume 188 | makeResume Nothing = return $ Resume False False 189 | makeResume (Just []) = return $ Resume False False 190 | makeResume (Just ("succeed": xs)) = setSucceed <$> makeResume (Just xs) 191 | makeResume (Just ("fail" : xs)) = setFail <$> makeResume (Just xs) 192 | makeResume _ = crash "Resumption must only be one of succeed or fail" 193 | 194 | setSucceed, setFail :: Resume -> Resume 195 | setSucceed r = r { succeed = True } 196 | setFail r = r { failure = True } 197 | 198 | catEithers :: Monoid a => [Either a b] -> Either a [b] 199 | catEithers l = 200 | case partitionEithers l 201 | of ([], xs) -> Right xs 202 | (es, _ ) -> Left (mconcat es) 203 | 204 | -- Logging 205 | 206 | newLogChan :: IO (ChanM String) 207 | newLogChan = C.newChan 208 | 209 | getChanContents :: ChanM a -> IO [a] 210 | getChanContents c = do 211 | cs <- C.getChanContents c 212 | return $ catMaybes $ takeWhile isJust cs 213 | 214 | writeChan :: ChanM a -> a -> IO () 215 | writeChan c a = C.writeChan c (Just a) 216 | 217 | printLogs :: ChanM String -> IO () 218 | printLogs logs = getChanContents logs >>= mapM_ putStrLn 219 | 220 | closeChan :: ChanM a -> IO () 221 | closeChan c = C.writeChan c Nothing 222 | 223 | makeLogger :: Int -> ChanM String -> Process -> String -> IO () 224 | makeLogger width logs p s = writeChan logs line 225 | where 226 | line = name p ++ padding ++ " | " ++ if sane p then (filter isPrint $ noEscape s) else s 227 | padding = replicate (width - length (name p)) ' ' 228 | 229 | -- STUPID - Should probably use a scheme rather than blocking yellow... 230 | -- [3J01913514 231 | noEscape ('\ESC' : '[' : 'H' : xs) = noEscape xs 232 | noEscape ('\ESC' : '[' : '2' : 'J' : xs) = noEscape xs 233 | noEscape ('\ESC' : '[' : '3' : 'J' : xs) = noEscape xs 234 | noEscape ('\ESC' : '[' : '3' : '3' : 'm' : xs) = noEscape xs 235 | noEscape ('\ESC' : '[' : 'm' : xs) = noEscape xs 236 | noEscape ('\ESC' : xs) = noEscape xs 237 | noEscape (x : xs) = x : noEscape xs 238 | noEscape [] = [] 239 | 240 | -- Process Inception and Running 241 | 242 | sallyForth :: [Process] -> IO () 243 | sallyForth ps = do 244 | logs <- newLogChan 245 | let 246 | logger :: Process -> String -> IO () 247 | logger = makeLogger (maximum (map (length . name) ps)) logs 248 | 249 | withAsync (mapConcurrently_ (embark logger) ps) $ \a1 -> do 250 | withAsync (printLogs logs) $ \a2 -> do 251 | wait a1 252 | closeChan logs 253 | wait a2 254 | 255 | embark :: Logger -> Process -> IO () 256 | embark log p = 257 | case runner p 258 | of Shell s -> log p ("Starting Process " ++ unpack (encode p)) >> startShell log p s 259 | Program s as -> log p ("Starting Process " ++ unpack (encode p)) >> startProgram as log p s 260 | 261 | startShell :: Logger -> Process -> String -> IO () 262 | startShell log p s 263 | -- Use withCreateProcess in order to handle exceptions to theads cleanly 264 | = withCreateProcess (P.shell s) (\xi xo xe xp -> manage log p (xi, xo, xe, xp)) 265 | >>= cleanup startShell log p s 266 | 267 | startProgram :: [String] -> Logger -> Process -> String -> IO () 268 | startProgram as log p s 269 | = withCreateProcess (P.proc s as) (\xi xo xe xp -> manage log p (xi, xo, xe, xp)) 270 | >>= cleanup (startProgram as) log p s 271 | 272 | manage :: Logger -> Process 273 | -> (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) 274 | -> IO (Maybe ExitCode) 275 | manage log p (_stdin, Just stdout_h, Just stderr_h, pid_h) = do 276 | 277 | hSetBuffering stdout_h LineBuffering 278 | hSetBuffering stderr_h LineBuffering 279 | 280 | stdout_reader <- async $ untilM (hIsEOF stdout_h) $ do 281 | l <- hGetLine stdout_h 282 | log p ("stdout -> " ++ l) 283 | 284 | stderr_reader <- async $ untilM (hIsEOF stderr_h) $ do 285 | l <- hGetLine stderr_h 286 | log p ("stderr -> " ++ l) 287 | 288 | wait stdout_reader 289 | wait stderr_reader 290 | 291 | Just <$> P.waitForProcess pid_h 292 | 293 | manage log p _ = do 294 | log p "Failure -> Couldn't get handles for process" 295 | return Nothing 296 | 297 | cleanup :: (Logger -> Process -> t -> IO ()) 298 | -> Logger 299 | -> Process -> t -> Maybe ExitCode 300 | -> IO () 301 | cleanup _ _ _ _ Nothing = return () 302 | cleanup f log p s (Just code) = case code 303 | of ExitSuccess -> do 304 | log p "Exited Successfully" 305 | when (succeed $ resumption $ p) $ do 306 | log p "Restarting process after success" 307 | (f log p s) 308 | 309 | ExitFailure c -> do 310 | log p ("Failure -> Failed with code " ++ show c) 311 | when (failure $ resumption $ p) $ do 312 | log p ("Restarting process after failure with exit code " ++ show c) 313 | f log p s 314 | 315 | untilM :: Monad m => m Bool -> m () -> m () 316 | untilM c m = do 317 | b <- c 318 | when (not b) (m >> untilM c m) 319 | 320 | createProcess :: P.CreateProcess -> IO (Maybe Handle, Maybe Handle, Maybe Handle, P.ProcessHandle) 321 | createProcess p = 322 | P.createProcess 323 | p { P.std_out = P.CreatePipe 324 | , P.std_err = P.CreatePipe 325 | } 326 | 327 | withCreateProcess :: P.CreateProcess -> (Maybe Handle -> Maybe Handle -> Maybe Handle -> P.ProcessHandle -> IO a) -> IO a 328 | withCreateProcess p = 329 | P.withCreateProcess 330 | p { P.std_out = P.CreatePipe 331 | , P.std_err = P.CreatePipe 332 | } 333 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-11.4 2 | 3 | packages: 4 | - . 5 | 6 | extra-deps: 7 | - concurrent-machines-0.3.1.3 8 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: 7 | - completed: 8 | hackage: concurrent-machines-0.3.1.3@sha256:e8f8c2cf5a1ce374800c8c84a650114487d5caf58518b95672086979f4764424,3882 9 | pantry-tree: 10 | size: 1055 11 | sha256: 53632806cdfaf33fdf55913a59c68a1c88ba1755a50f2ce725af3ecca42d6b10 12 | original: 13 | hackage: concurrent-machines-0.3.1.3 14 | snapshots: 15 | - completed: 16 | size: 507107 17 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/11/4.yaml 18 | sha256: 30c1ea48ccc07e484c1acf166f0be3afc990e208823751ba85f23ab082df4294 19 | original: lts-11.4 20 | -------------------------------------------------------------------------------- /test/processes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | osname: 3 | process: uname 4 | args: 5 | - "-a" 6 | 7 | echo: 8 | shell: "echo bar && sleep 1 && exit 1" 9 | resume: 10 | - fail 11 | 12 | testscript: 13 | process: ./test/test.bash 14 | resume: 15 | - succeed 16 | - fail 17 | -------------------------------------------------------------------------------- /test/test.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for i in `seq 1 10` 4 | do 5 | echo test.bash $i 6 | sleep 0.321 7 | done 8 | --------------------------------------------------------------------------------