{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE OverloadedStrings #-} -------------------------------------------------------------------------------- -- | Functions for handling dependencies module XMonad.Internal.Dependency ( MaybeAction , AnyFeature(..) , DepChoice(..) , MaybeX , FullDep(..) , DepTree(..) , Action(..) , DBusDep(..) , FeatureX , FeatureIO , Feature(..) , Feature_(..) , Warning(..) , Dependency(..) , UnitType(..) , DBusMember(..) , feature , ioFeature , evalFeature , systemUnit , userUnit , pathR , pathW , pathRW , featureDefault , featureExeArgs , featureExe , featureEndpoint , whenSatisfied , ifSatisfied , executeFeature , executeFeature_ , executeFeatureWith , executeFeatureWith_ , depName , fullDep , exe , listToAnds ) where import Control.Monad.IO.Class import Control.Monad.Identity -- import Data.Aeson import Data.List (find) import Data.Maybe -- import qualified Data.Text as T import DBus import DBus.Client import DBus.Internal import qualified DBus.Introspection as I import System.Directory (findExecutable, readable, writable) import System.Environment import System.Exit import XMonad.Core (X, io) import XMonad.Internal.IO import XMonad.Internal.Process import XMonad.Internal.Shell -------------------------------------------------------------------------------- -- | Features -- -- A 'feature' is composed of a 'dependency tree' which at the root has an -- 'action' to be performed with a number of 'dependencies' below it. -- -- NOTE: there is no way to make a feature depend on another feature. This is -- very complicated to implement and would only be applicable to a few instances -- (notably the dbus interfaces). In order to implement a dependency tree, use -- dependencies that target the output/state of another feature; this is more -- robust anyways, at the cost of being a bit slower. -- TODO some things to add to make this more feature-ful (lol) -- - use AndOr types to encode alternative dependencies into the tree -- - use an Alt data constructor for Features (which will mean "try A before B" -- - add an Either String Bool to dependency nodes that encodes testing status -- (where Right False means untested) -- - add a lens/functor mapper thingy to walk down the tree and update testing -- status fields -- - print to JSON -- - make sum type to hold all type instances of Feature blabla (eg IO and X) -- - figure out how to make features a dependency of another feature data Feature_ a = Feature_ { ftrDepTree :: DepTree a , ftrName :: String , ftrWarning :: Warning } data Feature a = Feature (Feature_ a) (Feature a) | NoFeature | ConstFeature a -- TODO this is silly as is, and could be made more useful by representing -- loglevels data Warning = Silent | Default type FeatureX = Feature (X ()) type FeatureIO = Feature (IO ()) data AnyFeature = FX FeatureX | FIO FeatureIO feature :: String -> Warning -> DepTree a -> Feature a feature n w t = Feature f NoFeature where f = Feature_ { ftrDepTree = t , ftrName = n , ftrWarning = w } ioFeature :: MonadIO m => Feature (IO b) -> Feature (m b) ioFeature (ConstFeature a) = ConstFeature $ liftIO a ioFeature NoFeature = NoFeature ioFeature (Feature f r) = Feature (f {ftrDepTree = liftIO <$> ftrDepTree f}) $ ioFeature r featureDefault :: String -> DepChoice (FullDep Dependency) -> a -> Feature a featureDefault n ds x = feature n Default $ GenTree (Single x) ds featureExe :: MonadIO m => String -> String -> Feature (m ()) featureExe n cmd = featureExeArgs n cmd [] featureExeArgs :: MonadIO m => String -> String -> [String] -> Feature (m ()) featureExeArgs n cmd args = featureDefault n (Only $ FullDep (Right False) $ Executable cmd) $ spawnCmd cmd args featureEndpoint :: String -> BusName -> ObjectPath -> InterfaceName -> MemberName -> Maybe Client -> FeatureIO featureEndpoint name busname path iface mem client = feature name Default $ DBusTree (Single cmd) client deps where cmd c = void $ callMethod c busname path iface mem deps = Only $ FullDep (Right False) $ Endpoint busname path iface $ Method_ mem -------------------------------------------------------------------------------- -- | Dependency Trees -- -- Dependency trees have two subtypes: general and DBus. The latter require a -- DBus client to evaluate (and will automatically fail if this is missing). -- The former can be evaluated independently. data DepChoice a = And (DepChoice a) (DepChoice a) | Or (DepChoice a) (DepChoice a) | Only a listToAnds :: a -> [a] -> DepChoice a listToAnds i = foldr (And . Only) (Only i) data DepTree a = GenTree (Action a) (DepChoice (FullDep Dependency)) | DBusTree (Action (Client -> a)) (Maybe Client) (DepChoice (FullDep DBusDep)) instance Functor DepTree where fmap f (GenTree a ds) = GenTree (f <$> a) ds fmap f (DBusTree a c ds) = DBusTree (fmap (fmap f) a) c ds -------------------------------------------------------------------------------- -- | Actions -- -- Actions have two subtypes: single and double. Single actions are just one -- independent action. Double actions have one dependent pre-step which the -- main action consumes (and fails if the pre-step fails). data Action a = Single a | forall b. Double (b -> a) (IO (Either [String] b)) instance Functor Action where fmap f (Single a) = Single (f a) fmap f (Double a b) = Double (f . a) b -------------------------------------------------------------------------------- -- | Feature evaluation -- -- Evaluate a feature by testing if its dependencies are satisfied, and return -- either the action of the feature or 0 or more error messages that signify -- what dependencies are missing and why. type MaybeAction a = Maybe a type MaybeX = MaybeAction (X ()) evalFeature :: Feature a -> IO (MaybeAction a) evalFeature (ConstFeature x) = return $ Just x evalFeature NoFeature = return Nothing -- TODO actually deal with alt evalFeature (Feature (Feature_{ftrDepTree = a, ftrName = n, ftrWarning = w}) _) = do procName <- getProgName res <- evalTree =<< evalTree' a either (printWarnings procName) (return . Just) res where printWarnings procName es = do case w of Silent -> skip Default -> let prefix = n ++ " disabled; " es' = fmap (fmtMsg procName . (prefix ++)) es in mapM_ putStrLn es' return Nothing fmtMsg procName msg = unwords [bracket procName, bracket "WARNING", msg] bracket s = "[" ++ s ++ "]" mapMDepChoice :: Monad m => (a -> m a) -> (a -> Bool) -> DepChoice a -> m (DepChoice a) mapMDepChoice f pass = fmap snd . go where go d@(And a b) = do (ra, a') <- go a if not ra then return (False, d) else do (rb, b') <- go b return $ if rb then (True, And a' b') else (False, d) go d@(Or a b) = do (ra, a') <- go a if ra then return (True, Or a' b) else do (rb, b') <- go b return $ if rb then (True, Or a' b') else (False, d) go d@(Only a) = do a' <- f a return $ if pass a' then (True, Only a') else (False, d) -- foldDepChoice :: (a -> Bool) -> DepChoice a -> Bool -- foldDepChoice get dc = case dc of -- And a b -> go a && go b -- Or a b -> go a || go b -- Only a -> get a -- where -- go = foldDepChoice get foldDepChoice' :: Bool -> (a -> Maybe b) -> DepChoice a -> [b] foldDepChoice' justSucceed get = fromMaybe [] . go [] where go acc (And a b) = Just $ andFun acc a b go acc (Or a b) = Just $ orFun acc a b go acc (Only a) = (:acc) <$> get a (andFun, orFun) = if justSucceed then (and', or') else (or', and') and' acc a b = case (go acc a, go acc b) of (Just a', Just b') -> a' ++ b' ++ acc (Just a', Nothing) -> a' ++ acc (Nothing, _) -> acc or' acc a b = fromMaybe [] (go acc a) ++ fromMaybe [] (go acc b) ++ acc -- foldDepChoice :: DepChoice a -> (a -> Maybe b) -> [b] -- foldDepChoice dc f = go [] dc -- where -- go acc d = case d of -- And a b -> do -- acc'@(a':_) <- go acc a -- if pass a' then go acc' b else return acc -- Or a b -> do -- acc'@(a':_) <- go acc a -- if pass a' then return [a'] else go acc' b -- Only a -> maybe acc $ f a -- TODO wet code evalTree :: DepTree a -> IO (Either [String] a) evalTree (GenTree a ds) = do case foldDepChoice' False fullDepMsg ds of [] -> evalAction a es -> return $ Left es evalTree (DBusTree a (Just client) ds) = do case foldDepChoice' False fullDepMsg ds of [] -> fmap (\f -> f client) <$> evalAction a es -> return $ Left es evalTree (DBusTree _ Nothing _) = return $ Left ["client not available"] fullDepMsg :: FullDep a -> Maybe String fullDepMsg (FullDep e _) = either Just (const Nothing) e evalTree' :: DepTree a -> IO (DepTree a) evalTree' (GenTree a ds) = GenTree a <$> mapMDepChoice eval pass ds where eval (FullDep _ d) = do r <- evalDependency d return $ FullDep (maybe (Right True) Left r) d pass (FullDep (Right True) _) = True pass _ = True evalTree' d@(DBusTree _ Nothing _) = return d evalTree' (DBusTree a (Just client) ds) = DBusTree a (Just client) <$> mapMDepChoice eval pass ds where eval (FullDep _ d) = do r <- eval' d return $ FullDep (maybe (Right True) Left r) d eval' (DBusGenDep d) = evalDependency d eval' x = dbusDepSatisfied client x pass (FullDep (Right True) _) = True pass _ = True evalAction :: Action a -> IO (Either [String] a) evalAction (Single a) = return $ Right a evalAction (Double a b) = fmap a <$> b executeFeatureWith :: MonadIO m => (m a -> m a) -> a -> Feature (IO a) -> m a executeFeatureWith iof def ftr = do a <- io $ evalFeature ftr maybe (return def) (iof . io) a executeFeatureWith_ :: MonadIO m => (m () -> m ()) -> Feature (IO ()) -> m () executeFeatureWith_ iof = executeFeatureWith iof () executeFeature :: MonadIO m => a -> Feature (IO a) -> m a executeFeature = executeFeatureWith id executeFeature_ :: Feature (IO ()) -> IO () executeFeature_ = executeFeature () whenSatisfied :: Monad m => MaybeAction (m ()) -> m () whenSatisfied = flip ifSatisfied skip ifSatisfied :: MaybeAction a -> a -> a ifSatisfied (Just x) _ = x ifSatisfied _ alt = alt -------------------------------------------------------------------------------- -- | Dependencies (General) data FullDep a = FullDep (Either String Bool) a deriving (Functor) fullDep :: a -> FullDep a fullDep = FullDep (Right True) data Dependency = Executable String | AccessiblePath FilePath Bool Bool | IOTest String (IO (Maybe String)) | Systemd UnitType String | DepFeature AnyFeature data UnitType = SystemUnit | UserUnit deriving (Eq, Show) exe :: String -> FullDep Dependency exe = fullDep . Executable pathR :: String -> FullDep Dependency pathR n = fullDep $ AccessiblePath n True False pathW :: String -> FullDep Dependency pathW n = fullDep $ AccessiblePath n False True pathRW :: String -> FullDep Dependency pathRW n = fullDep $ AccessiblePath n True True systemUnit :: String -> FullDep Dependency systemUnit = fullDep . Systemd SystemUnit userUnit :: String -> FullDep Dependency userUnit = fullDep . Systemd UserUnit -------------------------------------------------------------------------------- -- | Dependencies (DBus) data DBusMember = Method_ MemberName | Signal_ MemberName | Property_ String deriving (Eq, Show) data DBusDep = Bus BusName | Endpoint BusName ObjectPath InterfaceName DBusMember | DBusGenDep Dependency -------------------------------------------------------------------------------- -- | Dependency evaluation (General) -- -- Test the existence of dependencies and return either Nothing (which actually -- means success) or Just . evalDependency :: Dependency -> IO (Maybe String) evalDependency (Executable n) = exeSatisfied n evalDependency (IOTest _ t) = t evalDependency (Systemd t n) = unitSatisfied t n evalDependency (AccessiblePath p r w) = pathSatisfied p r w evalDependency (DepFeature _) = undefined -- TODO add something here to eval a nested feature's dependencies while -- bypassing the feature itself exeSatisfied :: String -> IO (Maybe String) exeSatisfied x = do r <- findExecutable x return $ case r of (Just _) -> Nothing _ -> Just $ "executable '" ++ x ++ "' not found" unitSatisfied :: UnitType -> String -> IO (Maybe String) unitSatisfied u x = do (rc, _, _) <- readCreateProcessWithExitCode' (shell cmd) "" return $ case rc of ExitSuccess -> Nothing _ -> Just $ "systemd " ++ unitType u ++ " unit '" ++ x ++ "' not found" where cmd = fmtCmd "systemctl" $ ["--user" | u == UserUnit] ++ ["status", x] unitType SystemUnit = "system" unitType UserUnit = "user" pathSatisfied :: FilePath -> Bool -> Bool -> IO (Maybe String) pathSatisfied p testread testwrite = do res <- getPermissionsSafe p let msg = permMsg res return msg where testPerm False _ _ = Nothing testPerm True f r = Just $ f r permMsg NotFoundError = Just "file not found" permMsg PermError = Just "could not get permissions" permMsg (PermResult r) = case (testPerm testread readable r, testPerm testwrite writable r) of (Just False, Just False) -> Just "file not readable or writable" (Just False, _) -> Just "file not readable" (_, Just False) -> Just "file not writable" _ -> Nothing -------------------------------------------------------------------------------- -- | Dependency evaluation (DBus) introspectInterface :: InterfaceName introspectInterface = interfaceName_ "org.freedesktop.DBus.Introspectable" introspectMethod :: MemberName introspectMethod = memberName_ "Introspect" dbusDepSatisfied :: Client -> DBusDep -> IO (Maybe String) dbusDepSatisfied client (Bus bus) = do ret <- callMethod client queryBus queryPath queryIface queryMem return $ case ret of Left e -> Just e Right b -> let ns = bodyGetNames b in if bus' `elem` ns then Nothing else Just $ unwords ["name", singleQuote bus', "not found on dbus"] where bus' = formatBusName bus queryBus = busName_ "org.freedesktop.DBus" queryIface = interfaceName_ "org.freedesktop.DBus" queryPath = objectPath_ "/" queryMem = memberName_ "ListNames" bodyGetNames [v] = fromMaybe [] $ fromVariant v :: [String] bodyGetNames _ = [] dbusDepSatisfied client (Endpoint busname objpath iface mem) = do ret <- callMethod client busname objpath introspectInterface introspectMethod return $ case ret of Left e -> Just e Right body -> procBody body where procBody body = let res = findMem =<< I.parseXML objpath =<< fromVariant =<< listToMaybe body in case res of Just True -> Nothing _ -> Just $ fmtMsg' mem findMem = fmap (matchMem mem) . find (\i -> I.interfaceName i == iface) . I.objectInterfaces matchMem (Method_ n) = elemMember n I.methodName I.interfaceMethods matchMem (Signal_ n) = elemMember n I.signalName I.interfaceSignals matchMem (Property_ n) = elemMember n I.propertyName I.interfaceProperties elemMember n fname fmember = elem n . fmap fname . fmember fmtMem (Method_ n) = "method " ++ singleQuote (formatMemberName n) fmtMem (Signal_ n) = "signal " ++ singleQuote (formatMemberName n) fmtMem (Property_ n) = "property " ++ singleQuote n fmtMsg' m = unwords [ "could not find" , fmtMem m , "on interface" , singleQuote $ formatInterfaceName iface , "on bus" , formatBusName busname ] dbusDepSatisfied _ (DBusGenDep d) = evalDependency d -------------------------------------------------------------------------------- -- | Printing dependencies -- instance ToJSON (DepTree a) where -- toJSON (GenTree _) = undefined -- instance ToJSON Dependency where -- toJSON (Executable n) = depValue "executable" Nothing n -- toJSON (IOTest d _) = depValue "internal" Nothing d -- toJSON (Systemd t n) = depValue "systemd" (Just $ tp t) n -- where -- tp SystemUnit = "sys" -- tp UserUnit = "user" -- toJSON (AccessiblePath p r w) = depValue "path" perms p -- where -- perms = case (r, w) of -- (True, True) -> Just "readwrite" -- (True, False) -> Just "read" -- (False, True) -> Just "write" -- _ -> Nothing -- depValue :: String -> Maybe String -> String -> Value -- depValue t s n = object -- [ "type" .= t -- , "name" .= n -- , "subtype" .= maybe Null (String . T.pack) s -- ] depName :: Dependency -> String depName (Executable n) = "executable: " ++ n depName (IOTest d _) = "internal: " ++ d depName (Systemd t n) = "systemd (" ++ tp t ++ "): " ++ n where tp SystemUnit = "sys" tp UserUnit = "user" depName (AccessiblePath p _ _) = "path: " ++ p depName (DepFeature _) = "feature: blablabla"