packages feed

hbro-1.2.0.0: library/Hbro/Options.hs

{-# LANGUAGE TemplateHaskell, ViewPatterns #-}
-- | Commandline options tools.
module Hbro.Options (
-- * Types
      CliOptions()
    , startURIL
    , socketPathL
    , dyreModeL
    , uiFileL
    , dyreDebugL
-- * Util
    , parseOptions
    , usage
    , getSocketURI
) where

-- {{{ Imports
import qualified Hbro.Dyre as Dyre
import Hbro.Error
import Hbro.Logger as Logger
import Hbro.Prelude

import Control.Lens.Getter
import Control.Lens.Lens
import Control.Lens.Setter
import Control.Lens.TH

import Data.Version

import Filesystem

import Network.URI.Monadic

import Paths_hbro

import System.Console.GetOpt
import System.Posix.Process
import qualified System.ZMQ4 as ZMQ (version)

import Text.Read hiding(get, lift)
-- }}}

-- {{{ Types
-- | Available commandline options (cf @hbro -h@).
data CliOptions = CliOptions
    { _startURI   :: Maybe URI
    , _socketPath :: Maybe FilePath
    , _uiFile     :: Maybe FilePath
    , _dyreMode   :: Dyre.Mode
    , _dyreDebug  :: Bool
    } deriving(Eq)

makeLensesWith ?? ''CliOptions $ lensRules
    & lensField .~ (\name -> Just (tail name ++ "L"))

instance Describable CliOptions where
    describe opts = unwords $ catMaybes
        [ ("URI=" ++) . tshow <$> (opts^.startURIL)
        , ("SOCKET=" ++) . fpToText  <$> (opts^.socketPathL)
        , ("UI_FILE=" ++) . fpToText <$> (opts^.uiFileL)
        , Just . ("DYRE_MODE=" ++) . tshow $ opts^.dyreModeL
        , Just "DYRE_DEBUG" <| opts^.dyreDebugL |> Nothing
        ]

instance Default CliOptions where
    def = CliOptions
        { _startURI   = Nothing
        , _socketPath = Nothing
        , _uiFile     = Nothing
        , _dyreMode   = def
        , _dyreDebug  = False
        }
-- }}}

-- {{{ Util
action :: (BaseIO m, MonadPlus m) => m () -> ArgDescr (a -> m b)
action f = NoArg . const $ f >> abort

flag :: (Monad m) => Lens' CliOptions a -> a -> ArgDescr (CliOptions -> m CliOptions)
flag attribute value = NoArg $ return . set attribute value

optional :: (Monad m, IsString s) => Lens' CliOptions (Maybe s) -> String -> ArgDescr (CliOptions -> m CliOptions)
optional attribute name = ReqArg (\x -> return . set attribute (Just $ fromString x)) name
-- }}}

description :: (BaseIO m, MonadPlus m) => [OptDescr (CliOptions -> m CliOptions)]
description =
-- Action
    [ Option ['h']     ["help"]               (action help)
        "Print this help."
    , Option ['r']     ["recompile"]          (action recompile)
        "Recompile configuration file and exit."
    , Option ['V']     ["version"]            (action printVersion)
        "Print version."
-- Log level
    , Option ['v']     ["verbose"]            (setLogLevel DEBUG)
        "Equivalent to -l DEBUG."
    , Option ['q']     ["quiet"]              (setLogLevel ERROR)
        "Equivalent to -l ERROR."
    , Option ['l']     ["log-level"]          (ReqArg setCustomLogLevel "VALUE")
        "Set log level. VALUE may be one of DEBUG|INFO|NOTICE|WARNING|ERROR|CRITICAL|ALERT|EMERGENCY."
-- Paths
    , Option ['u']     ["uri"]                (ReqArg setStartURI "URI")
        "URI to load at start-up."
    , Option ['S']     ["socket"]             (optional socketPathL "PATH")
        "Where to open IPC socket"
    , Option ['U']     ["ui"]                 (optional uiFileL "PATH")
        "Path to UI descriptor (XML file)"
-- Dynamic reconfiguration
    , Option ['1']     ["vanilla"]            (flag dyreModeL Dyre.Vanilla)
        "Do not read custom configuration file"
    , Option []        ["force-reconf"]       (flag dyreModeL Dyre.ForceReconfiguration)
        "Recompile configuration file before starting the program"
    , Option []        ["dyre-master-binary"] (ReqArg (const return) "PATH")
        "Internal flag used for dynamic reconfiguration."
    , Option []        ["deny-reconf"]        (flag dyreModeL Dyre.IgnoreReconfiguration)
        "Do not recompile configuration file even if it has changed"
    , Option []        ["dyre-debug"]         (NoArg return)
        "Use './cache/' as the cache directory and ./ as the configuration directory. Useful to debug the program."
    ]


help, printVersion, recompile :: (BaseIO m, MonadPlus m) => m ()
help         = putStrLn usage
recompile    = Dyre.recompile >>= mapM_ putStrLn
printVersion = do
    (a, b, c) <- io ZMQ.version
    putStrLn $ "hbro: v" ++ pack (showVersion version)
    putStrLn $ "0MQ library: v" ++ intercalate "." (map tshow [a, b, c])

setLogLevel :: (BaseIO m, MonadPlus m) => Priority -> ArgDescr (a -> m a)
setLogLevel priority = NoArg $ \options -> do
    Logger.initialize priority
    return options

setCustomLogLevel :: (BaseIO m) => String -> (a -> m a)
setCustomLogLevel level options = do
    maybe (errorM "hbro.options" message) Logger.initialize $ readMaybe level
    return options
  where message = "Invalid log level '" ++ pack level ++ "'."

-- | Parse URI passed in commandline, check whether it is a file path or an internet URI
-- and return the corresponding normalized URI (that is: prefixed with "file://" or "http://")
setStartURI :: (BaseIO m, MonadPlus m) => String -> (CliOptions -> m CliOptions)
setStartURI (pack -> uri) options = do
    fileURI    <- io . isFile $ fpFromText uri
    workingDir <- io getWorkingDirectory

    let fileURI' = ("file://" ++ fpToText workingDir ++ "/" ++ uri) <| fileURI |> uri
    parsedURI <- maybe abort return =<< logErrors (parseURIReference fileURI')

    return $ set startURIL (Just parsedURI) options

-- | Usage text (cf @hbro -h@)
usage :: Text
usage = pack $ usageInfo "Usage: hbro [OPTIONS]" (description :: [OptDescr (CliOptions -> IO CliOptions)])

-- | Get and parse commandline options
parseOptions :: (BaseIO m, MonadPlus m) => m CliOptions
parseOptions = do
    Logger.initialize INFO

    (opts, input', unknown', errors') <- io $ getOpt' Permute description . map unpack <$> getArgs
    let (input, unknown, errors) = (tailSafe $ (pack <$> input'), pack <$> unknown', pack <$> errors')

    unless (null errors)  $ errorM "hbro.options" (unlines errors) >> abort
    unless (null unknown) . infoM "hbro.options" $ "Unrecognized options: " ++ unwords unknown
    unless (null input) . infoM "hbro.options" $ "Ignored input: " ++ unlines input

    options <- foldl' (>>=) (return def) opts
    debugM "hbro.options" $ "Used options: " ++ describe options
    return options

-- | Return socket URI used by this instance
getSocketURI :: (BaseIO m) => CliOptions -> m Text
getSocketURI options = maybe getDefaultSocketURI (return . normalize) $ options^.socketPathL
  where
    normalize = ("ipc://" ++) . fpToText
    getDefaultSocketURI = do
      dir <- fpToText <$> (io $ getAppCacheDirectory "hbro")
      pid <- io getProcessID
      return $ "ipc://" ++ dir ++ "/hbro." ++ tshow pid