packages feed

spy (empty) → 0.4

raw patch · 7 files changed

+425/−0 lines, 7 filesdep +HUnitdep +QuickCheckdep +basesetup-changed

Dependencies added: HUnit, QuickCheck, base, cmdargs, directory, filemanip, filepath, hfsevents, json, process, test-framework, test-framework-hunit, test-framework-quickcheck2

Files

+ LICENSE view
@@ -0,0 +1,27 @@+Copyright 2012 Stefan Saasen++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions+are met:+1. Redistributions of source code must retain the above copyright+   notice, this list of conditions and the following disclaimer.+2. Redistributions in binary form must reproduce the above copyright+   notice, this list of conditions and the following disclaimer in the+   documentation and/or other materials provided with the distribution.+3. Neither the name of the author nor the names of his contributors+   may be used to endorse or promote products derived from this software+   without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF+SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,89 @@+Spy+===++Spy is a compact file system watcher for Mac OS X using the [File System Events API](https://developer.apple.com/library/mac/#documentation/Darwin/Conceptual/FSEvents_ProgGuide/Introduction/Introduction.html) via [hfsevents](https://github.com/luite/hfsevents).+++Usage+=====++Spy expects a single argument, the directory or a single file to watch. Spy currently supports two different modes: watch and run mode.++Watch mode+-----------++In output mode Spy will print the path to a modified file to STDOUT (followed by a newline) whenever a file modification (new file added, file modified, file deleted) occurs.++    $> spy watch .+    path/to/modified/file++Because watch is the default mode you can omit the `watch` command if you want:++    $> spy .++It's possible to watch a single file (this obviously only shows changes to that particular file):++    $> spy /path/to/file++The default format is the full path to the modified file followed by a newline. To make it easier to parse the output, the `--format=json` changes the output to be printed formatted as a JSON object (again followed by a newline).++    $> spy watch --format=json .+    {"path": "path/to/modified/file", "flags": ["ItemModified"], "id": 123143234}++For directories the following options apply:++An optional second argument can be used to filter the files in the given directory using a glob pattern:++    $> spy watch /path/to/directory "*.md"+++Run mode+--------++In run mode Spy will execute a given command whenever a file modification occurs without printing modifications to stdout. The command will be executed with the path to the modified file as the last argument.++    $> spy run "./run-build.sh"++In the example above the shell script `run-build.sh` would be executed with the path to the modified file as the first argument.++If the command to be executed does not expect any (additional) arguments the `--notify-only` flag can be used. This will cause spy to execute the command without passing the path as an argument:++    $> spy run --notify-only "rake test" .+++Installation+============++Spy only works on Mac OS X >= 10.7 (Lion and above)!++Binary distribution+-------------------++The binary distribution contains a 64bit binary compiled for Mac OS X > 10.7.++Download the tarball and run "make install" to copy the binary and the man page into the correct target directories:++    $> curl -OL https://bitbucket.org/ssaasen/spy/downloads/spy-osx-x86_64-v0.4.tar.gz+    $> tar xfz spy-osx-x86_64-v0.4.tar.gz+    $> cd spy+    $> make install++The user manual should now be available via `man spy` and the `spy` executable should be on your `$PATH`.+++Source distribution+-------------------++To install spy from source you need the Haskell platform installed and cabal-install available on your $PATH:++    $> cabal install --only-dependencies+    $> cabal configure+    $> cabal build++This will create the spy binary in the ./dist/build/spy directory.++To copy the spy binary to the cabal bin directory (which should be available on your PATH) use:++    $> cabal copy++
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple+main = defaultMain+
+ spy.cabal view
@@ -0,0 +1,62 @@+name:               spy+version:            0.4+license:            BSD3+license-file:       LICENSE+author:             Stefan Saasen+maintainer:         stefan@saasen.me+synopsis:           A compact file system watcher for Mac OS X+category:           Apple, Development+description:        Spy can be used to watch for file changes and to either report the modified files or run a command if files change. It can be used to trigger compilation, to run tests or start a deployment.+homepage:           https://bitbucket.org/ssaasen/spy+bug-reports:        https://bitbucket.org/ssaasen/spy/issues+cabal-version:      >= 1.8+build-type:         Simple+extra-source-files: README.md++source-repository   head+  type:             git+  location:         git@bitbucket.org:ssaasen/spy.git++flag                small_base+  description:      Choose the new, split-up base package.++test-suite spy-testsuite+  type:                     exitcode-stdio-1.0+  main-is:                  Tests.hs+  hs-source-dirs:           tests, src+  build-depends:            base < 5 && >= 3,+                            test-framework >= 0.3.3,+                            test-framework-quickcheck2 >= 0.2.9,+                            test-framework-hunit,+                            HUnit,+                            QuickCheck >= 2.4.0.1,+                            hfsevents >= 0.1.3,+                            cmdargs >= 0.10,+                            filepath >= 1.3,+                            filemanip >= 0.3.6.2,+                            process >= 1.1,+                            json >= 0.7,+                            directory >= 1.1++executable  spy+  main-is:          Main.hs+  other-modules:    Spy.Watcher+  build-depends:+                    base < 5 && >= 3,+                    hfsevents >= 0.1.3,+                    cmdargs >= 0.10,+                    filepath >= 1.3,+                    filemanip >= 0.3.6.2,+                    process >= 1.1,+                    json >= 0.7,+                    directory >= 1.1++  ghc-options:+                    -Wall+                    -rtsopts+                    -fno-warn-unused-do-bind+                    -fno-warn-missing-signatures++  hs-source-dirs:+                    src+
+ src/Main.hs view
@@ -0,0 +1,46 @@+{-# LANGUAGE DeriveDataTypeable #-}+module Main where++import System.Console.CmdArgs+import System.Directory (canonicalizePath)+import Spy.Watcher++version :: String+version = "spy v0.4, (C) Stefan Saasen"++hiddenOpts x = x &= help "Set to true if hidden files/directories should be included" &= name "i" &= typ  "BOOL"++watch :: Spy+watch = Watch+    {hidden     = hiddenOpts False+    ,dir        = "."               &= argPos 0  &= typ "FILE/DIR"+    ,format     = Just plainFormat  &= name "f"  &= help "Specify the output format ('json', 'plain')"+    ,glob       = Nothing           &= args &= typ "GLOB"+    }+    &= help "Watch a directory (or file) for file changes"+    &= auto++run :: Spy+run = Run+    {hidden     = hiddenOpts False+    ,command    = def           &= argPos 0                        &= typ "CMD"+    ,dir        = "."           &= argPos 1                        &= typ "FILE/DIR"+    ,glob       = Nothing       &= args                            &= typ "GLOB"+    ,notifyOnly = False         &= name "n"  &= name "notify-only" &= typ "BOOL"+    }+    &= help "Run a command whenever a file changes"+++mode :: Mode (CmdArgs Spy)+mode = cmdArgsMode $ modes [watch,run]+        &= help "spy watches for file changes on the file system"+        &= program "spy" &= summary version+++main :: IO ()+main = do+    config <- cmdArgsRun mode+    canonicalizedDir <- canonicalizePath $ dir config+    spy config { dir = canonicalizedDir }+    putStrLn "No eyes on the target"+
+ src/Spy/Watcher.hs view
@@ -0,0 +1,142 @@+{-# LANGUAGE DeriveDataTypeable,RecordWildCards #-}++module Spy.Watcher+(+ spy+,Format+,plainFormat+,Spy(..)+,skipEvent+,matchesFile+,containsHiddenPathElement+) where++import System.OSX.FSEvents+import System.Console.CmdArgs+import System.Cmd+import System.Exit+import System.IO (stderr, hPrint)+import System.FilePath.GlobPattern+import System.FilePath (splitDirectories, takeFileName)+import Control.Monad (unless)+import Control.Exception (bracket)+import Data.Maybe (fromMaybe, maybeToList)+import Data.Bits+import Data.Word+import Text.JSON++-- | The output format when Spy prints out changes to STDOUT+data Format = Json | Plain deriving (Show, Eq, Data, Typeable)++-- | Spy run modes.+data Spy = Watch {+     dir                :: FilePath+    ,glob               :: Maybe GlobPattern+    ,format             :: Maybe Format+    ,hidden             :: Bool+} | Run {+     dir                :: FilePath+    ,command            :: String+    ,glob               :: Maybe GlobPattern+    ,hidden             :: Bool+    ,notifyOnly         :: Bool+} deriving (Data,Typeable,Show,Eq)++-- | Return the Plain format.+plainFormat :: Format+plainFormat = Plain++-- | Register for FS events using the given Spy config.+spy :: Spy -> IO String+spy config = bracket+  (eventStreamCreate [dir config] 1.0 True True True $ handleEvent config)+  eventStreamDestroy+  (\_ -> getLine)++-- | Handle the FS event based on the current Spy run configuration+handleEvent :: Spy -> Event -> IO ()+handleEvent config@Run{..} event =+        unless (skipEvent config event) $+        runCommand command pathAsArg >>=+            \exit -> case exit of+                ExitSuccess     -> return ()+                ExitFailure i   -> hPrint stderr $ "Failed to execute " ++ command ++ " - exit code: " ++ show i+        where pathAsArg = if notifyOnly then+                            Nothing+                            else Just (eventPath event)++handleEvent config@Watch{..} event =+        unless (skipEvent config event) $+        putStrLn $ (outputHandler $ fromMaybe Plain format) event++-- =================================================================================++runCommand :: Command -> Maybe FilePath -> IO ExitCode+runCommand cmd maybePath = rawSystem p $ mergeArgs args'+    where (p, args')            = case words cmd of+                                    (x:xs) -> (x, xs)+                                    _      -> ("", [])+          mergeArgs defaultArgs = defaultArgs ++ maybeToList maybePath++-- =================================================================================++type Printer = (Event -> String)+type Command = String++outputHandler :: Format -> Printer+outputHandler Json  = \event -> encode $ makeObj [+    ("path", showJSON $ eventPath event),+    ("id", showJSON $ eventId event),+    ("flags", showJSONs $ showEventFlags $ eventFlags event)]+outputHandler Plain = eventPath+++-- | Skip events based on the configuration given+skipEvent :: Spy -> Event -> Bool+skipEvent config event = skipHidden || skipNonMatchingGlob+    where skipHidden            = let includeHiddenfiles = hidden config+                                  in not includeHiddenfiles && containsHiddenPathElement path+          skipNonMatchingGlob   = maybe False (not . matchesFile path) $ glob config+          path                  = eventPath event+++matchesFile :: FilePath -> GlobPattern -> Bool+matchesFile path glob' = takeFileName path ~~ glob'++containsHiddenPathElement :: FilePath -> Bool+containsHiddenPathElement path = any isHidden paths+                where paths = splitDirectories path+                      isHidden name' = case name' of+                                            (x:_)   -> x == '.'+                                            _       -> False++-- =================================================================================+-- The following code is taken from:+-- https://github.com/luite/hfsevents/blob/master/test/trace.hs+-- Copyright (c) 2012, Luite Stegeman+showEventFlags :: Word64 -> [String]+showEventFlags fl = map fst . filter hasFlag $ flagList+  where hasFlag (_,f) = fl .&. f /= 0+++flagList :: [(String, Word64)]+flagList = [ ("MustScanSubDirs"   , 0x00000001)+           , ("UserDropped"       , 0x00000002)+           , ("KernelDropped"     , 0x00000004)+           , ("EventIdsWrapped"   , 0x00000008)+           , ("HistoryDone"       , 0x00000010)+           , ("RootChanged"       , 0x00000020)+           , ("Mount"             , 0x00000040)+           , ("Unmount"           , 0x00000080)+           , ("ItemCreated"       , 0x00000100)+           , ("ItemRemoved"       , 0x00000200)+           , ("ItemInodeMetaMod"  , 0x00000400)+           , ("ItemRenamed"       , 0x00000800)+           , ("ItemModified"      , 0x00001000)+           , ("ItemFinderInfoMod" , 0x00002000)+           , ("ItemChangeOwner"   , 0x00004000)+           , ("ItemXattrMod"      , 0x00008000)+           , ("ItemIsFile"        , 0x00010000)+           , ("ItemIsDir"         , 0x00020000)+           , ("ItemIsSymlink"     , 0x00040000)+           ]
+ tests/Tests.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import qualified Test.HUnit as H+import Data.Maybe+import System.OSX.FSEvents+import Spy.Watcher+import Test.QuickCheck hiding ((.&.))+import Test.Framework (Test, defaultMain, testGroup)+import Test.Framework.Providers.QuickCheck2 (testProperty)+import Test.Framework.Providers.HUnit+++test_globMatchesFile = H.assertBool+  "*.hs should match a Haskell source file"+  (matchesFile "/path/to/Main.hs" "*.hs")++test_globDoesNotMatchFile = H.assertBool+  "*.hs should not match a C source file"+  (not $ matchesFile "/path/to/Main.c" "*.hs")++test_skipEventHidden = H.assertBool+  "Skip path if hidden directory and showing hidden files is not enabled"+  (skipEvent (mockWatch {hidden = False}) (mockEvent "/a/b/.git/refs"))++test_containsHiddenPathElement = H.assertBool+  "Should identify hidden directory"+  (containsHiddenPathElement "/a/b/.git/info/exclude")++-- ===========================================================+mockWatch :: Spy+mockWatch = Watch { dir = ".", glob = Nothing, format = Nothing, hidden = False }++mockEvent :: FilePath -> Event+mockEvent path = Event path 0x00000001 0x00000001+------------------------------------------------------------------------+-- Test harness++main :: IO ()+main = defaultMain tests++tests :: [Test]+tests =+    [ testGroup "Watcher"+      [+        testCase "watcher/shouldMatchFilePath" test_globMatchesFile,+        testCase "watcher/shouldNotMatchFilePath" test_globDoesNotMatchFile+      ],+      testGroup "Watcher opts"+      [+        testCase "watcher/optHidden" test_skipEventHidden,+        testCase "watcher/containsHiddenPathElement" test_containsHiddenPathElement+      ]+    ]+