lib/gsmlg/tor.ex

defmodule GSMLG.Tor do
  @moduledoc """
  Starting a tor server in elixir using Port.

  The Tor Project [https://www.torproject.org](https://www.torproject.org)

  """

  alias GSMLG.Tor.Config
  require Logger
  use GenServer

  @doc """
  Start tor server at using config file return by `GSMLG.Tor.Config.torrc()`

  Start server with

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

  or add to supervisor tree
  ```
  {GSMLG.Tor, []}
  ```

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

  @doc """
  Return GenServer state.

  `%{port: <port>}`

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

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

  @doc """
  Get current config file content
  """
  def get_torrc() do
    GSMLG.Tor.Config.torrc_file() |> File.read!()
  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

  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