{- git-annex assistant mount watcher, using either dbus or mtab polling
 -
 - Copyright 2012 Joey Hess <joey@kitenet.net>
 -
 - Licensed under the GNU GPL version 3 or higher.
 -}

{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}

module Assistant.Threads.MountWatcher where

import Assistant.Common
import Assistant.DaemonStatus
import Assistant.Sync
import qualified Annex
import qualified Git
import Utility.ThreadScheduler
import Utility.Mounts
import Remote.List
import qualified Types.Remote as Remote

import qualified Data.Set as S

#if WITH_DBUS
import Utility.DBus
import DBus.Client
import DBus
import Data.Word (Word32)
import Control.Concurrent
import qualified Control.Exception as E
#else
#warning Building without dbus support; will use mtab polling
#endif

thisThread :: ThreadName
thisThread = "MountWatcher"

mountWatcherThread :: NamedThread
mountWatcherThread = NamedThread "MountWatcher" $
#if WITH_DBUS
	dbusThread
#else
	pollingThread
#endif

#if WITH_DBUS

dbusThread :: Assistant ()
dbusThread = do
	runclient <- asIO1 go
	r <- liftIO $ E.try $ runClient getSessionAddress runclient
	either onerr (const noop) r
  where
	go client = ifM (checkMountMonitor client)
		( do
			{- Store the current mount points in an MVar, to be
			 - compared later. We could in theory work out the
			 - mount point from the dbus message, but this is
			 - easier. -}
			mvar <- liftIO $ newMVar =<< currentMountPoints
			handleevent <- asIO1 $ \_event -> do
				nowmounted <- liftIO $ currentMountPoints
				wasmounted <- liftIO $ swapMVar mvar nowmounted
				handleMounts wasmounted nowmounted
			liftIO $ forM_ mountChanged $ \matcher ->
				listen client matcher handleevent
		, do
			liftAnnex $
				warning "No known volume monitor available through dbus; falling back to mtab polling"
			pollingThread
		)
	onerr :: E.SomeException -> Assistant ()
	onerr e = do
		{- If the session dbus fails, the user probably
		 - logged out of their desktop. Even if they log
		 - back in, we won't have access to the dbus
		 - session key, so polling is the best that can be
		 - done in this situation. -}
		liftAnnex $
			warning $ "dbus failed; falling back to mtab polling (" ++ show e ++ ")"
		pollingThread

{- Examine the list of services connected to dbus, to see if there
 - are any we can use to monitor mounts. If not, will attempt to start one. -}
checkMountMonitor :: Client -> Assistant Bool
checkMountMonitor client = do
	running <- filter (`elem` usableservices)
		<$> liftIO (listServiceNames client)
	case running of
		[] -> startOneService client startableservices
		(service:_) -> do
			debug [ "Using running DBUS service"
				, service
				, "to monitor mount events."
				]
			return True
  where
	startableservices = [gvfs]
	usableservices = startableservices ++ [kde]
	gvfs = "org.gtk.Private.GduVolumeMonitor"
	kde = "org.kde.DeviceNotifications"

startOneService :: Client -> [ServiceName] -> Assistant Bool
startOneService _ [] = return False
startOneService client (x:xs) = do
	_ <- liftIO $ callDBus client "StartServiceByName"
		[toVariant x, toVariant (0 :: Word32)]
	ifM (liftIO $ elem x <$> listServiceNames client)
		( do
			debug
				[ "Started DBUS service", x
				, "to monitor mount events."
				]
			return True
		, startOneService client xs
		)

{- Filter matching events recieved when drives are mounted and unmounted. -}	
mountChanged :: [MatchRule]
mountChanged = [gvfs True, gvfs False, kde, kdefallback]
  where
	{- gvfs reliably generates this event whenever a
	 - drive is mounted/unmounted, whether automatically, or manually -}
	gvfs mount = matchAny
		{ matchInterface = Just "org.gtk.Private.RemoteVolumeMonitor"
		, matchMember = Just $ if mount then "MountAdded" else "MountRemoved"
		}
	{- This event fires when KDE prompts the user what to do with a drive,
	 - but maybe not at other times. And it's not received -}
	kde = matchAny
		{ matchInterface = Just "org.kde.Solid.Device"
		, matchMember = Just "setupDone"
		}
	{- This event may not be closely related to mounting a drive, but it's
	 - observed reliably when a drive gets mounted or unmounted. -}
	kdefallback = matchAny
		{ matchInterface = Just "org.kde.KDirNotify"
		, matchMember = Just "enteredDirectory"
		}

#endif

pollingThread :: Assistant ()
pollingThread = go =<< liftIO currentMountPoints
  where
	go wasmounted = do
		liftIO $ threadDelaySeconds (Seconds 10)
		nowmounted <- liftIO currentMountPoints
		handleMounts wasmounted nowmounted
		go nowmounted

handleMounts :: MountPoints -> MountPoints -> Assistant ()
handleMounts wasmounted nowmounted =
	mapM_ (handleMount . mnt_dir) $
		S.toList $ newMountPoints wasmounted nowmounted

handleMount :: FilePath -> Assistant ()
handleMount dir = do
	debug ["detected mount of", dir]
	rs <- filter (Git.repoIsLocal . Remote.repo) <$> remotesUnder dir
	reconnectRemotes True rs

{- Finds remotes located underneath the mount point.
 -
 - Updates state to include the remotes.
 -
 - The config of git remotes is re-read, as it may not have been available
 - at startup time, or may have changed (it could even be a different
 - repository at the same remote location..)
 -}
remotesUnder :: FilePath -> Assistant [Remote]
remotesUnder dir = do
	repotop <- liftAnnex $ fromRepo Git.repoPath
	rs <- liftAnnex remoteList
	pairs <- liftAnnex $ mapM (checkremote repotop) rs
	let (waschanged, rs') = unzip pairs
	when (any id waschanged) $ do
		liftAnnex $ Annex.changeState $ \s -> s { Annex.remotes = rs' }
		updateSyncRemotes
	return $ map snd $ filter fst pairs
  where
	checkremote repotop r = case Remote.localpath r of
		Just p | dirContains dir (absPathFrom repotop p) ->
			(,) <$> pure True <*> updateRemote r
		_ -> return (False, r)

type MountPoints = S.Set Mntent

currentMountPoints :: IO MountPoints
currentMountPoints = S.fromList <$> getMounts

newMountPoints :: MountPoints -> MountPoints -> MountPoints
newMountPoints old new = S.difference new old