{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE TypeFamilies #-}
module System.GPIO.Linux.Sysfs.Mock
(
MockWorld
, MockPinState(..)
, defaultMockPinState
, logicalValue
, setLogicalValue
, MockGpioChip(..)
, MockPins
, mockWorldPins
, initialMockWorld
, SysfsMockT(..)
, runSysfsMockT
, evalSysfsMockT
, execSysfsMockT
, SysfsGpioMock
, runSysfsGpioMock
, evalSysfsGpioMock
, execSysfsGpioMock
, SysfsGpioMockIO
, runSysfsGpioMockIO
, evalSysfsGpioMockIO
, execSysfsGpioMockIO
, MockFSException(..)
, SysfsMock
, runSysfsMock
, evalSysfsMock
, execSysfsMock
, SysfsMockIO
, runSysfsMockIO
, evalSysfsMockIO
, execSysfsMockIO
, doesDirectoryExist
, doesFileExist
, getDirectoryContents
, readFile
, writeFile
, unlockedWriteFile
, pollFile
) where
import Protolude
hiding (StateT, execStateT, readFile, runStateT, writeFile)
import Control.Monad.Base (MonadBase)
import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow, throwM)
import Control.Monad.Catch.Pure (Catch, runCatch)
import Control.Monad.Cont (MonadCont)
import Control.Monad.Fix (MonadFix)
import Control.Monad.Logger (MonadLogger, MonadLoggerIO)
import Control.Monad.State.Strict (StateT(..), execStateT)
import Control.Monad.Trans.Class (MonadTrans)
import Control.Monad.Trans.Control
(ComposeSt, MonadBaseControl(..), MonadTransControl(..),
defaultLiftBaseWith, defaultLiftWith, defaultRestoreM,
defaultRestoreT)
import Control.Monad.Writer (MonadWriter(..))
import qualified Data.ByteString.Char8 as C8 (unlines)
import Data.List (length)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map (empty, insert, insertLookupWithKey, lookup)
import Data.Text (unwords)
import Foreign.C.Types (CInt(..))
import GHC.IO.Exception (IOErrorType(..))
import System.FilePath ((</>), splitFileName)
import System.IO.Error (IOError, mkIOError)
import System.GPIO.Linux.Sysfs.Mock.Internal
(Directory, File(..), FileType(..), MockFSZipper(..), directory,
dirName, files, subdirs, findFile)
import qualified System.GPIO.Linux.Sysfs.Mock.Internal as Internal
(cd, mkdir, mkfile, pathFromRoot, rmdir)
import System.GPIO.Linux.Sysfs.Monad (SysfsGpioT(..))
import qualified System.GPIO.Linux.Sysfs.Monad as M (MonadSysfs(..))
import System.GPIO.Linux.Sysfs.Types (SysfsEdge(..))
import System.GPIO.Linux.Sysfs.Util
(bsToInt, intToBS, pinActiveLowFileName, pinDirectionFileName,
pinEdgeFileName, pinValueFileName, pinDirName, activeLowToBS,
bsToActiveLow, pinDirectionToBS, bsToPinDirection, sysfsEdgeToBS,
bsToSysfsEdge, pinValueToBS, bsToPinValue, sysfsPath)
import System.GPIO.Types
(Pin(..), PinDirection(..), PinValue(..), gpioExceptionToException,
gpioExceptionFromException, invertValue)
data MockPinState =
MockPinState {_direction :: !PinDirection
,_userVisibleDirection :: !Bool
,_activeLow :: !Bool
,_value :: !PinValue
,_edge :: Maybe SysfsEdge
}
deriving (Show,Eq)
logicalValue :: MockPinState -> PinValue
logicalValue s
| _activeLow s = invertValue $ _value s
| otherwise = _value s
setLogicalValue :: PinValue -> MockPinState -> MockPinState
setLogicalValue v s
| _activeLow s = s {_value = invertValue v}
| otherwise = s {_value = v}
defaultMockPinState :: MockPinState
defaultMockPinState =
MockPinState {_direction = Out
,_userVisibleDirection = True
,_activeLow = False
,_value = Low
,_edge = Just None}
data MockGpioChip =
MockGpioChip {_label :: !Text
,_base :: !Int
,_initialPinStates :: [MockPinState]
}
deriving (Show,Eq)
type MockPins = Map Pin MockPinState
data MockWorld =
MockWorld {_zipper :: MockFSZipper
,_pins :: MockPins}
deriving (Show,Eq)
mockWorldPins :: MockWorld -> MockPins
mockWorldPins = _pins
initialMockWorld :: MockWorld
initialMockWorld = MockWorld sysfsRootZipper Map.empty
newtype SysfsMockT m a = SysfsMockT
{ unSysfsMockT :: StateT MockWorld m a
} deriving ( Functor
, Alternative
, Applicative
, Monad
, MonadBase b
, MonadFix
, MonadPlus
, MonadThrow
, MonadCatch
, MonadMask
, MonadCont
, MonadIO
, MonadReader r
, MonadError e
, MonadWriter w
, MonadState MockWorld
, MonadLogger
, MonadLoggerIO
, MonadTrans
)
instance MonadBaseControl b m => MonadBaseControl b (SysfsMockT m) where
type StM (SysfsMockT m) a = ComposeSt SysfsMockT m a
liftBaseWith = defaultLiftBaseWith
restoreM = defaultRestoreM
{-# INLINABLE liftBaseWith #-}
{-# INLINABLE restoreM #-}
instance MonadTransControl SysfsMockT where
type StT SysfsMockT a = StT (StateT MockWorld) a
liftWith = defaultLiftWith SysfsMockT unSysfsMockT
restoreT = defaultRestoreT SysfsMockT
{-# INLINABLE liftWith #-}
{-# INLINABLE restoreT #-}
type MockM m = (Functor m, MonadThrow m)
getZipper :: (MockM m) => SysfsMockT m MockFSZipper
getZipper = gets _zipper
putZipper :: (MockM m) => MockFSZipper -> SysfsMockT m ()
putZipper z =
do s <- get
put $ s {_zipper = z}
getPins :: (MockM m) => SysfsMockT m MockPins
getPins = gets _pins
pinState :: (MockM m) => Pin -> SysfsMockT m MockPinState
pinState pin =
Map.lookup pin <$> getPins >>= \case
Nothing -> throwM $ InternalError $
unwords ["An operation attempted to get the mock pin state for non-existent pin", show pin]
Just s -> return s
putPins :: (MockM m) => MockPins -> SysfsMockT m ()
putPins ps =
do s <- get
put $ s {_pins = ps}
putPinState :: (MockM m) => Pin -> (MockPinState -> MockPinState) -> SysfsMockT m ()
putPinState pin f =
do ps <- pinState pin
(Map.insert pin (f ps) <$> getPins) >>= putPins
runSysfsMockT :: (MockM m) => SysfsMockT m a -> MockWorld -> [MockGpioChip] -> m (a, MockWorld)
runSysfsMockT action world chips =
do startState <- execStateT (unSysfsMockT $ pushd "/" (makeFileSystem chips)) world
runStateT (unSysfsMockT action) startState
evalSysfsMockT :: (MockM m) => SysfsMockT m a -> MockWorld -> [MockGpioChip] -> m a
evalSysfsMockT a w chips = fst <$> runSysfsMockT a w chips
execSysfsMockT :: (MockM m) => SysfsMockT m a -> MockWorld -> [MockGpioChip] -> m MockWorld
execSysfsMockT a w chips = snd <$> runSysfsMockT a w chips
instance (MockM m) => M.MonadSysfs (SysfsMockT m) where
doesDirectoryExist = doesDirectoryExist
doesFileExist = doesFileExist
getDirectoryContents = getDirectoryContents
readFile = readFile
writeFile = writeFile
unlockedWriteFile = unlockedWriteFile
pollFile = pollFile
type SysfsMock = SysfsMockT Catch
runSysfsMock :: SysfsMock a -> MockWorld -> [MockGpioChip] -> Either SomeException (a, MockWorld)
runSysfsMock a w chips = runCatch $ runSysfsMockT a w chips
evalSysfsMock :: SysfsMock a -> MockWorld -> [MockGpioChip] -> Either SomeException a
evalSysfsMock a w chips = fst <$> runSysfsMock a w chips
execSysfsMock :: SysfsMock a -> MockWorld -> [MockGpioChip] -> Either SomeException MockWorld
execSysfsMock a w chips = snd <$> runSysfsMock a w chips
type SysfsGpioMock = SysfsGpioT SysfsMock
runSysfsGpioMock :: SysfsGpioMock a -> MockWorld -> [MockGpioChip] -> Either SomeException (a, MockWorld)
runSysfsGpioMock a = runSysfsMock (runSysfsGpioT a)
evalSysfsGpioMock :: SysfsGpioMock a -> MockWorld -> [MockGpioChip] -> Either SomeException a
evalSysfsGpioMock a = evalSysfsMock (runSysfsGpioT a)
execSysfsGpioMock :: SysfsGpioMock a -> MockWorld -> [MockGpioChip] -> Either SomeException MockWorld
execSysfsGpioMock a = execSysfsMock (runSysfsGpioT a)
type SysfsMockIO = SysfsMockT IO
runSysfsMockIO :: SysfsMockIO a -> MockWorld -> [MockGpioChip] -> IO (a, MockWorld)
runSysfsMockIO = runSysfsMockT
evalSysfsMockIO :: SysfsMockIO a -> MockWorld -> [MockGpioChip] -> IO a
evalSysfsMockIO a w chips = fst <$> runSysfsMockIO a w chips
execSysfsMockIO :: SysfsMockIO a -> MockWorld -> [MockGpioChip] -> IO MockWorld
execSysfsMockIO a w chips = snd <$> runSysfsMockIO a w chips
type SysfsGpioMockIO = SysfsGpioT SysfsMockIO
runSysfsGpioMockIO :: SysfsGpioMockIO a -> MockWorld -> [MockGpioChip] -> IO (a, MockWorld)
runSysfsGpioMockIO a = runSysfsMockIO (runSysfsGpioT a)
evalSysfsGpioMockIO :: SysfsGpioMockIO a -> MockWorld -> [MockGpioChip] -> IO a
evalSysfsGpioMockIO a = evalSysfsMockIO (runSysfsGpioT a)
execSysfsGpioMockIO :: SysfsGpioMockIO a -> MockWorld -> [MockGpioChip] -> IO MockWorld
execSysfsGpioMockIO a = execSysfsMockIO (runSysfsGpioT a)
data MockFSException
= GpioChipOverlap Pin
| InternalError Text
deriving (Show,Eq,Typeable)
instance Exception MockFSException where
toException = gpioExceptionToException
fromException = gpioExceptionFromException
makeFileSystem :: (MockM m) => [MockGpioChip] -> SysfsMockT m MockFSZipper
makeFileSystem chips =
do mapM_ makeChip chips
getZipper
makeChip :: (MockM m) => MockGpioChip -> SysfsMockT m ()
makeChip chip =
let chipdir = sysfsPath </> ("gpiochip" ++ show (_base chip))
in
addPins (_base chip) (_initialPinStates chip) <$> getPins >>= \case
Left e -> throwM e
Right newPinState ->
do putPins newPinState
mkdir chipdir
mkfile (chipdir </> "base") (Constant [intToBS $ _base chip])
mkfile (chipdir </> "ngpio") (Constant [intToBS $ length (_initialPinStates chip)])
mkfile (chipdir </> "label") (Constant [toS $ _label chip])
addPins :: Int -> [MockPinState] -> MockPins -> Either MockFSException MockPins
addPins base states pm = foldrM addPin pm (zip (map Pin [base..]) states)
addPin :: (Pin, MockPinState) -> MockPins -> Either MockFSException MockPins
addPin (pin, st) pm =
let insertLookup = Map.insertLookupWithKey (\_ a _ -> a)
in
case insertLookup pin st pm of
(Nothing, newPm) -> Right newPm
(Just _, _) -> Left $ GpioChipOverlap pin
pushd :: (MockM m) => FilePath -> SysfsMockT m a -> SysfsMockT m a
pushd path action =
do z <- getZipper
let restorePath = Internal.pathFromRoot z
cd path >>= putZipper
result <- action
cd restorePath >>= putZipper
return result
cd :: (MockM m) => FilePath -> SysfsMockT m MockFSZipper
cd name =
do fsz <- getZipper
case Internal.cd name fsz of
Left e -> throwM e
Right newz -> return newz
mkdir :: (MockM m) => FilePath -> SysfsMockT m ()
mkdir path =
let (parentName, childName) = splitFileName path
in
do parent <- cd parentName
either throwM putZipper (Internal.mkdir childName parent)
rmdir :: (MockM m) => FilePath -> SysfsMockT m ()
rmdir path =
let (parentName, childName) = splitFileName path
in
do parent <- cd parentName
either throwM putZipper (Internal.rmdir childName parent)
mkfile :: (MockM m) => FilePath -> FileType -> SysfsMockT m ()
mkfile path filetype =
let (parentName, childName) = splitFileName path
in
do parent <- cd parentName
either throwM putZipper (Internal.mkfile childName filetype False parent)
doesDirectoryExist :: (MockM m) => FilePath -> SysfsMockT m Bool
doesDirectoryExist path =
do cwz <- getZipper
return $ either (const False) (const True) (Internal.cd path cwz)
doesFileExist :: (MockM m) => FilePath -> SysfsMockT m Bool
doesFileExist path =
let (dirPath, fileName) = splitFileName path
in
do cwz <- getZipper
case Internal.cd dirPath cwz of
Left _ -> return False
Right z ->
return $ isJust (findFile fileName (_cwd z))
getDirectoryContents :: (MockM m) => FilePath -> SysfsMockT m [FilePath]
getDirectoryContents path =
do parent <- _cwd <$> cd path
return $ fmap dirName (subdirs parent) ++ fmap _fileName (files parent)
readFile :: (MockM m) => FilePath -> SysfsMockT m ByteString
readFile path =
fileAt path >>= \case
Nothing ->
do isDirectory <- doesDirectoryExist path
if isDirectory
then throwM $ mkIOError InappropriateType "Mock.readFile" Nothing (Just path)
else throwM $ mkIOError NoSuchThing "Mock.readFile" Nothing (Just path)
Just (Constant contents) -> return $ C8.unlines contents
Just (Value pin) -> pinValueToBS . logicalValue <$> pinState pin
Just (ActiveLow pin) -> activeLowToBS . _activeLow <$> pinState pin
Just (Direction pin) ->
do visible <- _userVisibleDirection <$> pinState pin
if visible
then do direction <- _direction <$> pinState pin
return $ pinDirectionToBS direction
else throwM $
InternalError $
unwords ["Mock pin", show pin, "has no direction but direction attribute is exported"]
Just (Edge pin) ->
_edge <$> pinState pin >>= \case
Nothing -> throwM $ InternalError $
unwords ["Mock pin", show pin, "has no edge but edge attribute is exported"]
Just edge -> return $ sysfsEdgeToBS edge
Just _ -> throwM $ mkIOError PermissionDenied "Mock.readFile" Nothing (Just path)
writeFile :: (MockM m) => FilePath -> ByteString -> SysfsMockT m ()
writeFile path bs =
fileAt path >>= \case
Nothing ->
do isDirectory <- doesDirectoryExist path
if isDirectory
then throwM $ mkIOError InappropriateType "Mock.writeFile" Nothing (Just path)
else throwM $ mkIOError NoSuchThing "Mock.writeFile" Nothing (Just path)
Just Export ->
case bsToInt bs of
Just n -> export (Pin n)
Nothing -> throwM writeError
Just Unexport ->
case bsToInt bs of
Just n -> unexport (Pin n)
Nothing -> throwM writeError
Just (ActiveLow pin) ->
case bsToActiveLow bs of
Just b -> putPinState pin (\s -> s {_activeLow = b})
Nothing -> throwM writeError
Just (Value pin) ->
_direction <$> pinState pin >>= \case
Out ->
case bsToPinValue bs of
Just v -> putPinState pin (setLogicalValue v)
Nothing -> throwM writeError
_ ->
throwM permissionError
Just (Edge pin) ->
do ps <- pinState pin
case (_edge ps, _direction ps) of
(Nothing, _) -> throwM $ InternalError $
unwords ["Mock pin", show pin, "has no edge but edge attribute is exported"]
(_, Out) -> throwM $ mkIOError InvalidArgument "Mock.writeFile" Nothing (Just path)
_ -> case bsToSysfsEdge bs of
Just edge -> putPinState pin (\s -> s {_edge = Just edge})
Nothing -> throwM writeError
Just (Direction pin) ->
do ps <- pinState pin
case (_userVisibleDirection ps, _edge ps, bsToPinDirection bs) of
(False, _, _) -> throwM $ InternalError $
unwords ["Mock pin", show pin, "has no direction but direction attribute is exported"]
(True, _, Nothing) -> throwM writeError
(True, Nothing, Just (dir, Nothing)) -> putPinState pin (\s -> s {_direction = dir})
(True, Nothing, Just (dir, Just v)) -> putPinState pin (\s -> s {_direction = dir, _value = v})
(True, Just None, Just (dir, Nothing)) -> putPinState pin (\s -> s {_direction = dir})
(True, Just None, Just (dir, Just v)) -> putPinState pin (\s -> s {_direction = dir, _value = v})
(True, _, Just (In, _)) -> putPinState pin (\s -> s {_direction = In})
(True, _, Just (Out, _)) -> throwM $ mkIOError HardwareFault "Mock.writeFile" Nothing (Just path)
Just _ -> throwM permissionError
where
writeError :: IOError
writeError = mkIOError InvalidArgument "Mock.writeFile" Nothing (Just path)
permissionError :: IOError
permissionError = mkIOError PermissionDenied "Mock.writeFile" Nothing (Just path)
export :: (MockM m) => Pin -> SysfsMockT m ()
export pin =
Map.lookup pin <$> getPins >>= \case
Nothing -> throwM $ mkIOError InvalidArgument "Mock.writeFile" Nothing (Just path)
Just s ->
do let pindir = pinDirName pin
doesDirectoryExist pindir >>= \case
True -> throwM $ mkIOError ResourceBusy "Mock.writeFile" Nothing (Just path)
False ->
do mkdir pindir
mkfile (pinActiveLowFileName pin) (ActiveLow pin)
mkfile (pinValueFileName pin) (Value pin)
when (_userVisibleDirection s) $
mkfile (pinDirectionFileName pin) (Direction pin)
when (isJust $ _edge s) $
mkfile (pinEdgeFileName pin) (Edge pin)
unexport :: (MockM m) => Pin -> SysfsMockT m ()
unexport pin =
do let pindir = pinDirName pin
doesDirectoryExist pindir >>= \case
True -> rmdir pindir
False -> throwM $ mkIOError InvalidArgument "Mock.writeFile" Nothing (Just path)
fileAt :: (MockM m) => FilePath -> SysfsMockT m (Maybe FileType)
fileAt path =
let (dirPath, fileName) = splitFileName path
in
do parent <- _cwd <$> cd dirPath
return $ findFile fileName parent
unlockedWriteFile :: (MockM m) => FilePath -> ByteString -> SysfsMockT m ()
unlockedWriteFile = writeFile
pollFile :: (MockM m) => FilePath -> Int -> SysfsMockT m CInt
pollFile _ _ = return 1
sysfsRoot :: Directory
sysfsRoot =
directory "/"
[]
[directory "sys"
[]
[directory "class"
[]
[directory "gpio"
[File "export" Export
,File "unexport" Unexport]
[]]]]
sysfsRootZipper :: MockFSZipper
sysfsRootZipper = MockFSZipper sysfsRoot []