From 6385c9a41471e51b1450037c13194db7a772eed1 Mon Sep 17 00:00:00 2001 From: Mohamed Date: Tue, 25 Aug 2026 23:04:42 +0300 Subject: [PATCH] MDEV-40786 Make mariadb-plugin a package manager Add search, install, and uninstall subcommands to mariadb-plugin with input validation and case normalization, while keeping legacy ENABLE and DISABLE syntax functional for backward compatibility. Bake the installation layout into the binary at compile time (RPM, DEB, or tarball), as it cannot be derived from the path alone since RPM and DEB use the same directory, and verify the executable path at runtime via argv[0] and mysys path helpers to ensure package managers and tarball directories target the correct installation. Delegate install and uninstall to the system package manager on RPM and DEB installations, so that plugin files stay owned by it. The plugin name maps to the uniform package name mariadb-plugin-. Commands run through fork and execvp with an argument vector, never a shell, and inherit the standard streams, so the package manager prompts the user itself and its exit code is passed through. RPM packages are named differently, so they get the uniform name as a Provides, derived from the plugin component in cmake/plugin.cmake. DEB packages already carry it. On RPM, uninstall resolves the real package name with rpm --whatprovides, because dnf 5 does not accept Provides names for removal. --- client/CMakeLists.txt | 5 + client/mysql_plugin.c | 598 +++++++++++++++++++++++++++++++++++++++++- cmake/plugin.cmake | 7 + 3 files changed, 605 insertions(+), 5 deletions(-) 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)