Skip to content
This repository was archived by the owner on Nov 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions lib/cortex/file_watcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,50 @@ defmodule Cortex.FileWatcher do
use GenServer

@watched_dirs ["lib/", "test/", "apps/"]
@throttle_timeout_ms 100
@default_throttle_timeout_ms 100
@default_file_changed_receiver Controller

defmodule State do
defstruct [:watcher_pid, :file_events, :throttle_timer]
defstruct [
:watcher_pid,
:file_events,
:throttle_timer,
:throttle_timeout_ms,
:file_changed_receiver
]
end

##########################################
# Public API
##########################################

def start_link do
GenServer.start_link(__MODULE__, [])
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts)
end

##########################################
# GenServer Callbacks
##########################################

def init(_) do
def init(opts) do
throttle_timeout_ms = Keyword.get(opts, :throttle_timeout_ms, @default_throttle_timeout_ms)

file_changed_receiver =
Keyword.get(opts, :file_changed_receiver, @default_file_changed_receiver)

{:ok, watcher_pid} = FileSystem.start_link(dirs: watched_dirs())

FileSystem.subscribe(watcher_pid)

{:ok, %State{watcher_pid: watcher_pid, throttle_timer: nil, file_events: %{}}}
initial_state = %State{
watcher_pid: watcher_pid,
throttle_timer: nil,
throttle_timeout_ms: throttle_timeout_ms,
file_changed_receiver: file_changed_receiver,
file_events: %{}
}

{:ok, initial_state}
end

def handle_info(
Expand All @@ -53,10 +73,10 @@ defmodule Cortex.FileWatcher do
end

def handle_info(:throttle_timer_complete, state) do
%State{file_events: file_events} = state
%State{file_events: file_events, file_changed_receiver: file_changed_receiver} = state

Enum.each(file_events, fn {path, file_type} ->
GenServer.cast(Controller, {:file_changed, file_type, path})
GenServer.cast(file_changed_receiver, {:file_changed, file_type, path})
end)

{:noreply, %State{state | file_events: %{}, throttle_timer: nil}}
Expand All @@ -73,7 +93,8 @@ defmodule Cortex.FileWatcher do
##########################################

defp maybe_update_throttle_timer(%State{throttle_timer: nil} = state) do
throttle_timer = Process.send_after(self(), :throttle_timer_complete, @throttle_timeout_ms)
%State{throttle_timeout_ms: throttle_timeout_ms} = state
throttle_timer = Process.send_after(self(), :throttle_timer_complete, throttle_timeout_ms)
%State{state | throttle_timer: throttle_timer}
end

Expand Down
58 changes: 58 additions & 0 deletions test/cortex/file_watcher_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,67 @@ defmodule Cortex.FileWatcherTest do

alias Cortex.FileWatcher

@throttle_timeout_ms 10

test "file_type" do
assert FileWatcher.file_type("test/some_file.exs") == :test
assert FileWatcher.file_type("lib/some_file.ex") == :lib
assert FileWatcher.file_type("lib/.#some_file.ex") == :unknown
end

test "with one file event received, file changed is sent once" do
{watcher, watcher_pid} = start_file_watcher()

path = "test/some_file.exs"

send_file_event(watcher, watcher_pid, path, [:created, :modified])

assert_receive {:"$gen_cast", {:file_changed, :test, path}}
refute_receive {:"$gen_cast", {:file_changed, :lib, _}}
end

test "with one path event multiple times, file changed is sent once" do
{watcher, watcher_pid} = start_file_watcher()

path = "lib/some_file.ex"

send_file_event(watcher, watcher_pid, path, [:inodemetamod, :modified])
send_file_event(watcher, watcher_pid, path, [:inodemetamod, :modified])
send_file_event(watcher, watcher_pid, path, [:inodemetamod, :modified])

assert_receive {:"$gen_cast", {:file_changed, :lib, path}}
refute_receive {:"$gen_cast", {:file_changed, :lib, _}}
end

test "with multiple paths events sent multiple times, file changed is sent once per path" do
{watcher, watcher_pid} = start_file_watcher()

path1 = "lib/some_file.ex"
path2 = "lib/another_file.ex"

send_file_event(watcher, watcher_pid, path1, [:inodemetamod, :modified])
send_file_event(watcher, watcher_pid, path1, [:inodemetamod, :modified])
Process.sleep(3)
send_file_event(watcher, watcher_pid, path2, [:created, :modified])
send_file_event(watcher, watcher_pid, path1, [:inodemetamod, :modified])

assert_receive {:"$gen_cast", {:file_changed, :lib, path1}}
assert_receive {:"$gen_cast", {:file_changed, :lib, path2}}
refute_receive {:"$gen_cast", {:file_changed, :lib, _}}
end

defp start_file_watcher do
watcher =
start_supervised!(
{FileWatcher, file_changed_receiver: self(), throttle_timeout_ms: @throttle_timeout_ms}
)

%FileWatcher.State{watcher_pid: watcher_pid} = :sys.get_state(watcher)
{watcher, watcher_pid}
end

defp send_file_event(watcher, watcher_pid, path, events) do
message = {:file_event, watcher_pid, {path, events}}
send(watcher, message)
end
end