packages feed

watcher (empty) → 0.0.1

raw patch · 5 files changed

+357/−0 lines, 5 filesdep +basedep +filepathdep +hinotifysetup-changed

Dependencies added: base, filepath, hinotify

Files

+ LICENSE view
@@ -0,0 +1,19 @@+Copyright © 2013 Nathaniel Soares++Permission is hereby granted, free of charge, to any person obtaining a copy of+this software and associated documentation files (the "Software"), to deal in+the Software without restriction, including without limitation the rights to+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of+the Software, and to permit persons to whom the Software is furnished to do so,+subject to the following conditions:++The above copyright notice and this permission notice shall be included in all+copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE+SOFTWARE.
+ Setup.lhs view
@@ -0,0 +1,59 @@+#!/usr/local/bin/runhaskell++Copied from Edward Kmett's lens package: «https://github.com/ekmett/lens/blob/master/Setup.lhs»+Many thanks to Edward Kmett.++\begin{code}+{-# OPTIONS_GHC -Wall #-}+module Main (main) where++import Data.List ( nub )+import Data.Version ( showVersion )+import Distribution.Package ( PackageName(PackageName), Package, PackageId, InstalledPackageId, packageVersion, packageName )+import Distribution.PackageDescription ( PackageDescription(), TestSuite(..) )+import Distribution.Simple ( defaultMainWithHooks, UserHooks(..), simpleUserHooks )+import Distribution.Simple.Utils ( rewriteFile, createDirectoryIfMissingVerbose, copyFiles )+import Distribution.Simple.BuildPaths ( autogenModulesDir )+import Distribution.Simple.Setup ( BuildFlags(buildVerbosity), Flag(..), fromFlag, HaddockFlags(haddockDistPref))+import Distribution.Simple.LocalBuildInfo ( withLibLBI, withTestLBI, LocalBuildInfo(), ComponentLocalBuildInfo(componentPackageDeps) )+import Distribution.Text ( display )+import Distribution.Verbosity ( Verbosity, normal )+import System.FilePath ( (</>) )++main :: IO ()+main = defaultMainWithHooks simpleUserHooks+  { buildHook = \pkg lbi hooks flags -> do+     generateBuildModule (fromFlag (buildVerbosity flags)) pkg lbi+     buildHook simpleUserHooks pkg lbi hooks flags+  , postHaddock = \args flags pkg lbi -> do+     copyFiles normal (haddockOutputDir flags pkg) [("images","Hierarchy.png")]+     postHaddock simpleUserHooks args flags pkg lbi+  }++haddockOutputDir :: Package p => HaddockFlags -> p -> FilePath+haddockOutputDir flags pkg = destDir where+  baseDir = case haddockDistPref flags of+    NoFlag -> "."+    Flag x -> x+  destDir = baseDir </> "doc" </> "html" </> display (packageName pkg)++generateBuildModule :: Verbosity -> PackageDescription -> LocalBuildInfo -> IO ()+generateBuildModule verbosity pkg lbi = do+  let dir = autogenModulesDir lbi+  createDirectoryIfMissingVerbose verbosity True dir+  withLibLBI pkg lbi $ \_ libcfg -> do+    withTestLBI pkg lbi $ \suite suitecfg -> do+      rewriteFile (dir </> "Build_" ++ testName suite ++ ".hs") $ unlines+        [ "module Build_" ++ testName suite ++ " where"+        , "deps :: [String]"+        , "deps = " ++ (show $ formatdeps (testDeps libcfg suitecfg))+        ]+  where+    formatdeps = map (formatone . snd)+    formatone p = case packageName p of+      PackageName n -> n ++ "-" ++ showVersion (packageVersion p)++testDeps :: ComponentLocalBuildInfo -> ComponentLocalBuildInfo -> [(InstalledPackageId, PackageId)]+testDeps xs ys = nub $ componentPackageDeps xs ++ componentPackageDeps ys++\end{code}
+ src/Watcher.hs view
@@ -0,0 +1,206 @@+module Watcher+	( watch+	, unwatch+	, Action(Added, Changed, Moved, Removed)+	, Warning(MovedOutOfScope)+	, Watcher+	, Handler+	) where+import Control.Applicative+import Control.Concurrent+import Control.Monad (unless)+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Traversable (mapM)+import Prelude hiding (mapM)+import System.Directory+import System.INotify+import System.FilePath++-- |The types of actions that are reported.+data Action+	= Added+	| Changed+	| Moved FilePath+	| Removed+	deriving (Eq, Show)++-- |Badness that happened during a watch, usually due to inotify limitations.+data Warning+	= MovedOutOfScope FilePath+	deriving (Eq, Show)++-- |Functions that handle events+-- The filepaths passed will be joined with the path used to set up the watcher.+-- If you have+--    mydir/+--      file1+--      file2+-- And you do (watch myHandler "mydir") and file1 changes, myHandler will be+-- passed "mydir/file1" as the FilePath.+type Handler = Action -> FilePath -> IO ()++-- |A handler used to mutate and reference watchers.+data Watcher+	= Watcher+	{ notifier :: INotify+	, descriptors :: MVar (Map FilePath WatchDescriptor)+	, moves :: MVar (Map Cookie FilePath)+	} deriving Eq++-- |Applies a monadic function to a specific value of a map, and returns the map+-- with that value removed. If the key specifying the value is not in the map,+-- then the fallback is executed and the map is returned unmolested.+popM :: (Monad m, Ord k) => (v -> m u) -> m w -> k -> Map k v -> m (Map k v)+popM function fallback key dict = if key `Map.member` dict+	then function (dict Map.! key) >> return (key `Map.delete` dict)+	else fallback >> return dict++-- |Monadic variant of if.+ifM :: Monad m => m Bool -> m a -> m a -> m a+ifM mpred t f = do+	predicate <- mpred+	if predicate then t else f++-- |Whether a file is . or ..+isDirectoryReference :: FilePath -> Bool+isDirectoryReference = (`elem` [".", ".."])++startsWith :: FilePath -> FilePath -> Bool+startsWith prefix path = take (length prefix) path == prefix++-- All non-directory-reference files in a directory.+-- The returned paths are extensions of the input path.+-- If you have+--     mydir/+--       file1+--       file2+-- then (ls "mydir") yields [mydir/file1, mydir/file2]+ls :: FilePath -> IO [FilePath]+ls dir = do+	children <- getDirectoryContents dir+	pure $ addDirToEach $ filter isRelevant children+	where+	isRelevant = not . isDirectoryReference+	addDirToEach = map (dir </>)++-- |Like ls, but sorted. Directories on the left, files on the right.+directoriesAndFiles :: FilePath -> IO [Either FilePath FilePath]+directoriesAndFiles path = mapM partition =<< ls path+	where+	partition x = ifM (doesDirectoryExist x) (dir x) (file x)+	dir = pure . Left+	file = pure . Right++-- |Creates a watch for a single handler on a single directory.+watch :: Handler -> FilePath -> IO Watcher+watch handler filepath = do+	isDir <- doesDirectoryExist filepath+	unless isDir (ioError $ userError $ "Not a directory: " ++ filepath)+	watcher <- Watcher+		<$> initINotify+		<*> newMVar Map.empty+		<*> newMVar Map.empty+	watchDir watcher handler filepath+	pure watcher++-- |Used to generate watchDir and moveWatchedDir+watchDirWithInitializer+	:: (FilePath -> IO ())+	-> Watcher+	-> Handler+	-> FilePath+	-> IO ()+watchDirWithInitializer initialize watcher handler dir = do+	watchd <- addWatch (notifier watcher) relevantEvents dir event+	modifyMVar_ (descriptors watcher) (pure . Map.insert dir watchd)+	mapM_ (either recurse initialize) =<< directoriesAndFiles dir+	where+	recurse = watchDirWithInitializer initialize watcher handler+	moveFileTo = handler . Moved+	moveDirTo source = moveWatchedDir source watcher handler+	event :: Event -> IO ()+	-- New file created+	event (Created False child) = handler Added (dir </> child)+	-- New directory created.+	event (Created True child) = recurse (dir </> child)+	-- This file modified.+	event (Modified False Nothing) = handler Changed dir+	-- Child file modified.+	event (Modified False (Just child)) = handler Changed (dir </> child)+	-- Directory added via move.+	event (MovedIn True child cookie) = modifyMVar_ (moves watcher) (popM+		(`moveDirTo` (dir </> child))+		(recurse (dir </> child))+		cookie)+	-- File added via move.+	event (MovedIn False child cookie) = modifyMVar_ (moves watcher) (popM+		(`moveFileTo` (dir </> child))+		(handler Added (dir </> child))+		cookie)+	-- Something moved out. If it was moved out of scope, we can't fire events+	-- on it, because we don't know where it moved to. If you move watched/foo+	-- to ../bar, the MovedOut event only knows that watched/foo/ is gone.+	-- There's no way for us to figure out that it went to bar/, recurse bar/,+	-- and fire the moved events manually on the files there. Using inotify,+	-- there's no way for us to send move events unless the moved file lands in+	-- a watched directory.+	event (MovedOut _ child cookie) = modifyMVar_ (moves watcher)+		(pure . Map.insert cookie (dir </> child))+	-- Directory removed.+	event (Deleted True child) = unwatchDir watcher (dir </> child)+	-- File removed.+	event (Deleted False child) = handler Removed (dir </> child)+	-- Not relevant.+	-- Note that we don't in general care about directory modification.+	-- Self events are ignored and passed on to be handled by the parent.+	-- Other non-created/moved/modified/deleted events are ignored.+	event _ = pure ()++-- |Creates watchers on one directory, recursively.+watchDir :: Watcher -> Handler -> FilePath -> IO ()+watchDir watcher handler+	= watchDirWithInitializer (handler Added) watcher handler++-- |Moves a directory, firing the appropriate move events recursively.+-- The subdirectory watchers will be killed and recreated, which is a bit+-- expensive, but is far cleaner than manually maintaining the root-relative+-- path of each directory watcher.+moveWatchedDir :: FilePath -> Watcher -> Handler -> FilePath -> IO ()+moveWatchedDir source watcher handler dest = do+	unwatchDir watcher source+	watchDirWithInitializer moved watcher handler dest+	where+	oldPath path = source </> makeRelative dest path+	moved path = handler (Moved $ oldPath path) path++-- |Removes the watcher on one subdir and updates the watch dict accordingly.+-- Might be called after the directory has been moved/removed. It is not+-- possible in general to check the contents of the directory.+unwatchDir :: Watcher -> FilePath -> IO ()+unwatchDir watcher dir = modifyMVar_ (descriptors watcher) killChildren+	where+	killChildren dict = do+		let dying = Map.filterWithKey (\k _ -> startsWith dir k) dict+		mapM_ removeWatch $ Map.elems dying+		pure $ dict Map.\\ dying++-- |Shuts down all watching and the inotifier.+unwatch :: Watcher -> IO [Warning]+unwatch watcher = do+	lost <- withMVar (moves watcher) (pure . map MovedOutOfScope . Map.elems)+	modifyMVar_ (descriptors watcher) killDescriptors+	killINotify (notifier watcher)+	pure lost+	where+	killDescriptors dict = mapM removeWatch dict *> pure Map.empty++-- |inotify events that are listened to.+relevantEvents :: [EventVariety]+relevantEvents =+	[ Modify+	, Create+	, MoveIn+	, MoveOut+	, Delete+	]
+ tests/doctests.hs view
@@ -0,0 +1,34 @@+-- Adapted from Edward Kmett's doctests test:+-- «https://github.com/ekmett/lens/blob/master/tests/doctests.hsc»+-- Many thanks to Edward Kmett.+module Main where++import Build_doctests (deps)+import Control.Applicative+import Control.Monad+import Data.List+import System.Directory+import System.FilePath+import Test.DocTest++main :: IO ()+main = getSources >>= \sources -> doctest $+	  "-XOverloadedStrings"+	: "-isrc"+	: "-idist/build/autogen"+	: "-optP-include"+	: "-optPdist/build/autogen/cabal_macros.h"+	: "-hide-all-packages"+	: map ("-package="++) deps ++ sources++getSources :: IO [FilePath]+getSources = filter (isSuffixOf ".hs") <$> go "src"+	where+		go dir = do+			(dirs, files) <- getFilesAndDirectories dir+			(files ++) . concat <$> mapM go dirs++getFilesAndDirectories :: FilePath -> IO ([FilePath], [FilePath])+getFilesAndDirectories dir = do+	c <- map (dir </>) . filter (`notElem` ["..", "."]) <$> getDirectoryContents dir+	(,) <$> filterM doesDirectoryExist c <*> filterM doesFileExist c
+ watcher.cabal view
@@ -0,0 +1,39 @@+Name:          watcher+Version:       0.0.1+License:       MIT+License-file:  LICENSE+Category:      filesystem+Synopsis:      Opinionated filesystem watcher.+Description:   A thin opinionated wrapper around INotify. Fires+               Added/Changed/Moved/Removed events on files, and that's it.+               Watches (only files) recursively on a given directory.+               Designed for simple filesystem watchers that don't want to mess+               with INotify. Inherits all of INotify's limitations surrounding+               moved files: Events are not fired for overwritten files nor for+               files moved out of the watched directory.+Author:        Nate Soares+Maintainer:    nate@so8r.es+Build-Type:    Simple+Cabal-Version: >=1.14++Library+  default-language: Haskell2010+  build-depends:+    base >= 4 && < 5,+    filepath >= 1.3,+    hinotify >= 0.3.5+  exposed-modules: Watcher+  hs-source-dirs: src+  ghc-options: -Wall -rtsopts+  ghc-prof-options: -auto-all++test-suite doctests+  default-language: Haskell2010+  type: exitcode-stdio-1.0+  ghc-options: -threaded+  main-is: doctests.hs+  hs-source-dirs: tests+  build-depends:+    base,+    filepath,+    hinotify