defmodule Nerves.Firmware.Server do @moduledoc false use GenServer alias Nerves.Firmware.Fwup require Logger @type reason :: term @type state :: Struct.t defmodule State do @moduledoc false defstruct status: :active, device: nil end def start_link() do GenServer.start __MODULE__, :ok, name: __MODULE__ end @spec init(term) :: {:ok, state} | {:error, reason} def init(_arg) do Logger.debug "#{__MODULE__}.init" device = Application.get_env(:nerves_firmware, :device, "/dev/mmcblk0") {:ok, %State{status: :active, device: device}} end def handle_call({:state}, _from, state) do {:reply, public_state(state), state} end def handle_call({:allow_upgrade?}, _from, state) do {:reply, allow_upgrade?(state), state} end def handle_call({:apply, firmware, action}, _from, state) do try_apply_if_allowed state, fn() -> apply(firmware, state.device, action) end end def handle_call({:upgrade_and_finalize, firmware}, _from, state) do try_apply_if_allowed state, fn() -> do_upgrade_and_finalize(firmware, state) end end defp try_apply_if_allowed(state, afn) do if allow_upgrade?(state) do try_apply(state, afn) else {:reply, {:error, :await_restart}, state} end end defp try_apply(state, afn) do case afn.() do :ok -> {:reply, :ok, %{state | status: :await_restart}} {:error, reason} -> {:reply, {:error, reason}, state} end end defp allow_upgrade?(state) do state.status == :active end @spec do_upgrade_and_finalize(String.t, Struct.t) :: :ok | {:error, reason} defp do_upgrade_and_finalize(firmware, state) do finalize_fw = Application.get_env(:nerves_firmware, :finalize_fw, "/tmp/finalize.fw") File.rm(finalize_fw) case Fwup.apply(firmware, state.device, "upgrade") do {:error, reason} -> Logger.error "#{__MODULE__} upgrade failed: #{inspect reason}" {:error, reason} :ok -> Logger.info "upgrade succeded" if File.exists?(finalize_fw) do Logger.info "Found #{finalize_fw}, applying finalize/on-reboot" try_finalize(finalize_fw, state) else :ok end end end # called after upgrade to see if we have a finalize.fw for a 2-phase # firmware update. If so, apply it by running the on-reboot task. @spec try_finalize(String.t, state) :: :ok | {:error, :reason} defp try_finalize(ffw, state) do case Fwup.apply(ffw, state.device, "on-reboot") do :ok -> Logger.info "finalize/on-reboot succeded, ready for restart" :ok {:error, reason} -> Logger.info "finalize/on-reboot failed: #{inspect reason}" {:error, reason} end end # return the public "state" of the firmware (not genserver state) defp public_state(state) do %{status: state.status, device: state.device} end end