src/grisp_connect_updater.erl

%% @doc Library module containing grisp_updater helper functions
-module(grisp_connect_updater).


%--- INCLUDES ------------------------------------------------------------------

-include_lib("kernel/include/logger.hrl").


%--- EXPORTS -------------------------------------------------------------------

-export([is_available/0]).
-export([system_info/0]).
-export([start_update/1]).
-export([validate/0]).
-export([cancel/0]).

% Disable dialyzer warnings because of unknown dependencies
-dialyzer({nowarn_function, system_info/0}).
-dialyzer({nowarn_function, start_update/1}).
-dialyzer({nowarn_function, validate/0}).
-dialyzer({nowarn_function, cancel/0}).
-dialyzer({nowarn_function, update_info/0}).


%--- API -----------------------------------------------------------------------

is_available() ->
    is_running(grisp_updater).

system_info() ->
    RelInfo = try release_handler:which_releases() of
        [{RelName, RelVsn, _, _}] ->
            #{relname => list_to_binary(RelName),
              relvsn => list_to_binary(RelVsn)}
    catch
        exit:{noproc, _} ->
            % Running in a shell
            #{relname => null, relvsn => null};
        error:undef ->
            % Sasl is not running
            #{relname => null, relvsn => null}
    end,
    UpdateInfo = update_info(),
    maps:merge(RelInfo, UpdateInfo).

start_update(URL) ->
    case grisp_connect_updater:is_available() of
        true -> grisp_updater:start(URL,
                                    grisp_connect_updater_progress,
                                    #{client => self()}, #{});
        false -> {error, grisp_updater_unavailable}
    end.

validate() ->
    case grisp_connect_updater:is_available() of
        true -> grisp_updater:validate();
        false -> {error, grisp_updater_unavailable}
    end.

cancel() ->
    case grisp_connect_updater:is_available() of
        true -> grisp_updater:cancel();
        false -> {error, grisp_updater_unavailable}
    end.


%--- INTERNAL FUNCTIONS --------------------------------------------------------

is_running(AppName) ->
    Apps = application:which_applications(),
    case [App || {App, _Desc, _VSN} <- Apps, App =:= AppName] of
        [] -> false;
        [_] -> true
    end.

update_info() ->
    case grisp_connect_updater:is_available() of
        false ->
            #{update_enabled => false};
        true ->
            Info = grisp_updater:info(),
            Status = grisp_updater:status(),
            #{boot := Boot, valid := Valid, next := Next} = Info,
            case {Status, Boot, Valid, Next} of
                % Ready for update from removable media
                {ready,
                 #{type := removable} = Boot,
                 #{type := system, id := ValidId},
                 #{type := system, id := NextId}}
                    when ValidId =:= NextId ->
                        #{update_enabled => true,
                          boot_source => Boot,
                          update_status => ready,
                          update_message => <<"Device ready for update">>};
                % Ready for update from valid system
                {ready,
                 #{type := system, id := BootId} = Boot,
                 #{type := system, id := ValidId},
                 #{type := system, id := NextId}}
                    when ValidId =:= NextId, ValidId =:= BootId ->
                        #{update_enabled => true,
                          boot_source => Boot,
                          update_status => ready,
                          update_message => <<"Device ready for update">>};
                % Updating
                {{updating, Stats}, Boot, _, _} ->
                        #{data_total := Total, data_checked := Checked,
                          data_skipped := Skipped, data_written := Written} = Stats,
                        Percent = (Checked + Skipped + Written) * 100 div (Total * 2),
                        #{update_enabled => true,
                          boot_source => Boot,
                          update_status => updating,
                          update_progress => Percent,
                          update_message => <<"Device is updating">>};
                % Update Failed
                {{error, canceled}, Boot, _, _} ->
                        #{update_enabled => true,
                          boot_source => Boot,
                          update_status => canceled,
                          update_message => <<"Device update canceled">>};
                % Update Failed
                {{error, _Reason}, Boot, _, _} ->
                        #{update_enabled => true,
                          boot_source => Boot,
                          update_status => failed,
                          update_message => <<"Device update failed">>};
                % Update succeed
                {{success, _Stats},
                 Boot,
                 #{type := system, id := ValidId},
                 #{type := system, id := NextId}}
                    when ValidId =/= NextId ->
                        #{update_enabled => true,
                          update_status => updated,
                          boot_source => Boot,
                          update_message => <<"Device updated, reboot required to validate the update">>,
                          action_required => reboot};
                % Booted from removable after update
                {ready,
                 #{type := removable} = Boot,
                 #{type := system, id := ValidId},
                 #{type := system, id := NextId}}
                    when ValidId =/= NextId ->
                        #{update_enabled => true,
                          boot_source => Boot,
                          update_status => updated,
                          update_message => <<"Device updated but the SD card wasn't removed before rebooting">>,
                          action_required => remove_sdcard_and_reboot};
                % Updated and rebooted
                {ready,
                 #{type := system, id := TestId} = Boot,
                 #{type := system, id := ValidId},
                 #{type := system, id := NextId}}
                    when ValidId =/= TestId, ValidId =:= NextId ->
                        #{update_enabled => true,
                          boot_source => Boot,
                          update_status => updated,
                          update_message => <<"Device updated, validation required">>,
                          action_required => validate};
                _ ->
                    #{update_enabled => true}
            end
    end.