steeloverseer 0.2.0.0 → 0.5.0.0
raw patch · 3 files changed
+113/−68 lines, 3 filesdep +daemonsdep +unixdep −pipesdep ~text
Dependencies added: daemons, unix
Dependencies removed: pipes
Dependency ranges changed: text
Files
- src/Main.hs +41/−18
- src/SOS.hs +61/−41
- steeloverseer.cabal +11/−9
src/Main.hs view
@@ -3,6 +3,7 @@ import System.Environment (getArgs) import System.Console.GetOpt +import System.Posix.Daemon import Data.List import Control.Monad import Filesystem.Path.CurrentOS hiding (concat, null)@@ -20,47 +21,69 @@ data Options = Options { optShowVersion :: Bool , optCommands :: [String]- , optPatterns :: [String]+ , optPatterns :: [String] , optDirectory :: FilePath+ , optIsDaemon :: Bool+ , optDaemonCmd :: String } deriving (Show, Eq) defaultOptions :: Options defaultOptions = Options { optShowVersion = False- , optCommands = []- , optPatterns = []- , optDirectory = fromText $ T.pack "."+ , optCommands = []+ , optPatterns = []+ , optDirectory = fromText $ T.pack "."+ , optIsDaemon = False+ , optDaemonCmd = "start" } options :: [OptDescr (Options -> Options)] options = [ Option "v" ["version"] (NoArg (\opts -> opts { optShowVersion = True }))- "show version info"+ "Show version info." , Option "c" ["command"]- (ReqArg (\c opts -> opts { optCommands = optCommands opts ++ [c] }) "COMMAND")- "add command to run on file event"+ (ReqArg (\c opts -> opts { optCommands = optCommands opts ++ [c] }) "command")+ "Add command to run on file event." , Option "p" ["pattern"]- (ReqArg (\e opts -> opts { optPatterns = optPatterns opts ++ [e] }) "PATTERN")- "add pattern to match on file path" + (ReqArg (\e opts -> opts { optPatterns = optPatterns opts ++ [e] }) "pattern")+ "Add pattern to match on file path." , Option "d" ["directory"]- (ReqArg (\d opts -> opts { optDirectory = decodeString d }) "DIRECTORY")- "set directory to watch for changes (default is ./)"+ (ReqArg (\d opts -> opts { optDirectory = decodeString d }) "directory")+ "Set directory to watch for changes (default is ./)."+ , Option "b" ["daemon"]+ (ReqArg (\s opts -> opts { optIsDaemon = True, optDaemonCmd = s }) "start|stop")+ "Attempt to run in the background as a daemon. When a previous daemon is running from your working directory this option will kill that process." ] header :: String-header = "Usage: sos [v] -c command -p pattern"+header = "Usage: sos [vb] -c command -p pattern" version :: String-version = intercalate "\n" [ "\nSteel Overseer 0.2.0.0"- , " by Schell Scivally" - , ""- ]+version = "\nSteel Overseer 1.0.0.0\n" startWithOpts :: Options -> IO () startWithOpts opts = do let Options{..} = opts haveOptions = not (null optPatterns) patternsValid = and $ fmap (not . null) optPatterns+ daemonValid = optDaemonCmd == "start" || optDaemonCmd == "stop" when optShowVersion $ putStrLn version unless patternsValid $ error "One or more patterns are empty."- when (haveOptions && patternsValid) $ steelOverseer optDirectory optCommands optPatterns - + unless daemonValid $ error "The argument to -b,--daemon must be one of start or stop."+ didKill <- if optIsDaemon && optDaemonCmd == "stop"+ then do shouldKillPrev <- isRunning pIdFile+ if shouldKillPrev+ then do putStrLn "Killing previous steeloverseer daemon..."+ killAndWait pIdFile + putStrLn "Done."+ else putStrLn "There is no previous daemon to kill."+ return shouldKillPrev+ else return False+ let runSteelOverseer = steelOverseer optDirectory optCommands optPatterns optIsDaemon+ when (haveOptions && patternsValid && daemonValid) $ + if not optIsDaemon + then runSteelOverseer + else unless didKill $ do+ putStrLn "Running in the background as a daemon, logging output to sos.log."+ runDetached (Just pIdFile) (ToFile logFile) runSteelOverseer++
src/SOS.hs view
@@ -1,49 +1,68 @@+{-# LANGUAGE OverloadedStrings #-} module SOS where -import ANSIColors -import System.FSNotify-import System.Process-import System.Exit-import Control.Monad-import Control.Concurrent-import Data.Maybe-import Text.Regex.TDFA--import Filesystem.Path.CurrentOS as OS-import Data.Text as T-import Data.List as L--import System.IO ( Handle )--import Prelude hiding ( FilePath )--type RunningProcess = IO (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)+import ANSIColors+import System.FSNotify+import System.Process+import System.Exit+import System.Posix.Signals+import System.Posix.Daemon+import Control.Monad+import Control.Concurrent+import Data.Maybe+import Text.Regex.TDFA+import Filesystem.Path.CurrentOS as OS+import Prelude hiding ( FilePath )+import qualified Data.Text as T+import qualified Prelude --- | A tuple to hold our changed file events, +-- | A structure to hold our changed file events, -- list of commands to run and possibly the currently running process, -- in case it's one that hasn't terminated.-type SOSState = ([Event], [String], Maybe ProcessHandle)+data SOSState = SOSIdle + | SOSPending { accumulatedEvents :: [Event] }+ | SOSRunning { runningProccess :: ProcessHandle+ , pendingCommands :: [String]+ } -steelOverseer :: FilePath -> [String] -> [String] -> IO ()-steelOverseer dir cmds exts = do- putStrLn "Hit enter to quit.\n" +pIdFile :: Prelude.FilePath+pIdFile = "sos.pid"++logFile :: Prelude.FilePath+logFile = "sos.log"++steelOverseer :: FilePath -> [String] -> [String] -> Bool -> IO ()+steelOverseer dir cmds exts isDaemon = do+ unless isDaemon $ putStrLn "Hit enter to quit.\n" wm <- startManager mvar <- newEmptyMVar+ let predicate = actionPredicateForRegexes exts- action = performCommand mvar cmds in- watchTree wm dir predicate action+ action = performCommand mvar cmds+ watchTree wm dir predicate action - _ <- getLine+ unless isDaemon $ do + _ <- getLine+ cleanup wm mvar+ + when isDaemon $ do+ -- Install a sigkill handler to cleanup when the daemon is killed.+ _ <- installHandler sigQUIT (Catch $ cleanup wm mvar) Nothing + forever $ threadDelay 1000000 +cleanup :: WatchManager -> MVar SOSState -> IO ()+cleanup wm mvar = do+ putStrLn "Cleaning up." mState <- tryTakeMVar mvar- case mState of- Just (_, _, Just pid) -> terminatePID pid+ Just (SOSRunning pid _) -> terminatePID pid _ -> return ()- stopManager wm + isDaemon <- isRunning pIdFile + when isDaemon $ brutalKill pIdFile+ actionPredicateForRegexes :: [String] -> Event -> Bool actionPredicateForRegexes ptns event = or (fmap (filepath =~) ptns :: [Bool]) where filepath = case toText $ eventPath event of@@ -59,27 +78,27 @@ performCommand :: MVar SOSState -> [String] -> Event -> IO () performCommand mvar cmds event = do cyanPrint event- mEvProc <- tryTakeMVar mvar- eventsAndProcess <- case mEvProc of+ mSosState <- tryTakeMVar mvar+ sosState <- case mSosState of Nothing -> do -- This is the first file to change and there is no running process. startWriteProcess mvar cmds 1000000- return ([event], cmds, Nothing)+ return $ SOSPending [event] - Just ([], _, Just pid) -> do+ Just (SOSRunning pid _) -> do -- There is a hanging process. mExCode <- getProcessExitCode pid when (isNothing mExCode) $ terminatePID pid startWriteProcess mvar cmds 1000000- return ([event], cmds, Nothing)+ return $ SOSPending [event] - Just (events, leftoverCmds, Nothing) -> + Just (SOSPending events) -> -- SOS is waiting the 1sec delay for events before running the process. - return (event:events, leftoverCmds, Nothing)+ return $ SOSPending $ event:events - Just enp -> return enp+ Just state -> return state - putMVar mvar eventsAndProcess+ putMVar mvar sosState startWriteProcess :: MVar SOSState -> [String] -> Int -> IO () startWriteProcess mvar [] _ = do@@ -92,10 +111,11 @@ pId <- runCommand cmd mEvProcCurrent <- tryTakeMVar mvar case mEvProcCurrent of- Nothing -> putMVar mvar ([], cmds, Just pId)+ -- This shouldn't ever happen.+ Nothing -> putMVar mvar $ SOSRunning pId cmds - Just (_, _, _) -> void $ forkIO $ do- putMVar mvar ([], cmds, Just pId)+ Just _ -> void $ forkIO $ do+ putMVar mvar $ SOSRunning pId cmds exitCode <- waitForProcess pId case exitCode of ExitSuccess -> do
steeloverseer.cabal view
@@ -1,21 +1,21 @@--- Initial steeloverseer.cabal generated by cabal init. For further +-- Initial steeloverseer.cabal generated by cabal init. For further -- documentation, see http://haskell.org/cabal/users-guide/ name: steeloverseer-version: 0.2.0.0-synopsis: A file watcher. -description: A command line tool that responds to filesystem events. Users can provide regular expressions to match on filepaths and shell commands to execute in serial when matches are found. Displays text using a subset of available primary colors.+version: 0.5.0.0+synopsis: A file watcher.+description: A command line tool that responds to filesystem events. Allows the user to automatically execute commands after files are added or updated. Watches files using regular expressions. Can be invoked interactively or as a daemon. license: BSD3 license-file: LICENSE author: Schell Scivally maintainer: efsubenovex@gmail.com-stability: alpha+stability: stable bug-reports: https://github.com/schell/steeloverseer/issues homepage: https://github.com/schell/steeloverseer category: Development build-type: Simple cabal-version: >=1.8--- source+ source-repository head type: git location: git://github.com/schell/steeloverseer.git@@ -31,7 +31,9 @@ process >= 1.1.0.2, text >=0.11.2.3, time >=1.4,- pipes >=3.2, stm >=2.4,- regex-tdfa >=1.1.8- + regex-tdfa >=1.1.8,+ daemons >=0.2.1,+ unix >=2.6.0.1++