NervesGithubUpdater.Fwup (nerves_github_updater v0.1.1)

Copy Markdown View Source

Streams a .fw to fwup --apply --framing via an Erlang Port.

Library-shape: no host dependencies.

Wire protocol

Both directions are length-prefixed: [4-byte BE length | payload]. Input from us:

  • [length | bytes] records carrying .fw payload

  • [0] (zero-length record) marks end-of-stream

Output from fwup (each record's payload begins with a 2-byte type code):

  • PR + 2-byte BE percent — progress
  • OK + 2 bytes reserved — success
  • ER + UTF-8 error message — error
  • WN + UTF-8 warning — non-fatal warning

We deliberately do not pass --exit-handshake: in framing mode with an Erlang Port, the handshake byte deadlocks because fwup doesn't see EOF on stdin while the Port is alive. fwup exits cleanly on the zero-length terminator without the handshake.

Source for the framing format: nerves_firmware_http and ssh_subsystem_fwup use the same parser. We rely on Port.command/2's blocking semantics for back-pressure: when fwup's stdin buffer is full, the write blocks until fwup drains.

Caller contract

Callers MUST serialize apply/2 invocations. Each call spawns one fwup process against devpath; concurrent applies to the same device are undefined.

Isolation from port death

The Erlang Port is opened and owned by an isolated spawn_monitor worker, not by the calling process. If the fwup OS process dies mid-stream (broken pipe / :epipe — bad .fw, disk full, early exit), the linked port delivers an EXIT signal only to that worker (which traps exits), never to the caller. The caller only ever receives the worker's {ref, result} message or, if the worker itself crashes unexpectedly, a :DOWN message — both are folded into the documented :ok | {:error, term()} contract.

Accepted risks

  • No --exit-handshake (see above) — the final ER text can race behind :exit_status, so an error exit may report a nil/stale message.
  • :stderr_to_stdout merges stderr into the framed stream. A chatty stderr line could theoretically desynchronize frame parsing or stall it. Accepted because fwup in framing mode keeps stderr quiet in practice.
  • A mid-stream fwup death no longer crashes the caller: it is surfaced as {:error, {:fwup_port_exit, reason}} (trapped exit signal from the dying port) or {:error, :fwup_port_closed} (write attempted after the port was already gone), because the port itself is owned by an isolated monitored worker (see above) rather than by the caller.

Summary

Functions

Applies fw_path via fwup, blocking until fwup exits.

Types

opt()

@type opt() ::
  {:devpath, String.t() | nil}
  | {:task, String.t()}
  | {:progress, (term() -> any())}
  | {:chunk_size, pos_integer()}
  | {:fwup_path, String.t() | nil}
  | {:extra_args, [String.t()]}

Functions

apply(fw_path, opts \\ [])

@spec apply(Path.t(), [opt()]) :: :ok | {:error, term()}

Applies fw_path via fwup, blocking until fwup exits.

Options

  • :devpath — block device to write to. Required for real hardware; for the host-side smoke test it can be a path to a file used as a target.
  • :task — the fwup task name (default "upgrade").
  • :progress — callback invoked as {:flashing, percent} while fwup applies the firmware.
  • :chunk_size — read chunk size for fw_path (default 65536).
  • :fwup_path — explicit path to the fwup executable. Defaults to System.find_executable("fwup").
  • :extra_args — extra fwup CLI args appended after the fixed args (default []). Hook for future delta-update fwup flags like --unsafe.

Returns :ok on exit status 0; {:error, term()} otherwise.