diff options
author | Evadne Wu <ev@radi.ws> | 2019-07-03 21:18:28 +0100 |
---|---|---|
committer | James Every <devstopfix@gmail.com> | 2019-07-03 21:18:28 +0100 |
commit | 46375802f5c69b407140aab51854943e1c8e363c (patch) | |
tree | f0aa7c89fcad13ba423604c914ab7c1aa3a528e3 /lib |
Prototype using erlexec
Diffstat (limited to 'lib')
-rw-r--r-- | lib/gen_magic.ex | 27 | ||||
-rw-r--r-- | lib/gen_magic/apprentice_server.ex | 100 | ||||
-rw-r--r-- | lib/gen_magic/configuration.ex | 34 |
3 files changed, 161 insertions, 0 deletions
diff --git a/lib/gen_magic.ex b/lib/gen_magic.ex new file mode 100644 index 0000000..3711dfa --- /dev/null +++ b/lib/gen_magic.ex @@ -0,0 +1,27 @@ +defmodule GenMagic do + @moduledoc """ + Top-level namespace for GenMagic, the libMagic client for Elixir. + """ + + @doc """ + Top-level convenience function which creates an ad-hoc process. Usually + this will be wrapped in a pool established by the author of the application + that uses the library. + """ + def perform(path) do + {:ok, pid} = __MODULE__.ApprenticeServer.start_link() + result = GenServer.call(pid, {:perform, path}) + :ok = GenServer.stop(pid) + result + end + + def perform_infinite(path) do + {:ok, pid} = __MODULE__.ApprenticeServer.start_link() + perform_infinite(path, pid) + end + + defp perform_infinite(path, pid, count \\ 0) do + IO.inspect [count, GenServer.call(pid, {:perform, path})] + perform_infinite(path, pid, count + 1) + end +end diff --git a/lib/gen_magic/apprentice_server.ex b/lib/gen_magic/apprentice_server.ex new file mode 100644 index 0000000..5aee590 --- /dev/null +++ b/lib/gen_magic/apprentice_server.ex @@ -0,0 +1,100 @@ +defmodule GenMagic.ApprenticeServer do + @moduledoc """ + Provides access to the underlying libMagic client which performs file introspection. + """ + + alias GenMagic.Configuration + use GenServer + + def start_link(args \\ []) do + GenServer.start_link(__MODULE__, args) + end + + defmodule State do + defstruct pid: nil, ospid: nil, started: false, count: 0 + end + + def init(_) do + {:ok, %State{}} + end + + def handle_call(message, from, %{started: false} = state) do + case start(state) do + {:ok, state} -> handle_call(message, from, state) + {:error, _} = error -> {:reply, error, state} + end + end + + def handle_call({:perform, path}, _, state) do + max_count = Configuration.get_recycle_threshold() + + case {run(path, state), state.count + 1} do + {{:error, :worker_failure} = reply, _} -> + {:reply, reply, stop(state)} + {reply, ^max_count} -> + {:reply, reply, stop(state)} + {reply, count} -> + {:reply, reply, %{state | count: count}} + end + end + + def handle_info({:DOWN, _, :process, pid, :normal}, state) do + case state.pid do + ^pid -> {:noreply, %State{}} + _ -> {:noreply, state} + end + end + + defp start(%{started: false} = state) do + worker_command = Configuration.get_worker_command() + worker_options = [stdin: true, stdout: true, stderr: true, monitor: true] + worker_timeout = Configuration.get_worker_timeout() + {:ok, pid, ospid} = Exexec.run(worker_command, worker_options) + state = %{state | started: true, pid: pid, ospid: ospid} + + receive do + {:stdout, ^ospid, "ok\n"} -> {:ok, state} + {:stdout, ^ospid, "ok\r\n"} -> {:ok, state} + after worker_timeout -> + {:error, :worker_failure} + end + end + + defp stop(%{started: true} = state) do + :normal = Exexec.stop_and_wait(state.ospid) + %State{} + end + + defp run(path, %{pid: pid, ospid: ospid} = _state) do + worker_timeout = Configuration.get_worker_timeout() + :ok = Exexec.send(pid, "file; " <> path <> "\n") + + receive do + {stream, ^ospid, message} -> + handle_response(stream, message) + after worker_timeout -> + {:error, :worker_failure} + end + end + + defp handle_response(:stdout, "ok; " <> message) do + case message |> String.trim |> String.split("\t") do + [mime_type, encoding, content] -> {:ok, [mime_type: mime_type, encoding: encoding, content: content]} + _ -> {:error, :malformed_response} + end + end + + defp handle_response(:stderr, "error; " <> message) do + {:error, String.trim(message)} + end + + # TODO handle late responses under load + # 17:13:47.808 [error] GenServer #PID<0.199.0> terminating + # ** (FunctionClauseError) no function clause matching in GenMagic.ApprenticeServer.handle_info/2 + # (gen_magic) lib/gen_magic/apprentice_server.ex:41: GenMagic.ApprenticeServer.handle_info({:stderr, 12304, "\n"}, %GenMagic.ApprenticeServer.State{count: 2, ospid: 12304, pid: #PID<0.243.0>, started: true}) + # (stdlib) gen_server.erl:637: :gen_server.try_dispatch/4 + # (stdlib) gen_server.erl:711: :gen_server.handle_msg/6 + # (stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3 + # Last message: {:stderr, 12304, "\n"} + +end diff --git a/lib/gen_magic/configuration.ex b/lib/gen_magic/configuration.ex new file mode 100644 index 0000000..b815aad --- /dev/null +++ b/lib/gen_magic/configuration.ex @@ -0,0 +1,34 @@ +defmodule GenMagic.Configuration do + @moduledoc """ + Convenience module which returns information from configuration. + """ + + @otp_app Mix.Project.config[:app] + + def get_worker_command do + database_paths = get_database_paths() + worker_path = Path.join(:code.priv_dir(@otp_app), get_worker_name()) + worker_arguments = Enum.map(database_paths, & "--file " <> &1) + Enum.join([worker_path | worker_arguments], " ") + end + + def get_worker_name do + get_env(:worker_name) + end + + def get_worker_timeout do + get_env(:worker_timeout) + end + + def get_recycle_threshold do + get_env(:recycle_threshold) + end + + def get_database_paths do + get_env(:database_patterns) |> Enum.flat_map(&Path.wildcard/1) + end + + defp get_env(key) do + Application.get_env(@otp_app, key) + end +end |