Skip to content

Commit

Permalink
Add more information and rename partition_state request to system_info
Browse files Browse the repository at this point in the history
 * Add reboot request
  • Loading branch information
sylane committed Oct 10, 2024
1 parent 7521fec commit 0585dc5
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 112 deletions.
57 changes: 35 additions & 22 deletions docs/grisp_connect_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,42 +20,41 @@ We use [jsonrpc](https://www.jsonrpc.org) 2.0 between frontend and backend.

</p>
</details>
<details><summary><i>Get - partition_state</i></summary>
<details><summary><i>Get - system_info</i></summary>
<p>

Retrieves the current state of the system’s partition, indicating whether the
system requires a reboot, needs validation, or is running an old partition
with no updates pending. This can be used to check if the system is running a
valid system, or has an update pending.
Retrieves the current state of the system. It retrurns the currently running
release name and version and if update is enabled.

**`params`:**
| key (required *) | value | description |
| ----------------- | -------- | ------------------- |
| `"type"` * | string | `"partition_state"` |
| `"type"` * | string | `"system_info"` |

**`result`**: JSON Object

| key | value | type | description |
|-----------------|-----------|----------|----------------------------------------------------|
| state | string | required | `"old"`, `"old_no_update"`, `"new"`, `"unknown"` |
| message | string | required | Message describing the current state of the system |
| action_required | boolean | required | Indicates whether any action is required (e.g., reboot, validation). |
| key | value | type | description |
|-----------------|-----------|----------|------------------------------------------------------------------|
| relname | string | required | The name of the release running currently on the device |
| relvsn | string | required | The version of the release running currently on the device |
| update_enabled | boolean | required | If updating is enbaled on the device |
| boot_source | map | optional | {"type": "system", "id": ID} or {"type": "removable"} |
| update_status | string | optional | `"ready"`, `"updating"`, `"failed"`, or `"updated"` |
| update_progress | integer | optional | The progress as a percentage |
| update_message | string | optional | Message describing the current state of the system |
| action_required | boolean | optional | `null`, `"reboot"`, `"remove_sdcard_and_reboot"` or `"validate"` |

Meaning of the state:
Meaning of the status:

| key | description |
|-------------------|--------------------------------------------------------------------------------------------|
| `"new"` | The system has booted into a new partition. Validation is required to finalize the update. |
| `"old"` | Current partition is old. A reboot is required to load the new partition. |
| `"old_no_update"` | There is no update pending. The system is running the old partition. |
| `"unknown"` | The current partition state does not match any of the previous described states. |

**`error`**:

| Error Content | When it Happens |
| ----------------------------------------------------| -------------------------------------- |
| `{code: -10, message: "grisp_updater_unavailable"}` | Grisp updater app is not running |
| `"ready"` | The system is ready for initiating an update |
| `"updating"` | The system is in the process of updating |
| `"failed"` | The update failed, but a new update can be initiated |
| `"updated"` | The update succeed, but actions are required like "reboot" or "validate" |

</p>
</details>
<details><summary><i>Post - Start an update</i></summary>
<p>

Expand Down Expand Up @@ -99,11 +98,25 @@ This should only be called if the new software is functioning as expected.

| Error Content | When it Happens |
| ----------------------------------------------------| -------------------------------- |
| `{code: -10, message: "grisp_updater_unavailable"}` | Grisp updater app is not running |
| `{code: -13, message: "validate_from_unbooted", data: 0}` | The current partition N cannot be validated |

</p>
</details>

<details><summary><i>Post - Reboot the device</i></summary>
<p>

**`params`:**
| key (required *) | value | description |
| ----------------- | -------- | -------------------------- |
| `"type"` * | string | `"reboot"` |

**`result`**: `"ok"`

</p>
</details>

### Notifications

<details><summary><code>update</code> <code>{"type":"software_update_event"}</code> - notify the current progess of grisp_updater </summary>
Expand Down
102 changes: 12 additions & 90 deletions src/grisp_connect_api.erl
Original file line number Diff line number Diff line change
Expand Up @@ -63,25 +63,15 @@ handle_rpc_messages([{error, _Code, _Msg, _Data, _ID} = E | Batch], Replies) ->
handle_rpc_messages(Batch, [handle_response(E)| Replies]);
handle_rpc_messages([{internal_error, _, _} = E | Batch], Replies) ->
?LOG_ERROR("JsonRPC: ~p",[E]),
handle_rpc_messages(Batch,
[grisp_connect_jsonrpc:format_error(E)| Replies]).
handle_rpc_messages(Batch, Replies).

handle_request(?method_get, #{type := <<"partition_state">>} = _Params, ID) ->
Info = get_partition_info(),
Reply = case Info of
#{state := _State,
message := _Msg,
action_required := _ActionRequired} = Response ->
{result, Response, ID};
{error, Reason} ->
ReasonBinary = iolist_to_binary(io_lib:format("~p", [Reason])),
grisp_connect_jsonrpc:format_error({internal_error, ReasonBinary, ID})
end,
{send_response, grisp_connect_jsonrpc:encode(Reply)};
handle_request(?method_get, #{type := <<"system_info">>} = _Params, ID) ->
Info = grisp_connect_updater:system_info(),
{send_response, grisp_connect_jsonrpc:encode({result, Info, ID})};
handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) ->
try
URL = maps:get(url, Params),
Reply = case start_update(URL) of
Reply = case grisp_connect_updater:start_update(URL) of
{error, grisp_updater_unavailable} ->
{error, -10, grisp_updater_unavailable, undefined, ID};
{error, already_updating} ->
Expand All @@ -102,7 +92,9 @@ handle_request(?method_post, #{type := <<"start_update">>} = Params, ID) ->
{internal_error, invalid_params, ID})}
end;
handle_request(?method_post, #{type := <<"validate">>}, ID) ->
Reply = case grisp_updater:validate() of
Reply = case grisp_connect_updater:validate() of
{error, grisp_updater_unavailable} ->
{error, -10, grisp_updater_unavailable, undefined, ID};
{error, {validate_from_unbooted, PartitionIndex}} ->
{error, -13, validate_from_unbooted, PartitionIndex, ID};
{error, Reason} ->
Expand All @@ -112,7 +104,10 @@ handle_request(?method_post, #{type := <<"validate">>}, ID) ->
{result, ok, ID}
end,
{send_response, grisp_connect_jsonrpc:encode(Reply)};
handle_request(_, _, ID) ->
handle_request(?method_post, #{type := <<"reboot">>}, ID) ->
grisp_connect_client:reboot(),
{send_response, grisp_connect_jsonrpc:encode({result, ok, ID})};
handle_request(_T, _P, ID) ->
Error = {internal_error, method_not_found, ID},
FormattedError = grisp_connect_jsonrpc:format_error(Error),
{send_response, grisp_connect_jsonrpc:encode(FormattedError)}.
Expand All @@ -126,79 +121,6 @@ handle_response(Response) ->
end,
{handle_response, ID, Reply}.

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

get_partition_info() ->
case is_running(grisp_updater) of
true ->
Info = grisp_updater:info(),
#{boot := Boot, valid := Valid, next := Next} = Info,
ActionRequired = maps:get(action_required, Info, false),
case evaluate_partition_state(Boot, Valid, Next) of
new_boot ->
#{state => <<"new">>,
message => <<"New partition booted, validation required">>,
action_required => ActionRequired};
update_pending ->
#{state => <<"old">>,
message => <<"Reboot required to load new partition">>,
action_required => ActionRequired};
no_update_pending ->
#{state => <<"old_no_update">>,
message => <<"No update pending, running old partition">>,
action_required => ActionRequired};
_ ->
#{state => <<"unknown">>,
message => <<"Unknown partition state">>,
action_required => ActionRequired}
end;
false -> {error, grisp_updater_unavailable}
end.

evaluate_partition_state(BootPartition, ValidPartition, NextPartition) ->
case {BootPartition, ValidPartition, NextPartition} of
% Case 1: Booting from removable media, but system has a pending update
{#{type := removable},
#{type := system, id := ValidId},
#{type := system, id := NextId}}
when ValidId =/= NextId -> update_pending;
% Case 2: Booted from system partition, but a different system partition is pending update
{#{type := system, id := BootId},
#{type := system, id := ValidId},
#{type := system, id := NextId}}
when BootId == ValidId, ValidId =/= NextId -> update_pending;
% Case 3: Booted from a new partition, validation required
{#{type := system, id := BootId},
#{type := system, id := ValidId},
_}
when BootId =/= ValidId -> new_boot;
% Case 4: Booted from removable media, no update pending
{#{type := removable},
#{type := system, id := ValidId},
#{type := system, id := NextId}}
when ValidId == NextId -> no_update_pending;
% Case 5: Booted from system partition, no update pending
{#{type := system, id := BootId},
_,
#{type := system, id := NextId}}
when NextId == BootId -> no_update_pending;
% Default case: Unknown partition state
_ -> unknown_state
end.

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

error_atom(-1) -> device_not_linked;
error_atom(-2) -> token_expired;
error_atom(-3) -> device_already_linked;
Expand Down
10 changes: 10 additions & 0 deletions src/grisp_connect_client.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
-export([connected/0]).
-export([disconnected/0]).
-export([handle_message/1]).
-export([reboot/0]).

-behaviour(gen_statem).
-export([init/1, terminate/3, code_change/4, callback_mode/0]).
Expand Down Expand Up @@ -63,6 +64,9 @@ disconnected() ->
handle_message(Payload) ->
gen_statem:cast(?MODULE, {?FUNCTION_NAME, Payload}).

reboot() ->
erlang:send_after(1000, ?MODULE, reboot).

% gen_statem CALLBACKS ---------------------------------------------------------

init([]) ->
Expand Down Expand Up @@ -156,11 +160,17 @@ handle_common({call, From}, is_connected, State, _) when State =/= connected ->
handle_common({call, From}, {request, _, _, _}, State, _Data)
when State =/= connected ->
{keep_state_and_data, [{reply, From, {error, disconnected}}]};
handle_common(cast, {notify, _Method, _Type, _Params}, _State, _Data) ->
% We ignore notifications sent while disconnected
keep_state_and_data;
handle_common({timeout, ID}, request, _, #data{requests = Requests} = Data) ->
Caller = maps:get(ID, Requests),
{keep_state,
Data#data{requests = maps:remove(ID, Requests)},
[{reply, Caller, {error, timeout}}]};
handle_common(info, reboot, _, _) ->
init:stop(),
keep_state_and_data;
handle_common(cast, Cast, _, _) ->
error({unexpected_cast, Cast});
handle_common({call, _}, Call, _, _) ->
Expand Down
Loading

0 comments on commit 0585dc5

Please sign in to comment.