defmodule Serum.DevServer.Service.GenServer do @moduledoc false _moduledocp = """ A GenServer-based implementation of `Serum.DevServer.Service` behaviour. """ use GenServer import Serum.IOProxy, only: [put_err: 2, put_msg: 2] alias Serum.Build alias Serum.DevServer.Service alias Serum.Project alias Serum.Project.Loader, as: ProjectLoader alias Serum.Result @behaviour Service @type start_arg() :: {binary(), binary(), pos_integer()} # # GenServer Implementation - Client # @doc "Starts the GenServer process." @spec start_link(start_arg()) :: {:ok, pid()} | {:error, atom()} def start_link(args) do GenServer.start_link(__MODULE__, args, name: __MODULE__, timeout: :infinity) end @impl Service @spec rebuild() :: :ok def rebuild, do: GenServer.call(__MODULE__, :rebuild) @impl Service @spec source_dir() :: binary def source_dir, do: GenServer.call(__MODULE__, :source_dir) @impl Service @spec site_dir() :: binary def site_dir, do: GenServer.call(__MODULE__, :site_dir) @impl Service @spec port() :: pos_integer def port, do: GenServer.call(__MODULE__, :port) @impl Service @spec dirty?() :: boolean def dirty?, do: GenServer.call(__MODULE__, :is_dirty) @impl Service @spec subscribe() :: :ok def subscribe, do: GenServer.call(__MODULE__, :subscribe) # # GenServer Implementation - Server # @impl GenServer def init({dir, site, portnum}) do false = Process.flag(:trap_exit, true) watcher = case FileSystem.start_link(dirs: [Path.absname(dir)]) do {:ok, pid} -> pid :ignore -> nil end state = %{ watcher: watcher, dir: dir, site: site, portnum: portnum, is_dirty: false, subscribers: %{} } do_rebuild(dir, site) unless(is_nil(watcher), do: FileSystem.subscribe(watcher)) {:ok, state} end @impl GenServer def handle_call(msg, from, state) def handle_call(:rebuild, _from, state) do do_rebuild(state.dir, state.site) {:reply, :ok, state} end def handle_call(:source_dir, _from, state), do: {:reply, state.dir, state} def handle_call(:site_dir, _from, state), do: {:reply, state.site, state} def handle_call(:port, _from, state), do: {:reply, state.portnum, state} def handle_call(:is_dirty, _from, state), do: {:reply, state.is_dirty, %{state | is_dirty: false}} def handle_call(:subscribe, {caller, _}, state) do ref = Process.monitor(caller) state2 = %{state | subscribers: Map.put(state.subscribers, ref, caller)} {:reply, :ok, state2} end @impl GenServer def handle_info(msg, state) def handle_info({:file_event, _, _}, %{is_dirty: true} = state) do {:noreply, state} end def handle_info({:file_event, pid, :stop}, %{watcher: pid} = state) do {:noreply, state} end def handle_info({:file_event, pid, {path, _}}, %{watcher: pid} = state) do ignore? = path |> Path.relative_to(state.dir) |> Path.split() |> Enum.any?(&dotfile?/1) if ignore? do {:noreply, state} else server = self() spawn_link(fn -> receive do after 200 -> send(server, :tick) end end) {:noreply, %{state | is_dirty: true}} end end def handle_info(:tick, state) do do_rebuild(state.dir, state.site) Enum.each(state.subscribers, fn {_, pid} -> send(pid, :send_reload) end) {:noreply, %{state | is_dirty: false}} end def handle_info({:DOWN, ref, :process, _, _}, state) do {:noreply, %{state | subscribers: Map.delete(state.subscribers, ref)}} end def handle_info({:EXIT, _pid, :normal}, state), do: {:noreply, state} @impl GenServer def terminate(_reason, %{site: site, watcher: watcher}) do put_msg(:info, "Removing temporary directory \"#{site}\"...") File.rm_rf!(site) put_msg(:info, "Shutting down...") unless(is_nil(watcher), do: Process.exit(watcher, :shutdown)) end @spec do_rebuild(binary(), binary()) :: :ok defp do_rebuild(src, dest) do with {:ok, %Project{} = proj} <- ProjectLoader.load(src, dest), {:ok, ^dest} <- Build.build(proj) do :ok else {:error, _} = error -> build_failed(error) end end @spec build_failed(Result.t()) :: :ok defp build_failed(error) do Result.show(error) put_err(:warn, "Error occurred while building the website.") put_err(:warn, "The website may not be displayed correctly.") end @spec dotfile?(binary()) :: boolean() defp dotfile?(item) defp dotfile?(<>), do: true defp dotfile?(_), do: false end