lib/gsmlg/tor.ex

defmodule GSMLG.Tor do
  @moduledoc """
  Documentation for `GSMLG.Tor`.
  """

  alias GSMLG.Tor.Config
  require Logger
  use GenServer

  @doc """
  Start tor server at

  - `127.0.0.1:9050`
  - `[::1]:9050`

  Accept accesss from `127.0.0.1/8` or `::1/128`

  Start server with

  ```
  GSMLG.Tor.start()
  ```

  To start server, libevent must be installed.
  """
  def start() do
    GenServer.start(__MODULE__, [], name: __MODULE__)
  end

  def get_state() do
    GenServer.call(__MODULE__, :get_state)
  end

  def stop() do
    {:os_pid, pid} = get_state() |> Map.get(:port) |> Port.info(:os_pid)
    {_, code} = System.cmd("kill", ["#{pid}"])
    code
  end

  def start_link(_) do
    GenServer.start_link(__MODULE__, nil, name: __MODULE__)
  end

  def init(_init) do
    state = %{port: nil}
    # your trap_exit call should be here
    Process.flag(:trap_exit, true)
    {:ok, state, {:continue, :start_server}}
  end

  def handle_continue(:start_server, state) do
    Logger.info("Init Tor Config...")
    Config.init()

    Logger.info("Staring Tor Server...")

    port =
      Port.open(
        {:spawn_executable, cmd()},
        [
          {:args, ["-f", Config.torrc_file()]},
          {:cd, Application.app_dir(:gsmlg_tor, "priv/tor")},
          :stream,
          :binary,
          :exit_status,
          :hide,
          :use_stdio,
          :stderr_to_stdout
        ]
      )

    state = Map.put(state, :port, port)
    {:noreply, state}
  end

  def handle_call(:get_state, _from, state) do
    {:reply, state, state}
  end

  def handle_info({port, {:data, msg}}, state) do
    Logger.debug("Tor #{inspect(port)}: #{msg}")
    {:noreply, state}
  end

  def handle_info({port, {:exit_status, exit_status}}, state) do
    Logger.info("Tor #{inspect(port)}: exit_status: #{exit_status}")
    {:noreply, state}
  end

  # handle the trapped exit call
  def handle_info({:EXIT, _from, reason}, state) do
    Logger.info("Tor exit: #{inspect(reason)}")
    cleanup(reason, state)
    # see GenServer docs for other return types
    {:stop, reason, state}
  end

  # handle termination
  def terminate(reason, state) do
    Logger.info("terminating")
    cleanup(reason, state)
    state
  end

  defp cleanup(_reason, state) do
    case state |> Map.get(:port) |> Port.info(:os_pid) do
      {:os_pid, pid} ->
        {_, code} = System.cmd("kill", ["#{pid}"])
        code

      _ ->
        0
    end
  end

  defp cmd() do
    Config.command_path()
  end
end