Current section
Files
Jump to
Current section
Files
lib/nerves_firmware_ssh/fwup.ex
defmodule Nerves.Firmware.SSH.Fwup do
use GenServer
require Logger
@moduledoc false
def start_link(cm) do
GenServer.start_link(__MODULE__, [cm])
end
def send_chunk(pid, chunk) do
GenServer.call(pid, {:send, chunk})
end
def init([cm]) do
fwup = System.find_executable("fwup")
devpath = Nerves.Runtime.KV.get("nerves_fw_devpath") || "/dev/mmcblk0"
task = "upgrade"
args = if supports_handshake(), do: ["--exit-handshake"], else: []
args = args ++ ["--apply", "--no-unmount", "-d", devpath, "--task", task]
port =
Port.open({:spawn_executable, fwup}, [
{:args, args},
:use_stdio,
:binary,
:exit_status
])
{:ok, %{port: port, cm: cm}}
end
def handle_call(_cmd, _from, %{port: nil} = state) do
# In the process of closing down, so just ignore these.
{:reply, :error, state}
end
def handle_call({:send, chunk}, _from, state) do
# Since fwup may be slower than ssh, we need to provide backpressure
# here. It's tricky since `Port.command/2` is the only way to send
# bytes to fwup synchronously, but it's possible for fwup to error
# out when it's sending. If fwup errors out, then we need to make
# sure that a message gets back to the user for what happened.
# `Port.command/2` exits on error (it will be an :epipe error).
# Therefore we start a new process to call `Port.command/2` while
# we continue to handle responses. We also trap_exit to get messages
# when the port the Task exit.
result =
try do
Port.command(state.port, chunk)
:ok
rescue
ArgumentError ->
Logger.info("Port.command ArgumentError race condition detected and handled")
:error
end
{:reply, result, state}
end
def handle_info({port, {:data, response}}, %{port: port} = state) do
# fwup says that it's going to exit by sending a CTRL+Z (0x1a)
case String.split(response, "\x1a", parts: 2) do
[response] ->
:ssh_channel.cast(state.cm, {:fwup_data, response})
[response, <<status>>] ->
# fwup exited with status
Logger.info("fwup exited with status #{status}")
send(port, {self(), :close})
:ssh_channel.cast(state.cm, {:fwup_data, response})
:ssh_channel.cast(state.cm, {:fwup_exit, status})
[response, other] ->
# fwup exited without status
Logger.info("fwup exited improperly: #{inspect(other)}")
send(port, {self(), :close})
:ssh_channel.cast(state.cm, {:fwup_data, response})
end
{:noreply, state}
end
def handle_info({port, {:exit_status, status}}, %{port: port} = state) do
Logger.info("fwup exited with status #{status} without handshaking")
:ssh_channel.cast(state.cm, {:fwup_exit, status})
{:noreply, %{state | port: nil}}
end
def handle_info({port, :closed}, %{port: port} = state) do
Logger.info("fwup port was closed")
:ssh_channel.cast(state.cm, {:fwup_exit, 0})
{:noreply, %{state | port: nil}}
end
defp supports_handshake() do
Version.match?(fwup_version(), "> 0.17.0")
end
defp fwup_version() do
{version_str, 0} = System.cmd("fwup", ["--version"])
version_str
|> String.trim()
|> Version.parse!()
end
end