%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright © 2021-2024 Broadcom. All Rights Reserved. The term "Broadcom"
%% refers to Broadcom Inc. and/or its subsidiaries.
%%
%% @hidden
-module(khepri_utils).
-include_lib("stdlib/include/assert.hrl").
-include("include/khepri.hrl").
-include("src/khepri_error.hrl").
-export([start_timeout_window/1,
end_timeout_window/2,
sleep/2,
is_ra_server_alive/1,
node_props_to_payload/2,
flat_struct_to_tree/1,
flat_struct_to_tree/2,
display_tree/1,
display_tree/2,
display_tree/3,
should_process_module/1,
init_list_of_modules_to_skip/0,
clear_list_of_modules_to_skip/0,
format_exception/4]).
%% khepri:get_root/1 is unexported when compiled without `-DTEST'.
-dialyzer(no_missing_calls).
-type display_tree() :: #{data => khepri:data(),
sproc => horus:horus_fun(),
payload_version => khepri:payload_version(),
child_list_version => khepri:child_list_version(),
child_list_length => khepri:child_list_length(),
child_names => [khepri_path:node_id()],
child_nodes => #{khepri_path:node_id() =>
display_tree()}}.
-export_type([display_tree/0]).
-spec start_timeout_window(Timeout) -> Timestamp | none when
Timeout :: timeout(),
Timestamp :: integer().
start_timeout_window(infinity) ->
none;
start_timeout_window(_Timeout) ->
erlang:monotonic_time().
-spec end_timeout_window(Timeout, Timestamp | none) -> Timeout when
Timeout :: timeout(),
Timestamp :: integer().
end_timeout_window(infinity = Timeout, none) ->
Timeout;
end_timeout_window(Timeout, T0) ->
T1 = erlang:monotonic_time(),
TDiff = erlang:convert_time_unit(T1 - T0, native, millisecond),
Remaining = Timeout - TDiff,
erlang:max(Remaining, 0).
-spec sleep(Time, Timeout) -> Timeout when
Time :: non_neg_integer(),
Timeout :: timeout().
sleep(Time, infinity = Timeout) ->
timer:sleep(Time),
Timeout;
sleep(_Time, 0 = Timeout) ->
Timeout;
sleep(Time, Timeout) when Time =< Timeout ->
timer:sleep(Time),
Timeout - Time;
sleep(Time, Timeout) when Time > Timeout ->
timer:sleep(Timeout),
0.
-spec is_ra_server_alive(RaServer) -> IsAlive when
RaServer :: ra:server_id(),
IsAlive :: boolean().
is_ra_server_alive({RegName, Node}) when Node =:= node() ->
is_pid(erlang:whereis(RegName)).
node_props_to_payload(#{data := Data}, _Default) -> Data;
node_props_to_payload(#{sproc := StandaloneFun}, _Default) -> StandaloneFun;
node_props_to_payload(_NodeProps, Default) -> Default.
-spec flat_struct_to_tree(NodePropsMap) -> DisplayTree when
NodePropsMap :: khepri_adv:node_props_map(),
DisplayTree :: display_tree().
flat_struct_to_tree(FlatStruct) ->
flat_struct_to_tree(FlatStruct, fun(NodeProps) -> NodeProps end).
-spec flat_struct_to_tree(NodePropsMap, MapFun) -> DisplayTree when
NodePropsMap :: khepri_adv:node_props_map(),
MapFun :: fun((khepri:node_props()) -> khepri:node_props()),
DisplayTree :: display_tree().
flat_struct_to_tree(FlatStruct, MapFun) ->
NodeProps = maps:get([], FlatStruct, #{}),
Children = maps:fold(
fun(Children, Props, Tree) ->
flat_struct_to_tree(Children, Props, Tree, MapFun)
end,
#{},
maps:remove([], FlatStruct)),
NodeProps#{child_nodes => Children}.
flat_struct_to_tree([ChildName | [_ | _] = Path], NodeProps, Tree, MapFun) ->
Child1 = case Tree of
#{ChildName := Child} ->
Children = maps:get(child_nodes, Child, #{}),
Children1 = flat_struct_to_tree(
Path, NodeProps, Children, MapFun),
Child#{child_nodes => Children1};
_ ->
Children1 = flat_struct_to_tree(
Path, NodeProps, #{}, MapFun),
#{child_nodes => Children1}
end,
Tree#{ChildName => Child1};
flat_struct_to_tree([ChildName], NodeProps, Tree, MapFun) ->
case Tree of
#{ChildName := Child} ->
?assertEqual([child_nodes], maps:keys(Child)),
?assertNot(maps:is_key(child_nodes, NodeProps)),
NodeProps1 = maps:merge(NodeProps, Child),
Tree#{ChildName => MapFun(NodeProps1)};
_ ->
Tree#{ChildName => MapFun(NodeProps)}
end.
-spec display_tree(display_tree()) -> ok.
display_tree(Tree) ->
display_tree(Tree, "").
display_tree(Tree, Options) when is_map(Options) ->
display_tree(Tree, "", Options);
display_tree(Tree, Prefix) when is_list(Prefix) ->
display_tree(Tree, Prefix, #{colors => true,
lines => true}).
display_tree(#{child_nodes := Children}, Prefix, Options) ->
Keys = lists:sort(
fun(A, B) ->
ABin = ensure_is_binary(A),
BBin = ensure_is_binary(B),
case ABin == BBin of
true -> A =< B;
false -> ABin =< BBin
end
end, maps:keys(Children)),
display_nodes(Keys, Children, Prefix, Options);
display_tree(_, _, _) ->
ok.
ensure_is_binary(Key) when is_atom(Key) ->
list_to_binary(atom_to_list(Key));
ensure_is_binary(Key) when is_binary(Key) ->
Key.
display_nodes([Key | Rest], Children, Prefix, Options) ->
IsLast = Rest =:= [],
display_node_branch(Key, IsLast, Prefix, Options),
NodeProps = maps:get(Key, Children),
NewPrefix = Prefix ++ prefix(IsLast, Options),
DataPrefix = NewPrefix ++ data_prefix(NodeProps, Options),
case NodeProps of
#{data := Data} -> display_data(Data, DataPrefix, Options);
#{sproc := Fun} -> display_data(Fun, DataPrefix, Options);
_ -> ok
end,
display_tree(NodeProps, NewPrefix, Options),
display_nodes(Rest, Children, Prefix, Options);
display_nodes([], _, _, _) ->
ok.
display_node_branch(Key, false, Prefix, #{lines := false}) ->
io:format("~ts+-- ~ts~n", [Prefix, format_key(Key)]);
display_node_branch(Key, true, Prefix, #{lines := false}) ->
io:format("~ts`-- ~ts~n", [Prefix, format_key(Key)]);
display_node_branch(Key, false, Prefix, _Options) ->
io:format("~ts├── ~ts~n", [Prefix, format_key(Key)]);
display_node_branch(Key, true, Prefix, _Options) ->
io:format("~ts╰── ~ts~n", [Prefix, format_key(Key)]).
format_key(Key) ->
io_lib:format("~0p", [Key]).
display_data(Data, Prefix, Options) ->
Formatted = format_data(Data, Options),
Lines = string:split(Formatted, "\n", all),
case Options of
#{colors := false} ->
lists:foreach(
fun(Line) ->
io:format("~ts~ts~n", [Prefix, Line])
end, Lines);
_ ->
lists:foreach(
fun(Line) ->
io:format(
"~ts\033[38;5;246m~ts\033[0m~n",
[Prefix, Line])
end, Lines)
end,
io:format("~ts~n", [string:trim(Prefix, trailing)]).
prefix(false, #{lines := false}) -> "| ";
prefix(false, _Options) -> "│ ";
prefix(true, _Options) -> " ".
data_prefix(#{child_nodes := _}, #{lines := false}) -> "| ";
data_prefix(#{child_nodes := _}, _Options) -> "│ ";
data_prefix(_, #{lines := false}) -> " ";
data_prefix(_, _Options) -> " ".
format_data(Data, _Options) ->
lists:flatten(io_lib:format("Data: ~tp", [Data])).
%% -------------------------------------------------------------------
%% Helpers to standalone function extraction.
%% -------------------------------------------------------------------
-define(PT_MODULES_TO_SKIP, {khepri, skipped_modules_in_code_collection}).
should_process_module(Module) ->
case horus_utils:should_process_module(Module) of
true ->
Modules = persistent_term:get(?PT_MODULES_TO_SKIP),
not maps:is_key(Module, Modules);
false ->
false
end.
init_list_of_modules_to_skip() ->
Applications = [mnesia,
sasl,
ssl,
khepri],
LoadedApps = [App ||
{App, _Desc, _Vsn} <- application:loaded_applications()],
?assert(lists:member(khepri, LoadedApps)),
Modules = lists:foldl(
fun(App, Modules0) ->
_ = case lists:member(App, LoadedApps) of
true -> ok;
false -> application:load(App)
end,
Mods = case application:get_key(App, modules) of
{ok, Mods0} -> Mods0;
undefined -> []
end,
lists:foldl(
fun(Mod, Modules1) ->
Modules1#{Mod => true}
end, Modules0, Mods)
end, #{}, Applications),
?assert(maps:is_key(khepri, Modules)),
persistent_term:put(?PT_MODULES_TO_SKIP, Modules),
ok.
clear_list_of_modules_to_skip() ->
_ = persistent_term:erase(?PT_MODULES_TO_SKIP),
ok.
format_exception(Class, Reason, Stacktrace, Options) ->
erl_error:format_exception(Class, Reason, Stacktrace, Options).