How does one block until the earlier of (1) a keypress or (2) a previously input time of day in hh:mm
format is reached. I am using Windows in case that matters. This DOS assembler program (which does run on Windows too) does what I want via something like batchman waittil 16:30
from the Windows console but I want to do it entirely in Haskell, (i.e. without making use of that program).

- 147
- 5
1 Answers
You can start two threads: one reads a character, the other waits until the specified time is reached; they both write to a single MVar
to signal completion.
This is a little tricky, but mostly due to the details: we want to have stdin
in unbuffered and non-echoing mode so that a single keypress stops the waiting without printing anything, and then restore the original state afterwards; and we also need to kill both threads after either finishes, so that we, for example, stop reading from stdin
once the timeout expires. Additionally, we need to ensure things are cleaned up properly if an exception occurs. bracket
simplifies the clean-up logic here, but it's still pretty ugly:
import Prelude hiding (catch)
import Control.Exception
import Control.Concurrent
import System.IO
withRawStdin :: IO a -> IO a
withRawStdin = bracket uncook restore . const
where
uncook = do
oldBuffering <- hGetBuffering stdin
oldEcho <- hGetEcho stdin
hSetBuffering stdin NoBuffering
hSetEcho stdin False
return (oldBuffering, oldEcho)
restore (oldBuffering, oldEcho) = do
hSetBuffering stdin oldBuffering
hSetEcho stdin oldEcho
waitFor :: Int -> IO ()
waitFor delay = do
done <- newEmptyMVar
withRawStdin . bracket (start done) cleanUp $ \_ -> takeMVar done
where
start done = do
t1 <- forkIO $ getChar >> putMVar done ()
t2 <- forkIO $ threadDelay delay >> putMVar done ()
return (t1, t2)
cleanUp (t1, t2) = do
killThread t1
killThread t2
Even after all that, this solution still doesn't handle waiting until a specific time — just waiting a certain number of microseconds. For turning a time of day into a number of microseconds to sleep, this previous SO question may help. If the sleeps are sufficiently long, then they might not fit into an Int
of microseconds, so you might have to use threadDelay
in a loop, or delay
from the unbounded-delays package.
-
2Also, watch out for overflows on 32-bit systems if you want to sleep for extended periods of time. Since `threadDelay` takes an `Int` specifying microseconds, the maximum time you can specify with a 32-bit `Int` is about 2147 seconds (35 minutes). You may need to split it up into several `threadDelay` calls in a loop. – hammar Feb 04 '12 at 18:21
-
There's definitely a library on Hackage that includes a function of type `Integer -> IO ()` that does this loop, but I can't find it now... – Daniel Wagner Feb 04 '12 at 18:50
-
1@DanielWagner: It's [unbounded-delays](http://hackage.haskell.org/package/unbounded-delays). (Thanks, Hayoo!) – ehird Feb 04 '12 at 18:55
-
I wouldn't worry about (2); `threadDelay` is guaranteed not to return before the specified time has elapsed, and it's unlikely to sleep much longer than that (less than a second more under reasonable load, I would think). The complexity mostly arises from handling things correctly in the case of exceptions. (By the way, you seem to have asked this question with a new account; you might want to [merge them](http://meta.stackexchange.com/questions/18232/how-can-one-link-merge-combine-associate-two-accounts-users-anonymous-unregist).) – ehird Feb 04 '12 at 19:55
-
@ehird, Am revising this comment so its now slightly out of order. Thanks for interesting answer. (1) The time of day can result in a timeout of up to 24 hours, (2) I was concerned that calculating seconds to time of day was less accurate than what the DOS program does which is to repeatedly compare desired and current time of day. (3) was concerned about complexity which you mention although maybe its inherent in the problem. – user1189687 Feb 04 '12 at 20:30