| 1 |
|
%%% Textgroup server. |
| 2 |
|
%%% |
| 3 |
|
%%% Copyright (c) 2022 Holger Weiss <holger@zedat.fu-berlin.de>. |
| 4 |
|
%%% All rights reserved. |
| 5 |
|
%%% |
| 6 |
|
%%% Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 |
|
%%% you may not use this file except in compliance with the License. |
| 8 |
|
%%% You may obtain a copy of the License at |
| 9 |
|
%%% |
| 10 |
|
%%% http://www.apache.org/licenses/LICENSE-2.0 |
| 11 |
|
%%% |
| 12 |
|
%%% Unless required by applicable law or agreed to in writing, software |
| 13 |
|
%%% distributed under the License is distributed on an "AS IS" BASIS, |
| 14 |
|
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 |
|
%%% See the License for the specific language governing permissions and |
| 16 |
|
%%% limitations under the License. |
| 17 |
|
|
| 18 |
|
-module(textgroup_client). |
| 19 |
|
-behaviour(gen_server). |
| 20 |
|
-export([start/1, |
| 21 |
|
send/2, |
| 22 |
|
get_address/1]). |
| 23 |
|
-export([start_link/1]). |
| 24 |
|
-export([init/1, |
| 25 |
|
handle_call/3, |
| 26 |
|
handle_cast/2, |
| 27 |
|
handle_info/2, |
| 28 |
|
terminate/2, |
| 29 |
|
code_change/3]). |
| 30 |
|
-export_type([state/0]). |
| 31 |
|
|
| 32 |
|
-include_lib("kernel/include/logger.hrl"). |
| 33 |
|
-define(EOL, "\r\n"). |
| 34 |
|
-define(WELCOME_MSG, "Welcome to Textgroup! Type 'help' for help."). |
| 35 |
|
-define(GOODBYE_MSG, "Thanks for using Textgroup. See you!"). |
| 36 |
|
-define(HELP_MSG, |
| 37 |
|
"peers Show the IP addresses of your current peers" ?EOL |
| 38 |
|
"stats Show some statistic regarding this session" ?EOL |
| 39 |
|
"help Show this help message" ?EOL |
| 40 |
|
"quit Quit this session" ?EOL). |
| 41 |
|
|
| 42 |
|
-record(client_state, |
| 43 |
|
{socket :: gen_tcp:socket() | undefined, |
| 44 |
|
client :: binary() | undefined, |
| 45 |
|
n_sent = 0 :: non_neg_integer(), |
| 46 |
|
n_rcvd = 0 :: non_neg_integer()}). |
| 47 |
|
|
| 48 |
|
-opaque state() :: #client_state{}. |
| 49 |
|
|
| 50 |
|
%% API. |
| 51 |
|
|
| 52 |
|
-spec start(gen_tcp:socket()) -> ok. |
| 53 |
|
start(Socket) -> |
| 54 |
6 |
{ok, Proc} = supervisor:start_child(textgroup_client_sup, [Socket]), |
| 55 |
6 |
ok = gen_tcp:controlling_process(Socket, Proc), |
| 56 |
6 |
ok = set_queue_size(Socket). |
| 57 |
|
|
| 58 |
|
-spec send(pid(), iodata()) -> ok. |
| 59 |
|
send(PID, Data) -> |
| 60 |
6 |
gen_server:cast(PID, {send, Data}). |
| 61 |
|
|
| 62 |
|
-spec get_address(pid()) -> binary(). |
| 63 |
|
get_address(PID) -> |
| 64 |
1 |
gen_server:call(PID, get_address). |
| 65 |
|
|
| 66 |
|
%% API: supervisor callback. |
| 67 |
|
|
| 68 |
|
-spec start_link(gen_tcp:socket()) -> {ok, pid()} | ignore | {error, term()}. |
| 69 |
|
start_link(Socket) -> |
| 70 |
6 |
?LOG_DEBUG("Creating client handler process"), |
| 71 |
6 |
gen_server:start_link(?MODULE, [Socket], []). |
| 72 |
|
|
| 73 |
|
%% API: gen_server callbacks. |
| 74 |
|
|
| 75 |
|
-spec init([gen_tcp:socket()]) -> {ok, state()}. |
| 76 |
|
init([Socket]) -> |
| 77 |
6 |
process_flag(trap_exit, true), |
| 78 |
6 |
{ok, {Addr, _Port}} = inet:peername(Socket), |
| 79 |
6 |
Client = list_to_binary(inet:ntoa(Addr)), |
| 80 |
6 |
Greeting = <<?WELCOME_MSG ?EOL |
| 81 |
|
"Your IP address: ", Client/binary, ?EOL |
| 82 |
|
"Peers may query your IP address." ?EOL>>, |
| 83 |
6 |
ok = gen_tcp:send(Socket, Greeting), |
| 84 |
6 |
?LOG_NOTICE("Opening session of ~s", [Client]), |
| 85 |
6 |
{ok, #client_state{socket = Socket, client = Client}}. |
| 86 |
|
|
| 87 |
|
-spec handle_call(term(), {pid(), term()}, state()) |
| 88 |
|
-> {reply, {error, term()}, state()}. |
| 89 |
|
handle_call(get_address, From, #client_state{client = Client} = State) -> |
| 90 |
1 |
?LOG_DEBUG("Returning client address to ~p: ~s", [From, Client]), |
| 91 |
1 |
{reply, Client, State}; |
| 92 |
|
handle_call(Request, From, State) -> |
| 93 |
:-( |
?LOG_ERROR("Got unexpected request from ~p: ~p", [From, Request]), |
| 94 |
:-( |
{reply, {error, badarg}, State}. |
| 95 |
|
|
| 96 |
|
-spec handle_cast(term(), state()) -> {noreply, state()}. |
| 97 |
|
handle_cast({send, Data}, #client_state{socket = Socket, |
| 98 |
|
client = Client, |
| 99 |
|
n_rcvd = Rcvd} = State) -> |
| 100 |
6 |
?LOG_DEBUG("Received message for ~s: ~s", [Client, Data]), |
| 101 |
6 |
ok = gen_tcp:send(Socket, Data), |
| 102 |
6 |
{noreply, State#client_state{n_rcvd = Rcvd + 1}}; |
| 103 |
|
handle_cast(Msg, State) -> |
| 104 |
:-( |
?LOG_ERROR("Got unexpected message: ~p", [Msg]), |
| 105 |
:-( |
{noreply, State}. |
| 106 |
|
|
| 107 |
|
-spec handle_info(term(), state()) -> {noreply, state()}. |
| 108 |
|
handle_info({tcp, _Socket, <<"quit", EOL/binary>>}, |
| 109 |
|
#client_state{client = Client} = State) |
| 110 |
|
when EOL =:= <<$\n>>; |
| 111 |
|
EOL =:= <<$\r, $\n>> -> |
| 112 |
6 |
?LOG_DEBUG("Got quit query from ~s", [Client]), |
| 113 |
6 |
{stop, normal, State}; |
| 114 |
|
handle_info({tcp, Socket, <<"help", EOL/binary>>}, |
| 115 |
|
#client_state{client = Client} = State) |
| 116 |
|
when EOL =:= <<$\n>>; |
| 117 |
|
EOL =:= <<$\r, $\n>> -> |
| 118 |
1 |
?LOG_DEBUG("Got help query from ~s", [Client]), |
| 119 |
1 |
Response = <<?HELP_MSG>>, |
| 120 |
1 |
ok = gen_tcp:send(Socket, Response), |
| 121 |
1 |
{noreply, State}; |
| 122 |
|
handle_info({tcp, Socket, <<"stats", EOL/binary>>}, |
| 123 |
|
#client_state{client = Client, |
| 124 |
|
n_sent = Sent, |
| 125 |
|
n_rcvd = Rcvd} = State) |
| 126 |
|
when EOL =:= <<$\n>>; |
| 127 |
|
EOL =:= <<$\r, $\n>> -> |
| 128 |
1 |
?LOG_DEBUG("Got stats query from ~s", [Client]), |
| 129 |
1 |
Response = io_lib:format("Messages sent: ~B~s" |
| 130 |
|
"Messages rcvd: ~B~s", |
| 131 |
|
[Sent, EOL, Rcvd, EOL]), |
| 132 |
1 |
ok = gen_tcp:send(Socket, Response), |
| 133 |
1 |
{noreply, State}; |
| 134 |
|
handle_info({tcp, Socket, <<"peers", EOL/binary>>}, |
| 135 |
|
#client_state{client = Client} = State) |
| 136 |
|
when EOL =:= <<$\n>>; |
| 137 |
|
EOL =:= <<$\r, $\n>> -> |
| 138 |
1 |
?LOG_DEBUG("Got peers query from ~s", [Client]), |
| 139 |
1 |
foreach_peer(fun(PID) -> |
| 140 |
1 |
try get_address(PID) of |
| 141 |
|
Addr -> |
| 142 |
1 |
Response = [Addr, EOL], |
| 143 |
1 |
ok = gen_tcp:send(Socket, Response) |
| 144 |
|
catch exit:Err -> |
| 145 |
:-( |
?LOG_DEBUG("Cannot query ~p: ~p", [PID, Err]), |
| 146 |
:-( |
ok |
| 147 |
|
end |
| 148 |
|
end), |
| 149 |
1 |
{noreply, State}; |
| 150 |
|
handle_info({tcp, _Socket, Data}, #client_state{client = Client, |
| 151 |
|
n_sent = Sent} = State) -> |
| 152 |
6 |
?LOG_DEBUG("Sending text message from ~s to peers", [Client]), |
| 153 |
6 |
foreach_peer(fun(PID) -> send(PID, Data) end), |
| 154 |
6 |
{noreply, State#client_state{n_sent = Sent + 1}}; |
| 155 |
|
handle_info({tcp_passive, Socket}, #client_state{client = Client} = State) -> |
| 156 |
3 |
?LOG_DEBUG("Resetting active queue size for ~s", [Client]), |
| 157 |
3 |
ok = set_queue_size(Socket), |
| 158 |
3 |
{noreply, State}; |
| 159 |
|
handle_info({tcp_closed, _Socket}, #client_state{client = Client} = State) -> |
| 160 |
:-( |
?LOG_DEBUG("~s closed the TCP connection", [Client]), |
| 161 |
:-( |
{stop, normal, State}; |
| 162 |
|
handle_info({tcp_error, _Socket, Reason}, |
| 163 |
|
#client_state{client = Client} = State) -> |
| 164 |
:-( |
?LOG_NOTICE("Got TCP error for ~s: ~p", [Client, Reason]), |
| 165 |
:-( |
{stop, Reason, State}; |
| 166 |
|
handle_info(Info, State) -> |
| 167 |
:-( |
?LOG_ERROR("Got unexpected info: ~p", [Info]), |
| 168 |
:-( |
{noreply, State}. |
| 169 |
|
|
| 170 |
|
-spec terminate(normal | shutdown | {shutdown, term()} | term(), state()) -> ok. |
| 171 |
|
terminate(Reason, #client_state{socket = Socket, client = Client}) -> |
| 172 |
6 |
?LOG_NOTICE("Closing session of ~s (~p)", [Client, Reason]), |
| 173 |
6 |
Goodbye = <<?GOODBYE_MSG ?EOL>>, |
| 174 |
6 |
_ = gen_tcp:send(Socket, Goodbye), |
| 175 |
6 |
_ = gen_tcp:close(Socket). |
| 176 |
|
|
| 177 |
|
-spec code_change({down, term()} | term(), state(), term()) -> {ok, state()}. |
| 178 |
|
code_change(_OldVsn, State, _Extra) -> |
| 179 |
:-( |
?LOG_INFO("Got code change request"), |
| 180 |
:-( |
{ok, State}. |
| 181 |
|
|
| 182 |
|
%% Internal functions. |
| 183 |
|
|
| 184 |
|
-spec foreach_peer(fun((pid()) -> ok)) -> ok. |
| 185 |
|
foreach_peer(Fun) -> |
| 186 |
7 |
lists:foreach(fun({_, PID, _, [textgroup_client]}) when PID =:= self() -> |
| 187 |
7 |
ok; |
| 188 |
|
({_, PID, _, [textgroup_client]}) -> |
| 189 |
7 |
ok = Fun(PID) |
| 190 |
|
end, supervisor:which_children(textgroup_client_sup)). |
| 191 |
|
|
| 192 |
|
-spec set_queue_size(gen_tcp:socket()) -> ok | {error, inet:posix()}. |
| 193 |
|
set_queue_size(Socket) -> |
| 194 |
9 |
{ok, N} = application:get_env(tcp_queue_size), |
| 195 |
9 |
inet:setopts(Socket, [{active, N}]). |