msp/src/msp_router.erl
Jarvis Carroll e8ef0b4304 buffer out-of-order messages
This took a while to debug!! Turns out I don't actually
handle datagrams that have been received before the
workers are all initialized... oops!!
2025-10-27 07:31:27 +00:00

139 lines
3.8 KiB
Erlang

%%% @doc
%%% Minimal Stream Protocol: Connection Worker
%%% @end
-module(msp_router).
-vsn("0.1.0").
-behavior(gen_server).
-author("Jarvis Carroll <spiveehere@gmail.com>").
-copyright("Jarvis Carroll <spiveehere@gmail.com>").
-license("MIT").
-export([begin_routing/4, add_channel/3]).
%% gen_server
-export([start_link/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2]).
-include("$zx_include/zx_logger.hrl").
%%% Type and Record Definitions
-type endpoint() :: {inet:ip_address(), inet:port_number()}.
% msp_channel server.
-type channel() :: pid().
-record(s,
{socket :: gen_udp:socket(),
peer :: endpoint(),
side :: 0 | 1,
connections = #{} :: #{integer() => channel()}}).
-type state() :: none | #s{}.
%%% Interface
% msp does not negotiate new connections, but once you have a
% socket, and a host that knows you will be talking MSP, then a
% new msp_router can be initialized.
begin_routing(Router, OurSock, {TheirIP, TheirPort}, OurSide) ->
% Transfer the socket to the gen_server. If it is
% active then a bunch of messages will be received
% at once, but that is fine.
case gen_udp:controlling_process(OurSock, Router) of
ok ->
gen_server:cast(Router, {begin_routing, OurSock, {TheirIP, TheirPort}, OurSide});
{error, Reason} ->
{error, Reason}
end.
add_channel(Router, ChanID, ChanPID) ->
gen_server:cast(Router, {add_channel, ChanID, ChanPID}).
%%% gen_server
-spec start_link() -> Result
when Result :: {ok, pid()}
| {error, Reason :: term()}.
start_link() ->
gen_server:start_link(?MODULE, none, []).
% TODO: SWP? SWP of SWPs?
init(none) ->
{ok, none}.
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call from ~tp: ~tp", [From, Unexpected]),
{reply, undef, State}.
handle_cast({begin_routing, Sock, Peer, Side}, none) ->
State = do_begin_routing(Sock, Peer, Side),
{noreply, State};
handle_cast({add_channel, ChanID, ChanPID}, State) ->
NewState = do_add_channel(ChanID, ChanPID, State),
{noreply, NewState};
handle_cast(Unexpected, State) ->
ok = log(warning, "Unexpected cast: ~tp", [Unexpected]),
{noreply, State}.
handle_info({udp, Sock, IP, Port, Packet}, State = #s{socket = Sock, peer = {IP, Port}}) ->
io:format("Got packet: ~p~n", [Packet]),
do_dispatch(State, Packet),
{noreply, State};
handle_info(Unexpected, State) ->
ok = io:format("Unexpected info: ~tp~n", [Unexpected]),
{noreply, State}.
-spec code_change(OldVersion, State, Extra) -> {ok, NewState} | {error, Reason}
when OldVersion :: Version | {old, Version},
Version :: term(),
State :: state(),
Extra :: term(),
NewState :: term(),
Reason :: term().
code_change(_, State, _) ->
{ok, State}.
terminate(_, _) ->
ok.
%%% Doer Functions
do_begin_routing(Sock, Peer, Side) ->
ok = inet:setopts(Sock, [{active, once}, {mode, binary}]),
State = #s{socket = Sock,
peer = Peer,
side = Side},
State.
do_add_channel(ChanID, ChanPID, State = #s{connections = Conns}) ->
NewConns = maps:put(ChanID, ChanPID, Conns),
State#s{connections = NewConns}.
do_dispatch(#s{socket = Sock, peer = Peer, connections = Conns}, <<ID:8, Packet/bytes>>) ->
ok = inet:setopts(Sock, [{active, once}]),
case maps:find(ID, Conns) of
{ok, Conn} ->
erlang:send(Conn, {msp_fragment, Packet});
error ->
{ok, OurPort} = inet:port(Sock),
{TheirIP, TheirPort} = Peer,
msp_channel_man:dispatch_fallback(OurPort, TheirIP, TheirPort, ID, Packet)
end;
do_dispatch(_, <<>>) ->
% Empty datagram?
ok.