145 lines
3.7 KiB
Elixir
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
|