packages feed

wlsunset-sni-0.1.0.0: src/Wlsunset/Process.hs

{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Wlsunset.Process
  ( WlsunsetMode (..)
  , WlsunsetState (..)
  , WlsunsetConfig (..)
  , Wlsunset
  , newWlsunset
  , getWlsunsetState
  , dupWlsunsetChan
  , cycleWlsunsetMode
  , cycleWlsunsetToMode
  , startWlsunset
  , stopWlsunset
  , toggleWlsunset
  , restartWlsunsetWithTemps
  )
where

import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Exception (SomeException, catch)
import Control.Monad (forever, replicateM_, void, when)
import Data.List (isPrefixOf)
import System.Posix.Signals (sigUSR1, signalProcess)
import System.Posix.Types (CPid (..))
import System.Process (readProcess, spawnCommand)
import Text.Read (readMaybe)

-- | The three operating modes that wlsunset cycles through when it receives
-- SIGUSR1.
data WlsunsetMode
  = WlsunsetAuto
  | WlsunsetForcedHighTemp
  | WlsunsetForcedLowTemp
  deriving (Eq, Show, Ord, Enum, Bounded)

-- | Observable state of the wlsunset process.
data WlsunsetState = WlsunsetState
  { wlsunsetRunning :: Bool
  , wlsunsetMode :: WlsunsetMode
  , wlsunsetEffectiveHighTemp :: Int
  , wlsunsetEffectiveLowTemp :: Int
  }
  deriving (Eq, Show)

-- | Configuration for monitoring and starting wlsunset.
data WlsunsetConfig = WlsunsetConfig
  { wlsunsetCommand :: String
  , wlsunsetHighTemp :: Int
  , wlsunsetLowTemp :: Int
  , wlsunsetPollIntervalSec :: Int
  }
  deriving (Eq, Show)

-- | A running wlsunset monitor.
data Wlsunset = Wlsunset
  { wsCfg :: WlsunsetConfig
  , wsStateVar :: MVar WlsunsetState
  , wsChan :: TChan WlsunsetState
  }

newWlsunset :: WlsunsetConfig -> IO Wlsunset
newWlsunset wsCfg@WlsunsetConfig {..} = do
  let initial =
        WlsunsetState
          { wlsunsetRunning = False
          , wlsunsetMode = WlsunsetAuto
          , wlsunsetEffectiveHighTemp = wlsunsetHighTemp
          , wlsunsetEffectiveLowTemp = wlsunsetLowTemp
          }
  wsStateVar <- newMVar initial
  wsChan <- newBroadcastTChanIO
  let ws = Wlsunset {..}
  void $ forkIO $ forever $ do
    pollWlsunset ws
    threadDelay (wlsunsetPollIntervalSec * 1000000)
  pure ws

getWlsunsetState :: Wlsunset -> IO WlsunsetState
getWlsunsetState = readMVar . wsStateVar

-- | Create a private read-end of the broadcast channel.
dupWlsunsetChan :: Wlsunset -> IO (TChan WlsunsetState)
dupWlsunsetChan = atomically . dupTChan . wsChan

broadcastUpdate :: Wlsunset -> (WlsunsetState -> WlsunsetState) -> IO ()
broadcastUpdate ws f =
  modifyMVar_ (wsStateVar ws) $ \old -> do
    let new = f old
    when (new /= old) $ atomically $ writeTChan (wsChan ws) new
    pure new

-- ---------------------------------------------------------------------------
-- Process helpers
-- ---------------------------------------------------------------------------

pgrepWlsunset :: IO [CPid]
pgrepWlsunset =
  (parsePids <$> readProcess "pgrep" ["-x", "wlsunset"] "")
    `catch` (\(_ :: SomeException) -> pure [])
  where
    parsePids = map (CPid . fromIntegral) . concatMap toList . lines
    toList s = maybe [] pure (readMaybe s :: Maybe Int)

sendUSR1 :: CPid -> IO ()
sendUSR1 = signalProcess sigUSR1

pollWlsunset :: Wlsunset -> IO ()
pollWlsunset ws = do
  pids <- pgrepWlsunset
  let isRunning = not (null pids)
  broadcastUpdate ws $ \old ->
    let wasRunning = wlsunsetRunning old
        newMode
          | not wasRunning && isRunning = WlsunsetAuto
          | not isRunning = WlsunsetAuto
          | otherwise = wlsunsetMode old
     in old {wlsunsetRunning = isRunning, wlsunsetMode = newMode}

-- ---------------------------------------------------------------------------
-- Actions
-- ---------------------------------------------------------------------------

cycleWlsunsetMode :: Wlsunset -> IO ()
cycleWlsunsetMode ws = do
  pids <- pgrepWlsunset
  case pids of
    [] -> pure ()
    _ -> do
      mapM_ sendUSR1 pids
      broadcastUpdate ws $ \old ->
        let newMode = case wlsunsetMode old of
              WlsunsetAuto -> WlsunsetForcedHighTemp
              WlsunsetForcedHighTemp -> WlsunsetForcedLowTemp
              WlsunsetForcedLowTemp -> WlsunsetAuto
         in old {wlsunsetMode = newMode, wlsunsetRunning = True}

cyclesToReach :: WlsunsetMode -> WlsunsetMode -> Int
cyclesToReach from to
  | from == to = 0
  | otherwise = (toOrd to - toOrd from) `mod` 3
  where
    toOrd WlsunsetAuto = 0
    toOrd WlsunsetForcedHighTemp = 1
    toOrd WlsunsetForcedLowTemp = 2

cycleWlsunsetToMode :: Wlsunset -> WlsunsetMode -> IO ()
cycleWlsunsetToMode ws target = do
  st <- getWlsunsetState ws
  replicateM_ (cyclesToReach (wlsunsetMode st) target) (cycleWlsunsetMode ws)

startWlsunset :: Wlsunset -> IO ()
startWlsunset ws = do
  void $ spawnCommand (wlsunsetCommand (wsCfg ws))
  broadcastUpdate ws $ \old -> old {wlsunsetRunning = True, wlsunsetMode = WlsunsetAuto}

stopWlsunset :: Wlsunset -> IO ()
stopWlsunset ws = do
  pids <- pgrepWlsunset
  mapM_ (signalProcess 15) pids
  broadcastUpdate ws $ \old -> old {wlsunsetRunning = False, wlsunsetMode = WlsunsetAuto}

toggleWlsunset :: Wlsunset -> IO ()
toggleWlsunset ws = do
  st <- getWlsunsetState ws
  if wlsunsetRunning st then stopWlsunset ws else startWlsunset ws

restartWlsunsetWithTemps :: Wlsunset -> Int -> Int -> IO ()
restartWlsunsetWithTemps ws lowTemp highTemp = do
  pids <- pgrepWlsunset
  mapM_ (signalProcess 15) pids
  let cmd = buildCommandWithTemps (wlsunsetCommand (wsCfg ws)) lowTemp highTemp
  void $ spawnCommand cmd
  broadcastUpdate ws $ \old ->
    old
      { wlsunsetRunning = True
      , wlsunsetMode = WlsunsetAuto
      , wlsunsetEffectiveHighTemp = highTemp
      , wlsunsetEffectiveLowTemp = lowTemp
      }

-- ---------------------------------------------------------------------------
-- Command building
-- ---------------------------------------------------------------------------

buildCommandWithTemps :: String -> Int -> Int -> String
buildCommandWithTemps baseCmd lowTemp highTemp =
  unwords (stripTempArgs (words baseCmd))
    ++ " -t "
    ++ show lowTemp
    ++ " -T "
    ++ show highTemp

stripTempArgs :: [String] -> [String]
stripTempArgs [] = []
stripTempArgs ("-t" : _ : rest) = stripTempArgs rest
stripTempArgs ("-T" : _ : rest) = stripTempArgs rest
stripTempArgs (x : rest)
  | "-t" `isPrefixOf` x && length x > 2 = stripTempArgs rest
  | "-T" `isPrefixOf` x && length x > 2 = stripTempArgs rest
  | otherwise = x : stripTempArgs rest