Packages
nerves_hub_link
0.12.0
2.12.0
2.11.1
2.11.0
retired
2.10.2
2.10.1
2.10.0
2.9.0
2.9.0-rc.3
2.9.0-rc.2
2.9.0-rc.1
2.8.1
2.8.0
2.7.3
2.7.2
2.7.0
retired
2.6.0
2.5.2
2.5.1
2.5.0
2.4.0
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.0
1.4.1
1.4.0
1.3.0
1.2.0
1.1.0
1.0.1
1.0.0
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.2
0.10.1
retired
0.10.0
retired
0.10.0-rc.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.6
Manage your Nerves fleet by connecting it to NervesHub
Current section
Files
Jump to
Current section
Files
lib/nerves_hub_link/console_channel.ex
defmodule NervesHubLink.ConsoleChannel do
use GenServer
require Logger
@moduledoc """
Starts an IEx shell in process to allow remote console interaction
The remote console ability is disabled by default and requires the
`remote_iex` key to be enabled in the config:
```
config :nerves_hub_link, remote_iex: true
```
Once connected, shell data on the device will be pushed up the socket
for the following events:
The following events are supported _from_ the Server:
* `phx_close` or `phx_error` - This will cause the channel to attempt rejoining
every 5 seconds. You can change the rejoin timing in the config
```
config :nerves_hub_link, rejoin_after: 3_000
```
* `dn` - String data to send to the shell for evaluation
* `restart` - Restart the IEx shell process
* `window_size` - A map with `:height` and `:width` keys for resizing the terminal
The following events are supported _from_ this client:
* `up` - String data to be displayed to the user console frontend
"""
alias PhoenixClient.{Channel, Message}
alias NervesHubLink.Client
@rejoin_after Application.get_env(:nerves_hub_link, :rejoin_after, 5_000)
@version "1.0.0"
defmodule State do
defstruct socket: nil,
topic: "console",
channel: nil,
params: [],
iex_pid: nil
end
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
@impl GenServer
def init(opts) do
send(self(), :join)
{:ok, State.__struct__(opts)}
end
@impl GenServer
def handle_info(:join, %{socket: socket, topic: topic, params: params} = state) do
params = Map.put(params, "console_version", @version)
case Channel.join(socket, topic, params) do
{:ok, _reply, channel} ->
{:noreply, %{state | channel: channel}}
_error ->
Process.send_after(self(), :join, @rejoin_after)
{:noreply, state}
end
end
def handle_info({:tty_data, data}, state) do
Channel.push_async(state.channel, "up", %{data: data})
{:noreply, state, iex_timeout()}
end
def handle_info(%Message{event: "restart"}, state) do
Logger.warn("[#{inspect(__MODULE__)}] Restarting IEx process from web request")
Channel.push_async(state.channel, "up", %{data: "\r\n*** Restarting IEx ***\r\n"})
state =
state
|> stop_iex()
|> start_iex()
{:noreply, state}
end
def handle_info(%Message{event: "dn"} = msg, %{iex_pid: nil} = state) do
handle_info(msg, start_iex(state))
end
def handle_info(%Message{event: "dn", payload: %{"data" => data}}, state) do
ExTTY.send_text(state.iex_pid, data)
{:noreply, state, iex_timeout()}
end
def handle_info(
%Message{event: "window_size", payload: %{"height" => height, "width" => width}},
state
) do
ExTTY.window_change(state.iex_pid, width, height)
{:noreply, state}
end
def handle_info(%Message{event: event, payload: payload}, state)
when event in ["phx_error", "phx_close"] do
reason = Map.get(payload, :reason, "unknown")
_ = Client.handle_error(reason)
Process.send_after(self(), :join, @rejoin_after)
{:noreply, state}
end
def handle_info(:timeout, state) do
msg = """
\r
****************************************\r
* Session timeout due to inactivity *\r
* *\r
* Press any key to continue... *\r
****************************************\r
"""
Channel.push_async(state.channel, "up", %{data: msg})
{:noreply, stop_iex(state)}
end
def handle_info(req, state) do
Client.handle_error("Unhandled Console handle_info - #{inspect(req)}")
{:noreply, state}
end
defp iex_timeout() do
Application.get_env(:nerves_hub_link, :remote_iex_timeout, 300) * 1000
end
defp start_iex(state) do
shell_opts = [[dot_iex_path: dot_iex_path()]]
{:ok, iex_pid} = ExTTY.start_link(handler: self(), type: :elixir, shell_opts: shell_opts)
%{state | iex_pid: iex_pid}
end
defp dot_iex_path() do
[".iex.exs", "~/.iex.exs", "/etc/iex.exs"]
|> Enum.map(&Path.expand/1)
|> Enum.find("", &File.regular?/1)
end
defp stop_iex(%{iex_pid: nil} = state), do: state
defp stop_iex(%{iex_pid: iex} = state) do
_ = Process.unlink(iex)
:ok = GenServer.stop(iex, 10_000)
%{state | iex_pid: nil}
end
end