Packages

application to call remote functions

Current section

Files

Jump to
teleport src teleport_protocol.erl
Raw

src/teleport_protocol.erl

%% Copyright (c) 2016 Contributors as noted in the AUTHORS file
%%
%% This file is part teleport
%%
%% 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 http://mozilla.org/MPL/2.0/.
-module(teleport_protocol).
-author("Benoit Chesneau").
-behaviour(gen_statem).
-behaviour(ranch_protocol).
-export([
start_link/4
]).
-export([
terminate/3,
code_change/4,
init/1,
callback_mode/0
]).
-export([
wait_for_handshake/3,
wait_for_data/3
]).
start_link(Ref, Socket, Transport, Opts) ->
proc_lib:start_link(?MODULE, init, [{self(), Ref, Socket, Transport, Opts}]).
init({Parent, Ref, Socket, Transport, Opts}) ->
proc_lib:init_ack(Parent, {ok, self()}),
ok = ranch:accept_ack(Ref),
process_flag(trap_exit, true),
ok = Transport:setopts(Socket, [{active, once}, binary, {packet, 4}]),
{ok, Heartbeat} = timer:send_interval(5000, self(), heartbeat),
{ok, {PeerHost, PeerPort}} = Transport:peername(Socket),
ets:insert(teleport_incoming_conns, {self(), PeerHost, undefined}),
{OK, _Closed, _Error} = Transport:messages(),
#{ host := Host, transport := Transport, name := Name} = Opts,
Data =
#{
transport => Transport,
sock => Socket,
heartbeat => Heartbeat,
missed_heartbeats => 0,
peer_host => PeerHost,
peer_port => PeerPort,
ok => OK,
name => Name,
host => Host,
transport_uri => Transport,
channels => []
},
gen_statem:enter_loop(?MODULE, [], wait_for_handshake, activate_socket(Data)).
callback_mode() -> state_functions.
terminate(_Reason, _State, Data) ->
_ = cleanup(Data),
ok.
code_change(_OldVsn, State, Data, _Extra) ->
{ok, State, Data}.
wait_for_handshake(info, {OK, Sock, PayLoad}, Data = #{ sock := Sock, ok := OK}) ->
#{ peer_host := PeerHost} = Data,
Cookie = erlang:get_cookie(),
try erlang:binary_to_term(PayLoad) of
{connect, ClientCookie, PeerNode} when ClientCookie =:= Cookie ->
lager:info("teleport: server connected to peer node: ~p~n", [PeerNode]),
ets:insert(teleport_incoming_conns, {self(), PeerHost, PeerNode}),
ok = send_handshake(Data),
{next_state, wait_for_data, activate_socket(Data)};
{connect, _InvalidCookie} ->
lager:warning("teleport: invalid cookie from ~p", [PeerHost]),
{stop, normal, Data};
heartbeat ->
{keep_state, activate_socket(Data#{missed_heartbeats => 0})};
OtherMsg ->
lager:error(
"teleport: invalid message from ~p: ~w",
[PeerHost, OtherMsg]
),
{stop, normal, Data}
catch
error:badarg ->
lager:warning("teleport: bad handshake from ~p: ~w", [PeerHost, Data]),
{stop, normal, Data}
end;
wait_for_handshake(EventType, Event, Data) ->
handle_event(EventType, wait_for_handshake, Event, Data).
wait_for_data(info, {OK, Sock, PayLoad}, Data = #{ sock := Sock, ok := OK, channels := Channels}) ->
try erlang:binary_to_term(PayLoad) of
{call, Headers, Mod, Fun, Args} ->
_ = spawn(fun() -> worker(Headers, Mod, Fun, Args, Data) end),
{keep_state, activate_socket(Data)};
{cast, _Headers, Mod, Fun, Args} ->
_ = spawn(fun() -> worker(Mod, Fun, Args) end),
{keep_state, activate_socket(Data)};
{blocking_call, Headers, Mod, Fun, Args} ->
_ = worker(Headers, Mod, Fun, Args, Data),
{keep_state, activate_socket(Data)};
{abcast, ProcName, Msg} ->
catch ProcName ! Msg,
{keep_state, activate_socket(Data)};
{sbcast, Headers, ProcName, Msg} ->
_ = worker(Headers, ProcName, Msg, Data),
{keep_state, activate_socket(Data)};
{new_channel, ChannelId} ->
Pid = spawn_link(fun() -> channel(ChannelId, Data) end),
Data2 = Data#{ channels := [{ChannelId, Pid} | Channels] },
{keep_state, activate_socket(Data2)};
{channel_msg, ChannelId, Msg} ->
handle_channel_msg(ChannelId, Msg, Data),
{keep_state, activate_socket(Data)};
{close_channel, ChannelId} ->
Data2 = handle_channel_closed(ChannelId, Data),
{keep_state, activate_socket(Data2)};
heartbeat ->
{keep_state, activate_socket(Data#{missed_heartbeats => 0})};
OtherMsg ->
lager:error("teleport: invalid message: ~p~n", [OtherMsg]),
{stop, error, normal}
catch
error:badarg ->
lager:warning("teleport: bad data: ~w", [Data]),
cleanup(Data),
{stop, normal, Data}
end;
wait_for_data(EventType, Event, Data) ->
handle_event(EventType, wait_for_data, Event, Data).
handle_event(info, _State, heartbeat, Data) ->
#{transport := Transport,
sock := Sock,
missed_heartbeats := M,
peer_host := PeerHost} = Data,
Packet = term_to_binary(heartbeat),
ok = Transport:send(Sock, Packet),
M2 = M + 1,
if
M2 > 3 ->
lager:warning("Missed ~p heartbeats from ~p. Closing connection~n", [M2, PeerHost]),
_ = cleanup(Data),
{stop, normal, Data};
true ->
{keep_state, activate_socket(Data#{missed_heartbeats => M2})}
end;
handle_event(info, _State, {'EXIT', Pid, _Reason}, Data = #{ channels := Channels }) ->
case lists:keyfind(Pid, 2, Channels) of
{ChannelId, Pid} ->
Data2 = Data#{ channels => delete_channel(ChannelId, Channels) },
{keep_state, activate_socket(Data2)};
false ->
{keep_state, activate_socket(Data)}
end;
handle_event(EventType, State, Event, Data) ->
#{ transport := Transport, sock := Sock } = Data,
{_OK, Closed, Error} = Transport:messages(),
case Event of
{Closed, Sock} ->
handle_conn_closed(Closed, State, Data);
{Error, Sock, Reason} ->
handle_conn_closed({Error, Reason}, State, Data);
_ ->
lager:error(
"teleport: server [~p] received an unknown event: ~p ~p",
[State, Event, EventType]
),
{stop, normal, cleanup(Data)}
end.
handle_conn_closed(Error, State, Data = #{ peer_host := Host}) ->
lager:debug("teleport: connection from ~p closed (~p). Reason: ~p~n", [Host, State, Error]),
{stop, normal, cleanup(Data)}.
worker(Mod, Fun, Args) -> apply(Mod, Fun, Args).
worker(Headers, ProcName, Msg, Data) ->
Reply = case catch ProcName ! Msg of
{'EXIT', _} ->
{sbcast_failed, Headers};
Msg ->
{sbcast_success, Headers}
end,
Packet = term_to_binary(Reply),
ok = send(Data, Packet).
worker(Headers, Mod, Fun, Args, Data) ->
Result = (catch apply(Mod , Fun, Args)),
Packet = erlang:term_to_binary({call_result, Headers, Result}),
ok = send(Data, Packet).
channel(ChannelId, Data) ->
receive
{channel_msg, ChannelId, {To, '$channel_register'}} ->
To ! {'$channel_register', ChannelId, self()},
channel(ChannelId, Data);
{channel_msg, ChannelId, {To, '$channel_unregister'}} ->
To ! {'$channel_unregister', ChannelId, self()},
channel(ChannelId, Data);
{channel_msg, ChannelId, {To, {'$channel_req', Msg}}} ->
To ! {'$channel_req', ChannelId, self(), Msg},
channel(ChannelId, Data);
{channel_msg, ChannelId, {To, Msg}} ->
To ! Msg,
channel(ChannelId, Data);
{channel_closed, ChannelId} ->
exit(normal);
Msg ->
Packet = erlang:term_to_binary({channel_msg, ChannelId, Msg}),
ok = send(Data, Packet),
channel(ChannelId, Data)
end.
get_channel_by_id(ChannelId, #{ channels := Channels }) ->
lists:keyfind(ChannelId, 1, Channels).
delete_channel(ChannelId, Data = #{ channels := Channels }) ->
Channels2 = lists:keydelete(ChannelId, 1, Channels),
Data#{ channels => Channels2}.
handle_channel_msg(ChannelId, Msg, Data) ->
case get_channel_by_id(ChannelId, Data) of
{ChannelId, Pid} ->
Pid ! {channel_msg, ChannelId, Msg};
false ->
ok
end.
handle_channel_closed(ChannelId, Data) ->
case get_channel_by_id(ChannelId, Data) of
{ChannelId, Pid} ->
Pid ! {channel_closed, ChannelId},
delete_channel(ChannelId, Data);
false ->
ok
end.
activate_socket(Data = #{ transport := Transport, sock := Sock}) ->
ok = Transport:setopts(Sock, [{active, once}, {packet, 4}]),
Data.
send_handshake(#{name := Name, host := Host, transport := T, sock := S}) ->
{ok, {_, Port}} = T:sockname(S),
Node = binary_to_atom(
iolist_to_binary(
[teleport_lib:to_list(Name), "@", Host, ":", integer_to_list(Port)]
),
latin1
),
Packet = erlang:term_to_binary({connected, Node}),
T:send(S, Packet).
send(#{ transport := Transport, sock := Sock}, Packet) ->
Transport:send(Sock, Packet).
cleanup(State = #{ transport := Transport, sock := Sock}) ->
catch ets:delete(teleport_incoming_conns, self()),
catch Transport:close(Sock),
State.