farmbot_os/farmbot_core/lib/farmbot_core/asset_workers/farmware_installation_worke...

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