Packages

A simple file tail functionality. Calls a callback function whenever new lines are detected on a file.

Current section

Files

Jump to
tail lib tail.ex
Raw

lib/tail.ex

require Logger
defmodule Tail do
@moduledoc """
Tail implements a simple file tail functionality.
Given a file, a function, and an interval, Tail will execute the function with a list of new lines found
in the file and continue checking for additional lines on the interval.
## Usage
{:ok, pid} = Tail.start_link("test.txt", &IO.inspect(&1), 1000)
Tail.stop(pid)
"""
use GenServer
@type state :: {File.Stream.t(), ([String.t()] -> nil), integer, term, integer, integer}
@doc """
Public interface. Starts a Tail Genserver for the given file, function, and interval (in ms)
"""
@spec start_link(String.t(), ([String.t()] -> nil), integer) :: GenServer.on_start()
def start_link(file, fun, interval \\ 1000) do
GenServer.start_link(__MODULE__, {file, fun, interval})
end
@doc """
Public interface. Sends a call to kill the GenServer
"""
@spec stop(pid) :: :ok
def stop(pid) do
GenServer.call(pid, :kill)
end
@doc """
Init callback. Starts the check loop by casting :check to self and then returns the initial state
"""
@spec init({String.t(), ([String.t()] -> nil), integer}) :: {:ok, state}
def init({file, fun, interval}) do
stream = File.stream!(file)
GenServer.cast(self(), :check)
{:ok, {stream, fun, interval, nil, 0, 0}}
end
@doc """
Main loop. Calls check_for_lines, sleeps, then continues the loop by casting :check to self
and returning with the (possibly updated) last_modified and position
"""
@spec handle_cast(:check, state) :: {:noreply, state}
def handle_cast(:check, state = {_stream, _fun, interval, _last_modified, _position, _size}) do
state = check_for_lines(state)
:timer.sleep(interval)
GenServer.cast(self(), :check)
{:noreply, state}
end
@doc """
Handles :kill call. Checks for any final lines before stopping the genserver
"""
@spec handle_call(:kill, {pid, term}, state) :: {:stop, :normal, :ok, state}
def handle_call(:kill, _from, state) do
state = check_for_lines(state)
{:stop, :normal, :ok, state}
end
# Implementation of line checking. If the file doesn't exist, it simply returns the current state, assuming the
# file will appear eventually. If the file hasn't been modified since last time, it also returns the current state.
# If the file has been modified, Stream.drop(position) skips lines previously read, then Enum.each gathers the new lines.
# Returns the new last_modified and position.
@spec check_for_lines(state) :: state
defp check_for_lines(state = {stream, fun, interval, last_modified, position, size}) do
with {:exists, true} <- {:exists, File.exists?(stream.path)},
{:ok, stat} <- File.stat(stream.path),
{:mtime, true} <- {:mtime, stat.mtime != last_modified},
{:size, true} <- {:size, stat.size >= size} do
lines =
stream
|> Stream.drop(position)
|> Enum.into([])
if length(lines) > 0 do
fun.(lines)
end
{stream, fun, interval, stat.mtime, position + length(lines), stat.size}
else
{:exists, false} -> {File.stream!(stream.path), fun, interval, last_modified, 0, 0}
{:error, _} -> {File.stream!(stream.path), fun, interval, last_modified, 0, 0}
{:size, false} -> {File.stream!(stream.path), fun, interval, last_modified, 0, 0}
{:mtime, false} -> state
end
end
end