{-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} module Main where import Control.Concurrent (threadDelay) import Control.Monad (forever, void) import Data.ByteString (ByteString) import Data.Foldable (for_) import Data.String (IsString (..)) import Foreign (allocaArray0) import Foreign.C (CInt (..), CSize (..), CString, peekCString, throwErrnoIfMinus1_) import Network.Wreq (post) import Options.Applicative import SystemD (connectSystem, monitorService, systemdSubscribe) import Text.Printf (printf) data Options = Options { endpoint :: String, services :: [String] } parseOptions :: IO Options parseOptions = execParser opts where opts = info (parser <**> helper) ( fullDesc <> progDesc "Send ntfy alerts when SystemD services change state" ) parser = Options <$> strOption ( short 'n' <> long "endpoint" <> help "The endpoint to send the notification to" <> metavar "URL" ) <*> many ( strOption ( short 's' <> long "service" <> help "Repeated. Units to monitor. Must include the suffix (.service)" <> metavar "SERVICE" ) ) main :: IO () main = do opts <- parseOptions hostname <- getHostName client <- connectSystem systemdSubscribe client for_ opts.services $ \serviceName -> do mHandler <- monitorService client serviceName $ \activeState -> do let msg :: ByteString = fromString $ printf "%s: Service %s state is %s" hostname serviceName activeState void $ post opts.endpoint msg case mHandler of Nothing -> printf "Could not monitor service %s\n" serviceName _ -> pure () forever (threadDelay 1_000_000) foreign import ccall "gethostname" c_gethostname :: CString -> CSize -> IO CInt getHostName :: IO String getHostName = do let size = 256 allocaArray0 size $ \cstr -> do throwErrnoIfMinus1_ "getHostName" $ c_gethostname cstr (fromIntegral size) peekCString cstr