happstack-plugins-6.0.3: Happstack/Plugins/Plugins.hs
{-# LANGUAGE EmptyDataDecls, TemplateHaskell #-}
module Happstack.Plugins.Plugins
( rebuild
, func
, funcTH
, withIO
, PluginHandle(..)
, initPersistentINotify
) where
import Control.Applicative ((<$>))
import Control.Concurrent.MVar (MVar,readMVar,modifyMVar,modifyMVar_,newMVar)
import Control.Exception (bracketOnError)
import Data.List (nub)
import Data.Maybe (mapMaybe)
import qualified Data.Map as Map
import Data.Map (Map)
import Language.Haskell.TH.Syntax (Name(Name),NameFlavour(NameG), occString, modString)
import System.FilePath (addExtension, dropExtension)
import System.Plugins.Load (Module, Symbol, LoadStatus(..), getImports, load, unloadAll)
import System.Plugins.Make (Errors, MakeStatus(..), MakeCode(..), makeAll)
import System.INotify (INotify, WatchDescriptor, Event(..), EventVariety(..), addWatch, removeWatch, initINotify)
import System.FilePath (splitFileName)
import Unsafe.Coerce (unsafeCoerce)
-- A very unsafe version of Data.Dynamic
data Sym
toSym :: a -> Sym
toSym = unsafeCoerce
fromSym :: Sym -> a
fromSym = unsafeCoerce
newtype PluginHandle = PluginHandle
( PersistentINotify -- Inotify handle
, MVar
( Map FilePath -- source file being observed
( [WatchDescriptorP] -- watch descriptor of the source file and its dependecies
, [FilePath] -- depedencies of the source file
, Maybe Errors -- errors when compiling the file if any
, Map Symbol -- symbol defined in the source file
(FilePath -> IO (Either Errors (Module, Sym)) -- function for reloading the symbol
, Either Errors (Module, Sym)) -- the state of the symbol (probably the result of the last call to the function in the first component)
)
)
)
funcTH :: PluginHandle -> Name -> IO (Either Errors a)
funcTH objMap name =
do let (fp, sym) = nameToFileSym name
func objMap fp sym
withIO :: PluginHandle -> Name -> (a -> IO ()) -> IO ()
withIO objMap name use =
do r <- funcTH objMap name
case r of
(Left e) -> putStrLn $ unlines e
(Right f) -> use f
nameToFileSym :: Name -> (FilePath, Symbol)
nameToFileSym (Name occName (NameG _ _ mn)) =
let dotToSlash '.' = '/'
dotToSlash c = c
fp = (map dotToSlash (modString mn)) ++ ".hs"
sym = occString occName
in (fp, sym)
nameToFileSym n = error $ "nameToFileSym failed because Name was not the right kind. " ++ show n
func :: PluginHandle -> FilePath -> Symbol -> IO (Either Errors a)
func ph@(PluginHandle (_inotify, objMap)) fp sym =
do om <- readMVar objMap
case Map.lookup fp om of
Nothing ->
do bracketOnError
(addSymbol ph fp sym)
(const$ deleteSymbol ph fp sym)
(const$ rebuild ph fp True)
func ph fp sym
(Just (_, _, Just errs, _)) -> return $ Left errs
(Just (_, _, Nothing, symbols)) ->
case Map.lookup sym symbols of
Nothing ->
do bracketOnError
(addSymbol ph fp sym)
(const$ deleteSymbol ph fp sym)
(const$ rebuild ph fp True)
func ph fp sym
(Just (_, Left errs)) -> return $ Left errs
(Just (_, Right (_, dynSym))) -> return (Right $ fromSym dynSym)
replaceSuffix :: FilePath -> String -> FilePath
replaceSuffix p sfx = case [ i | (i,'.') <- zip [0..] p ] of
[] -> p++sfx
ixs -> take (last ixs) p ++ '.':sfx
rebuild :: PluginHandle -- ^ list of currently loaded modules/symbols
-> FilePath -- ^ source file to compile
-> Bool
-> IO ()
rebuild p@(PluginHandle (_inotify, objMap)) fp forceReload =
do putStrLn ("Rebuilding " ++ fp)
makeStatus <- makeAll fp ["-odir",".","-hidir",".","-o",replaceSuffix fp "o"] -- FIXME: allow user to specify additional flags, such as -O2
case makeStatus of
(MakeFailure errs) ->
do unload <- modifyMVar objMap $ \om ->
case Map.lookup fp om of
Nothing -> do wds <- observeFiles p fp []
return (Map.insert fp (wds, [], Just errs, Map.empty) om, [])
(Just (wds, deps, _, symbols)) ->
let symbols' = Map.map (\(loader,_) -> (loader, Left errs)) symbols -- propogate error to all symbols
in return (Map.insert fp (wds, deps, Just errs, symbols') om, unloadList symbols)
mapM_ unloadAll unload
putStrLn $ unlines errs
(MakeSuccess NotReq _objFilePath) | not forceReload ->
do putStrLn "skipped reload."
return ()
(MakeSuccess _makeCode objFilePath) ->
do om <- readMVar objMap
case Map.lookup fp om of
Nothing -> return ()
(Just (oldWds, _, _, symbols)) ->
do mapM_ unloadAll (unloadList symbols)
mapM_ removeWatchP oldWds
res <- mapM (load' objFilePath) (Map.assocs symbols)
imports <- map (\bn -> addExtension (mnameToPath bn) ".hs") <$> getImports (dropExtension objFilePath)
wds <- observeFiles p fp imports
modifyMVar_ objMap $ return . Map.insert fp (wds, [], Nothing, Map.fromList res)
where
unloadList symbols =
nub $ mapMaybe (\(_, eSym) ->
case eSym of
(Left _) -> Nothing
(Right (m,_)) -> Just m) (Map.elems symbols)
load' :: FilePath
-> (Symbol, (FilePath -> IO (Either Errors (Module, Sym)), Either Errors (Module, Sym)))
-> IO (Symbol, (FilePath -> IO (Either Errors (Module, Sym)), Either Errors (Module, Sym)))
load' obj (symbol, (reloader, _)) =
do r <- reloader obj
case r of
(Left errs) -> putStrLn $ unlines errs
(Right _) -> return ()
return (symbol, (reloader, r))
mnameToPath :: FilePath -> FilePath
mnameToPath = replace '.' '/'
where replace x y = foldr (\a r -> if x==a then y:r else a:r) []
observeFiles :: PluginHandle -> FilePath -> [FilePath] -> IO [WatchDescriptorP]
observeFiles p@(PluginHandle (inotify,_objMap)) fp imports =
mapM (\depFp -> do putStrLn ("Adding watch for: " ++ depFp)
let handler e = putStrLn ("Got event for " ++ depFp ++ ": " ++ show e) >> rebuild p fp False
addWatchP inotify depFp handler
) (fp:imports)
addSymbol :: PluginHandle -> FilePath -> Symbol -> IO ()
addSymbol p@(PluginHandle (_inotify, objMap)) sourceFP sym =
do let reloader obj =
do putStrLn $ "loading " ++ sym ++ " from " ++ sourceFP
ldStatus <- load obj ["."] [] sym
case ldStatus of
(LoadSuccess m s) ->
do putStrLn "Succeed."
return (Right (m, toSym s))
(LoadFailure errs) ->
do putStrLn "Failed."
return (Left errs)
symVal = (reloader, Left ["Not loaded yet.."])
modifyMVar_ objMap $ \om ->
case Map.lookup sourceFP om of
Nothing -> do wds <- observeFiles p sourceFP []
return$ Map.insert sourceFP (wds, [], Nothing, Map.singleton sym symVal) om
(Just (wds, deps, errs, symbols)) ->
let symbols' = Map.insert sym symVal symbols
in return$ Map.insert sourceFP (wds, deps, errs, symbols') om
return ()
deleteSymbol :: PluginHandle -> FilePath -> Symbol -> IO ()
deleteSymbol (PluginHandle (_inotify, objMap)) sourceFP sym =
modifyMVar_ objMap $ \om ->
case Map.lookup sourceFP om of
Nothing -> return om
(Just (wds, deps, errs, symbols)) ->
let symbols' = Map.delete sym symbols
in return$ Map.insert sourceFP (wds, deps, errs, symbols') om
-- Keeps watching a file even after it has been deleted and created again.
--
-- It does so by observing the folder which contains the file. When no files
-- are observed in a given folder, the folder stops being observed.
data PersistentINotify = PersistentINotify
INotify -- INotify handle
(MVar
(Map FilePath -- Folder containing the file
( WatchDescriptor -- Watch descriptor of the folder
, Map String -- File being observed
(Event -> IO ()) -- Handler to run on file events
)
)
)
data WatchDescriptorP = WatchDescriptorP PersistentINotify FilePath
initPersistentINotify :: IO PersistentINotify
initPersistentINotify = do
iN <- initINotify
fmvar <- newMVar Map.empty
return$ PersistentINotify iN fmvar
-- Replacement for splitFileName which returns "." instead of an empty folder.
splitFileName' :: FilePath -> (FilePath,String)
splitFileName' fp =
let (d,f) = splitFileName fp
in (if null d then "." else d,f)
addWatchP :: PersistentINotify -> FilePath -> (Event -> IO ()) -> IO WatchDescriptorP
addWatchP piN@(PersistentINotify iN fmvar) fp hdl =
let (d,f) = splitFileName' fp
in modifyMVar fmvar$ \fm ->
case Map.lookup d fm of
Nothing -> do
wd <- addWatch iN [Modify, Move, Delete] d $ \e -> do
case e of
Ignored -> return ()
Deleted { filePath = f' } -> callHandler e d f'
MovedIn { filePath = f' } -> callHandler e d f'
Modified { maybeFilePath = Just f' } -> callHandler e d f'
_ -> return ()
return ( Map.insert d (wd,Map.singleton f hdl) fm
, WatchDescriptorP piN fp
)
Just (wd,ffm) -> return ( Map.insert d (wd,Map.insert f hdl ffm) fm
, WatchDescriptorP piN fp
)
where
callHandler e d f = do
fm <- readMVar fmvar
case Map.lookup d fm of
Nothing -> return ()
Just (_,ffm) -> case Map.lookup f ffm of
Nothing -> return ()
Just mhdl -> mhdl e
removeWatchP :: WatchDescriptorP -> IO ()
removeWatchP (WatchDescriptorP (PersistentINotify iN fmvar) fp) =
let (d,f) = splitFileName' fp
in modifyMVar_ fmvar$ \fm ->
case Map.lookup d fm of
Nothing -> error$ "removeWatchP: invalid handle for file "++fp
Just (wd,ffm) -> let ffm' = Map.delete f ffm
in if Map.null ffm' then removeWatch wd >> return (Map.delete d fm)
else return (Map.insert d (wd,ffm') fm)