diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 1a56e55d5ac3c..db6fa588f5473 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -76,6 +76,11 @@ TARGET_LINK_LIBRARIES(mariadb-show ${CLIENT_LIB}) MYSQL_ADD_EXECUTABLE(mariadb-plugin mysql_plugin.c) TARGET_LINK_LIBRARIES(mariadb-plugin ${CLIENT_LIB}) +# The tool needs to know how MariaDB was installed to pick the way plugins +# are installed. The install layout is only known when building, so it is +# passed to the tool, together with the directories it has to verify. +SET_TARGET_PROPERTIES(mariadb-plugin PROPERTIES COMPILE_DEFINITIONS + "INSTALL_LAYOUT_${INSTALL_LAYOUT};INSTALL_PLUGINDIR=${INSTALL_PLUGINDIR};INSTALL_BINDIRABS=${INSTALL_BINDIRABS}") MYSQL_ADD_EXECUTABLE(mariadb-binlog mysqlbinlog.cc mysqlbinlog-engine.cc) TARGET_LINK_LIBRARIES(mariadb-binlog ${CLIENT_LIB} mysys_ssl) diff --git a/client/mysql_plugin.c b/client/mysql_plugin.c index 00870fa72e026..a70b1a3eb9945 100644 --- a/client/mysql_plugin.c +++ b/client/mysql_plugin.c @@ -24,6 +24,32 @@ #include #include +#define STR(s) _STR(s) +#define _STR(s) #s + +/* + The build system defines INSTALL_LAYOUT_RPM or INSTALL_LAYOUT_DEB for the + packaged builds, and neither of them for a binary tarball. +*/ +#if defined(INSTALL_LAYOUT_RPM) +#define INSTALL_METHOD_NAME "rpm" +#elif defined(INSTALL_LAYOUT_DEB) +#define INSTALL_METHOD_NAME "deb" +#else +#define INSTALL_METHOD_NAME "tarball" +#endif + +/* + On rpm and deb installations install/uninstall delegate to the system + package manager. Tarball installations manage plugin files themselves, + so none of the delegation code applies (and neither do its unix-only + process primitives). +*/ +#if defined(INSTALL_LAYOUT_RPM) || defined(INSTALL_LAYOUT_DEB) +#define PKG_DELEGATION 1 +#include +#endif + /* Global variables. */ static uint my_end_arg= 0; static uint opt_verbose=0; @@ -87,6 +113,14 @@ static int find_plugin(char *tp_path); static int build_bootstrap_file(char *operation, char *bootstrap); static int dump_bootstrap_file(char *bootstrap_file); static int bootstrap_server(char *server_path, char *bootstrap_file); +static void usage(void); +static int run_new_command(int argc, char **argv); +static int validate_plugin_name(const char *name); +static int is_legacy_syntax(int argc, char **argv); +static int detect_install_method(char *basedir, size_t basedir_size); +static int do_search(const char *name, const char *basedir); +static int do_install(const char *name, const char *basedir); +static int do_uninstall(const char *name, const char *basedir); int main(int argc,char *argv[]) @@ -100,6 +134,20 @@ int main(int argc,char *argv[]) sf_leaking_memory=1; /* don't report memory leaks on early exits */ plugin_data.name= 0; /* initialize name */ + /* + The new package-manager style commands (search|install|uninstall) are + handled in run_new_command(). The legacy " ENABLE|DISABLE" + syntax is recognized by scanning the raw arguments, before any option + parsing, and continues through the original code path below unchanged, + for backward compatibility. + */ + if (!is_legacy_syntax(argc, argv)) + { + error= run_new_command(argc, argv); + my_end(my_end_arg); + exit(error); + } + /* The following operations comprise the method for enabling or disabling a plugin. We begin by processing the command options then check the @@ -416,11 +464,14 @@ static int get_default_values() static void usage(void) { print_version(); - puts("Copyright (c) 2011, 2015, Oracle and/or its affiliates. " - "All rights reserved.\n"); - puts("Enable or disable plugins."); - printf("\nUsage: %s [options] ENABLE|DISABLE\n\nOptions:\n", - my_progname); + puts(ORACLE_WELCOME_COPYRIGHT_NOTICE("2011")); + puts("Manage MariaDB plugins across package managers and binary distributions."); + printf("\nUsage:\n"); + printf(" %s search \n", my_progname); + printf(" %s install \n", my_progname); + printf(" %s uninstall \n\n", my_progname); + printf("Legacy syntax (deprecated, kept for backward compatibility):\n"); + printf(" %s [options] ENABLE|DISABLE\n\nOptions:\n", my_progname); my_print_help(my_long_options); puts("\n"); } @@ -1239,3 +1290,540 @@ static int bootstrap_server(char *server_path, char *bootstrap_file) return error; } + + +/** + Detect the legacy " ENABLE|DISABLE" command line syntax. + + The check is done on the raw arguments, before any option parsing, so + that legacy invocations take the original code path unchanged. + + @param[in] argc The number of arguments. + @param[in] argv The arguments. + + @retval int legacy syntax = 1, new syntax = 0 +*/ + +static int is_legacy_syntax(int argc, char **argv) +{ + int i; + + for (i= 1; i < argc; i++) + { + if (strcasecmp(argv[i], "ENABLE") == 0 || + strcasecmp(argv[i], "DISABLE") == 0) + return 1; + } + return 0; +} + + +/** + Check that a plugin name contains only safe characters. + + The name is later used to construct package names and file paths, so + only lower case alphanumerics, '_' and '-' are accepted. The name is + expected to be normalized to lower case before this check. + + @param[in] name The normalized plugin name. + + @retval int error = 1, success = 0 +*/ + +static int validate_plugin_name(const char *name) +{ + const char *p; + + if (*name == '\0') + { + fprintf(stderr, "ERROR: plugin name cannot be empty.\n"); + return 1; + } + for (p= name; *p; p++) + { + if (!isalnum((unsigned char) *p) && *p != '_' && *p != '-') + { + fprintf(stderr, "ERROR: invalid character '%c' in plugin name. " + "Use only [a-z0-9_-].\n", *p); + return 1; + } + } + return 0; +} + +/** + Verify that the tool is part of the installation it was built for. + + The installation method is known at build time, so only the location has + to be checked. It is taken from argv[0] and not from the server, as one + machine can have several server installations. + + @param[out] basedir The base directory, empty for rpm and deb, + where the package manager owns the files. + @param[in] basedir_size The size of the basedir buffer. + + @retval int error = 1, success = 0 +*/ + +static int detect_install_method(char *basedir, size_t basedir_size) +{ + char self_path[FN_REFLEN], real_path[FN_REFLEN], real_dir[FN_REFLEN]; + size_t length; +#if !defined(INSTALL_LAYOUT_RPM) && !defined(INSTALL_LAYOUT_DEB) + char plugin_dir[FN_REFLEN]; + char *slash; +#endif + + /* + my_path() searches PATH when argv[0] is a bare program name. The path + is resolved afterwards, so that a symbolic link, like the one for the + old mysql_plugin name, does not hide where the tool is installed. + */ + my_path(self_path, my_progname, ""); + safe_strcat(self_path, sizeof(self_path), base_name(my_progname)); + if (my_realpath(real_path, self_path, MYF(0))) + safe_strcpy(real_path, sizeof(real_path), self_path); + + dirname_part(real_dir, real_path, &length); + + length= strlen(real_dir); + while (length > 1 && (real_dir[length - 1] == FN_LIBCHAR || + real_dir[length - 1] == FN_LIBCHAR2)) + real_dir[--length]= '\0'; + +#if defined(INSTALL_LAYOUT_RPM) || defined(INSTALL_LAYOUT_DEB) + if (strcmp(real_dir, STR(INSTALL_BINDIRABS)) != 0) + { + fprintf(stderr, "ERROR: this is a %s build, but it runs from '%s' " + "instead of '%s', so it is not part of a %s installation.\n", + INSTALL_METHOD_NAME, real_dir, STR(INSTALL_BINDIRABS), + INSTALL_METHOD_NAME); + return 1; + } + basedir[0]= '\0'; +#else + /* The base directory is one level above the directory of the tool. */ + safe_strcpy(basedir, basedir_size, real_dir); + slash= strrchr(basedir, FN_LIBCHAR); + if (!slash) + slash= strrchr(basedir, FN_LIBCHAR2); + if (!slash) + { + fprintf(stderr, "ERROR: cannot determine the MariaDB base directory " + "from '%s'.\n", real_dir); + return 1; + } + *slash= '\0'; + + safe_strcpy(plugin_dir, sizeof(plugin_dir), basedir); + safe_strcat(plugin_dir, sizeof(plugin_dir), "/" STR(INSTALL_PLUGINDIR)); + if (!file_exists(plugin_dir)) + { + fprintf(stderr, "ERROR: '%s' does not look like a MariaDB installation, " + "'%s' not found.\n", basedir, plugin_dir); + return 1; + } +#endif + return 0; +} + + +#ifdef PKG_DELEGATION + +/** + Pick the package manager to delegate to. + + On deb installations it is always apt-get (the script-stable interface, + unlike apt). On rpm installations dnf and zypper manage the same rpm + database, so whichever is present is usable; dnf is tried first. + + @retval const char* the program name, or NULL with an error printed +*/ + +static const char *get_package_manager(void) +{ +#if defined(INSTALL_LAYOUT_DEB) + return "apt-get"; +#else + char dir[FN_REFLEN]; + + if (find_file_in_path(dir, "dnf")) + return "dnf"; + if (find_file_in_path(dir, "zypper")) + return "zypper"; + fprintf(stderr, "ERROR: no package manager found: neither dnf nor zypper " + "is in PATH.\n"); + return NULL; +#endif +} + + +/** + Refuse to continue without root privileges. + + The package manager would fail anyway, but only after a repository + refresh, with an error that does not mention this tool. + + @param[in] verb The command name, for the error message. + + @retval int error = 1, success = 0 +*/ + +static int check_root(const char *verb) +{ + if (geteuid() != 0) + { + fprintf(stderr, "ERROR: '%s' requires root privileges. " + "Run as root or with sudo.\n", verb); + return 1; + } + return 0; +} + + +/** + Run a command and wait for it to finish. + + The command is executed directly, not through a shell, so the arguments + cannot be reinterpreted. The child inherits the standard streams: the + package manager talks to the user directly, including its own + confirmation prompts and progress output. + + @param[in] cmd_argv NULL-terminated argument vector. + + @retval int the command exit code, 127 if it could not be run +*/ + +static int run_argv(char **cmd_argv) +{ + pid_t pid; + int status; + + fflush(stdout); + fflush(stderr); + if ((pid= fork()) < 0) + { + fprintf(stderr, "ERROR: cannot fork: %s.\n", strerror(errno)); + return 127; + } + if (pid == 0) + { + execvp(cmd_argv[0], cmd_argv); + fprintf(stderr, "ERROR: cannot run '%s': %s.\n", cmd_argv[0], + strerror(errno)); + _exit(127); + } + while (waitpid(pid, &status, 0) < 0) + { + if (errno != EINTR) + { + fprintf(stderr, "ERROR: cannot wait for '%s': %s.\n", cmd_argv[0], + strerror(errno)); + return 127; + } + } + if (WIFSIGNALED(status)) + { + fprintf(stderr, "ERROR: '%s' was terminated by signal %d.\n", + cmd_argv[0], WTERMSIG(status)); + return 127; + } + return WEXITSTATUS(status); +} + + +#ifdef INSTALL_LAYOUT_RPM +/** + Run a command and capture its standard output. + + Standard error stays on the terminal. Output beyond the buffer size is + discarded. + + @param[in] cmd_argv NULL-terminated argument vector. + @param[out] out Buffer for the output, always zero-terminated. + @param[in] out_size Size of the buffer. + + @retval int the command exit code, 127 if it could not be run +*/ + +static int run_argv_capture(char **cmd_argv, char *out, size_t out_size) +{ + int fds[2]; + pid_t pid; + int status; + ssize_t n; + size_t len= 0; + + if (pipe(fds)) + { + fprintf(stderr, "ERROR: cannot create a pipe: %s.\n", strerror(errno)); + return 127; + } + fflush(stdout); + fflush(stderr); + if ((pid= fork()) < 0) + { + fprintf(stderr, "ERROR: cannot fork: %s.\n", strerror(errno)); + close(fds[0]); + close(fds[1]); + return 127; + } + if (pid == 0) + { + dup2(fds[1], STDOUT_FILENO); + close(fds[0]); + close(fds[1]); + execvp(cmd_argv[0], cmd_argv); + fprintf(stderr, "ERROR: cannot run '%s': %s.\n", cmd_argv[0], + strerror(errno)); + _exit(127); + } + close(fds[1]); + while ((n= read(fds[0], out + len, out_size - 1 - len))) + { + if (n < 0) + { + if (errno == EINTR) + continue; + break; + } + if ((len+= n) == out_size - 1) + break; + } + out[len]= '\0'; + close(fds[0]); + while (waitpid(pid, &status, 0) < 0) + { + if (errno != EINTR) + { + fprintf(stderr, "ERROR: cannot wait for '%s': %s.\n", cmd_argv[0], + strerror(errno)); + return 127; + } + } + if (WIFSIGNALED(status)) + return 127; + return WEXITSTATUS(status); +} +#endif /* INSTALL_LAYOUT_RPM */ + + +/** + Build the distribution-independent package name (D2): mariadb-plugin- + followed by the plugin name, which is already validated and lowercased. + + @param[out] to Buffer for the package name. + @param[in] size Size of the buffer. + @param[in] name The normalized plugin name. +*/ + +static void build_package_name(char *to, size_t size, const char *name) +{ + safe_strcpy(to, size, "mariadb-plugin-"); + safe_strcat(to, size, name); +} + +#endif /* PKG_DELEGATION */ + + +/** + Search for a plugin. Stub, not implemented yet. + + @param[in] name The normalized plugin name. + @param[in] basedir The base directory, empty for packaged installations. + + @retval int error = 1, success = 0 +*/ + +static int do_search(const char *name, const char *basedir) +{ + printf("search: '%s' (%s installation%s%s) not implemented yet\n", name, + INSTALL_METHOD_NAME, *basedir ? ", basedir=" : "", basedir); + return 0; +} + + +/** + Install a plugin. + + On rpm and deb installations the work is delegated to the system package + manager, which resolves the uniform package name through its own real + package names (via Provides on rpm). Its exit code is passed through. + + @param[in] name The normalized plugin name. + @param[in] basedir The base directory, empty for packaged installations. + + @retval int error = nonzero, success = 0 +*/ + +static int do_install(const char *name, const char *basedir) +{ +#ifdef PKG_DELEGATION + char package[NAME_CHAR_LEN + 16]; + const char *pm; + char *cmd_argv[4]; + + if (check_root("install")) + return 1; + if (!(pm= get_package_manager())) + return 1; + + build_package_name(package, sizeof(package), name); + cmd_argv[0]= (char *) pm; + cmd_argv[1]= (char *) "install"; + cmd_argv[2]= package; + cmd_argv[3]= 0; + return run_argv(cmd_argv); +#else + printf("install: '%s' (%s installation%s%s) not implemented yet\n", name, + INSTALL_METHOD_NAME, *basedir ? ", basedir=" : "", basedir); + return 0; +#endif +} + + +/** + Uninstall a plugin. + + On deb installations the packages carry the uniform name, so it is passed + to apt-get directly. On rpm installations the uniform name is only a + Provides alias of the real package name, and dnf 5 does not resolve + "remove" arguments through Provides (dnf 4 and zypper do), so the alias + is first translated by querying the rpm database. This also gives a + clear error when the plugin is not installed. + + @param[in] name The normalized plugin name. + @param[in] basedir The base directory, empty for packaged installations. + + @retval int error = nonzero, success = 0 +*/ + +static int do_uninstall(const char *name, const char *basedir) +{ +#ifdef PKG_DELEGATION + char package[NAME_CHAR_LEN + 16]; + const char *pm; + const char *target; + char *cmd_argv[7]; +#ifdef INSTALL_LAYOUT_RPM + char providers[1024]; + char *nl; +#endif + + if (check_root("uninstall")) + return 1; + if (!(pm= get_package_manager())) + return 1; + + build_package_name(package, sizeof(package), name); + target= package; + +#ifdef INSTALL_LAYOUT_RPM + cmd_argv[0]= (char *) "rpm"; + cmd_argv[1]= (char *) "-q"; + cmd_argv[2]= (char *) "--whatprovides"; + cmd_argv[3]= package; + cmd_argv[4]= (char *) "--qf"; + cmd_argv[5]= (char *) "%{NAME}\n"; + cmd_argv[6]= 0; + if (run_argv_capture(cmd_argv, providers, sizeof(providers)) || + !providers[0]) + { + fprintf(stderr, "ERROR: plugin '%s' is not installed.\n", name); + return 1; + } + if (!(nl= strchr(providers, '\n'))) + nl= strend(providers); + if (nl[0] && nl[1]) + { + fprintf(stderr, "ERROR: several packages provide '%s':\n%s" + "Remove the right one with the package manager directly.\n", + package, providers); + return 1; + } + *nl= '\0'; + target= providers; +#endif + + cmd_argv[0]= (char *) pm; + cmd_argv[1]= (char *) "remove"; + cmd_argv[2]= (char *) target; + cmd_argv[3]= 0; + return run_argv(cmd_argv); +#else + printf("uninstall: '%s' (%s installation%s%s) not implemented yet\n", name, + INSTALL_METHOD_NAME, *basedir ? ", basedir=" : "", basedir); + return 0; +#endif +} + + +/** + Run the new package-manager style commands. + + Parses the options (--help, --version, etc. are handled by + handle_options), then validates the verb and the plugin name and + dispatches to the appropriate command handler. The plugin name is + normalized to lower case before validation. + + @param[in] argc The number of arguments. + @param[in] argv The arguments. + + @retval int error = 1, success = 0 +*/ + +static int run_new_command(int argc, char **argv) +{ + char name[NAME_CHAR_LEN + 1]; + char basedir[FN_REFLEN]; + const char *verb; + size_t i, len; + int error; + + if ((error= handle_options(&argc, &argv, my_long_options, get_one_option))) + return 1; + + if (argc < 1) + { + usage(); + return 1; + } + + verb= argv[0]; + if (strcmp(verb, "search") != 0 && strcmp(verb, "install") != 0 && + strcmp(verb, "uninstall") != 0) + { + fprintf(stderr, "ERROR: unknown command '%s'.\n", verb); + usage(); + return 1; + } + + if (argc != 2) + { + fprintf(stderr, "ERROR: '%s' requires exactly one plugin name.\n", verb); + usage(); + return 1; + } + + len= strlen(argv[1]); + if (len > NAME_CHAR_LEN) + { + fprintf(stderr, "ERROR: plugin name is too long (max %d characters).\n", + NAME_CHAR_LEN); + return 1; + } + for (i= 0; i <= len; i++) + name[i]= (char) tolower((unsigned char) argv[1][i]); + + if (validate_plugin_name(name)) + return 1; + + if (detect_install_method(basedir, sizeof(basedir))) + return 1; + + if (strcmp(verb, "search") == 0) + return do_search(name, basedir); + if (strcmp(verb, "install") == 0) + return do_install(name, basedir); + return do_uninstall(name, basedir); +} diff --git a/cmake/plugin.cmake b/cmake/plugin.cmake index 07c9220525558..a59fadcf4dfef 100644 --- a/cmake/plugin.cmake +++ b/cmake/plugin.cmake @@ -278,6 +278,13 @@ MACRO(MYSQL_ADD_PLUGIN) IF (NOT ARG_CLIENT) SET(CPACK_RPM_${ARG_COMPONENT}_PACKAGE_REQUIRES "MariaDB-server${ver}" PARENT_SCOPE) ENDIF() + + # rpm packages have their own names, but plugins are installed + # everywhere by the deb-style name, mariadb-plugin- + STRING(REGEX REPLACE "-engine(-|$)" "\\1" plugin_package "${ARG_COMPONENT}") + SET(CPACK_RPM_${ARG_COMPONENT}_PACKAGE_PROVIDES + "mariadb-plugin-${plugin_package}" PARENT_SCOPE) + SET(CPACK_RPM_${ARG_COMPONENT}_USER_FILELIST ${ignored} PARENT_SCOPE) IF (ARG_VERSION) SET(CPACK_RPM_${ARG_COMPONENT}_PACKAGE_VERSION ${SERVER_VERSION}_${ARG_VERSION} PARENT_SCOPE)