blob: d1d439688cca6d5d786910d64a14b25604ecc5d5 [file] [log] [blame]
Harald Welte176e28c2010-12-19 22:56:58 +01001% ip.access IPA multiplex protocol
2
Harald Welted0f391e2019-08-11 20:00:17 +02003% (C) 2010,2012,2019 by Harald Welte <laforge@gnumonks.org>
Harald Welte176e28c2010-12-19 22:56:58 +01004% (C) 2010 by On-Waves
5%
6% All Rights Reserved
7%
8% This program is free software; you can redistribute it and/or modify
9% it under the terms of the GNU General Public License as published by
10% the Free Software Foundation; either version 2 of the License, or
11% (at your option) any later version.
12%
13% This program is distributed in the hope that it will be useful,
14% but WITHOUT ANY WARRANTY; without even the implied warranty of
15% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16% GNU General Public License for more details.
17%
18% You should have received a copy of the GNU General Public License along
19% with this program; if not, write to the Free Software Foundation, Inc.,
20% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22-module(ipa_proto).
23-author('Harald Welte <laforge@gnumonks.org>').
24-compile(export_all).
25
Matt Johnson44a4dd62020-09-08 01:35:23 -070026-include("ipa.hrl").
27
Harald Welte176e28c2010-12-19 22:56:58 +010028-define(TIMEOUT, 1000).
29-define(IPA_SOCKOPTS, [binary, {packet, 0}, {reuseaddr, true}, {active, false}]).
30
31-define(IPAC_MSGT_PING, 0).
32-define(IPAC_MSGT_PONG, 1).
33-define(IPAC_MSGT_ID_GET, 4).
34-define(IPAC_MSGT_ID_RESP, 5).
35-define(IPAC_MSGT_ID_ACK, 6).
36
Harald Welted0f391e2019-08-11 20:00:17 +020037-define(IPAC_PROTO_OSMO, 238).
38-define(IPAC_PROTO_CCM, 254).
39
Harald Welte176e28c2010-12-19 22:56:58 +010040-export([register_socket/1, register_stream/3, unregister_stream/2,
41 send/3, connect/3, connect/4, listen_accept_handle/2,
Harald Welte3b95a7c2019-08-11 20:31:41 +020042 start_listen/3, controlling_process/3, register_codec/3]).
43
44-type stream_id() :: integer() | {osmo, integer()}.
Harald Welte176e28c2010-12-19 22:56:58 +010045
46-record(ipa_socket, {socket, ipaPid, streamTbl, listenType}).
47
Harald Welte3b95a7c2019-08-11 20:31:41 +020048-record(ipa_codec, {streamId :: stream_id(),
49 encodeFn :: fun(),
50 decodeFn :: fun()
51 }).
Harald Welte176e28c2010-12-19 22:56:58 +010052
53% register a TCP socket with this IPA protocol implementation
54register_socket(Socket) ->
55 IpaPid = spawn(?MODULE, init_sock, [Socket, self()]),
56 % synchronously wait for init_sock to be done
57 receive
58 {ipa_init_sock_done, Socket} ->
59 % assign ownership of the socket to the new IPA handler process
60 gen_tcp:controlling_process(Socket, IpaPid),
61 {ok, IpaPid}
62 after
63 ?TIMEOUT ->
64 {error, timeout}
65 end.
66
67% call_sync() preceeded by a Socket -> Pid lookup
68call_sync_sock(Socket, Request) ->
69 % resolve PID responsible for this socket
70 case ets:lookup(ipa_sockets, Socket) of
71 [IpaSock] ->
72 call_sync(IpaSock#ipa_socket.ipaPid, Request);
73 _ ->
74 io:format("No Process for Socket ~p~n", [Socket]),
75 {error, no_sock_for_pid}
76 end.
77
78% a user process wants to register itself for a given Socket/StreamID tuple
79register_stream(Socket, StreamID, Pid) ->
80 call_sync_sock(Socket, {ipa_reg_stream, Socket, StreamID, Pid}).
81
82register_streams(_S, []) ->
83 ok;
84register_streams(S, [{StreamID, Pid}|SList]) ->
85 ipa_proto:register_stream(S, StreamID, Pid),
86 register_streams(S, SList).
87
88% unregister for a given stream
89unregister_stream(Socket, StreamID) ->
90 call_sync_sock(Socket, {ipa_unreg_stream, Socket, StreamID}).
91
92% change the controlling process for a given {Socket, StreamID}
93controlling_process(Socket, StreamID, NewPid) ->
94 call_sync_sock(Socket, {ipa_ctrl_proc, Socket, StreamID, NewPid}).
95
Matt Johnson44a4dd62020-09-08 01:35:23 -070096% Set the metadata required for the ipa CCM sub-protocol.
97set_ccm_options(Socket, CcmOptions) ->
98 call_sync_sock(Socket, {ipa_set_ccm_options, Socket, CcmOptions}).
99
Harald Welte176e28c2010-12-19 22:56:58 +0100100% unblock the socket from further processing
101unblock(Socket) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100102 call_sync_sock(Socket, {ipa_unblock, Socket}).
103
Harald Welte176e28c2010-12-19 22:56:58 +0100104% server-side handler for unregister_stream()
Matt Johnson46214562020-09-08 01:42:49 -0700105request({ipa_reg_stream, Socket, StreamID, Pid}, _) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100106 io:format("Registering handler ~p for socket ~p Stream ~p~n", [Pid, Socket, StreamID]),
107 [IpaSock] = ets:lookup(ipa_sockets, Socket),
108 ets:insert_new(IpaSock#ipa_socket.streamTbl, {{Socket, StreamID}, Pid});
109% server-side handler for unregister_stream()
Matt Johnson46214562020-09-08 01:42:49 -0700110request({ipa_unreg_stream, Socket, StreamID}, _) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100111 io:format("Unregistering handler for Socket ~p Stream ~p~n", [Socket, StreamID]),
112 [IpaSock] = ets:lookup(ipa_sockets, Socket),
113 ets:delete(IpaSock#ipa_socket.streamTbl, {Socket, StreamID});
114% server-side handler for controlling_process()
Matt Johnson46214562020-09-08 01:42:49 -0700115request({ipa_ctrl_proc, Socket, StreamID, NewPid}, _) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100116 io:format("Changing handler for socket ~p Stream ~p~n", [Socket, StreamID]),
117 [IpaSock] = ets:lookup(ipa_sockets, Socket),
118 ets:delete(IpaSock#ipa_socket.streamTbl, {Socket, StreamID}),
119 ets:insert_new(IpaSock#ipa_socket.streamTbl, {{Socket, StreamID}, NewPid});
Matt Johnson44a4dd62020-09-08 01:35:23 -0700120% server-side handler for set_ccm_options()
121% set ccm protocol metadata options reported with connection setup.
Matt Johnson46214562020-09-08 01:42:49 -0700122request({ipa_set_ccm_options, Socket, CcmOptions}, _) ->
Matt Johnson44a4dd62020-09-08 01:35:23 -0700123 io:format("Setting ccm options for socket ~p to ~p~n", [Socket, CcmOptions]),
124 {ccm_options, CcmOptions};
Harald Welte176e28c2010-12-19 22:56:58 +0100125% server-side handler for unblock()
Matt Johnson46214562020-09-08 01:42:49 -0700126request({ipa_unblock, Socket}, CcmOptions) ->
127 if
128 CcmOptions#ipa_ccm_options.initiate_ack -> send_ccm_id_ack(Socket);
129 true -> send_ccm_id_get(Socket)
130 end,
Harald Welte176e28c2010-12-19 22:56:58 +0100131 io:format("Unblocking socket ~p~n", [Socket]),
132 %[IpaSock] = ets:lookup(ipa_sockets, Socket),
133 Ret = inet:setopts(Socket, [{active, once}]),
134 io:format("Unblocking socket ~p:~p~n", [Socket, Ret]).
135
136% split an incoming IPA message and split it into Length/StreamID/Payload
137split_ipa_msg(DataBin) ->
138 % FIXME: This will throw an exception if DataBin doesn't contain all payload
139 <<Length:16/big-unsigned-integer, StreamID:8, Payload:Length/binary, Trailer/binary>> = DataBin,
140 io:format("Stream ~p, ~p bytes~n", [StreamID, Length]),
141 {StreamID, Payload, Trailer}.
142
143% deliver an incoming message to the process that is registered for the socket/stream_id
144deliver_rx_ipa_msg(Socket, StreamID, StreamMap, DataBin) ->
Harald Welte3b95a7c2019-08-11 20:31:41 +0200145 DataDec = try_decode(StreamID, DataBin),
Harald Welte176e28c2010-12-19 22:56:58 +0100146 case ets:lookup(StreamMap, {Socket, StreamID}) of
147 [{_,{process_id, Pid}}] ->
Harald Welte3b95a7c2019-08-11 20:31:41 +0200148 Pid ! {ipa, Socket, StreamID, DataDec};
Harald Welte176e28c2010-12-19 22:56:58 +0100149 [{_,{callback_fn, Fn, Args}}] ->
Harald Welte3b95a7c2019-08-11 20:31:41 +0200150 Fn(Socket, StreamID, DataDec, Args);
Harald Welte176e28c2010-12-19 22:56:58 +0100151 [] ->
152 io:format("No Pid registered for Socket ~p Stream ~p~n", [Socket, StreamID])
153 end.
154
Harald Welte3b95a7c2019-08-11 20:31:41 +0200155% register a Codec with this IPA protocol implementation
156-spec register_codec(stream_id(), fun(), fun()) -> boolean().
157register_codec(StreamID, EncodeFn, DecodeFn) ->
158 ets:insert(ipa_codecs, #ipa_codec{streamId=StreamID, encodeFn=EncodeFn, decodeFn=DecodeFn}).
159
160-spec try_decode(stream_id(), binary()) -> any().
161try_decode(StreamID, Data) ->
162 case ets:lookup(ipa_codecs, StreamID) of
163 [IpaCodec] ->
164 Fun = IpaCodec#ipa_codec.decodeFn,
165 Fun(Data);
166 [] ->
167 Data
168 end.
169
170-spec try_encode(stream_id(), any()) -> binary().
171try_encode(_StreamID, Data) when is_binary(Data) ->
172 Data;
173try_encode(StreamID, Data) ->
174 case ets:lookup(ipa_codecs, StreamID) of
175 [IpaCodec] ->
176 Fun = IpaCodec#ipa_codec.encodeFn,
177 Fun(Data);
178 [] ->
179 Data
180 end.
181
Harald Welte176e28c2010-12-19 22:56:58 +0100182% process (split + deliver) an incoming IPA message
Matt Johnson44a4dd62020-09-08 01:35:23 -0700183process_rx_ipa_msg(_S, _StreamMap, _, <<>>) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100184 ok;
Matt Johnson44a4dd62020-09-08 01:35:23 -0700185process_rx_ipa_msg(S, StreamMap, CcmOptions, Data) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100186 {StreamID, PayloadBin, Trailer} = split_ipa_msg(Data),
187 case StreamID of
Harald Welted0f391e2019-08-11 20:00:17 +0200188 ?IPAC_PROTO_CCM ->
Matt Johnson44a4dd62020-09-08 01:35:23 -0700189 process_rx_ccm_msg(S, StreamID, CcmOptions, PayloadBin);
Harald Welted0f391e2019-08-11 20:00:17 +0200190 ?IPAC_PROTO_OSMO ->
191 <<ExtStreamID:8, PayloadExt/binary>> = PayloadBin,
192 deliver_rx_ipa_msg(S, {osmo, ExtStreamID}, StreamMap, PayloadExt);
Harald Welte176e28c2010-12-19 22:56:58 +0100193 _ ->
194 deliver_rx_ipa_msg(S, StreamID, StreamMap, PayloadBin)
195 end,
Matt Johnson44a4dd62020-09-08 01:35:23 -0700196 process_rx_ipa_msg(S, StreamMap, CcmOptions, Trailer).
Harald Welte176e28c2010-12-19 22:56:58 +0100197
198send_close_signal([]) ->
199 ok;
200send_close_signal([StreamSpec|Tail]) ->
Harald Weltef66bbfa2012-01-28 14:08:52 +0100201 io:format("send_close_signal ~p ~p~n", [StreamSpec, Tail]),
202 case StreamSpec of
203 [{{Socket, StreamID}, {process_id, Pid}}] ->
204 Pid ! {ipa_closed, {Socket, StreamID}};
205 [{{Socket, StreamID}, {callback_fn, Fn, Args}}] ->
206 Fn(Socket, StreamID, ipa_closed, Args)
207 end,
Harald Welte176e28c2010-12-19 22:56:58 +0100208 send_close_signal(Tail).
Harald Weltef66bbfa2012-01-28 14:08:52 +0100209
Harald Welte176e28c2010-12-19 22:56:58 +0100210process_tcp_closed(S, StreamMap) ->
211 % signal the closed socket to the user
212 StreamList = ets:match(StreamMap, '$1'),
213 send_close_signal(StreamList),
214 % remove the stream map for this socket
215 ets:delete(StreamMap),
216 % remove any entry regarding 'S' from ipa_sockets
217 ets:delete(ipa_sockets, S),
218 ok.
219
220% send a binary message through a given Socket / StreamID
Harald Welted0f391e2019-08-11 20:00:17 +0200221send(Socket, {osmo, StreamIdExt}, DataBin) ->
Harald Welte3b95a7c2019-08-11 20:31:41 +0200222 DataEnc = try_encode({osmo, StreamIdExt}, DataBin),
223 send(Socket, ?IPAC_PROTO_OSMO, [StreamIdExt, DataEnc]);
Harald Welte176e28c2010-12-19 22:56:58 +0100224send(Socket, StreamID, DataBin) ->
Harald Welte3b95a7c2019-08-11 20:31:41 +0200225 DataEnc = try_encode(StreamID, DataBin),
226 Size = iolist_size(DataEnc),
227 gen_tcp:send(Socket, iolist_to_binary([<<Size:2/big-unsigned-integer-unit:8>>, StreamID, DataEnc])).
Harald Welte176e28c2010-12-19 22:56:58 +0100228
229
230call_sync(Pid, Request) ->
231 Ref = make_ref(),
232 Pid ! {request, {self(), Ref}, Request},
233 receive
234 {reply, Ref, Reply} -> Reply
235 after
236 ?TIMEOUT -> {error, timeout}
237 end.
238
239reply({From, Ref}, Reply) ->
240 From ! {reply, Ref, Reply}.
241
242
243% global module initialization
244init() ->
Harald Welte3b95a7c2019-08-11 20:31:41 +0200245 ipa_sockets = ets:new(ipa_sockets, [named_table, set, public, {keypos, #ipa_socket.socket}]),
246 ipa_codecs = ets:new(ipa_codecs, [named_table, set, public, {keypos, #ipa_codec.streamId}]).
Harald Welte176e28c2010-12-19 22:56:58 +0100247
248% initialize a signle socket, create its handle process
249init_sock(Socket, CallingPid) ->
250 StreamMap = ets:new(stream_map, [set]),
251 ets:insert(ipa_sockets, #ipa_socket{socket=Socket, ipaPid=self(), streamTbl=StreamMap}),
252 CallingPid ! {ipa_init_sock_done, Socket},
Matt Johnson44a4dd62020-09-08 01:35:23 -0700253 loop(Socket, StreamMap, #ipa_ccm_options{}).
Harald Welte176e28c2010-12-19 22:56:58 +0100254
Matt Johnson44a4dd62020-09-08 01:35:23 -0700255loop(S, StreamMap, CcmOptions) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100256 receive
257 {request, From, Request} ->
Matt Johnson46214562020-09-08 01:42:49 -0700258 case ipa_proto:request(Request, CcmOptions) of
Matt Johnson44a4dd62020-09-08 01:35:23 -0700259 {ccm_options, NewCcmOptions} ->
260 NextCcmOptions = NewCcmOptions,
261 Reply = ok;
262 EmbeddedReply ->
263 NextCcmOptions = CcmOptions,
264 Reply = EmbeddedReply
265 end,
Harald Welte176e28c2010-12-19 22:56:58 +0100266 ipa_proto:reply(From, Reply),
Matt Johnson44a4dd62020-09-08 01:35:23 -0700267 ipa_proto:loop(S, StreamMap, NextCcmOptions);
Harald Welte176e28c2010-12-19 22:56:58 +0100268 {ipa_send, S, StreamId, Data} ->
269 send(S, StreamId, Data),
Matt Johnson44a4dd62020-09-08 01:35:23 -0700270 ipa_proto:loop(S, StreamMap, CcmOptions);
Harald Welte176e28c2010-12-19 22:56:58 +0100271 {tcp, S, Data} ->
272 % process incoming IPA message and mark socket active once more
Matt Johnson44a4dd62020-09-08 01:35:23 -0700273 ipa_proto:process_rx_ipa_msg(S, StreamMap, CcmOptions, Data),
Harald Welte176e28c2010-12-19 22:56:58 +0100274 inet:setopts(S, [{active, once}]),
Matt Johnson44a4dd62020-09-08 01:35:23 -0700275 ipa_proto:loop(S, StreamMap, CcmOptions);
Harald Welte176e28c2010-12-19 22:56:58 +0100276 {tcp_closed, S} ->
277 io:format("Socket ~w closed [~w]~n", [S,self()]),
278 ipa_proto:process_tcp_closed(S, StreamMap),
279 % terminate the process by not looping further
280 ok
281 end.
282
283% Respond with PONG to PING
Matt Johnson44a4dd62020-09-08 01:35:23 -0700284process_ccm_msg(Socket, StreamID, _, ping, _) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100285 io:format("Socket ~p Stream ~p: PING -> PONG~n", [Socket, StreamID]),
286 send(Socket, StreamID, <<?IPAC_MSGT_PONG>>);
Matt Johnson46214562020-09-08 01:42:49 -0700287% Respond to ID_ACK with ID_ACK if this instance did not initiate
288process_ccm_msg(Socket, StreamID, CcmOptions, id_ack, _) ->
289 if
290 CcmOptions#ipa_ccm_options.initiate_ack /= true ->
291 % Only respond to an ack if this instance did
292 % not initiate to prevent an infinite ack loop.
293 io:format("Socket ~p Stream ~p: ID_ACK -> ID_ACK~n", [Socket, StreamID]),
294 send(Socket, StreamID, <<?IPAC_MSGT_ID_ACK>>);
295 true ->
296 io:format("Socket ~p Stream ~p: ID_ACK -> None~n", [Socket, StreamID])
297 end;
Harald Welte176e28c2010-12-19 22:56:58 +0100298% Simply respond to ID_RESP with ID_ACK
Matt Johnson44a4dd62020-09-08 01:35:23 -0700299process_ccm_msg(Socket, StreamID, _, id_resp, _) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100300 io:format("Socket ~p Stream ~p: ID_RESP -> ID_ACK~n", [Socket, StreamID]),
301 send(Socket, StreamID, <<?IPAC_MSGT_ID_ACK>>);
Matt Johnson44a4dd62020-09-08 01:35:23 -0700302% Simply respond to ID_GET with ID_RESP
303process_ccm_msg(Socket, StreamID, CcmOptions, id_req, _) ->
304 io:format("Socket ~p Stream ~p: ID_GET -> ID_RESP~n", [Socket, StreamID]),
305 CcmBin = ipa_proto_ccm:encode(
306 {id_resp,
307 [{string,serial_nr,CcmOptions#ipa_ccm_options.serial_number},
308 {string,unit_id,CcmOptions#ipa_ccm_options.unit_id},
309 {string,mac_address,CcmOptions#ipa_ccm_options.mac_address},
310 {string,location,CcmOptions#ipa_ccm_options.location},
311 {string,unit_type,CcmOptions#ipa_ccm_options.unit_type},
312 {string,equip_vers,CcmOptions#ipa_ccm_options.equipment_version},
313 {string,sw_version,CcmOptions#ipa_ccm_options.sw_version},
314 {string,unit_name,CcmOptions#ipa_ccm_options.unit_name}
315 ]}),
316 send(Socket, StreamID, CcmBin);
317
Harald Welte176e28c2010-12-19 22:56:58 +0100318% Default message handler for unknown messages
Matt Johnson44a4dd62020-09-08 01:35:23 -0700319process_ccm_msg(Socket, StreamID, _, MsgType, Opts) ->
Harald Welte176e28c2010-12-19 22:56:58 +0100320 io:format("Socket ~p Stream ~p: Unknown CCM message type ~p Opts ~p~n",
321 [Socket, StreamID, MsgType, Opts]).
322
323% process an incoming CCM message (Stream ID 254)
Matt Johnson44a4dd62020-09-08 01:35:23 -0700324process_rx_ccm_msg(Socket, StreamID, CcmOptions, PayloadBin) ->
Harald Welte515aa332019-08-22 17:58:21 +0200325 {MsgType, Opts} = ipa_proto_ccm:decode(PayloadBin),
Matt Johnson44a4dd62020-09-08 01:35:23 -0700326 process_ccm_msg(Socket, StreamID, CcmOptions, MsgType, Opts).
Harald Welte176e28c2010-12-19 22:56:58 +0100327
328send_ccm_id_get(Socket) ->
Harald Welted0f391e2019-08-11 20:00:17 +0200329 send(Socket, ?IPAC_PROTO_CCM, <<?IPAC_MSGT_ID_GET>>).
Harald Welte176e28c2010-12-19 22:56:58 +0100330
Matt Johnson46214562020-09-08 01:42:49 -0700331send_ccm_id_ack(Socket) ->
332 send(Socket, ?IPAC_PROTO_CCM, <<?IPAC_MSGT_ID_ACK>>).
333
Harald Welte176e28c2010-12-19 22:56:58 +0100334% convenience wrapper for interactive use / debugging from the shell
335listen_accept_handle(LPort, Opts) ->
336 case gen_tcp:listen(LPort, ?IPA_SOCKOPTS ++ Opts) of
337 {ok, ListenSock} ->
338 {ok, Port} = inet:port(ListenSock),
339 {ok, Sock} = gen_tcp:accept(ListenSock),
340 {ok, IpaPid} = ipa_proto:register_socket(Sock),
341 ipa_proto:register_stream(Sock, 0, self()),
342 ipa_proto:register_stream(Sock, 255, self()),
343 gen_tcp:controlling_process(Sock, IpaPid),
344 {ok, Port};
345 {error, Reason} ->
346 {error, Reason}
347 end.
348
349% gen_tcp:connect() convenience wrappers
350connect(Address, Port, Options) ->
351 connect(Address, Port, Options, infinity).
352
353connect(Address, Port, Options, Timeout) ->
354 case gen_tcp:connect(Address, Port, ?IPA_SOCKOPTS ++ Options, Timeout) of
355 {ok, Socket} ->
356 case ipa_proto:register_socket(Socket) of
Harald Welte5cde7622012-01-23 23:18:28 +0100357 {ok, IpaPid} ->
358 {ok, {Socket, IpaPid}};
Harald Welte176e28c2010-12-19 22:56:58 +0100359 {error, Reason} ->
360 gen_tcp:close(Socket),
361 {error, Reason}
362 end;
363 {error, Reason} ->
364 {error, Reason}
365 end.
366
367% Utility function to continuously server incomming IPA connections on a
368% listening TCP socket
369start_listen(LPort, NumServers, Opts) ->
370 case gen_tcp:listen(LPort, ?IPA_SOCKOPTS ++ Opts) of
371 {ok, ListenSock} ->
372 start_servers(NumServers, ListenSock, self()),
373 {ok, Port} = inet:port(ListenSock),
374 Port;
375 {error, Reason} ->
376 {error, Reason}
377 end.
378
379start_servers(0, _, _) ->
380 ok;
381start_servers(Num, LS, CtrlPid) ->
382 spawn(?MODULE, listen_server, [LS, CtrlPid]),
383 start_servers(Num-1, LS, CtrlPid).
384
385listen_server(LS, CtrlPid) ->
386 case gen_tcp:accept(LS) of
387 {ok, S} ->
388 io:format("Accepted TCP connection from ~p~n", [inet:peername(S)]),
389 % assign the socket to the Controlling process
390 gen_tcp:controlling_process(S, CtrlPid),
391 CtrlPid ! {ipa_tcp_accept, S},
392 listen_server(LS, CtrlPid);
393 Other ->
394 io:format("accept returned ~w - goodbye!~n", [Other]),
395 ok
396 end.