0

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).

user1189687
  • 147
  • 5

1 Answers1

5

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.

Community
  • 1
  • 1
ehird
  • 40,602
  • 3
  • 180
  • 182
  • 2
    Also, 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