defmodule A2S.Client do
@moduledoc """
An easy to use client that performs the packet assembly necessary to execute A2S queries.
This client handles requests concurrently and should be suitable for most general uses.
Note: must supply a `:name`.
"""
use Supervisor
@default_name A2S_Singleton
defmodule Config do
@moduledoc false
defstruct [:name, :port, :idle_timeout, :recv_timeout]
end
## Initialization
def child_spec(opts) do
%{
id: a2s_name(opts),
start: {__MODULE__, :start_link, [opts]}
}
end
@doc """
The following configuration options are available:
* `:name` - Alias of the top-level supervisor. Can be provided if you intend to run multiple client instances. Defaults to `#{inspect(@default_name)}`.
* `:port` - UDP socket port used for querying game servers. Defaults to `20850`.
* `:idle_timeout` - `A2S.Client` internally allocates a `:gen_statem` for each address queried, that should exit after idling to keep from leaking resources. Defaults to `120_000` or 2 minutes.
* `:recv_timeout` - Timeout the `:gen_statem`'s will wait between receiving individual packets before returning `{:error, :recv_timeout}`. Defaults to `3000` or 3 seconds.
"""
@spec start_link(keyword()) :: :ignore | {:error, any()} | {:ok, pid()}
def start_link(opts \\ []) do
name = a2s_name(opts)
config = %Config{
name: name,
port: Keyword.get(opts, :port, 20850),
idle_timeout: Keyword.get(opts, :idle_timeout, 120_000),
recv_timeout: Keyword.get(opts, :recv_timeout, 3000)
}
Supervisor.start_link(__MODULE__, config, name: supervisor_name(name))
end
defp a2s_name(opts), do: Keyword.get(opts, :name, @default_name)
@impl true
def init(%Config{} = config) do
%Config{
name: name,
port: port,
idle_timeout: idle_timeout,
recv_timeout: recv_timeout
} = config
:persistent_term.put({A2S.Statem, :recv_timeout}, recv_timeout)
:persistent_term.put({A2S.Statem, :idle_timeout}, idle_timeout)
children = [
{Registry, [keys: :unique, name: registry_name(name)]},
{A2S.DynamicSupervisor, [name: dynamic_supervisor_name(name)]},
{A2S.Socket, [name: socket_name(name), port: port, a2s_registry: registry_name(name)]}
]
Supervisor.init(children, strategy: :one_for_all)
end
## API
@doc """
Query a game server running at `address` for the data specified by `query`.
Additional options are available as a keyword list:
* `:name` - Alias of the top-level supervisor. Defaults to `A2S.Client`.
* `:timeout` - Absolute timeout for the request to complete. Defaults to `5000` or 5 seconds.
"""
@spec query(
:info | :players | :rules,
{:inet.ip_address(), :inet.port_number()},
list({atom(), any()})
) ::
{:info, A2S.Info.t()}
| {:players, A2S.Players.t()}
| {:rules, A2S.Rules.t()}
| {:error, any}
def query(query, address, opts \\ []) do
client_name = Keyword.get(opts, :name, @default_name)
timeout = Keyword.get(opts, :timeout, 5000)
:gen_statem.call(find_or_start(address, client_name), query, timeout)
end
defp find_or_start(address, client) do
registry = registry_name(client)
case Registry.lookup(registry, address) do
[{pid, _value}] ->
pid
[] ->
case A2S.DynamicSupervisor.start_child(address, client) do
{:ok, pid} -> pid
{:ok, pid, _info} -> pid
{:error, {:already_started, pid}} -> pid
end
end
end
## Helpers
@doc false
def registry_name(name), do: :"#{name}.Registry"
@doc false
def dynamic_supervisor_name(name), do: :"#{name}.DynamicSupervisor"
@doc false
def socket_name(name), do: :"#{name}.Socket"
@doc false
def supervisor_name(name), do: :"#{name}.Supervisor"
end