module Control.Concurrent.AlarmClock
( AlarmClock()
, newAlarmClock
, destroyAlarmClock
, setAlarm
, setAlarmNow
) where
import Control.Applicative
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Data.Time
import System.Timeout
data AlarmClock = AlarmClock (IO ()) (TVar AlarmSetting)
newAlarmClock
:: (AlarmClock -> IO ())
-> IO AlarmClock
newAlarmClock onWakeUp = do
joinVar <- atomically $ newTVar False
ac <- atomically $ AlarmClock (waitOn joinVar) <$> newTVar AlarmNotSet
void $ forkIO $ runAlarmClock ac (onWakeUp ac) `finally` atomically (writeTVar joinVar True)
return ac
waitOn :: TVar Bool -> IO ()
waitOn v = atomically $ readTVar v >>= \case True -> return (); False -> retry
destroyAlarmClock :: AlarmClock -> IO ()
destroyAlarmClock (AlarmClock j q) = atomically (writeTVar q AlarmDestroyed) >> j
setAlarm :: AlarmClock -> UTCTime -> IO ()
setAlarm (AlarmClock _ q) t = atomically $ modifyTVar' q $ \case
AlarmDestroyed -> AlarmDestroyed
AlarmNotSet -> AlarmSet t
AlarmSet t' -> AlarmSet $! min t t'
setAlarmNow :: AlarmClock -> IO ()
setAlarmNow alarm = getCurrentTime >>= setAlarm alarm
data AlarmSetting = AlarmNotSet | AlarmSet UTCTime | AlarmDestroyed
readNextAlarmSetting :: AlarmClock -> IO (Maybe UTCTime)
readNextAlarmSetting (AlarmClock _ q) = atomically $ readTVar q >>= \case
AlarmNotSet -> retry
AlarmDestroyed -> return Nothing
AlarmSet t -> writeTVar q AlarmNotSet >> return (Just t)
runAlarmClock :: AlarmClock -> IO () -> IO ()
runAlarmClock ac wakeUpAction = loop
where
loop = readNextAlarmSetting ac >>= go
go Nothing = return ()
go (Just wakeUpTime) = wakeNoLaterThan wakeUpTime
wakeNoLaterThan wakeUpTime = do
dt <- diffUTCTime wakeUpTime <$> getCurrentTime
if dt <= 0
then actAndContinue
else timeout (fromIntegral $ min maxDelay $ ceiling $ 1000000 * dt)
(readNextAlarmSetting ac)
>>= \case
Nothing -> do
t' <- getCurrentTime
if t' < wakeUpTime
then wakeNoLaterThan wakeUpTime
else actAndContinue
Just newSetting -> go newSetting
actAndContinue = wakeUpAction >> loop
maxDelay :: Integer
maxDelay = fromIntegral (maxBound :: Int)