Packages
nerves_hub_link
2.10.2
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/support_scripts_manager.ex
# SPDX-FileCopyrightText: 2025 Josh Kalderimis
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule NervesHubLink.SupportScriptsManager do
@moduledoc false
# Executes support scripts on the device, sends the results to Hub (via Socket),
# and makes sure the scripts don't run forever.
use GenServer
alias NervesHubLink.Socket
require Logger
@default_timeout 10_000
@spec start_link(GenServer.options()) :: GenServer.on_start()
def start_link(opts \\ []) do
opts = Keyword.put_new(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, [], opts)
end
@doc """
Starts a task to execute a support script on the device.
"""
@spec start_task(
identifier :: String.t(),
script :: String.t(),
timeout :: non_neg_integer(),
handler :: any()
) :: :ok
def start_task(identifier, script, timeout, handler \\ Socket) do
timeout = timeout || @default_timeout
GenServer.cast(__MODULE__, {:start_task, script, identifier, timeout, handler})
end
@impl GenServer
def init(_) do
{:ok, %{running_scripts: %{}}}
end
@impl GenServer
def handle_cast({:start_task, script, identifier, timeout, handler}, %{running_scripts: running}) do
task =
Task.Supervisor.async_nolink(SupportScriptsTaskSupervisor, fn ->
# 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)
try do
Code.eval_string(script)
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
end)
timeout = Process.send_after(self(), {:timeout, task}, timeout)
details = %{timeout_pid: timeout, identifier: identifier, handler: handler}
{:noreply, %{running_scripts: Map.put(running, task.ref, details)}}
end
# the timeout fired
@impl GenServer
def handle_info({:timeout, task}, %{running_scripts: running}) do
task_info = running[task.ref]
_ = Task.shutdown(task, :brutal_kill)
Logger.info(
"Script killed due to timeout : ref:#{inspect(task.ref)} identifier:#{inspect(task_info.identifier)}"
)
send(task_info.handler, {"scripts/result", task_info.identifier, {:error, :timeout}})
{:noreply, %{running_scripts: Map.delete(running, task.ref)}}
end
# The task completed successfully
def handle_info({ref, {result, output}}, %{running_scripts: running}) do
task_info = running[ref]
_ = Process.cancel_timer(task_info.timeout_pid)
Logger.info(
"Script completed successfully : ref:#{inspect(ref)} identifier:#{inspect(task_info.identifier)}"
)
send(task_info.handler, {"scripts/result", task_info.identifier, {:ok, result, output}})
Process.demonitor(ref, [:flush])
{:noreply, %{running_scripts: Map.delete(running, ref)}}
end
# The task failed
def handle_info({:DOWN, ref, :process, _pid, reason}, %{running_scripts: running}) do
task_info = running[ref]
Logger.info(
"Script encountered an error : ref:#{inspect(ref)} identifier:#{inspect(task_info.identifier)} - #{inspect(reason)}"
)
_ = Process.cancel_timer(task_info.timeout_pid)
send(task_info.handler, {"scripts/result", task_info.identifier, {:error, reason}})
{:noreply, %{running_scripts: Map.delete(running, ref)}}
end
end