Remove use of arguments and load DB from messages
Also: - Adds `GenMagic.Server.recycle` and `GenMagic.Server.reload` which allows to restart/reload the apprentice server with a new set of databases, - Fix compilation on musl based distributions (was caused by the args code), - Handle timeouts better in `GenMagic.Server` - Force the port to close in recycling if the graceful stop timeouts
This commit is contained in:
parent
08ebe0575b
commit
011d1150e3
7 changed files with 279 additions and 170 deletions
|
@ -5,19 +5,13 @@ defmodule GenMagic.Config do
|
||||||
@startup_timeout 1_000
|
@startup_timeout 1_000
|
||||||
@process_timeout 30_000
|
@process_timeout 30_000
|
||||||
@recycle_threshold :infinity
|
@recycle_threshold :infinity
|
||||||
@database_patterns [:default]
|
|
||||||
|
|
||||||
def get_port_name do
|
def get_port_name do
|
||||||
{:spawn_executable, to_charlist(get_executable_name())}
|
{:spawn_executable, to_charlist(get_executable_name())}
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_port_options(options) do
|
def get_port_options(_options) do
|
||||||
arguments = [:use_stdio, :binary, :exit_status, {:packet, 2}]
|
[:use_stdio, :binary, :exit_status, {:packet, 2}]
|
||||||
|
|
||||||
case get_executable_arguments(options) do
|
|
||||||
[] -> arguments
|
|
||||||
list -> [{:args, list} | arguments]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_startup_timeout(options) do
|
def get_startup_timeout(options) do
|
||||||
|
@ -36,13 +30,6 @@ defmodule GenMagic.Config do
|
||||||
Path.join(:code.priv_dir(@otp_app), @executable_name)
|
Path.join(:code.priv_dir(@otp_app), @executable_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_executable_arguments(options) do
|
|
||||||
Enum.flat_map(List.wrap(get(options, :database_patterns, @database_patterns)), fn
|
|
||||||
:default -> ["--database-default"]
|
|
||||||
pattern -> pattern |> Path.wildcard() |> Enum.flat_map(&["--database-file", &1])
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp get(options, key, default) do
|
defp get(options, key, default) do
|
||||||
Keyword.get(options, key, default)
|
Keyword.get(options, key, default)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,6 +10,7 @@ defmodule GenMagic.Server do
|
||||||
alias GenMagic.Server.Data
|
alias GenMagic.Server.Data
|
||||||
alias GenMagic.Server.Status
|
alias GenMagic.Server.Status
|
||||||
import Kernel, except: [send: 2]
|
import Kernel, except: [send: 2]
|
||||||
|
require Logger
|
||||||
|
|
||||||
@typedoc """
|
@typedoc """
|
||||||
Represents the reference to the underlying server, as returned by `:gen_statem`.
|
Represents the reference to the underlying server, as returned by `:gen_statem`.
|
||||||
|
@ -48,6 +49,7 @@ defmodule GenMagic.Server do
|
||||||
|
|
||||||
[:default, "path/to/my/magic"]
|
[:default, "path/to/my/magic"]
|
||||||
"""
|
"""
|
||||||
|
@database_patterns [:default]
|
||||||
@type option ::
|
@type option ::
|
||||||
{:name, atom() | :gen_statem.server_name()}
|
{:name, atom() | :gen_statem.server_name()}
|
||||||
| {:startup_timeout, timeout()}
|
| {:startup_timeout, timeout()}
|
||||||
|
@ -128,6 +130,20 @@ defmodule GenMagic.Server do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Reloads a Server with a new set of databases.
|
||||||
|
"""
|
||||||
|
def reload(server_ref, database_patterns \\ nil, timeout \\ 5000) do
|
||||||
|
:gen_statem.call(server_ref, {:reload, database_patterns}, timeout)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Same as `reload/2,3` but with a full restart of the underlying C port.
|
||||||
|
"""
|
||||||
|
def recycle(server_ref, database_patterns \\ nil, timeout \\ 5000) do
|
||||||
|
:gen_statem.call(server_ref, {:recycle, database_patterns}, timeout)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns status of the Server.
|
Returns status of the Server.
|
||||||
"""
|
"""
|
||||||
|
@ -155,6 +171,7 @@ defmodule GenMagic.Server do
|
||||||
|
|
||||||
data = %Data{
|
data = %Data{
|
||||||
port_name: get_port_name(),
|
port_name: get_port_name(),
|
||||||
|
database_patterns: Keyword.get(options, :database_patterns, []),
|
||||||
port_options: get_port_options(options),
|
port_options: get_port_options(options),
|
||||||
startup_timeout: get_startup_timeout(options),
|
startup_timeout: get_startup_timeout(options),
|
||||||
process_timeout: get_process_timeout(options),
|
process_timeout: get_process_timeout(options),
|
||||||
|
@ -170,11 +187,16 @@ defmodule GenMagic.Server do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def starting(:enter, _, %{request: nil, port: nil} = data) do
|
def starting(:enter, _, %{port: nil} = data) do
|
||||||
port = Port.open(data.port_name, data.port_options)
|
port = Port.open(data.port_name, data.port_options)
|
||||||
{:keep_state, %{data | port: port}, data.startup_timeout}
|
{:keep_state, %{data | port: port}, data.startup_timeout}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def starting(:enter, _, data) do
|
||||||
|
{:keep_state_and_data, data.startup_timeout}
|
||||||
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def starting({:call, from}, :status, data) do
|
def starting({:call, from}, :status, data) do
|
||||||
handle_status_call(from, :starting, data)
|
handle_status_call(from, :starting, data)
|
||||||
|
@ -188,25 +210,79 @@ defmodule GenMagic.Server do
|
||||||
@doc false
|
@doc false
|
||||||
def starting(:info, {port, {:data, ready}}, %{port: port} = data) do
|
def starting(:info, {port, {:data, ready}}, %{port: port} = data) do
|
||||||
case :erlang.binary_to_term(ready) do
|
case :erlang.binary_to_term(ready) do
|
||||||
:ready -> {:next_state, :available, data}
|
:ready -> {:next_state, :loading, data}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def starting(:info, {port, {:exit_status, code}}, %{port: port} = data) do
|
def starting(:info, {port, {:exit_status, code}}, %{port: port} = data) do
|
||||||
error =
|
error =
|
||||||
case code do
|
case code do
|
||||||
1 -> :no_database
|
1 -> :bad_db
|
||||||
2 -> :no_argument
|
2 -> :ei_error
|
||||||
3 -> :missing_database
|
3 -> :ei_bad_term
|
||||||
4 -> :ei_alloc_failed
|
|
||||||
5 -> :ei_bad_term
|
|
||||||
code -> {:unexpected_error, code}
|
code -> {:unexpected_error, code}
|
||||||
end
|
end
|
||||||
|
|
||||||
{:stop, {:error, error}, data}
|
{:stop, {:error, error}, data}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def loading(:enter, _old_state, data) do
|
||||||
|
databases = Enum.flat_map(List.wrap(data.database_patterns || @database_patterns), fn
|
||||||
|
:default -> [:default]
|
||||||
|
pattern -> Path.wildcard(pattern)
|
||||||
|
end)
|
||||||
|
databases = if databases == [] do
|
||||||
|
[:default]
|
||||||
|
else
|
||||||
|
databases
|
||||||
|
end
|
||||||
|
{:keep_state, {databases, data}, {:state_timeout, 0, :load}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def loading(:state_timeout, :load_timeout, {[database | _], data}) do
|
||||||
|
{:stop, {:error, {:database_loading_timeout, database}}, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
def loading(:state_timeout, :load, {[], data}) do
|
||||||
|
{:next_state, :available, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
def loading(:state_timeout, :load, {[database | databases], data} = state) do
|
||||||
|
command = case database do
|
||||||
|
:default -> {:add_default_database, nil}
|
||||||
|
path -> {:add_database, database}
|
||||||
|
end
|
||||||
|
send(data.port, command)
|
||||||
|
{:keep_state, state, {:state_timeout, data.startup_timeout, :load_timeout}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def loading(:info, {port, {:data, response}}, {[database | databases], %{port: port} = data}) do
|
||||||
|
case :erlang.binary_to_term(response) do
|
||||||
|
{:ok, :loaded} ->
|
||||||
|
{:keep_state, {databases, data}, {:state_timeout, 0, :load}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def loading(:info, {port, {:exit_status, 1}}, {[database | _], %{port: port} = data}) do
|
||||||
|
{:stop, {:error, {:database_not_found, database}}, data}
|
||||||
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
def loading({:call, from}, :status, {[database | _], data}) do
|
||||||
|
handle_status_call(from, :loading, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def loading({:call, _from}, {:perform, _path}, _data) do
|
||||||
|
{:keep_state_and_data, :postpone}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def available(:enter, _old_state, %{request: {:reload, from, _}}) do
|
||||||
|
response = {:reply, from, :ok}
|
||||||
|
{:keep_state_and_data, response}
|
||||||
|
end
|
||||||
|
|
||||||
def available(:enter, _old_state, %{request: nil}) do
|
def available(:enter, _old_state, %{request: nil}) do
|
||||||
:keep_state_and_data
|
:keep_state_and_data
|
||||||
end
|
end
|
||||||
|
@ -225,6 +301,17 @@ defmodule GenMagic.Server do
|
||||||
{:next_state, :processing, data}
|
{:next_state, :processing, data}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def available({:call, from}, {:reload, databases}, data) do
|
||||||
|
send(data.port, {:reload, :reload})
|
||||||
|
{:next_state, :starting,
|
||||||
|
%{data | database_patterns: databases || data.database_patterns, request: {:reload, from, :reload}}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def available({:call, from}, {:recycle, databases}, data) do
|
||||||
|
{:next_state, :recycling,
|
||||||
|
%{data | database_patterns: databases || data.database_patterns, request: {:reload, from, :recycle}}}
|
||||||
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def available({:call, from}, :status, data) do
|
def available({:call, from}, :status, data) do
|
||||||
handle_status_call(from, :available, data)
|
handle_status_call(from, :available, data)
|
||||||
|
@ -245,6 +332,12 @@ defmodule GenMagic.Server do
|
||||||
handle_status_call(from, :processing, data)
|
handle_status_call(from, :processing, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def processing(:state_timeout, _, %{port: port, request: {_, from, _}} = data) do
|
||||||
|
response = {:reply, from, {:error, :timeout}}
|
||||||
|
{:next_state, :recycling, %{data | request: nil}, [response, :hibernate]}
|
||||||
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def processing(:info, {port, {:data, response}}, %{port: port, request: {_, from, _}} = data) do
|
def processing(:info, {port, {:data, response}}, %{port: port, request: {_, from, _}} = data) do
|
||||||
response = {:reply, from, handle_response(response)}
|
response = {:reply, from, handle_response(response)}
|
||||||
|
@ -253,9 +346,9 @@ defmodule GenMagic.Server do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def recycling(:enter, _, %{request: nil, port: port} = data) when is_port(port) do
|
def recycling(:enter, _, %{port: port} = data) when is_port(port) do
|
||||||
send(data.port, {:stop, :recycle})
|
send(data.port, {:stop, :recycle})
|
||||||
{:keep_state_and_data, data.startup_timeout}
|
{:keep_state_and_data, {:state_timeout, data.startup_timeout, :stop}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
@ -268,8 +361,22 @@ defmodule GenMagic.Server do
|
||||||
handle_status_call(from, :recycling, data)
|
handle_status_call(from, :recycling, data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# In case of timeout, force close.
|
||||||
|
def recycling(:state_timeout, :stop, data) do
|
||||||
|
Kernel.send(data.port, {self(), :close})
|
||||||
|
{:keep_state_and_data, {:state_timeout, data.startup_timeout, :close}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def recycling(:state_timeout, :close, data) do
|
||||||
|
{:stop, {:error, :port_close_failed}}
|
||||||
|
end
|
||||||
|
|
||||||
|
def recycling(:info, {port, :closed}, %{port: port} = data) do
|
||||||
|
{:next_state, :starting, %{data | port: nil, cycles: 0}}
|
||||||
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def recycling(:info, {port, {:exit_status, 0}}, %{port: port} = data) do
|
def recycling(:info, {port, {:exit_status, _}}, %{port: port} = data) do
|
||||||
{:next_state, :starting, %{data | port: nil, cycles: 0}}
|
{:next_state, :starting, %{data | port: nil, cycles: 0}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -21,5 +21,6 @@ defmodule GenMagic.Server.Data do
|
||||||
process_timeout: :infinity,
|
process_timeout: :infinity,
|
||||||
recycle_threshold: :infinity,
|
recycle_threshold: :infinity,
|
||||||
cycles: 0,
|
cycles: 0,
|
||||||
|
database_patterns: nil,
|
||||||
request: nil
|
request: nil
|
||||||
end
|
end
|
||||||
|
|
206
src/apprentice.c
206
src/apprentice.c
|
@ -6,35 +6,35 @@
|
||||||
// yum or brew. Refer to the Makefile for further reference.
|
// yum or brew. Refer to the Makefile for further reference.
|
||||||
//
|
//
|
||||||
// This program is designed to run interactively as a backend daemon to the
|
// This program is designed to run interactively as a backend daemon to the
|
||||||
// GenMagic library, and follows the command line pattern:
|
// GenMagic library.
|
||||||
//
|
|
||||||
// $ apprentice --database-file <file> --database-default
|
|
||||||
//
|
|
||||||
// Where each argument either refers to a compiled or uncompiled magic database,
|
|
||||||
// or the default database. They will be loaded in the sequence that they were
|
|
||||||
// specified. Note that you must specify at least one database.
|
|
||||||
//
|
//
|
||||||
// Communication is done over STDIN/STDOUT as binary packets of 2 bytes length
|
// Communication is done over STDIN/STDOUT as binary packets of 2 bytes length
|
||||||
// plus X bytes payload, where the payload is an erlang term encoded with
|
// plus X bytes payload, where the payload is an erlang term encoded with
|
||||||
// :erlang.term_to_binary/1 and decoded with :erlang.binary_to_term/1.
|
// :erlang.term_to_binary/1 and decoded with :erlang.binary_to_term/1.
|
||||||
//
|
//
|
||||||
// Once the program is ready, it sends the `:ready` atom. The startup can fail
|
// Once the program is ready, it sends the `:ready` atom.
|
||||||
// for multiples reasons, and the program will exit accordingly:
|
//
|
||||||
// - 1: No database
|
// It is then up to the Erlang side to load databases, by sending messages:
|
||||||
// - 2: Missing/Bad argument
|
// - `{:add_database, path}`
|
||||||
// - 3: Missing database
|
// - `{:add_default_database, _}`
|
||||||
|
//
|
||||||
|
// If the requested database have been loaded, an `{:ok, :loaded}` message will
|
||||||
|
// follow. Otherwise, the process will exit (exit code 1).
|
||||||
//
|
//
|
||||||
// Commands are sent to the program STDIN as an erlang term of `{Operation,
|
// Commands are sent to the program STDIN as an erlang term of `{Operation,
|
||||||
// Argument}`, and response of `{:ok | :error, Response}`.
|
// Argument}`, and response of `{:ok | :error, Response}`.
|
||||||
//
|
//
|
||||||
// Invalid packets will cause the program to exit (exit code 4). This will
|
// Invalid packets will cause the program to exit (exit code 3). This will
|
||||||
// happen if your Erlang Term format doesn't match the version the program has
|
// happen if your Erlang Term format doesn't match the version the program has
|
||||||
// been compiled with, or if you send a command too huge.
|
// been compiled with, or if you send a command too huge.
|
||||||
//
|
//
|
||||||
// The program may exit with error codes 5 or 255 if something went wrong (such
|
// The program may exit with exit code 3 if something went wrong with ei_*
|
||||||
// as error allocating terms, or if stdin is lost).
|
// functions.
|
||||||
//
|
//
|
||||||
// Commands:
|
// Commands:
|
||||||
|
// {:reload, _} :: :ready
|
||||||
|
// {:add_database, String.t()} :: {:ok, _} | {:error, _}
|
||||||
|
// {:add_default_database, _} :: {:ok, _} | {:error, _}
|
||||||
// {:file, path :: String.t()} :: {:ok, {type, encoding, name}} | {:error,
|
// {:file, path :: String.t()} :: {:ok, {type, encoding, name}} | {:error,
|
||||||
// :badarg} | {:error, {errno :: integer(), String.t()}}
|
// :badarg} | {:error, {errno :: integer(), String.t()}}
|
||||||
// {:bytes, binary()} :: same as :file
|
// {:bytes, binary()} :: same as :file
|
||||||
|
@ -55,11 +55,9 @@
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#define ERROR_OK 0
|
#define ERROR_OK 0
|
||||||
#define ERROR_NO_DATABASE 1
|
#define ERROR_DB 1
|
||||||
#define ERROR_NO_ARGUMENT 2
|
#define ERROR_EI 2
|
||||||
#define ERROR_MISSING_DATABASE 3
|
#define ERROR_BAD_TERM 3
|
||||||
#define ERROR_EI 4
|
|
||||||
#define ERROR_BAD_TERM 5
|
|
||||||
|
|
||||||
// We use a bigger than possible valid command length (around 4111 bytes) to
|
// We use a bigger than possible valid command length (around 4111 bytes) to
|
||||||
// allow more precise errors when using too long paths.
|
// allow more precise errors when using too long paths.
|
||||||
|
@ -72,6 +70,7 @@ magic_t magic_setup(int flags);
|
||||||
#define EI_ENSURE(result) \
|
#define EI_ENSURE(result) \
|
||||||
do { \
|
do { \
|
||||||
if (result != 0) { \
|
if (result != 0) { \
|
||||||
|
fprintf(stderr, "EI ERROR, line: %d", __LINE__); \
|
||||||
exit(ERROR_EI); \
|
exit(ERROR_EI); \
|
||||||
} \
|
} \
|
||||||
} while (0);
|
} while (0);
|
||||||
|
@ -79,10 +78,8 @@ magic_t magic_setup(int flags);
|
||||||
typedef char byte;
|
typedef char byte;
|
||||||
|
|
||||||
void setup_environment();
|
void setup_environment();
|
||||||
void setup_options(int argc, char **argv);
|
void magic_open_all();
|
||||||
void setup_options_file(char *optarg);
|
int magic_load_all(char *path);
|
||||||
void setup_options_default();
|
|
||||||
void setup_system();
|
|
||||||
int process_command(uint16_t len, byte *buf);
|
int process_command(uint16_t len, byte *buf);
|
||||||
void process_file(char *path, ei_x_buff *result);
|
void process_file(char *path, ei_x_buff *result);
|
||||||
void process_bytes(char *bytes, int size, ei_x_buff *result);
|
void process_bytes(char *bytes, int size, ei_x_buff *result);
|
||||||
|
@ -92,28 +89,14 @@ void error(ei_x_buff *result, const char *error);
|
||||||
void handle_magic_error(magic_t handle, int errn, ei_x_buff *result);
|
void handle_magic_error(magic_t handle, int errn, ei_x_buff *result);
|
||||||
void fdseek(uint16_t count);
|
void fdseek(uint16_t count);
|
||||||
|
|
||||||
struct magic_file {
|
|
||||||
struct magic_file *prev;
|
|
||||||
struct magic_file *next;
|
|
||||||
char *path;
|
|
||||||
};
|
|
||||||
|
|
||||||
static struct magic_file *magic_database;
|
|
||||||
static magic_t magic_mime_type; // MAGIC_MIME_TYPE
|
static magic_t magic_mime_type; // MAGIC_MIME_TYPE
|
||||||
static magic_t magic_mime_encoding; // MAGIC_MIME_ENCODING
|
static magic_t magic_mime_encoding; // MAGIC_MIME_ENCODING
|
||||||
static magic_t magic_type_name; // MAGIC_NONE
|
static magic_t magic_type_name; // MAGIC_NONE
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
ei_init();
|
EI_ENSURE(ei_init());
|
||||||
setup_environment();
|
setup_environment();
|
||||||
setup_options(argc, argv);
|
magic_open_all();
|
||||||
setup_system();
|
|
||||||
|
|
||||||
ei_x_buff ok_buf;
|
|
||||||
EI_ENSURE(ei_x_new_with_version(&ok_buf));
|
|
||||||
EI_ENSURE(ei_x_encode_atom(&ok_buf, "ready"));
|
|
||||||
write_cmd(ok_buf.buff, ok_buf.index);
|
|
||||||
EI_ENSURE(ei_x_free(&ok_buf));
|
|
||||||
|
|
||||||
byte buf[COMMAND_BUFFER_SIZE];
|
byte buf[COMMAND_BUFFER_SIZE];
|
||||||
uint16_t len;
|
uint16_t len;
|
||||||
|
@ -158,6 +141,7 @@ int process_command(uint16_t len, byte *buf) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// {:file, path}
|
||||||
if (strlen(atom) == 4 && strncmp(atom, "file", 4) == 0) {
|
if (strlen(atom) == 4 && strncmp(atom, "file", 4) == 0) {
|
||||||
char path[4097];
|
char path[4097];
|
||||||
ei_get_type(buf, &index, &termtype, &termsize);
|
ei_get_type(buf, &index, &termtype, &termsize);
|
||||||
|
@ -176,6 +160,7 @@ int process_command(uint16_t len, byte *buf) {
|
||||||
error(&result, "badarg");
|
error(&result, "badarg");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// {:bytes, bytes}
|
||||||
} else if (strlen(atom) == 5 && strncmp(atom, "bytes", 5) == 0) {
|
} else if (strlen(atom) == 5 && strncmp(atom, "bytes", 5) == 0) {
|
||||||
int termtype;
|
int termtype;
|
||||||
int termsize;
|
int termsize;
|
||||||
|
@ -191,8 +176,47 @@ int process_command(uint16_t len, byte *buf) {
|
||||||
error(&result, "badarg");
|
error(&result, "badarg");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// {:add_database, path}
|
||||||
|
} else if (strlen(atom) == 12 && strncmp(atom, "add_database", 12) == 0) {
|
||||||
|
char path[4097];
|
||||||
|
ei_get_type(buf, &index, &termtype, &termsize);
|
||||||
|
|
||||||
|
if (termtype == ERL_BINARY_EXT) {
|
||||||
|
if (termsize < 4096) {
|
||||||
|
long bin_length;
|
||||||
|
EI_ENSURE(ei_decode_binary(buf, &index, path, &bin_length));
|
||||||
|
path[termsize] = '\0';
|
||||||
|
if (magic_load_all(path) == 0) {
|
||||||
|
EI_ENSURE(ei_x_encode_atom(&result, "ok"));
|
||||||
|
EI_ENSURE(ei_x_encode_atom(&result, "loaded"));
|
||||||
|
} else {
|
||||||
|
exit(ERROR_DB);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error(&result, "enametoolong");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error(&result, "badarg");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// {:add_default_database, _}
|
||||||
|
} else if (strlen(atom) == 20 &&
|
||||||
|
strncmp(atom, "add_default_database", 20) == 0) {
|
||||||
|
if (magic_load_all(NULL) == 0) {
|
||||||
|
EI_ENSURE(ei_x_encode_atom(&result, "ok"));
|
||||||
|
EI_ENSURE(ei_x_encode_atom(&result, "loaded"));
|
||||||
|
} else {
|
||||||
|
exit(ERROR_DB);
|
||||||
|
}
|
||||||
|
// {:reload, _}
|
||||||
|
} else if (strlen(atom) == 6 && strncmp(atom, "reload", 6) == 0) {
|
||||||
|
magic_open_all();
|
||||||
|
return 0;
|
||||||
|
// {:stop, _}
|
||||||
} else if (strlen(atom) == 4 && strncmp(atom, "stop", 4) == 0) {
|
} else if (strlen(atom) == 4 && strncmp(atom, "stop", 4) == 0) {
|
||||||
exit(ERROR_OK);
|
exit(ERROR_OK);
|
||||||
|
// badarg
|
||||||
} else {
|
} else {
|
||||||
error(&result, "badarg");
|
error(&result, "badarg");
|
||||||
return 1;
|
return 1;
|
||||||
|
@ -206,88 +230,40 @@ int process_command(uint16_t len, byte *buf) {
|
||||||
|
|
||||||
void setup_environment() { opterr = 0; }
|
void setup_environment() { opterr = 0; }
|
||||||
|
|
||||||
void setup_options(int argc, char **argv) {
|
void magic_open_all() {
|
||||||
const char *option_string = "f:";
|
if (magic_mime_encoding) {
|
||||||
static struct option long_options[] = {
|
magic_close(magic_mime_encoding);
|
||||||
{"database-file", required_argument, 0, 'f'},
|
}
|
||||||
{"database-default", no_argument, 0, 'd'},
|
if (magic_mime_type) {
|
||||||
{0, 0, 0, 0}};
|
magic_close(magic_mime_type);
|
||||||
|
}
|
||||||
|
if (magic_type_name) {
|
||||||
|
magic_close(magic_type_name);
|
||||||
|
}
|
||||||
|
magic_mime_encoding = magic_open(MAGIC_FLAGS_COMMON | MAGIC_MIME_ENCODING);
|
||||||
|
magic_mime_type = magic_open(MAGIC_FLAGS_COMMON | MAGIC_MIME_TYPE);
|
||||||
|
magic_type_name = magic_open(MAGIC_FLAGS_COMMON | MAGIC_NONE);
|
||||||
|
|
||||||
int option_character;
|
ei_x_buff ok_buf;
|
||||||
while (1) {
|
EI_ENSURE(ei_x_new_with_version(&ok_buf));
|
||||||
int option_index = 0;
|
EI_ENSURE(ei_x_encode_atom(&ok_buf, "ready"));
|
||||||
option_character =
|
write_cmd(ok_buf.buff, ok_buf.index);
|
||||||
getopt_long(argc, argv, option_string, long_options, &option_index);
|
EI_ENSURE(ei_x_free(&ok_buf));
|
||||||
if (-1 == option_character) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
switch (option_character) {
|
|
||||||
case 'f': {
|
|
||||||
setup_options_file(optarg);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'd': {
|
|
||||||
setup_options_default();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case '?':
|
|
||||||
default: {
|
|
||||||
exit(ERROR_NO_ARGUMENT);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup_options_file(char *optarg) {
|
int magic_load_all(char *path) {
|
||||||
if (0 != access(optarg, R_OK)) {
|
int res;
|
||||||
exit(ERROR_MISSING_DATABASE);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct magic_file *next = malloc(sizeof(struct magic_file));
|
if ((res = magic_load(magic_mime_encoding, path)) != 0) {
|
||||||
size_t path_length = strlen(optarg) + 1;
|
return res;
|
||||||
char *path = malloc(path_length);
|
|
||||||
memcpy(path, optarg, path_length);
|
|
||||||
next->path = path;
|
|
||||||
next->prev = magic_database;
|
|
||||||
if (magic_database) {
|
|
||||||
magic_database->next = next;
|
|
||||||
}
|
}
|
||||||
magic_database = next;
|
if ((res = magic_load(magic_mime_type, path)) != 0) {
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
if ((res = magic_load(magic_type_name, path)) != 0) {
|
||||||
void setup_options_default() {
|
return res;
|
||||||
struct magic_file *next = malloc(sizeof(struct magic_file));
|
|
||||||
next->path = NULL;
|
|
||||||
next->prev = magic_database;
|
|
||||||
if (magic_database) {
|
|
||||||
magic_database->next = next;
|
|
||||||
}
|
}
|
||||||
magic_database = next;
|
return 0;
|
||||||
}
|
|
||||||
|
|
||||||
void setup_system() {
|
|
||||||
magic_mime_encoding = magic_setup(MAGIC_FLAGS_COMMON | MAGIC_MIME_ENCODING);
|
|
||||||
magic_mime_type = magic_setup(MAGIC_FLAGS_COMMON | MAGIC_MIME_TYPE);
|
|
||||||
magic_type_name = magic_setup(MAGIC_FLAGS_COMMON | MAGIC_NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
magic_t magic_setup(int flags) {
|
|
||||||
|
|
||||||
magic_t magic = magic_open(flags);
|
|
||||||
struct magic_file *current_database = magic_database;
|
|
||||||
if (!current_database) {
|
|
||||||
exit(ERROR_NO_DATABASE);
|
|
||||||
}
|
|
||||||
|
|
||||||
while (current_database->prev) {
|
|
||||||
current_database = current_database->prev;
|
|
||||||
}
|
|
||||||
while (current_database) {
|
|
||||||
magic_load(magic, current_database->path);
|
|
||||||
current_database = current_database->next;
|
|
||||||
}
|
|
||||||
return magic;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void process_bytes(char *path, int size, ei_x_buff *result) {
|
void process_bytes(char *path, int size, ei_x_buff *result) {
|
||||||
|
|
|
@ -7,49 +7,51 @@ defmodule GenMagic.ApprenticeTest do
|
||||||
test "sends ready" do
|
test "sends ready" do
|
||||||
port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
|
port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
|
||||||
on_exit(fn() -> send(port, {self(), :close}) end)
|
on_exit(fn() -> send(port, {self(), :close}) end)
|
||||||
assert_ready(port)
|
assert_ready_and_init_default(port)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "stops" do
|
test "stops" do
|
||||||
port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
|
port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
|
||||||
on_exit(fn() -> send(port, {self(), :close}) end)
|
on_exit(fn() -> send(port, {self(), :close}) end)
|
||||||
assert_ready(port)
|
assert_ready_and_init_default(port)
|
||||||
send(port, {self(), {:command, :erlang.term_to_binary({:stop, :stop})}})
|
send(port, {self(), {:command, :erlang.term_to_binary({:stop, :stop})}})
|
||||||
assert_receive {^port, {:exit_status, 0}}
|
assert_receive {^port, {:exit_status, 0}}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exits with no database" do
|
test "exits with non existent database with an error" do
|
||||||
opts = [:use_stdio, :binary, :exit_status, {:packet, 2}, {:args, []}]
|
opts = [:use_stdio, :binary, :exit_status, {:packet, 2}, {:args, []}]
|
||||||
port = Port.open(GenMagic.Config.get_port_name(), opts)
|
port = Port.open(GenMagic.Config.get_port_name(), opts)
|
||||||
on_exit(fn() -> send(port, {self(), :close}) end)
|
on_exit(fn() -> send(port, {self(), :close}) end)
|
||||||
|
assert_ready(port)
|
||||||
|
send(port, {self(), {:command, :erlang.term_to_binary({:add_database, "/somewhere/nowhere"})}})
|
||||||
assert_receive {^port, {:exit_status, 1}}
|
assert_receive {^port, {:exit_status, 1}}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exits with a non existent database" do
|
#test "exits with a non existent database" do
|
||||||
opts = [
|
# opts = [
|
||||||
{:args, ["--database-file", "/no/such/database"]},
|
# {:args, ["--database-file", "/no/such/database"]},
|
||||||
:use_stdio,
|
# :use_stdio,
|
||||||
:binary,
|
# :binary,
|
||||||
:exit_status,
|
# :exit_status,
|
||||||
{:packet, 2}
|
# {:packet, 2}
|
||||||
]
|
# ]
|
||||||
|
#
|
||||||
port = Port.open(GenMagic.Config.get_port_name(), opts)
|
# port = Port.open(GenMagic.Config.get_port_name(), opts)
|
||||||
on_exit(fn() -> send(port, {self(), :close}) end)
|
# on_exit(fn() -> send(port, {self(), :close}) end)
|
||||||
assert_receive {^port, {:exit_status, 3}}
|
# assert_receive {^port, {:exit_status, 3}}
|
||||||
end
|
#end
|
||||||
|
|
||||||
describe "port" do
|
describe "port" do
|
||||||
setup do
|
setup do
|
||||||
port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
|
port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
|
||||||
on_exit(fn() -> send(port, {self(), :close}) end)
|
on_exit(fn() -> send(port, {self(), :close}) end)
|
||||||
assert_ready(port)
|
assert_ready_and_init_default(port)
|
||||||
%{port: port}
|
%{port: port}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exits with badly formatted erlang terms", %{port: port} do
|
test "exits with badly formatted erlang terms", %{port: port} do
|
||||||
send(port, {self(), {:command, "i forgot to term_to_binary!!"}})
|
send(port, {self(), {:command, "i forgot to term_to_binary!!"}})
|
||||||
assert_receive {^port, {:exit_status, 5}}
|
assert_receive {^port, {:exit_status, 3}}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "errors with wrong command", %{port: port} do
|
test "errors with wrong command", %{port: port} do
|
||||||
|
@ -140,6 +142,14 @@ defmodule GenMagic.ApprenticeTest do
|
||||||
assert :ready == :erlang.binary_to_term(data)
|
assert :ready == :erlang.binary_to_term(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assert_ready_and_init_default(port) do
|
||||||
|
assert_receive {^port, {:data, data}}
|
||||||
|
assert :ready == :erlang.binary_to_term(data)
|
||||||
|
send(port, {self(), {:command, :erlang.term_to_binary({:add_default_database, nil})}})
|
||||||
|
assert_receive {^port, {:data, data}}
|
||||||
|
assert {:ok, _} = :erlang.binary_to_term(data)
|
||||||
|
end
|
||||||
|
|
||||||
def too_big(path, filename, limit \\ 4095) do
|
def too_big(path, filename, limit \\ 4095) do
|
||||||
last_len = byte_size(filename)
|
last_len = byte_size(filename)
|
||||||
path_len = byte_size(path)
|
path_len = byte_size(path)
|
||||||
|
|
|
@ -24,7 +24,6 @@ defmodule GenMagicTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Non-existent file" do
|
test "Non-existent file" do
|
||||||
Process.flag(:trap_exit, true)
|
|
||||||
{:ok, pid} = GenMagic.Server.start_link([])
|
{:ok, pid} = GenMagic.Server.start_link([])
|
||||||
path = missing_filename()
|
path = missing_filename()
|
||||||
assert_no_file(GenMagic.Server.perform(pid, path))
|
assert_no_file(GenMagic.Server.perform(pid, path))
|
||||||
|
@ -41,10 +40,16 @@ defmodule GenMagicTest do
|
||||||
assert "text/x-makefile" = result.mime_type
|
assert "text/x-makefile" = result.mime_type
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Custom database file recognises Elixir files" do
|
describe "custom database" do
|
||||||
|
|
||||||
|
setup do
|
||||||
database = absolute_path("elixir.mgc")
|
database = absolute_path("elixir.mgc")
|
||||||
on_exit(fn() -> File.rm(database) end)
|
on_exit(fn() -> File.rm(database) end)
|
||||||
{_, 0} = System.cmd("file", ["-C", "-m", absolute_path("test/elixir")])
|
{_, 0} = System.cmd("file", ["-C", "-m", absolute_path("test/elixir")])
|
||||||
|
[database: database]
|
||||||
|
end
|
||||||
|
|
||||||
|
test "recognises Elixir files", %{database: database} do
|
||||||
{:ok, pid} = GenMagic.Server.start_link(database_patterns: [database])
|
{:ok, pid} = GenMagic.Server.start_link(database_patterns: [database])
|
||||||
path = absolute_path("mix.exs")
|
path = absolute_path("mix.exs")
|
||||||
assert {:ok, %Result{} = result} = GenMagic.Server.perform(pid, path)
|
assert {:ok, %Result{} = result} = GenMagic.Server.perform(pid, path)
|
||||||
|
@ -52,4 +57,16 @@ defmodule GenMagicTest do
|
||||||
assert "us-ascii" = result.encoding
|
assert "us-ascii" = result.encoding
|
||||||
assert "Elixir module source text" = result.content
|
assert "Elixir module source text" = result.content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "recognises Elixir files after a reload", %{database: database} do
|
||||||
|
{:ok, pid} = GenMagic.Server.start_link([])
|
||||||
|
path = absolute_path("mix.exs")
|
||||||
|
{:ok, %Result{mime_type: mime}} = GenMagic.Server.perform(pid, path)
|
||||||
|
refute mime == "text/x-elixir"
|
||||||
|
:ok = GenMagic.Server.reload(pid, [database])
|
||||||
|
assert {:ok, %Result{mime_type: "text/x-elixir"}} = GenMagic.Server.perform(pid, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -31,4 +31,15 @@ defmodule GenMagic.ServerTest do
|
||||||
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
|
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "recycle" do
|
||||||
|
{:ok, pid} = GenMagic.Server.start_link([])
|
||||||
|
path = absolute_path("Makefile")
|
||||||
|
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
|
||||||
|
assert {:ok, _} = GenMagic.Server.perform(pid, path)
|
||||||
|
assert {:ok, %{cycles: 1}} = GenMagic.Server.status(pid)
|
||||||
|
assert :ok = GenMagic.Server.recycle(pid)
|
||||||
|
assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue