taffybar-4.1.2: src/System/Taffybar/Information/Systemd.hs
{-# LANGUAGE OverloadedStrings #-}
-----------------------------------------------------------------------------
-- |
-- Module : System.Taffybar.Information.Systemd
-- Copyright : (c) Ivan A. Malison
-- License : BSD3-style (see LICENSE)
--
-- Maintainer : Ivan A. Malison
-- Stability : unstable
-- Portability : unportable
--
-- This module provides information about systemd failed units using the
-- org.freedesktop.systemd1 DBus interface. It monitors both the system bus
-- (for system units) and the session bus (for user units).
-----------------------------------------------------------------------------
module System.Taffybar.Information.Systemd
( SystemdState(..)
, SystemdInfo(..)
, getSystemdInfo
, getSystemdInfoFromClients
, getSystemdInfoChan
, getSystemdInfoState
, systemdUnknownInfo
) where
import Control.Concurrent
import Control.Concurrent.STM.TChan
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.STM (atomically)
import Control.Monad.Trans.Class
import Control.Monad.Trans.Reader
import DBus
import DBus.Client
import qualified DBus.TH as DBus
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Word (Word32)
import System.Log.Logger
import System.Taffybar.Context
-- | The state of a systemd instance (system or user).
data SystemdState
= SystemdRunning -- ^ All units are running properly
| SystemdDegraded -- ^ Some units have failed
| SystemdStarting -- ^ System is starting up
| SystemdStopping -- ^ System is shutting down
| SystemdMaintenance -- ^ System is in maintenance mode
| SystemdInitializing -- ^ System is initializing
| SystemdOther Text -- ^ Unknown state
| SystemdUnavailable -- ^ systemd is not available on this bus
deriving (Eq, Show)
-- | Information about systemd failed units.
data SystemdInfo = SystemdInfo
{ systemState :: SystemdState -- ^ State of system systemd
, userState :: SystemdState -- ^ State of user systemd
, systemFailedCount :: Int -- ^ Number of failed system units
, userFailedCount :: Int -- ^ Number of failed user units
, totalFailedCount :: Int -- ^ Total failed units (system + user)
} deriving (Eq, Show)
-- | Default info when systemd is unknown/unavailable.
systemdUnknownInfo :: SystemdInfo
systemdUnknownInfo = SystemdInfo
{ systemState = SystemdUnavailable
, userState = SystemdUnavailable
, systemFailedCount = 0
, userFailedCount = 0
, totalFailedCount = 0
}
systemdLogPath :: String
systemdLogPath = "System.Taffybar.Information.Systemd"
-- | DBus constants for systemd.
systemdBusName :: BusName
systemdBusName = "org.freedesktop.systemd1"
systemdObjectPath :: ObjectPath
systemdObjectPath = "/org/freedesktop/systemd1"
systemdManagerInterface :: InterfaceName
systemdManagerInterface = "org.freedesktop.systemd1.Manager"
-- | Parse a SystemdState from a DBus string.
parseSystemdState :: Text -> SystemdState
parseSystemdState txt =
case T.toLower txt of
"running" -> SystemdRunning
"degraded" -> SystemdDegraded
"starting" -> SystemdStarting
"stopping" -> SystemdStopping
"maintenance" -> SystemdMaintenance
"initializing" -> SystemdInitializing
_ -> SystemdOther txt
-- | Newtype wrapper for the channel/var pair stored in Context.
newtype SystemdInfoChanVar = SystemdInfoChanVar (TChan SystemdInfo, MVar SystemdInfo)
-- | Get the current systemd info state.
getSystemdInfoState :: TaffyIO SystemdInfo
getSystemdInfoState = do
SystemdInfoChanVar (_, theVar) <- getSystemdInfoChanVar
lift $ readMVar theVar
-- | Get the broadcast channel for systemd info updates.
getSystemdInfoChan :: TaffyIO (TChan SystemdInfo)
getSystemdInfoChan = do
SystemdInfoChanVar (chan, _) <- getSystemdInfoChanVar
return chan
-- | Get or create the channel/var pair for systemd monitoring.
getSystemdInfoChanVar :: TaffyIO SystemdInfoChanVar
getSystemdInfoChanVar =
getStateDefault $ SystemdInfoChanVar <$> monitorSystemdInfo
-- | The debounce delay in microseconds (1 second, like Waybar).
debounceDelayMicros :: Int
debounceDelayMicros = 1_000_000
-- | Start monitoring systemd on both system and session buses.
monitorSystemdInfo :: TaffyIO (TChan SystemdInfo, MVar SystemdInfo)
monitorSystemdInfo = do
infoVar <- lift $ newMVar systemdUnknownInfo
chan <- liftIO newBroadcastTChanIO
debounceVar <- liftIO $ newMVar False
taffyFork $ do
ctx <- ask
systemClient <- asks systemDBusClient
sessionClient <- asks sessionDBusClient
let doUpdate = updateSystemdInfo chan infoVar systemClient sessionClient
debouncedUpdate = do
pending <- liftIO $ swapMVar debounceVar True
unless pending $ do
liftIO $ threadDelay debounceDelayMicros
liftIO $ void $ swapMVar debounceVar False
doUpdate
signalCallback _ _ _ _ = runReaderT debouncedUpdate ctx
-- Register for PropertiesChanged on system bus
_ <- lift $ DBus.registerForPropertiesChanged
systemClient
matchAny { matchInterface = Just systemdManagerInterface
, matchPath = Just systemdObjectPath
}
signalCallback
-- Register for PropertiesChanged on session bus
_ <- lift $ DBus.registerForPropertiesChanged
sessionClient
matchAny { matchInterface = Just systemdManagerInterface
, matchPath = Just systemdObjectPath
}
signalCallback
-- Initial update
doUpdate
return (chan, infoVar)
-- | Update the systemd info by querying both buses.
updateSystemdInfo
:: TChan SystemdInfo
-> MVar SystemdInfo
-> Client
-> Client
-> TaffyIO ()
updateSystemdInfo chan var systemClient sessionClient = do
info <- liftIO $ getSystemdInfoFromClients systemClient sessionClient
liftIO $ do
_ <- swapMVar var info
atomically $ writeTChan chan info
-- | Get systemd info using the clients from Context.
getSystemdInfo :: TaffyIO SystemdInfo
getSystemdInfo = do
systemClient <- asks systemDBusClient
sessionClient <- asks sessionDBusClient
liftIO $ getSystemdInfoFromClients systemClient sessionClient
-- | Get systemd info from the provided DBus clients.
getSystemdInfoFromClients :: Client -> Client -> IO SystemdInfo
getSystemdInfoFromClients systemClient sessionClient = do
(sysState, sysFailed) <- getSystemdStateAndFailed systemClient "system"
(usrState, usrFailed) <- getSystemdStateAndFailed sessionClient "user"
return SystemdInfo
{ systemState = sysState
, userState = usrState
, systemFailedCount = sysFailed
, userFailedCount = usrFailed
, totalFailedCount = sysFailed + usrFailed
}
-- | Query a single systemd instance for its state and failed count.
getSystemdStateAndFailed :: Client -> String -> IO (SystemdState, Int)
getSystemdStateAndFailed client busType = do
stateResult <- getSystemdProperty client "SystemState"
case stateResult of
Left err -> do
logM systemdLogPath DEBUG $
"Failed to get " ++ busType ++ " systemd state: " ++ show err
return (SystemdUnavailable, 0)
Right stateVariant -> do
let state = maybe (SystemdOther "unknown")
parseSystemdState
(fromVariant stateVariant :: Maybe Text)
-- Only query failed count if degraded
if state == SystemdDegraded
then do
failedResult <- getSystemdProperty client "NFailedUnits"
case failedResult of
Left err -> do
logM systemdLogPath WARNING $
"Failed to get " ++ busType ++ " failed units count: " ++ show err
return (state, 0)
Right failedVariant -> do
let failed = fromMaybe 0 (fromVariant failedVariant :: Maybe Word32)
return (state, fromIntegral failed)
else return (state, 0)
-- | Get a property from the systemd1.Manager interface.
getSystemdProperty :: Client -> MemberName -> IO (Either MethodError Variant)
getSystemdProperty client propName = do
reply <- call client $ (methodCall systemdObjectPath
"org.freedesktop.DBus.Properties"
"Get")
{ methodCallDestination = Just systemdBusName
, methodCallBody = [toVariant systemdManagerInterface, toVariant propName]
}
case reply of
Left err -> return $ Left err
Right ret ->
case methodReturnBody ret of
[v] -> case fromVariant v of
Just inner -> return $ Right inner
Nothing -> return $ Left $ methodError
(methodReturnSerial ret)
(errorName_ "org.taffybar.Systemd.TypeMismatch")
_ -> return $ Left $ methodError
(methodReturnSerial ret)
(errorName_ "org.taffybar.Systemd.InvalidResponse")