fswait-1.0.0: src/Main.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Main where
import Control.Applicative (empty, (<|>))
import Control.Concurrent (threadDelay)
import qualified Control.Concurrent.STM as STM
import qualified Control.Concurrent.STM.TMVar as TMVar
import Control.Monad (when)
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import qualified Data.Text as Text
import qualified Data.Time.Units as Time.Units
import qualified Filesystem.Path as Path
import qualified Filesystem.Path.CurrentOS as Path
import qualified Options.Applicative as Options
import Options.Generic
import System.INotify as INotify
import qualified System.Timeout
import Turtle (ExitCode (..), fp, s, void, (%))
import qualified Turtle
data Options w = Options
{ timeout :: w ::: Maybe Time.Units.Second <?> "Window to observe a filesystem event (default: 120s, negative values wait indefinitely)"
, path :: w ::: Path.FilePath <?> "Observe filesystem events for path"
, exists :: w ::: Bool <?> "Return immediately if the filepath already exists"
, events :: w ::: NonEmpty EventVariety <?> "Observable event"
} deriving (Generic)
instance ParseRecord (Options Wrapped)
deriving instance Show (Options Unwrapped)
instance ParseRecord Time.Units.Second where
parseRecord = fmap getOnly parseRecord
instance ParseFields Time.Units.Second
instance ParseField Time.Units.Second where
parseField h n =
fmap (Time.Units.fromMicroseconds . (*μ))
(Options.option Options.auto $
( Options.metavar "Seconds"
<> foldMap (Options.long . Text.unpack) n
<> foldMap (Options.help . Text.unpack) h
)
)
deriving instance Show EventVariety
instance ParseRecord EventVariety where
parseRecord = fmap getOnly parseRecord
instance ParseFields EventVariety
instance ParseField EventVariety where
parseField _ _ =
Options.flag' Access (Options.long "access")
<|> Options.flag' Modify (Options.long "modify")
<|> Options.flag' Attrib (Options.long "attrib")
<|> Options.flag' Close (Options.long "close")
<|> Options.flag' CloseWrite (Options.long "closeWrite")
<|> Options.flag' CloseNoWrite (Options.long "closeNoWrite")
<|> Options.flag' Open (Options.long "open")
<|> Options.flag' Move (Options.long "move")
<|> Options.flag' MoveIn (Options.long "moveIn")
<|> Options.flag' MoveOut (Options.long "moveOut")
<|> Options.flag' MoveSelf (Options.long "moveSelf")
<|> Options.flag' Create (Options.long "create")
<|> Options.flag' Delete (Options.long "delete")
<|> Options.flag' OnlyDir (Options.long "onlyDir")
<|> Options.flag' NoSymlink (Options.long "noSymlink")
<|> Options.flag' MaskAdd (Options.long "maskAdd")
<|> Options.flag' OneShot (Options.long "oneShot")
<|> Options.flag' AllEvents (Options.long "all")
μ :: Integer
μ = 10 ^ (6 :: Integer)
main :: IO ()
main = do
Options{..} <- unwrapRecord "Wait and observe events on the filesystem for a path, with a timeout"
when exists $ do
pathExists <- Turtle.testfile path
when pathExists $ do
Turtle.err $ Turtle.format ("exists: "%fp) path
Turtle.exit ExitSuccess
mvar <- STM.atomically TMVar.newEmptyTMVar
let eventSet = NonEmpty.toList (NonEmpty.nub events)
let watchdir = Path.encodeString (Path.directory path)
let watchfile = Path.encodeString (Path.filename path)
let timeout' = fromMaybe (Time.Units.fromMicroseconds (120 * μ)) timeout
let eventsStr = Text.unwords $ fmap (Text.pack . show) eventSet
let eventObservation =
STM.atomically (TMVar.tryTakeTMVar mvar) >>= \case
Nothing -> threadDelay 500000 >> eventObservation
Just ev -> pure ev
let writeEvent = STM.atomically . TMVar.tryPutTMVar mvar
let fileEvent f e | f == watchfile = void (writeEvent e)
| otherwise = empty
let maybeFileEvent f e =
case f of
Nothing -> empty
Just filepath
| filepath == watchfile
-> void (writeEvent e)
| otherwise
-> empty
Turtle.err $ Turtle.format ("observing "%s%" for "%fp) eventsStr path
let timeoutWindow | timeout' > 0 = Text.pack (show timeout')
| otherwise = "indefinite"
Turtle.err $ Turtle.format ("the window for an observation is "%s) timeoutWindow
let doWatch =
withINotify (\i -> do
wid <- addWatch i eventSet watchdir $ \case
ev@Accessed{..} -> maybeFileEvent maybeFilePath ev
ev@Attributes{..} -> maybeFileEvent maybeFilePath ev
ev@Closed{..} -> maybeFileEvent maybeFilePath ev
ev@Created{..} -> fileEvent filePath ev
ev@Deleted{..} -> fileEvent filePath ev
ev@Modified{..} -> maybeFileEvent maybeFilePath ev
ev@MovedIn{..} -> fileEvent filePath ev
ev@MovedOut{..} -> fileEvent filePath ev
ev@Opened{..} -> maybeFileEvent maybeFilePath ev
ev -> void (writeEvent ev)
ev <- eventObservation
removeWatch wid
pure ev)
System.Timeout.timeout
(fromIntegral $ Time.Units.toMicroseconds timeout')
doWatch >>= \case
Nothing -> Turtle.exit (ExitFailure 1)
Just ev -> Turtle.echo (Text.pack $ show ev)