tail 0.1.0.0 → 0.2.0.0
raw patch · 5 files changed
+164/−128 lines, 5 filesnew-component:exe:tail-test
Files
- LICENSE +21/−0
- src/System/Tail.hs +19/−9
- tail.cabal +111/−94
- test-fixture.txt +5/−0
- test/System/TailSpec.hs +8/−25
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2025 Sridhar Ratnakumar++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.
src/System/Tail.hs view
@@ -1,11 +1,12 @@ {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-} {- | File tailing library with multi-subscriber support. This library provides a Haskell API for @tail -f@ style file streaming-using 'STM', 'Control.Concurrent.Async', and system processes.+using 'Control.Concurrent.STM', 'Control.Concurrent.Async', and system processes. Example usage: @@ -14,10 +15,10 @@ main :: IO () main = do- tail <- 'tailFile' \"\/var\/log\/app.log\"- subscriber <- 'tailSubscribe' tail+ tail <- tailFile 100 \"\/var\/log\/app.log\"+ subscriber <- tailSubscribe tail -- Read from subscriber...- 'tailStop' tail+ tailStop tail @ -} module System.Tail (@@ -37,8 +38,9 @@ import System.Directory (doesFileExist) import System.IO (hGetLine) import System.Process (CreateProcess (..), ProcessHandle, StdStream (..), createProcess, proc, terminateProcess, waitForProcess)+import System.Which (staticWhich) --- | Represent `tail -f`'ing a file in Haskell+-- | Represent running @tail -f@ on a file in Haskell data Tail = Tail { filePath :: FilePath -- ^ The file being tailed@@ -55,9 +57,17 @@ {- | Create a new 'Tail' handle for the given file path with specified buffer size. -The tail process starts immediately and begins reading from the file.+The @tail@ process starts immediately and begins reading from the file. New subscribers will receive a ring buffer containing the last @bufferSize@ lines. -}++{- | Path to the @tail@ executable++This should be available in the PATH, thanks to Nix and 'System.Which.staticWhich'.+-}+tailBin :: FilePath+tailBin = $(staticWhich "tail")+ tailFile :: (HasCallStack) => Int -> FilePath -> IO Tail tailFile bufferSize filePath = do unlessM (doesFileExist filePath) $ error $ "File does not exist: " <> toText filePath@@ -70,7 +80,7 @@ void $ async $ tailRun t pure t -{- | Signal the tail process to stop reading the file.+{- | Signal the 'Tail' process to stop reading the file. This will terminate the underlying @tail@ process and close all subscriber queues. -}@@ -81,7 +91,7 @@ tailRun :: Tail -> IO () tailRun t = do -- Start the tail -F process (show entire file from beginning)- let createProc = (proc "tail" ["-F", "-n", "+1", t.filePath]) {std_out = CreatePipe}+ let createProc = (proc tailBin ["-F", "-n", "+1", t.filePath]) {std_out = CreatePipe} (_, Just hout, _, ph) <- createProcess createProc -- Start async reader that reads from tail process and distributes to queues@@ -125,7 +135,7 @@ readLines readLines -{- | Subscribe to tail output and receive a 'CircularBuffer' for reading lines.+{- | Subscribe to 'Tail' output and receive a 'CircularBuffer' for reading lines. The returned buffer will contain any previously read lines from the ring buffer, plus all new lines as they are read from the file.
tail.cabal view
@@ -1,130 +1,147 @@ cabal-version: 2.0 --- This file has been generated from package.yaml by hpack version 0.37.0.+-- This file has been generated from package.yaml by hpack version 0.38.3. -- -- see: https://github.com/sol/hpack -name: tail-version: 0.1.0.0-synopsis: Haskell API for tail -f streaming-description: A Haskell library providing an API for tail -f style file streaming using STM, process, and async.-category: System-homepage: https://github.com/juspay/vira-author: Sridhar Ratnakumar-maintainer: srid@srid.ca-copyright: 2025 Sridhar Ratnakumar-license: MIT-build-type: Simple+name: tail+version: 0.2.0.0+synopsis: Haskell API for tail -f streaming+description:+ A Haskell library providing an API for tail -f style file streaming using STM, process, and async. +category: System+homepage: https://github.com/srid/tail-hs+author: Sridhar Ratnakumar+maintainer: srid@srid.ca+copyright: 2025 Sridhar Ratnakumar+license: MIT+license-file: LICENSE+build-type: Simple+data-files: test-fixture.txt+ library exposed-modules:- Control.Concurrent.STM.CircularBuffer- System.Tail- other-modules:- Paths_tail- autogen-modules:- Paths_tail- hs-source-dirs:- src+ Control.Concurrent.STM.CircularBuffer+ System.Tail++ other-modules: Paths_tail+ autogen-modules: Paths_tail+ hs-source-dirs: src default-extensions:- DataKinds- DeriveDataTypeable- DeriveGeneric- DeriveTraversable- DerivingStrategies- DerivingVia- ExplicitForAll- FlexibleContexts- FlexibleInstances- GeneralizedNewtypeDeriving- ImportQualifiedPost- LambdaCase- MultiParamTypeClasses- MultiWayIf- NamedFieldPuns- NoStarIsType- NumericUnderscores- OverloadedStrings- ScopedTypeVariables- StrictData- TypeApplications- TypeFamilies- TypeOperators- TypeSynonymInstances- ViewPatterns- ghc-options: -Wall -optP-Wno-nonportable-include-path -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-deriving-strategies -Wunused-foralls -fprint-explicit-foralls -fprint-explicit-kinds+ DataKinds+ DeriveDataTypeable+ DeriveGeneric+ DeriveTraversable+ DerivingStrategies+ DerivingVia+ ExplicitForAll+ FlexibleContexts+ FlexibleInstances+ GeneralizedNewtypeDeriving+ ImportQualifiedPost+ LambdaCase+ MultiParamTypeClasses+ MultiWayIf+ NamedFieldPuns+ NoStarIsType+ NumericUnderscores+ OverloadedStrings+ ScopedTypeVariables+ StrictData+ TypeApplications+ TypeFamilies+ TypeOperators+ TypeSynonymInstances+ ViewPatterns++ ghc-options:+ -Wall -optP-Wno-nonportable-include-path+ -Wincomplete-record-updates -Wincomplete-uni-patterns+ -Wmissing-deriving-strategies -Wunused-foralls+ -fprint-explicit-foralls -fprint-explicit-kinds+ build-depends: async- , base ==4.*+ , base >=4 && <5 , directory , filepath , process- , relude >=1.0+ , relude >=1.0 , stm , stm-chans , text , which+ mixins:- base hiding (Prelude)- , relude (Relude as Prelude, Relude.Container.One)- , relude - default-language: GHC2021+ base hiding (Prelude),+ relude (Relude as Prelude, Relude.Container.One),+ relude -test-suite tail-test- type: exitcode-stdio-1.0- main-is: Spec.hs+ default-language: GHC2021++executable tail-test+ main-is: Spec.hs other-modules:- Control.Concurrent.STM.CircularBufferSpec- System.TailSpec- Paths_tail- autogen-modules:- Paths_tail- hs-source-dirs:- test+ Control.Concurrent.STM.CircularBufferSpec+ Paths_tail+ System.TailSpec++ autogen-modules: Paths_tail+ hs-source-dirs: test default-extensions:- DataKinds- DeriveDataTypeable- DeriveGeneric- DeriveTraversable- DerivingStrategies- DerivingVia- ExplicitForAll- FlexibleContexts- FlexibleInstances- GeneralizedNewtypeDeriving- ImportQualifiedPost- LambdaCase- MultiParamTypeClasses- MultiWayIf- NamedFieldPuns- NoStarIsType- NumericUnderscores- OverloadedStrings- ScopedTypeVariables- StrictData- TypeApplications- TypeFamilies- TypeOperators- TypeSynonymInstances- ViewPatterns- ghc-options: -Wall -optP-Wno-nonportable-include-path -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-deriving-strategies -Wunused-foralls -fprint-explicit-foralls -fprint-explicit-kinds+ DataKinds+ DeriveDataTypeable+ DeriveGeneric+ DeriveTraversable+ DerivingStrategies+ DerivingVia+ ExplicitForAll+ FlexibleContexts+ FlexibleInstances+ GeneralizedNewtypeDeriving+ ImportQualifiedPost+ LambdaCase+ MultiParamTypeClasses+ MultiWayIf+ NamedFieldPuns+ NoStarIsType+ NumericUnderscores+ OverloadedStrings+ ScopedTypeVariables+ StrictData+ TypeApplications+ TypeFamilies+ TypeOperators+ TypeSynonymInstances+ ViewPatterns++ ghc-options:+ -Wall -optP-Wno-nonportable-include-path+ -Wincomplete-record-updates -Wincomplete-uni-patterns+ -Wmissing-deriving-strategies -Wunused-foralls+ -fprint-explicit-foralls -fprint-explicit-kinds -threaded -rtsopts+ -with-rtsopts=-N+ build-depends: async- , base ==4.*+ , base >=4 && <5 , directory , filepath , hspec , hspec-discover , process- , relude >=1.0+ , relude >=1.0 , stm , stm-chans , tail , temporary , text , which+ mixins:- base hiding (Prelude)- , relude (Relude as Prelude, Relude.Container.One)- , relude - default-language: GHC2021+ base hiding (Prelude),+ relude (Relude as Prelude, Relude.Container.One),+ relude++ default-language: GHC2021
+ test-fixture.txt view
@@ -0,0 +1,5 @@+Line 1: This is a test fixture file+Line 2: Used for testing tail functionality+Line 3: Contains multiple lines+Line 4: For comprehensive testing+Line 5: End of test fixture
test/System/TailSpec.hs view
@@ -4,7 +4,7 @@ import Control.Concurrent.STM.CircularBuffer (CircularBuffer) import Control.Concurrent.STM.CircularBuffer qualified as CB import GHC.IO.Handle (hClose)-import System.Directory (doesFileExist)+import Paths_tail (getDataFileName) import System.IO.Temp (withSystemTempFile) import System.Process (system) import System.Tail qualified as Tail@@ -26,16 +26,15 @@ t <- Tail.tailFile 100 "/dev/null" Tail.tailStop t it "streams a static file" $ do- thisFile <- findThisFile- t <- Tail.tailFile 100 thisFile+ testFile <- getDataFileName "test-fixture.txt"+ t <- Tail.tailFile 100 testFile q <- Tail.tailSubscribe t- threadDelay 1_000_000 -- Wait 1 second for tail to start and read content- Tail.tailStop t+ threadDelay 1_000_000 >> Tail.tailStop t ls <- drainAll q- viaNonEmpty head ls `shouldBe` Just "module System.TailSpec where"+ viaNonEmpty head ls `shouldBe` Just "Line 1: This is a test fixture file" -- Find the last line from the lines we got let lastLine = viaNonEmpty last ls- lastLine `shouldBe` Just "-- End of file."+ lastLine `shouldBe` Just "Line 5: End of test fixture" it "streams a log file being appended to by another process" $ do -- Create a file under a temp directory. Then spawn an external process that writes to it lines over time. withSystemTempFile "tail-spec" $ \tempFile h -> do@@ -44,11 +43,9 @@ t <- Tail.tailFile 100 tempFile q <- Tail.tailSubscribe t void $ forkIO $ do- threadDelay 1_000_000 void $ system $ "echo 'World' >> " <> tempFile- Tail.tailStop t+ threadDelay 1_000_000 >> Tail.tailStop t -- Read everything and compare output- threadDelay 3_000_000 ls <- drainAll q ls `shouldBe` ["Hello", "World"] it "ring buffer provides last lines to new subscribers" $ do@@ -56,24 +53,10 @@ hClose h void $ system $ "echo 'Line1' >> " <> tempFile t <- Tail.tailFile 100 tempFile- threadDelay 500_000 -- Let tail read existing lines q <- Tail.tailSubscribe t void $ system $ "echo 'Line2' >> " <> tempFile -- Write *after* subscribing- Tail.tailStop t+ threadDelay 1_000_000 >> Tail.tailStop t ls <- drainAll q ls `shouldBe` ["Line1", "Line2"]---- | Find this test file by trying common paths-findThisFile :: IO FilePath-findThisFile = do- let paths =- [ "test/System/TailSpec.hs" -- cabal test / nix build- , "packages/tail/test/System/TailSpec.hs" -- ghcid from root- ]- let findFirst [] = error "TailSpec.hs not found"- findFirst (p : ps) = do- exists <- doesFileExist p- if exists then pure p else findFirst ps- findFirst paths -- End of file.