diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/command_modules.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/command_modules.ex index 1eec4fb818..1e6ce08807 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/command_modules.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/command_modules.ex @@ -20,12 +20,20 @@ alias RabbitMQ.CLI.Core.Helpers, as: Helpers defmodule RabbitMQ.CLI.Core.CommandModules do @commands_ns ~r/RabbitMQ.CLI.(.*).Commands/ - def module_map do - Application.get_env(:rabbitmqctl, :commands) || load(%{}) + def module_map(opts \\ %{}) do + Application.get_env(:rabbitmqctl, :commands) || load(opts) end - def is_command?([head | _]), do: is_command?(head) - def is_command?(str), do: module_map()[str] != nil + def module_map_core(opts \\ %{}) do + Application.get_env(:rabbitmqctl, :commands_core) || load_core(opts) + end + + def load_core(opts) do + scope = script_scope(opts) + commands = load_commands_core(scope) + Application.put_env(:rabbitmqctl, :commands_core, commands) + commands + end def load(opts) do scope = script_scope(opts) @@ -39,8 +47,47 @@ defmodule RabbitMQ.CLI.Core.CommandModules do scopes[Config.get_option(:script_name, opts)] || :none end + def load_commands_core(scope) do + make_module_map(ctl_modules(), scope) + end + def load_commands(scope, opts) do - ctl_and_plugin_modules(opts) + make_module_map(plugin_modules(opts) ++ ctl_modules(), scope) + end + + def ctl_modules() do + Application.spec(:rabbitmqctl, :modules) + end + + def plugin_modules(opts) do + Helpers.require_rabbit(opts) + + partitioned = + Enum.group_by(PluginsHelpers.read_enabled(opts), fn(app) -> + case Application.load(app) do + :ok -> :loaded; + {:error, {:already_loaded, ^app}} -> :loaded; + _ -> :not_found + end + end) + + loaded = partitioned[:loaded] || [] + missing = partitioned[:not_found] || [] + ## If plugins are not in ERL_LIBS, they should be loaded from plugins_dir + case missing do + [] -> :ok; + _ -> + Helpers.add_plugins_to_load_path(opts) + Enum.each(missing, fn(app) -> Application.load(app) end) + end + + Enum.flat_map(loaded ++ missing, fn(app) -> + Application.spec(app, :modules) || [] + end) + end + + defp make_module_map(modules, scope) do + modules |> Enum.filter(fn(mod) -> to_string(mod) =~ @commands_ns and @@ -54,13 +101,6 @@ defmodule RabbitMQ.CLI.Core.CommandModules do |> Map.new end - def ctl_and_plugin_modules(opts) do - Helpers.require_rabbit_and_plugins(opts) - enabled_plugins = PluginsHelpers.read_enabled(opts) - [:rabbitmqctl | enabled_plugins] - |> Enum.flat_map(fn(app) -> Application.spec(app, :modules) || [] end) - end - defp module_exists?(nil) do false end diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/distribution.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/distribution.ex index 23b24f92d1..2bac8f1f52 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/distribution.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/distribution.ex @@ -26,14 +26,25 @@ defmodule RabbitMQ.CLI.Core.Distribution do def start(options) do node_name_type = Config.get_option(:longnames, options) - :rabbit_nodes.ensure_epmd() start(node_name_type, 10, :undefined) end - def start_as(node_name, opts) do - :rabbit_nodes.ensure_epmd() - node_name_type = Config.get_option(:longnames, opts) - :net_kernel.start([node_name, node_name_type]) + def start_as(node_name, options) do + node_name_type = Config.get_option(:longnames, options) + start_with_epmd(node_name, node_name_type) + end + + ## Optimization. We try to start EPMD only if distribution fails + def start_with_epmd(node_name, node_name_type) do + case :net_kernel.start([node_name, node_name_type]) do + {:ok, _} = ok -> ok; + {:error, {:already_started, _}} = started -> started; + {:error, {{:already_started, _}, _}} = started -> started; + ## EPMD can be stopped. Retry with EPMD + {:error, _} -> + :rabbit_nodes.ensure_epmd() + :net_kernel.start([node_name, node_name_type]) + end end # @@ -46,7 +57,7 @@ defmodule RabbitMQ.CLI.Core.Distribution do defp start(node_name_type, attempts, _last_err) do candidate = generate_cli_node_name(node_name_type) - case :net_kernel.start([candidate, node_name_type]) do + case start_with_epmd(candidate, node_name_type) do {:ok, _} -> :ok {:error, {:already_started, pid}} -> {:ok, pid}; {:error, {{:already_started, pid}, _}} -> {:ok, pid}; diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/helpers.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/helpers.ex index 09291dc4d6..612b2e43c2 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/helpers.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/helpers.ex @@ -97,17 +97,13 @@ defmodule RabbitMQ.CLI.Core.Helpers do end end - def require_rabbit(opts) do - try_load_rabbit_code(opts) - end - def require_rabbit_and_plugins(opts) do - with :ok <- try_load_rabbit_code(opts), - :ok <- try_load_rabbit_plugins(opts), + with :ok <- require_rabbit(opts), + :ok <- add_plugins_to_load_path(opts), do: :ok end - defp try_load_rabbit_code(opts) do + def require_rabbit(opts) do home = Config.get_option(:rabbitmq_home, opts) case home do nil -> @@ -128,50 +124,56 @@ defmodule RabbitMQ.CLI.Core.Helpers do end end - defp try_load_rabbit_plugins(opts) do + def add_plugins_to_load_path(opts) do with {:ok, plugins_dir} <- plugins_dir(opts) do String.split(to_string(plugins_dir), separator()) |> - Enum.map(&try_load_plugins_from_directory/1) + Enum.map(&add_directory_plugins_to_load_path/1) :ok end end - defp try_load_plugins_from_directory(directory_with_plugins_inside_it) do + def add_directory_plugins_to_load_path(directory_with_plugins_inside_it) do with {:ok, files} <- File.ls(directory_with_plugins_inside_it) do - Enum.filter_map(files, - fn(filename) -> String.ends_with?(filename, [".ez"]) end, - fn(archive) -> - ## Check that the .app file is present and take the app name from there - {:ok, ez_files} = :zip.list_dir(String.to_charlist(Path.join([directory_with_plugins_inside_it, archive]))) - case find_dot_app(ez_files) do - :not_found -> :ok - dot_app -> - app_name = Path.basename(dot_app, ".app") - ebin_dir = Path.join([directory_with_plugins_inside_it, Path.dirname(dot_app)]) - ebin_dir |> Code.append_path() - app_name |> String.to_atom() |> Application.load() - end - end) + Enum.map(files, + fn(filename) -> + cond do + String.ends_with?(filename, [".ez"]) -> + Path.join([directory_with_plugins_inside_it, filename]) + |> String.to_charlist + |> add_archive_code_path(); + File.dir?(filename) -> + Path.join([directory_with_plugins_inside_it, filename]) + |> add_dir_code_path(); + true -> + {:error, {:not_a_plugin, filename}} + end + end) end end - defp find_dot_app([head | tail]) when Record.is_record(head, :zip_file) do - name = :erlang.element(2, head) - case Regex.match?(~r/(.+)\/ebin\/(.+)\.app$/, to_string name) do - true -> - name - false -> - find_dot_app(tail) + defp add_archive_code_path(ez_dir) do + case :erl_prim_loader.list_dir(ez_dir) do + {:ok, [app_dir]} -> + app_in_ez = :filename.join(ez_dir, app_dir) + add_dir_code_path(app_in_ez); + _ -> {:error, :no_app_dir} end end - defp find_dot_app([_head | tail]) do - find_dot_app(tail) - end - defp find_dot_app([]) do - :not_found + + defp add_dir_code_path(app_dir) do + case :erl_prim_loader.list_dir(app_dir) do + {:ok, list} -> + case Enum.member?(list, 'ebin') do + true -> + ebin_dir = :filename.join(app_dir, 'ebin') + Code.append_path(ebin_dir) + false -> {:error, :no_ebin} + end; + _ -> {:error, :app_dir_empty} + end end def require_mnesia_dir(opts) do diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/parser.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/parser.ex index ef53a469f8..0527e50c72 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/parser.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/parser.ex @@ -30,8 +30,6 @@ defmodule RabbitMQ.CLI.Core.Parser do def parse(input) do {parsed_args, options, invalid} = parse_global(input) - CommandModules.load(options) - {command_name, command_module, arguments} = look_up_command(parsed_args, options) case command_module do @@ -53,10 +51,20 @@ defmodule RabbitMQ.CLI.Core.Parser do defp look_up_command(parsed_args, options) do case parsed_args do [cmd_name | arguments] -> - module_map = CommandModules.module_map - command = module_map[cmd_name] || - command_alias(cmd_name, module_map, options) || - command_suggestion(cmd_name, module_map) + ## This is an optimisation for pluggable command discovery. + ## Most of the time a command will be from rabbitmqctl application + ## so there is not point in scanning plugins for potential commands + CommandModules.load_core(options) + core_commands = CommandModules.module_map_core + command = case core_commands[cmd_name] do + nil -> + CommandModules.load(options) + module_map = CommandModules.module_map + module_map[cmd_name] || + command_alias(cmd_name, module_map, options) || + command_suggestion(cmd_name, module_map); + c -> c + end {cmd_name, command, arguments} [] -> {"", nil, []} diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/help_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/help_command.ex index 16a617d388..e4e6cbaa7f 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/help_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/help_command.ex @@ -28,14 +28,14 @@ defmodule RabbitMQ.CLI.Ctl.Commands.HelpCommand do def scopes(), do: [:ctl, :diagnostics, :plugins] def run([command_name], opts) do - case CommandModules.is_command?(command_name) do - true -> - command = CommandModules.module_map[command_name] + CommandModules.load(opts) + case CommandModules.module_map[command_name] do + nil -> + all_usage(opts); + command -> Enum.join([base_usage(command, opts)] ++ options_usage() ++ - input_types(command), "\n"); - false -> - all_usage(opts) + input_types(command), "\n") end end def run(_, opts) do @@ -51,6 +51,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.HelpCommand do end def all_usage(opts) do + CommandModules.load(opts) Enum.join(tool_usage(program_name(opts)) ++ options_usage() ++ commands() ++ diff --git a/deps/rabbitmq_cli/lib/rabbitmqctl.ex b/deps/rabbitmq_cli/lib/rabbitmqctl.ex index 05843fb461..708fdcb959 100644 --- a/deps/rabbitmq_cli/lib/rabbitmqctl.ex +++ b/deps/rabbitmq_cli/lib/rabbitmqctl.ex @@ -35,8 +35,7 @@ defmodule RabbitMQCtl do auto_complete(script_basename, args) end def main(unparsed_command) do - unparsed_command - |> exec_command(fn(command, output, options) -> + exec_command(unparsed_command, fn(command, output, options) -> formatter = get_formatter(command, options) printer = get_printer(options) diff --git a/deps/rabbitmq_cli/test/command_modules_test.exs b/deps/rabbitmq_cli/test/command_modules_test.exs index ec85c0265f..c2144cda34 100644 --- a/deps/rabbitmq_cli/test/command_modules_test.exs +++ b/deps/rabbitmq_cli/test/command_modules_test.exs @@ -110,45 +110,6 @@ defmodule CommandModulesTest do end - ## ------------------- is_command?/1 tests -------------------- - - test "a valid implemented command returns true" do - set_scope(:ctl) - @subject.load(%{}) - assert @subject.is_command?("status") == true - end - - test "an invalid command returns false" do - set_scope(:ctl) - @subject.load(%{}) - assert @subject.is_command?("quack") == false - end - - test "a nil returns false" do - set_scope(:ctl) - @subject.load(%{}) - assert @subject.is_command?(nil) == false - end - - test "an empty array returns false" do - set_scope(:ctl) - @subject.load(%{}) - assert @subject.is_command?([]) == false - end - - test "an non-empty array tests the first element" do - set_scope(:ctl) - @subject.load(%{}) - assert @subject.is_command?(["status", "quack"]) == true - assert @subject.is_command?(["quack", "status"]) == false - end - - test "a non-string list returns false" do - set_scope(:ctl) - @subject.load(%{}) - assert @subject.is_command?([{"status", "quack"}, {4, "Fantastic"}]) == false - end - ## ------------------- commands/0 tests -------------------- test "command_modules has existing commands" do diff --git a/deps/rabbitmq_cli/test/core/helpers_test.exs b/deps/rabbitmq_cli/test/core/helpers_test.exs index f96208fda9..8e34f9d5bb 100644 --- a/deps/rabbitmq_cli/test/core/helpers_test.exs +++ b/deps/rabbitmq_cli/test/core/helpers_test.exs @@ -118,23 +118,25 @@ test "RabbitMQ hostname is properly formed" do ## ------------------- require_rabbit/1 tests -------------------- - test "load plugin with version number in filename" do + test "locate plugin with version number in filename" do plugins_directory_03 = fixture_plugins_path("plugins-subdirectory-03") rabbitmq_home = :rabbit_misc.rpc_call(node(), :code, :lib_dir, [:rabbit]) opts = %{plugins_dir: to_string(plugins_directory_03), rabbitmq_home: rabbitmq_home} assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_03, 'New project', '0.1.0'}) == false @subject.require_rabbit_and_plugins(opts) + Application.load(:mock_rabbitmq_plugins_03) assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_03, 'New project', '0.1.0'}) end - test "load plugin without version number in filename" do + test "locate plugin without version number in filename" do plugins_directory_04 = fixture_plugins_path("plugins-subdirectory-04") rabbitmq_home = :rabbit_misc.rpc_call(node(), :code, :lib_dir, [:rabbit]) opts = %{plugins_dir: to_string(plugins_directory_04), rabbitmq_home: rabbitmq_home} assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_04, 'New project', 'rolling'}) == false @subject.require_rabbit_and_plugins(opts) + Application.load(:mock_rabbitmq_plugins_04) assert Enum.member?(Application.loaded_applications(), {:mock_rabbitmq_plugins_04, 'New project', 'rolling'}) end