312 lines
7.9 KiB
Elixir
312 lines
7.9 KiB
Elixir
defmodule Farmbot.System do
|
|
@moduledoc """
|
|
Common functionality that should be implemented by a system
|
|
"""
|
|
|
|
alias Farmbot.System.Init.Ecto
|
|
use Farmbot.Logger
|
|
|
|
error_msg = """
|
|
Please configure `:system_tasks` and `:data_path`!
|
|
"""
|
|
|
|
@system_tasks Application.get_env(:farmbot, :behaviour)[:system_tasks]
|
|
@system_tasks || Mix.raise(error_msg)
|
|
|
|
@data_path Application.get_env(:farmbot, :data_path)
|
|
@data_path || Mix.raise(error_msg)
|
|
|
|
@typedoc """
|
|
Reason for a task to execute. Should be human readable.
|
|
"""
|
|
@type reason :: binary
|
|
|
|
@typedoc """
|
|
Any ole data that caused a factory reset.
|
|
Will try to format it as a human readable binary.
|
|
"""
|
|
@type unparsed_reason :: any
|
|
|
|
@doc """
|
|
Should remove all persistant data. this includes:
|
|
* network config
|
|
* credentials
|
|
"""
|
|
@callback factory_reset(reason) :: no_return
|
|
|
|
@doc "Restarts the machine."
|
|
@callback reboot(reason) :: no_return
|
|
|
|
@doc "Shuts down the machine."
|
|
@callback shutdown(reason) :: no_return
|
|
|
|
@doc "Remove all configuration data, and reboot."
|
|
@spec factory_reset(unparsed_reason) :: no_return
|
|
def factory_reset(reason) do
|
|
if Farmbot.Project.env == :dev do
|
|
# credo:disable-for-next-line
|
|
require IEx; IEx.pry()
|
|
end
|
|
alias Farmbot.System.ConfigStorage
|
|
import ConfigStorage, only: [get_config_value: 3]
|
|
if Process.whereis ConfigStorage do
|
|
if get_config_value(:bool, "settings", "disable_factory_reset") do
|
|
reboot(reason)
|
|
else
|
|
do_reset(reason)
|
|
end
|
|
else
|
|
do_reset(reason)
|
|
end
|
|
end
|
|
|
|
defp try_lock_fw do
|
|
try do
|
|
Logger.warn 1, "Trying to emergency lock firmware before powerdown"
|
|
Farmbot.Firmware.emergency_lock()
|
|
catch
|
|
_, _ ->
|
|
Logger.error 1, "Firmware unavailable. Can't emergency_lock"
|
|
end
|
|
|
|
end
|
|
|
|
defp do_reset(reason) do
|
|
formatted = format_reason(reason)
|
|
case formatted do
|
|
nil -> reboot("Escape factory reset: #{inspect reason}")
|
|
{:ignore, reason} -> reboot(reason)
|
|
_ ->
|
|
Farmbot.System.NervesHub.deconfigure()
|
|
path = Farmbot.Farmware.Installer.install_root_path()
|
|
File.rm_rf(path)
|
|
Ecto.drop()
|
|
write_file(formatted)
|
|
try_lock_fw()
|
|
@system_tasks.factory_reset(formatted)
|
|
end
|
|
end
|
|
|
|
@doc "Reboot."
|
|
@spec reboot(unparsed_reason) :: no_return
|
|
def reboot(reason) do
|
|
formatted = format_reason(reason)
|
|
write_file(formatted)
|
|
try_lock_fw()
|
|
@system_tasks.reboot(formatted)
|
|
end
|
|
|
|
@doc "Shutdown."
|
|
@spec shutdown(unparsed_reason) :: no_return
|
|
def shutdown(reason) do
|
|
formatted = format_reason(reason)
|
|
write_file(formatted)
|
|
try_lock_fw()
|
|
@system_tasks.shutdown(formatted)
|
|
end
|
|
|
|
defp write_file(nil) do
|
|
file = Path.join(@data_path, "last_shutdown_reason")
|
|
File.rm_rf(file)
|
|
end
|
|
|
|
defp write_file(reason) do
|
|
IO.puts "Farmbot powering down: #{reason}"
|
|
file = Path.join(@data_path, "last_shutdown_reason")
|
|
File.write!(file, reason)
|
|
end
|
|
|
|
@ref Farmbot.Project.commit()
|
|
@target Farmbot.Project.target()
|
|
@env Farmbot.Project.env()
|
|
|
|
@doc "Format an error for human consumption."
|
|
def format_reason(reason) do
|
|
formated = do_format_reason(reason)
|
|
footer = """
|
|
<hr>
|
|
<p>
|
|
<p>
|
|
<p> <strong> environment: </strong> #{@env}
|
|
<p> <strong> source_ref: </strong> #{@ref}
|
|
<p> <strong> target: </strong> #{@target}
|
|
<p>
|
|
<p>
|
|
"""
|
|
|
|
case formated do
|
|
nil -> nil
|
|
{:ignore, reason} -> {:ignore, reason}
|
|
formatted when is_binary(formatted) ->
|
|
if String.contains?(formatted, "DbConnection") do
|
|
{:ignore, """
|
|
https://github.com/scouten/sqlite_ecto2/issues/204
|
|
"""}
|
|
else
|
|
formated <> footer
|
|
end
|
|
end
|
|
end
|
|
|
|
# This mess of pattern matches cleans up erlang startup errors. It's very
|
|
# recursive, and kind of cryptic, but should always produce a human readable
|
|
# message that can be read by an end user.
|
|
alias Farmbot.Bootstrap
|
|
def do_format_reason(
|
|
{:error,
|
|
{:shutdown,
|
|
{:failed_to_start_child, Bootstrap.Supervisor, rest}}})
|
|
do
|
|
do_format_reason(rest)
|
|
end
|
|
|
|
def do_format_reason(
|
|
{:error,
|
|
{:shutdown,
|
|
{:failed_to_start_child, child, rest}}})
|
|
do
|
|
{failed_child, failed_reason} = enumerate_ftsc_error(child, rest)
|
|
if failed_reason do
|
|
"""
|
|
Failed to start child: #{failed_child}
|
|
reason: #{do_format_reason(failed_reason)}
|
|
|
|
This is likely a bug. Please copy or screenshot this error and send it to
|
|
the Farmbot developers.
|
|
"""
|
|
else
|
|
nil
|
|
end
|
|
end
|
|
|
|
def do_format_reason({:bad_return, {Bootstrap.Supervisor, :init, error}}) do
|
|
"""
|
|
Failed to Authorize with Farmbot Web Services.
|
|
reason: #{do_format_reason(error)}
|
|
|
|
This is likely because of bad configuration.
|
|
"""
|
|
end
|
|
|
|
def do_format_reason({:error, reason})
|
|
when is_atom(reason) or is_binary(reason)
|
|
do
|
|
reason |> to_string()
|
|
end
|
|
|
|
def do_format_reason({:error, reason}), do: inspect(reason)
|
|
|
|
def do_format_reason(
|
|
{:failed_connect,
|
|
[{:to_address, {server, port}}, {_, _, reason}]})
|
|
do
|
|
"""
|
|
Failed to connect to server: #{server}:#{port}
|
|
reason: #{do_format_reason(reason)}
|
|
This is likely the result of a
|
|
misconfigured "server" field during configuration.
|
|
"""
|
|
end
|
|
|
|
# TODO(Connor) Remove this some day.
|
|
def do_format_reason({{:case_clause, {:raise, %Sqlite.DbConnection.Error{}}}, _}) do
|
|
{:ignore, """
|
|
https://github.com/scouten/sqlite_ecto2/issues/204
|
|
"""}
|
|
end
|
|
|
|
def do_format_reason({:EXIT, {{:case_clause, {:raise, %Sqlite.DbConnection.Error{}}}, _}}) do
|
|
{:ignore, """
|
|
https://github.com/scouten/sqlite_ecto2/issues/204
|
|
"""}
|
|
end
|
|
|
|
def do_format_reason({:badarg, [{:ets, :lookup_element, _, _} | _]}) do
|
|
{:ignore, """
|
|
Bad Ecto call. This usually is a result of an over the air update and can
|
|
likely be ignored.
|
|
"""}
|
|
end
|
|
|
|
def do_format_reason({{exception, [{module, function, args, info} | _] = stacktrace}, {module_, function_, args_}})
|
|
when is_atom(exception)
|
|
and is_atom(module)
|
|
and is_atom(function)
|
|
and is_list(args)
|
|
and is_list(info)
|
|
and is_atom(module_)
|
|
and is_atom(function_)
|
|
and is_list(args_) do
|
|
"""
|
|
Caught exception: #{exception}
|
|
#{inspect format_stacktrace(stacktrace)}
|
|
"""
|
|
end
|
|
|
|
def do_format_reason(reason), do: do_format_reason({:error, reason})
|
|
|
|
def format_stacktrace(stacktrace, acc \\ [])
|
|
|
|
def format_stacktrace([{_module, _function, arity, _info} = entry | rest], acc) when is_integer(arity) do
|
|
format_stacktrace(rest, [entry | acc])
|
|
end
|
|
|
|
def format_stacktrace([{module, function, args, info} | rest], acc) when is_list(args) do
|
|
entry = {module, function, sanitize_args(args), info}
|
|
format_stacktrace(rest, [entry | acc])
|
|
end
|
|
|
|
def format_stacktrace([], acc), do: Enum.reverse(acc)
|
|
|
|
def sanitize_args(args, acc \\ [])
|
|
|
|
def sanitize_args([arg | rest], acc) do
|
|
sanitize_args(rest, [sanitize_arg(arg) | acc])
|
|
end
|
|
|
|
def sanitize_args([], acc), do: Enum.reverse(acc)
|
|
|
|
# Rudementary check for a token.
|
|
def sanitize_arg(arg) when byte_size(arg) > 80 do
|
|
case Farmbot.Jwt.decode(arg) do
|
|
{:ok, _} -> "[TOKEN REDACTED BY SANITIZER]"
|
|
_ -> arg
|
|
end
|
|
end
|
|
|
|
def sanitize_arg("device_" <> _) do
|
|
"[DEVICE_ID REDACTED BY SANITIZER]"
|
|
end
|
|
|
|
def sanitize_arg(%{} = map_arg) do
|
|
Map.new(map_arg, fn({key, val}) ->
|
|
{key, sanitize_arg(val)}
|
|
end)
|
|
end
|
|
|
|
def sanitize_arg(arg) when is_list(arg) do
|
|
Enum.map(arg, fn(itm) ->
|
|
sanitize_arg(itm)
|
|
end)
|
|
end
|
|
|
|
def sanitize_arg(tuple) when is_tuple(tuple) do
|
|
Tuple.to_list(tuple) |> sanitize_arg() |> List.to_tuple()
|
|
end
|
|
|
|
def sanitize_arg(arg), do: arg
|
|
|
|
# This cleans up nested supervisors/workers.
|
|
defp enumerate_ftsc_error(_child,
|
|
{:shutdown,
|
|
{:failed_to_start_child, child, rest}})
|
|
do
|
|
enumerate_ftsc_error(child, rest)
|
|
end
|
|
|
|
defp enumerate_ftsc_error(child, err) do
|
|
{child, err}
|
|
end
|
|
|
|
end
|