Skip to content

Commit

Permalink
Run a NervesHub script on device (#207)
Browse files Browse the repository at this point in the history
* Run a NervesHub script on device

Evaluate a script locally on device from NervesHub instead of relying on
the console channel. Bumps the api version so NervesHub is aware this is
possible on the connecting device.

* Run script capture in a separate process

In case of bad code we won't crash the socket process or potentially
leave the socket process with a bad group leader
  • Loading branch information
oestrich authored Jul 23, 2024
1 parent d8dda2d commit 515cb48
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 1 deletion.
2 changes: 1 addition & 1 deletion lib/nerves_hub_link/configurator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule NervesHubLink.Configurator do
alias __MODULE__.Config
require Logger

@device_api_version "2.0.0"
@device_api_version "2.1.0"
@console_version "2.0.0"

defmodule Config do
Expand Down
46 changes: 46 additions & 0 deletions lib/nerves_hub_link/script.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
defmodule NervesHubLink.Script do
use GenServer

@doc """
Run a script from NervesHub and capture its output
"""
def capture(text, ref) do
_ = GenServer.start_link(__MODULE__, {self(), text, ref})

:ok
end

@impl GenServer
def init({pid, text, ref}) do
state = %{pid: pid, text: text, ref: ref}
{:ok, state, {:continue, :capture}}
end

@impl GenServer
def handle_continue(:capture, state) do
# Inspired from ExUnit.CaptureIO
# https://github.com/elixir-lang/elixir/blob/main/lib/ex_unit/lib/ex_unit/capture_io.ex
{:ok, string_io} = StringIO.open("")

Process.group_leader(self(), string_io)

{result, output} =
try do
Code.eval_string(state.text)
catch
kind, reason ->
{:ok, {_input, output}} = StringIO.close(string_io)
result = Exception.format_banner(kind, reason, __STACKTRACE__)
output = output <> "\n" <> result
{nil, output}
else
{result, _binding} ->
{:ok, {_input, output}} = StringIO.close(string_io)
{result, output}
end

send(state.pid, {"scripts/run", state.ref, output, result})

{:stop, :normal, state}
end
end
18 changes: 18 additions & 0 deletions lib/nerves_hub_link/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defmodule NervesHubLink.Socket do
alias NervesHubLink.Client
alias NervesHubLink.Configurator
alias NervesHubLink.Configurator.SharedSecret
alias NervesHubLink.Script
alias NervesHubLink.UpdateManager
alias NervesHubLink.UploadFile

Expand Down Expand Up @@ -306,6 +307,12 @@ defmodule NervesHubLink.Socket do
{:ok, socket}
end

def handle_message(@device_topic, "scripts/run", params, socket) do
# See related handle_info for pushing back the script result
:ok = Script.capture(params["text"], params["ref"])
{:ok, socket}
end

def handle_message(@device_topic, "archive", params, socket) do
{:ok, info} = NervesHubLink.Message.ArchiveInfo.parse(params)
_ = ArchiveManager.apply_archive(info, socket.assigns.config.archive_public_keys)
Expand Down Expand Up @@ -410,6 +417,17 @@ defmodule NervesHubLink.Socket do
end
end

def handle_info({"scripts/run", ref, output, return}, socket) do
_ =
push(socket, @device_topic, "scripts/run", %{
ref: ref,
output: output,
return: inspect(return, pretty: true)
})

{:noreply, socket}
end

def handle_info({:tty_data, data}, socket) do
_ = push(socket, @console_topic, "up", %{data: data})
{:noreply, set_iex_timer(socket)}
Expand Down

0 comments on commit 515cb48

Please sign in to comment.