/home/runner/work/textgroup/textgroup/_build/test/cover/aggregate/textgroup_client.html

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