Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support a new set of firmware update status messages #272

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions lib/nerves_hub_link.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,14 @@ defmodule NervesHubLink do
@doc """
Send update progress percentage for display in web
"""
@spec send_update_progress(non_neg_integer()) :: :ok
defdelegate send_update_progress(progress), to: Socket
@spec send_firmware_update_progress(non_neg_integer()) :: :ok
defdelegate send_firmware_update_progress(progress), to: Socket

@doc """
Send an update status to web
Send a firmware status update status to hub
"""
@spec send_update_status(String.t() | atom()) :: :ok
defdelegate send_update_status(status), to: Socket
@spec send_firmware_update_status(status :: atom(), payload :: map()) :: :ok
defdelegate send_firmware_update_status(status, payload \\ %{}), to: Socket

@doc """
Send a file to the connected console
Expand Down
5 changes: 3 additions & 2 deletions lib/nerves_hub_link/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,13 @@ defmodule NervesHubLink.Client do
# TODO: nasty side effects here. Consider moving somewhere else
case data do
{:progress, percent} ->
NervesHubLink.send_update_progress(percent)
NervesHubLink.send_firmware_update_progress(percent)

{:error, _, message} ->
NervesHubLink.send_update_status("fwup error #{message}")
NervesHubLink.send_firmware_update_status(:error, %{reason: message})

{:ok, 0, _message} ->
NervesHubLink.send_firmware_update_status(:finalizing)
initiate_reboot()

_ ->
Expand Down
4 changes: 3 additions & 1 deletion lib/nerves_hub_link/configurator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule NervesHubLink.Configurator do

require Logger

@device_api_version "2.2.0"
@device_api_version "2.3.0"
@console_version "2.0.0"

defmodule Config do
Expand Down Expand Up @@ -156,6 +156,8 @@ defmodule NervesHubLink.Configurator do
|> Map.put("fwup_version", fwup_version())
|> Map.put("device_api_version", @device_api_version)
|> Map.put("console_version", @console_version)
|> Map.put("library", "nerves_hub_link")
|> Map.put("library_version", to_string(Application.spec(:nerves_hub_link, :vsn)))

%{base | params: params, socket: socket, ssl: ssl, fwup_devpath: fwup_devpath}
end
Expand Down
51 changes: 40 additions & 11 deletions lib/nerves_hub_link/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ defmodule NervesHubLink.Socket do
GenServer.cast(__MODULE__, :reconnect)
end

@spec send_update_progress(non_neg_integer()) :: :ok
def send_update_progress(progress) do
GenServer.cast(__MODULE__, {:send_update_progress, progress})
@spec send_firmware_update_progress(percent_progress :: non_neg_integer()) :: :ok
def send_firmware_update_progress(percent_progress) do
GenServer.cast(
__MODULE__,
{:send_firmware_update_status, :progress, %{percent: percent_progress}}
)
end

@spec send_update_status(String.t()) :: :ok
def send_update_status(status) do
GenServer.cast(__MODULE__, {:send_update_status, status})
@spec send_firmware_update_status(status :: atom(), payload :: map()) :: :ok
def send_firmware_update_status(status, payload \\ %{}) do
GenServer.cast(__MODULE__, {:send_firmware_update_status, status, payload})
end

@spec check_connection(atom()) :: boolean()
Expand Down Expand Up @@ -270,13 +273,39 @@ defmodule NervesHubLink.Socket do
{:noreply, disconnect(socket)}
end

def handle_cast({:send_update_progress, progress}, socket) do
_ = push(socket, @device_topic, "fwup_progress", %{value: progress})
def handle_cast({:send_firmware_update_progress, :progress, progress}, socket) do
_ =
push(socket, @device_topic, "firmware_update_status", %{
status: "progress",
percent: progress
})

{:noreply, socket}
end

def handle_cast({:send_firmware_update_status, :finalizing, payload}, socket) do
{:ok, push_ref} =
push(
socket,
@device_topic,
"firmware_update_status",
Map.merge(payload, %{status: "finalizing"})
)

_ = await_reply(push_ref)

{:noreply, socket}
end

def handle_cast({:send_update_status, status}, socket) do
_ = push(socket, @device_topic, "status_update", %{status: status})
def handle_cast({:send_firmware_update_status, status, payload}, socket) do
_ =
push(
socket,
@device_topic,
"firmware_update_status",
Map.merge(payload, %{status: to_string(status)})
)

{:noreply, socket}
end

Expand Down Expand Up @@ -366,7 +395,7 @@ defmodule NervesHubLink.Socket do
into: %{},
do: {name, to_string(ver)}

{:ok, join(socket, "extensions", available_extensions)}
{:ok, join(socket, @extensions_topic, available_extensions)}
end

##
Expand Down
84 changes: 44 additions & 40 deletions lib/nerves_hub_link/update_manager.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,26 @@ defmodule NervesHubLink.UpdateManager do

@type status ::
:idle
| {:fwup_error, String.t()}
| :update_rescheduled
| {:updating, integer()}
| :finalizing

@type previous_status ::
:ignored
| :rescheduled
| :successful
| {:error, String.t()}

@type t :: %__MODULE__{
status: status(),
update_reschedule_timer: nil | :timer.tref(),
previous_status: previous_status(),
download: nil | GenServer.server(),
fwup: nil | GenServer.server(),
fwup_config: FwupConfig.t(),
update_info: nil | UpdateInfo.t()
}

defstruct status: :idle,
update_reschedule_timer: nil,
previous_status: nil,
fwup: nil,
download: nil,
fwup_config: nil,
Expand Down Expand Up @@ -110,8 +115,8 @@ defmodule NervesHubLink.UpdateManager do
_from,
%State{} = state
) do
state = maybe_update_firmware(update, fwup_public_keys, state)
{:reply, state.status, state}
{result, state} = maybe_update_firmware(update, fwup_public_keys, state)
{:reply, result, state}
end

def handle_call(:currently_downloading_uuid, _from, %State{update_info: nil} = state) do
Expand Down Expand Up @@ -143,38 +148,38 @@ defmodule NervesHubLink.UpdateManager do
{:reply, :ok, state}
end

@impl GenServer
def handle_info({:update_reschedule, response, fwup_public_keys}, state) do
{:noreply,
maybe_update_firmware(response, fwup_public_keys, %State{
state
| update_reschedule_timer: nil
})}
end

# messages from FWUP
@impl GenServer
def handle_info({:fwup, message}, state) do
_ = state.fwup_config.handle_fwup_message.(message)

case message do
{:ok, 0, _message} ->
Logger.info("[NervesHubLink] FWUP Finished")
:alarm_handler.clear_alarm(NervesHubLink.UpdateInProgress)
{:noreply, %State{state | fwup: nil, update_info: nil, status: :idle}}

{:noreply,
%State{
state
| fwup: nil,
update_info: nil,
status: :finalizing,
previous_status: :successful
}}

{:progress, percent} ->
{:noreply, %State{state | status: {:updating, percent}}}

{:error, _, message} ->
:alarm_handler.clear_alarm(NervesHubLink.UpdateInProgress)
{:noreply, %State{state | status: {:fwup_error, message}}}
{:noreply, %State{state | status: :idle, previous_status: {:error, message}}}

_ ->
{:noreply, state}
end
end

@spec maybe_update_firmware(UpdateInfo.t(), [binary()], State.t()) :: State.t()
@spec maybe_update_firmware(UpdateInfo.t(), [binary()], State.t()) :: {atom(), State.t()}
defp maybe_update_firmware(
%UpdateInfo{} = _update_info,
_fwup_public_keys,
Expand All @@ -186,43 +191,42 @@ defmodule NervesHubLink.UpdateManager do
# interrupt FWUP and let the task finish. After update and reboot, the
# device will check-in and get an update message if it was actually new and
# required
state
{:ignored, state}
end

defp maybe_update_firmware(%UpdateInfo{} = update_info, fwup_public_keys, %State{} = state) do
# Cancel an existing timer if it exists.
# This prevents rescheduled updates`
# from compounding.
state = maybe_cancel_timer(state)

# possibly offload update decision to an external module.
# This will allow application developers
# to control exactly when an update is applied.
# note: update_available is a behaviour function
case state.fwup_config.update_available.(update_info) do
:apply ->
start_fwup_stream(update_info, fwup_public_keys, state)
NervesHubLink.send_firmware_update_status(:updating)
{{:updating, 0}, start_fwup_stream(update_info, fwup_public_keys, state)}

:ignore ->
state
NervesHubLink.send_firmware_update_status(:ignored)
Logger.info("[NervesHubLink] ignoring firmware update request")
{:ignored, %{state | status: :idle, previous_status: :ignored}}

{:reschedule, ms} ->
timer =
Process.send_after(self(), {:update_reschedule, update_info, fwup_public_keys}, ms)

Logger.info("[NervesHubLink] rescheduling firmware update in #{ms} milliseconds")
%{state | status: :update_rescheduled, update_reschedule_timer: timer}
end
end
{:reschedule, minutes, :minutes} ->
until =
DateTime.utc_now()
|> DateTime.add(minutes, :minute)

defp maybe_update_firmware(_, _, state), do: state
NervesHubLink.send_firmware_update_status(:reschedule, %{until: until})
Logger.info("[NervesHubLink] rescheduling firmware update in #{minutes} minutes")
{:rescheduled, %{state | status: :idle, previous_status: :rescheduled}}

defp maybe_cancel_timer(%{update_reschedule_timer: nil} = state), do: state

defp maybe_cancel_timer(%{update_reschedule_timer: timer} = state) do
_ = Process.cancel_timer(timer)
{:reschedule, ms} ->
until =
DateTime.utc_now()
|> DateTime.add(round(ms / 1_000), :second)

%{state | update_reschedule_timer: nil}
NervesHubLink.send_firmware_update_status(:reschedule, %{until: until})
Logger.info("[NervesHubLink] rescheduling firmware update in #{ms / 1_000} seconds")
{:rescheduled, %{state | status: :idle, previous_status: :rescheduled}}
end
end

@spec start_fwup_stream(UpdateInfo.t(), [binary()], State.t()) :: State.t()
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule NervesHubLink.MixProject do
use Mix.Project

@version "2.6.0"
@version "2.7.0"
@description "Manage your Nerves fleet by connecting it to NervesHub"
@source_url "https://github.com/nerves-hub/nerves_hub_link"

Expand Down
7 changes: 1 addition & 6 deletions test/nerves_hub_link/update_manager_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,8 @@ defmodule NervesHubLink.UpdateManagerTest do
}

{:ok, manager} = UpdateManager.start_link(fwup_config)
assert UpdateManager.apply_update(manager, update_payload, []) == :update_rescheduled
assert UpdateManager.apply_update(manager, update_payload, []) == :rescheduled
assert_received :rescheduled
refute_received {:fwup, _}

assert_receive {:fwup, {:progress, 0}}, 250
assert_receive {:fwup, {:progress, 100}}
assert_receive {:fwup, {:ok, 0, ""}}
end

test "apply with fwup environment", %{update_payload: update_payload, devpath: devpath} do
Expand Down