Packages
Mishka Installer is a system plugin manager and run time installer for elixir.
Current section
Files
Jump to
Current section
Files
lib/installer/dep_changes_protector.ex
defmodule MishkaInstaller.Installer.DepChangesProtector do
@moduledoc """
This module helps you to protect the extension.json file and every action like add or delete or even edit a dependency is happened,
it can handle it and create new state.
"""
use GenServer, restart: :permanent
require Logger
@re_check_json_time 10_000
@module "dep_changes_protector"
alias MishkaInstaller.Installer.DepHandler
alias MishkaInstaller.Dependency
@spec start_link(list()) :: :ignore | {:error, any} | {:ok, pid}
def start_link(args \\ []) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
@spec push(map(), String.t() | atom()) :: map()
def push(app, status) do
GenServer.call(__MODULE__, {:push, app: app, status: status})
end
@spec get(String.t()) :: map()
def get(app) do
GenServer.call(__MODULE__, {:get, app})
end
@spec get :: map()
def get() do
GenServer.call(__MODULE__, :get)
end
@spec pop(String.t()) :: map()
def pop(app) do
GenServer.call(__MODULE__, {:pop, app})
end
@spec clean :: :ok
def clean() do
GenServer.cast(__MODULE__, :clean)
end
@spec deps(String.t(), atom()) :: :ok
def deps(app, type \\ :port) do
GenServer.cast(__MODULE__, {:deps, app, type})
end
@impl true
def init(_state) do
Logger.info("OTP Dependencies changes protector server was started")
# Start update ets
MishkaInstaller.DepUpdateJob.ets()
{:ok, %{data: nil, ref: nil}, {:continue, :check_json}}
end
@impl true
def handle_continue(:check_json, state) do
check_custom_pubsub_loaded(state)
end
@impl true
def handle_info(:check_json, state) do
if Mix.env() not in [:dev, :test],
do: Logger.info("OTP Dependencies changes protector Cache server was valued by JSON.")
Process.send_after(self(), :check_json, @re_check_json_time)
new_state =
if is_nil(state.ref) do
Map.merge(state, %{data: json_check_and_create()})
else
state
end
{:noreply, new_state}
end
@impl true
def handle_info({:ok, :dependency, _action, _repo_data}, state) do
DepHandler.extensions_json_path()
|> File.rm_rf()
{:noreply, Map.merge(state, %{data: json_check_and_create()})}
end
@impl true
def handle_info({_ref, {:installing_app, app_name, _move_apps, app_res}}, state) do
# TODO: problem with dependency_activity to store output of errors
project_path = MishkaInstaller.get_config(:project_path)
case app_res do
{:ok, :application_ensure} ->
[
Path.join(project_path, ["deps/", "#{app_name}", "/_build"]),
Path.join(project_path, ["deps/", "#{app_name}", "/deps"])
]
|> Enum.map(&File.rm_rf(&1))
notify_subscribers({:ok, app_res, app_name})
{:error, do_runtime, app, operation: operation, output: output} ->
notify_subscribers({:error, app_res, "#{app}"})
MishkaInstaller.dependency_activity(
%{state: [{:error, do_runtime, "#{app}", operation: operation, output: output}]},
"high"
)
{:error, do_runtime, ensure, output} ->
notify_subscribers({:error, app_res, app_name})
MishkaInstaller.dependency_activity(
%{state: [{:error, do_runtime, app_name, operation: ensure, output: output}]},
"high"
)
end
Oban.resume_queue(queue: :compile_events)
{:noreply, state}
end
@impl true
def handle_info({ref, answer}, %{ref: ref} = state) do
# The task completed successfully
{:noreply,
Map.merge(state, %{data: update_dependency_type(answer, state) || state, ref: nil, app: nil})}
end
@impl true
def handle_info({:DOWN, _ref, :process, _pid, _status}, state) do
{:noreply, %{state | ref: nil}}
end
@impl true
def handle_info({:do_compile, app, type}, state) do
task =
Task.Supervisor.async_nolink(DepChangesProtectorTask, fn ->
MishkaInstaller.Installer.RunTimeSourcing.do_deps_compile(app, type)
end)
{:noreply, Map.merge(state, %{ref: task.ref, app: app})}
end
@impl true
def handle_info(:start_oban, state) do
Logger.info("We sent a request to start oban")
MishkaInstaller.start_oban_in_runtime()
{:noreply, state}
end
@impl true
def handle_info(:timeout, state) do
Logger.info("We are waiting for your custom pubsub is loaded")
check_custom_pubsub_loaded(state)
end
@impl true
def handle_info(_param, state) do
# TODO: it should be loged
Logger.info("We have an uncontrolled output")
{:noreply, state}
end
@impl true
def handle_call({:push, app: app, status: status}, _from, state) do
new_state =
case Enum.find(state.data, &(&1.app == app)) do
nil ->
new_app = %{app: app, status: status, time: DateTime.utc_now()}
{:reply, new_app, Map.merge(state, %{data: state.data ++ [new_app]})}
_ ->
{:reply, {:duplicate, app}, state}
end
new_state
end
@impl true
def handle_call({:get, app}, _from, state) do
{:reply, Enum.find(state.data, &(&1.app == app)), state}
end
@impl true
def handle_call(:get, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call({:pop, app}, _from, state) do
new_state = Enum.reject(state.data, &(&1.app == app))
{:reply, new_state, %{state | data: new_state}}
end
@impl true
def handle_cast({:deps, app, type}, state) do
Process.send_after(self(), {:do_compile, app, type}, 100)
{:noreply, state}
end
@impl true
def handle_cast(:clean, _state) do
{:noreply, []}
end
@spec is_dependency_compiling? :: boolean
def is_dependency_compiling?(), do: if(is_nil(get().ref), do: false, else: true)
# For now, we decided to remove and re-create JSON file to prevent user not to delete or wrong edit manually
defp json_check_and_create() do
File.rm_rf(DepHandler.extensions_json_path())
{:ok, :check_or_create_deps_json, json} = DepHandler.check_or_create_deps_json()
{:ok, :read_dep_json, data} = DepHandler.read_dep_json(json)
Enum.filter(data, &(&1["dependency_type"] == "force_update"))
|> Enum.map(&%{app: &1["app"], status: &1["dependency_type"], time: DateTime.utc_now()})
rescue
_e -> []
end
def update_dependency_type(answer, state, dependency_type \\ "none") do
with {:ok, :do_deps_compile, app_name} <- answer,
{:ok, :change_dependency_type_with_app, _repo_data} <-
Dependency.change_dependency_type_with_app(app_name, dependency_type) do
json_check_and_create()
with {:ok, :compare_installed_deps_with_app_file, apps_list} <-
DepHandler.compare_installed_deps_with_app_file("#{app_name}") do
Task.Supervisor.async_nolink(DepChangesProtectorTask, fn ->
MishkaInstaller.Installer.RunTimeSourcing.do_runtime(
String.to_atom(state.app),
:uninstall
)
{
:installing_app,
app_name,
DepHandler.move_and_replace_compiled_app_build(apps_list),
MishkaInstaller.Installer.RunTimeSourcing.do_runtime(String.to_atom(state.app), :add)
}
end)
end
else
{:error, :do_deps_compile, app, operation: _operation, output: output} ->
with {:ok, :get_record_by_field, :dependency, record_info} <- Dependency.show_by_name(app) do
Dependency.delete(record_info.id)
end
notify_subscribers({:error, output, app})
MishkaInstaller.dependency_activity(%{state: [answer]}, "high")
{:error, :change_dependency_type_with_app, :dependency, :not_found} ->
MishkaInstaller.dependency_activity(%{state: [answer], action: "no_app_found"}, "high")
{:error, :change_dependency_type_with_app, :dependency, repo_error} ->
MishkaInstaller.dependency_activity(
%{state: [answer], action: "edit", error: repo_error},
"high"
)
end
end
defp check_custom_pubsub_loaded(state) do
custom_pubsub = MishkaInstaller.get_config(:pubsub)
cond do
!is_nil(custom_pubsub) && is_nil(Process.whereis(custom_pubsub)) ->
{:noreply, state, 100}
true ->
Process.send_after(self(), :check_json, @re_check_json_time)
Process.send_after(self(), :start_oban, @re_check_json_time)
MishkaInstaller.Dependency.subscribe()
{:noreply, state}
end
end
@spec subscribe :: :ok | {:error, {:already_registered, pid}}
def subscribe do
Phoenix.PubSub.subscribe(
MishkaInstaller.get_config(:pubsub) || MishkaInstaller.PubSub,
@module
)
end
@spec notify_subscribers({atom(), any, String.t() | atom()}) :: :ok | {:error, any}
def notify_subscribers({status, answer, app}) do
Phoenix.PubSub.broadcast(
MishkaInstaller.get_config(:pubsub) || MishkaInstaller.PubSub,
@module,
{status, String.to_atom(@module), answer, app}
)
end
end