defmodule MishkaInstaller.Hook do alias MishkaInstaller.PluginState alias MishkaInstaller.PluginStateDynamicSupervisor, as: PSupervisor alias MishkaInstaller.Plugin alias MishkaInstaller.PluginETS @allowed_fields [:name, :event, :priority, :status, :depend_type, :depends, :extra, :id] @type event() :: String.t() @type plugin() :: event() @spec register([{:depends, :force} | {:event, MishkaInstaller.PluginState.t()}]) :: {:error, :register, any} | {:ok, :register, :activated | :force} def register(event: %PluginState{} = event) do extra = (event.extra || []) ++ [%{operations: :hook}, %{fun: :register}] register_status = with {:ok, :ensure_event, _msg} <- ensure_event(event, :debug), {:error, :get_record_by_field, :plugin} <- Plugin.show_by_name("#{event.name}"), {:ok, :add, :plugin, _record_info} <- Plugin.create(event, @allowed_fields) do # Create a Genserver with DynamicSupervisor PluginState.push_call(event) # Save all event info into ETS, Existed-key is overwritten PluginETS.push(event) {:ok, :register, :activated} else {:error, :ensure_event, %{errors: check_data}} -> MishkaInstaller.plugin_activity( "add", Map.merge(event, %{extra: extra}), "high", "error" ) {:error, :register, check_data} {:ok, :get_record_by_field, :plugin, record_info} -> # Create a Genserver with DynamicSupervisor PluginState.push_call(plugin_state_struct(record_info)) # Save all event info into ETS, Existed-key is overwritten PluginETS.push(plugin_state_struct(record_info)) {:ok, :register, :activated} {:error, :add, :plugin, repo_error} -> MishkaInstaller.plugin_activity( "add", Map.merge(event, %{extra: extra}), "high", "error" ) {:error, :register, repo_error} end register_status end def register(event: %PluginState{} = event, depends: :force) do # Create a Genserver with DynamicSupervisor PluginState.push_call(event) # Save all event info into ETS PluginETS.push(event) {:ok, :register, :force} end @spec start([{:depends, :force} | {:event, event()} | {:module, plugin()}]) :: list | {:error, :start, any()} | {:ok, :start, :force | String.t()} def start(module: module_name) do with {:ok, :get_record_by_field, :plugin, record_info} <- Plugin.show_by_name("#{module_name}"), {:ok, :ensure_event, _msg} <- ensure_event(plugin_state_struct(record_info), :debug) do # Create a Genserver with DynamicSupervisor PluginState.push_call(plugin_state_struct(record_info) |> Map.merge(%{status: :started})) # Save all event info into ETS, Existed-key is overwritten PluginETS.push(plugin_state_struct(record_info) |> Map.merge(%{status: :started})) {:ok, :start, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module's status was changed" )} else {:error, :get_record_by_field, :plugin} -> {:error, :start, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist in the database." )} {:error, :ensure_event, %{errors: check_data}} -> {:error, :start, check_data} end end def start(module: module_name, depends: :force) do with {:ok, :get_record_by_field, :plugin, record_info} <- Plugin.show_by_name("#{module_name}") do # Create a Genserver with DynamicSupervisor PluginState.push_call(plugin_state_struct(record_info) |> Map.merge(%{status: :started})) # Save all event info into ETS, Existed-key is overwritten PluginETS.push(plugin_state_struct(record_info) |> Map.merge(%{status: :started})) {:ok, :start, :force} else {:error, :get_record_by_field, :plugin} -> {:error, :start, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist in the database." )} end end def start(event: event) do Plugin.plugins(event: event) |> Enum.map(&start(module: &1.name)) end def start(event: event, depends: :force) do Plugin.plugins(event: event) |> Enum.map(&start(module: &1.name, depends: :force)) end @spec restart([{:depends, :force} | {:event, event()} | {:module, plugin()}]) :: list | {:error, :restart, any()} | {:ok, :restart, String.t()} def restart(module: module_name) do with {:ok, :delete} <- PluginState.delete(module: module_name), {:ok, :get_record_by_field, :plugin, record_info} <- Plugin.show_by_name("#{module_name}"), {:ok, :ensure_event, _msg} <- ensure_event(plugin_state_struct(record_info), :debug) do # Create a Genserver with DynamicSupervisor PluginState.push_call(plugin_state_struct(record_info)) # Save all event info into ETS, Existed-key is overwritten PluginETS.push(plugin_state_struct(record_info)) {:ok, :restart, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned was restarted" )} else {:error, :delete, :not_found} -> {:error, :restart, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist in the state." )} {:error, :ensure_event, %{errors: check_data}} -> {:error, :restart, check_data} {:error, :get_record_by_field, :plugin} -> {:error, :restart, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist in the database." )} end end def restart(module: module_name, depends: :force) do with {:ok, :delete} <- PluginState.delete(module: module_name), {:ok, :get_record_by_field, :plugin, record_info} <- Plugin.show_by_name("#{module_name}") do # Create a Genserver with DynamicSupervisor PluginState.push_call(plugin_state_struct(record_info)) # Save all event info into ETS, Existed-key is overwritten PluginETS.push(plugin_state_struct(record_info)) {:ok, :restart, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned was restarted" )} else {:error, :delete, :not_found} -> {:error, :restart, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist in the state." )} {:error, :get_record_by_field, :plugin} -> {:error, :restart, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist in the database." )} end end def restart(event: event_name) do Plugin.plugins(event: event_name) |> Enum.map(&restart(module: &1.name)) end def restart(event: event_name, depends: :force) do Plugin.plugins(event: event_name) |> Enum.map(&restart(module: &1.name, depends: :force)) end def restart(depends: :force) do Plugin.plugins() |> Enum.map(&restart(module: &1.name, depends: :force)) end def restart() do Plugin.plugins() |> Enum.map(&restart(module: &1.name)) end @spec stop([{:event, event()} | {:module, plugin()}]) :: list | {:error, :stop, String.t()} | {:ok, :stop, String.t()} def stop(module: module_name) do case PluginState.stop(module: module_name) do {:ok, :stop} -> PluginETS.delete(module: module_name) {:ok, :stop, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned was stopped" )} {:error, :stop, :not_found} -> {:error, :stop, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist in database." )} end end def stop(event: event_name) do PSupervisor.running_imports(event_name) |> Enum.map(&stop(module: &1.id)) end @spec delete([{:event, event()} | {:module, plugin()}]) :: list | {:error, :delete, String.t()} | {:ok, :delete, String.t()} def delete(module: module_name) do case PluginState.delete(module: module_name) do {:ok, :delete} -> PluginETS.delete(module: module_name) {:ok, :delete, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module's state (%{name}) was deleted", name: module_name )} {:error, :delete, :not_found} -> {:error, :delete, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned (%{name}) doesn't exist in the state.", name: module_name )} end end def delete(event: event_name) do PSupervisor.running_imports(event_name) |> Enum.map(&delete(module: &1.id)) end @spec unregister([{:event, event()} | {:module, plugin()}]) :: list | {:error, :unregister, any} | {:ok, :unregister, Stream.timer()} def unregister(module: module_name) do with {:ok, :delete, _msg} <- delete(module: module_name), {:ok, :get_record_by_field, :plugin, record_info} <- Plugin.show_by_name(module_name), {:ok, :delete, :plugin, _} <- Plugin.delete(record_info.id) do Plugin.delete_plugins(module_name) {:ok, :unregister, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned (%{name}) and its dependencies were unregister", name: module_name )} else {:error, :delete, msg} -> {:error, :unregister, msg} {:error, :get_record_by_field, :plugin} -> {:error, :unregister, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The %{name} module doesn't exist in the database.", name: module_name )} {:error, :delete, status, _error_tag} when status in [:uuid, :get_record_by_id, :forced_to_delete] -> {:error, :unregister, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "There is a problem to find or delete the record in the database %{status}, module: %{name}", status: status, name: module_name )} {:error, :delete, :plugin, repo_error} -> {:error, :unregister, repo_error} end end def unregister(event: event_name) do Plugin.plugins(event: event_name) |> Enum.map(&unregister(module: &1.name)) end def call(event: event_name, state: state, operation: :no_return) do call(event: event_name, state: state) state end def call(event: event_name, state: state) do call_output = PluginETS.get_all(event: event_name) |> sorted_plugins() |> run_plugin_state({:reply, state}) if !is_nil(Map.get(state, :private)), do: Map.merge(call_output, %{private: state.private}), else: call_output rescue _e -> state end defp run_plugin_state([], {:reply, state}), do: state defp run_plugin_state(_plugins, {:reply, :halt, state}), do: state defp run_plugin_state([h | t], {:reply, state}) do new_state = apply(String.to_atom("Elixir.#{h.name}"), :call, [state]) run_plugin_state(t, new_state) end defp sorted_plugins(plugins) do plugins |> Enum.map(fn event -> case ensure_event(event, :debug) do {:error, :ensure_event, %{errors: _check_data}} -> extra = event.extra ++ [%{operations: :hook}, %{fun: :call}] MishkaInstaller.plugin_activity( "read", Map.merge(event, %{extra: extra}), "high", "error" ) [] {:ok, :ensure_event, _msg} -> %{name: event.name, priority: event.priority, status: event.status} end end) |> Enum.filter(&(&1 != [] and &1.status == :started)) |> Enum.sort_by(fn item -> {item.priority, item.name} end) end @spec ensure_event?(PluginState.t()) :: boolean def ensure_event?(%PluginState{depend_type: :hard, depends: depends} = event) do check_data = check_dependencies(depends, event.name) Enum.any?(check_data, fn {status, _error_atom, _event, _msg} -> status == :error end) |> case do true -> false false -> true end end def ensure_event?(%PluginState{} = _event), do: true @spec ensure_event(PluginState.t(), :debug) :: {:error, :ensure_event, %{errors: list}} | {:ok, :ensure_event, String.t()} def ensure_event(%PluginState{depend_type: :hard, depends: depends} = event, :debug) when depends != [] do check_data = check_dependencies(depends, event.name) Enum.any?(check_data, fn {status, _error_atom, _event, _msg} -> status == :error end) |> case do true -> {:error, :ensure_event, %{errors: check_data}} false -> {:ok, :ensure_event, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The modules concerned are activated" )} end end def ensure_event(%PluginState{depend_type: :hard} = _event, :debug), do: {:ok, :ensure_event, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The modules concerned are activated" )} def ensure_event(%PluginState{} = _event, :debug), do: {:ok, :ensure_event, "The modules concerned are activated"} defp check_dependencies(depends, event_name) do Enum.map(depends, fn evn -> with {:ensure_loaded, true} <- {:ensure_loaded, Code.ensure_loaded?(String.to_atom("Elixir.#{evn}"))}, plugin_state <- PluginETS.get(module: evn), {:plugin_state?, true, _state} <- {:plugin_state?, is_struct(plugin_state), plugin_state}, {:activated_plugin, true, _state} <- {:activated_plugin, Map.get(plugin_state, :status) == :started, plugin_state} do {:ok, :ensure_event, evn, "The module concerned is activated"} else {:ensure_loaded, false} -> {:error, :ensure_loaded, evn, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist." )} {:plugin_state?, false, _state} -> {:error, :plugin_state?, evn, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The event concerned doesn't exist in state." )} {:activated_plugin, false, _state} -> {:error, :activated_plugin, evn, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The event concerned is not activated." )} end end) ++ [string_ensure_loaded(event_name)] end defp string_ensure_loaded(event_name) do case Code.ensure_loaded?(String.to_atom("Elixir.#{event_name}")) do true -> {:ok, :ensure_event, event_name, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned is activated" )} false -> {:error, :ensure_loaded, event_name, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "The module concerned doesn't exist." )} end end defp plugin_state_struct(output) do %PluginState{ name: output.name, event: output.event, priority: output.priority, status: output.status, depend_type: output.depend_type, depends: Map.get(output, :depends) || [], extra: Map.get(output, :extra) || [], parent_pid: Map.get(output, :parent_pid) } end defmacro __using__(opts) do quote(bind_quoted: [opts: opts]) do import MishkaInstaller.Hook use GenServer, restart: :transient require Logger alias MishkaInstaller.{PluginState, Hook} module_selected = Keyword.get(opts, :module) initial_entry = Keyword.get(opts, :initial) behaviour = Keyword.get(opts, :behaviour) event = Keyword.get(opts, :event) @ref event @behaviour behaviour # Start registering with Genserver and set this in application file of MishkaInstaller def start_link(_args) do GenServer.start_link(unquote(module_selected), %{id: "#{unquote(module_selected)}"}, name: unquote(module_selected) ) end def init(state) do if Mix.env() != :test, do: {:ok, state, 300}, else: {:ok, state, 3000} end # This part helps us to wait for database and completing PubSub either def handle_info(:timeout, state) do cond do !is_nil(MishkaInstaller.get_config(:pubsub)) && is_nil(Process.whereis(MishkaInstaller.get_config(:pubsub))) -> {:noreply, state, 100} !is_nil(MishkaInstaller.get_config(:pubsub)) -> unquote(module_selected).initial(unquote(initial_entry)) {:noreply, state} true -> unquote(module_selected).initial(unquote(initial_entry)) {:noreply, state} end end end end end