defmodule Towerops.Profiles.ProfileWatcher do @moduledoc """ Watches YAML profile files for changes and automatically reloads them. Uses FileSystem to monitor the priv/profiles directory for file changes. When a .yaml or .yml file is created, modified, or deleted, triggers a reload of all profiles via YamlProfiles.reload/0. Note: Only available in development mode. FileSystem is a dev-only dependency. """ use GenServer alias Towerops.Profiles.YamlProfiles require Logger @profiles_dir "priv/profiles" def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @impl true def init(_opts) do # Only start file watcher in development # FileSystem is a dev-only dependency if Code.ensure_loaded?(FileSystem) do # Get the absolute path to the profiles directory profiles_path = Path.join(Application.app_dir(:towerops), @profiles_dir) # Start watching the profiles directory for changes (using apply to avoid compile-time warnings) case apply(FileSystem, :start_link, [[dirs: [profiles_path]]]) do {:ok, watcher_pid} -> apply(FileSystem, :subscribe, [watcher_pid]) Logger.info("ProfileWatcher: Started watching #{profiles_path}") {:ok, %{watcher_pid: watcher_pid, profiles_path: profiles_path}} :ignore -> # inotify-tools not available (e.g., in CI) Logger.debug("ProfileWatcher: FileSystem watcher not available, running in no-op mode") {:ok, %{}} end else # In production, FileSystem is not available Logger.debug("ProfileWatcher: FileSystem not available, running in no-op mode (dev-only feature)") {:ok, %{}} end end @impl true def handle_info({:file_event, _watcher_pid, {path, events}}, state) do # Only reload if it's a YAML file change if yaml_file?(path) and should_reload?(events) do Logger.info("ProfileWatcher: Detected change in #{Path.relative_to_cwd(path)}, reloading profiles...") case YamlProfiles.reload() do {:ok, profiles} when is_list(profiles) -> Logger.info("ProfileWatcher: Successfully reloaded #{length(profiles)} profiles") {:error, reason} -> Logger.error("ProfileWatcher: Failed to reload profiles: #{inspect(reason)}") end end {:noreply, state} end @impl true def handle_info({:file_event, _watcher_pid, :stop}, state) do Logger.warning("ProfileWatcher: File watcher stopped") {:noreply, state} end @doc false def yaml_file?(path) do String.ends_with?(path, [".yaml", ".yml"]) end @doc false def should_reload?(events) when is_list(events) do Enum.any?(events, fn event -> event in [:created, :modified, :removed, :renamed] end) end def should_reload?(_), do: false end