442 lines
19 KiB
Erlang
442 lines
19 KiB
Erlang
%%%-------------------------------------------------------------------
|
||
%%% @author anlicheng
|
||
%%% @copyright (C) 2025, <COMPANY>
|
||
%%% @doc
|
||
%%%
|
||
%%% @end
|
||
%%% Created : 20. 4月 2026 00:00
|
||
%%%-------------------------------------------------------------------
|
||
-module(efka_iot_client).
|
||
-author("anlicheng").
|
||
-include("efka_tables.hrl").
|
||
|
||
-behaviour(gen_statem).
|
||
|
||
%% API
|
||
-export([start_link/0]).
|
||
-export([metric_data/2, ping/13, task_event_stream/3, close_task_event_stream/2]).
|
||
-export([is_activated/0, dropped_message_count/0]).
|
||
|
||
%% gen_statem callbacks
|
||
-export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]).
|
||
|
||
-define(SERVER, ?MODULE).
|
||
|
||
%% 标记当前agent的状态,只有在 activated 状态下才可以正常的发送数据
|
||
-define(STATE_DISCONNECTED, disconnected).
|
||
%% 等待校验中
|
||
-define(STATE_AUTH, auth).
|
||
%% 激活状态下
|
||
-define(STATE_ACTIVATED, activated).
|
||
-define(SSL_PING_INTERVAL, 30000).
|
||
-define(OUTBOX_SEGMENT_RECORD_LIMIT, 2000).
|
||
-define(OUTBOX_MAX_SEGMENTS, 5).
|
||
-define(OUTBOX_MAX_RECORD_BYTES, 16 * 1024 * 1024).
|
||
|
||
-record(state, {
|
||
socket :: undefined | ssl:sslsocket(),
|
||
outbox :: efka_iot_outbox:outbox(),
|
||
%% 保存当前auth请求的ref,用来建立auth请求和响应的对应关系
|
||
auth_ref = undefined :: undefined | binary(),
|
||
ping_timer_ref = undefined :: undefined | reference(),
|
||
dropped_message_count = 0 :: non_neg_integer()
|
||
}).
|
||
|
||
%%%===================================================================
|
||
%%% API
|
||
%%%===================================================================
|
||
|
||
%% 发送数据
|
||
-spec metric_data(RouteKey :: binary(), Metric :: binary()) -> ok.
|
||
metric_data(RouteKey, Metric) when is_binary(RouteKey), is_binary(Metric) ->
|
||
gen_statem:cast(?SERVER, {metric_data, RouteKey, Metric}).
|
||
|
||
-spec task_event_stream(TaskId :: integer(), Type :: binary(), Stream :: binary()) -> ok.
|
||
task_event_stream(TaskId, Type, Stream) when is_integer(TaskId), is_binary(Type), is_binary(Stream) ->
|
||
gen_statem:cast(?SERVER, {task_event_stream, TaskId, Type, Stream}).
|
||
|
||
-spec close_task_event_stream(TaskId :: integer(), Reason :: binary()) -> ok.
|
||
close_task_event_stream(TaskId, Reason) when is_integer(TaskId), is_binary(Reason) ->
|
||
gen_statem:cast(?SERVER, {close_task_event_stream, TaskId, Reason}).
|
||
|
||
-spec is_activated() -> boolean().
|
||
is_activated() ->
|
||
gen_statem:call(?SERVER, is_activated).
|
||
|
||
-spec dropped_message_count() -> non_neg_integer().
|
||
dropped_message_count() ->
|
||
gen_statem:call(?SERVER, dropped_message_count).
|
||
|
||
-spec ping(term(), term(), term(), term(), term(), term(), term(), term(), term(), term(), term(), term(), term()) -> ok.
|
||
ping(AdCode, BootTime, Province, City, EfkaVersion, KernelArch, Ips, CpuCore, CpuLoad, CpuTemperature, Disk, Memory, Interfaces) ->
|
||
gen_statem:cast(?SERVER, {ping, AdCode, BootTime, Province, City, EfkaVersion, KernelArch, Ips, CpuCore, CpuLoad, CpuTemperature, Disk, Memory, Interfaces}).
|
||
|
||
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
|
||
start_link() ->
|
||
gen_statem:start_link({local, ?SERVER}, ?MODULE, [], []).
|
||
|
||
%%%===================================================================
|
||
%%% gen_statem callbacks
|
||
%%%===================================================================
|
||
|
||
-spec init(list()) -> {ok, atom(), #state{}}.
|
||
init([]) ->
|
||
case efka_iot_outbox:open(outbox_options()) of
|
||
{ok, Outbox} ->
|
||
erlang:start_timer(0, self(), create_transport),
|
||
{ok, ?STATE_DISCONNECTED, #state{socket = undefined, outbox = Outbox}};
|
||
{error, Reason} ->
|
||
{stop, Reason}
|
||
end.
|
||
|
||
-spec callback_mode() -> handle_event_function.
|
||
callback_mode() ->
|
||
handle_event_function.
|
||
|
||
-spec outbox_options() -> map().
|
||
outbox_options() ->
|
||
{ok, DetsDir} = application:get_env(efka, dets_dir),
|
||
#{
|
||
dir => filename:join(DetsDir, "iot_outbox"),
|
||
segment_record_limit => ?OUTBOX_SEGMENT_RECORD_LIMIT,
|
||
max_segments => ?OUTBOX_MAX_SEGMENTS,
|
||
max_record_bytes => ?OUTBOX_MAX_RECORD_BYTES
|
||
}.
|
||
|
||
%% 异步发送数据,连接存在时直接发送;否则写入持久化 outbox。
|
||
-spec handle_event(term(), term(), atom(), #state{}) -> term().
|
||
handle_event(cast, {metric_data, RouteKey, Metric}, StateName, State = #state{socket = Socket}) ->
|
||
Packet = term_to_binary({<<"message">>, {<<"data">>, #{<<"route_key">> => RouteKey, <<"metric">> => Metric}}}),
|
||
case StateName of
|
||
?STATE_ACTIVATED ->
|
||
ok = ssl:send(Socket, Packet),
|
||
{keep_state, State};
|
||
_ ->
|
||
case efka_iot_outbox:append(Packet, State#state.outbox) of
|
||
{ok, Outbox} ->
|
||
{keep_state, State#state{outbox = Outbox}};
|
||
{dropped, capacity_reached, Outbox} ->
|
||
logger:warning("[efka_iot_client] outbox capacity reached, drop offline metric"),
|
||
{keep_state, State#state{
|
||
outbox = Outbox,
|
||
dropped_message_count = State#state.dropped_message_count + 1
|
||
}};
|
||
{error, Reason} ->
|
||
logger:warning("[efka_iot_client] append outbox failed, reason: ~p", [Reason]),
|
||
{keep_state, State#state{dropped_message_count = State#state.dropped_message_count + 1}}
|
||
end
|
||
end;
|
||
|
||
%% Task的stream流,只做实时的
|
||
handle_event(cast, {task_event_stream, TaskId, Type, Stream}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
|
||
logger:debug("[efka_iot_client] event_stream task_id: ~p, stream: ~ts", [TaskId, Stream]),
|
||
Packet = term_to_binary({<<"message">>, {<<"task_event">>, #{<<"task_id">> => TaskId, <<"type">> => Type, <<"stream">> => Stream}}}),
|
||
ok = ssl:send(Socket, Packet),
|
||
{keep_state, State};
|
||
|
||
handle_event(cast, {close_task_event_stream, TaskId, Reason}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
|
||
Packet = term_to_binary({<<"message">>, {<<"task_event">>, #{<<"task_id">> => TaskId, <<"type">> => <<"close">>, <<"stream">> => Reason}}}),
|
||
ok = ssl:send(Socket, Packet),
|
||
{keep_state, State};
|
||
|
||
%% 其他情况下直接忽略
|
||
handle_event(cast, _, _, State = #state{}) ->
|
||
{keep_state, State};
|
||
|
||
handle_event({call, From}, is_activated, ?STATE_ACTIVATED, State = #state{}) ->
|
||
{keep_state, State, [{reply, From, true}]};
|
||
handle_event({call, From}, is_activated, _StateName, State = #state{}) ->
|
||
{keep_state, State, [{reply, From, false}]};
|
||
handle_event({call, From}, dropped_message_count, _StateName, State = #state{dropped_message_count = DroppedCount}) ->
|
||
{keep_state, State, [{reply, From, DroppedCount}]};
|
||
|
||
%% 异步建立到服务器的连接
|
||
handle_event(info, {timeout, _, create_transport}, ?STATE_DISCONNECTED, State) ->
|
||
case connect_socket() of
|
||
{ok, Socket} ->
|
||
Ref = request_ref(),
|
||
AuthPacket = auth_packet(Ref),
|
||
ok = ssl:send(Socket, AuthPacket),
|
||
logger:debug("[efka_iot_client] send auth request, ref: ~p", [Ref]),
|
||
{next_state, ?STATE_AUTH, State#state{socket = Socket, auth_ref = Ref}, [{state_timeout, 5000, auth_timeout}]};
|
||
{error, _Reason} ->
|
||
schedule_reconnect(),
|
||
{keep_state, State#state{socket = undefined}}
|
||
end;
|
||
|
||
handle_event(state_timeout, auth_timeout, ?STATE_AUTH, State = #state{socket = Socket}) ->
|
||
logger:debug("[efka_iot_client] auth request timeout"),
|
||
disconnect(Socket),
|
||
schedule_reconnect(),
|
||
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
|
||
|
||
handle_event(info, {timeout, TimerRef, ssl_ping}, ?STATE_ACTIVATED, State = #state{socket = Socket, ping_timer_ref = TimerRef}) ->
|
||
Packet = term_to_binary({<<"message">>, <<"ping">>}),
|
||
case ssl:send(Socket, Packet) of
|
||
ok ->
|
||
{keep_state, schedule_ssl_ping(State)};
|
||
{error, Reason} ->
|
||
logger:warning("[efka_iot_client] send ssl ping failed, reason: ~p", [Reason]),
|
||
disconnect(Socket),
|
||
schedule_reconnect(),
|
||
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, ping_timer_ref = undefined}}
|
||
end;
|
||
handle_event(info, {timeout, _TimerRef, ssl_ping}, _StateName, State) ->
|
||
{keep_state, State};
|
||
|
||
%% 将缓存中的数据推送到服务器端
|
||
handle_event(info, flush_cache, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
|
||
case efka_iot_outbox:next(State#state.outbox) of
|
||
{ok, Seq, Packet} ->
|
||
ok = ssl:send(Socket, Packet),
|
||
case efka_iot_outbox:ack(Seq, State#state.outbox) of
|
||
{ok, Outbox} ->
|
||
{keep_state, State#state{outbox = Outbox}, [{next_event, info, flush_cache}]};
|
||
{error, Reason} ->
|
||
logger:warning("[efka_iot_client] ack outbox failed, seq: ~p, reason: ~p", [Seq, Reason]),
|
||
{keep_state, State}
|
||
end;
|
||
eof ->
|
||
{keep_state, State};
|
||
{error, Reason} ->
|
||
logger:warning("[efka_iot_client] read outbox failed, reason: ~p", [Reason]),
|
||
{keep_state, State}
|
||
end;
|
||
handle_event(info, flush_cache, _, State) ->
|
||
{keep_state, State};
|
||
|
||
%% 处理收到的ssl消息
|
||
handle_event(info, {ssl, Socket, PacketBin}, _, State = #state{socket = Socket}) when is_binary(PacketBin) ->
|
||
try binary_to_term(PacketBin, [safe]) of
|
||
Packet ->
|
||
{keep_state, State, [{next_event, internal, Packet}]}
|
||
catch
|
||
error:Error ->
|
||
logger:warning("[efka_iot_client] binary_to_term get error: ~p, packet_size: ~p", [Error, byte_size(PacketBin)]),
|
||
disconnect(Socket),
|
||
cancel_ssl_ping(State),
|
||
schedule_reconnect(),
|
||
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, ping_timer_ref = undefined}}
|
||
end;
|
||
handle_event(info, {ssl_error, Socket, Reason}, _, State = #state{}) ->
|
||
logger:debug("[efka_iot_client] ssl error: ~p", [Reason]),
|
||
disconnect(Socket),
|
||
cancel_ssl_ping(State),
|
||
schedule_reconnect(),
|
||
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, ping_timer_ref = undefined}};
|
||
handle_event(info, {ssl_closed, Socket}, _, State = #state{}) ->
|
||
logger:debug("[efka_iot_client] ssl closed"),
|
||
disconnect(Socket),
|
||
cancel_ssl_ping(State),
|
||
schedule_reconnect(),
|
||
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined, ping_timer_ref = undefined}};
|
||
|
||
%%% 处理内部消息,ssl收到的消息会先 binary_to_term,再由这里按协议结构模式匹配
|
||
|
||
%% 容器管理命令由 iot 发起,使用 command/command_response 语义。
|
||
handle_event(internal, {<<"command">>, Ref, {<<"container">>, Request}}, ?STATE_ACTIVATED, State = #state{socket = Socket}) ->
|
||
handle_container_command(Ref, Request, Socket),
|
||
{keep_state, State};
|
||
handle_event(internal, {<<"command">>, Ref, {<<"container">>, Request}}, _StateName, State = #state{socket = Socket}) ->
|
||
logger:notice("[efka_iot_client] get an invalid command: ~p, agent invalid", [Request]),
|
||
send_container_response(Socket, Ref, {error, <<"agent invalid">>}),
|
||
{keep_state, State};
|
||
|
||
%% 处理response
|
||
handle_event(internal, {<<"response">>, AuthRef, {<<"auth_response">>, <<"ok">>}}, ?STATE_AUTH, State = #state{auth_ref = AuthRef}) ->
|
||
logger:debug("[efka_iot_client] auth success"),
|
||
State1 = schedule_ssl_ping(State#state{auth_ref = undefined}),
|
||
{next_state, ?STATE_ACTIVATED, State1, [{next_event, info, flush_cache}]};
|
||
handle_event(internal, {<<"response">>, AuthRef, {<<"auth_response">>, {<<"error">>, Reason}}}, ?STATE_AUTH, State = #state{socket = Socket, auth_ref = AuthRef}) ->
|
||
logger:debug("[efka_iot_client] auth failed, reason: ~p", [Reason]),
|
||
disconnect(Socket),
|
||
schedule_reconnect(),
|
||
{next_state, ?STATE_DISCONNECTED, State#state{socket = undefined, auth_ref = undefined}};
|
||
handle_event(internal, {<<"response">>, _Ref, Reply}, StateName, State) ->
|
||
logger:warning("[efka_iot_client] ignore unexpected response in state ~p: ~p", [StateName, Reply]),
|
||
{keep_state, State};
|
||
handle_event(internal, {<<"command_response">>, _Ref, Reply}, StateName, State) ->
|
||
logger:warning("[efka_iot_client] ignore unexpected command_response in state ~p: ~p", [StateName, Reply]),
|
||
{keep_state, State};
|
||
|
||
%% 处理Pub/Sub机制
|
||
handle_event(internal, {<<"message">>, <<"pong">>}, ?STATE_ACTIVATED, State) ->
|
||
{keep_state, State};
|
||
handle_event(internal, {<<"message">>, {<<"pub">>, #{<<"topic">> := Topic, <<"qos">> := Qos, <<"content">> := Content}}}, ?STATE_ACTIVATED, State) ->
|
||
logger:debug("[efka_iot_client] get pub topic: ~p, qos: ~p, content: ~p", [Topic, Qos, Content]),
|
||
efka_subscription:publish(Topic, Qos, Content),
|
||
{keep_state, State};
|
||
handle_event(internal, Packet, _StateName, State) ->
|
||
logger:warning("[efka_iot_client] ignore unknown packet: ~p", [Packet]),
|
||
{keep_state, State};
|
||
|
||
handle_event(info, Info, _, State = #state{}) ->
|
||
logger:notice("[efka_iot_client] get unknown info: ~p", [Info]),
|
||
{keep_state, State}.
|
||
|
||
-spec handle_container_command(binary(), term(), ssl:sslsocket()) -> ok.
|
||
handle_container_command(Ref, #{<<"action">> := <<"list">>}, Socket) ->
|
||
Reply = docker_commands:get_containers(),
|
||
send_container_response(Socket, Ref, Reply),
|
||
ok;
|
||
handle_container_command(Ref, #{<<"action">> := <<"deploy">>, <<"task_id">> := TaskId, <<"params">> := Params}, Socket) ->
|
||
Reply = docker_deploy_manager:deploy(TaskId, Params),
|
||
send_container_response(Socket, Ref, Reply),
|
||
ok;
|
||
handle_container_command(Ref, #{<<"action">> := <<"start">>, <<"target">> := Target}, Socket) ->
|
||
Reply = docker_commands:start_container(container_target(Target)),
|
||
send_container_response(Socket, Ref, Reply),
|
||
ok;
|
||
handle_container_command(Ref, #{<<"action">> := <<"stop">>, <<"target">> := Target, <<"timeout_seconds">> := TimeoutSeconds}, Socket) ->
|
||
Reply = docker_commands:stop_container(container_target(Target), TimeoutSeconds),
|
||
send_container_response(Socket, Ref, Reply),
|
||
ok;
|
||
handle_container_command(Ref, #{<<"action">> := <<"kill">>, <<"target">> := Target, <<"signal">> := Signal}, Socket) ->
|
||
Reply = docker_commands:kill_container(container_target(Target), to_binary(Signal)),
|
||
send_container_response(Socket, Ref, Reply),
|
||
ok;
|
||
handle_container_command(Ref, #{<<"action">> := <<"remove">>, <<"target">> := Target, <<"force">> := Force, <<"remove_volumes">> := RemoveVolumes}, Socket) ->
|
||
Reply = docker_commands:remove_container(container_target(Target), to_bool(Force), to_bool(RemoveVolumes)),
|
||
send_container_response(Socket, Ref, Reply),
|
||
ok;
|
||
handle_container_command(Ref, #{<<"action">> := <<"config">>, <<"target">> := Target, <<"config">> := Config}, Socket) ->
|
||
Reply = docker_helper:update_container_config(container_target(Target), iolist_to_binary(Config)),
|
||
send_container_response(Socket, Ref, Reply),
|
||
ok;
|
||
handle_container_command(Ref, Request, Socket) ->
|
||
logger:notice("[efka_iot_client] get an invalid command: ~p, agent invalid", [Request]),
|
||
send_container_response(Socket, Ref, {error, <<"agent invalid">>}),
|
||
ok.
|
||
|
||
-spec terminate(term(), atom(), #state{}) -> ok.
|
||
terminate(Reason, _StateName, State = #state{socket = Socket, outbox = Outbox}) ->
|
||
cancel_ssl_ping(State),
|
||
disconnect(Socket),
|
||
efka_iot_outbox:close(Outbox),
|
||
logger:notice("[efka_iot_client] terminate with reason: ~p", [Reason]),
|
||
ok.
|
||
|
||
-spec code_change(term(), atom(), #state{}, term()) -> {ok, atom(), #state{}}.
|
||
code_change(_OldVsn, StateName, State = #state{}, _Extra) ->
|
||
{ok, StateName, State}.
|
||
|
||
%%%===================================================================
|
||
%%% Internal functions
|
||
%%%===================================================================
|
||
|
||
-spec auth_packet(binary()) -> binary().
|
||
auth_packet(Ref) when is_binary(Ref) ->
|
||
{ok, AuthInfo} = application:get_env(efka, auth),
|
||
UUID = proplists:get_value(uuid, AuthInfo),
|
||
Token = proplists:get_value(token, AuthInfo),
|
||
|
||
Timestamp = efka_util:timestamp(),
|
||
term_to_binary({<<"request">>, Ref, {<<"auth_request">>, #{
|
||
<<"uuid">> => list_to_binary(UUID),
|
||
<<"token">> => list_to_binary(Token),
|
||
<<"timestamp">> => Timestamp
|
||
}}}).
|
||
|
||
-spec connect_socket() -> {ok, ssl:sslsocket()} | {error, term()}.
|
||
connect_socket() ->
|
||
{ok, Props} = application:get_env(efka, iot_server),
|
||
Host = proplists:get_value(host, Props),
|
||
Port = proplists:get_value(tls_port, Props),
|
||
SslOptions = [
|
||
binary,
|
||
{active, true},
|
||
{packet, 4},
|
||
{verify, verify_none}
|
||
],
|
||
ssl:connect(Host, Port, SslOptions, 5000).
|
||
|
||
-spec disconnect(undefined | ssl:sslsocket()) -> ok.
|
||
disconnect(undefined) ->
|
||
ok;
|
||
disconnect(Socket) ->
|
||
catch ssl:close(Socket),
|
||
ok.
|
||
|
||
-spec schedule_reconnect() -> reference().
|
||
schedule_reconnect() ->
|
||
erlang:start_timer(5000, self(), create_transport).
|
||
|
||
-spec schedule_ssl_ping(#state{}) -> #state{}.
|
||
schedule_ssl_ping(State = #state{ping_timer_ref = TimerRef}) ->
|
||
cancel_timer(TimerRef),
|
||
State#state{ping_timer_ref = erlang:start_timer(?SSL_PING_INTERVAL, self(), ssl_ping)}.
|
||
|
||
-spec cancel_ssl_ping(#state{}) -> ok.
|
||
cancel_ssl_ping(#state{ping_timer_ref = TimerRef}) ->
|
||
cancel_timer(TimerRef).
|
||
|
||
-spec cancel_timer(undefined | reference()) -> ok.
|
||
cancel_timer(undefined) ->
|
||
ok;
|
||
cancel_timer(TimerRef) ->
|
||
_ = erlang:cancel_timer(TimerRef),
|
||
ok.
|
||
|
||
-spec request_ref() -> binary().
|
||
request_ref() ->
|
||
crypto:strong_rand_bytes(16).
|
||
|
||
-spec send_container_response(ssl:sslsocket(), binary(), term()) -> ok.
|
||
send_container_response(Socket, Ref, Reply) ->
|
||
Packet = term_to_binary({<<"command_response">>, Ref, {<<"container">>, safe_reply(Reply)}}),
|
||
ok = ssl:send(Socket, Packet).
|
||
|
||
-spec safe_reply(term()) -> term().
|
||
safe_reply(ok) ->
|
||
<<"ok">>;
|
||
safe_reply({ok, Result}) ->
|
||
{<<"ok">>, safe_term(Result)};
|
||
safe_reply({error, Reason}) ->
|
||
{<<"error">>, safe_term(Reason)}.
|
||
|
||
-spec safe_term(term()) -> term().
|
||
safe_term(true) ->
|
||
true;
|
||
safe_term(false) ->
|
||
false;
|
||
safe_term(undefined) ->
|
||
undefined;
|
||
safe_term(Value) when is_atom(Value) ->
|
||
atom_to_binary(Value, utf8);
|
||
safe_term(Value) when is_map(Value) ->
|
||
maps:from_list([{safe_term(K), safe_term(V)} || {K, V} <- maps:to_list(Value)]);
|
||
safe_term(Value) when is_list(Value) ->
|
||
[safe_term(Item) || Item <- Value];
|
||
safe_term(Value) when is_tuple(Value) ->
|
||
list_to_tuple([safe_term(Item) || Item <- tuple_to_list(Value)]);
|
||
safe_term(Value) ->
|
||
Value.
|
||
|
||
-spec container_target(map()) -> binary().
|
||
container_target(Target) when is_map(Target) ->
|
||
NameBin = to_binary(maps:get(<<"name">>, Target, <<>>)),
|
||
IdBin = to_binary(maps:get(<<"id">>, Target, <<>>)),
|
||
case NameBin of
|
||
<<>> ->
|
||
true = IdBin =/= <<>>,
|
||
IdBin;
|
||
_ ->
|
||
NameBin
|
||
end.
|
||
|
||
-spec to_binary(binary() | list()) -> binary().
|
||
to_binary(Value) when is_binary(Value) ->
|
||
Value;
|
||
to_binary(Value) when is_list(Value) ->
|
||
unicode:characters_to_binary(Value).
|
||
|
||
-spec to_bool(true | false | 0 | 1) -> boolean().
|
||
to_bool(true) ->
|
||
true;
|
||
to_bool(1) ->
|
||
true;
|
||
to_bool(false) ->
|
||
false;
|
||
to_bool(0) ->
|
||
false.
|