packages feed

evdev 1.1.0.1 → 1.2.0.0

raw patch · 3 files changed

+89/−9 lines, 3 filesdep +pathsdep +streamly-fsnotifyPVP ok

version bump matches the API change (PVP)

Dependencies added: paths, streamly-fsnotify

API changes (from Hackage documentation)

+ Evdev: Event :: EventType -> EventCode -> EventValue -> DiffTime -> Event
+ Evdev: [evCode] :: Event -> EventCode
+ Evdev: [evTime] :: Event -> DiffTime
+ Evdev: [evType] :: Event -> EventType
+ Evdev: [evValue] :: Event -> EventValue
+ Evdev.Stream: newDevices :: (IsStream t, Monad (t IO)) => t IO Device

Files

CHANGELOG.md view
@@ -15,3 +15,8 @@ ## 1.1.0.1 -- 2019-12-18  * Add `pkgconfig-depends` field to cabal file.++## 1.2.0.0 -- 2019-12-19++* Add `newDevices` stream.+* More robust error handling when reading from multiple devices.
evdev.cabal view
@@ -1,6 +1,6 @@ cabal-version:       2.2 name:                evdev-version:             1.1.0.1+version:             1.2.0.0 author:              George Thomas maintainer:          George Thomas description:         Provides access to the Linux event device interface, with an optional high-level Streamly-based API.@@ -37,6 +37,8 @@                        streamly             ^>= 0.6.1 || ^>= 0.7,                        safe                 ^>= 0.3.18,                        monad-loops          ^>= 0.4.3,+                       streamly-fsnotify    ^>= 1.0,+                       paths                ^>= 0.2,   build-tool-depends:  c2hs:c2hs   default-language:    Haskell2010   default-extensions:  FlexibleContexts
src/Evdev/Stream.hs view
@@ -2,15 +2,24 @@     allDevices,     allEvents,     makeDevices,+    newDevices,     readEvents,     readEventsMany,-    ) where+) where -import Control.Exception-import Control.Monad+import Data.Bool+import Data.Either.Extra+import Data.Functor+import System.IO import System.IO.Error +import Data.Set (Set)+import qualified Data.Set as Set+import qualified Data.ByteString.Char8 as BS import RawFilePath.Directory (doesFileExist,listDirectory)+import qualified Streamly.FSNotify as N+import Streamly.FSNotify (EventPredicate(EventPredicate),FSEntryType(NotDir),watchDirectory)+import System.Path (Absolute,Path,fromFilePath,toFilePath) import System.Posix.ByteString (RawFilePath) import System.Posix.FilePath ((</>)) @@ -25,10 +34,15 @@ readEvents dev = S.repeatM $ nextEvent dev defaultReadFlags  -- | Concurrently read events from multiple devices.+-- | If a read fails on one, the exception is printed to stderr and the stream continues to read from the others. readEventsMany :: IsStream t => AsyncT IO Device -> t IO (Device, Event) readEventsMany ds = asyncly $ do     d <- ds-    S.map (d,) $ serially $ readEvents d+    S.map (d,) $ serially $ readEvents' d+    where+        -- catch all IO errors+        readEvents' :: Device -> SerialT IO Event+        readEvents' dev = unfoldM $ printIOError' $ nextEvent dev defaultReadFlags  -- | Create devices for all paths in the stream. -- | Will throw an exception if a path doesn't correspond to a valid input device.@@ -39,9 +53,68 @@ allEvents :: IsStream t => t IO (Device, Event) allEvents = readEventsMany allDevices --- | All valid devices (in /dev/input).+-- | All valid existing devices (in /dev/input). allDevices :: (IsStream t, Monad (t IO)) => t IO Device allDevices =-    let maybeNewDevice path = catchJust (guard . isIllegalOperation) (Just <$> newDevice path) (\() -> return Nothing)-        paths = S.filterM doesFileExist $ S.map (evdevDir </>) $ S.fromFoldable =<< S.yieldM (listDirectory evdevDir)-    in  S.mapMaybeM maybeNewDevice paths+    let paths = S.filterM doesFileExist $ S.map (evdevDir </>) $ S.fromFoldable =<< S.yieldM (listDirectory evdevDir)+    in  S.mapMaybeM (printIOError' . newDevice) paths++-- | All new devices created (in /dev/input).+-- | Watches for new file paths (using inotify), and those corresponding to valid devices are added to the stream.+newDevices :: (IsStream t, Monad (t IO)) => t IO Device+newDevices =+    let -- 'watching' keeps track of the set of paths which have been added, but don't yet have the right permissions+        watch :: Set (Path Absolute) -> N.Event -> IO (Maybe Device, Set (Path Absolute))+        watch watching = \case+            N.Added p _ NotDir ->+                tryNewDevice p <&> \case+                    Right d -> -- success - return new device+                        (Just d, watching)+                    Left e -> -- fail - if it's only a permission error then watch for changes on device+                        (Nothing, applyWhen (isPermissionError e) (Set.insert p) watching)+            N.Modified p _ NotDir ->+                if p `elem` watching then+                    tryNewDevice p <&> \case+                        Right d -> -- success - no longer watch for changes+                            (Just d, Set.delete p watching)+                        Left _ -> -- fail - continue to watch+                            (Nothing, watching)+                else -- this isn't an event we care about+                    return (Nothing, watching)+            N.Removed p _ NotDir -> -- device is gone - no longer watch for changes+                return (Nothing, Set.delete p watching)+            _ -> return (Nothing, watching)+        tryNewDevice = printIOError . newDevice . BS.pack . toFilePath+    in do+        (_,es) <- S.yieldM $ watchDirectory (fromFilePath $ BS.unpack evdevDir) (EventPredicate $ const True)+        scanMaybe watch [] es+++{- Util -}++-- specialized form of S.scanlM'+-- for each a, f updates s, and possibly produces a new b, to add to the output stream+-- I really can't think of a good name for this...+-- TODO perhaps some way to use State monad instead?+scanMaybe :: (IsStream t, Monad m) => (s -> a -> m (Maybe b, s)) -> s -> t m a -> t m b+scanMaybe f e  = S.mapMaybe fst . S.scanlM' (f . snd) (Nothing, e) ++-- specialised form of S.unfoldrM+-- this should perhaps be in streamly (it's in monad-loops)+--TODO this is rather ugly - can it be done in terms of the Unfold type?+unfoldM :: (IsStream t, MonadAsync m) => m (Maybe a) -> t m a+unfoldM x = S.unfoldrM (const $ fmap (,undefined) <$> x) undefined++-- like tryIOError, but also prints the error to stderr+printIOError :: IO a -> IO (Either IOError a)+printIOError f = (Right <$> f) `catchIOError` \err -> do+    hPrint stderr err+    return $ Left err++-- variant of printIOError which doesn't care what the exception was+printIOError' :: IO a -> IO (Maybe a)+printIOError' = fmap eitherToMaybe . printIOError++-- apply the function iff the guard passes+applyWhen :: Bool -> (a -> a) -> a -> a+applyWhen = flip $ bool id