packages feed

systemd-ntfy-0.1.0: app/Main.hs

{-# 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