502 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			502 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /*
 | |
|  * Copyright (c) 2016 MariaDB Corporation Ab
 | |
|  *
 | |
|  * Use of this software is governed by the Business Source License included
 | |
|  * in the LICENSE.TXT file and at www.mariadb.com/bsl11.
 | |
|  *
 | |
|  * Change Date: 2024-06-02
 | |
|  *
 | |
|  * On the date above, in accordance with the Business Source License, use
 | |
|  * of this software will be governed by version 2 or later of the General
 | |
|  * Public License.
 | |
|  */
 | |
| 
 | |
| #include "internal/externcmd.h"
 | |
| 
 | |
| #include <ctype.h>
 | |
| #include <errno.h>
 | |
| #include <string.h>
 | |
| #include <sys/wait.h>
 | |
| #include <fcntl.h>
 | |
| #include <string>
 | |
| #include <thread>
 | |
| 
 | |
| #include <maxbase/assert.h>
 | |
| #include <maxscale/alloc.h>
 | |
| #include <maxscale/log.h>
 | |
| #include <maxscale/pcre2.h>
 | |
| 
 | |
| /**
 | |
|  * Tokenize a string into arguments suitable for a `execvp` call
 | |
|  *
 | |
|  * @param args Argument string
 | |
|  * @param argv Array of char pointers to be filled with tokenized arguments
 | |
|  *
 | |
|  * @return 0 on success, -1 on error
 | |
|  */
 | |
| static int tokenize_arguments(const char* argstr, char** argv)
 | |
| {
 | |
|     int i = 0;
 | |
|     bool quoted = false;
 | |
|     bool read = false;
 | |
|     bool escaped = false;
 | |
|     char* ptr, * start;
 | |
|     char args[strlen(argstr) + 1];
 | |
|     char qc = 0;
 | |
| 
 | |
|     strcpy(args, argstr);
 | |
|     start = args;
 | |
|     ptr = start;
 | |
| 
 | |
|     while (*ptr != '\0' && i < MAXSCALE_EXTCMD_ARG_MAX)
 | |
|     {
 | |
|         if (escaped)
 | |
|         {
 | |
|             escaped = false;
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             if (*ptr == '\\')
 | |
|             {
 | |
|                 escaped = true;
 | |
|             }
 | |
|             else if (quoted && !escaped && *ptr == qc)      /** End of quoted string */
 | |
|             {
 | |
|                 *ptr = '\0';
 | |
|                 argv[i++] = MXS_STRDUP_A(start);
 | |
|                 read = false;
 | |
|                 quoted = false;
 | |
|             }
 | |
|             else if (!quoted)
 | |
|             {
 | |
|                 if (isspace(*ptr))
 | |
|                 {
 | |
|                     *ptr = '\0';
 | |
|                     if (read)   /** New token */
 | |
|                     {
 | |
|                         argv[i++] = MXS_STRDUP_A(start);
 | |
|                         read = false;
 | |
|                     }
 | |
|                 }
 | |
|                 else if (*ptr == '\"' || *ptr == '\'')
 | |
|                 {
 | |
|                     /** New quoted token, strip quotes */
 | |
|                     quoted = true;
 | |
|                     qc = *ptr;
 | |
|                     start = ptr + 1;
 | |
|                 }
 | |
|                 else if (!read)
 | |
|                 {
 | |
|                     start = ptr;
 | |
|                     read = true;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         ptr++;
 | |
|     }
 | |
|     if (read)
 | |
|     {
 | |
|         argv[i++] = MXS_STRDUP_A(start);
 | |
|     }
 | |
| 
 | |
|     argv[i] = NULL;
 | |
| 
 | |
|     return 0;
 | |
| }
 | |
| 
 | |
| EXTERNCMD* externcmd_allocate(const char* argstr, uint32_t timeout)
 | |
| {
 | |
|     EXTERNCMD* cmd = (EXTERNCMD*) MXS_MALLOC(sizeof(EXTERNCMD));
 | |
|     char** argv = (char**) MXS_MALLOC(sizeof(char*) * MAXSCALE_EXTCMD_ARG_MAX);
 | |
| 
 | |
|     if (argstr && cmd && argv)
 | |
|     {
 | |
|         cmd->timeout = timeout;
 | |
|         cmd->argv = argv;
 | |
|         if (tokenize_arguments(argstr, cmd->argv) == 0)
 | |
|         {
 | |
|             if (access(cmd->argv[0], X_OK) != 0)
 | |
|             {
 | |
|                 if (access(cmd->argv[0], F_OK) != 0)
 | |
|                 {
 | |
|                     MXS_ERROR("Cannot find file: %s", cmd->argv[0]);
 | |
|                 }
 | |
|                 else
 | |
|                 {
 | |
|                     MXS_ERROR("Cannot execute file '%s'. Missing "
 | |
|                               "execution permissions.",
 | |
|                               cmd->argv[0]);
 | |
|                 }
 | |
|                 externcmd_free(cmd);
 | |
|                 cmd = NULL;
 | |
|             }
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             MXS_ERROR("Failed to parse argument string for external command: %s",
 | |
|                       argstr);
 | |
|             externcmd_free(cmd);
 | |
|             cmd = NULL;
 | |
|         }
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         MXS_FREE(cmd);
 | |
|         MXS_FREE(argv);
 | |
|         cmd = NULL;
 | |
|     }
 | |
|     return cmd;
 | |
| }
 | |
| 
 | |
| void externcmd_free(EXTERNCMD* cmd)
 | |
| {
 | |
|     if (cmd)
 | |
|     {
 | |
|         for (int i = 0; cmd->argv[i]; i++)
 | |
|         {
 | |
|             MXS_FREE(cmd->argv[i]);
 | |
|         }
 | |
|         MXS_FREE(cmd->argv);
 | |
|         MXS_FREE(cmd);
 | |
|     }
 | |
| }
 | |
| 
 | |
| static const char* skip_whitespace(const char* ptr)
 | |
| {
 | |
|     while (*ptr && isspace(*ptr))
 | |
|     {
 | |
|         ptr++;
 | |
|     }
 | |
| 
 | |
|     return ptr;
 | |
| }
 | |
| 
 | |
| static const char* skip_prefix(const char* str)
 | |
| {
 | |
|     const char* ptr = strchr(str, ':');
 | |
|     mxb_assert(ptr);
 | |
| 
 | |
|     ptr++;
 | |
|     return skip_whitespace(ptr);
 | |
| }
 | |
| 
 | |
| static void log_output(const char* cmd, const std::string& str)
 | |
| {
 | |
|     int err;
 | |
| 
 | |
|     if (mxs_pcre2_simple_match("(?i)^[[:space:]]*alert[[:space:]]*[:]",
 | |
|                                str.c_str(),
 | |
|                                0,
 | |
|                                &err) == MXS_PCRE2_MATCH)
 | |
|     {
 | |
|         MXS_ALERT("%s: %s", cmd, skip_prefix(str.c_str()));
 | |
|     }
 | |
|     else if (mxs_pcre2_simple_match("(?i)^[[:space:]]*error[[:space:]]*[:]",
 | |
|                                     str.c_str(),
 | |
|                                     0,
 | |
|                                     &err) == MXS_PCRE2_MATCH)
 | |
|     {
 | |
|         MXS_ERROR("%s: %s", cmd, skip_prefix(str.c_str()));
 | |
|     }
 | |
|     else if (mxs_pcre2_simple_match("(?i)^[[:space:]]*warning[[:space:]]*[:]",
 | |
|                                     str.c_str(),
 | |
|                                     0,
 | |
|                                     &err) == MXS_PCRE2_MATCH)
 | |
|     {
 | |
|         MXS_WARNING("%s: %s", cmd, skip_prefix(str.c_str()));
 | |
|     }
 | |
|     else if (mxs_pcre2_simple_match("(?i)^[[:space:]]*notice[[:space:]]*[:]",
 | |
|                                     str.c_str(),
 | |
|                                     0,
 | |
|                                     &err) == MXS_PCRE2_MATCH)
 | |
|     {
 | |
|         MXS_NOTICE("%s: %s", cmd, skip_prefix(str.c_str()));
 | |
|     }
 | |
|     else if (mxs_pcre2_simple_match("(?i)^[[:space:]]*(info|debug)[[:space:]]*[:]",
 | |
|                                     str.c_str(),
 | |
|                                     0,
 | |
|                                     &err) == MXS_PCRE2_MATCH)
 | |
|     {
 | |
|         MXS_INFO("%s: %s", cmd, skip_prefix(str.c_str()));
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         // No special format, log as notice level message
 | |
|         MXS_NOTICE("%s: %s", cmd, skip_whitespace(str.c_str()));
 | |
|     }
 | |
| }
 | |
| 
 | |
| int externcmd_execute(EXTERNCMD* cmd)
 | |
| {
 | |
|     // Create a pipe where the command can print output
 | |
|     int fd[2];
 | |
| 
 | |
|     if (pipe(fd) == -1)
 | |
|     {
 | |
|         MXS_ERROR("Failed to open pipe: [%d] %s", errno, mxs_strerror(errno));
 | |
|         return -1;
 | |
|     }
 | |
| 
 | |
|     int rval = 0;
 | |
|     pid_t pid;
 | |
| 
 | |
|     // The SIGCHLD handler must be disabled before child process is forked,
 | |
|     // otherwise we'll get an error
 | |
|     pid = fork();
 | |
| 
 | |
|     if (pid < 0)
 | |
|     {
 | |
|         close(fd[0]);
 | |
|         close(fd[1]);
 | |
|         MXS_ERROR("Failed to execute command '%s', fork failed: [%d] %s",
 | |
|                   cmd->argv[0],
 | |
|                   errno,
 | |
|                   mxs_strerror(errno));
 | |
|         rval = -1;
 | |
|     }
 | |
|     else if (pid == 0)
 | |
|     {
 | |
|         // This is the child process. Close the read end of the pipe and redirect
 | |
|         // both stdout and stderr to the write end of the pipe
 | |
|         close(fd[0]);
 | |
|         dup2(fd[1], STDOUT_FILENO);
 | |
|         dup2(fd[1], STDERR_FILENO);
 | |
| 
 | |
|         // Execute the command
 | |
|         execvp(cmd->argv[0], cmd->argv);
 | |
| 
 | |
|         // Close the write end of the pipe and exit
 | |
|         close(fd[1]);
 | |
|         _exit(1);
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         MXS_INFO("Executing command '%s' in process %d", cmd->argv[0], pid);
 | |
|         cmd->child = pid;
 | |
|         cmd->n_exec++;
 | |
| 
 | |
|         std::string output;
 | |
|         bool first_warning = true;
 | |
|         bool again = true;
 | |
|         uint64_t t = 0;
 | |
|         uint64_t t_max = cmd->timeout * 1000;
 | |
| 
 | |
|         // Close the write end of the pipe and make the read end non-blocking
 | |
|         close(fd[1]);
 | |
|         fcntl(fd[0], F_SETFL, O_NONBLOCK);
 | |
| 
 | |
|         while (again)
 | |
|         {
 | |
|             int exit_status;
 | |
| 
 | |
|             switch (waitpid(pid, &exit_status, WNOHANG))
 | |
|             {
 | |
|             case -1:
 | |
|                 MXS_ERROR("Failed to wait for child process: %d, %s", errno, mxs_strerror(errno));
 | |
|                 again = false;
 | |
|                 break;
 | |
| 
 | |
|             case 0:
 | |
|                 if (t++ > t_max)
 | |
|                 {
 | |
|                     // Command timed out
 | |
|                     t = 0;
 | |
|                     if (first_warning)
 | |
|                     {
 | |
|                         MXS_WARNING("Soft timeout for command '%s', sending SIGTERM", cmd->argv[0]);
 | |
|                         kill(pid, SIGTERM);
 | |
|                         first_warning = false;
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         MXS_ERROR("Hard timeout for command '%s', sending SIGKILL", cmd->argv[0]);
 | |
|                         kill(pid, SIGKILL);
 | |
|                     }
 | |
|                 }
 | |
|                 else
 | |
|                 {
 | |
|                     // Sleep and try again
 | |
|                     std::this_thread::sleep_for(std::chrono::milliseconds(1));
 | |
|                 }
 | |
|                 break;
 | |
| 
 | |
|             default:
 | |
|                 again = false;
 | |
| 
 | |
|                 if (WIFEXITED(exit_status))
 | |
|                 {
 | |
|                     rval = WEXITSTATUS(exit_status);
 | |
|                 }
 | |
|                 else if (WIFSIGNALED(exit_status))
 | |
|                 {
 | |
|                     rval = WTERMSIG(exit_status);
 | |
|                 }
 | |
|                 else
 | |
|                 {
 | |
|                     rval = exit_status;
 | |
|                     MXS_ERROR("Command '%s' did not exit normally. Exit status: %d",
 | |
|                               cmd->argv[0],
 | |
|                               exit_status);
 | |
|                 }
 | |
|                 break;
 | |
|             }
 | |
| 
 | |
|             int n;
 | |
|             char buf[4096];     // This seems like enough space
 | |
| 
 | |
|             while ((n = read(fd[0], buf, sizeof(buf))) > 0)
 | |
|             {
 | |
|                 // Read all available output
 | |
|                 output.append(buf, n);
 | |
| 
 | |
|                 for (size_t pos = output.find("\n");
 | |
|                      pos != std::string::npos; pos = output.find("\n"))
 | |
|                 {
 | |
|                     if (pos == 0)
 | |
|                     {
 | |
|                         output.erase(0, 1);
 | |
|                     }
 | |
|                     else
 | |
|                     {
 | |
|                         std::string line = output.substr(0, pos);
 | |
|                         output.erase(0, pos + 1);
 | |
|                         log_output(cmd->argv[0], line);
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (!output.empty())
 | |
|         {
 | |
|             log_output(cmd->argv[0], output);
 | |
|         }
 | |
| 
 | |
|         // Close the read end of the pipe and copy the data to the output parameter
 | |
|         close(fd[0]);
 | |
|     }
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| bool externcmd_substitute_arg(EXTERNCMD* cmd, const char* match, const char* replace)
 | |
| {
 | |
|     int err;
 | |
|     bool rval = true;
 | |
|     size_t errpos;
 | |
|     pcre2_code* re = pcre2_compile((PCRE2_SPTR) match, PCRE2_ZERO_TERMINATED, 0, &err, &errpos, NULL);
 | |
|     if (re)
 | |
|     {
 | |
|         for (int i = 0; cmd->argv[i] && rval; i++)
 | |
|         {
 | |
|             size_t size_orig = strlen(cmd->argv[i]);
 | |
|             size_t size_replace = strlen(replace);
 | |
|             size_t size = MXS_MAX(size_orig, size_replace);
 | |
|             char* dest = (char*)MXS_MALLOC(size);
 | |
|             if (dest)
 | |
|             {
 | |
|                 mxs_pcre2_result_t rc = mxs_pcre2_substitute(re, cmd->argv[i], replace, &dest, &size);
 | |
| 
 | |
|                 switch (rc)
 | |
|                 {
 | |
|                 case MXS_PCRE2_ERROR:
 | |
|                     MXS_FREE(dest);
 | |
|                     rval = false;
 | |
|                     break;
 | |
| 
 | |
|                 case MXS_PCRE2_MATCH:
 | |
|                     MXS_FREE(cmd->argv[i]);
 | |
|                     cmd->argv[i] = dest;
 | |
|                     break;
 | |
| 
 | |
|                 case MXS_PCRE2_NOMATCH:
 | |
|                     MXS_FREE(dest);
 | |
|                     break;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         pcre2_code_free(re);
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         rval = false;
 | |
|     }
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Get the name of the command being executed.
 | |
|  *
 | |
|  * This copies the command being executed into a new string.
 | |
|  * @param str Command string, optionally with arguments
 | |
|  * @return Command part of the string if arguments were defined
 | |
|  */
 | |
| static char* get_command(const char* str)
 | |
| {
 | |
|     char* rval = NULL;
 | |
|     const char* start = str;
 | |
| 
 | |
|     while (*start && isspace(*start))
 | |
|     {
 | |
|         start++;
 | |
|     }
 | |
| 
 | |
|     const char* end = start;
 | |
| 
 | |
|     while (*end && !isspace(*end))
 | |
|     {
 | |
|         end++;
 | |
|     }
 | |
| 
 | |
|     size_t len = end - start;
 | |
| 
 | |
|     if (len > 0)
 | |
|     {
 | |
|         rval = (char*)MXS_MALLOC(len + 1);
 | |
| 
 | |
|         if (rval)
 | |
|         {
 | |
|             memcpy(rval, start, len);
 | |
|             rval[len] = '\0';
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| bool externcmd_can_execute(const char* argstr)
 | |
| {
 | |
|     bool rval = false;
 | |
|     char* command = get_command(argstr);
 | |
| 
 | |
|     if (command)
 | |
|     {
 | |
|         if (access(command, X_OK) == 0)
 | |
|         {
 | |
|             rval = true;
 | |
|         }
 | |
|         else if (access(command, F_OK) == 0)
 | |
|         {
 | |
|             MXS_ERROR("The executable cannot be executed: %s", command);
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             MXS_ERROR("The executable cannot be found: %s", command);
 | |
|         }
 | |
|         MXS_FREE(command);
 | |
|     }
 | |
|     return rval;
 | |
| }
 | |
| 
 | |
| bool externcmd_matches(const EXTERNCMD* cmd, const char* match)
 | |
| {
 | |
|     for (int i = 0; cmd->argv[i]; i++)
 | |
|     {
 | |
|         if (strstr(cmd->argv[i], match))
 | |
|         {
 | |
|             return true;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
| }
 | 
