1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2003-2021. 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%% %CopyrightEnd%
19%%
20
21%%
22%% Generic connection owner process.
23%%
24
25-module(ct_gen_conn).
26
27-behaviour(gen_server).
28
29-export([start/4,
30         stop/1,
31         get_conn_pid/1]).
32
33-export([call/2,
34         call/3,
35         return/2,
36         do_within_time/2]).
37
38-export([log/3,
39         start_log/1,
40         cont_log/2,
41         cont_log_no_timestamp/2,
42         end_log/0]).
43
44%% gen_server callbacks
45-export([init/1,
46         handle_info/2,
47         handle_cast/2,
48         handle_call/3,
49         code_change/3,
50         terminate/2]).
51
52%% test
53-export([make_opts/1]).
54
55-ifdef(debug).
56-define(dbg,true).
57-else.
58-define(dbg,false).
59-endif.
60
61-type handle() :: pid(). %% connection-owning process spawned here
62
63-record(gen_opts, {callback  :: module(),
64                   name      :: ct:target_name(),
65                   address   :: term(),
66                   init_data :: term(),
67                   reconnect    = true  :: boolean(),
68                   forward      = false :: boolean(),
69                   use_existing = true  :: boolean(),
70                   old          = false :: boolean(),
71                   conn_pid       :: pid() | undefined,
72                   cb_state       :: term(),
73                   ct_util_server :: pid() | undefined}).
74
75%% ---------------------------------------------------------------------
76
77-spec start(Address, InitData, CallbackMod, [Opt])
78   -> {ok, handle()} | {error, Reason :: term()}
79 when Address :: term(),
80      InitData :: term(),
81      CallbackMod :: module(),
82      Opt :: {name, Name :: ct:target_name()}
83           | {use_existing_connection, boolean()}
84           | {reconnect, boolean()}
85           | {forward_messages, boolean()}
86      ;    (Name, Address, InitData, CallbackMod)
87   -> {ok, handle()} | {error, Reason :: term()}
88 when Name :: ct:target_name(),
89      Address :: term(),
90      InitData :: term(),
91      CallbackMod :: module().
92
93%% Open a connection and start the generic connection owner process.
94%%
95%% CallbackMod is a specific callback module for
96%% each type of connection (e.g. telnet, ftp, netconf). It must export:
97%%
98%%   init(Name, Address, InitData) -> {ok, ConnectionPid, State}
99%%                                  | {error,Reason}.
100%%
101%% Name defaults to undefined if unspecified.
102%%
103%% The callback modules must also export:
104%%
105%%   handle_msg(Msg, From, State) -> {reply, Reply, State}
106%%                                 | {noreply, State}
107%%                                 | {stop, Reply, State}
108%%   terminate(ConnectionPid, State) -> term()
109%%   close(Handle) -> term()
110%%
111%% A Reply of the form {retry, _} results in a new call to the server with
112%% the retry tuple as message.
113%%
114%% The close/1 callback function is actually a callback
115%% for ct_util, for closing registered connections when
116%% ct_util_server is terminated. Handle is the Pid of
117%% the ct_gen_conn process.
118%%
119%% If option reconnect is true, then the
120%% callback must also export:
121%%
122%%   reconnect(Address, State) -> {ok, ConnectionPid, State}
123%%
124%% If option forward_messages is true then
125%% the callback must also export:
126%%
127%%   handle_msg(Msg,State) -> {noreply,State}
128%%                          | {stop,State}
129%%
130%% An old interface still exists. This is used by ct_telnet, ct_ftp
131%% and ct_ssh. The start function then has an explicit
132%% Name argument, and no Opts argument.
133
134start(Address, InitData, CallbackMod, Opts) when is_list(Opts) ->
135    do_start(Address, InitData, CallbackMod, Opts);
136start(Name, Address, InitData, CallbackMod) ->
137    do_start(Address, InitData, CallbackMod, [{name, Name}, {old, true}]).
138
139%% ---------------------------------------------------------------------
140
141-spec stop(handle()) -> ok | {error, Reason :: term()}.
142
143%% Close the connection and stop the process managing it.
144
145stop(Handle) ->
146    call(Handle, stop, 5000).
147
148%% ---------------------------------------------------------------------
149
150-spec get_conn_pid(handle()) -> pid().
151
152%% Return the connection pid associated with Handle
153
154get_conn_pid(Handle) ->
155    call(Handle, get_conn_pid).
156
157%% ---------------------------------------------------------------------
158
159-spec log(Heading, Format, Args) -> ok
160 when Heading :: iodata(),
161      Format  :: io:format(),
162      Args    :: [term()].
163
164%% Log activities on the current connection.
165%% See ct_logs:log/3
166
167log(Heading, Format, Args) ->
168    log(log, [Heading, Format, Args]).
169
170%% ---------------------------------------------------------------------
171
172-spec start_log(Heading :: iodata()) -> ok.
173
174%% Log activities on the current connection.
175%% See ct_logs:start_log/1
176
177start_log(Heading) ->
178    log(start_log, [Heading]).
179
180%% ---------------------------------------------------------------------
181
182-spec cont_log(Format, Args) -> ok
183 when Format :: io:format(),
184      Args :: [term()].
185
186%% Log activities on the current connection.
187%% See ct_logs:cont_log/2
188
189cont_log(Format,Args) ->
190    log(cont_log, [Format, Args]).
191
192%% ---------------------------------------------------------------------
193
194-spec cont_log_no_timestamp(Format, Args) -> ok
195 when Format :: io:format(),
196      Args :: [term()].
197
198%% Log activities on the current connection.
199%% See ct_logs:cont_log/2
200
201cont_log_no_timestamp(Format, Args) ->
202    log(cont_log_no_timestamp, [Format, Args]).
203
204%% ---------------------------------------------------------------------
205
206-spec end_log() -> ok.
207
208%% Log activities on the current connection.
209%% See ct_logs:end_log/0
210
211end_log() ->
212    log(end_log, []).
213
214%% ---------------------------------------------------------------------
215
216-spec do_within_time(Fun, Tmo)
217   -> Result
218 when Fun :: fun(),
219      Tmo :: non_neg_integer(),
220      Result :: term().
221
222%% Return the result of evaluating Fun, or interrupt after Tmo
223%% milliseconds or if the connection is closed. Assumes the caller
224%% is trapping exits.
225
226do_within_time(Fun, Tmo) ->
227    do_within_time(Fun, Tmo, get(silent), get(conn_pid)).
228
229%% Work around the fact that ct_telnet calls do_within_time/2 in its
230%% init callback, before it returns the connection pid for init/1 to
231%% write to the process dictionary. Monitoring on self() is pointless,
232%% but harmless. Should really be fixed by not using the process
233%% dictionary to pass arguments.
234do_within_time(Fun, Tmo, Silent, undefined) ->
235    do_within_time(Fun, Tmo, Silent, self());
236
237do_within_time(Fun, Tmo, Silent, ConnPid) ->
238    MRef = monitor(process, ConnPid),
239    Pid = spawn_link(fun() ->
240                             ct_util:mark_process(),
241                             put(silent, Silent),
242                             exit({MRef, Fun()})
243                     end),
244    down(Pid, MRef, Tmo, failure).
245
246down(Pid, MRef, Tmo, Reason) ->
247    receive
248        {'EXIT', Pid, T} ->
249            infinity == Tmo orelse demonitor(MRef, [flush]),
250            rc(MRef, T, Reason);
251        {'DOWN', MRef, process, _, _} ->
252            down(Pid, MRef, connection_closed)
253    after Tmo ->
254            demonitor(MRef, [flush]),
255            down(Pid, MRef, timeout)
256    end.
257
258down(Pid, MRef, Reason) ->
259    exit(Pid, kill),
260    down(Pid, MRef, infinity, Reason).
261
262rc(Ref, {Ref, RC}, _Reason) ->
263    RC;
264rc(_, Reason, failure) ->  %% failure before timeout or lost connection
265    {error, Reason};
266rc(_, _, Reason) ->
267    {error, Reason}.
268
269%% ===========================================================================
270
271do_start(Address, InitData, CallbackMod, OptsList) ->
272    #gen_opts{name = Name}
273        = Opts
274        = make_opts(OptsList, #gen_opts{callback  = CallbackMod,
275                                        address   = Address,
276                                        init_data = InitData}),
277    %% Testing for an existing connection is slightly broken as long
278    %% as calls to start aren't serialized: concurrent starts can both
279    %% end up in the final clause.
280    case ct_util:does_connection_exist(Name, Address, CallbackMod) of
281        {ok, _Pid} = Ok when Opts#gen_opts.use_existing ->
282            log("ct_gen_conn:start","Using existing connection!\n", []),
283            Ok;
284        {ok, Pid} when not Opts#gen_opts.use_existing ->
285            {error, {connection_exists, Pid}};
286        false ->
287            do_start(Opts)
288    end.
289
290do_start(Opts) ->
291    try gen_server:start(?MODULE, Opts, []) of
292        {ok, _} = Ok    -> Ok;
293        {error, Reason} -> {error, rc(Reason)}
294    catch
295        exit: Reason ->
296            log("ct_gen_conn:start",
297                "Connection process died: ~tp\n",
298                [Reason]),
299            {error, {connection_process_died, Reason}}
300    end.
301
302%% Unwrap a {shutdown, _} exit reason for backwards compatibility.
303rc({shutdown, Reason}) -> Reason;
304rc(T)                  -> T.
305
306make_opts(Opts) ->
307    make_opts(Opts, #gen_opts{}).
308
309make_opts(Opts, #gen_opts{} = Rec) ->
310    lists:foldl(fun opt/2, Rec, Opts).
311
312opt({name, Name}, Rec)                    -> Rec#gen_opts{name = Name};
313opt({reconnect, Bool}, Rec)               -> Rec#gen_opts{reconnect = Bool};
314opt({forward_messages, Bool}, Rec)        -> Rec#gen_opts{forward = Bool};
315opt({use_existing_connection, Bool}, Rec) -> Rec#gen_opts{use_existing = Bool};
316opt({old, Bool}, Rec)                     -> Rec#gen_opts{old = Bool}.
317
318%% ===========================================================================
319
320call(Pid, Msg) ->
321    call(Pid, Msg, infinity).
322
323-spec call(Handle, Msg, Tmo)
324   -> term()
325 when Handle :: handle(),
326      Msg :: term(),
327      Tmo :: non_neg_integer()
328           | infinity.
329
330call(Pid, Msg, infinity = Tmo) ->
331    gen_call(Pid, Msg, Tmo);
332
333%% Spawn a middleman process if the call can timeout to avoid the
334%% possibilty of a reply being left in the caller's mailbox after a
335%% timeout.
336call(Pid, Msg, Tmo) ->
337    {_, MRef} = spawn_monitor(fun() -> exit(gen_call(Pid, Msg, Tmo)) end),
338    receive {'DOWN', MRef, process, _, RC} -> RC end.
339
340gen_call(Pid, Msg, Tmo) ->
341    try gen_server:call(Pid, Msg, Tmo) of
342        T -> retry(Pid, T, Tmo)
343    catch
344        exit: Reason -> {error, {process_down, Pid, rc(Pid, Reason)}}
345    end.
346
347retry(Pid, {retry, _} = T, Tmo) -> gen_call(Pid, T, Tmo);
348retry(_, T, _)                  -> T.
349
350%% Unwrap the MFA gen_server puts into exit reasons.
351rc(Pid, {Reason, {gen_server, call, _}}) ->
352    rc(Pid, Reason);
353
354rc(Pid, timeout) ->
355    log("ct_gen_conn",
356        "Connection process ~w not responding. Killing now!",
357        [Pid]),
358    exit(Pid, kill),
359    forced_termination;
360
361rc(_, Reason) ->
362    rc(Reason).
363
364return(From, Result) ->
365    gen_server:reply(From, Result).
366
367%% ===========================================================================
368%% gen_server callbacks
369
370%% init/1
371
372init(#gen_opts{callback = Mod,
373               name = Name,
374               address = Addr,
375               init_data = InitData}
376     = Opts) ->
377    process_flag(trap_exit, true),
378    ct_util:mark_process(),
379    put(silent, false),
380    try Mod:init(Name, Addr, InitData) of
381        {ok, ConnPid, State} when is_pid(ConnPid) ->
382            link(ConnPid),
383            put(conn_pid, ConnPid),
384            SrvPid = whereis(ct_util_server),
385            link(SrvPid),
386            ct_util:register_connection(Name, Addr, Mod, self()),
387            {ok, Opts#gen_opts{conn_pid = ConnPid,
388                               cb_state = State,
389                               ct_util_server = SrvPid}};
390        {error, Reason} ->
391            {stop, {shutdown, Reason}}
392    catch
393        C: Reason when C /= error ->
394            {stop, {shutdown, Reason}}
395    end.
396
397%% handle_call/2
398
399handle_call(get_conn_pid, _From, #gen_opts{conn_pid = Pid} = Opts) ->
400    {reply, Pid, Opts};
401
402handle_call(stop, _From, Opts) ->
403    {stop, normal, ok, Opts};
404
405%% Only retry if failure is because of a reconnection.
406handle_call({retry, {Error, _Name, ConnPid, _Msg}},
407            _From,
408            #gen_opts{conn_pid = ConnPid}
409            = Opts) ->
410    {reply, error_rc(Error), Opts};
411
412handle_call({retry, {_Error, _Name, _CPid, Msg}},
413            _From,
414            #gen_opts{callback = Mod,
415                      cb_state = State}
416            = Opts) ->
417    log("Rerunning command","Connection reestablished. "
418        "Rerunning command...",
419        []),
420    {Reply, NewState} = Mod:handle_msg(Msg, State),
421    {reply, Reply, Opts#gen_opts{cb_state = NewState}};
422
423handle_call(Msg, _From, #gen_opts{old = true,
424                                  callback = Mod,
425                                  cb_state = State}
426                        = Opts) ->
427    {Reply, NewState} = Mod:handle_msg(Msg, State),
428    {reply, Reply, Opts#gen_opts{cb_state = NewState}};
429
430handle_call(Msg, From, #gen_opts{callback = Mod,
431                                  cb_state = State}
432                        = Opts) ->
433    case Mod:handle_msg(Msg, From, State) of
434        {reply, Reply, NewState} ->
435            {reply, Reply, Opts#gen_opts{cb_state = NewState}};
436        {noreply, NewState} ->
437            {noreply, Opts#gen_opts{cb_state = NewState}};
438        {stop, Reply, NewState} ->
439            {stop, normal, Reply, Opts#gen_opts{cb_state = NewState}}
440    end.
441
442%% handle_cast/2
443
444handle_cast(_, Opts) ->
445    {noreply, Opts}.
446
447%% handle_info/2
448
449handle_info({'EXIT', Pid, Reason},
450            #gen_opts{reconnect = true,
451                      conn_pid = Pid,
452                      address = Addr}
453            = Opts) ->
454    log("Connection down!\nOpening new!",
455        "Reason: ~tp\nAddress: ~tp\n",
456        [Reason, Addr]),
457    case reconnect(Opts) of
458        {ok, NewPid, NewState} ->
459            link(NewPid),
460            put(conn_pid, NewPid),
461            {noreply, Opts#gen_opts{conn_pid = NewPid,
462                                    cb_state = NewState}};
463        Error ->
464            log("Reconnect failed. Giving up!",
465                "Reason: ~tp\n",
466                [Error]),
467            {stop, normal, Opts}
468    end;
469
470handle_info({'EXIT', Pid, Reason},
471            #gen_opts{reconnect = false,
472                      conn_pid = Pid}
473            = Opts) ->
474    log("Connection closed!", "Reason: ~tp\n", [Reason]),
475    {stop, normal, Opts};
476
477handle_info({'EXIT', Pid, Reason},
478            #gen_opts{ct_util_server = Pid}
479            = Opts) ->
480    {stop, {shutdown, Reason}, Opts};
481
482handle_info(Msg, #gen_opts{forward = true,
483                           callback = Mod,
484                           cb_state = State}
485                 = Opts) ->
486    case Mod:handle_msg(Msg, State) of
487        {noreply, NewState} ->
488            {noreply, Opts#gen_opts{cb_state = NewState}};
489        {stop, NewState} ->
490            {stop, normal, Opts#gen_opts{cb_state = NewState}}
491    end;
492
493handle_info(_, #gen_opts{} = Opts) ->
494    {noreply, Opts}.
495
496%% code_change/2
497
498code_change(_Vsn, State, _Extra) ->
499    {ok, State}.
500
501%% terminate/2
502
503%% Cleanup is only in this case since ct_util also cleans up and
504%% expects us not to, which is at least an odd expectation.
505terminate(normal, #gen_opts{callback = Mod,
506                            conn_pid = Pid,
507                            cb_state = State}) ->
508    ct_util:unregister_connection(self()),
509    unlink(Pid),
510    Mod:terminate(Pid, State);
511
512terminate(_, #gen_opts{}) ->
513    ok.
514
515%% ===========================================================================
516
517error_rc({error, _} = T) -> T;
518error_rc(Reason)         -> {error, Reason}.
519
520reconnect(#gen_opts{callback = Mod,
521                    address = Addr,
522                    cb_state = State}) ->
523    Mod:reconnect(Addr, State).
524
525log(Func, Args) ->
526    case get(silent) of
527        true when not ?dbg ->
528            ok;
529        _ ->
530            apply(ct_logs, Func, Args)
531    end.
532