packages feed

minions-0.1.0.0: minions.hs

{-# LANGUAGE RecordWildCards #-}

module Main where

import System.Environment (getArgs)
import System.Exit (ExitCode(..), exitFailure, exitSuccess)
import System.Console.ANSI
import System.Console.GetOpt
import System.Process (readProcessWithExitCode)
import System.Timeout (timeout)
import System.IO (stderr, hPutStrLn)

import Control.Concurrent
import Control.Monad

import Data.Time.Clock
import Data.String.Utils
import Text.Printf


data Task = Task
    { taskHost    :: String
    , taskCmd :: [String]
    } deriving Show

data Result = Result
    { resHost    :: String
    , resPayload :: Either String String
    , resTime    :: NominalDiffTime
    } deriving Show

-- | time the given IO action (clock time) and return a tuple 
--   of the execution time and the result
timeIO :: IO a -> IO (NominalDiffTime, a)
timeIO ioa = do
    t1 <- getCurrentTime
    a <- ioa
    t2 <- getCurrentTime
    return (diffUTCTime t2 t1, a)

runTasks :: [Task] -> Int -> (Result -> IO a) -> IO ()
runTasks ts tmout handler = do
    out <- newChan
    forM_ ts $ run out
    replicateM_ (length ts) (readChan out >>= handler)
  where run ch t = forkIO $ do
                       res <- runTask t tmout
                       writeChan ch res

runTask :: Task -> Int -> IO Result
runTask Task{..} tmout = do
    (time, res) <- timeIO . timeout tmout $ readProcessWithExitCode "ssh" args [] 
    case res of
      Nothing -> output time $ Left "timed out\n"
      Just (code, stdout', stderr') -> 
        output time $ case code of
            ExitSuccess   -> Right stdout'
            ExitFailure _ -> Left stderr'
  where args = "-T" : taskHost : taskCmd 
        output time payload = return $ Result taskHost payload time

mkTasks :: [String] -> [String] -> [Task]
mkTasks hosts cmd = map f hosts
  where f h = Task h cmd


----------------
-- PRINTING
----------------

putColorLn :: ColorIntensity -> Color -> String -> IO ()
putColorLn intensity color str = do
    setSGR [SetColor Foreground intensity color]
    putStrLn str
    setSGR []

printResult :: Result -> IO ()
printResult Result{..} = do
    putColorLn Dull Blue (resHost ++ t)
    case resPayload of
      Left s  -> putColorLn Vivid Red s
      Right s -> putStrLn s
    where t = printf " (%.1fs)" (realToFrac resTime :: Double) 

printShortResult :: Result -> IO ()
printShortResult Result{..} =
    case resPayload of
      Left s  -> putColorLn Vivid Red $ resHost ++ t ++ rstrip s
      Right s -> putStrLn $ resHost ++ t ++ rstrip s
    where t = printf " (%.1fs): " (realToFrac resTime :: Double) 


----------------
-- ARGS
----------------

data Options = Options
    { oHelp    :: Bool
    , oHosts   :: FilePath
    , oTimeout :: Int
    , oHandler :: Result -> IO ()
    }

defaultOptions :: Options
defaultOptions = Options False "" (10 * 1000000) printResult

options :: [OptDescr (Options -> Options)]
options =
    [ Option [] ["help"]
      (NoArg (\opts -> opts { oHelp = True }))
      "display this help"
    , Option "h" ["hosts"]
      (ReqArg (\f opts -> opts { oHosts = f }) "FILE")
      "FILE containing ssh host names (one per line)"
    , Option "t" ["timeout"] 
      (ReqArg (\f opts -> opts { oTimeout = (read f :: Int) * 1000000 }) 
        "SECONDS")
      "ssh timeout in SECONDS (default 10)"
    , Option "s" ["short"]
      (NoArg (\opts -> opts { oHandler = printShortResult }))
      "display results in short format"
    ]

parseArgs :: IO (Options, [String])
parseArgs = do
    argv <- getArgs
    case getOpt RequireOrder options argv of
      (o, n, []) -> return (foldl (flip id) defaultOptions o, n)
      (_, _, es) -> showError $ concat es

showError :: String -> IO a
showError msg = hPutStrLn stderr (msg ++ header) >> exitFailure

header :: String
header = usageInfo "Usage: minions [-hst] command" options

main :: IO ()
main = do
    (Options{..}, cmd) <- parseArgs
    when oHelp (putStrLn header >> exitSuccess)
    when (oHosts == "") (showError "Please specify a hostname file with -h\n")
    when (null cmd) (showError "Please specify a command to run\n")
    hosts <- (filter ((not . null) . strip) . lines) `fmap` readFile oHosts
    runTasks (mkTasks hosts cmd) oTimeout oHandler