Current section
Files
Jump to
Current section
Files
lib/etherex/client.ex
defmodule Etherex.Client do
@moduledoc false
defmacro __using__(opts) do
command = Keyword.get(opts, :command)
args = Keyword.get(opts, :args)
killall = Keyword.get(opts, :killall)
quote do
@command unquote(command)
@args unquote(args)
@killall unquote(killall)
@command_line if is_nil(@args), do: @command, else: "#{@command} #{@args}"
@moduledoc """
Module to start/stop an Ethereum client by using `#{@command}`.
Start it with `start/0`, stop it with `stop/0` and get the logs with `log/0`.
"""
require Logger
use GenServer
######################################################################
## API
@doc """
Starts an ethereum client using command `#{@command}`.
"""
def start() do
case GenServer.start(__MODULE__, [], [{:name, __MODULE__}]) do
{:ok, _} -> :ok
error -> error
end
end
@doc """
Returns the list of log lines from the standard output and
standard error of the Ethereum client. Resets the log.
"""
def log() do
GenServer.call(__MODULE__, :log)
end
@doc """
Stops the ethereum client previously started with command
`#{@command}`. Returns the list of log lines from the standard
output and standard error since last reset.
"""
def stop() do
try do
GenServer.call(__MODULE__, :kill, 10_000)
catch
:exit, {{:shutdown, log}, _call} -> log
end
end
######################################################################
## GenServer callbacks
def init([]) do
Logger.warning("Starting #{@command_line}")
# Try starting the client
port = Port.open({:spawn, @command_line}, [:stderr_to_stdout])
# Let's wait for a client to listen (if any)
client_listening = wait_for_net()
case Port.info(port, :os_pid) do
nil ->
error = "#{@command} did not started, check #{@command} is in the PATH"
Logger.warning(error)
{:stop, error}
{:os_pid, pid} ->
if client_listening do
case Port.info(port, :input) do
nil ->
kill_the_client(port)
error = "#{@command} did not started, check #{@command} is in the PATH"
Logger.warning(error)
{:stop, error}
{:input, 0} ->
# Ethnode produces some output on starting, otherwise...
error = "Fail starting #{@command}, maybe a client is already running"
Logger.warning(error)
# Maybe the client is still alive, let's kill it
kill_the_client(port)
{:stop, error}
{:input, _input} ->
Logger.info("#{@command} started, OS PID #{pid}")
{:ok, %{port: port, log: []}}
end
else
error =
"#{@command} is not actively listening for network connections, check OS PID #{pid}"
Logger.warning(error)
{:stop, error}
end
end
end
def handle_call(:log, _from, %{log: log} = state) do
{:reply, Enum.reverse(log), %{state | log: []}}
end
def handle_call(:kill, _from, %{port: port, log: log} = state) do
case kill_the_client(port) do
:ok ->
{:stop, {:shutdown, log}, state}
error ->
{:stop, error}
end
end
def handle_info({port, {:data, line}}, %{port: port, log: log} = state) do
{:noreply, %{state | log: [line | log]}}
end
######################################################################
## Helpers
defp wait_for_net(n \\ 10)
defp wait_for_net(n) when n == 0, do: false
defp wait_for_net(n) when n > 0 do
case Etherex.net_listening() do
{:ok, true} ->
true
_ ->
Process.sleep(1000)
wait_for_net(n - 1)
end
end
defp wait_no_net(n \\ 3)
defp wait_no_net(n) when n == 0, do: false
defp wait_no_net(n) when n > 0 do
case Etherex.net_listening() do
{:error, :econnrefused} ->
true
_ ->
Process.sleep(1000)
wait_no_net(n - 1)
end
end
defp log(port, log \\ "") do
receive do
{^port, {:data, line}} ->
log(port, "#{log}#{line}")
after
0 ->
log
end
end
defp kill_the_client(port) do
case Port.info(port, :os_pid) do
nil ->
Logger.info("No need to kill #{@command}")
:ok
{:os_pid, pid} ->
Logger.info("Killing #{@command}, OS PID #{pid}")
case System.cmd("kill", ["#{pid}"]) do
{"", 0} ->
if @killall != nil do
System.cmd("killall", [@killall])
end
wait_no_net()
Logger.info("#{@command} killed")
:ok
_ ->
error = "Cannot kill #{@command} process with PID #{pid}"
Logger.warning(error)
{:error, error}
end
end
end
end
end
end