podbox_ash/lib/podbox/download/connectivity_monitor.ex

145 lines
3.7 KiB
Elixir

defmodule Podbox.Download.ConnectivityMonitor do
@option_schema [
interval: [
type: :pos_integer,
doc: "How often to re-check connectivity. Milliseconds.",
required: false,
default: :timer.seconds(5)
],
address: [
type:
{:or,
[
{:tuple, [{:in, 0..0xFF}, {:in, 0..0xFF}, {:in, 0..0xFF}, {:in, 0..0xFF}]},
{:tuple,
[
{:in, 0..0xFFFF},
{:in, 0..0xFFFF},
{:in, 0..0xFFFF},
{:in, 0..0xFFFF},
{:in, 0..0xFFFF},
{:in, 0..0xFFFF},
{:in, 0..0xFFFF},
{:in, 0..0xFFFF}
]}
]},
doc: "The IP address to check.",
required: true
],
port: [
type: {:in, 1..0xFFFF},
doc: "The port to connect to.",
required: true
],
timeout: [
type: :timeout,
doc: "How long to wait for a connection before giving up. Milliseconds.",
required: false,
default: :timer.seconds(1)
],
log_level: [
type: {:in, Logger.levels() ++ [false]},
doc: "Log level to use for connectivity information, or `false` to disable.",
required: false,
default: false
]
]
@moduledoc """
Monitors internet connectivity by attempting to connect to a configured host
and port.
## Options
#{Spark.Options.docs(@option_schema)}
"""
use GenServer
require Logger
alias Podbox.PubSub
@doc "Returns the connection state"
@spec connected? :: boolean
def connected?, do: GenServer.call(__MODULE__, :connected?)
@doc false
@spec start_link(keyword) :: GenServer.on_start()
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
@doc false
@impl true
def init(opts) do
with {:ok, opts} <- validate_options(opts),
{:ok, tref} <- :timer.send_interval(opts[:interval], :check) do
state =
opts
|> Map.new()
|> Map.merge(%{timer: tref, connected?: false})
log(state, fn ->
dest =
if tuple_size(state.address) == 8 do
"[#{:inet.ntoa(state.address)}]:#{state.port}"
else
"#{:inet.ntoa(state.address)}:#{state.port}"
end
"[#{inspect(__MODULE__)}]: Starting connectivity check to #{dest} on #{state.interval}ms interval."
end)
{:ok, state}
else
{:error, reason} -> {:stop, reason}
end
end
@doc false
@impl true
def handle_call(:connected?, _from, state), do: {:reply, state.connected?, state}
@doc false
@impl true
def handle_info(:check, state) do
opts = if tuple_size(state.address) == 4, do: [:inet], else: [:inet6]
case :gen_tcp.connect(state.address, state.port, opts, state.timeout) do
{:ok, socket} ->
:gen_tcp.close(socket)
unless state.connected? do
PubSub.broadcast("internet:connected?", :established, true)
log(state, "[#{inspect(__MODULE__)}]: Connectivity established.")
end
{:noreply, %{state | connected?: true}}
_ ->
if state.connected? do
PubSub.broadcast("internet:connected?", :lost, false)
log(state, "[#{inspect(__MODULE__)}]: Connectivity established.")
end
{:noreply, %{state | connected?: false}}
end
end
defp validate_options(opts) do
:podbox
|> Application.get_env(__MODULE__, [])
|> Keyword.merge(opts)
|> Spark.Options.validate(@option_schema)
end
defp log(state, logger) when is_function(logger, 0) do
if state.log_level do
Logger.log(state.log_level, logger)
end
end
defp log(state, message) when is_binary(message) do
if state.log_level do
Logger.log(state.log_level, message)
end
end
end