summaryrefslogtreecommitdiff
path: root/deps/rabbit_common/src/rabbit_env.erl
diff options
context:
space:
mode:
Diffstat (limited to 'deps/rabbit_common/src/rabbit_env.erl')
-rw-r--r--deps/rabbit_common/src/rabbit_env.erl1850
1 files changed, 1850 insertions, 0 deletions
diff --git a/deps/rabbit_common/src/rabbit_env.erl b/deps/rabbit_common/src/rabbit_env.erl
new file mode 100644
index 0000000000..8817103e81
--- /dev/null
+++ b/deps/rabbit_common/src/rabbit_env.erl
@@ -0,0 +1,1850 @@
+%% 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) 2019-2020 VMware, Inc. or its affiliates. All rights reserved.
+%%
+
+-module(rabbit_env).
+
+-include_lib("kernel/include/file.hrl").
+
+-export([get_context/0,
+ get_context/1,
+ get_context_before_logging_init/0,
+ get_context_before_logging_init/1,
+ get_context_after_logging_init/1,
+ get_context_after_reloading_env/1,
+ dbg_config/0,
+ get_used_env_vars/0,
+ log_process_env/0,
+ log_context/1,
+ context_to_app_env_vars/1,
+ context_to_app_env_vars_no_logging/1,
+ context_to_code_path/1]).
+
+-ifdef(TEST).
+-export([parse_conf_env_file_output2/2,
+ value_is_yes/1]).
+-endif.
+
+-define(USED_ENV_VARS,
+ [
+ "RABBITMQ_ALLOW_INPUT",
+ "RABBITMQ_ADVANCED_CONFIG_FILE",
+ "RABBITMQ_BASE",
+ "RABBITMQ_CONF_ENV_FILE",
+ "RABBITMQ_CONFIG_FILE",
+ "RABBITMQ_CONFIG_FILES",
+ "RABBITMQ_DBG",
+ "RABBITMQ_DIST_PORT",
+ "RABBITMQ_ENABLED_PLUGINS",
+ "RABBITMQ_ENABLED_PLUGINS_FILE",
+ "RABBITMQ_FEATURE_FLAGS",
+ "RABBITMQ_FEATURE_FLAGS_FILE",
+ "RABBITMQ_HOME",
+ "RABBITMQ_KEEP_PID_FILE_ON_EXIT",
+ "RABBITMQ_LOG",
+ "RABBITMQ_LOG_BASE",
+ "RABBITMQ_LOG_FF_REGISTRY",
+ "RABBITMQ_LOGS",
+ "RABBITMQ_MNESIA_BASE",
+ "RABBITMQ_MNESIA_DIR",
+ "RABBITMQ_MOTD_FILE",
+ "RABBITMQ_NODE_IP_ADDRESS",
+ "RABBITMQ_NODE_PORT",
+ "RABBITMQ_NODENAME",
+ "RABBITMQ_PID_FILE",
+ "RABBITMQ_PLUGINS_DIR",
+ "RABBITMQ_PLUGINS_EXPAND_DIR",
+ "RABBITMQ_PRODUCT_NAME",
+ "RABBITMQ_PRODUCT_VERSION",
+ "RABBITMQ_QUORUM_DIR",
+ "RABBITMQ_STREAM_DIR",
+ "RABBITMQ_UPGRADE_LOG",
+ "RABBITMQ_USE_LONGNAME",
+ "SYS_PREFIX"
+ ]).
+
+get_context() ->
+ Context0 = get_context_before_logging_init(),
+ Context1 = get_context_after_logging_init(Context0),
+ get_context_after_reloading_env(Context1).
+
+get_context(TakeFromRemoteNode) ->
+ Context0 = get_context_before_logging_init(TakeFromRemoteNode),
+ Context1 = get_context_after_logging_init(Context0),
+ get_context_after_reloading_env(Context1).
+
+get_context_before_logging_init() ->
+ get_context_before_logging_init(false).
+
+get_context_before_logging_init(TakeFromRemoteNode) ->
+ %% The order of steps below is important because some of them
+ %% depends on previous steps.
+ Steps = [
+ fun os_type/1,
+ fun log_levels/1,
+ fun interactive_shell/1,
+ fun output_supports_colors/1
+ ],
+
+ run_context_steps(context_base(TakeFromRemoteNode), Steps).
+
+get_context_after_logging_init(Context) ->
+ %% The order of steps below is important because some of them
+ %% depends on previous steps.
+ Steps = [
+ fun sys_prefix/1,
+ fun rabbitmq_base/1,
+ fun data_dir/1,
+ fun rabbitmq_home/1,
+ fun config_base_dir/1,
+ fun load_conf_env_file/1,
+ fun log_levels/1
+ ],
+
+ run_context_steps(Context, Steps).
+
+get_context_after_reloading_env(Context) ->
+ %% The order of steps below is important because some of them
+ %% depends on previous steps.
+ Steps = [
+ fun nodename_type/1,
+ fun nodename/1,
+ fun split_nodename/1,
+ fun maybe_setup_dist_for_remote_query/1,
+ fun dbg_config/1,
+ fun main_config_file/1,
+ fun additional_config_files/1,
+ fun advanced_config_file/1,
+ fun log_base_dir/1,
+ fun main_log_file/1,
+ fun upgrade_log_file/1,
+ fun mnesia_base_dir/1,
+ fun mnesia_dir/1,
+ fun quorum_queue_dir/1,
+ fun stream_queue_dir/1,
+ fun pid_file/1,
+ fun keep_pid_file_on_exit/1,
+ fun feature_flags_file/1,
+ fun forced_feature_flags_on_init/1,
+ fun log_feature_flags_registry/1,
+ fun plugins_path/1,
+ fun plugins_expand_dir/1,
+ fun enabled_plugins_file/1,
+ fun enabled_plugins/1,
+ fun maybe_stop_dist_for_remote_query/1,
+ fun amqp_ipaddr/1,
+ fun amqp_tcp_port/1,
+ fun erlang_dist_tcp_port/1,
+ fun product_name/1,
+ fun product_version/1,
+ fun motd_file/1
+ ],
+
+ run_context_steps(Context, Steps).
+
+context_base(TakeFromRemoteNode) ->
+ Context = #{},
+ case TakeFromRemoteNode of
+ false ->
+ Context;
+ offline ->
+ update_context(Context,
+ from_remote_node,
+ offline);
+ _ when is_atom(TakeFromRemoteNode) ->
+ update_context(Context,
+ from_remote_node,
+ {TakeFromRemoteNode, 10000});
+ {RemoteNode, infinity}
+ when is_atom(RemoteNode) ->
+ update_context(Context,
+ from_remote_node,
+ TakeFromRemoteNode);
+ {RemoteNode, Timeout}
+ when is_atom(RemoteNode) andalso
+ is_integer(Timeout) andalso
+ Timeout >= 0 ->
+ update_context(Context,
+ from_remote_node,
+ {TakeFromRemoteNode, Timeout})
+ end.
+
+-ifdef(TEST).
+os_type(Context) ->
+ {OSType, Origin} =
+ try
+ {persistent_term:get({?MODULE, os_type}), environment}
+ catch
+ _:badarg ->
+ {os:type(), default}
+ end,
+ update_context(Context, os_type, OSType, Origin).
+-else.
+os_type(Context) ->
+ update_context(Context, os_type, os:type(), default).
+-endif.
+
+run_context_steps(Context, Steps) ->
+ lists:foldl(
+ fun(Step, Context1) -> Step(Context1) end,
+ Context,
+ Steps).
+
+update_context(Context, Key, Value) ->
+ Context#{Key => Value}.
+
+-define(origin_is_valid(O),
+ O =:= default orelse
+ O =:= environment orelse
+ O =:= remote_node).
+
+update_context(#{var_origins := Origins} = Context, Key, Value, Origin)
+ when ?origin_is_valid(Origin) ->
+ Context#{Key => Value,
+ var_origins => Origins#{Key => Origin}};
+update_context(Context, Key, Value, Origin)
+ when ?origin_is_valid(Origin) ->
+ Context#{Key => Value,
+ var_origins => #{Key => Origin}}.
+
+get_used_env_vars() ->
+ lists:filter(
+ fun({Var, _}) -> var_is_used(Var) end,
+ lists:sort(os:list_env_vars())).
+
+log_process_env() ->
+ rabbit_log_prelaunch:debug("Process environment:"),
+ lists:foreach(
+ fun({Var, Value}) ->
+ rabbit_log_prelaunch:debug(" - ~s = ~ts", [Var, Value])
+ end, lists:sort(os:list_env_vars())).
+
+log_context(Context) ->
+ rabbit_log_prelaunch:debug("Context (based on environment variables):"),
+ lists:foreach(
+ fun(Key) ->
+ Value = maps:get(Key, Context),
+ rabbit_log_prelaunch:debug(" - ~s: ~p", [Key, Value])
+ end,
+ lists:sort(maps:keys(Context))).
+
+context_to_app_env_vars(Context) ->
+ rabbit_log_prelaunch:debug(
+ "Setting default application environment variables:"),
+ Fun = fun({App, Param, Value}) ->
+ rabbit_log_prelaunch:debug(
+ " - ~s:~s = ~p", [App, Param, Value]),
+ ok = application:set_env(
+ App, Param, Value, [{persistent, true}])
+ end,
+ context_to_app_env_vars1(Context, Fun).
+
+context_to_app_env_vars_no_logging(Context) ->
+ Fun = fun({App, Param, Value}) ->
+ ok = application:set_env(
+ App, Param, Value, [{persistent, true}])
+ end,
+ context_to_app_env_vars1(Context, Fun).
+
+context_to_app_env_vars1(
+ #{mnesia_dir := MnesiaDir,
+ feature_flags_file := FFFile,
+ quorum_queue_dir := QuorumQueueDir,
+ stream_queue_dir := StreamQueueDir,
+ plugins_path := PluginsPath,
+ plugins_expand_dir := PluginsExpandDir,
+ enabled_plugins_file := EnabledPluginsFile} = Context,
+ Fun) ->
+ lists:foreach(
+ Fun,
+ %% Those are all the application environment variables which
+ %% were historically set on the erl(1) command line in
+ %% rabbitmq-server(8).
+ [{kernel, inet_default_connect_options, [{nodelay, true}]},
+ {sasl, errlog_type, error},
+ {os_mon, start_cpu_sup, false},
+ {os_mon, start_disksup, false},
+ {os_mon, start_memsup, false},
+ {mnesia, dir, MnesiaDir},
+ {ra, data_dir, QuorumQueueDir},
+ {osiris, data_dir, StreamQueueDir},
+ {rabbit, feature_flags_file, FFFile},
+ {rabbit, plugins_dir, PluginsPath},
+ {rabbit, plugins_expand_dir, PluginsExpandDir},
+ {rabbit, enabled_plugins_file, EnabledPluginsFile}]),
+
+ case Context of
+ #{erlang_dist_tcp_port := DistTcpPort} ->
+ lists:foreach(
+ Fun,
+ [{kernel, inet_dist_listen_min, DistTcpPort},
+ {kernel, inet_dist_listen_max, DistTcpPort}]);
+ _ ->
+ ok
+ end,
+ case Context of
+ #{amqp_ipaddr := IpAddr,
+ amqp_tcp_port := TcpPort}
+ when IpAddr /= undefined andalso TcpPort /= undefined ->
+ Fun({rabbit, tcp_listeners, [{IpAddr, TcpPort}]});
+ _ ->
+ ok
+ end,
+ ok.
+
+context_to_code_path(#{os_type := OSType, plugins_path := PluginsPath}) ->
+ Dirs = get_user_lib_dirs(OSType, PluginsPath),
+ code:add_pathsa(lists:reverse(Dirs)).
+
+%% -------------------------------------------------------------------
+%% Code copied from `kernel/src/code_server.erl`.
+%%
+%% The goal is to mimic the behavior of the `$ERL_LIBS` environment
+%% variable.
+
+get_user_lib_dirs(OSType, Path) ->
+ Sep = case OSType of
+ {win32, _} -> ";";
+ _ -> ":"
+ end,
+ SplitPath = string:lexemes(Path, Sep),
+ get_user_lib_dirs_1(SplitPath).
+
+get_user_lib_dirs_1([Dir|DirList]) ->
+ case erl_prim_loader:list_dir(Dir) of
+ {ok, Dirs} ->
+ Paths = make_path(Dir, Dirs),
+ %% Only add paths trailing with ./ebin.
+ [P || P <- Paths, filename:basename(P) =:= "ebin"] ++
+ get_user_lib_dirs_1(DirList);
+ error ->
+ get_user_lib_dirs_1(DirList)
+ end;
+get_user_lib_dirs_1([]) -> [].
+
+%%
+%% Create the initial path.
+%%
+make_path(BundleDir, Bundles0) ->
+ Bundles = choose_bundles(Bundles0),
+ make_path(BundleDir, Bundles, []).
+
+choose_bundles(Bundles) ->
+ ArchiveExt = archive_extension(),
+ Bs = lists:sort([create_bundle(B, ArchiveExt) || B <- Bundles]),
+ [FullName || {_Name,_NumVsn,FullName} <-
+ choose(lists:reverse(Bs), [], ArchiveExt)].
+
+create_bundle(FullName, ArchiveExt) ->
+ BaseName = filename:basename(FullName, ArchiveExt),
+ case split_base(BaseName) of
+ {Name, VsnStr} ->
+ case vsn_to_num(VsnStr) of
+ {ok, VsnNum} ->
+ {Name,VsnNum,FullName};
+ false ->
+ {FullName,[0],FullName}
+ end;
+ _ ->
+ {FullName,[0],FullName}
+ end.
+
+%% Convert "X.Y.Z. ..." to [K, L, M| ...]
+vsn_to_num(Vsn) ->
+ case is_vsn(Vsn) of
+ true ->
+ {ok, [list_to_integer(S) || S <- string:lexemes(Vsn, ".")]};
+ _ ->
+ false
+ end.
+
+is_vsn(Str) when is_list(Str) ->
+ Vsns = string:lexemes(Str, "."),
+ lists:all(fun is_numstr/1, Vsns).
+
+is_numstr(Cs) ->
+ lists:all(fun (C) when $0 =< C, C =< $9 -> true;
+ (_) -> false
+ end, Cs).
+
+choose([{Name,NumVsn,NewFullName}=New|Bs], Acc, ArchiveExt) ->
+ case lists:keyfind(Name, 1, Acc) of
+ {_, NV, OldFullName} when NV =:= NumVsn ->
+ case filename:extension(OldFullName) =:= ArchiveExt of
+ false ->
+ choose(Bs,Acc, ArchiveExt);
+ true ->
+ Acc2 = lists:keystore(Name, 1, Acc, New),
+ choose(Bs,Acc2, ArchiveExt)
+ end;
+ {_, _, _} ->
+ choose(Bs,Acc, ArchiveExt);
+ false ->
+ choose(Bs,[{Name,NumVsn,NewFullName}|Acc], ArchiveExt)
+ end;
+choose([],Acc, _ArchiveExt) ->
+ Acc.
+
+make_path(_, [], Res) ->
+ Res;
+make_path(BundleDir, [Bundle|Tail], Res) ->
+ Dir = filename:append(BundleDir, Bundle),
+ Ebin = filename:append(Dir, "ebin"),
+ %% First try with /ebin
+ case is_dir(Ebin) of
+ true ->
+ make_path(BundleDir, Tail, [Ebin|Res]);
+ false ->
+ %% Second try with archive
+ Ext = archive_extension(),
+ Base = filename:basename(Bundle, Ext),
+ Ebin2 = filename:join([BundleDir, Base ++ Ext, Base, "ebin"]),
+ Ebins =
+ case split_base(Base) of
+ {AppName,_} ->
+ Ebin3 = filename:join([BundleDir, Base ++ Ext,
+ AppName, "ebin"]),
+ [Ebin3, Ebin2, Dir];
+ _ ->
+ [Ebin2, Dir]
+ end,
+ case try_ebin_dirs(Ebins) of
+ {ok,FoundEbin} ->
+ make_path(BundleDir, Tail, [FoundEbin|Res]);
+ error ->
+ make_path(BundleDir, Tail, Res)
+ end
+ end.
+
+try_ebin_dirs([Ebin|Ebins]) ->
+ case is_dir(Ebin) of
+ true -> {ok,Ebin};
+ false -> try_ebin_dirs(Ebins)
+ end;
+try_ebin_dirs([]) ->
+ error.
+
+split_base(BaseName) ->
+ case string:lexemes(BaseName, "-") of
+ [_, _|_] = Toks ->
+ Vsn = lists:last(Toks),
+ AllButLast = lists:droplast(Toks),
+ {string:join(AllButLast, "-"),Vsn};
+ [_|_] ->
+ BaseName
+ end.
+
+is_dir(Path) ->
+ case erl_prim_loader:read_file_info(Path) of
+ {ok,#file_info{type=directory}} -> true;
+ _ -> false
+ end.
+
+archive_extension() ->
+ init:archive_extension().
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_NODENAME
+%% Erlang node name.
+%% Default: rabbit@<hostname>
+%%
+%% RABBITMQ_USE_LONGNAME
+%% Flag indicating if long Erlang node names should be used instead
+%% of short ones.
+%% Default: unset (use short names)
+
+nodename_type(Context) ->
+ case get_prefixed_env_var("RABBITMQ_USE_LONGNAME") of
+ false ->
+ update_context(Context, nodename_type, shortnames, default);
+ Value ->
+ NameType = case value_is_yes(Value) of
+ true -> longnames;
+ false -> shortnames
+ end,
+ update_context(Context, nodename_type, NameType, environment)
+ end.
+
+nodename(#{nodename_type := NameType} = Context) ->
+ LongHostname = net_adm:localhost(),
+ ShortHostname = re:replace(LongHostname, "\\..*$", "", [{return, list}]),
+ case get_prefixed_env_var("RABBITMQ_NODENAME") of
+ false when NameType =:= shortnames ->
+ Nodename = rabbit_nodes_common:make({"rabbit", ShortHostname}),
+ update_context(Context, nodename, Nodename, default);
+ false when NameType =:= longnames ->
+ Nodename = rabbit_nodes_common:make({"rabbit", LongHostname}),
+ update_context(Context, nodename, Nodename, default);
+ Value ->
+ Nodename = case string:find(Value, "@") of
+ nomatch when NameType =:= shortnames ->
+ rabbit_nodes_common:make({Value, ShortHostname});
+ nomatch when NameType =:= longnames ->
+ rabbit_nodes_common:make({Value, LongHostname});
+ _ ->
+ rabbit_nodes_common:make(Value)
+ end,
+ update_context(Context, nodename, Nodename, environment)
+ end.
+
+split_nodename(#{nodename := Nodename} = Context) ->
+ update_context(Context,
+ split_nodename, rabbit_nodes_common:parts(Nodename)).
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_CONFIG_FILE
+%% Main configuration file.
+%% Extension is optional. `.config` for the old erlang-term-based
+%% format, `.conf` for the new Cuttlefish-based format.
+%% Default: (Unix) ${SYS_PREFIX}/etc/rabbitmq/rabbitmq
+%% (Windows) ${RABBITMQ_BASE}\rabbitmq
+%%
+%% RABBITMQ_CONFIG_FILES
+%% Additional configuration files.
+%% If a directory, all files directly inside it are loaded.
+%% If a glob pattern, all matching file are loaded.
+%% Only considered if the main configuration file is Cuttlefish-based.
+%% Default: (Unix) ${SYS_PREFIX}/etc/rabbitmq/conf.d/*.conf
+%% (Windows) ${RABBITMQ_BASE}\conf.d\*.conf
+%%
+%% RABBITMQ_ADVANCED_CONFIG_FILE
+%% Advanced configuration file.
+%% Erlang-term-based format with a `.config` extension.
+%% Default: (Unix) ${SYS_PREFIX}/etc/rabbitmq/advanced.config
+%% (Windows) ${RABBITMQ_BASE}\advanced.config
+
+config_base_dir(#{os_type := {unix, _},
+ sys_prefix := SysPrefix} = Context) ->
+ Dir = filename:join([SysPrefix, "etc", "rabbitmq"]),
+ update_context(Context, config_base_dir, Dir);
+config_base_dir(#{os_type := {win32, _},
+ rabbitmq_base := Dir} = Context) ->
+ update_context(Context, config_base_dir, Dir).
+
+main_config_file(Context) ->
+ case get_prefixed_env_var("RABBITMQ_CONFIG_FILE") of
+ false ->
+ File = get_default_main_config_file(Context),
+ update_context(Context, main_config_file, File, default);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, main_config_file, File, environment)
+ end.
+
+get_default_main_config_file(#{config_base_dir := ConfigBaseDir}) ->
+ filename:join(ConfigBaseDir, "rabbitmq").
+
+additional_config_files(Context) ->
+ case get_prefixed_env_var("RABBITMQ_CONFIG_FILES") of
+ false ->
+ Pattern = get_default_additional_config_files(Context),
+ update_context(
+ Context, additional_config_files, Pattern, default);
+ Value ->
+ Pattern = normalize_path(Value),
+ update_context(
+ Context, additional_config_files, Pattern, environment)
+ end.
+
+get_default_additional_config_files(#{config_base_dir := ConfigBaseDir}) ->
+ filename:join([ConfigBaseDir, "conf.d", "*.conf"]).
+
+advanced_config_file(Context) ->
+ case get_prefixed_env_var("RABBITMQ_ADVANCED_CONFIG_FILE") of
+ false ->
+ File = get_default_advanced_config_file(Context),
+ update_context(Context, advanced_config_file, File, default);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, advanced_config_file, File, environment)
+ end.
+
+get_default_advanced_config_file(#{config_base_dir := ConfigBaseDir}) ->
+ filename:join(ConfigBaseDir, "advanced.config").
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_LOG_BASE
+%% Directory to write log files
+%% Default: (Unix) ${SYS_PREFIX}/var/log/rabbitmq
+%% (Windows) ${RABBITMQ_BASE}\log
+%%
+%% RABBITMQ_LOGS
+%% Main log file
+%% Default: ${RABBITMQ_LOG_BASE}/${RABBITMQ_NODENAME}.log
+%%
+%% RABBITMQ_UPDATE_LOG
+%% Upgrade-procesure-specific log file
+%% Default: ${RABBITMQ_LOG_BASE}/${RABBITMQ_NODENAME}_upgrade.log
+%%
+%% RABBITMQ_LOG
+%% Log level; overrides the configuration file value
+%% Default: (undefined)
+%%
+%% RABBITMQ_DBG
+%% List of `module`, `module:function` or `module:function/arity`
+%% to watch with dbg.
+%% Default: (undefined)
+
+log_levels(Context) ->
+ case get_prefixed_env_var("RABBITMQ_LOG") of
+ false ->
+ update_context(Context, log_levels, undefined, default);
+ Value ->
+ LogLevels = parse_log_levels(string:lexemes(Value, ","), #{}),
+ update_context(Context, log_levels, LogLevels, environment)
+ end.
+
+parse_log_levels([CategoryValue | Rest], Result) ->
+ case string:lexemes(CategoryValue, "=") of
+ ["+color"] ->
+ Result1 = Result#{color => true},
+ parse_log_levels(Rest, Result1);
+ ["-color"] ->
+ Result1 = Result#{color => false},
+ parse_log_levels(Rest, Result1);
+ [CategoryOrLevel] ->
+ case parse_level(CategoryOrLevel) of
+ undefined ->
+ Result1 = Result#{CategoryOrLevel => info},
+ parse_log_levels(Rest, Result1);
+ Level ->
+ Result1 = Result#{global => Level},
+ parse_log_levels(Rest, Result1)
+ end;
+ [Category, Level0] ->
+ case parse_level(Level0) of
+ undefined ->
+ parse_log_levels(Rest, Result);
+ Level ->
+ Result1 = Result#{Category => Level},
+ parse_log_levels(Rest, Result1)
+ end
+ end;
+parse_log_levels([], Result) ->
+ Result.
+
+parse_level("debug") -> debug;
+parse_level("info") -> info;
+parse_level("notice") -> notice;
+parse_level("warning") -> warning;
+parse_level("error") -> error;
+parse_level("critical") -> critical;
+parse_level("alert") -> alert;
+parse_level("emergency") -> emergency;
+parse_level("none") -> none;
+parse_level(_) -> undefined.
+
+log_base_dir(#{os_type := OSType} = Context) ->
+ case {get_prefixed_env_var("RABBITMQ_LOG_BASE"), OSType} of
+ {false, {unix, _}} ->
+ #{sys_prefix := SysPrefix} = Context,
+ Dir = filename:join([SysPrefix, "var", "log", "rabbitmq"]),
+ update_context(Context, log_base_dir, Dir, default);
+ {false, {win32, _}} ->
+ #{rabbitmq_base := RabbitmqBase} = Context,
+ Dir = filename:join([RabbitmqBase, "log"]),
+ update_context(Context, log_base_dir, Dir, default);
+ {Value, _} ->
+ Dir = normalize_path(Value),
+ update_context(Context, log_base_dir, Dir, environment)
+ end.
+
+main_log_file(#{nodename := Nodename,
+ log_base_dir := LogBaseDir} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_LOGS") of
+ false ->
+ File= filename:join(LogBaseDir,
+ atom_to_list(Nodename) ++ ".log"),
+ update_context(Context, main_log_file, File, default);
+ "-" ->
+ update_context(Context, main_log_file, "-", environment);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, main_log_file, File, environment)
+ end.
+
+upgrade_log_file(#{nodename := Nodename,
+ log_base_dir := LogBaseDir} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_UPGRADE_LOG") of
+ false ->
+ File = filename:join(LogBaseDir,
+ atom_to_list(Nodename) ++ "_upgrade.log"),
+ update_context(Context, upgrade_log_file, File, default);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, upgrade_log_file, File, environment)
+ end.
+
+dbg_config() ->
+ {Mods, Output} = get_dbg_config(),
+ #{dbg_output => Output,
+ dbg_mods => Mods}.
+
+dbg_config(Context) ->
+ DbgContext = dbg_config(),
+ maps:merge(Context, DbgContext).
+
+get_dbg_config() ->
+ Output = stdout,
+ DbgValue = get_prefixed_env_var("RABBITMQ_DBG"),
+ case DbgValue of
+ false -> {[], Output};
+ _ -> get_dbg_config1(string:lexemes(DbgValue, ","), [], Output)
+ end.
+
+get_dbg_config1(["=" ++ Filename | Rest], Mods, _) ->
+ get_dbg_config1(Rest, Mods, Filename);
+get_dbg_config1([SpecValue | Rest], Mods, Output) ->
+ Pattern = "([^:]+)(?::([^/]+)(?:/([0-9]+))?)?",
+ Options = [{capture, all_but_first, list}],
+ Mods1 = case re:run(SpecValue, Pattern, Options) of
+ {match, [M, F, A]} ->
+ Entry = {list_to_atom(M),
+ list_to_atom(F),
+ list_to_integer(A)},
+ [Entry | Mods];
+ {match, [M, F]} ->
+ Entry = {list_to_atom(M),
+ list_to_atom(F),
+ '_'},
+ [Entry | Mods];
+ {match, [M]} ->
+ Entry = {list_to_atom(M),
+ '_',
+ '_'},
+ [Entry | Mods];
+ nomatch ->
+ Mods
+ end,
+ get_dbg_config1(Rest, Mods1, Output);
+get_dbg_config1([], Mods, Output) ->
+ {lists:reverse(Mods), Output}.
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_MNESIA_BASE
+%% Directory where to create Mnesia directory.
+%% Default: (Unix) ${SYS_PREFIX}/var/lib/rabbitmq/mnesia
+%% (Windows) ${RABBITMQ_BASE}/db
+%%
+%% RABBITMQ_MNESIA_DIR
+%% Directory where to put Mnesia data.
+%% Default: (Unix) ${RABBITMQ_MNESIA_BASE}/${RABBITMQ_NODENAME}
+%% (Windows) ${RABBITMQ_MNESIA_BASE}\${RABBITMQ_NODENAME}-mnesia
+
+mnesia_base_dir(#{from_remote_node := Remote} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_MNESIA_BASE") of
+ false when Remote =:= offline ->
+ update_context(Context, mnesia_base_dir, undefined, default);
+ false ->
+ mnesia_base_dir_from_node(Context);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, mnesia_base_dir, Dir, environment)
+ end;
+mnesia_base_dir(Context) ->
+ mnesia_base_dir_from_env(Context).
+
+mnesia_base_dir_from_env(Context) ->
+ case get_prefixed_env_var("RABBITMQ_MNESIA_BASE") of
+ false ->
+ Dir = get_default_mnesia_base_dir(Context),
+ update_context(Context, mnesia_base_dir, Dir, default);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, mnesia_base_dir, Dir, environment)
+ end.
+
+mnesia_base_dir_from_node(Context) ->
+ %% This variable is used to compute other variables only, we
+ %% don't need to know what a remote node used initially. Only the
+ %% variables based on it are relevant.
+ update_context(Context, mnesia_base_dir, undefined, default).
+
+get_default_mnesia_base_dir(#{data_dir := DataDir} = Context) ->
+ Basename = case Context of
+ #{os_type := {unix, _}} -> "mnesia";
+ #{os_type := {win32, _}} -> "db"
+ end,
+ filename:join(DataDir, Basename).
+
+mnesia_dir(#{from_remote_node := Remote} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_MNESIA_DIR") of
+ false when Remote =:= offline ->
+ update_context(Context, mnesia_dir, undefined, default);
+ false ->
+ mnesia_dir_from_node(Context);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, mnesia_dir, Dir, environment)
+ end;
+mnesia_dir(Context) ->
+ mnesia_dir_from_env(Context).
+
+mnesia_dir_from_env(Context) ->
+ case get_prefixed_env_var("RABBITMQ_MNESIA_DIR") of
+ false ->
+ Dir = get_default_mnesia_dir(Context),
+ update_context(Context, mnesia_dir, Dir, default);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, mnesia_dir, Dir, environment)
+ end.
+
+mnesia_dir_from_node(#{from_remote_node := Remote} = Context) ->
+ Ret = query_remote(Remote, application, get_env, [mnesia, dir]),
+ case Ret of
+ {ok, undefined} ->
+ throw({query, Remote, {mnesia, dir, undefined}});
+ {ok, {ok, Value}} ->
+ Dir = normalize_path(Value),
+ update_context(Context, mnesia_dir, Dir, remote_node);
+ {badrpc, nodedown} ->
+ update_context(Context, mnesia_dir, undefined, default)
+ end.
+
+get_default_mnesia_dir(#{os_type := {unix, _},
+ nodename := Nodename,
+ mnesia_base_dir := MnesiaBaseDir})
+ when MnesiaBaseDir =/= undefined ->
+ filename:join(MnesiaBaseDir, atom_to_list(Nodename));
+get_default_mnesia_dir(#{os_type := {win32, _},
+ nodename := Nodename,
+ mnesia_base_dir := MnesiaBaseDir})
+ when MnesiaBaseDir =/= undefined ->
+ filename:join(MnesiaBaseDir, atom_to_list(Nodename) ++ "-mnesia").
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_QUORUM_DIR
+%% Directory where to store Ra state for quorum queues.
+%% Default: ${RABBITMQ_MNESIA_DIR}/quorum
+
+quorum_queue_dir(#{mnesia_dir := MnesiaDir} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_QUORUM_DIR") of
+ false when MnesiaDir =/= undefined ->
+ Dir = filename:join(MnesiaDir, "quorum"),
+ update_context(Context, quorum_queue_dir, Dir, default);
+ false when MnesiaDir =:= undefined ->
+ update_context(Context, quorum_queue_dir, undefined, default);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, quorum_queue_dir, Dir, environment)
+ end.
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_STREAM_DIR
+%% Directory where to store Ra state for stream queues.
+%% Default: ${RABBITMQ_MNESIA_DIR}/stream
+
+stream_queue_dir(#{mnesia_dir := MnesiaDir} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_STREAM_DIR") of
+ false when MnesiaDir =/= undefined ->
+ Dir = filename:join(MnesiaDir, "stream"),
+ update_context(Context, stream_queue_dir, Dir, default);
+ false when MnesiaDir =:= undefined ->
+ update_context(Context, stream_queue_dir, undefined, default);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, stream_queue_dir, Dir, environment)
+ end.
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_PID_FILE
+%% File used to write the Erlang VM OS PID.
+%% Default: ${RABBITMQ_MNESIA_DIR}.pid
+%%
+%% RABBITMQ_KEEP_PID_FILE_ON_EXIT
+%% Whether to keep or remove the PID file on Erlang VM exit.
+%% Default: true
+
+pid_file(#{mnesia_base_dir := MnesiaBaseDir,
+ nodename := Nodename} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_PID_FILE") of
+ false when MnesiaBaseDir =/= undefined ->
+ File = filename:join(MnesiaBaseDir,
+ atom_to_list(Nodename) ++ ".pid"),
+ update_context(Context, pid_file, File, default);
+ false when MnesiaBaseDir =:= undefined ->
+ update_context(Context, pid_file, undefined, default);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, pid_file, File, environment)
+ end.
+
+keep_pid_file_on_exit(Context) ->
+ case get_prefixed_env_var("RABBITMQ_KEEP_PID_FILE_ON_EXIT") of
+ false ->
+ update_context(Context, keep_pid_file_on_exit, false, default);
+ Value ->
+ Keep = value_is_yes(Value),
+ update_context(Context, keep_pid_file_on_exit, Keep, environment)
+ end.
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_FEATURE_FLAGS_FILE
+%% File used to store enabled feature flags.
+%% Default: ${RABBITMQ_MNESIA_BASE}/${RABBITMQ_NODENAME}-feature_flags
+
+feature_flags_file(#{from_remote_node := Remote} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_FEATURE_FLAGS_FILE") of
+ false when Remote =:= offline ->
+ update_context(Context, feature_flags_file, undefined, default);
+ false ->
+ feature_flags_file_from_node(Context);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, feature_flags_file, File, environment)
+ end;
+feature_flags_file(Context) ->
+ feature_flags_file_from_env(Context).
+
+feature_flags_file_from_env(#{mnesia_base_dir := MnesiaBaseDir,
+ nodename := Nodename} = Context) ->
+ case get_env_var("RABBITMQ_FEATURE_FLAGS_FILE") of
+ false ->
+ File = filename:join(MnesiaBaseDir,
+ atom_to_list(Nodename) ++ "-feature_flags"),
+ update_context(Context, feature_flags_file, File, default);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, feature_flags_file, File, environment)
+ end.
+
+feature_flags_file_from_node(#{from_remote_node := Remote} = Context) ->
+ Ret = query_remote(Remote,
+ application, get_env, [rabbit, feature_flags_file]),
+ case Ret of
+ {ok, undefined} ->
+ throw({query, Remote, {rabbit, feature_flags_file, undefined}});
+ {ok, {ok, Value}} ->
+ File = normalize_path(Value),
+ update_context(Context, feature_flags_file, File, remote_node);
+ {badrpc, nodedown} ->
+ update_context(Context, feature_flags_file, undefined, default)
+ end.
+
+forced_feature_flags_on_init(Context) ->
+ Value = get_prefixed_env_var("RABBITMQ_FEATURE_FLAGS",
+ [keep_empty_string_as_is]),
+ case Value of
+ false ->
+ %% get_prefixed_env_var() considers an empty string
+ %% is the same as an undefined environment variable.
+ update_context(Context,
+ forced_feature_flags_on_init, undefined, default);
+ _ ->
+ Flags = [list_to_atom(V) || V <- string:lexemes(Value, ",")],
+ update_context(Context,
+ forced_feature_flags_on_init, Flags, environment)
+ end.
+
+log_feature_flags_registry(Context) ->
+ case get_prefixed_env_var("RABBITMQ_LOG_FF_REGISTRY") of
+ false ->
+ update_context(Context,
+ log_feature_flags_registry, false, default);
+ Value ->
+ Log = value_is_yes(Value),
+ update_context(Context,
+ log_feature_flags_registry, Log, environment)
+ end.
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_PLUGINS_DIR
+%% List of directories where to look for plugins.
+%% Directories are separated by:
+%% ':' on Unix
+%% ';' on Windows
+%% Default: ${RABBITMQ_HOME}/plugins
+%%
+%% RABBITMQ_PLUGINS_EXPAND_DIR
+%% Directory where to expand plugin archives.
+%% Default: ${RABBITMQ_MNESIA_BASE}/${RABBITMQ_NODENAME}-plugins-expand
+%%
+%% RABBITMQ_ENABLED_PLUGINS_FILE
+%% File where the list of enabled plugins is stored.
+%% Default: (Unix) ${SYS_PREFIX}/etc/rabbitmq/enabled_plugins
+%% (Windows) ${RABBITMQ_BASE}\enabled_plugins
+%%
+%% RABBITMQ_ENABLED_PLUGINS
+%% List of plugins to enable on startup.
+%% Values are:
+%% "ALL" to enable all plugins
+%% "" to enable no plugin
+%% a list of plugin names, separated by a coma (',')
+%% Default: Empty (i.e. use ${RABBITMQ_ENABLED_PLUGINS_FILE})
+
+plugins_path(#{from_remote_node := Remote} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_PLUGINS_DIR") of
+ false when Remote =:= offline ->
+ update_context(Context, plugins_path, undefined, default);
+ false ->
+ plugins_path_from_node(Context);
+ Path ->
+ update_context(Context, plugins_path, Path, environment)
+ end;
+plugins_path(Context) ->
+ plugins_path_from_env(Context).
+
+plugins_path_from_env(Context) ->
+ case get_prefixed_env_var("RABBITMQ_PLUGINS_DIR") of
+ false ->
+ Path = get_default_plugins_path_from_env(Context),
+ update_context(Context, plugins_path, Path, default);
+ Path ->
+ update_context(Context, plugins_path, Path, environment)
+ end.
+
+plugins_path_from_node(#{from_remote_node := Remote} = Context) ->
+ Ret = query_remote(Remote, application, get_env, [rabbit, plugins_dir]),
+ case Ret of
+ {ok, undefined} ->
+ throw({query, Remote, {rabbit, plugins_dir, undefined}});
+ {ok, {ok, Path}} ->
+ update_context(Context, plugins_path, Path, remote_node);
+ {badrpc, nodedown} ->
+ update_context(Context, plugins_path, undefined, default)
+ end.
+
+get_default_plugins_path(#{from_remote_node := offline}) ->
+ undefined;
+get_default_plugins_path(#{from_remote_node := Remote}) ->
+ get_default_plugins_path_from_node(Remote);
+get_default_plugins_path(Context) ->
+ get_default_plugins_path_from_env(Context).
+
+get_default_plugins_path_from_env(#{os_type := OSType}) ->
+ ThisModDir = this_module_dir(),
+ PluginsDir = rabbit_common_mod_location_to_plugins_dir(ThisModDir),
+ case {OSType, PluginsDir} of
+ {{unix, _}, "/usr/lib/rabbitmq/" ++ _} ->
+ UserPluginsDir = filename:join(
+ ["/", "usr", "lib", "rabbitmq", "plugins"]),
+ UserPluginsDir ++ ":" ++ PluginsDir;
+ _ ->
+ PluginsDir
+ end.
+
+get_default_plugins_path_from_node(Remote) ->
+ Ret = query_remote(Remote, code, where_is_file, ["rabbit_common.app"]),
+ case Ret of
+ {ok, non_existing = Error} ->
+ throw({query, Remote, {code, where_is_file, Error}});
+ {ok, Path} ->
+ rabbit_common_mod_location_to_plugins_dir(filename:dirname(Path));
+ {badrpc, nodedown} ->
+ undefined
+ end.
+
+rabbit_common_mod_location_to_plugins_dir(ModDir) ->
+ case filename:basename(ModDir) of
+ "ebin" ->
+ case filelib:is_dir(ModDir) of
+ false ->
+ %% rabbit_common in the plugin's .ez archive.
+ filename:dirname(
+ filename:dirname(
+ filename:dirname(ModDir)));
+ true ->
+ %% rabbit_common in the plugin's directory.
+ filename:dirname(
+ filename:dirname(ModDir))
+ end;
+ _ ->
+ %% rabbit_common in the CLI escript.
+ filename:join(
+ filename:dirname(
+ filename:dirname(ModDir)),
+ "plugins")
+ end.
+
+plugins_expand_dir(#{mnesia_base_dir := MnesiaBaseDir,
+ nodename := Nodename} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_PLUGINS_EXPAND_DIR") of
+ false when MnesiaBaseDir =/= undefined ->
+ Dir = filename:join(
+ MnesiaBaseDir,
+ atom_to_list(Nodename) ++ "-plugins-expand"),
+ update_context(Context, plugins_expand_dir, Dir, default);
+ false when MnesiaBaseDir =:= undefined ->
+ update_context(Context, plugins_expand_dir, undefined, default);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, plugins_expand_dir, Dir, environment)
+ end.
+
+enabled_plugins_file(#{from_remote_node := Remote} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_ENABLED_PLUGINS_FILE") of
+ false when Remote =:= offline ->
+ update_context(Context, enabled_plugins_file, undefined, default);
+ false ->
+ enabled_plugins_file_from_node(Context);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, enabled_plugins_file, File, environment)
+ end;
+enabled_plugins_file(Context) ->
+ enabled_plugins_file_from_env(Context).
+
+enabled_plugins_file_from_env(Context) ->
+ case get_prefixed_env_var("RABBITMQ_ENABLED_PLUGINS_FILE") of
+ false ->
+ File = get_default_enabled_plugins_file(Context),
+ update_context(Context, enabled_plugins_file, File, default);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, enabled_plugins_file, File, environment)
+ end.
+
+get_default_enabled_plugins_file(#{config_base_dir := ConfigBaseDir}) ->
+ filename:join(ConfigBaseDir, "enabled_plugins").
+
+enabled_plugins_file_from_node(#{from_remote_node := Remote} = Context) ->
+ Ret = query_remote(Remote,
+ application, get_env, [rabbit, enabled_plugins_file]),
+ case Ret of
+ {ok, undefined} ->
+ throw({query, Remote, {rabbit, enabled_plugins_file, undefined}});
+ {ok, {ok, Value}} ->
+ File = normalize_path(Value),
+ update_context(Context, enabled_plugins_file, File, remote_node);
+ {badrpc, nodedown} ->
+ update_context(Context, enabled_plugins_file, undefined, default)
+ end.
+
+enabled_plugins(Context) ->
+ Value = get_prefixed_env_var(
+ "RABBITMQ_ENABLED_PLUGINS",
+ [keep_empty_string_as_is]),
+ case Value of
+ false ->
+ update_context(Context, enabled_plugins, undefined, default);
+ "ALL" ->
+ update_context(Context, enabled_plugins, all, environment);
+ "" ->
+ update_context(Context, enabled_plugins, [], environment);
+ _ ->
+ Plugins = [list_to_atom(P) || P <- string:lexemes(Value, ",")],
+ update_context(Context, enabled_plugins, Plugins, environment)
+ end.
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_NODE_IP_ADDRESS
+%% AMQP TCP IP address to listen on
+%% Default: unset (i.e. listen on all interfaces)
+%%
+%% RABBITMQ_NODE_PORT
+%% AMQP TCP port.
+%% Default: 5672
+%%
+%% RABBITMQ_DIST_PORT
+%% Erlang distribution TCP port.
+%% Default: ${RABBITMQ_NODE_PORT} + 20000
+
+amqp_ipaddr(Context) ->
+ case get_prefixed_env_var("RABBITMQ_NODE_IP_ADDRESS") of
+ false ->
+ update_context(Context, amqp_ipaddr, "auto", default);
+ Value ->
+ update_context(Context, amqp_ipaddr, Value, environment)
+ end.
+
+amqp_tcp_port(Context) ->
+ case get_prefixed_env_var("RABBITMQ_NODE_PORT") of
+ false ->
+ update_context(Context, amqp_tcp_port, 5672, default);
+ TcpPortStr ->
+ try
+ TcpPort = erlang:list_to_integer(TcpPortStr),
+ update_context(Context, amqp_tcp_port, TcpPort, environment)
+ catch
+ _:badarg ->
+ rabbit_log_prelaunch:error(
+ "Invalid value for $RABBITMQ_NODE_PORT: ~p",
+ [TcpPortStr]),
+ throw({exit, ex_config})
+ end
+ end.
+
+erlang_dist_tcp_port(#{amqp_tcp_port := AmqpTcpPort} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_DIST_PORT") of
+ false ->
+ TcpPort = AmqpTcpPort + 20000,
+ update_context(Context, erlang_dist_tcp_port, TcpPort, default);
+ TcpPortStr ->
+ try
+ TcpPort = erlang:list_to_integer(TcpPortStr),
+ update_context(Context,
+ erlang_dist_tcp_port, TcpPort, environment)
+ catch
+ _:badarg ->
+ rabbit_log_prelaunch:error(
+ "Invalid value for $RABBITMQ_DIST_PORT: ~p",
+ [TcpPortStr]),
+ throw({exit, ex_config})
+ end
+ end.
+
+%% -------------------------------------------------------------------
+%%
+%% SYS_PREFIX [Unix only]
+%% Default: ""
+%%
+%% RABBITMQ_BASE [Windows only]
+%% Directory where to put RabbitMQ data.
+%% Default: !APPDATA!\RabbitMQ
+
+sys_prefix(#{os_type := {unix, _}} = Context) ->
+ case get_env_var("SYS_PREFIX") of
+ false ->
+ update_context(Context, sys_prefix, "", default);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, sys_prefix, Dir, environment)
+ end;
+sys_prefix(Context) ->
+ Context.
+
+rabbitmq_base(#{os_type := {win32, _}} = Context) ->
+ case get_env_var("RABBITMQ_BASE") of
+ false ->
+ AppData = normalize_path(get_env_var("APPDATA")),
+ Dir = filename:join(AppData, "RabbitMQ"),
+ update_context(Context, rabbitmq_base, Dir, default);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, rabbitmq_base, Dir, environment)
+ end;
+rabbitmq_base(Context) ->
+ Context.
+
+data_dir(#{os_type := {unix, _},
+ sys_prefix := SysPrefix} = Context) ->
+ Dir = filename:join([SysPrefix, "var", "lib", "rabbitmq"]),
+ update_context(Context, data_dir, Dir);
+data_dir(#{os_type := {win32, _},
+ rabbitmq_base := RabbitmqBase} = Context) ->
+ update_context(Context, data_dir, RabbitmqBase).
+
+rabbitmq_home(Context) ->
+ case get_env_var("RABBITMQ_HOME") of
+ false ->
+ Dir = filename:dirname(get_default_plugins_path(Context)),
+ update_context(Context, rabbitmq_home, Dir, default);
+ Value ->
+ Dir = normalize_path(Value),
+ update_context(Context, rabbitmq_home, Dir, environment)
+ end.
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_ALLOW_INPUT
+%% Indicate if an Erlang shell is started or not.
+%% Default: false
+
+interactive_shell(Context) ->
+ case get_env_var("RABBITMQ_ALLOW_INPUT") of
+ false ->
+ update_context(Context,
+ interactive_shell, false, default);
+ Value ->
+ update_context(Context,
+ interactive_shell, value_is_yes(Value), environment)
+ end.
+
+%% FIXME: We would need a way to call isatty(3) to make sure the output
+%% is a terminal.
+output_supports_colors(#{os_type := {unix, _}} = Context) ->
+ update_context(Context, output_supports_colors, true, default);
+output_supports_colors(#{os_type := {win32, _}} = Context) ->
+ update_context(Context, output_supports_colors, false, default).
+
+%% -------------------------------------------------------------------
+%%
+%% RABBITMQ_PRODUCT_NAME
+%% Override the product name
+%% Default: unset (i.e. "RabbitMQ")
+%%
+%% RABBITMQ_PRODUCT_VERSION
+%% Override the product version
+%% Default: unset (i.e. `rabbit` application version).
+%%
+%% RABBITMQ_MOTD_FILE
+%% Indicate a filename containing a "message of the day" to add to
+%% the banners, both the logged and the printed ones.
+%% Default: (Unix) ${SYS_PREFIX}/etc/rabbitmq/motd
+%% (Windows) ${RABBITMQ_BASE}\motd.txt
+
+product_name(#{from_remote_node := Remote} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_PRODUCT_NAME") of
+ false when Remote =:= offline ->
+ update_context(Context, product_name, undefined, default);
+ false ->
+ product_name_from_node(Context);
+ Value ->
+ update_context(Context, product_name, Value, environment)
+ end;
+product_name(Context) ->
+ product_name_from_env(Context).
+
+product_name_from_env(Context) ->
+ case get_prefixed_env_var("RABBITMQ_PRODUCT_NAME") of
+ false ->
+ update_context(Context, product_name, undefined, default);
+ Value ->
+ update_context(Context, product_name, Value, environment)
+ end.
+
+product_name_from_node(#{from_remote_node := Remote} = Context) ->
+ Ret = (catch query_remote(Remote, rabbit, product_name, [])),
+ case Ret of
+ {badrpc, nodedown} ->
+ update_context(Context, product_name, undefined, default);
+ {query, _, _} ->
+ update_context(Context, product_name, undefined, default);
+ Value ->
+ update_context(Context, product_name, Value, remote_node)
+ end.
+
+product_version(#{from_remote_node := Remote} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_PRODUCT_VERSION") of
+ false when Remote =:= offline ->
+ update_context(Context, product_version, undefined, default);
+ false ->
+ product_version_from_node(Context);
+ Value ->
+ update_context(Context, product_version, Value, environment)
+ end;
+product_version(Context) ->
+ product_version_from_env(Context).
+
+product_version_from_env(Context) ->
+ case get_prefixed_env_var("RABBITMQ_PRODUCT_VERSION") of
+ false ->
+ update_context(Context, product_version, undefined, default);
+ Value ->
+ update_context(Context, product_version, Value, environment)
+ end.
+
+product_version_from_node(#{from_remote_node := Remote} = Context) ->
+ Ret = (catch query_remote(Remote, rabbit, product_version, [])),
+ case Ret of
+ {badrpc, _} ->
+ update_context(Context, product_version, undefined, default);
+ {query, _, _} ->
+ update_context(Context, product_version, undefined, default);
+ Value ->
+ update_context(Context, product_version, Value, remote_node)
+ end.
+
+motd_file(#{from_remote_node := Remote} = Context) ->
+ case get_prefixed_env_var("RABBITMQ_MOTD_FILE") of
+ false when Remote =:= offline ->
+ update_context(Context, motd_file, undefined, default);
+ false ->
+ motd_file_from_node(Context);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, motd_file, File, environment)
+ end;
+motd_file(Context) ->
+ motd_file_from_env(Context).
+
+motd_file_from_env(Context) ->
+ case get_prefixed_env_var("RABBITMQ_MOTD_FILE") of
+ false ->
+ File = get_default_motd_file(Context),
+ update_context(Context, motd_file, File, default);
+ Value ->
+ File = normalize_path(Value),
+ update_context(Context, motd_file, File, environment)
+ end.
+
+get_default_motd_file(#{os_type := {unix, _},
+ config_base_dir := ConfigBaseDir}) ->
+ filename:join(ConfigBaseDir, "motd");
+get_default_motd_file(#{os_type := {win32, _},
+ config_base_dir := ConfigBaseDir}) ->
+ filename:join(ConfigBaseDir, "motd.txt").
+
+motd_file_from_node(#{from_remote_node := Remote} = Context) ->
+ Ret = (catch query_remote(Remote, rabbit, motd_file, [])),
+ case Ret of
+ {badrpc, _} ->
+ update_context(Context, motd_file, undefined, default);
+ {query, _, _} ->
+ update_context(Context, motd_file, undefined, default);
+ File ->
+ update_context(Context, motd_file, File, remote_node)
+ end.
+
+%% -------------------------------------------------------------------
+%% Loading of rabbitmq-env.conf.
+%% -------------------------------------------------------------------
+
+load_conf_env_file(#{os_type := {unix, _},
+ sys_prefix := SysPrefix} = Context) ->
+ {ConfEnvFile, Origin} =
+ case get_prefixed_env_var("RABBITMQ_CONF_ENV_FILE") of
+ false ->
+ File = filename:join(
+ [SysPrefix, "etc", "rabbitmq", "rabbitmq-env.conf"]),
+ {File, default};
+ Value ->
+ {normalize_path(Value), environment}
+ end,
+ Context1 = update_context(Context, conf_env_file, ConfEnvFile, Origin),
+ case loading_conf_env_file_enabled(Context1) of
+ true ->
+ case filelib:is_regular(ConfEnvFile) of
+ false ->
+ rabbit_log_prelaunch:debug(
+ "No $RABBITMQ_CONF_ENV_FILE (~ts)", [ConfEnvFile]),
+ Context1;
+ true ->
+ case os:find_executable("sh") of
+ false -> Context1;
+ Sh -> do_load_conf_env_file(Context1,
+ Sh,
+ ConfEnvFile)
+ end
+ end;
+ false ->
+ rabbit_log_prelaunch:debug(
+ "Loading of $RABBITMQ_CONF_ENV_FILE (~ts) is disabled",
+ [ConfEnvFile]),
+ Context1
+ end;
+load_conf_env_file(#{os_type := {win32, _},
+ rabbitmq_base := RabbitmqBase} = Context) ->
+ {ConfEnvFile, Origin} =
+ case get_prefixed_env_var("RABBITMQ_CONF_ENV_FILE") of
+ false ->
+ File = filename:join([RabbitmqBase, "rabbitmq-env-conf.bat"]),
+ {File, default};
+ Value ->
+ {normalize_path(Value), environment}
+ end,
+ Context1 = update_context(Context, conf_env_file, ConfEnvFile, Origin),
+ case loading_conf_env_file_enabled(Context1) of
+ true ->
+ case filelib:is_regular(ConfEnvFile) of
+ false ->
+ rabbit_log_prelaunch:debug(
+ "No $RABBITMQ_CONF_ENV_FILE (~ts)", [ConfEnvFile]),
+ Context1;
+ true ->
+ case os:find_executable("cmd.exe") of
+ false ->
+ Cmd = os:getenv("ComSpec"),
+ CmdExists =
+ Cmd =/= false andalso
+ filelib:is_regular(Cmd),
+ case CmdExists of
+ false -> Context1;
+ true -> do_load_conf_env_file(Context1,
+ Cmd,
+ ConfEnvFile)
+ end;
+ Cmd ->
+ do_load_conf_env_file(Context1, Cmd, ConfEnvFile)
+ end
+ end;
+ false ->
+ rabbit_log_prelaunch:debug(
+ "Loading of $RABBITMQ_CONF_ENV_FILE (~ts) is disabled",
+ [ConfEnvFile]),
+ Context1
+ end;
+load_conf_env_file(Context) ->
+ Context.
+
+-spec loading_conf_env_file_enabled(map()) -> boolean().
+
+-ifdef(TEST).
+loading_conf_env_file_enabled(_) ->
+ persistent_term:get({?MODULE, load_conf_env_file}, true).
+-else.
+loading_conf_env_file_enabled(_) ->
+ %% When this module is built without `TEST` defined, we want this
+ %% function to always return true. However, this makes Dialyzer
+ %% think it can only return true: this is not the case when the
+ %% module is compiled with `TEST` defined. The following line is
+ %% here to trick Dialyzer.
+ erlang:get({?MODULE, always_undefined}) =:= undefined.
+-endif.
+
+do_load_conf_env_file(#{os_type := {unix, _}} = Context, Sh, ConfEnvFile) ->
+ rabbit_log_prelaunch:debug(
+ "Sourcing $RABBITMQ_CONF_ENV_FILE: ~ts", [ConfEnvFile]),
+
+ %% The script below sources the `CONF_ENV_FILE` file, then it shows a
+ %% marker line and all environment variables.
+ %%
+ %% The marker line is useful to distinguish any output from the sourced
+ %% script from the variables we are interested in.
+ Marker = vars_list_marker(),
+ Script = rabbit_misc:format(
+ ". \"~ts\" && "
+ "echo \"~s\" && "
+ "set", [ConfEnvFile, Marker]),
+
+ #{sys_prefix := SysPrefix,
+ rabbitmq_home := RabbitmqHome} = Context,
+ MainConfigFile = re:replace(
+ get_default_main_config_file(Context),
+ "\\.(conf|config)$", "", [{return, list}]),
+
+ %% The variables below are those the `CONF_ENV_FILE` file can expect.
+ Env = [
+ {"SYS_PREFIX", SysPrefix},
+ {"RABBITMQ_HOME", RabbitmqHome},
+ {"CONFIG_FILE", MainConfigFile},
+ {"ADVANCED_CONFIG_FILE", get_default_advanced_config_file(Context)},
+ {"MNESIA_BASE", get_default_mnesia_base_dir(Context)},
+ {"ENABLED_PLUGINS_FILE", get_default_enabled_plugins_file(Context)},
+ {"PLUGINS_DIR", get_default_plugins_path_from_env(Context)},
+ {"CONF_ENV_FILE_PHASE", "rabbtimq-prelaunch"}
+ ],
+
+ Args = ["-ex", "-c", Script],
+ Opts = [{args, Args},
+ {env, Env},
+ binary,
+ use_stdio,
+ stderr_to_stdout,
+ exit_status],
+ Port = erlang:open_port({spawn_executable, Sh}, Opts),
+ collect_conf_env_file_output(Context, Port, Marker, <<>>);
+do_load_conf_env_file(#{os_type := {win32, _}} = Context, Cmd, ConfEnvFile) ->
+ %% rabbitmq/rabbitmq-common#392
+ rabbit_log_prelaunch:debug(
+ "Executing $RABBITMQ_CONF_ENV_FILE: ~ts", [ConfEnvFile]),
+
+ %% The script below executes the `CONF_ENV_FILE` file, then it shows a
+ %% marker line and all environment variables.
+ %%
+ %% The marker line is useful to distinguish any output from the sourced
+ %% script from the variables we are interested in.
+ %%
+ %% Arguments are split into a list of strings to support a filename with
+ %% whitespaces in the path.
+ Marker = vars_list_marker(),
+ Script = [ConfEnvFile, "&&",
+ "echo", Marker, "&&",
+ "set"],
+
+ #{rabbitmq_base := RabbitmqBase,
+ rabbitmq_home := RabbitmqHome} = Context,
+ MainConfigFile = re:replace(
+ get_default_main_config_file(Context),
+ "\\.(conf|config)$", "", [{return, list}]),
+
+ %% The variables below are those the `CONF_ENV_FILE` file can expect.
+ Env = [
+ {"RABBITMQ_BASE", RabbitmqBase},
+ {"RABBITMQ_HOME", RabbitmqHome},
+ {"CONFIG_FILE", MainConfigFile},
+ {"ADVANCED_CONFIG_FILE", get_default_advanced_config_file(Context)},
+ {"MNESIA_BASE", get_default_mnesia_base_dir(Context)},
+ {"ENABLED_PLUGINS_FILE", get_default_enabled_plugins_file(Context)},
+ {"PLUGINS_DIR", get_default_plugins_path_from_env(Context)},
+ {"CONF_ENV_FILE_PHASE", "rabbtimq-prelaunch"}
+ ],
+
+ Args = ["/Q", "/C" | Script],
+ Opts = [{args, Args},
+ {env, Env},
+ hide,
+ binary,
+ stderr_to_stdout,
+ exit_status],
+ Port = erlang:open_port({spawn_executable, Cmd}, Opts),
+ collect_conf_env_file_output(Context, Port, "\"" ++ Marker ++ "\" ", <<>>).
+
+vars_list_marker() ->
+ rabbit_misc:format(
+ "-----BEGIN VARS LIST FOR PID ~s-----", [os:getpid()]).
+
+collect_conf_env_file_output(Context, Port, Marker, Output) ->
+ receive
+ {Port, {exit_status, ExitStatus}} ->
+ Lines = post_port_cmd_output(Context, Output, ExitStatus),
+ case ExitStatus of
+ 0 -> parse_conf_env_file_output(Context, Marker, Lines);
+ _ -> Context
+ end;
+ {Port, {data, Chunk}} ->
+ collect_conf_env_file_output(
+ Context, Port, Marker, [Output, Chunk])
+ end.
+
+post_port_cmd_output(#{os_type := {OSType, _}}, Output, ExitStatus) ->
+ rabbit_log_prelaunch:debug(
+ "$RABBITMQ_CONF_ENV_FILE exit status: ~b",
+ [ExitStatus]),
+ DecodedOutput = unicode:characters_to_list(Output),
+ LineSep = case OSType of
+ win32 -> "\r\n";
+ _ -> "\n"
+ end,
+ Lines = string:split(string:trim(DecodedOutput), LineSep, all),
+ rabbit_log_prelaunch:debug("$RABBITMQ_CONF_ENV_FILE output:"),
+ [rabbit_log_prelaunch:debug(" ~ts", [Line]) || Line <- Lines],
+ Lines.
+
+parse_conf_env_file_output(Context, _, []) ->
+ Context;
+parse_conf_env_file_output(Context, Marker, [Marker | Lines]) ->
+ %% Found our marker, let's parse variables.
+ parse_conf_env_file_output1(Context, Lines);
+parse_conf_env_file_output(Context, Marker, [_ | Lines]) ->
+ parse_conf_env_file_output(Context, Marker, Lines).
+
+parse_conf_env_file_output1(Context, Lines) ->
+ Vars = parse_conf_env_file_output2(Lines, #{}),
+ %% Re-export variables.
+ lists:foreach(
+ fun(Var) ->
+ IsUsed = var_is_used(Var),
+ IsSet = var_is_set(Var),
+ case IsUsed andalso not IsSet of
+ true ->
+ rabbit_log_prelaunch:debug(
+ "$RABBITMQ_CONF_ENV_FILE: re-exporting variable $~s",
+ [Var]),
+ os:putenv(Var, maps:get(Var, Vars));
+ false ->
+ ok
+ end
+ end, lists:sort(maps:keys(Vars))),
+ Context.
+
+parse_conf_env_file_output2([], Vars) ->
+ Vars;
+parse_conf_env_file_output2([Line | Lines], Vars) ->
+ SetXOutput = is_sh_set_x_output(Line),
+ ShFunction = is_sh_function(Line, Lines),
+ if
+ SetXOutput ->
+ parse_conf_env_file_output2(Lines, Vars);
+ ShFunction ->
+ skip_sh_function(Lines, Vars);
+ true ->
+ case string:split(Line, "=") of
+ [Var, IncompleteValue] ->
+ {Value, Lines1} = parse_sh_literal(IncompleteValue, Lines, ""),
+ Vars1 = Vars#{Var => Value},
+ parse_conf_env_file_output2(Lines1, Vars1);
+ _ ->
+ %% Parsing failed somehow.
+ rabbit_log_prelaunch:warning(
+ "Failed to parse $RABBITMQ_CONF_ENV_FILE output: ~p",
+ [Line]),
+ #{}
+ end
+ end.
+
+is_sh_set_x_output(Line) ->
+ re:run(Line, "^\\++ ", [{capture, none}]) =:= match.
+
+is_sh_function(_, []) ->
+ false;
+is_sh_function(Line, Lines) ->
+ re:run(Line, "\\s\\(\\)\\s*$", [{capture, none}]) =:= match
+ andalso
+ re:run(hd(Lines), "^\\s*\\{\\s*$", [{capture, none}]) =:= match.
+
+parse_sh_literal("'" ++ SingleQuoted, Lines, Literal) ->
+ parse_single_quoted_literal(SingleQuoted, Lines, Literal);
+parse_sh_literal("\"" ++ DoubleQuoted, Lines, Literal) ->
+ parse_double_quoted_literal(DoubleQuoted, Lines, Literal);
+parse_sh_literal("$'" ++ DollarSingleQuoted, Lines, Literal) ->
+ parse_dollar_single_quoted_literal(DollarSingleQuoted, Lines, Literal);
+parse_sh_literal(Unquoted, Lines, Literal) ->
+ {lists:reverse(Literal) ++ Unquoted, Lines}.
+
+parse_single_quoted_literal([$' | Rest], Lines, Literal) ->
+ %% We reached the closing single quote.
+ parse_sh_literal(Rest, Lines, Literal);
+parse_single_quoted_literal([], [Line | Lines], Literal) ->
+ %% We reached the end of line before finding the closing single
+ %% quote. The literal continues on the next line and includes that
+ %% newline character.
+ parse_single_quoted_literal(Line, Lines, [$\n | Literal]);
+parse_single_quoted_literal([C | Rest], Lines, Literal) ->
+ parse_single_quoted_literal(Rest, Lines, [C | Literal]).
+
+parse_double_quoted_literal([$" | Rest], Lines, Literal) ->
+ %% We reached the closing double quote.
+ parse_sh_literal(Rest, Lines, Literal);
+parse_double_quoted_literal([], [Line | Lines], Literal) ->
+ %% We reached the end of line before finding the closing double
+ %% quote. The literal continues on the next line and includes that
+ %% newline character.
+ parse_double_quoted_literal(Line, Lines, [$\n | Literal]);
+parse_double_quoted_literal([C | Rest], Lines, Literal) ->
+ parse_double_quoted_literal(Rest, Lines, [C | Literal]).
+
+parse_dollar_single_quoted_literal([$'], Lines, Literal) ->
+ %% We reached the closing single quote.
+ {lists:reverse(Literal), Lines};
+parse_dollar_single_quoted_literal([], [Line | Lines], Literal) ->
+ %% We reached the end of line before finding the closing single
+ %% quote. The literal continues on the next line and includes that
+ %% newline character.
+ parse_dollar_single_quoted_literal(Line, Lines, [$\n | Literal]);
+parse_dollar_single_quoted_literal([C | Rest], Lines, Literal) ->
+ parse_dollar_single_quoted_literal(Rest, Lines, [C | Literal]).
+
+skip_sh_function(["}" | Lines], Vars) ->
+ parse_conf_env_file_output2(Lines, Vars);
+skip_sh_function([_ | Lines], Vars) ->
+ skip_sh_function(Lines, Vars).
+
+%% -------------------------------------------------------------------
+%% Helpers.
+%% -------------------------------------------------------------------
+
+get_env_var(VarName) ->
+ get_env_var(VarName, []).
+
+get_env_var(VarName, Options) ->
+ KeepEmptyString = lists:member(keep_empty_string_as_is, Options),
+ case os:getenv(VarName) of
+ false -> false;
+ "" when not KeepEmptyString -> false;
+ Value -> Value
+ end.
+
+get_prefixed_env_var(VarName) ->
+ get_prefixed_env_var(VarName, []).
+
+get_prefixed_env_var("RABBITMQ_" ++ Suffix = VarName,
+ Options) ->
+ case get_env_var(VarName, Options) of
+ false -> get_env_var(Suffix, Options);
+ Value -> Value
+ end.
+
+var_is_used("RABBITMQ_" ++ _ = PrefixedVar) ->
+ lists:member(PrefixedVar, ?USED_ENV_VARS);
+var_is_used("HOME") ->
+ false;
+var_is_used(Var) ->
+ lists:member("RABBITMQ_" ++ Var, ?USED_ENV_VARS).
+
+%% The $RABBITMQ_* variables have precedence over their un-prefixed equivalent.
+%% Therefore, when we check if $RABBITMQ_* is set, we only look at this
+%% variable. However, when we check if an un-prefixed variable is set, we first
+%% look at its $RABBITMQ_* variant.
+var_is_set("RABBITMQ_" ++ _ = PrefixedVar) ->
+ os:getenv(PrefixedVar) /= false;
+var_is_set(Var) ->
+ os:getenv("RABBITMQ_" ++ Var) /= false orelse
+ os:getenv(Var) /= false.
+
+value_is_yes(Value) when is_list(Value) orelse is_binary(Value) ->
+ Options = [{capture, none}, caseless],
+ re:run(string:trim(Value), "^(1|yes|true)$", Options) =:= match;
+value_is_yes(_) ->
+ false.
+
+normalize_path("" = Path) ->
+ Path;
+normalize_path(Path) ->
+ filename:join(filename:split(Path)).
+
+this_module_dir() ->
+ File = code:which(?MODULE),
+ %% Possible locations:
+ %% - the rabbit_common plugin (as an .ez archive):
+ %% .../plugins/rabbit_common-$version.ez/rabbit_common-$version/ebin
+ %% - the rabbit_common plugin (as a directory):
+ %% .../plugins/rabbit_common-$version/ebin
+ %% - the CLI:
+ %% .../escript/$cli
+ filename:dirname(File).
+
+maybe_setup_dist_for_remote_query(
+ #{from_remote_node := offline} = Context) ->
+ Context;
+maybe_setup_dist_for_remote_query(
+ #{from_remote_node := {RemoteNode, _}} = Context) ->
+ {NamePart, HostPart} = rabbit_nodes_common:parts(RemoteNode),
+ NameType = rabbit_nodes_common:name_type(RemoteNode),
+ ok = rabbit_nodes_common:ensure_epmd(),
+ Context1 = setup_dist_for_remote_query(
+ Context, NamePart, HostPart, NameType, 50),
+ case is_rabbitmq_loaded_on_remote_node(Context1) of
+ true -> Context1;
+ false -> maybe_stop_dist_for_remote_query(
+ update_context(Context, from_remote_node, offline))
+ end;
+maybe_setup_dist_for_remote_query(Context) ->
+ Context.
+
+setup_dist_for_remote_query(
+ #{dist_started_for_remote_query := true} = Context,
+ _, _, _, _) ->
+ Context;
+setup_dist_for_remote_query(Context, _, _, _, 0) ->
+ Context;
+setup_dist_for_remote_query(#{from_remote_node := {Remote, _}} = Context,
+ NamePart, HostPart, NameType,
+ Attempts) ->
+ RndNamePart = NamePart ++ "_ctl_" ++ integer_to_list(rand:uniform(100)),
+ Nodename = rabbit_nodes_common:make({RndNamePart, HostPart}),
+ case net_kernel:start([Nodename, NameType]) of
+ {ok, _} ->
+ update_context(Context, dist_started_for_remote_query, true);
+ {error, {already_started, _}} ->
+ Context;
+ {error, {{already_started, _}, _}} ->
+ Context;
+ Error ->
+ logger:error(
+ "rabbit_env: Failed to setup distribution (as ~s) to "
+ "query node ~s: ~p",
+ [Nodename, Remote, Error]),
+ setup_dist_for_remote_query(Context,
+ NamePart, HostPart, NameType,
+ Attempts - 1)
+ end.
+
+is_rabbitmq_loaded_on_remote_node(
+ #{from_remote_node := Remote}) ->
+ case query_remote(Remote, application, loaded_applications, []) of
+ {ok, Apps} ->
+ lists:keymember(mnesia, 1, Apps) andalso
+ lists:keymember(rabbit, 1, Apps);
+ _ ->
+ false
+ end.
+
+maybe_stop_dist_for_remote_query(
+ #{dist_started_for_remote_query := true} = Context) ->
+ net_kernel:stop(),
+ maps:remove(dist_started_for_remote_query, Context);
+maybe_stop_dist_for_remote_query(Context) ->
+ Context.
+
+query_remote({RemoteNode, Timeout}, Mod, Func, Args) ->
+ Ret = rpc:call(RemoteNode, Mod, Func, Args, Timeout),
+ case Ret of
+ {badrpc, nodedown} = Error -> Error;
+ {badrpc, _} = Error -> throw({query, RemoteNode, Error});
+ _ -> {ok, Ret}
+ end.