1-module(rabbit_prelaunch).
2
3-include_lib("kernel/include/logger.hrl").
4-include_lib("eunit/include/eunit.hrl").
5
6-include_lib("rabbit_common/include/logging.hrl").
7
8-export([run_prelaunch_first_phase/0,
9         assert_mnesia_is_stopped/0,
10         get_context/0,
11         get_stop_reason/0,
12         set_stop_reason/1,
13         clear_stop_reason/0,
14         is_initial_pass/0,
15         initial_pass_finished/0,
16         shutdown_func/1]).
17
18-ifdef(TEST).
19-export([store_context/1,
20         clear_context_cache/0]).
21-endif.
22
23-define(PT_KEY_CONTEXT,       {?MODULE, context}).
24-define(PT_KEY_INITIAL_PASS,  {?MODULE, initial_pass_finished}).
25-define(PT_KEY_SHUTDOWN_FUNC, {?MODULE, chained_shutdown_func}).
26-define(PT_KEY_STOP_REASON,   {?MODULE, stop_reason}).
27
28run_prelaunch_first_phase() ->
29    try
30        ok = logger:set_process_metadata(
31               #{domain => ?RMQLOG_DOMAIN_PRELAUNCH}),
32        do_run()
33    catch
34        throw:{error, _} = Error ->
35            rabbit_prelaunch_errors:log_error(Error),
36            set_stop_reason(Error),
37            rabbit_boot_state:set(stopped),
38            Error;
39        Class:Exception:Stacktrace ->
40            rabbit_prelaunch_errors:log_exception(
41              Class, Exception, Stacktrace),
42            Error = {error, Exception},
43            set_stop_reason(Error),
44            rabbit_boot_state:set(stopped),
45            Error
46    end.
47
48do_run() ->
49    %% Indicate RabbitMQ is booting.
50    clear_stop_reason(),
51    rabbit_boot_state:set(booting),
52
53    %% Configure dbg if requested.
54    rabbit_prelaunch_early_logging:enable_quick_dbg(rabbit_env:dbg_config()),
55
56    %% Setup signal handler.
57    ok = rabbit_prelaunch_sighandler:setup(),
58
59    %% We assert Mnesia is stopped before we run the prelaunch
60    %% phases.
61    %%
62    %% We need this because our cluster consistency check (in the second
63    %% phase) depends on Mnesia not being started before it has a chance
64    %% to run.
65    %%
66    %% Also, in the initial pass, we don't want Mnesia to run before
67    %% Erlang distribution is configured.
68    assert_mnesia_is_stopped(),
69
70    %% Get informations to setup logging.
71    Context0 = rabbit_env:get_context_before_logging_init(),
72    ?assertMatch(#{}, Context0),
73
74    %% Setup logging for the prelaunch phase.
75    ok = rabbit_prelaunch_early_logging:setup_early_logging(Context0),
76
77    IsInitialPass = is_initial_pass(),
78    case IsInitialPass of
79        true ->
80            ?LOG_DEBUG(""),
81            ?LOG_DEBUG("== Prelaunch phase [1/2] (initial pass) =="),
82            ?LOG_DEBUG("");
83        false ->
84            ?LOG_DEBUG(""),
85            ?LOG_DEBUG("== Prelaunch phase [1/2] =="),
86            ?LOG_DEBUG("")
87    end,
88    rabbit_env:log_process_env(),
89
90    %% Load rabbitmq-env.conf, redo logging setup and continue.
91    Context1 = rabbit_env:get_context_after_logging_init(Context0),
92    ?assertMatch(#{}, Context1),
93    ok = rabbit_prelaunch_early_logging:setup_early_logging(Context1),
94    rabbit_env:log_process_env(),
95
96    %% Complete context now that we have the final environment loaded.
97    Context2 = rabbit_env:get_context_after_reloading_env(Context1),
98    ?assertMatch(#{}, Context2),
99    store_context(Context2),
100    rabbit_env:log_context(Context2),
101    ok = setup_shutdown_func(),
102
103    Context = Context2#{initial_pass => IsInitialPass},
104
105    rabbit_env:context_to_code_path(Context),
106    rabbit_env:context_to_app_env_vars(Context),
107
108    %% 1. Erlang/OTP compatibility check.
109    ok = rabbit_prelaunch_erlang_compat:check(Context),
110
111    %% 2. Configuration check + loading.
112    ok = rabbit_prelaunch_conf:setup(Context),
113
114    %% 3. Erlang distribution check + start.
115    ok = rabbit_prelaunch_dist:setup(Context),
116
117    %% 4. Write PID file.
118    ?LOG_DEBUG(""),
119    _ = write_pid_file(Context),
120
121    %% Garbage collect before returning because we do not want
122    %% to keep memory around forever unnecessarily, even if just
123    %% a few MiBs, because it will pollute output from tools like
124    %% Observer or observer_cli.
125    _ = erlang:garbage_collect(),
126
127    ignore.
128
129assert_mnesia_is_stopped() ->
130    ?assertNot(lists:keymember(mnesia, 1, application:which_applications())).
131
132store_context(Context) when is_map(Context) ->
133    persistent_term:put(?PT_KEY_CONTEXT, Context).
134
135get_context() ->
136    case persistent_term:get(?PT_KEY_CONTEXT, undefined) of
137        undefined -> undefined;
138        Context   -> Context#{initial_pass => is_initial_pass()}
139    end.
140
141-ifdef(TEST).
142clear_context_cache() ->
143    persistent_term:erase(?PT_KEY_CONTEXT).
144-endif.
145
146get_stop_reason() ->
147    persistent_term:get(?PT_KEY_STOP_REASON, undefined).
148
149set_stop_reason(Reason) ->
150    case get_stop_reason() of
151        undefined ->
152            ?LOG_DEBUG("Set stop reason to: ~p", [Reason]),
153            persistent_term:put(?PT_KEY_STOP_REASON, Reason);
154        _ ->
155            ok
156    end.
157
158clear_stop_reason() ->
159    persistent_term:erase(?PT_KEY_STOP_REASON).
160
161is_initial_pass() ->
162    not persistent_term:get(?PT_KEY_INITIAL_PASS, false).
163
164initial_pass_finished() ->
165    persistent_term:put(?PT_KEY_INITIAL_PASS, true).
166
167setup_shutdown_func() ->
168    ThisMod = ?MODULE,
169    ThisFunc = shutdown_func,
170    ExistingShutdownFunc = application:get_env(kernel, shutdown_func),
171    case ExistingShutdownFunc of
172        {ok, {ThisMod, ThisFunc}} ->
173            ok;
174        {ok, {ExistingMod, ExistingFunc}} ->
175            ?LOG_DEBUG(
176              "Setting up kernel shutdown function: ~s:~s/1 "
177              "(chained with ~s:~s/1)",
178              [ThisMod, ThisFunc, ExistingMod, ExistingFunc]),
179            ok = persistent_term:put(
180                   ?PT_KEY_SHUTDOWN_FUNC,
181                   ExistingShutdownFunc),
182            ok = record_kernel_shutdown_func(ThisMod, ThisFunc);
183        _ ->
184            ?LOG_DEBUG(
185              "Setting up kernel shutdown function: ~s:~s/1",
186              [ThisMod, ThisFunc]),
187            ok = record_kernel_shutdown_func(ThisMod, ThisFunc)
188    end.
189
190record_kernel_shutdown_func(Mod, Func) ->
191    application:set_env(
192      kernel, shutdown_func, {Mod, Func},
193      [{persistent, true}]).
194
195shutdown_func(Reason) ->
196    ?LOG_DEBUG(
197      "Running ~s:shutdown_func() as part of `kernel` shutdown", [?MODULE]),
198    Context = get_context(),
199    remove_pid_file(Context),
200    ChainedShutdownFunc = persistent_term:get(
201                            ?PT_KEY_SHUTDOWN_FUNC,
202                            undefined),
203    case ChainedShutdownFunc of
204        {ChainedMod, ChainedFunc} -> ChainedMod:ChainedFunc(Reason);
205        _                         -> ok
206    end.
207
208write_pid_file(#{pid_file := PidFile}) ->
209    ?LOG_DEBUG("Writing PID file: ~s", [PidFile]),
210    case filelib:ensure_dir(PidFile) of
211        ok ->
212            OSPid = os:getpid(),
213            case file:write_file(PidFile, OSPid) of
214                ok ->
215                    ok;
216                {error, Reason} = Error ->
217                    ?LOG_WARNING(
218                      "Failed to write PID file \"~s\": ~s",
219                      [PidFile, file:format_error(Reason)]),
220                    Error
221            end;
222        {error, Reason} = Error ->
223            ?LOG_WARNING(
224              "Failed to create PID file \"~s\" directory: ~s",
225              [PidFile, file:format_error(Reason)]),
226            Error
227    end;
228write_pid_file(_) ->
229    ok.
230
231remove_pid_file(#{pid_file := PidFile, keep_pid_file_on_exit := true}) ->
232    ?LOG_DEBUG("Keeping PID file: ~s", [PidFile]),
233    ok;
234remove_pid_file(#{pid_file := PidFile}) ->
235    ?LOG_DEBUG("Deleting PID file: ~s", [PidFile]),
236    _ = file:delete(PidFile),
237    ok;
238remove_pid_file(_) ->
239    ok.
240