defmodule MishkaInstaller.PluginState do @moduledoc """ This module served as the first layer in the domain of plugin state management in earlier versions; however, following optimization using ETS, it is now regarded as the main structure in the second layer. If a developer chooses to construct a custom plugin by using the `use MishkaInstaller.Hook` directive, then the developer actually implements this module as the first layer to register a plugin and the second layer as a supervisor. The scope of a plugin's state management is not restricted to the automatically specified duties performed by the `MishkaInstaller`; for instance, it provides developers with a temporary state and offers them the freedom to act in accordance with whatever method they choose. ### Each plugin should have these parameters ```elixir defstruct [:name, :event, priority: 1, status: :started, depend_type: :soft, depends: [], extra: [], parent_pid: nil] ``` ### Module communication process of MishkaInstaller.PluginState ``` +------------------------------------------------+ | | | | +------+ MishkaInstaller.PluginStateDynamicSupervisor | | | | | | | | +--------------------------------------+----^----+ | | | | | | | | | | +------------------------+ | | | | | | | +------> PluginStateRegistry | | | | | | | +--------^---------------+ | | | | | | | | | | | | | | | +--------------------v----+-----+ | | | | | MishkaInstaller.PluginState | +--------+ | +--------------------+--------^-+ | | | | | | | | +----------------------------+ | | | | | | | MishkaInstaller.PluginETS <------+ | | | | +----------------------------+ | | | | +--------------------+-+ | | +-------------+ MishkaInstaller.Hook | | | | | +----------------------+ | | +---------v----------------------+ | | | Behaviour References | | | +--------------------------------+ ``` """ use GenServer require Logger alias MishkaInstaller.PluginStateDynamicSupervisor, as: PSupervisor alias MishkaInstaller.Plugin alias __MODULE__ defstruct [ :name, :event, :extension, priority: 1, status: :started, depend_type: :soft, depends: [], extra: [], parent_pid: nil ] @typedoc "This type can be used when you want to introduce the parameters of a plugin" @type params() :: map() @typedoc "This type can be used when you want to introduce an ID of a plugin" @type id() :: String.t() @typedoc "This type can be used when you want to introduce an plugin name" @type module_name() :: String.t() @typedoc "This type can be used when you want to introduce an event name" @type event_name() :: String.t() @typedoc "This type can be used when you want to introduce an event owner extension" @type extension() :: String.t() @typedoc "This type can be used when you want to introduce an event" @type plugin() :: %PluginState{ name: module_name(), event: event_name(), extension: module(), priority: integer(), status: :started | :stopped | :restarted, depend_type: :soft | :hard, parent_pid: any(), depends: list(String.t()), extra: list(map()) } @typedoc "This type can be used when you want to introduce an event" @type t :: plugin() @doc """ You should in no way use this function in its direct form. The supervisor coverage needs to run before using this function since it will establish a state for your plugin and save its PID in the registry. Make use of the function named `MishkaInstaller.PluginStateDynamicSupervisor.start_job/1`. """ def start_link(args) do {id, type, parent_pid} = {Map.get(args, :id), Map.get(args, :type), Map.get(args, :parent_pid)} GenServer.start_link(__MODULE__, default(id, type, parent_pid), name: via(id, type)) end @doc false def child_spec(process_name) do %{ id: __MODULE__, start: {__MODULE__, :start_link, [process_name]}, restart: :transient, max_restarts: 4 } end defp default(plugin_name, event, parent_pid) do %PluginState{name: plugin_name, event: event, parent_pid: parent_pid} end @doc """ This function helps you to create the state of a plugin. Please see `t:plugin/0` type documents. This function does not wait for a response. ## Examples ```elixir plugin = %MishkaInstaller.PluginState{ name: "unnested_plugin_five", event: "nested_event_one", depend_type: :hard, depends: ["unnested_plugin_four"] } MishkaInstaller.PluginState.push(plugin) ``` """ @spec push(MishkaInstaller.PluginState.t()) :: :ok | {:error, :push, any} def push(%PluginState{} = element) do case PSupervisor.start_job(%{ id: element.name, type: element.event, parent_pid: element.parent_pid }) do {:ok, status, pid} -> GenServer.cast(pid, {:push, status, element}) {:error, result} -> {:error, :push, result} end end @doc """ This function does the same thing as the `push/1` function, except that it waits for a response. """ @spec push_call(MishkaInstaller.PluginState.t()) :: :ok | {:error, :push, any} def push_call(%PluginState{} = element) do case PSupervisor.start_job(%{ id: element.name, type: element.event, parent_pid: element.parent_pid }) do {:ok, status, pid} -> if Mix.env() == :test, do: Logger.warn("Plugin State of #{element.name} is being pushed") GenServer.call(pid, {:push, status, element}) {:error, result} -> {:error, :push, result} end end @doc """ It gets a plugin information from the state. ## Examples ```elixir MishkaInstaller.PluginState.get(module: "PluginTest") ``` """ @spec get([{:module, module_name()}]) :: plugin() | {:error, :get, :not_found} def get(module: module_name) do case PSupervisor.get_plugin_pid(module_name) do {:ok, :get_plugin_pid, pid} -> GenServer.call(pid, {:pop, :module}) {:error, :get_plugin_pid} -> {:error, :get, :not_found} end end @doc """ This function gets all information of plugins which are under a specific event. ## Examples ```elixir MishkaInstaller.PluginState.get_all(event: "event_test") ``` """ def get_all(event: event_name) do PSupervisor.running_imports(event_name) |> Enum.map(&get(module: &1.id)) end @doc """ This function gets all information of plugins which are pushed on the state. ## Examples ```elixir MishkaInstaller.PluginState.get_all() ``` """ def get_all() do PSupervisor.running_imports() |> Enum.map(&get(module: &1.id)) end @doc """ Delete a plugin or plugins based on a specific event from the state. ## Examples ```elixir MishkaInstaller.PluginState.delete(module: "PluginTest") # or MishkaInstaller.PluginState.delete(event: "EventTest") ``` """ def delete(module: module_name) do case PSupervisor.get_plugin_pid(module_name) do {:ok, :get_plugin_pid, pid} -> GenServer.cast(pid, {:delete, :module}) {:ok, :delete} {:error, :get_plugin_pid} -> {:error, :delete, :not_found} end end def delete(event: event_name) do PSupervisor.running_imports(event_name) |> Enum.map(&delete(module: &1.id)) end @doc """ Terminate a PID from the supervisor directly. ## Examples ```elixir MishkaInstaller.PluginState.delete_child(module: "PluginTest") ``` """ def delete_child(module: module_name) do case PSupervisor.get_plugin_pid(module_name) do {:ok, :get_plugin_pid, pid} -> DynamicSupervisor.terminate_child(PluginStateOtpRunner, pid) {:error, :get_plugin_pid} -> {:error, :delete, :not_found} end end @doc """ Terminate all PIDs of the plugin state from the supervisor directly. ## Examples ```elixir MishkaInstaller.PluginState.terminate_all_pids() ``` """ def terminate_all_pids() do Enum.map(PSupervisor.running_imports(), fn item -> GenServer.cast(item.pid, {:delete, :module}) end) end @doc """ Stop a plugin state. ## Examples ```elixir MishkaInstaller.PluginState.stop(module: PluginTest) # or MishkaInstaller.PluginState.stop(event: "event_test") ``` """ def stop(module: module_name) do case PSupervisor.get_plugin_pid(module_name) do {:ok, :get_plugin_pid, pid} -> GenServer.cast(pid, {:stop, :module}) {:ok, :stop} {:error, :get_plugin_pid} -> {:error, :stop, :not_found} end end def stop(event: event_name) do PSupervisor.running_imports(event_name) |> Enum.map(&stop(module: &1.id)) end # Callbacks @impl true def init(%PluginState{} = state) do if Mix.env() == :test, do: MishkaInstaller.Database.Helper.get_parent_pid(state) Logger.info( "#{Map.get(state, :name)} from #{Map.get(state, :event)} event of Plugins manager system was started" ) {:ok, state, {:continue, {:sync_with_database, :take}}} end @impl true def handle_call({:pop, :module}, _from, %PluginState{} = state) do {:reply, state, state} end @impl true def handle_call({:push, _status, %PluginState{} = element}, _from, %PluginState{} = _state) do element |> Map.from_struct() |> Plugin.add_or_edit_by_name() {:reply, element, element} end @impl true def handle_cast({:push, status, %PluginState{} = element}, _state) do {:noreply, element, {:continue, {:sync_with_database, status}}} end @impl true def handle_cast({:stop, :module}, %PluginState{} = state) do new_state = Map.merge(state, %{status: :stopped}) {:noreply, new_state, {:continue, {:sync_with_database, :edit}}} end @impl true def handle_cast({:delete, :module}, %PluginState{} = state) do MishkaInstaller.plugin_activity("destroy", state, "high", "report") {:stop, :normal, state} end @impl true def handle_continue({:sync_with_database, _status}, %PluginState{} = state) do state |> Map.from_struct() |> Plugin.add_or_edit_by_name() |> check_output(state) {:noreply, state} end @impl true def handle_continue({:sync_with_database, :take}, %PluginState{} = state) do state = case Plugin.show_by_name("#{state.name}") do {:ok, :get_record_by_field, _error_atom, record_info} -> struct(__MODULE__, Map.from_struct(record_info)) {:error, _result, _error_atom} -> state end {:noreply, state} end if Mix.env() in [:test, :dev] do @impl true def handle_info({:DOWN, _ref, :process, _pid, _reason}, state) do # log that this happened, etc. Don't use Repo! {:stop, :normal, state} end end @impl true def terminate(reason, %PluginState{} = state) do MishkaInstaller.plugin_activity("read", state, "high", "throw") if reason != :normal do Task.Supervisor.start_child(PluginEtsTask, fn -> MishkaInstaller.PluginETS.sync_with_database() end) Logger.warn( "#{Map.get(state, :name)} from #{Map.get(state, :event)} event of Plugins manager was Terminated, Reason of Terminate #{inspect(reason)}" ) end end defp via(id, value) do {:via, Registry, {PluginStateRegistry, id, value}} end defp check_output({:error, status, _, _} = _output, %PluginState{} = state) do MishkaInstaller.plugin_activity("#{status}", state, "high", "error") end defp check_output({:ok, status, _, _} = _output, %PluginState{} = state) do action = cond do status == :add -> "add" state.status == :stopped -> "delete" true -> "edit" end MishkaInstaller.plugin_activity(action, state, "high") end end