Current section

Files

Jump to
nerves_runtime lib nerves_runtime.ex
Raw

lib/nerves_runtime.ex

defmodule Nerves.Runtime do
require Logger
alias Nerves.Runtime.OutputLogger
# This is provided by all of the official Nerves system images
@revert_fw_path "/usr/share/fwup/revert.fw"
@typedoc """
Options for `Nerves.Runtime.revert/1`.
* `:reboot` - Call `Nerves.Runtime.reboot/0` after reverting (defaults to `true`)
"""
@type revert_options :: {:reboot, boolean()}
@moduledoc """
Nerves.Runtime contains functions useful for almost all Nerves-based devices.
"""
@doc """
Reboot the device and gracefully shutdown the Erlang VM.
This calls `:init.stop/0` internally. If `:init.stop/0` takes longer than the
`erlinit.config`'s `--graceful-powerdown` setting (likely 10 seconds) then
the system will be hard rebooted.
"""
@spec reboot() :: no_return()
def reboot(), do: logged_shutdown("reboot")
@doc """
Power off the device.
This calls `:init.stop/0` internally. If `:init.stop/0` takes longer than the
`erlinit.config`'s `--graceful-powerdown` setting (likely 10 seconds) then
the system will be hard rebooted.
"""
@spec poweroff() :: no_return()
def poweroff(), do: logged_shutdown("poweroff")
@doc """
Halt the device (meaning hang, not power off, nor reboot).
Note: this is different than :erlang.halt(), which exits BEAM, and may end up
rebooting the device if `erlinit.config` settings allow reboot on exit.
"""
@spec halt() :: no_return()
def halt(), do: logged_shutdown("halt")
@doc """
Revert the device to running the previous firmware.
This requires a specially constructed fw file.
"""
@spec revert([revert_options]) :: :ok | {:error, reason :: any} | no_return()
def revert(opts \\ []) do
reboot? = if opts[:reboot] != nil, do: opts[:reboot], else: true
if File.exists?(@revert_fw_path) do
{_, 0} = cmd("fwup", [@revert_fw_path, "-t", "revert", "-d", "/dev/rootdisk0"], :info)
if reboot? do
reboot()
else
:ok
end
else
{:error, "Unable to locate revert firmware at path: #{@revert_fw_path}"}
end
end
@doc """
Run system command and log output into logger.
"""
@spec cmd(binary(), [binary()], :debug | :info | :warn | :error | :return) ::
{Collectable.t(), exit_status :: non_neg_integer()}
def cmd(cmd, params, :return), do: System.cmd(cmd, params, stderr_to_stdout: true)
def cmd(cmd, params, out),
do: System.cmd(cmd, params, into: OutputLogger.new(out), stderr_to_stdout: true)
@doc """
Return whether the application was built for either the host or the target
"""
@spec target() :: String.t()
def target() do
target = Application.get_env(:nerves_runtime, :target)
if target == "host", do: "host", else: "target"
end
# private helpers
@spec logged_shutdown(String.t()) :: no_return()
defp logged_shutdown(cmd) do
try do
_ = Logger.info("#{__MODULE__} : device told to #{cmd}")
# Invoke the appropriate command to tell erlinit that a shutdown of the
# Erlang VM is imminent. Once this returns, the Erlang has about 10
# seconds to exit unless `--graceful-powerdown` is used in the
# `erlinit.config` to modify the timeout.
{_, 0} = cmd(cmd, [], :info)
# Start a graceful shutdown
:ok = :init.stop()
# `:init.stop()` is asynchronous, so sleep longer than it takes to avoid
# returning.
Process.sleep(60_000)
after
# If anything unexpected happens, call :erlang.halt() to avoid getting
# stuck in a state where the application thinks it's done.
:erlang.halt()
end
end
end