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}]). |