summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorEvadne Wu <ev@radi.ws>2019-07-03 21:18:28 +0100
committerJames Every <devstopfix@gmail.com>2019-07-03 21:18:28 +0100
commit46375802f5c69b407140aab51854943e1c8e363c (patch)
treef0aa7c89fcad13ba423604c914ab7c1aa3a528e3 /lib
Prototype using erlexec
Diffstat (limited to 'lib')
-rw-r--r--lib/gen_magic.ex27
-rw-r--r--lib/gen_magic/apprentice_server.ex100
-rw-r--r--lib/gen_magic/configuration.ex34
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