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