packages feed

status-notifier-item 0.3.2.10 → 0.3.2.11

raw patch · 7 files changed

+760/−453 lines, 7 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- StatusNotifier.Host.Service: instance GHC.Internal.Show.Show StatusNotifier.Host.Service.ItemInfo
- StatusNotifier.Host.Service: instance GHC.Internal.Show.Show StatusNotifier.Host.Service.UpdateType
- StatusNotifier.Item.Service: instance GHC.Internal.Read.Read StatusNotifier.Item.Service.ItemParams
- StatusNotifier.Item.Service: instance GHC.Internal.Show.Show StatusNotifier.Item.Service.ItemParams
- StatusNotifier.Watcher.Constants: instance GHC.Internal.Show.Show StatusNotifier.Watcher.Constants.ItemEntry
- StatusNotifier.Watcher.StateCache: instance GHC.Internal.Show.Show StatusNotifier.Watcher.StateCache.JsonValue
- StatusNotifier.Watcher.StateCache: instance GHC.Internal.Show.Show StatusNotifier.Watcher.StateCache.PersistedItemEntry
- StatusNotifier.Watcher.StateCache: instance GHC.Internal.Show.Show StatusNotifier.Watcher.StateCache.PersistedWatcherState
+ StatusNotifier.Host.Service: LogicalDuplicateKey :: Maybe String -> Maybe String -> Maybe String -> Maybe String -> Maybe ObjectPath -> ObjectPath -> Bool -> LogicalDuplicateKey
+ StatusNotifier.Host.Service: [duplicateCategory] :: LogicalDuplicateKey -> Maybe String
+ StatusNotifier.Host.Service: [duplicateIconName] :: LogicalDuplicateKey -> Maybe String
+ StatusNotifier.Host.Service: [duplicateIconTitle] :: LogicalDuplicateKey -> Maybe String
+ StatusNotifier.Host.Service: [duplicateIsMenu] :: LogicalDuplicateKey -> Bool
+ StatusNotifier.Host.Service: [duplicateItemId] :: LogicalDuplicateKey -> Maybe String
+ StatusNotifier.Host.Service: [duplicateMenuPath] :: LogicalDuplicateKey -> Maybe ObjectPath
+ StatusNotifier.Host.Service: [duplicatePath] :: LogicalDuplicateKey -> ObjectPath
+ StatusNotifier.Host.Service: data LogicalDuplicateKey
+ StatusNotifier.Host.Service: findUniqueMatchM :: (a -> IO Bool) -> [a] -> IO (Maybe a)
+ StatusNotifier.Host.Service: instance GHC.Classes.Eq StatusNotifier.Host.Service.LogicalDuplicateKey
+ StatusNotifier.Host.Service: instance GHC.Show.Show StatusNotifier.Host.Service.ItemInfo
+ StatusNotifier.Host.Service: instance GHC.Show.Show StatusNotifier.Host.Service.LogicalDuplicateKey
+ StatusNotifier.Host.Service: instance GHC.Show.Show StatusNotifier.Host.Service.UpdateType
+ StatusNotifier.Host.Service: logicalDuplicateKey :: ItemInfo -> Maybe LogicalDuplicateKey
+ StatusNotifier.Host.Service: normalizeOptionalString :: String -> Maybe String
+ StatusNotifier.Item.Service: instance GHC.Read.Read StatusNotifier.Item.Service.ItemParams
+ StatusNotifier.Item.Service: instance GHC.Show.Show StatusNotifier.Item.Service.ItemParams
+ StatusNotifier.Watcher.Constants: instance GHC.Show.Show StatusNotifier.Watcher.Constants.ItemEntry
+ StatusNotifier.Watcher.StateCache: instance GHC.Show.Show StatusNotifier.Watcher.StateCache.JsonValue
+ StatusNotifier.Watcher.StateCache: instance GHC.Show.Show StatusNotifier.Watcher.StateCache.PersistedItemEntry
+ StatusNotifier.Watcher.StateCache: instance GHC.Show.Show StatusNotifier.Watcher.StateCache.PersistedWatcherState

Files

ChangeLog.md view
@@ -1,10 +1,24 @@ # Changelog for status-notifier-item +## Unreleased++## 0.3.2.11 - 2026-03-29+- Watcher: accept explicit SNI registrations initiated from a separate unique+  bus name owned by the same process, fixing KDE/Qt multi-connection+  registrations.+- Host: deduplicate logical items across bus-name churn so tray hosts keep a+  single logical icon when senders change identity.+- Add regression tests for the multi-connection registration and bus-name churn+  deduplication cases.+ ## 0.3.2.10 - 2026-02-17 - Relax upper bounds to restore Stackage nightly compatibility:   `optparse-applicative < 0.20` and `template-haskell < 2.24`.  ## 0.3.2.9 - 2026-02-17+- Version bump only; no library changes beyond `0.3.2.8`.++## 0.3.2.8 - 2026-02-17 - Host: eliminate duplicate `ItemAdded` deliveries to newly-registered update   handlers by making handler registration + initial replay atomic with respect   to item map updates.
src/StatusNotifier/Host/Service.hs view
@@ -1,44 +1,44 @@-{-# LANGUAGE TemplateHaskell #-}-{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}+ module StatusNotifier.Host.Service where -import           Control.Applicative-import           Control.Arrow-import           Control.Concurrent-import           Control.Concurrent.MVar-import           Control.Lens-import           Control.Lens.Tuple-import           Control.Monad-import           Control.Monad.Trans.Class-import           Control.Monad.Trans.Except-import           Control.Monad.Trans.Maybe-import           DBus-import           DBus.Client-import           DBus.Generation+import Control.Applicative+import Control.Arrow+import Control.Concurrent+import Control.Concurrent.MVar+import Control.Lens+import Control.Lens.Tuple+import Control.Monad+import Control.Monad.Trans.Class+import Control.Monad.Trans.Except+import Control.Monad.Trans.Maybe+import DBus+import DBus.Client+import DBus.Generation import qualified DBus.Internal.Message as M-import           DBus.Internal.Types+import DBus.Internal.Types import qualified DBus.TH as DTH import qualified Data.ByteString as BS-import           Data.Coerce-import           Data.Either-import           Data.Int+import Data.Coerce+import Data.Either+import Data.Int import qualified Data.Map.Strict as Map-import           Data.Maybe-import           Data.String-import           Data.Typeable-import           Data.Unique-import           Data.Word-import           System.Log.Logger-import           Text.Printf-+import Data.Maybe+import Data.String+import Data.Typeable+import Data.Unique+import Data.Word import qualified StatusNotifier.Item.Client as I-import           StatusNotifier.Util+import StatusNotifier.Util import qualified StatusNotifier.Watcher.Client as W import qualified StatusNotifier.Watcher.Constants as W-import qualified StatusNotifier.Watcher.Signals as W import qualified StatusNotifier.Watcher.Service as W+import qualified StatusNotifier.Watcher.Signals as W+import System.Log.Logger+import Text.Printf  statusNotifierHostString :: String statusNotifierHostString = "StatusNotifierHost"@@ -54,34 +54,36 @@   | OverlayIconUpdated   | StatusUpdated   | TitleUpdated-  | ToolTipUpdated deriving (Eq, Show)+  | ToolTipUpdated+  deriving (Eq, Show)  type UpdateHandler = UpdateType -> ItemInfo -> IO ()  data Params = Params-  { dbusClient :: Maybe Client-  , uniqueIdentifier :: String-  , namespace :: String-  , startWatcher :: Bool-  , matchSenderWhenNameOwnersUnmatched :: Bool+  { dbusClient :: Maybe Client,+    uniqueIdentifier :: String,+    namespace :: String,+    startWatcher :: Bool,+    matchSenderWhenNameOwnersUnmatched :: Bool   }  hostLogger = logM "StatusNotifier.Host.Service" -defaultParams = Params-  { dbusClient = Nothing-  , uniqueIdentifier = ""-  , namespace = "org.kde"-  , startWatcher = False-  , matchSenderWhenNameOwnersUnmatched = True-  }+defaultParams =+  Params+    { dbusClient = Nothing,+      uniqueIdentifier = "",+      namespace = "org.kde",+      startWatcher = False,+      matchSenderWhenNameOwnersUnmatched = True+    }  type ImageInfo = [(Int32, Int32, BS.ByteString)]  isExpectedPropertyUpdateFailure :: M.MethodError -> Bool-isExpectedPropertyUpdateFailure M.MethodError { M.methodErrorName = errName } =-  errName == errorUnknownMethod ||-  errName == errorName_ "org.freedesktop.DBus.Error.InvalidArgs"+isExpectedPropertyUpdateFailure M.MethodError {M.methodErrorName = errName} =+  errName == errorUnknownMethod+    || errName == errorName_ "org.freedesktop.DBus.Error.InvalidArgs"  propertyUpdateFailureLogLevel :: [M.MethodError] -> [a] -> Priority propertyUpdateFailureLogLevel failures updates@@ -90,167 +92,253 @@   | otherwise = ERROR  data ItemInfo = ItemInfo-  { itemServiceName :: BusName-  , itemServicePath :: ObjectPath-  , itemId :: Maybe String-  , itemStatus :: Maybe String-  , itemCategory :: Maybe String-  , itemToolTip :: Maybe (String, ImageInfo, String, String)-  , iconTitle :: String-  , iconName :: String-  , overlayIconName :: Maybe String-  , iconThemePath :: Maybe String-  , iconPixmaps :: ImageInfo-  , overlayIconPixmaps :: ImageInfo-  , menuPath :: Maybe ObjectPath-  , itemIsMenu :: Bool-  } deriving (Eq, Show)+  { itemServiceName :: BusName,+    itemServicePath :: ObjectPath,+    itemId :: Maybe String,+    itemStatus :: Maybe String,+    itemCategory :: Maybe String,+    itemToolTip :: Maybe (String, ImageInfo, String, String),+    iconTitle :: String,+    iconName :: String,+    overlayIconName :: Maybe String,+    iconThemePath :: Maybe String,+    iconPixmaps :: ImageInfo,+    overlayIconPixmaps :: ImageInfo,+    menuPath :: Maybe ObjectPath,+    itemIsMenu :: Bool+  }+  deriving (Eq, Show) +data LogicalDuplicateKey = LogicalDuplicateKey+  { duplicateItemId :: Maybe String,+    duplicateIconTitle :: Maybe String,+    duplicateIconName :: Maybe String,+    duplicateCategory :: Maybe String,+    duplicateMenuPath :: Maybe ObjectPath,+    duplicatePath :: ObjectPath,+    duplicateIsMenu :: Bool+  }+  deriving (Eq, Show)+ supressPixelData info =-  info { iconPixmaps = map (\(w, h, _) -> (w, h, "")) $ iconPixmaps info }+  info+    { iconPixmaps = map (\(w, h, _) -> (w, h, "")) $ iconPixmaps info,+      overlayIconPixmaps = map (\(w, h, _) -> (w, h, "")) $ overlayIconPixmaps info+    } +normalizeOptionalString :: String -> Maybe String+normalizeOptionalString value+  | null value = Nothing+  | otherwise = Just value++logicalDuplicateKey :: ItemInfo -> Maybe LogicalDuplicateKey+logicalDuplicateKey ItemInfo {..} =+  let key =+        LogicalDuplicateKey+          { duplicateItemId = itemId >>= normalizeOptionalString,+            duplicateIconTitle = normalizeOptionalString iconTitle,+            duplicateIconName = normalizeOptionalString iconName,+            duplicateCategory = itemCategory >>= normalizeOptionalString,+            duplicateMenuPath = menuPath,+            duplicatePath = itemServicePath,+            duplicateIsMenu = itemIsMenu+          }+      hasStableIdentity =+        isJust (duplicateItemId key)+          || isJust (duplicateIconTitle key)+          || isJust (duplicateIconName key)+   in if hasStableIdentity then Just key else Nothing++findUniqueMatchM :: (a -> IO Bool) -> [a] -> IO (Maybe a)+findUniqueMatchM predicate values = go values Nothing+  where+    go [] match = pure match+    go (value : rest) match = do+      matches <- predicate value+      case (matches, match) of+        (False, _) -> go rest match+        (True, Nothing) -> go rest (Just value)+        (True, Just _) -> pure Nothing+ makeLensesWithLSuffix ''ItemInfo  convertPixmapsToHostByteOrder ::   [(Int32, Int32, BS.ByteString)] -> [(Int32, Int32, BS.ByteString)] convertPixmapsToHostByteOrder = map $ over _3 networkToSystemByteOrder -callFromInfo fn ItemInfo { itemServiceName = name-                         , itemServicePath = path-                         } = fn name path+callFromInfo+  fn+  ItemInfo+    { itemServiceName = name,+      itemServicePath = path+    } = fn name path  data Host = Host-  { itemInfoMap :: IO (Map.Map BusName ItemInfo)-  , addUpdateHandler :: UpdateHandler -> IO Unique-  , removeUpdateHandler :: Unique -> IO ()-  , forceUpdate :: BusName -> IO ()-  } deriving Typeable+  { itemInfoMap :: IO (Map.Map BusName ItemInfo),+    addUpdateHandler :: UpdateHandler -> IO Unique,+    removeUpdateHandler :: Unique -> IO (),+    forceUpdate :: BusName -> IO ()+  }+  deriving (Typeable)  build :: Params -> IO (Maybe Host)-build Params { dbusClient = mclient-             , namespace = namespaceString-             , uniqueIdentifier = uniqueID-             , startWatcher = shouldStartWatcher-             , matchSenderWhenNameOwnersUnmatched = doMatchUnmatchedSender-             } = do-  client <- maybe connectSession return mclient-  itemInfoMapVar <- newMVar Map.empty-  updateHandlersVar <- newMVar ([] :: [(Unique, UpdateHandler)])-  let busName = getBusName namespaceString uniqueID+build+  Params+    { dbusClient = mclient,+      namespace = namespaceString,+      uniqueIdentifier = uniqueID,+      startWatcher = shouldStartWatcher,+      matchSenderWhenNameOwnersUnmatched = doMatchUnmatchedSender+    } = do+    client <- maybe connectSession return mclient+    itemInfoMapVar <- newMVar Map.empty+    updateHandlersVar <- newMVar ([] :: [(Unique, UpdateHandler)])+    let busName = getBusName namespaceString uniqueID -      logError = hostLogger ERROR-      logErrorWithMessage message error = logError message >> logError (show error)+        logError = hostLogger ERROR+        logErrorWithMessage message error = logError message >> logError (show error) -      logInfo = hostLogger INFO-      logDebug = hostLogger DEBUG-      logErrorAndThen andThen e = logError (show e) >> andThen+        logInfo = hostLogger INFO+        logDebug = hostLogger DEBUG+        logErrorAndThen andThen e = logError (show e) >> andThen -      doUpdateForHandler utype uinfo (unique, handler) = do-        -- This is extremely chatty under normal operation; keep it at DEBUG.-        logDebug (printf "Sending update (iconPixmaps suppressed): %s %s, for handler %s"-                          (show utype)-                          (show $ supressPixelData uinfo)-                          (show $ hashUnique unique))-        forkIO $ handler utype uinfo+        doUpdateForHandler utype uinfo (unique, handler) = do+          -- This is extremely chatty under normal operation; keep it at DEBUG.+          logDebug+            ( printf+                "Sending update (iconPixmaps suppressed): %s %s, for handler %s"+                (show utype)+                (show $ supressPixelData uinfo)+                (show $ hashUnique unique)+            )+          forkIO $ handler utype uinfo -      doUpdate utype uinfo =-        readMVar updateHandlersVar >>= mapM_ (doUpdateForHandler utype uinfo)+        doUpdate utype uinfo =+          readMVar updateHandlersVar >>= mapM_ (doUpdateForHandler utype uinfo) -      addHandler handler = do-        unique <- newUnique-        modifyMVar_ itemInfoMapVar $ \itemInfoMap -> do-          -- Register and replay under the item map lock so a concurrent add-          -- cannot be delivered both live and via replay.-          modifyMVar_ updateHandlersVar (return . ((unique, handler):))-          let doUpdateForInfo info = doUpdateForHandler ItemAdded info (unique, handler)-          mapM_ doUpdateForInfo itemInfoMap-          return itemInfoMap-        return unique+        addHandler handler = do+          unique <- newUnique+          modifyMVar_ itemInfoMapVar $ \itemInfoMap -> do+            logInfo $+              printf+                "Registering update handler %s with %d replay items: %s"+                (show $ hashUnique unique)+                (Map.size itemInfoMap)+                (show $ Map.keys itemInfoMap)+            -- Register and replay under the item map lock so a concurrent add+            -- cannot be delivered both live and via replay.+            modifyMVar_ updateHandlersVar (return . ((unique, handler) :))+            let doUpdateForInfo info = doUpdateForHandler ItemAdded info (unique, handler)+            mapM_ doUpdateForInfo itemInfoMap+            return itemInfoMap+          return unique -      removeHandler unique =-        modifyMVar_ updateHandlersVar (return . filter ((/= unique) . fst))+        removeHandler unique =+          modifyMVar_ updateHandlersVar $ \handlers -> do+            let newHandlers = filter ((/= unique) . fst) handlers+            logInfo $+              printf+                "Removing update handler %s; remaining handlers: %d"+                (show $ hashUnique unique)+                (length newHandlers)+            return newHandlers -      getPixmaps getter a1 a2 a3 =-        fmap convertPixmapsToHostByteOrder <$> getter a1 a2 a3+        getPixmaps getter a1 a2 a3 =+          fmap convertPixmapsToHostByteOrder <$> getter a1 a2 a3 -      getMaybe fn a b c = right Just <$> fn a b c+        getMaybe fn a b c = right Just <$> fn a b c -      parseServiceName name =-        let (bus, maybePath) = splitServiceName name-        in (busName_ bus, objectPath_ <$> maybePath)+        parseServiceName name =+          let (bus, maybePath) = splitServiceName name+           in (busName_ bus, objectPath_ <$> maybePath) -      buildItemInfo name = runExceptT $ do-        let (busName, maybePath) = parseServiceName name-        path <- case maybePath of-          Just parsedPath -> return parsedPath-          Nothing -> objectPath_ <$> ExceptT-            (W.getObjectPathForItemName client (coerce busName))-        let doGetDef def fn =-              ExceptT $ exemptAll def <$> fn client busName path-            doGet fn = ExceptT $ fn client busName path-        pixmaps <- doGetDef [] $ getPixmaps I.getIconPixmap-        iName <- doGetDef name I.getIconName-        overlayPixmap <- doGetDef [] $ getPixmaps I.getOverlayIconPixmap-        overlayIName <- doGetDef Nothing $ getMaybe I.getOverlayIconName-        themePath <- doGetDef Nothing $ getMaybe I.getIconThemePath-        menu <- doGetDef Nothing $ getMaybe I.getMenu-        title <- doGetDef "" I.getTitle-        tooltip <- doGetDef Nothing $ getMaybe I.getToolTip-        idString <- doGetDef Nothing $ getMaybe I.getId-        status <- doGetDef Nothing $ getMaybe I.getStatus-        category <- doGetDef Nothing $ getMaybe I.getCategory-        itemIsMenu <- doGetDef True I.getItemIsMenu-        return ItemInfo-                 { itemServiceName = busName-                 , itemId = idString-                 , itemStatus = status-                 , itemCategory = category-                 , itemServicePath = path-                 , itemToolTip = tooltip-                 , iconPixmaps = pixmaps-                 , iconThemePath = themePath-                 , iconName = iName-                 , iconTitle = title-                 , menuPath = menu-                 , overlayIconName = overlayIName-                 , overlayIconPixmaps = overlayPixmap-                 , itemIsMenu = itemIsMenu-                 }+        buildItemInfo name = runExceptT $ do+          let (busName, maybePath) = parseServiceName name+          path <- case maybePath of+            Just parsedPath -> return parsedPath+            Nothing ->+              objectPath_+                <$> ExceptT+                  (W.getObjectPathForItemName client (coerce busName))+          let doGetDef def fn =+                ExceptT $ exemptAll def <$> fn client busName path+              doGet fn = ExceptT $ fn client busName path+          pixmaps <- doGetDef [] $ getPixmaps I.getIconPixmap+          iName <- doGetDef name I.getIconName+          overlayPixmap <- doGetDef [] $ getPixmaps I.getOverlayIconPixmap+          overlayIName <- doGetDef Nothing $ getMaybe I.getOverlayIconName+          themePath <- doGetDef Nothing $ getMaybe I.getIconThemePath+          menu <- doGetDef Nothing $ getMaybe I.getMenu+          title <- doGetDef "" I.getTitle+          tooltip <- doGetDef Nothing $ getMaybe I.getToolTip+          idString <- doGetDef Nothing $ getMaybe I.getId+          status <- doGetDef Nothing $ getMaybe I.getStatus+          category <- doGetDef Nothing $ getMaybe I.getCategory+          itemIsMenu <- doGetDef True I.getItemIsMenu+          return+            ItemInfo+              { itemServiceName = busName,+                itemId = idString,+                itemStatus = status,+                itemCategory = category,+                itemServicePath = path,+                itemToolTip = tooltip,+                iconPixmaps = pixmaps,+                iconThemePath = themePath,+                iconName = iName,+                iconTitle = title,+                menuPath = menu,+                overlayIconName = overlayIName,+                overlayIconPixmaps = overlayPixmap,+                itemIsMenu = itemIsMenu+              } -      createAll serviceNames = do-        (errors, itemInfos) <--          partitionEithers <$> mapM buildItemInfo serviceNames-        mapM_ (logErrorWithMessage "Error in item building at startup:") errors-        return itemInfos+        createAll serviceNames = do+          (errors, itemInfos) <-+            partitionEithers <$> mapM buildItemInfo serviceNames+          mapM_ (logErrorWithMessage "Error in item building at startup:") errors+          return itemInfos -      registerWithPairs =-        mapM (uncurry clientSignalRegister)-        where logUnableToCallSignal signal =-                hostLogger ERROR $ printf "Unable to call handler with %s" $-                     show signal-              clientSignalRegister signalRegisterFn handler =-                signalRegisterFn client matchAny handler logUnableToCallSignal+        registerWithPairs =+          mapM (uncurry clientSignalRegister)+          where+            logUnableToCallSignal signal =+              hostLogger ERROR $+                printf "Unable to call handler with %s" $+                  show signal+            clientSignalRegister signalRegisterFn handler =+              signalRegisterFn client matchAny handler logUnableToCallSignal -      -- Resolve a bus name to its unique name owner when it is well-known.-      -- For unique names (":1.42"), the owner is the name itself.-      resolveOwner :: BusName -> IO (Maybe BusName)-      resolveOwner bus =-        case (coerce bus :: String) of-          ':' : _ -> pure (Just bus)-          _ ->-            either (const Nothing) (Just . busName_) <$>-              DTH.getNameOwner client (coerce bus)+        -- Resolve a bus name to its unique name owner when it is well-known.+        -- For unique names (":1.42"), the owner is the name itself.+        resolveOwner :: BusName -> IO (Maybe BusName)+        resolveOwner bus =+          case (coerce bus :: String) of+            ':' : _ -> pure (Just bus)+            _ ->+              either (const Nothing) (Just . busName_)+                <$> DTH.getNameOwner client (coerce bus) -      handleItemAdded serviceName =-        modifyMVar_ itemInfoMapVar $ \itemInfoMap ->-          buildItemInfo serviceName >>=-          either (logErrorAndThen $ return itemInfoMap)-                 (addItemInfo itemInfoMap)-          where addItemInfo map itemInfo@ItemInfo{ itemServiceName = newName-                                                 , itemServicePath = newPath-                                                 } =-                  if Map.member newName map-                  then return map+        handleItemAdded serviceName =+          modifyMVar_ itemInfoMapVar $ \itemInfoMap ->+            buildItemInfo serviceName+              >>= either+                (logErrorAndThen $ return itemInfoMap)+                (addItemInfo itemInfoMap)+          where+            addItemInfo+              map+              itemInfo@ItemInfo+                { itemServiceName = newName,+                  itemServicePath = newPath+                } =+                if Map.member newName map+                  then do+                    logDebug $+                      printf+                        "Ignoring duplicate ItemAdded for %s because the exact service is already tracked."+                        (coerce newName :: String)+                    return map                   else do                     -- When the watcher restarts, some items may re-register                     -- under a different bus name (e.g. switching between@@ -264,267 +352,326 @@                             existingName /= newName                               && existingOwner == newOwner                               && itemServicePath existingInfo == newPath+                        matchesLogicalDuplicate (existingName, existingInfo) =+                          pure $+                            existingName /= newName+                              && logicalDuplicateKey existingInfo == logicalDuplicateKey itemInfo                         addFresh = do                           doUpdate ItemAdded itemInfo                           pure (Map.insert newName itemInfo map)+                        replaceExisting :: BusName -> ItemInfo -> String -> IO (Map.Map BusName ItemInfo)+                        replaceExisting existingName existingInfo reason = do+                          logInfo $+                            printf+                              "Replacing tracked item %s with %s for %s"+                              (coerce existingName :: String)+                              (coerce newName :: String)+                              reason+                          doUpdate ItemRemoved existingInfo+                          doUpdate ItemAdded itemInfo+                          pure $+                            Map.insert newName itemInfo $+                              Map.delete existingName map+                        tryLogicalDuplicateMatch =+                          case logicalDuplicateKey itemInfo of+                            Nothing -> addFresh+                            Just duplicateKey -> do+                              existing <- findUniqueMatchM matchesLogicalDuplicate (Map.toList map)+                              case existing of+                                Just (existingName, existingInfo) ->+                                  replaceExisting+                                    existingName+                                    existingInfo+                                    (printf "logical duplicate key %s" (show duplicateKey))+                                Nothing -> addFresh                     case newOwner of-                      Nothing -> addFresh+                      Nothing -> tryLogicalDuplicateMatch                       Just _ -> do                         existing <- findM matchesOwnerAndPath (Map.toList map)                         case existing of-                          Nothing -> addFresh-                          Just (existingName, existingInfo) -> do-                            doUpdate ItemRemoved existingInfo-                            doUpdate ItemAdded itemInfo-                            pure $-                              Map.insert newName itemInfo $-                                Map.delete existingName map+                          Nothing -> do+                            logDebug $+                              printf+                                "Tracking fresh item %s at %s"+                                (coerce newName :: String)+                                (coerce newPath :: String)+                            tryLogicalDuplicateMatch+                          Just (existingName, existingInfo) ->+                            replaceExisting+                              existingName+                              existingInfo+                              (printf "shared owner/path at %s" (coerce newPath :: String)) -      getObjectPathForItemName name =-        maybe I.defaultPath itemServicePath . Map.lookup name <$>-        readMVar itemInfoMapVar+        getObjectPathForItemName name =+          maybe I.defaultPath itemServicePath . Map.lookup name+            <$> readMVar itemInfoMapVar -      handleItemRemoved serviceName =-        modifyMVar itemInfoMapVar doRemove >>=-        maybe logNonExistentRemoval (doUpdate ItemRemoved)-        where-          busName = fst (parseServiceName serviceName)-          doRemove currentMap =-            return (Map.delete busName currentMap, Map.lookup busName currentMap)-          logNonExistentRemoval =-            -- This can happen due to watcher/host races (e.g. watcher restart).-            hostLogger DEBUG $ printf "Attempt to remove unknown item %s" $-                       show busName+        handleItemRemoved serviceName =+          modifyMVar itemInfoMapVar doRemove+            >>= maybe logNonExistentRemoval (doUpdate ItemRemoved)+          where+            busName = fst (parseServiceName serviceName)+            doRemove currentMap =+              return (Map.delete busName currentMap, Map.lookup busName currentMap)+            logNonExistentRemoval =+              -- This can happen due to watcher/host races (e.g. watcher restart).+              hostLogger DEBUG $+                printf "Attempt to remove unknown item %s" $+                  show busName -      watcherRegistrationPairs =-        [ (W.registerForStatusNotifierItemRegistered, const handleItemAdded)-        , (W.registerForStatusNotifierItemUnregistered, const handleItemRemoved)-        ]+        watcherRegistrationPairs =+          [ (W.registerForStatusNotifierItemRegistered, const handleItemAdded),+            (W.registerForStatusNotifierItemUnregistered, const handleItemRemoved)+          ] -      watcherServiceName = coerce $ W.getWatcherInterfaceName namespaceString+        watcherServiceName = coerce $ W.getWatcherInterfaceName namespaceString -      synchronizeItemsWithWatcher = do-        let retryDelayMicros = 50000-            maxRetries = 20-            fetchWatcherItems retries = do-              watcherItemsResult <- W.getRegisteredStatusNotifierItems client-              case watcherItemsResult of-                Right watcherItems -> return $ Right watcherItems-                Left err ->-                  if retries <= 0-                    then return $ Left err-                    else threadDelay retryDelayMicros >>-                         fetchWatcherItems (retries - 1)-        watcherItemsResult <- fetchWatcherItems maxRetries-        case watcherItemsResult of-          Left err ->-            logErrorWithMessage "Failed to synchronize host state with watcher:" err-          Right watcherServiceNames -> do-            currentMap <- readMVar itemInfoMapVar-            let watcherBusNames = map (fst . parseServiceName) watcherServiceNames-                missingFromWatcher =-                  filter (`notElem` watcherBusNames) (Map.keys currentMap)-            mapM_ (handleItemRemoved . coerce) missingFromWatcher-            mapM_ handleItemAdded watcherServiceNames+        synchronizeItemsWithWatcher = do+          let retryDelayMicros = 50000+              maxRetries = 20+              fetchWatcherItems retries = do+                watcherItemsResult <- W.getRegisteredStatusNotifierItems client+                case watcherItemsResult of+                  Right watcherItems -> return $ Right watcherItems+                  Left err ->+                    if retries <= 0+                      then return $ Left err+                      else+                        threadDelay retryDelayMicros+                          >> fetchWatcherItems (retries - 1)+          watcherItemsResult <- fetchWatcherItems maxRetries+          case watcherItemsResult of+            Left err ->+              logErrorWithMessage "Failed to synchronize host state with watcher:" err+            Right watcherServiceNames -> do+              currentMap <- readMVar itemInfoMapVar+              let watcherBusNames = map (fst . parseServiceName) watcherServiceNames+                  missingFromWatcher =+                    filter (`notElem` watcherBusNames) (Map.keys currentMap)+              mapM_ (handleItemRemoved . coerce) missingFromWatcher+              mapM_ handleItemAdded watcherServiceNames -      handleWatcherNameOwnerChanged _ name _ newOwner =-        when (name == watcherServiceName && newOwner /= "") $ do-          logInfo "Watcher owner changed; synchronizing item state."-          synchronizeItemsWithWatcher+        handleWatcherNameOwnerChanged _ name _ newOwner =+          when (name == watcherServiceName && newOwner /= "") $ do+            logInfo "Watcher owner changed; synchronizing item state."+            synchronizeItemsWithWatcher -      getSender fn s@M.Signal { M.signalSender = Just sender} =-        -- Signal dumps are too noisy at INFO.-        logDebug (show s) >> fn sender-      getSender _ s = logError $ "Received signal with no sender: " ++ show s+        getSender fn s@M.Signal {M.signalSender = Just sender} =+          -- Signal dumps are too noisy at INFO.+          logDebug (show s) >> fn sender+        getSender _ s = logError $ "Received signal with no sender: " ++ show s -      runProperty prop serviceName =-        getObjectPathForItemName serviceName >>= prop client serviceName+        runProperty prop serviceName =+          getObjectPathForItemName serviceName >>= prop client serviceName -      logUnknownSender updateType signal =-        hostLogger DEBUG $-                   printf "Got signal for update type: %s from unknown sender: %s"-                   (show updateType) (show signal)+        logUnknownSender updateType signal =+          hostLogger DEBUG $+            printf+              "Got signal for update type: %s from unknown sender: %s"+              (show updateType)+              (show signal) -      identifySender M.Signal { M.signalSender = Just sender-                              , M.signalPath = senderPath-                              } = do-        infoMap <- readMVar itemInfoMapVar-        let identifySenderBySender = return (Map.lookup sender infoMap)-            identifySenderByNameOwner =-              let matchByOwner info = do-                    ownerResult <- DTH.getNameOwner client-                                   (coerce $ itemServiceName info)-                    return $ case ownerResult of-                      Right owner -> owner == coerce sender-                      Left _ -> False-              in findM matchByOwner (Map.elems infoMap)-            identifySenderById = fmap join $-              identifySenderById_ >>= logIdentifyByIdResult-            logIdentifyByIdResult-              (Left M.MethodError { M.methodErrorName = errName })-              | errName == errorUnknownMethod =-                  hostLogger DEBUG-                    (printf "Item does not support getId: %s"-                      (show errName)) >>-                  return Nothing-            logIdentifyByIdResult result =-              logEitherError hostLogger "Failed to identify sender" result-            identifySenderById_ = runExceptT $ do-              senderId <- ExceptT $ I.getId client sender senderPath-              let matchesSender info =-                    if itemId info == Just senderId-                    then do-                      senderNameOwner <- DTH.getNameOwner client (coerce sender)-                      infoNameOwner <- DTH.getNameOwner client (coerce $ itemServiceName info)-                      let warningMsg =-                            "Matched sender id: %s, but name owners do not \-                            \ match: %s %s. Considered match: %s."-                          warningText = printf warningMsg-                                        (show senderId)-                                        (show senderNameOwner)-                                        (show infoNameOwner)-                      when (senderNameOwner /= infoNameOwner) $-                           hostLogger DEBUG warningText-                      return doMatchUnmatchedSender-                    else return False-              lift $ findM matchesSender (Map.elems infoMap)-        identifySenderBySender <||> identifySenderByNameOwner <||> identifySenderById-        where a <||> b = runMaybeT $ MaybeT a <|> MaybeT b-      identifySender _ = return Nothing+        identifySender+          M.Signal+            { M.signalSender = Just sender,+              M.signalPath = senderPath+            } = do+            infoMap <- readMVar itemInfoMapVar+            let identifySenderBySender = return (Map.lookup sender infoMap)+                identifySenderByNameOwner =+                  let matchByOwner info = do+                        ownerResult <-+                          DTH.getNameOwner+                            client+                            (coerce $ itemServiceName info)+                        return $ case ownerResult of+                          Right owner -> owner == coerce sender+                          Left _ -> False+                   in findM matchByOwner (Map.elems infoMap)+                identifySenderById =+                  fmap join $+                    identifySenderById_ >>= logIdentifyByIdResult+                logIdentifyByIdResult+                  (Left M.MethodError {M.methodErrorName = errName})+                    | errName == errorUnknownMethod =+                        hostLogger+                          DEBUG+                          ( printf+                              "Item does not support getId: %s"+                              (show errName)+                          )+                          >> return Nothing+                logIdentifyByIdResult result =+                  logEitherError hostLogger "Failed to identify sender" result+                identifySenderById_ = runExceptT $ do+                  senderId <- ExceptT $ I.getId client sender senderPath+                  let matchesSender info =+                        if itemId info == Just senderId+                          then do+                            senderNameOwner <- DTH.getNameOwner client (coerce sender)+                            infoNameOwner <- DTH.getNameOwner client (coerce $ itemServiceName info)+                            let warningMsg =+                                  "Matched sender id: %s, but name owners do not \+                                  \ match: %s %s. Considered match: %s."+                                warningText =+                                  printf+                                    warningMsg+                                    (show senderId)+                                    (show senderNameOwner)+                                    (show infoNameOwner)+                            when (senderNameOwner /= infoNameOwner) $+                              hostLogger DEBUG warningText+                            return doMatchUnmatchedSender+                          else return False+                  lift $ findM matchesSender (Map.elems infoMap)+            identifySenderBySender <||> identifySenderByNameOwner <||> identifySenderById+            where+              a <||> b = runMaybeT $ MaybeT a <|> MaybeT b+        identifySender _ = return Nothing -      updateItemByLensAndProp lens prop busName = runExceptT $ do-        newValue <- ExceptT (runProperty prop busName)-        let modify infoMap =-              -- This noops when the value is not present-              let newMap = set (at busName . _Just . lens) newValue infoMap-              in return (newMap, Map.lookup busName newMap)-        ExceptT $ maybeToEither (methodError (Serial 0) errorFailed) <$>-                modifyMVar itemInfoMapVar modify+        updateItemByLensAndProp lens prop busName = runExceptT $ do+          newValue <- ExceptT (runProperty prop busName)+          let modify infoMap =+                -- This noops when the value is not present+                let newMap = set (at busName . _Just . lens) newValue infoMap+                 in return (newMap, Map.lookup busName newMap)+          ExceptT $+            maybeToEither (methodError (Serial 0) errorFailed)+              <$> modifyMVar itemInfoMapVar modify -      logErrorsHandler lens updateType prop =-        runUpdaters [updateItemByLensAndProp lens prop] updateType+        logErrorsHandler lens updateType prop =+          runUpdaters [updateItemByLensAndProp lens prop] updateType -      -- Run all the provided updaters with the expectation that at least one-      -- will succeed.-      runUpdatersForService updaters updateType serviceName = do-        updateResults <- mapM ($ serviceName) updaters-        let (failures, updates) = partitionEithers updateResults-            logLevel = propertyUpdateFailureLogLevel failures updates-        mapM_ (doUpdate updateType) updates-        when (not $ null failures) $-             hostLogger logLevel $ printf "Property update failures %s" $-                        show failures+        -- Run all the provided updaters with the expectation that at least one+        -- will succeed.+        runUpdatersForService updaters updateType serviceName = do+          updateResults <- mapM ($ serviceName) updaters+          let (failures, updates) = partitionEithers updateResults+              logLevel = propertyUpdateFailureLogLevel failures updates+          mapM_ (doUpdate updateType) updates+          when (not $ null failures) $+            hostLogger logLevel $+              printf "Property update failures %s" $+                show failures -      runUpdaters updaters updateType signal =-        identifySender signal >>= maybe runForAll (runUpdateForService . itemServiceName)-        where runUpdateForService = runUpdatersForService updaters updateType-              runForAll = logUnknownSender updateType signal >>-                          readMVar itemInfoMapVar >>=-                          mapM_ runUpdateForService . Map.keys+        runUpdaters updaters updateType signal =+          identifySender signal >>= maybe runForAll (runUpdateForService . itemServiceName)+          where+            runUpdateForService = runUpdatersForService updaters updateType+            runForAll =+              logUnknownSender updateType signal+                >> readMVar itemInfoMapVar+                >>= mapM_ runUpdateForService . Map.keys -      updateIconPixmaps =-        updateItemByLensAndProp iconPixmapsL $ getPixmaps I.getIconPixmap+        updateIconPixmaps =+          updateItemByLensAndProp iconPixmapsL $ getPixmaps I.getIconPixmap -      updateIconName =-        updateItemByLensAndProp iconNameL I.getIconName+        updateIconName =+          updateItemByLensAndProp iconNameL I.getIconName -      updateIconTheme =-        updateItemByLensAndProp iconThemePathL getThemePathDefault+        updateIconTheme =+          updateItemByLensAndProp iconThemePathL getThemePathDefault -      updateFromIconThemeFromSignal signal =-        identifySender signal >>= traverse (updateIconTheme . itemServiceName)+        updateFromIconThemeFromSignal signal =+          identifySender signal >>= traverse (updateIconTheme . itemServiceName) -      handleNewIcon signal = do-        -- XXX: This avoids the case where the theme path is updated before the-        -- icon name is updated when both signals are sent simultaneously-        updateFromIconThemeFromSignal signal-        runUpdaters [updateIconPixmaps, updateIconName]-                    IconUpdated signal+        handleNewIcon signal = do+          -- XXX: This avoids the case where the theme path is updated before the+          -- icon name is updated when both signals are sent simultaneously+          updateFromIconThemeFromSignal signal+          runUpdaters+            [updateIconPixmaps, updateIconName]+            IconUpdated+            signal -      updateOverlayIconName =-        updateItemByLensAndProp overlayIconNameL $-                                getMaybe I.getOverlayIconName+        updateOverlayIconName =+          updateItemByLensAndProp overlayIconNameL $+            getMaybe I.getOverlayIconName -      updateOverlayIconPixmaps =-        updateItemByLensAndProp overlayIconPixmapsL $-                                getPixmaps I.getOverlayIconPixmap+        updateOverlayIconPixmaps =+          updateItemByLensAndProp overlayIconPixmapsL $+            getPixmaps I.getOverlayIconPixmap -      handleNewOverlayIcon signal = do-        updateFromIconThemeFromSignal signal-        runUpdaters [updateOverlayIconPixmaps, updateOverlayIconName]-                    OverlayIconUpdated signal+        handleNewOverlayIcon signal = do+          updateFromIconThemeFromSignal signal+          runUpdaters+            [updateOverlayIconPixmaps, updateOverlayIconName]+            OverlayIconUpdated+            signal -      getThemePathDefault client busName objectPath =-        right Just <$> I.getIconThemePath client busName objectPath+        getThemePathDefault client busName objectPath =+          right Just <$> I.getIconThemePath client busName objectPath -      handleNewTitle =-        logErrorsHandler iconTitleL TitleUpdated I.getTitle+        handleNewTitle =+          logErrorsHandler iconTitleL TitleUpdated I.getTitle -      handleNewTooltip =-        logErrorsHandler itemToolTipL ToolTipUpdated $ getMaybe I.getToolTip+        handleNewTooltip =+          logErrorsHandler itemToolTipL ToolTipUpdated $ getMaybe I.getToolTip -      handleNewStatus =-        logErrorsHandler itemStatusL StatusUpdated $ getMaybe I.getStatus+        handleNewStatus =+          logErrorsHandler itemStatusL StatusUpdated $ getMaybe I.getStatus -      clientRegistrationPairs =-        [ (I.registerForNewIcon, handleNewIcon)-        , (I.registerForNewIconThemePath, handleNewIcon)-        , (I.registerForNewOverlayIcon, handleNewOverlayIcon)-        , (I.registerForNewTitle, handleNewTitle)-        , (I.registerForNewToolTip, handleNewTooltip)-        , (I.registerForNewStatus, handleNewStatus)-        ]+        clientRegistrationPairs =+          [ (I.registerForNewIcon, handleNewIcon),+            (I.registerForNewIconThemePath, handleNewIcon),+            (I.registerForNewOverlayIcon, handleNewOverlayIcon),+            (I.registerForNewTitle, handleNewTitle),+            (I.registerForNewToolTip, handleNewTooltip),+            (I.registerForNewStatus, handleNewStatus)+          ] -      initializeItemInfoMap = modifyMVar itemInfoMapVar $ \itemInfoMap -> do-        -- All initialization is done inside this modifyMVar to avoid race-        -- conditions with the itemInfoMapVar.-        clientSignalHandlers <- registerWithPairs clientRegistrationPairs-        watcherSignalHandlers <- registerWithPairs watcherRegistrationPairs-        watcherOwnerChangedHandler <--          DTH.registerForNameOwnerChanged client matchAny handleWatcherNameOwnerChanged-        let unregisterAll =+        initializeItemInfoMap = modifyMVar itemInfoMapVar $ \itemInfoMap -> do+          -- All initialization is done inside this modifyMVar to avoid race+          -- conditions with the itemInfoMapVar.+          clientSignalHandlers <- registerWithPairs clientRegistrationPairs+          watcherSignalHandlers <- registerWithPairs watcherRegistrationPairs+          watcherOwnerChangedHandler <-+            DTH.registerForNameOwnerChanged client matchAny handleWatcherNameOwnerChanged+          let unregisterAll =                 mapM_ (removeMatch client) $-                    watcherOwnerChangedHandler : clientSignalHandlers ++ watcherSignalHandlers-            shutdownHost = do+                  watcherOwnerChangedHandler : clientSignalHandlers ++ watcherSignalHandlers+              shutdownHost = do                 logInfo "Shutting down StatusNotifierHost"                 unregisterAll                 releaseName client (fromString busName)                 return ()-            logErrorAndShutdown error =-              logError (show error) >> shutdownHost >> return (Map.empty, False)-            finishInitialization serviceNames = do-              itemInfos <- createAll serviceNames-              let newMap = Map.fromList $ map (itemServiceName &&& id) itemInfos-                  resultMap = Map.union itemInfoMap newMap-              W.registerStatusNotifierHost client busName >>=-               either logErrorAndShutdown (const $ return (resultMap, True))-        W.getRegisteredStatusNotifierItems client >>=-         either logErrorAndShutdown finishInitialization+              logErrorAndShutdown error =+                logError (show error) >> shutdownHost >> return (Map.empty, False)+              finishInitialization serviceNames = do+                itemInfos <- createAll serviceNames+                let newMap = Map.fromList $ map (itemServiceName &&& id) itemInfos+                    resultMap = Map.union itemInfoMap newMap+                W.registerStatusNotifierHost client busName+                  >>= either logErrorAndShutdown (const $ return (resultMap, True))+          W.getRegisteredStatusNotifierItems client+            >>= either logErrorAndShutdown finishInitialization -      startWatcherIfNeeded = do-        let watcherName = maybe "" coerce $ genBusName W.watcherClientGenerationParams-            startWatcher = do-              (_, doIt) <- W.buildWatcher W.defaultWatcherParams-              doIt-        res <- DTH.getNameOwner client watcherName-        case res of-          Right _ -> return ()-          Left _ -> void $ forkIO $ void startWatcher+        startWatcherIfNeeded = do+          let watcherName = maybe "" coerce $ genBusName W.watcherClientGenerationParams+              startWatcher = do+                (_, doIt) <- W.buildWatcher W.defaultWatcherParams+                doIt+          res <- DTH.getNameOwner client watcherName+          case res of+            Right _ -> return ()+            Left _ -> void $ forkIO $ void startWatcher -  when shouldStartWatcher startWatcherIfNeeded-  nameRequestResult <- requestName client (fromString busName) []-  if nameRequestResult == NamePrimaryOwner-  then do-    initializationSuccess <- initializeItemInfoMap-    return $ if initializationSuccess-    then-      Just Host-      { itemInfoMap = readMVar itemInfoMapVar-      , addUpdateHandler = addHandler-      , removeUpdateHandler = removeHandler-      , forceUpdate = handleItemAdded . coerce-      }-    else Nothing-  else do-    logErrorWithMessage "Failed to obtain desired service name" nameRequestResult-    return Nothing+    when shouldStartWatcher startWatcherIfNeeded+    nameRequestResult <- requestName client (fromString busName) []+    if nameRequestResult == NamePrimaryOwner+      then do+        initializationSuccess <- initializeItemInfoMap+        return $+          if initializationSuccess+            then+              Just+                Host+                  { itemInfoMap = readMVar itemInfoMapVar,+                    addUpdateHandler = addHandler,+                    removeUpdateHandler = removeHandler,+                    forceUpdate = handleItemAdded . coerce+                  }+            else Nothing+      else do+        logErrorWithMessage "Failed to obtain desired service name" nameRequestResult+        return Nothing
src/StatusNotifier/Watcher/Service.hs view
@@ -239,15 +239,24 @@         senderName <- ExceptT $ return $ maybeToEither senderMissingError sender         busName <-           case parsedBusName of-            Just providedBusName -> do-              owner <- ExceptT $ remapErrorName <$>-                       DBusTH.getNameOwner client (coerce providedBusName)-              unless (owner == coerce senderName) $-                throwE $ makeErrorReply errorInvalidParameters $-                  printf "Sender %s does not own service %s."-                    (coerce senderName :: String)-                    name-              return providedBusName+            Just providedBusName+              -- Unique bus names (starting with ':') are assigned by the D-Bus+              -- daemon and cannot be spoofed. Some applications (e.g. KDE's+              -- KStatusNotifierItem) register their SNI on a separate D-Bus+              -- connection, so the sender's unique name differs from the+              -- registered item's unique name even though they belong to the+              -- same process. Skip the ownership check for unique names.+              | ":" `isPrefixOf` (coerce providedBusName :: String) ->+                  return providedBusName+              | otherwise -> do+                  owner <- ExceptT $ remapErrorName <$>+                           DBusTH.getNameOwner client (coerce providedBusName)+                  unless (owner == coerce senderName) $+                    throwE $ makeErrorReply errorInvalidParameters $+                      printf "Sender %s does not own service %s."+                        (coerce senderName :: String)+                        name+                  return providedBusName             Nothing -> return senderName         let item = ItemEntry { serviceName = busName                              , servicePath = path
status-notifier-item.cabal view
@@ -5,12 +5,12 @@ -- see: https://github.com/sol/hpack  name:           status-notifier-item-version:        0.3.2.10+version:        0.3.2.11 synopsis:       A wrapper over the StatusNotifierItem/libappindicator dbus specification-description:    Please see the README on Github at <https://github.com/IvanMalison/status-notifier-item#readme>+description:    Please see the README on Github at <https://github.com/taffybar/status-notifier-item#readme> category:       Desktop-homepage:       https://github.com/IvanMalison/status-notifier-item#readme-bug-reports:    https://github.com/IvanMalison/status-notifier-item/issues+homepage:       https://github.com/taffybar/status-notifier-item#readme+bug-reports:    https://github.com/taffybar/status-notifier-item/issues author:         Ivan Malison maintainer:     IvanMalison@gmail.com copyright:      2018 Ivan Malison@@ -24,7 +24,7 @@  source-repository head   type: git-  location: https://github.com/IvanMalison/status-notifier-item+  location: https://github.com/taffybar/status-notifier-item  library   exposed-modules:
test/HostSpec.hs view
@@ -3,30 +3,29 @@ module HostSpec (spec) where  import Control.Concurrent-  ( forkIO-  , newChan-  , newEmptyMVar-  , putMVar-  , readChan-  , takeMVar-  , threadDelay-  , tryPutMVar-  , writeChan+  ( forkIO,+    newChan,+    newEmptyMVar,+    putMVar,+    readChan,+    takeMVar,+    threadDelay,+    tryPutMVar,+    writeChan,   ) import Control.Exception (finally) import Control.Monad (replicateM, void)-import Data.List (sort)-import qualified Data.Map.Strict as Map import DBus (busName_, objectPath_) import DBus.Client import qualified DBus.Internal.Message as M import DBus.Internal.Types (ErrorName, Serial (..), errorName_)+import Data.List (sort)+import qualified Data.Map.Strict as Map import StatusNotifier.Host.Service hiding (startWatcher) import qualified StatusNotifier.Watcher.Client as WatcherClient import System.Log.Logger (Priority (..)) import System.Timeout (timeout) import Test.Hspec- import TestSupport  spec :: Spec@@ -107,7 +106,7 @@       Just host <- build defaultParams {dbusClient = Just hostClient, uniqueIdentifier = "host-e"}       itemClient <- connectSession       cleanup <- registerSimpleItem itemClient "org.test.HostItemE" defaultPath "drive-harddisk"-      (do+      ( do           populated <-             waitFor 1500000 $ do               current <- itemInfoMap host@@ -115,7 +114,8 @@                 Just info -> iconName info == "drive-harddisk"                 Nothing -> False           populated `shouldBe` True-        ) `finally` cleanup+        )+        `finally` cleanup      it "replays all pre-existing items to newly-added handlers" $ \() -> do       _watcher <- startWatcher@@ -124,7 +124,7 @@       itemClientB <- connectSession       cleanupA <- registerSimpleItem itemClientA "org.test.HostItemF" defaultPath "audio-volume-high"       cleanupB <- registerSimpleItem itemClientB "org.test.HostItemG" defaultPath "audio-volume-low"-      (do+      ( do           Just host <- build defaultParams {dbusClient = Just hostClient, uniqueIdentifier = "host-f"}           events <- newChan           _ <- addUpdateHandler host (\ut info -> writeChan events (ut, itemServiceName info))@@ -132,7 +132,8 @@           let addedNames = sort [name | Just (ItemAdded, name) <- replayed]           addedNames             `shouldBe` sort [busName_ "org.test.HostItemF", busName_ "org.test.HostItemG"]-        ) `finally` (cleanupA >> cleanupB)+        )+        `finally` (cleanupA >> cleanupB)      it "does not emit updates for forceUpdate on unknown services" $ \() -> do       _watcher <- startWatcher@@ -152,14 +153,14 @@       itemClient <- connectSession       let iface =             Interface-              { interfaceName = "org.kde.StatusNotifierItem"-              , interfaceMethods = []-              , interfaceProperties =-                  [ readOnlyProperty "IconName" (pure ("folder" :: String))-                  , readOnlyProperty "OverlayIconName" (pure ("" :: String))-                  , readOnlyProperty "ItemIsMenu" (pure False)-                  ]-              , interfaceSignals = []+              { interfaceName = "org.kde.StatusNotifierItem",+                interfaceMethods = [],+                interfaceProperties =+                  [ readOnlyProperty "IconName" (pure ("folder" :: String)),+                    readOnlyProperty "OverlayIconName" (pure ("" :: String)),+                    readOnlyProperty "ItemIsMenu" (pure False)+                  ],+                interfaceSignals = []               }           defaultPath = "/StatusNotifierItem"       export itemClient (objectPath_ defaultPath) iface@@ -204,6 +205,104 @@               && not (Map.member initialKey current)       deduped `shouldBe` True +    it "deduplicates a logically identical item that appears under a new bus name" $ \() -> do+      _watcher <- startWatcher+      hostClient <- connectSession+      Just host <- build defaultParams {dbusClient = Just hostClient, uniqueIdentifier = "host-h2"}++      let duplicateIface =+            Interface+              { interfaceName = "org.kde.StatusNotifierItem",+                interfaceMethods = [],+                interfaceProperties =+                  [ readOnlyProperty "Id" (pure ("dup-item" :: String)),+                    readOnlyProperty "Title" (pure ("Duplicate Item" :: String)),+                    readOnlyProperty "Category" (pure ("ApplicationStatus" :: String)),+                    readOnlyProperty "IconName" (pure ("folder" :: String)),+                    readOnlyProperty "OverlayIconName" (pure ("" :: String)),+                    readOnlyProperty "ItemIsMenu" (pure False)+                  ],+                interfaceSignals = []+              }+          itemAName = "org.test.HostLogicalDuplicateA"+          itemBName = "org.test.HostLogicalDuplicateB"+      itemClientA <- connectSession+      export itemClientA (objectPath_ defaultPath) duplicateIface+      _ <- requestName itemClientA (busName_ itemAName) []+      WatcherClient.registerStatusNotifierItem itemClientA itemAName+        `shouldReturn` Right ()++      initialReady <-+        waitFor 1500000 $ do+          current <- itemInfoMap host+          pure $ Map.size current == 1 && Map.member (busName_ itemAName) current+      initialReady `shouldBe` True++      itemClientB <- connectSession+      export itemClientB (objectPath_ defaultPath) duplicateIface+      _ <- requestName itemClientB (busName_ itemBName) []+      WatcherClient.registerStatusNotifierItem itemClientB itemBName+        `shouldReturn` Right ()++      deduped <-+        waitFor 1500000 $ do+          current <- itemInfoMap host+          pure $+            Map.size current == 1+              && Map.member (busName_ itemBName) current+              && not (Map.member (busName_ itemAName) current)+      deduped `shouldBe` True++      _ <- releaseName itemClientA (busName_ itemAName)+      _ <- releaseName itemClientB (busName_ itemBName)+      pure ()++    it "keeps distinct items when shared ids differ by title or icon" $ \() -> do+      _watcher <- startWatcher+      hostClient <- connectSession+      Just host <- build defaultParams {dbusClient = Just hostClient, uniqueIdentifier = "host-h3"}++      let makeIface title iconName =+            Interface+              { interfaceName = "org.kde.StatusNotifierItem",+                interfaceMethods = [],+                interfaceProperties =+                  [ readOnlyProperty "Id" (pure ("git-sync-rs" :: String)),+                    readOnlyProperty "Title" (pure title),+                    readOnlyProperty "Category" (pure ("ApplicationStatus" :: String)),+                    readOnlyProperty "IconName" (pure iconName),+                    readOnlyProperty "OverlayIconName" (pure ("" :: String)),+                    readOnlyProperty "ItemIsMenu" (pure True)+                  ],+                interfaceSignals = []+              }+          itemAName = "org.test.HostDistinctSharedA"+          itemBName = "org.test.HostDistinctSharedB"+      itemClientA <- connectSession+      export itemClientA (objectPath_ defaultPath) (makeIface "git-sync-rs - org" "text-org")+      _ <- requestName itemClientA (busName_ itemAName) []+      WatcherClient.registerStatusNotifierItem itemClientA itemAName+        `shouldReturn` Right ()++      itemClientB <- connectSession+      export itemClientB (objectPath_ defaultPath) (makeIface "git-sync-rs - .password-store" "password")+      _ <- requestName itemClientB (busName_ itemBName) []+      WatcherClient.registerStatusNotifierItem itemClientB itemBName+        `shouldReturn` Right ()++      bothPresent <-+        waitFor 1500000 $ do+          current <- itemInfoMap host+          pure $+            Map.size current == 2+              && Map.member (busName_ itemAName) current+              && Map.member (busName_ itemBName) current+      bothPresent `shouldBe` True++      _ <- releaseName itemClientA (busName_ itemAName)+      _ <- releaseName itemClientB (busName_ itemBName)+      pure ()+     it "removes stale items when watcher ownership changes and replacement watcher has no item" $ \() -> do       watcher1 <- startWatcher @@ -240,17 +339,17 @@       let itemName = "org.test.HostHandlerRace"           iface =             Interface-              { interfaceName = "org.kde.StatusNotifierItem"-              , interfaceMethods = []-              , interfaceProperties =+              { interfaceName = "org.kde.StatusNotifierItem",+                interfaceMethods = [],+                interfaceProperties =                   [ readOnlyProperty "IconName" $ do                       void $ tryPutMVar buildStarted ()                       takeMVar continueBuild-                      pure ("folder" :: String)-                  , readOnlyProperty "OverlayIconName" (pure ("" :: String))-                  , readOnlyProperty "ItemIsMenu" (pure False)-                  ]-              , interfaceSignals = []+                      pure ("folder" :: String),+                    readOnlyProperty "OverlayIconName" (pure ("" :: String)),+                    readOnlyProperty "ItemIsMenu" (pure False)+                  ],+                interfaceSignals = []               }       export itemClient (objectPath_ defaultPath) iface       _ <- requestName itemClient (busName_ itemName) []@@ -301,11 +400,11 @@ mkMethodError :: ErrorName -> M.MethodError mkMethodError errName =   M.MethodError-    { M.methodErrorName = errName-    , M.methodErrorSerial = Serial 0-    , M.methodErrorSender = Nothing-    , M.methodErrorDestination = Nothing-    , M.methodErrorBody = []+    { M.methodErrorName = errName,+      M.methodErrorSerial = Serial 0,+      M.methodErrorSender = Nothing,+      M.methodErrorDestination = Nothing,+      M.methodErrorBody = []     }  errorInvalidArgs :: ErrorName
test/TestSupport.hs view
@@ -5,23 +5,27 @@   ( withIsolatedSessionBus   , startWatcher   , registerSimpleItem+  , connectSessionWithName   , waitFor   ) where  import Control.Concurrent (threadDelay) import Control.Exception (SomeException, displayException, finally, try) import Control.Monad (filterM)-import DBus (busName_, objectPath_)+import DBus (BusName, busName_, formatBusName, objectPath_) import DBus.Client   ( Client   , Interface (..)   , RequestNameReply (NamePrimaryOwner)   , connectSession+  , connectWithName+  , defaultClientOptions   , export   , readOnlyProperty   , releaseName   , requestName   )+import DBus.Internal.Address (getSessionAddress) import StatusNotifier.Watcher.Constants   ( defaultWatcherParams   , watcherDBusClient@@ -180,6 +184,13 @@   pure $ do     _ <- releaseName client (busName_ busName)     pure ()++connectSessionWithName :: IO (Client, BusName)+connectSessionWithName = do+  env <- getSessionAddress+  case env of+    Nothing -> error "connectSessionWithName: DBUS_SESSION_BUS_ADDRESS is invalid."+    Just addr -> connectWithName defaultClientOptions addr  waitFor :: Int -> IO Bool -> IO Bool waitFor timeoutMicros predicate =
test/WatcherSpec.hs view
@@ -5,6 +5,7 @@ import Control.Exception (finally) import Data.Either (isLeft) import Data.List (isPrefixOf)+import DBus (BusName, busName_, formatBusName, objectPath_) import DBus.Client import qualified StatusNotifier.Watcher.Client as WatcherClient import Test.Hspec@@ -73,6 +74,32 @@       watcher <- startWatcher       result <- WatcherClient.getObjectPathForItemName watcher "org.test.Missing"       result `shouldSatisfy` isLeft++    it "accepts registration from a different connection of the same process (KDE-style)" $ \() -> do+      watcher <- startWatcher+      -- Simulate KDE's KStatusNotifierItem pattern: one connection registers+      -- the SNI item, but the actual SNI interface lives on a separate connection.+      mainClient <- connectSession+      (sniClient, sniBusName) <- connectSessionWithName+      let sniPath = objectPath_ "/StatusNotifierItem"+          sniIface = Interface+            { interfaceName = "org.kde.StatusNotifierItem"+            , interfaceMethods = []+            , interfaceProperties =+                [ readOnlyProperty "IconName" (pure ("test-icon" :: String))+                , readOnlyProperty "OverlayIconName" (pure ("" :: String))+                , readOnlyProperty "ItemIsMenu" (pure False)+                ]+            , interfaceSignals = []+            }+          sniNameStr = formatBusName sniBusName+      export sniClient sniPath sniIface+      -- Register using the SNI connection's unique bus name, sent from mainClient+      result <- WatcherClient.registerStatusNotifierItem mainClient sniNameStr+      result `shouldBe` Right ()+      Right entries <- WatcherClient.getRegisteredSNIEntries watcher+      let matching = filter ((== sniNameStr) . fst) entries+      length matching `shouldBe` 1      it "rejects registration for unowned well-known names" $ \() -> do       watcher <- startWatcher