245 lines
7.3 KiB
Elixir
245 lines
7.3 KiB
Elixir
defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FarmwareInstallation do
|
|
use GenServer
|
|
require Farmbot.Logger
|
|
|
|
alias Farmbot.Asset.Repo
|
|
alias Farmbot.Asset.FarmwareInstallation, as: FWI
|
|
|
|
config = Application.get_env(:farmbot_core, __MODULE__)
|
|
@install_dir config[:install_dir] || Mix.raise("Missing Install Dir")
|
|
@error_retry_time_ms config[:error_retry_time_ms] || 30_000
|
|
@manifest_name "manifest.json"
|
|
|
|
def preload(%FWI{}), do: []
|
|
|
|
def start_link(fwi, _args) do
|
|
GenServer.start_link(__MODULE__, fwi)
|
|
end
|
|
|
|
def init(fwi) do
|
|
{:ok, fwi, 0}
|
|
end
|
|
|
|
def handle_info(:timeout, %FWI{manifest: nil} = fwi) do
|
|
Farmbot.Logger.busy(3, "Installing Farmware from url: #{fwi.url}")
|
|
|
|
with {:ok, %{} = manifest} <- get_manifest_json(fwi),
|
|
%{valid?: true} = changeset <- FWI.changeset(fwi, %{manifest: manifest}),
|
|
{:ok, %FWI{} = updated} <- Repo.update(changeset),
|
|
{:ok, zip_binary} <- get_zip(updated),
|
|
:ok <- install_zip(updated, zip_binary),
|
|
:ok <- install_farmware_tools(updated),
|
|
:ok <- write_manifest(updated) do
|
|
# TODO(Connor) -> No reason to keep this process alive?
|
|
{:noreply, fwi}
|
|
else
|
|
error ->
|
|
error_log(fwi, "failed to download Farmware manifest: #{inspect(error)}")
|
|
{:noreply, fwi, @error_retry_time_ms}
|
|
end
|
|
end
|
|
|
|
def handle_info(:timeout, %FWI{} = fwi) do
|
|
with {:ok, %{} = i_manifest} <- load_manifest_json(fwi),
|
|
%{valid?: true} = d_changeset <- FWI.changeset(fwi, %{manifest: i_manifest}),
|
|
%FWI{} = dirty <- Ecto.Changeset.apply_changes(d_changeset),
|
|
{:ok, n_manifest} <- get_manifest_json(fwi),
|
|
%{valid?: true} = n_changeset <- FWI.changeset(fwi, %{manifest: n_manifest}),
|
|
{:ok, %FWI{} = updated} <- Repo.update(n_changeset) do
|
|
maybe_update(dirty, updated)
|
|
else
|
|
# Farmware wasn't found. Reinstall
|
|
{:error, :enoent} ->
|
|
updated =
|
|
FWI.changeset(fwi, %{manifest: nil})
|
|
|> Repo.update!()
|
|
|
|
{:noreply, updated, 0}
|
|
|
|
error ->
|
|
error_log(fwi, "failed to check for updates: #{inspect(error)}")
|
|
|
|
{:noreply, fwi, @error_retry_time_ms}
|
|
end
|
|
end
|
|
|
|
def maybe_update(%FWI{} = installed_fwi, %FWI{} = updated) do
|
|
case Version.compare(installed_fwi.manifest.version, updated.manifest.version) do
|
|
# Installed is newer than remote.
|
|
:gt ->
|
|
success_log(updated, "up to date.")
|
|
{:noreply, updated}
|
|
|
|
# No difference between installed and remote.
|
|
:eq ->
|
|
success_log(updated, "up to date.")
|
|
{:noreply, updated}
|
|
|
|
# Installed version is older than remote
|
|
:lt ->
|
|
success_log(updated, "update available.")
|
|
|
|
with {:ok, zip_binary} <- get_zip(updated),
|
|
:ok <- install_zip(updated, zip_binary),
|
|
:ok <- install_farmware_tools(updated),
|
|
:ok <- write_manifest(updated) do
|
|
{:noreply, updated}
|
|
else
|
|
er ->
|
|
error_log(updated, "update failed: #{inspect(er)}")
|
|
{:noreply, updated, @error_retry_time_ms}
|
|
end
|
|
end
|
|
end
|
|
|
|
def get_manifest_json(%FWI{url: "file://" <> path}) do
|
|
Farmbot.Logger.debug(1, "Using local directory for Farmware manifest")
|
|
|
|
case File.read(Path.join(Path.expand(path), @manifest_name)) do
|
|
{:ok, data} -> Farmbot.JSON.decode(data)
|
|
err -> err
|
|
end
|
|
end
|
|
|
|
def get_manifest_json(%FWI{url: url}) do
|
|
with {:ok, {{_, 200, _}, _headers, data}} <- get(url) do
|
|
Farmbot.JSON.decode(data)
|
|
end
|
|
end
|
|
|
|
def load_manifest_json(%FWI{manifest: %{}} = fwi) do
|
|
with {:ok, data} <- File.read(Path.join(install_dir(fwi), @manifest_name)) do
|
|
Farmbot.JSON.decode(data)
|
|
end
|
|
end
|
|
|
|
def get_zip(%FWI{manifest: %{zip: url}}), do: get_zip(url)
|
|
|
|
def get_zip("file://" <> path) do
|
|
Farmbot.Logger.debug(1, "Using local directory for Farmware zip")
|
|
|
|
with {:ok, files} <- File.ls(path),
|
|
file_list <-
|
|
Enum.map(files, fn filename ->
|
|
{to_charlist(filename), File.read!(Path.join(path, filename))}
|
|
end),
|
|
{:ok, {_path, zip_binary}} <- :zip.create(to_charlist(path), file_list, [:memory]) do
|
|
{:ok, zip_binary}
|
|
end
|
|
end
|
|
|
|
def get_zip(url) when is_binary(url) do
|
|
with {:ok, {{_, 200, _}, _headers, zip_binary}} <- get(url),
|
|
{:ok, zip} <- :zip.zip_open(zip_binary, [:memory]),
|
|
:ok <- :zip.zip_close(zip) do
|
|
{:ok, zip_binary}
|
|
end
|
|
end
|
|
|
|
def install_zip(%FWI{} = fwi, binary) when is_binary(binary) do
|
|
install_zip(install_dir(fwi), binary)
|
|
end
|
|
|
|
def install_zip(dir, binary) do
|
|
with {:ok, _} <- :zip.extract(binary, [{:cwd, dir}]) do
|
|
:ok
|
|
end
|
|
end
|
|
|
|
defp write_manifest(%FWI{manifest: manifest} = fwi) do
|
|
json = FWI.Manifest.view(manifest) |> Farmbot.JSON.encode!()
|
|
|
|
fwi
|
|
|> install_dir()
|
|
|> Path.join(@manifest_name)
|
|
|> File.write(json)
|
|
end
|
|
|
|
def install_farmware_tools(%FWI{manifest: %{farmware_tools_version: version}} = fwi) do
|
|
install_dir = install_dir(fwi)
|
|
File.mkdir_p(Path.join(install_dir, "farmware_tools"))
|
|
|
|
release_url =
|
|
if version == "latest" do
|
|
"https://api.github.com/repos/FarmBot-Labs/farmware-tools/releases/latest"
|
|
else
|
|
"https://api.github.com/repos/FarmBot-Labs/farmware-tools/releases/tags/#{version}"
|
|
end
|
|
|
|
with {:ok, {_commit, zip_url}} <- get_tools_zip_url(release_url),
|
|
{:ok, zip_binary} <- get_zip(zip_url),
|
|
:ok <- install_zip(install_dir, zip_binary) do
|
|
fun = fn {:zip_file, dir, _info, _, _, _} ->
|
|
[_ | rest] = Path.split(to_string(dir))
|
|
List.first(rest) == "farmware_tools"
|
|
end
|
|
|
|
case :zip.extract(zip_binary, [:memory, file_filter: fun]) do
|
|
{:ok, list} when is_list(list) ->
|
|
Enum.each(list, fn {filename, data} ->
|
|
out_file =
|
|
Path.join([install_dir, "farmware_tools", Path.basename(to_string(filename))])
|
|
|
|
File.write!(out_file, data)
|
|
end)
|
|
|
|
{:error, reason} ->
|
|
raise(reason)
|
|
end
|
|
|
|
:ok
|
|
end
|
|
end
|
|
|
|
def get_tools_zip_url(release_url) do
|
|
case get(release_url) do
|
|
{:ok, {{_, 200, _}, _, msg}} ->
|
|
release = Farmbot.JSON.decode!(msg)
|
|
release_commit = release["target_commitish"]
|
|
{:ok, {release_commit, release["zipball_url"]}}
|
|
|
|
{:ok, {{_, _, _}, _, msg}} ->
|
|
case Farmbot.JSON.decode(msg) do
|
|
{:ok, %{"message" => message}} -> {:error, message}
|
|
_ -> {:error, msg}
|
|
end
|
|
|
|
error ->
|
|
error
|
|
end
|
|
end
|
|
|
|
defp get(url) do
|
|
:httpc.request(:get, {to_charlist(url), httpc_headers()}, [], httpc_options())
|
|
end
|
|
|
|
defp httpc_options, do: [body_format: :binary]
|
|
defp httpc_headers, do: [{'user-agent', 'farmbot-os'}]
|
|
|
|
def install_dir(%FWI{} = fwi) do
|
|
install_dir(fwi.manifest)
|
|
end
|
|
|
|
def install_dir(%FWI.Manifest{package: package}) do
|
|
dir = Path.join(@install_dir, package)
|
|
File.mkdir_p!(dir)
|
|
dir
|
|
end
|
|
|
|
defp error_log(%FWI{manifest: %{package: package}}, msg) do
|
|
Farmbot.Logger.error(3, "Farmware #{package} " <> msg)
|
|
end
|
|
|
|
defp error_log(%FWI{}, msg) do
|
|
Farmbot.Logger.error(3, "Farmware " <> msg)
|
|
end
|
|
|
|
defp success_log(%FWI{manifest: %{package: package}}, msg) do
|
|
Farmbot.Logger.success(3, "Farmware #{package} " <> msg)
|
|
end
|
|
|
|
defp success_log(%FWI{}, msg) do
|
|
Farmbot.Logger.success(3, "Farmware " <> msg)
|
|
end
|
|
end
|