%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright (c) 2020-2023 VMware, Inc. or its affiliates.  All rights reserved.
%%

-module(signal_handling_SUITE).

-include_lib("common_test/include/ct.hrl").
-include_lib("eunit/include/eunit.hrl").

-export([suite/0,
         all/0,
         groups/0,
         init_per_suite/1,
         end_per_suite/1,
         init_per_group/2,
         end_per_group/2,
         init_per_testcase/2,
         end_per_testcase/2,

         send_sighup/1,
         send_sigterm/1,
         send_sigtstp/1
        ]).

suite() ->
    [{timetrap, {minutes, 5}}].

all() ->
    [
     {group, signal_sent_to_pid_in_pidfile},
     {group, signal_sent_to_pid_from_os_getpid}
    ].

groups() ->
    Signals = [sighup,
               sigterm,
               sigtstp],
    Tests = [list_to_existing_atom(rabbit_misc:format("send_~ts", [Signal]))
             || Signal <- Signals],
    [
     {signal_sent_to_pid_in_pidfile, [], Tests},
     {signal_sent_to_pid_from_os_getpid, [], Tests}
    ].

%% -------------------------------------------------------------------
%% Testsuite setup/teardown.
%% -------------------------------------------------------------------

init_per_suite(Config) ->
    case os:type() of
        {unix, _} ->
            rabbit_ct_helpers:log_environment(),
            rabbit_ct_helpers:run_setup_steps(Config);
        _ ->
            {skip, "This testsuite is only relevant on Unix"}
    end.

end_per_suite(Config) ->
    rabbit_ct_helpers:run_teardown_steps(Config).

init_per_group(_, Config) ->
    Config.

end_per_group(_, Config) ->
    Config.

init_per_testcase(Testcase, Config) ->
    rabbit_ct_helpers:testcase_started(Config, Testcase),
    ClusterSize = 1,
    TestNumber = rabbit_ct_helpers:testcase_number(Config, ?MODULE, Testcase),
    Config1 = rabbit_ct_helpers:set_config(
                Config,
                [
                 {rmq_nodename_suffix, Testcase},
                 {tcp_ports_base, {skip_n_nodes, TestNumber * ClusterSize}}
                ]),
    rabbit_ct_helpers:run_steps(Config1,
      rabbit_ct_broker_helpers:setup_steps() ++
      rabbit_ct_client_helpers:setup_steps()).

end_per_testcase(Testcase, Config) ->
    Config1 = rabbit_ct_helpers:run_steps(Config,
      rabbit_ct_client_helpers:teardown_steps() ++
      rabbit_ct_broker_helpers:teardown_steps()),
    rabbit_ct_helpers:testcase_finished(Config1, Testcase).

%% -------------------------------------------------------------------
%% Testcases.
%% -------------------------------------------------------------------

send_sighup(Config) ->
    {PidFile, Pid} = get_pidfile_and_pid(Config),

    %% A SIGHUP signal should be ignored and the node should still be
    %% running.
    send_signal(Pid, "HUP"),
    timer:sleep(10000),
    A = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
    ?assert(rabbit_ct_broker_helpers:rpc(Config, A, rabbit, is_running, [])),
    ?assert(filelib:is_regular(PidFile)).

send_sigterm(Config) ->
    {PidFile, Pid} = get_pidfile_and_pid(Config),

    %% After sending a SIGTERM to the process, we expect the node to
    %% exit.
    send_signal(Pid, "TERM"),
    wait_for_node_exit(Pid),

    %% After a clean exit, the PID file should be removed.
    ?assertNot(filelib:is_regular(PidFile)).

send_sigtstp(Config) ->
    {PidFile, Pid} = get_pidfile_and_pid(Config),

    %% A SIGHUP signal should be ignored and the node should still be
    %% running.
    send_signal(Pid, "TSTP"),
    timer:sleep(10000),
    A = rabbit_ct_broker_helpers:get_node_config(Config, 0, nodename),
    ?assert(rabbit_ct_broker_helpers:rpc(Config, A, rabbit, is_running, [])),
    ?assert(filelib:is_regular(PidFile)).

get_pidfile_and_pid(Config) ->
    PidFile = rabbit_ct_broker_helpers:get_node_config(Config, 0, pid_file),
    ?assert(filelib:is_regular(PidFile)),

    %% We send the signal to either the process referenced in the
    %% PID file or the Erlang VM process. Up-to 3.8.x, they can be
    %% the different process because the PID file may reference the
    %% rabbitmq-server(8) script wrapper.
    [{name, Group} | _] = ?config(tc_group_properties, Config),
    Pid = case Group of
              signal_sent_to_pid_in_pidfile ->
                  {ok, P} = file:read_file(PidFile),
                  string:trim(P, trailing, [$\r,$\n]);
              signal_sent_to_pid_from_os_getpid ->
                  A = rabbit_ct_broker_helpers:get_node_config(
                        Config, 0, nodename),
                  rabbit_ct_broker_helpers:rpc(Config, A, os, getpid, [])
          end,
    {PidFile, Pid}.

send_signal(Pid, Signal) ->
    Cmd = ["kill",
           "-" ++ Signal,
           Pid],
    ?assertMatch({ok, _}, rabbit_ct_helpers:exec(Cmd)).

wait_for_node_exit(Pid) ->
    case rabbit_misc:is_os_process_alive(Pid) of
        true ->
            timer:sleep(1000),
            wait_for_node_exit(Pid);
        false ->
            ok
    end.
