{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, ScopedTypeVariables, TypeSynonymInstances #-} -- | Construct an ADT representing block and character devices -- (but mostly block devices) by interpreting the contents of -- the Linux sysfs filesystem. module System.Unix.SpecialDevice (SpecialDevice, sysMountPoint, -- IO String ofNode, -- FilePath -> IO (Maybe SpecialDevice) ofNodeStatus, -- FileStatus -> Maybe SpecialDevice ofPath, -- FilePath -> IO (Maybe SpecialDevice) rootPart, -- IO (Maybe SpecialDevice) ofDevNo, -- (DeviceID -> SpecialDevice) -> Int -> SpecialDevice ofSysName, -- String -> IO (Maybe SpecialDevice) ofSysPath, -- (DeviceID -> SpecialDevice) -> FilePath -> IO (Maybe SpecialDevice) toDevno, -- SpecialDevice -> Int --major, -- SpecialDevice -> Int --minor, -- SpecialDevice -> Int ofMajorMinor, -- (DeviceID -> SpecialDevice) -> Int -> Int -> SpecialDevice node, -- SpecialDevice -> IO (Maybe FilePath) nodes, -- SpecialDevice -> IO [FilePath] sysName, -- SpecialDevice -> IO (Maybe String) splitPart, -- String -> (String, Int) sysDir, -- SpecialDevice -> IO (Maybe FilePath) diskOfPart, -- SpecialDevice -> IO (Maybe SpecialDevice) getAllDisks, -- IO [SpecialDevice] getAllPartitions, -- IO [SpecialDevice] getAllCdroms, -- IO [SpecialDevice] getAllRemovable, -- IO [SpecialDevice] -- toDevName, -- getBlkidAlist, -- getBlkidInfo, -- deviceOfUuid, -- devicesOfLabel, -- updateBlkidFns, -- update ) where import Control.Exception import System.IO import System.Directory import Data.Char import Data.List import Data.Maybe import System.FilePath import System.Posix.Types import System.Posix.Files import System.Posix.User import Text.Regex.TDFA data SpecialDevice = BlockDevice DeviceID | CharacterDevice DeviceID deriving (Show, Ord, Eq) -- | FIXME: We should really get this value from the mount table. sysMountPoint :: FilePath sysMountPoint = "/sys" ofPath :: FilePath -> IO (Maybe SpecialDevice) ofPath path = -- Catch the exception thrown on an invalid symlink (try $ getFileStatus path) >>= return . either (\ (_ :: SomeException) -> Nothing) (Just . BlockDevice . deviceID) rootPart :: IO (Maybe SpecialDevice) rootPart = ofPath "/" -- | Return the device represented by a device node, such as \/dev\/sda2. -- Returns Nothing if there is an exception trying to stat the node, or -- if the node turns out not to be a special device. ofNode :: FilePath -> IO (Maybe SpecialDevice) ofNode "/dev/root" = ofPath "/" ofNode node = (try $ getFileStatus node) >>= return . either (\ (_ :: SomeException) -> Nothing) ofNodeStatus ofNodeStatus :: FileStatus -> Maybe SpecialDevice ofNodeStatus status = if isBlockDevice status then (Just . BlockDevice . specialDeviceID $ status) else if isCharacterDevice status then (Just . CharacterDevice . specialDeviceID $ status) else Nothing ofSysName :: String -> IO (Maybe SpecialDevice) ofSysName name = do paths <- directory_find False (sysMountPoint ++ "/block") >>= return . map fst . filter isDev case filter (\ x -> basename (dirname x) == name) paths of [path] -> ofSysPath BlockDevice (dirname path) where isDev (path, status) = basename path == "dev" ofSysPath :: (DeviceID -> SpecialDevice) -> FilePath -> IO (Maybe SpecialDevice) ofSysPath typ path = readFile (path ++ "/dev") >>= return . parseSysDevFile typ parseSysDevFile :: (DeviceID -> SpecialDevice) -> String -> Maybe SpecialDevice parseSysDevFile typ text = case filter (all isDigit) . groupBy (\ a b -> isDigit a && isDigit b) $ text of [major, minor] -> Just (ofMajorMinor typ (read major) (read minor)) _ -> Nothing ofMajorMinor :: (DeviceID -> SpecialDevice) -> Int -> Int -> SpecialDevice ofMajorMinor typ major minor = ofDevNo typ $ major * 256 + minor ofDevNo :: (DeviceID -> SpecialDevice) -> Int -> SpecialDevice ofDevNo typ n = typ . fromInteger . toInteger $ n {- major :: SpecialDevice -> Integer major dev = toInteger (toDevno dev) minor :: SpecialDevice -> Int minor dev = mod (fromInteger (toInteger (toDevno dev))) 256 -} toDevno :: SpecialDevice -> DeviceID toDevno (BlockDevice n) = n toDevno (CharacterDevice n) = n node :: SpecialDevice -> IO (Maybe FilePath) node dev@(BlockDevice _) = nodes dev >>= return . listToMaybe nodes :: SpecialDevice -> IO [FilePath] nodes dev@(BlockDevice _) = do pairs <- directory_find True "/dev" >>= return . filter (not . isPrefixOf "/dev/.static/" . fst) . filter (not . isPrefixOf "/dev/.udevdb/" . fst) let pairs' = filter (\ (node, status) -> (ofNodeStatus status) == Just dev) pairs return . map fst $ pairs' where mapSnd f (a, b) = (a, f b) splitPart :: String -> (String, Int) splitPart name = mapSnd read (break isDigit name) where mapSnd f (a, b) = (a, f b) diskOfPart :: SpecialDevice -> IO (Maybe SpecialDevice) diskOfPart part = sysName part >>= return . maybe Nothing (Just . fst . splitPart) >>= maybe (return Nothing) ofSysName sysName :: SpecialDevice -> IO (Maybe String) sysName dev = sysDir dev >>= return . maybe Nothing (Just . basename) sysDir :: SpecialDevice -> IO (Maybe FilePath) sysDir dev@(BlockDevice _) = do (pairs' :: [(FilePath, FileStatus)]) <- directory_find False (sysMountPoint ++ "/block") let (paths :: [FilePath]) = map fst . filter isDev $ pairs' devs <- mapM readFile paths >>= return . map (parseSysDevFile BlockDevice) let pairs = zip devs (map dirname paths) return . lookup (Just dev) $ pairs where isDev (path, status) = basename path == "dev" diskGroup :: IO GroupID diskGroup = getGroupEntryForName "disk" >>= return . groupID cdromGroup :: IO GroupID cdromGroup = getGroupEntryForName "cdrom" >>= return . groupID -- | Removable devices, such as USB keys, are in this group. floppyGroup :: IO GroupID floppyGroup = getGroupEntryForName "floppy" >>= return . groupID getDisksInGroup :: GroupID -> IO [SpecialDevice] getDisksInGroup group = directory_find True "/dev/disk/by-path" >>= return . filter (inGroup group) >>= return . catMaybes . map (ofNodeStatus . snd) where inGroup group (_, status) = fileGroup status == group getAllDisks :: IO [SpecialDevice] getAllDisks = do group <- diskGroup devs <- directory_find True "/dev/disk/by-path" >>= return . filter (not . isPart) . filter (inGroup group) >>= return . map (ofNodeStatus . snd) return (catMaybes devs) where inGroup group (_, status) = fileGroup status == group getAllPartitions :: IO [SpecialDevice] getAllPartitions = directory_find True "/dev/disk/by-path" >>= return . filter isPart >>= return . catMaybes . map (ofNodeStatus . snd) isPart :: (FilePath, FileStatus) -> Bool isPart (path, _) = case path =~ "-part[0-9]+$" of x | mrMatch x == "" -> False x -> True getAllCdroms :: IO [SpecialDevice] getAllCdroms = cdromGroup >>= getDisksInGroup getAllRemovable :: IO [SpecialDevice] getAllRemovable = floppyGroup >>= getDisksInGroup -- ofNode "/dev/sda1" >>= maybe (return Nothing) sysDir >>= putStrLn . show -- -> Just "/sys/block/sda/sda1/dev" -- | Traverse a directory and return a list of all the (path, -- fileStatus) pairs. directory_find :: Bool -> FilePath -> IO [(FilePath, FileStatus)] directory_find follow path = if follow then fileStatus else linkStatus where fileStatus = try (getFileStatus path) >>= either (\ (_ :: SomeException) -> return []) useStatus linkStatus = getSymbolicLinkStatus path >>= useStatus useStatus status | isDirectory status = do -- Catch the exception thrown if we lack read permission subs <- (try $ getDirectoryContents path) >>= return . either (\ (_ :: SomeException) -> []) id >>= return . map (path ) . filter (not . flip elem [".", ".."]) >>= mapM (directory_find follow) >>= return . concat return $ (path, status) : subs | True = return [(path, status)] dirname path = reverse . tail . snd . break (== '/') . reverse $ path basename path = reverse . fst . break (== '/') . reverse $ path