Page MenuHomePhorge

No OneTemporary

Size
60 KB
Referenced Files
None
Subscribers
None
diff --git a/.builds/alpine.yaml b/.builds/alpine.yaml
new file mode 100644
index 0000000..0c8a630
--- /dev/null
+++ b/.builds/alpine.yaml
@@ -0,0 +1,20 @@
+image: alpine/latest
+packages:
+ - g++
+ - make
+ - elixir
+ - file
+ - file-dev
+sources:
+ - https://git.sr.ht/~href/gen_magic
+tasks:
+ - setup: |
+ mix local.hex --force
+ - build: |
+ cd gen_magic
+ mix deps.get
+ MIX_ENV=test mix compile
+ - test: |
+ cd gen_magic
+ mix test
+
diff --git a/.builds/archlinux.yaml b/.builds/archlinux.yaml
new file mode 100644
index 0000000..4dc0c26
--- /dev/null
+++ b/.builds/archlinux.yaml
@@ -0,0 +1,17 @@
+image: archlinux
+packages:
+ - elixir
+ - file
+sources:
+ - https://git.sr.ht/~href/gen_magic
+tasks:
+ - setup: |
+ mix local.hex --force
+ - build: |
+ cd gen_magic
+ mix deps.get
+ MIX_ENV=test mix compile
+ - test: |
+ cd gen_magic
+ mix test
+
diff --git a/.builds/debian-oldstable.yaml b/.builds/debian-oldstable.yaml
new file mode 100644
index 0000000..915f1a2
--- /dev/null
+++ b/.builds/debian-oldstable.yaml
@@ -0,0 +1,20 @@
+image: debian/oldstable
+packages:
+ - build-essential
+ - erlang
+ - erlang-dev
+ - elixir
+ - libmagic-dev
+sources:
+ - https://git.sr.ht/~href/gen_magic
+tasks:
+ - setup: |
+ mix local.hex --force
+ - build: |
+ cd gen_magic
+ mix deps.get
+ MIX_ENV=test mix compile
+ - test: |
+ cd gen_magic
+ mix test
+
diff --git a/.builds/debian-stable.yaml b/.builds/debian-stable.yaml
new file mode 100644
index 0000000..d6bdbe2
--- /dev/null
+++ b/.builds/debian-stable.yaml
@@ -0,0 +1,20 @@
+image: debian/stable
+packages:
+ - build-essential
+ - erlang
+ - erlang-dev
+ - elixir
+ - libmagic-dev
+sources:
+ - https://git.sr.ht/~href/gen_magic
+tasks:
+ - setup: |
+ mix local.hex --force
+ - build: |
+ cd gen_magic
+ mix deps.get
+ MIX_ENV=test mix compile
+ - test: |
+ cd gen_magic
+ mix test
+
diff --git a/.builds/debian-testing.yaml b/.builds/debian-testing.yaml
new file mode 100644
index 0000000..bda46e4
--- /dev/null
+++ b/.builds/debian-testing.yaml
@@ -0,0 +1,20 @@
+image: debian/testing
+packages:
+ - build-essential
+ - erlang
+ - erlang-dev
+ - elixir
+ - libmagic-dev
+sources:
+ - https://git.sr.ht/~hrefhref/gen_magic
+tasks:
+ - setup: |
+ mix local.hex --force
+ - build: |
+ cd gen_magic
+ mix deps.get
+ MIX_ENV=test mix compile
+ - test: |
+ cd gen_magic
+ mix test
+
diff --git a/.builds/fedora-latest.yaml b/.builds/fedora-latest.yaml
new file mode 100644
index 0000000..ba87865
--- /dev/null
+++ b/.builds/fedora-latest.yaml
@@ -0,0 +1,22 @@
+image: fedora/latest
+packages:
+ - make
+ - gcc
+ - kernel-devel
+ - erlang
+ - elixir
+ - file-devel
+sources:
+ - https://git.sr.ht/~href/gen_magic
+tasks:
+ - setup: |
+ sudo dnf -y group install 'Development Tools'
+ mix local.hex --force
+ - build: |
+ cd gen_magic
+ mix deps.get
+ MIX_ENV=test mix compile
+ - test: |
+ cd gen_magic
+ mix test
+
diff --git a/.builds/freebsd.yaml b/.builds/freebsd.yaml
new file mode 100644
index 0000000..3a78947
--- /dev/null
+++ b/.builds/freebsd.yaml
@@ -0,0 +1,17 @@
+image: freebsd/latest
+packages:
+ - elixir
+ - gmake
+sources:
+ - https://git.sr.ht/~href/gen_magic
+tasks:
+ - setup: |
+ mix local.hex --force
+ - build: |
+ cd gen_magic
+ mix deps.get
+ MIX_ENV=test mix compile
+ - test: |
+ cd gen_magic
+ mix test
+
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c8ad9a..d320cd5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,42 +1,59 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog][1], and this project adheres to [Semantic Versioning][2].
[1]: https://keepachangelog.com/en/1.0.0/
[2]: https://semver.org/spec/v2.0.0.html
-## [Unreleased]
+## majic [Unreleased]
+
+## Added
+
+- Forked gen_magic.
+- Pool: `Majic.Pool`
+- Unified API: `Majic.perform/1,2,3`
+
+## Changed
+
+- C port now using erl_interface
+- `Majic.Server.reload/2,3`
+- `Majic.Server.recycle/2,3`
+- Bytes support: `Majic.Server.perform(ref, {:bytes, <<>>})`
+- Builds on Musl
+- Better error and timeout handling
+
+## gen_majic [1.0]
### Added
- Added support for process recycling (evadne).
- Added documentation (evadne).
### Changed
- Replaced GenServer with `:gen_statem` (evadne).
- Changed API; added support for customisation.
- Refined tests and other aspects of the library (evadne).
## [0.20.83]
### Added
- Soak testing script (devstopfix)
### Changed
- Replaced Erlexec usage with Port (devstopfix)
## 0.0.1
### Added
- Initial Elixir wrapper with Erlexec (evadne)
- Intiial C program (evadne)
[unreleased]: https://github.com/evadne/gen_magic/compare/develop
[0.20.83]: https://github.com/devstopfix/gen_magic/commit/7e27fd094cb462d26ba54fde0205a5be313d12da
diff --git a/Makefile b/Makefile
index cbd6fec..ce342ea 100644
--- a/Makefile
+++ b/Makefile
@@ -1,21 +1,19 @@
-# Apprentice binary
-
ERL_EI_INCLUDE:=$(shell erl -eval 'io:format("~s", [code:lib_dir(erl_interface, include)])' -s init stop -noshell | head -1)
ERL_EI_LIB:=$(shell erl -eval 'io:format("~s", [code:lib_dir(erl_interface, lib)])' -s init stop -noshell | head -1)
CFLAGS = -std=c99 -g -Wall -Werror
CPPFLAGS = -I$(ERL_EI_INCLUDE)
LDFLAGS = -L$(ERL_EI_LIB)
LDLIBS = -lpthread -lei -lm -lmagic
PRIV = priv/
RM = rm -Rf
-all: priv/apprentice
+all: priv/libmagic_port
-priv/apprentice: src/apprentice.c
+priv/libmagic_port: src/libmagic_port.c
mkdir -p priv
$(CC) $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $^ $(LDLIBS) -o $@
clean:
$(RM) $(PRIV)
.PHONY: clean
diff --git a/README.md b/README.md
index 7d1d402..99d6e82 100644
--- a/README.md
+++ b/README.md
@@ -1,155 +1,165 @@
-# GenMagic
+# Majic
-**GenMagic** provides supervised and customisable access to [libmagic](http://man7.org/linux/man-pages/man3/libmagic.3.html) using a supervised external process.
+**Majic** provides a robust integration of [libmagic](http://man7.org/linux/man-pages/man3/libmagic.3.html) for Elixir.
-With this library, you can start an one-off process to run a single check, or run the process as a daemon if you expect to run many checks.
+With this library, you can start an one-off process to run a single check, or run the process as a daemon if you expect to run
+many checks.
+
+It is a friendly fork of [gen_magic](https://github.com/evadne/gen_magic) featuring a (arguably) more robust C-code
+using erl_interface, built in pooling, unified/clean API, and an optional Plug.
+
+This package is regulary tested on multiple platforms (Debian, macOS, Fedora, Alpine, FreeBSD) to ensure it'll work fine
+in any environment.
## Installation
-The package can be installed by adding `gen_magic` to your list of dependencies in `mix.exs`:
+The package can be installed by adding `majic` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
- {:gen_magic, "~> 1.0.0"}
+ {:majic, "~> 1.0"}
]
end
```
You must also have [libmagic](http://man7.org/linux/man-pages/man3/libmagic.3.html) installed locally with headers, alongside common compilation tools (i.e. build-essential). These can be acquired by apt-get, yum, brew, etc.
Compilation of the underlying C program is automatic and handled by [elixir_make](https://github.com/elixir-lang/elixir_make).
## Usage
-Depending on the use case, you may utilise a single (one-off) GenMagic process without reusing it as a daemon, or utilise a connection pool (such as Poolboy) in your application to run multiple persistent GenMagic processes.
+Depending on the use case, you may utilise a single (one-off) Majic process without reusing it as a daemon, or utilise a connection pool (such as Poolboy) in your application to run multiple persistent Majic processes.
-To use GenMagic directly, you can use `GenMagic.Helpers.perform_once/1`:
+To use Majic directly, you can use `Majic.Helpers.perform_once/1`:
```elixir
-iex(1)> GenMagic.perform(".", once: true)
+iex(1)> Majic.perform(".", once: true)
{:ok,
- %GenMagic.Result{
+ %Majic.Result{
content: "directory",
encoding: "binary",
mime_type: "inode/directory"
}}
```
-To use the GenMagic server as a daemon, you can start it first, keep a reference, then feed messages to it as you require:
+To use the Majic server as a daemon, you can start it first, keep a reference, then feed messages to it as you require:
```elixir
-{:ok, pid} = GenMagic.Server.start_link([])
-{:ok, result} = GenMagic.perform(path, server: pid)
+{:ok, pid} = Majic.Server.start_link([])
+{:ok, result} = Majic.perform(path, server: pid)
```
-See `GenMagic.Server.start_link/1` and `t:GenMagic.Server.option/0` for more information on startup parameters.
+See `Majic.Server.start_link/1` and `t:Majic.Server.option/0` for more information on startup parameters.
-See `GenMagic.Result` for details on the result provided.
+See `Majic.Result` for details on the result provided.
## Configuration
-When using `GenMagic.Server.start_link/1` to start a persistent server, or `GenMagic.Helpers.perform_once/2` to run an ad-hoc request, you can override specific options to suit your use case.
+When using `Majic.Server.start_link/1` to start a persistent server, or `Majic.Helpers.perform_once/2` to run an ad-hoc request, you can override specific options to suit your use case.
| Name | Default | Description |
| - | - | - |
| `:startup_timeout` | 1000 | Number of milliseconds to wait for client startup |
| `:process_timeout` | 30000 | Number of milliseconds to process each request |
| `:recycle_threshold` | 10 | Number of cycles before the C process is replaced |
| `:database_patterns` | `[:default]` | Databases to load |
-See `t:GenMagic.Server.option/0` for details.
+See `t:Majic.Server.option/0` for details.
### Use Cases
### Ad-Hoc Requests
-For ad-hoc requests, you can use the helper method `GenMagic.Helpers.perform_once/2`:
+For ad-hoc requests, you can use the helper method `Majic.Helpers.perform_once/2`:
```elixir
-iex(1)> GenMagic.perform(Path.join(File.cwd!(), "Makefile"), once: true)
+iex(1)> Majic.perform(Path.join(File.cwd!(), "Makefile"), once: true)
{:ok,
- %GenMagic.Result{
+ %Majic.Result{
content: "makefile script, ASCII text",
encoding: "us-ascii",
mime_type: "text/x-makefile"
}}
```
### Supervised Requests
The Server should be run under a supervisor which provides resiliency.
Here we run it under a supervisor:
```elixir
-iex(1)> {:ok, pid} = Supervisor.start_link([{GenMagic.Server, name: :gen_magic}], strategy: :one_for_one)
+iex(1)> {:ok, pid} = Supervisor.start_link([{Majic.Server, name: :gen_magic}], strategy: :one_for_one)
{:ok, #PID<0.199.0>}
```
Now we can ask it to inspect a file:
```elixir
-iex(2)> GenMagic.perform(Path.expand("~/.bash_history"), server: :gen_magic)
-{:ok, %GenMagic.Result{mime_type: "text/plain", encoding: "us-ascii", content: "ASCII text"}}
+iex(2)> Majic.perform(Path.expand("~/.bash_history"), server: :gen_magic)
+{:ok, %Majic.Result{mime_type: "text/plain", encoding: "us-ascii", content: "ASCII text"}}
```
Note that in this case we have opted to use a named process.
### Pool
-For concurrency *and* resiliency, you may start the `GenMagic.Pool`. By default, it will start a `GenMagic.Server`
+For concurrency *and* resiliency, you may start the `Majic.Pool`. By default, it will start a `Majic.Server`
worker per online scheduler:
You can add a pool in your application supervisor by adding it as a child:
```
children =
[
# ...
- {GenMagic.Pool, [name: YourApp.GenMagicPool, pool_size: 2]}
+ {Majic.Pool, [name: YourApp.MajicPool, pool_size: 2]}
]
- opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
+ opts = [strategy: :one_for_one, name: YourApp.Supervisor]
Supervisor.start_link(children, opts)
```
-And then you can use it with `GenMagic.perform/2` with `pool: YourApp.GenMagicPool` option:
+And then you can use it with `Majic.perform/2` with `pool: YourApp.MajicPool` option:
```
-iex(1)> GenMagic.perform(Path.expand("~/.bash_history"), pool: YourApp.GenMagicPool)
-{:ok, %GenMagic.Result{mime_type: "text/plain", encoding: "us-ascii", content: "ASCII text"}}
+iex(1)> Majic.perform(Path.expand("~/.bash_history"), pool: YourApp.MajicPool)
+{:ok, %Majic.Result{mime_type: "text/plain", encoding: "us-ascii", content: "ASCII text"}}
```
### Check Uploaded Files
If you use Phoenix, you can inspect the file from your controller:
```elixir
def upload(conn, %{"upload" => %{path: path}}) do,
- {:ok, result} = GenMagic.perform(path, server: :gen_magic)
+ {:ok, result} = Majic.perform(path, server: :gen_magic)
text(conn, "Received your file containing #{result.content}")
end
```
-Obviously, it will be more ideal if you have wrapped `GenMagic.Server` in a pool such as Poolboy, to avoid constantly starting and stopping the underlying C program.
+Obviously, it will be more ideal if you have wrapped `Majic.Server` in a pool such as Poolboy, to avoid constantly starting and stopping the underlying C program.
## Notes
### Soak Test
Run an endless cycle to prove that the program is resilient:
```bash
find /usr/share/ -name *png | xargs mix run test/soak.exs
find . -name *ex | xargs mix run test/soak.exs
```
## Acknowledgements
-During design and prototype development of this library, the Author has drawn inspiration from the following individuals, and therefore thanks all contributors for their generosity:
+During design and prototype development of this library, the Author has drawn inspiration from the following individuals, and therefore
+thanks all contributors for their generosity:
+- [Evadne Wu](https://github.com/evadne)
+ - Original work
- Mr [James Every](https://github.com/devstopfix)
- Enhanced Elixir Wrapper (based on GenServer)
- Initial Hex packaging (v.0.22)
- Soak Testing
diff --git a/lib/gen_magic.ex b/lib/majic.ex
similarity index 74%
rename from lib/gen_magic.ex
rename to lib/majic.ex
index 292dfe8..0b1b2d0 100644
--- a/lib/gen_magic.ex
+++ b/lib/majic.ex
@@ -1,40 +1,34 @@
-defmodule GenMagic do
- @moduledoc """
- Top-level namespace for GenMagic, the libmagic client for Elixir.
-
- See `GenMagic.Server` or the README for usage.
- """
-
+defmodule Majic do
@doc """
Perform on `path`.
An option of `server: ServerName`, `pool: PoolName` or `once: true` must be passed.
"""
+ @type name :: {:pool, atom()} | {:server, GenMagic.Server.t()} | {:once, true}
@type option :: name
- when name: {:pool, atom()} | {:server, GenMagic.Server.t()} | {:once, true}
@spec perform(GenMagic.Server.target(), [option()]) :: GenMagic.Server.result()
def perform(path, opts, timeout \\ 5000) do
mod = cond do
Keyword.has_key?(opts, :pool) -> {GenMagic.Pool, Keyword.get(opts, :pool)}
Keyword.has_key?(opts, :server) -> {GenMagic.Server, Keyword.get(opts, :server)}
Keyword.has_key?(opts, :once) -> {GenMagic.Helpers, nil}
true -> nil
end
if mod do
do_perform(mod, path, timeout)
else
{:error, :no_method}
end
end
defp do_perform({GenMagic.Helpers, _}, path, timeout) do
GenMagic.Helpers.perform_once(path, timeout)
end
defp do_perform({mod, name}, path, timeout) do
- mod.perform(name, path, tiemout)
+ mod.perform(name, path, timeout)
end
end
diff --git a/lib/gen_magic/config.ex b/lib/majic/config.ex
similarity index 94%
rename from lib/gen_magic/config.ex
rename to lib/majic/config.ex
index 70ae355..a591c37 100644
--- a/lib/gen_magic/config.ex
+++ b/lib/majic/config.ex
@@ -1,44 +1,44 @@
-defmodule GenMagic.Config do
+defmodule Majic.Config do
@moduledoc false
@otp_app Mix.Project.config()[:app]
- @executable_name "apprentice"
+ @executable_name "libmagic_port"
@startup_timeout 1_000
@process_timeout 30_000
@recycle_threshold :infinity
def get_port_name do
{:spawn_executable, to_charlist(get_executable_name())}
end
def get_port_options(_options) do
[:use_stdio, :binary, :exit_status, {:packet, 2}]
end
def get_startup_timeout(options) do
get_value(options, :startup_timeout, @startup_timeout)
end
def get_process_timeout(options) do
get_value(options, :process_timeout, @process_timeout)
end
def get_recycle_threshold(options) do
get_value(options, :recycle_threshold, @recycle_threshold)
end
defp get_executable_name do
Path.join(:code.priv_dir(@otp_app), @executable_name)
end
defp get(options, key, default) do
Keyword.get(options, key, default)
end
defp get_value(options, key, default) do
case get(options, key, default) do
value when is_integer(value) and value > 0 -> value
:infinity -> :infinity
_ -> raise ArgumentError, message: "Invalid #{key}"
end
end
end
diff --git a/lib/gen_magic/helpers.ex b/lib/majic/helpers.ex
similarity index 70%
rename from lib/gen_magic/helpers.ex
rename to lib/majic/helpers.ex
index 13ab3de..f4eb3cd 100644
--- a/lib/gen_magic/helpers.ex
+++ b/lib/majic/helpers.ex
@@ -1,30 +1,30 @@
-defmodule GenMagic.Helpers do
+defmodule Majic.Helpers do
@moduledoc """
Contains convenience functions for one-off use.
"""
- alias GenMagic.Result
- alias GenMagic.Server
+ alias Majic.Result
+ alias Majic.Server
@spec perform_once(Path.t() | {:bytes, binary}, [Server.option()]) ::
{:ok, Result.t()} | {:error, term()}
@doc """
Runs a one-shot process without supervision.
Useful in tests, but not recommended for actual applications.
## Example
- iex(1)> {:ok, result} = GenMagic.Helpers.perform_once(".")
+ iex(1)> {:ok, result} = Majic.Helpers.perform_once(".")
iex(2)> result
- %GenMagic.Result{content: "directory", encoding: "binary", mime_type: "inode/directory"}
+ %Majic.Result{content: "directory", encoding: "binary", mime_type: "inode/directory"}
"""
def perform_once(path, options \\ []) do
with {:ok, pid} <- Server.start_link(options),
{:ok, result} <- Server.perform(pid, path),
:ok <- Server.stop(pid) do
{:ok, result}
end
end
end
diff --git a/lib/gen_magic/pool.ex b/lib/majic/pool.ex
similarity index 87%
rename from lib/gen_magic/pool.ex
rename to lib/majic/pool.ex
index f542b30..e28aca8 100644
--- a/lib/gen_magic/pool.ex
+++ b/lib/majic/pool.ex
@@ -1,67 +1,67 @@
-defmodule GenMagic.Pool do
+defmodule Majic.Pool do
@behaviour NimblePool
- @moduledoc "Pool of `GenMagic.Server`"
+ @moduledoc "Pool of `Majic.Server`"
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
def start_link(options, pool_size \\ nil) do
pool_size = pool_size || System.schedulers_online()
NimblePool.start_link(worker: {__MODULE__, options}, pool_size: pool_size)
end
def perform(pool, path, opts \\ []) do
pool_timeout = Keyword.get(opts, :pool_timeout, 5000)
timeout = Keyword.get(opts, :timeout, 5000)
NimblePool.checkout!(
pool,
:checkout,
fn _, server ->
- {GenMagic.Server.perform(server, path, timeout), server}
+ {Majic.Server.perform(server, path, timeout), server}
end,
pool_timeout
)
end
@impl NimblePool
def init_pool(options) do
{name, options} =
case Keyword.pop(options, :name) do
{name, options} when is_atom(name) -> {name, options}
{nil, options} -> {__MODULE__, options}
{_, options} -> {nil, options}
end
if name, do: Process.register(self(), name)
{:ok, options}
end
@impl NimblePool
def init_worker(options) do
- {:ok, server} = GenMagic.Server.start_link(options || [])
+ {:ok, server} = Majic.Server.start_link(options || [])
{:ok, server, options}
end
@impl NimblePool
def handle_checkout(:checkout, _from, server) do
{:ok, server, server}
end
@impl NimblePool
def handle_checkin(_, _, server) do
{:ok, server}
end
@impl NimblePool
def terminate_worker(_reason, _worker, state) do
{:ok, state}
end
end
diff --git a/lib/gen_magic/result.ex b/lib/majic/result.ex
similarity index 96%
rename from lib/gen_magic/result.ex
rename to lib/majic/result.ex
index 0cfa361..c84b544 100644
--- a/lib/gen_magic/result.ex
+++ b/lib/majic/result.ex
@@ -1,30 +1,30 @@
-defmodule GenMagic.Result do
+defmodule Majic.Result do
@moduledoc """
Represents the results obtained from libmagic.
Please note that this struct is only returned if the underlying check has succeeded.
"""
@typedoc """
Represents the result.
Contains the MIME type, Encoding and Content fields returned by libmagic, as per the flags:
- MIME Type: `MAGIC_FLAGS_COMMON|MAGIC_MIME_TYPE`
- Encoding: `MAGIC_FLAGS_COMMON|MAGIC_MIME_ENCODING`
- Type Name (Content): `MAGIC_FLAGS_COMMON|MAGIC_NONE`
"""
@type t :: %__MODULE__{
mime_type: String.t(),
encoding: String.t(),
content: String.t()
}
@enforce_keys ~w(mime_type encoding content)a
defstruct mime_type: nil, encoding: nil, content: nil
@doc false
def build(mime_type, encoding, content) do
%__MODULE__{mime_type: mime_type, encoding: encoding, content: content}
end
end
diff --git a/lib/gen_magic/server.ex b/lib/majic/server.ex
similarity index 98%
rename from lib/gen_magic/server.ex
rename to lib/majic/server.ex
index adb3c8a..2bc9a44 100644
--- a/lib/gen_magic/server.ex
+++ b/lib/majic/server.ex
@@ -1,451 +1,451 @@
-defmodule GenMagic.Server do
+defmodule Majic.Server do
@moduledoc """
Provides access to the underlying libmagic client, which performs file introspection.
The Server needs to be supervised, since it will terminate if it receives any unexpected error.
"""
@behaviour :gen_statem
- alias GenMagic.Result
- alias GenMagic.Server.Data
- alias GenMagic.Server.Status
+ alias Majic.Result
+ alias Majic.Server.Data
+ alias Majic.Server.Status
import Kernel, except: [send: 2]
require Logger
@typedoc """
Represents the reference to the underlying server, as returned by `:gen_statem`.
"""
@type t :: :gen_statem.server_ref()
@typedoc """
Represents values accepted as startup options, which can be passed to `start_link/1`.
- `:name`: If present, this will be the registered name for the underlying process.
Note that `:gen_statem` requires `{:local, name}`, but given widespread GenServer convention,
atoms are accepted and will be converted to `{:local, name}`.
- `:startup_timeout`: Specifies how long the Server waits for the C program to initialise.
However, if the underlying C program exits, then the process exits immediately.
Can be set to `:infinity`.
- `:process_timeout`: Specifies how long the Server waits for each request to complete.
Can be set to `:infinity`.
Please note that, if you have chosen a custom timeout value, you should also pass it when
- using `GenMagic.Server.perform/3`.
+ using `Majic.Server.perform/3`.
- `:recycle_threshold`: Specifies the number of requests processed before the underlying C
program is recycled.
Can be set to `:infinity` if you do not wish for the program to be recycled.
- `:database_patterns`: Specifies what magic databases to load; you can specify a list of either
Path Patterns (see `Path.wildcard/2`) or `:default` to instruct the C program to load the
appropriate databases.
For example, if you have had to add custom magics, then you can set this value to:
[:default, "path/to/my/magic"]
"""
@database_patterns [:default]
@type option ::
{:name, atom() | :gen_statem.server_name()}
| {:startup_timeout, timeout()}
| {:process_timeout, timeout()}
| {:recycle_threshold, non_neg_integer() | :infinity}
| {:database_patterns, nonempty_list(:default | Path.t())}
@type target :: Path.t() | {:bytes, binary()}
@type result :: {:ok, Result.t()} | {:error, term() | String.t()}
@typedoc """
Current state of the Server:
- `:pending`: This is the initial state; the Server will attempt to start the underlying Port
and the libmagic client, then automatically transition to either Available or Crashed.
- `:available`: This is the default state. In this state the Server is able to accept requests
and they will be replied in the same order.
- `:processing`: This is the state the Server will be in if it is processing requests. In this
state, further requests can still be lodged and they will be processed when the Server is
available again.
For proper concurrency, use a process pool like Poolboy, Sbroker, etc.
- `:recycling`: This is the state the Server will be in, if its underlying C program needs to be
recycled. This state is triggered whenever the cycle count reaches the defined value as per
`:recycle_threshold`.
In this state, the Server is able to accept requests, but they will not be processed until the
underlying C server program has been started again.
"""
@type state :: :starting | :processing | :available | :recycling
@spec child_spec([option()]) :: Supervisor.child_spec()
@spec start_link([option()]) :: :gen_statem.start_ret()
@spec perform(t(), target(), timeout()) :: result()
@spec status(t(), timeout()) :: {:ok, Status.t()} | {:error, term()}
@spec stop(t(), term(), timeout()) :: :ok
@doc """
Returns the default Child Specification for this Server for use in Supervisors.
You can override this with `Supervisor.child_spec/2` as required.
"""
def child_spec(options) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [options]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
@doc """
Starts a new Server.
See `t:option/0` for further details.
"""
def start_link(options) do
{name, options} = Keyword.pop(options, :name)
case name do
nil -> :gen_statem.start_link(__MODULE__, options, [])
name when is_atom(name) -> :gen_statem.start_link({:local, name}, __MODULE__, options, [])
{:global, _} -> :gen_statem.start_link(name, __MODULE__, options, [])
{:via, _, _} -> :gen_statem.start_link(name, __MODULE__, options, [])
{:local, _} -> :gen_statem.start_link(name, __MODULE__, options, [])
end
end
@doc """
Determines the type of the file provided.
"""
def perform(server_ref, path, timeout \\ 5000) do
case :gen_statem.call(server_ref, {:perform, path}, timeout) do
{:ok, %Result{} = result} -> {:ok, result}
{:error, reason} -> {:error, reason}
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 """
Returns status of the Server.
"""
def status(server_ref, timeout \\ 5000) do
:gen_statem.call(server_ref, :status, timeout)
end
@doc """
Stops the Server with reason `:normal` and timeout `:infinity`.
"""
def stop(server_ref) do
:gen_statem.stop(server_ref)
end
@doc """
Stops the Server with the specified reason and timeout.
"""
def stop(server_ref, reason, timeout) do
:gen_statem.stop(server_ref, reason, timeout)
end
@impl :gen_statem
def init(options) do
- import GenMagic.Config
+ import Majic.Config
data = %Data{
port_name: get_port_name(),
database_patterns: Keyword.get(options, :database_patterns, []),
port_options: get_port_options(options),
startup_timeout: get_startup_timeout(options),
process_timeout: get_process_timeout(options),
recycle_threshold: get_recycle_threshold(options)
}
{:ok, :starting, data}
end
@impl :gen_statem
def callback_mode do
[:state_functions, :state_enter]
end
@doc false
def starting(:enter, _, %{port: nil} = data) do
port = Port.open(data.port_name, data.port_options)
{:keep_state, %{data | port: port}, data.startup_timeout}
end
@doc false
def starting(:enter, _, data) do
{:keep_state_and_data, data.startup_timeout}
end
@doc false
def starting({:call, from}, :status, data) do
handle_status_call(from, :starting, data)
end
@doc false
def starting({:call, _from}, {:perform, _path}, _data) do
{:keep_state_and_data, :postpone}
end
@doc false
def starting(:info, {port, {:data, ready}}, %{port: port} = data) do
case :erlang.binary_to_term(ready) do
:ready -> {:next_state, :loading, data}
end
end
@doc false
def starting(:info, {port, {:exit_status, code}}, %{port: port} = data) do
error =
case code do
1 -> :bad_db
2 -> :ei_error
3 -> :ei_bad_term
code -> {:unexpected_error, code}
end
{:stop, {:error, error}, data}
end
@doc false
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
@doc false
def loading(:state_timeout, :load_timeout, {[database | _], data}) do
{:stop, {:error, {:database_loading_timeout, database}}, data}
end
@doc false
def loading(:state_timeout, :load, {[], data}) do
{:next_state, :available, data}
end
@doc false
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
@doc false
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
@doc false
def loading(:info, {port, {:exit_status, 1}}, {[database | _], %{port: port} = data}) do
{:stop, {:error, {:database_not_found, database}}, data}
end
@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
@doc false
def available(:enter, _old_state, %{request: nil}) do
:keep_state_and_data
end
@doc false
def available({:call, from}, {:perform, path}, data) do
data = %{data | cycles: data.cycles + 1, request: {path, from, :erlang.now()}}
arg =
case path do
path when is_binary(path) -> {:file, path}
{:bytes, bytes} -> {:bytes, bytes}
end
send(data.port, arg)
{:next_state, :processing, data}
end
@doc false
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
@doc false
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
def available({:call, from}, :status, data) do
handle_status_call(from, :available, data)
end
@doc false
def processing(:enter, _old_state, %{request: {_path, _from, _time}} = data) do
{:keep_state_and_data, data.process_timeout}
end
@doc false
def processing({:call, _from}, {:perform, _path}, _data) do
{:keep_state_and_data, :postpone}
end
@doc false
def processing({:call, from}, :status, data) do
handle_status_call(from, :processing, data)
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
def processing(:info, {port, {:data, response}}, %{port: port, request: {_, from, _}} = data) do
response = {:reply, from, handle_response(response)}
next_state = (data.cycles >= data.recycle_threshold && :recycling) || :available
{:next_state, next_state, %{data | request: nil}, [response, :hibernate]}
end
@doc false
def recycling(:enter, _, %{port: port} = data) when is_port(port) do
send(data.port, {:stop, :recycle})
{:keep_state_and_data, {:state_timeout, data.startup_timeout, :stop}}
end
@doc false
def recycling({:call, _from}, {:perform, _path}, _data) do
{:keep_state_and_data, :postpone}
end
@doc false
def recycling({:call, from}, :status, data) do
handle_status_call(from, :recycling, data)
end
@doc false
# 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
@doc false
def recycling(:state_timeout, :close, data) do
{:stop, {:error, :port_close_failed}}
end
@doc false
def recycling(:info, {port, :closed}, %{port: port} = data) do
{:next_state, :starting, %{data | port: nil, cycles: 0}}
end
@doc false
def recycling(:info, {port, {:exit_status, _}}, %{port: port} = data) do
{:next_state, :starting, %{data | port: nil, cycles: 0}}
end
@doc false
@impl :gen_statem
def terminate(_, _, %{port: port}) do
Kernel.send(port, {self(), :close})
end
@doc false
def terminate(_, _, _) do
:ok
end
defp send(port, command) do
Kernel.send(port, {self(), {:command, :erlang.term_to_binary(command)}})
end
@errnos %{
2 => :enoent,
13 => :eaccess,
20 => :enotdir,
12 => :enomem,
24 => :emfile,
36 => :enametoolong
}
@errno Map.keys(@errnos)
defp handle_response(data) do
case :erlang.binary_to_term(data) do
{:ok, {mime_type, encoding, content}} -> {:ok, Result.build(mime_type, encoding, content)}
{:error, {errno, _}} when errno in @errno -> {:error, @errnos[errno]}
{:error, {errno, string}} -> {:error, "#{errno}: #{string}"}
{:error, _} = error -> error
end
end
defp handle_status_call(from, state, data) do
response = {:ok, %__MODULE__.Status{state: state, cycles: data.cycles}}
{:keep_state_and_data, {:reply, from, response}}
end
end
diff --git a/lib/gen_magic/server/data.ex b/lib/majic/server/data.ex
similarity index 95%
rename from lib/gen_magic/server/data.ex
rename to lib/majic/server/data.ex
index 25d23e9..86c076b 100644
--- a/lib/gen_magic/server/data.ex
+++ b/lib/majic/server/data.ex
@@ -1,26 +1,26 @@
-defmodule GenMagic.Server.Data do
+defmodule Majic.Server.Data do
@moduledoc false
@type request :: {Path.t(), {pid(), term()}, requested_at :: integer()}
@type t :: %__MODULE__{
port_name: Port.name(),
port_options: list(),
port: port(),
startup_timeout: timeout(),
process_timeout: timeout(),
recycle_threshold: non_neg_integer() | :infinity,
cycles: non_neg_integer(),
request: request | nil
}
defstruct port_name: nil,
port_options: nil,
port: nil,
startup_timeout: :infinity,
process_timeout: :infinity,
recycle_threshold: :infinity,
cycles: 0,
database_patterns: nil,
request: nil
end
diff --git a/lib/gen_magic/server/status.ex b/lib/majic/server/status.ex
similarity index 84%
rename from lib/gen_magic/server/status.ex
rename to lib/majic/server/status.ex
index 7279b57..0df3a9d 100644
--- a/lib/gen_magic/server/status.ex
+++ b/lib/majic/server/status.ex
@@ -1,20 +1,20 @@
-defmodule GenMagic.Server.Status do
+defmodule Majic.Server.Status do
@moduledoc """
Represents Status of the underlying Server.
"""
@typedoc """
Represnets Staus of the Server.
- `:state`: Represents the current state of the Server
- `:cycles`: Represents the number of cycles the Server has run; note that this resets if
recycling is enabled.
"""
@type t :: %__MODULE__{
- state: GenMagic.Server.state(),
+ state: Majic.Server.state(),
cycles: non_neg_integer()
}
defstruct state: nil, cycles: 0
end
diff --git a/mix.exs b/mix.exs
index 83c1597..4e9b699 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,67 +1,67 @@
-defmodule GenMagic.MixProject do
+defmodule Majic.MixProject do
use Mix.Project
if :erlang.system_info(:otp_release) < '21' do
- raise "GenMagic requires Erlang/OTP 21 or newer"
+ raise "Majic requires Erlang/OTP 21 or newer"
end
def project do
[
- app: :gen_magic,
+ app: :majic,
version: "1.0.0",
elixir: "~> 1.7",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
compilers: [:elixir_make] ++ Mix.compilers(),
package: package(),
deps: deps(),
dialyzer: dialyzer(),
- name: "GenMagic",
+ name: "Majic",
description: "File introspection with libmagic",
- source_url: "https://github.com/evadne/gen_magic",
+ source_url: "https://github.com/hrefhref/majic",
docs: docs()
]
end
def application do
[extra_applications: [:logger]]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp dialyzer do
[
plt_add_apps: [:mix, :iex, :ex_unit],
flags: ~w(error_handling no_opaque race_conditions underspecs unmatched_returns)a,
ignore_warnings: "dialyzer-ignore-warnings.exs",
list_unused_filters: true
]
end
defp deps do
[
{:credo, "~> 1.4.0", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false},
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
{:elixir_make, "~> 0.4", runtime: false},
{:nimble_pool, "~> 0.1"}
]
end
defp package do
[
files: ~w(lib/gen_magic/* src/*.c Makefile),
licenses: ["Apache 2.0"],
links: %{"GitHub" => "https://github.com/evadne/packmatic"},
source_url: "https://github.com/evadne/packmatic"
]
end
defp docs do
[
main: "readme",
extras: ["README.md", "CHANGELOG.md"]
]
end
end
diff --git a/src/apprentice.c b/src/libmagic_port.c
similarity index 100%
rename from src/apprentice.c
rename to src/libmagic_port.c
diff --git a/test/gen_magic/gen_magic_test.exs b/test/gen_magic/gen_magic_test.exs
deleted file mode 100644
index 92574f4..0000000
--- a/test/gen_magic/gen_magic_test.exs
+++ /dev/null
@@ -1,69 +0,0 @@
-defmodule GenMagicTest do
- use GenMagic.MagicCase
- alias GenMagic.Result
-
- doctest GenMagic
- @iterations 100
-
- test "Makefile is text file" do
- {:ok, pid} = GenMagic.Server.start_link([])
- path = absolute_path("Makefile")
- assert {:ok, %{mime_type: "text/x-makefile"}} = GenMagic.Server.perform(pid, path)
- end
-
- @tag external: true
- test "Load test local files" do
- {:ok, pid} = GenMagic.Server.start_link([])
-
- files_stream()
- |> Stream.cycle()
- |> Stream.take(@iterations)
- |> Stream.map(&assert {:ok, %Result{}} = GenMagic.Server.perform(pid, &1))
- |> Enum.all?()
- |> assert
- end
-
- test "Non-existent file" do
- {:ok, pid} = GenMagic.Server.start_link([])
- path = missing_filename()
- assert_no_file(GenMagic.Server.perform(pid, path))
- end
-
- test "Named process" do
- {:ok, pid} = GenMagic.Server.start_link(name: :gen_magic)
- path = absolute_path("Makefile")
- assert {:ok, %{cycles: 0}} = GenMagic.Server.status(:gen_magic)
- assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
- assert {:ok, %Result{} = result} = GenMagic.Server.perform(:gen_magic, path)
- assert {:ok, %{cycles: 1}} = GenMagic.Server.status(:gen_magic)
- assert {:ok, %{cycles: 1}} = GenMagic.Server.status(pid)
- assert "text/x-makefile" = result.mime_type
- end
-
- describe "custom database" do
- setup do
- database = absolute_path("elixir.mgc")
- on_exit(fn -> File.rm(database) end)
- {_, 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])
- path = absolute_path("mix.exs")
- assert {:ok, %Result{} = result} = GenMagic.Server.perform(pid, path)
- assert "text/x-elixir" = result.mime_type
- assert "us-ascii" = result.encoding
- assert "Elixir module source text" = result.content
- 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
diff --git a/test/gen_magic/helpers_test.exs b/test/gen_magic/helpers_test.exs
deleted file mode 100644
index d2690ef..0000000
--- a/test/gen_magic/helpers_test.exs
+++ /dev/null
@@ -1,9 +0,0 @@
-defmodule GenMagic.HelpersTest do
- use GenMagic.MagicCase
- doctest GenMagic.Helpers
-
- test "perform_once" do
- path = absolute_path("Makefile")
- assert {:ok, %{mime_type: "text/x-makefile"}} = GenMagic.Helpers.perform_once(path)
- end
-end
diff --git a/test/gen_magic/pool_test.exs b/test/gen_magic/pool_test.exs
deleted file mode 100644
index b987186..0000000
--- a/test/gen_magic/pool_test.exs
+++ /dev/null
@@ -1,16 +0,0 @@
-defmodule GenMagic.PoollTest do
- use GenMagic.MagicCase
-
- test "pool" do
- {:ok, _} = GenMagic.Pool.start_link(name: TestPool, pool_size: 2)
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- assert {:ok, _} = GenMagic.Pool.perform(TestPool, absolute_path("Makefile"))
- end
-end
diff --git a/test/gen_magic/server_test.exs b/test/gen_magic/server_test.exs
deleted file mode 100644
index d00933a..0000000
--- a/test/gen_magic/server_test.exs
+++ /dev/null
@@ -1,44 +0,0 @@
-defmodule GenMagic.ServerTest do
- use GenMagic.MagicCase
- doctest GenMagic.Server
-
- describe "recycle_threshold" do
- test "resets" do
- {:ok, pid} = GenMagic.Server.start_link(recycle_threshold: 3)
- 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.perform(pid, path)
- assert {:ok, %{cycles: 2}} = GenMagic.Server.status(pid)
- assert {:ok, _} = GenMagic.Server.perform(pid, path)
- Process.sleep(100)
- assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
- end
-
- test "resets before reply" do
- {:ok, pid} = GenMagic.Server.start_link(recycle_threshold: 1)
- path = absolute_path("Makefile")
- assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
- assert {:ok, _} = GenMagic.Server.perform(pid, path)
- Process.sleep(100)
- assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
- assert {:ok, _} = GenMagic.Server.perform(pid, path)
- Process.sleep(100)
- assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
- assert {:ok, _} = GenMagic.Server.perform(pid, path)
- Process.sleep(100)
- assert {:ok, %{cycles: 0}} = GenMagic.Server.status(pid)
- 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
diff --git a/test/majic/helpers_test.exs b/test/majic/helpers_test.exs
new file mode 100644
index 0000000..ce514c5
--- /dev/null
+++ b/test/majic/helpers_test.exs
@@ -0,0 +1,9 @@
+defmodule Majic.HelpersTest do
+ use Majic.MagicCase
+ doctest Majic.Helpers
+
+ test "perform_once" do
+ path = absolute_path("Makefile")
+ assert {:ok, %{mime_type: "text/x-makefile"}} = Majic.Helpers.perform_once(path)
+ end
+end
diff --git a/test/majic/majic_test.exs b/test/majic/majic_test.exs
new file mode 100644
index 0000000..a47812d
--- /dev/null
+++ b/test/majic/majic_test.exs
@@ -0,0 +1,69 @@
+defmodule MajicTest do
+ use Majic.MagicCase
+ alias Majic.Result
+
+ doctest Majic
+ @iterations 100
+
+ test "Makefile is text file" do
+ {:ok, pid} = Majic.Server.start_link([])
+ path = absolute_path("Makefile")
+ assert {:ok, %{mime_type: "text/x-makefile"}} = Majic.Server.perform(pid, path)
+ end
+
+ @tag external: true
+ test "Load test local files" do
+ {:ok, pid} = Majic.Server.start_link([])
+
+ files_stream()
+ |> Stream.cycle()
+ |> Stream.take(@iterations)
+ |> Stream.map(&assert {:ok, %Result{}} = Majic.Server.perform(pid, &1))
+ |> Enum.all?()
+ |> assert
+ end
+
+ test "Non-existent file" do
+ {:ok, pid} = Majic.Server.start_link([])
+ path = missing_filename()
+ assert_no_file(Majic.Server.perform(pid, path))
+ end
+
+ test "Named process" do
+ {:ok, pid} = Majic.Server.start_link(name: :gen_magic)
+ path = absolute_path("Makefile")
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(:gen_magic)
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ assert {:ok, %Result{} = result} = Majic.Server.perform(:gen_magic, path)
+ assert {:ok, %{cycles: 1}} = Majic.Server.status(:gen_magic)
+ assert {:ok, %{cycles: 1}} = Majic.Server.status(pid)
+ assert "text/x-makefile" = result.mime_type
+ end
+
+ describe "custom database" do
+ setup do
+ database = absolute_path("elixir.mgc")
+ on_exit(fn -> File.rm(database) end)
+ {_, 0} = System.cmd("file", ["-C", "-m", absolute_path("test/elixir")])
+ [database: database]
+ end
+
+ test "recognises Elixir files", %{database: database} do
+ {:ok, pid} = Majic.Server.start_link(database_patterns: [database])
+ path = absolute_path("mix.exs")
+ assert {:ok, %Result{} = result} = Majic.Server.perform(pid, path)
+ assert "text/x-elixir" = result.mime_type
+ assert "us-ascii" = result.encoding
+ assert "Elixir module source text" = result.content
+ end
+
+ test "recognises Elixir files after a reload", %{database: database} do
+ {:ok, pid} = Majic.Server.start_link([])
+ path = absolute_path("mix.exs")
+ {:ok, %Result{mime_type: mime}} = Majic.Server.perform(pid, path)
+ refute mime == "text/x-elixir"
+ :ok = Majic.Server.reload(pid, [database])
+ assert {:ok, %Result{mime_type: "text/x-elixir"}} = Majic.Server.perform(pid, path)
+ end
+ end
+end
diff --git a/test/majic/pool_test.exs b/test/majic/pool_test.exs
new file mode 100644
index 0000000..12f8edf
--- /dev/null
+++ b/test/majic/pool_test.exs
@@ -0,0 +1,16 @@
+defmodule Majic.PoollTest do
+ use Majic.MagicCase
+
+ test "pool" do
+ {:ok, _} = Majic.Pool.start_link(name: TestPool, pool_size: 2)
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ assert {:ok, _} = Majic.Pool.perform(TestPool, absolute_path("Makefile"))
+ end
+end
diff --git a/test/gen_magic/apprentice_test.exs b/test/majic/port_test.exs
similarity index 93%
rename from test/gen_magic/apprentice_test.exs
rename to test/majic/port_test.exs
index dfb86a0..7c5f089 100644
--- a/test/gen_magic/apprentice_test.exs
+++ b/test/majic/port_test.exs
@@ -1,165 +1,165 @@
-defmodule GenMagic.ApprenticeTest do
- use GenMagic.MagicCase
+defmodule Majic.ApprenticeTest do
+ use Majic.MagicCase
@tmp_path "/tmp/testgenmagicx"
require Logger
test "sends ready" do
- port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
+ port = Port.open(Majic.Config.get_port_name(), Majic.Config.get_port_options([]))
on_exit(fn -> send(port, {self(), :close}) end)
assert_ready_and_init_default(port)
end
test "stops" do
- port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
+ port = Port.open(Majic.Config.get_port_name(), Majic.Config.get_port_options([]))
on_exit(fn -> send(port, {self(), :close}) end)
assert_ready_and_init_default(port)
send(port, {self(), {:command, :erlang.term_to_binary({:stop, :stop})}})
assert_receive {^port, {:exit_status, 0}}
end
test "exits with non existent database with an error" do
opts = [:use_stdio, :binary, :exit_status, {:packet, 2}, {:args, []}]
- port = Port.open(GenMagic.Config.get_port_name(), opts)
+ port = Port.open(Majic.Config.get_port_name(), opts)
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}}
end
describe "port" do
setup do
- port = Port.open(GenMagic.Config.get_port_name(), GenMagic.Config.get_port_options([]))
+ port = Port.open(Majic.Config.get_port_name(), Majic.Config.get_port_options([]))
on_exit(fn -> send(port, {self(), :close}) end)
assert_ready_and_init_default(port)
%{port: port}
end
test "exits with badly formatted erlang terms", %{port: port} do
send(port, {self(), {:command, "i forgot to term_to_binary!!"}})
assert_receive {^port, {:exit_status, 3}}
end
test "errors with wrong command", %{port: port} do
send(port, {self(), {:command, :erlang.term_to_binary(:wrong)}})
assert_receive {^port, {:data, data}}
assert {:error, :badarg} = :erlang.binary_to_term(data)
refute_receive _
send(port, {self(), {:command, :erlang.term_to_binary({:file, 42})}})
assert_receive {^port, {:data, data}}
assert {:error, :badarg} = :erlang.binary_to_term(data)
refute_receive _
send(port, {self(), {:command, :erlang.term_to_binary("more wrong")}})
assert_receive {^port, {:data, data}}
assert {:error, :badarg} = :erlang.binary_to_term(data)
refute_receive _
send(port, {self(), {:command, :erlang.term_to_binary({"no", "no"})}})
assert_receive {^port, {:data, data}}
assert {:error, :badarg} = :erlang.binary_to_term(data)
refute_receive _
end
test "file works", %{port: port} do
send(port, {self(), {:command, :erlang.term_to_binary({:file, Path.expand("Makefile")})}})
assert_receive {^port, {:data, data}}
assert {:ok, _} = :erlang.binary_to_term(data)
end
test "bytes works", %{port: port} do
send(port, {self(), {:command, :erlang.term_to_binary({:bytes, "some bytes!"})}})
assert_receive {^port, {:data, data}}
assert {:ok, _} = :erlang.binary_to_term(data)
end
test "fails with non existent file", %{port: port} do
send(port, {self(), {:command, :erlang.term_to_binary({:file, "/path/to/nowhere"})}})
assert_receive {^port, {:data, data}}
assert {:error, _} = :erlang.binary_to_term(data)
end
test "works with big file path", %{port: port} do
# Test with longest valid path.
{dir, bigfile} = too_big(@tmp_path, "/a")
case File.mkdir_p(dir) do
:ok ->
File.touch!(bigfile)
on_exit(fn -> File.rm_rf!(@tmp_path) end)
send(port, {self(), {:command, :erlang.term_to_binary({:file, bigfile})}})
assert_receive {^port, {:data, data}}
assert {:ok, _} = :erlang.binary_to_term(data)
refute_receive _
# This path should be long enough for buffers, but larger than a valid path name.
# Magic will return an errno 36.
file = @tmp_path <> String.duplicate("a", 256)
send(port, {self(), {:command, :erlang.term_to_binary({:file, file})}})
assert_receive {^port, {:data, data}}
assert {:error, {36, _}} = :erlang.binary_to_term(data)
refute_receive _
# Theses filename should be too big for the path buffer.
file = bigfile <> "aaaaaaaaaa"
send(port, {self(), {:command, :erlang.term_to_binary({:file, file})}})
assert_receive {^port, {:data, data}}
assert {:error, :enametoolong} = :erlang.binary_to_term(data)
refute_receive _
# This call should be larger than the COMMAND_BUFFER_SIZE. Ensure nothing bad happens!
file = String.duplicate(bigfile, 4)
send(port, {self(), {:command, :erlang.term_to_binary({:file, file})}})
assert_receive {^port, {:data, data}}
assert {:error, :badarg} = :erlang.binary_to_term(data)
refute_receive _
# We re-run a valid call to ensure the buffer/... haven't been corrupted in port land.
send(port, {self(), {:command, :erlang.term_to_binary({:file, bigfile})}})
assert_receive {^port, {:data, data}}
assert {:ok, _} = :erlang.binary_to_term(data)
refute_receive _
{:error, :enametoolong} ->
Logger.info(
"Skipping test, operating system does not support max POSIX length for directories"
)
:ignore
end
end
end
def assert_ready(port) do
assert_receive {^port, {:data, data}}
assert :ready == :erlang.binary_to_term(data)
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
last_len = byte_size(filename)
path_len = byte_size(path)
needed = limit - (last_len + path_len)
extra = make_too_big(needed, "")
{path <> extra, path <> extra <> filename}
end
def make_too_big(needed, acc) when needed <= 255 do
acc <> "/" <> String.duplicate("a", needed - 1)
end
def make_too_big(needed, acc) do
acc = acc <> "/" <> String.duplicate("a", 254)
make_too_big(needed - 255, acc)
end
end
diff --git a/test/majic/server_test.exs b/test/majic/server_test.exs
new file mode 100644
index 0000000..d9b2c7a
--- /dev/null
+++ b/test/majic/server_test.exs
@@ -0,0 +1,44 @@
+defmodule Majic.ServerTest do
+ use Majic.MagicCase
+ doctest Majic.Server
+
+ describe "recycle_threshold" do
+ test "resets" do
+ {:ok, pid} = Majic.Server.start_link(recycle_threshold: 3)
+ path = absolute_path("Makefile")
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ assert {:ok, _} = Majic.Server.perform(pid, path)
+ assert {:ok, %{cycles: 1}} = Majic.Server.status(pid)
+ assert {:ok, _} = Majic.Server.perform(pid, path)
+ assert {:ok, %{cycles: 2}} = Majic.Server.status(pid)
+ assert {:ok, _} = Majic.Server.perform(pid, path)
+ Process.sleep(100)
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ end
+
+ test "resets before reply" do
+ {:ok, pid} = Majic.Server.start_link(recycle_threshold: 1)
+ path = absolute_path("Makefile")
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ assert {:ok, _} = Majic.Server.perform(pid, path)
+ Process.sleep(100)
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ assert {:ok, _} = Majic.Server.perform(pid, path)
+ Process.sleep(100)
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ assert {:ok, _} = Majic.Server.perform(pid, path)
+ Process.sleep(100)
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ end
+ end
+
+ test "recycle" do
+ {:ok, pid} = Majic.Server.start_link([])
+ path = absolute_path("Makefile")
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ assert {:ok, _} = Majic.Server.perform(pid, path)
+ assert {:ok, %{cycles: 1}} = Majic.Server.status(pid)
+ assert :ok = Majic.Server.recycle(pid)
+ assert {:ok, %{cycles: 0}} = Majic.Server.status(pid)
+ end
+end
diff --git a/test/soak.exs b/test/soak.exs
index 87a40a2..8f9826c 100644
--- a/test/soak.exs
+++ b/test/soak.exs
@@ -1,33 +1,33 @@
defmodule Soak do
@moduledoc """
Run with a list of files to inspect:
find /usr/share/ -name *png | xargs mix run test/soak.exs
"""
def perform_infinite([]), do: false
def perform_infinite(paths) do
- {:ok, pid} = GenMagic.Server.start_link(database_patterns: ["/usr/local/share/misc/*.mgc"])
+ {:ok, pid} = Majic.Server.start_link(database_patterns: ["/usr/local/share/misc/*.mgc"])
perform_infinite(paths, [], pid, 0)
end
defp perform_infinite([], done, pid, count) do
perform_infinite(done, [], pid, count)
end
defp perform_infinite([path | paths], done, pid, count) do
if rem(count, 1000) == 0, do: IO.puts(Integer.to_string(count))
- {:ok, %GenMagic.Result{}} = GenMagic.Server.perform(pid, path)
+ {:ok, %Majic.Result{}} = Majic.Server.perform(pid, path)
perform_infinite(paths, [path | done], pid, count + 1)
end
end
# Run with a list of files to inspect
#
# find /usr/share/ -name *png | xargs mix run test/soak.exs
System.argv()
|> Enum.filter(&File.exists?/1)
|> Soak.perform_infinite()
diff --git a/test/support/magic_case.ex b/test/support/magic_case.ex
index 53df01e..3ba7e69 100644
--- a/test/support/magic_case.ex
+++ b/test/support/magic_case.ex
@@ -1,33 +1,33 @@
-defmodule GenMagic.MagicCase do
+defmodule Majic.MagicCase do
@moduledoc false
use ExUnit.CaseTemplate
using do
quote do
import unquote(__MODULE__)
end
end
def missing_filename do
:crypto.strong_rand_bytes(8) |> Base.url_encode64()
end
def files_stream do
Path.join(File.cwd!(), "deps/**/*")
|> Path.wildcard()
|> Stream.reject(&File.dir?/1)
|> Stream.chunk_every(10)
|> Stream.flat_map(&Enum.shuffle/1)
end
def assert_no_file(message) do
assert {:error, :enoent} = message
end
def absolute_path(path) do
__ENV__.file
|> Path.join("../../..")
|> Path.join(path)
|> Path.expand()
end
end

File Metadata

Mime Type
text/x-diff
Expires
Tue, Nov 26, 12:32 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40300
Default Alt Text
(60 KB)

Event Timeline