evdev-0.1.0.0: src/Evdev/Stream.hs
module Evdev.Stream where
import Control.Concurrent
import Control.Exception.Extra
import Control.Monad
import Control.Monad.IO.Class
import Data.Either.Combinators
import GHC.IO.Exception (IOErrorType(PermissionDenied),ioe_type)
import RawFilePath.Directory (doesFileExist,listDirectory)
import System.INotify (addWatch,initINotify)
import qualified System.INotify as I
import System.Posix.ByteString (RawFilePath)
import System.Posix.FilePath ((</>),combine)
import Streamly
import qualified Streamly.Prelude as S
import Evdev
allEvents :: IsStream t => t IO (Device, Event)
allEvents = filteredEvents $ const True
filteredEvents :: IsStream t => (Device -> Bool) -> t IO (Device, Event)
filteredEvents p = readEventsMany $ S.filter p $ makeDevices $ existingDevicePaths <> newDevicePaths
-- reads until encountering an IOException
readEvents :: IsStream t => Device -> t IO Event
readEvents dev = serially $ unfoldrM' $ fmap rightToMaybe $ tryIO $ nextEvent dev defaultReadFlags
readEventsMany :: IsStream t => AsyncT IO Device -> t IO (Device, Event)
readEventsMany = asyncly . join . (S.map $ \d -> (S.map (d,) $ readEvents d))
makeDevices :: (Functor (t IO), IsStream t) => t IO RawFilePath -> t IO Device
makeDevices = S.mapMaybeM maybeNewDevice
existingDevicePaths :: (IsStream t, MonadIO (t IO)) => t IO RawFilePath
existingDevicePaths = S.fromFoldable =<< S.yieldM (lsFiles evdevDir)
newDevicePaths :: (MonadIO (t IO), IsStream t) => t IO RawFilePath
newDevicePaths =
let watcher mvar = \case
I.Created False path -> do -- file (not directory) created
let fullPath = evdevDir </> path
handleBoolRetry ((== PermissionDenied) . ioe_type) 100 $ putMVar mvar fullPath
_ -> return ()
in do
mvar <- liftIO newEmptyMVar
iNot <- liftIO initINotify
_ <- liftIO $ addWatch iNot [I.Create] evdevDir (watcher mvar)
serially $ S.repeatM $ takeMVar mvar
-- retry the action after encountering an exception satisfying p
handleBoolRetry :: Exception e => (e -> Bool) -> Int -> IO a -> IO a
handleBoolRetry p t x = handleBool p (const $ threadDelay t >> handleBoolRetry p t x) x
{- Util -}
-- a specialization of S.unfoldrM which doesn't make use of any value from the previous round
unfoldrM' :: (IsStream t, MonadAsync m) => m (Maybe a) -> t m a
unfoldrM' x = S.unfoldrM (const $ fmap (,undefined) <$> x) undefined
-- lists files only, and returns full paths.
lsFiles :: RawFilePath -> IO [RawFilePath]
lsFiles = filterM doesFileExist <=< ((fmap . map . combine) <*> listDirectory)