From 9cd3d57433fc18b2fb7f6758c8270862f67ea992 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 25 Jan 2026 13:38:17 -0600 Subject: [PATCH] live reload yaml in dev --- lib/towerops/application.ex | 13 +++++ lib/towerops/profiles/profile_watcher.ex | 74 ++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 lib/towerops/profiles/profile_watcher.ex diff --git a/lib/towerops/application.ex b/lib/towerops/application.ex index 78508179..de742134 100644 --- a/lib/towerops/application.ex +++ b/lib/towerops/application.ex @@ -34,6 +34,7 @@ defmodule Towerops.Application do SnmpKit.SnmpMgr.Config, SnmpKit.SnmpMgr.MIB ] ++ + dev_only_workers() ++ background_workers() ++ [ # Start to serve requests, typically the last entry @@ -68,6 +69,18 @@ defmodule Towerops.Application do end end + # Configure dev-only workers - profile watcher for auto-reload + defp dev_only_workers do + if Application.get_env(:towerops, :env) == :dev do + [ + # Watch YAML profiles for changes and auto-reload + Towerops.Profiles.ProfileWatcher + ] + else + [] + end + end + # Returns PubSub spec - uses Redis in production/clustered environments, # falls back to default PG2 adapter for Mix tasks and development defp pubsub_spec do diff --git a/lib/towerops/profiles/profile_watcher.ex b/lib/towerops/profiles/profile_watcher.ex new file mode 100644 index 00000000..a6a4eb18 --- /dev/null +++ b/lib/towerops/profiles/profile_watcher.ex @@ -0,0 +1,74 @@ +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. + """ + 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 + # 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 + {:ok, watcher_pid} = FileSystem.start_link(dirs: [profiles_path]) + FileSystem.subscribe(watcher_pid) + + Logger.info("ProfileWatcher: Started watching #{profiles_path}") + + {:ok, %{watcher_pid: watcher_pid, profiles_path: profiles_path}} + 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, count} -> + Logger.info("ProfileWatcher: Successfully reloaded #{count} 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 + + # Check if the file is a YAML profile file + defp yaml_file?(path) do + String.ends_with?(path, [".yaml", ".yml"]) + end + + # Check if the events indicate we should reload + # Reload on created, modified, or removed events + # Don't reload on :undefined or other metadata-only changes + defp should_reload?(events) when is_list(events) do + Enum.any?(events, fn event -> + event in [:created, :modified, :removed, :renamed] + end) + end + + defp should_reload?(_), do: false +end