]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
test: Add ability to capture child process output
authorPhilip Prindeville <philipp@redfish-solutions.com>
Tue, 3 May 2022 04:49:54 +0000 (22:49 -0600)
committerGeorge Joseph <gjoseph@digium.com>
Mon, 12 Sep 2022 13:15:51 +0000 (08:15 -0500)
ASTERISK-30037

Change-Id: Icbf84ce05addb197a458361c35d784e460d8d6c2

include/asterisk/test.h
main/Makefile
main/test.c

index 78d9788f7ebd4686ce3f94bd255a2bfec0c893bb..12aed650be3a7db47461a9d6578fca8bd09c83c3 100644 (file)
@@ -208,6 +208,27 @@ enum ast_test_command {
  */
 struct ast_test;
 
+/*!
+ * \brief A capture of running an external process.
+ *
+ * This contains a buffer holding stdout, another containing stderr,
+ * the process id of the child, and its exit code.
+ */
+struct ast_test_capture {
+       /*! \brief buffer holding stdout */
+       char *outbuf;
+       /*! \brief length of buffer holding stdout */
+       size_t outlen;
+       /*! \brief buffer holding stderr */
+       char *errbuf;
+       /*! \brief length of buffer holding stderr */
+       size_t errlen;
+       /*! \brief process id of child */
+       pid_t pid;
+       /*! \brief exit code of child */
+       int exitcode;
+};
+
 /*!
  * \brief Contains all the initialization information required to store a new test definition
  */
@@ -417,5 +438,40 @@ int __ast_test_status_update(const char *file, const char *func, int line, struc
        } \
 })
 
+/*!
+ * \brief Release the storage (buffers) associated with capturing
+ * the output of an external child process.
+ *
+ * \since 19.4.0
+ *
+ * \param capture The structure describing the child process and its
+ * associated output.
+ */
+void ast_test_capture_free(struct ast_test_capture *capture);
+
+/*!
+ * \brief Run a child process and capture its output and exit code.
+ *
+ * \!since 19.4.0
+ *
+ * \param capture The structure describing the child process and its
+ * associated output.
+ *
+ * \param file The name of the file to execute (uses $PATH to locate).
+ *
+ * \param argv The NULL-terminated array of arguments to pass to the
+ * child process, starting with the command name itself.
+ *
+ * \param data The buffer of input to be sent to child process's stdin;
+ * optional and may be NULL.
+ *
+ * \param datalen The length of the buffer, if not NULL, otherwise zero.
+ *
+ * \retval 1 for success
+ * \retval other failure
+ */
+
+int ast_test_capture_command(struct ast_test_capture *capture, const char *file, char *const argv[], const char *data, unsigned datalen);
+
 #endif /* TEST_FRAMEWORK */
 #endif /* _AST_TEST_H */
index 9f31a3a011f44cf9fc608fb465d660bdaca31757..ac4742307fa3dcf26bbc165b00bbf356681746f1 100644 (file)
@@ -167,6 +167,9 @@ lock.o: _ASTCFLAGS+=$(call get_menuselect_cflags,DETECT_DEADLOCKS)
 options.o: _ASTCFLAGS+=$(call get_menuselect_cflags,REF_DEBUG)
 sched.o: _ASTCFLAGS+=$(call get_menuselect_cflags,DEBUG_SCHEDULER DUMP_SCHEDULER)
 tcptls.o: _ASTCFLAGS+=$(OPENSSL_INCLUDE) -Wno-deprecated-declarations
+# since we're using open_memstream(), we need to release the buffer with
+# the native free() function or we might get unexpected behavior.
+test.o: _ASTCFLAGS+=-DASTMM_LIBC=ASTMM_IGNORE
 uuid.o: _ASTCFLAGS+=$(UUID_INCLUDE)
 stasis.o: _ASTCFLAGS+=$(call get_menuselect_cflags,AO2_DEBUG)
 time.o: _ASTCFLAGS+=-D_XOPEN_SOURCE=700
index 51358032349f151a220263713a0696197e0a5216..747262c7bfb89ca8bd871e01f91efe531d25d62d 100644 (file)
 #include "asterisk/astobj2.h"
 #include "asterisk/stasis.h"
 #include "asterisk/json.h"
+#include "asterisk/app.h"              /* for ast_replace_sigchld(), etc. */
+
+#include <stdio.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <signal.h>
 
 /*! \since 12
  * \brief The topic for test suite messages
@@ -100,6 +110,42 @@ enum test_mode {
        TEST_NAME_CATEGORY = 2,
 };
 
+#define zfclose(fp) \
+       ({ if (fp != NULL) { \
+               fclose(fp); \
+               fp = NULL; \
+          } \
+          (void)0; \
+        })
+
+#define zclose(fd) \
+       ({ if (fd != -1) { \
+               close(fd); \
+               fd = -1; \
+          } \
+          (void)0; \
+        })
+
+#define movefd(oldfd, newfd) \
+       ({ if (oldfd != newfd) { \
+               dup2(oldfd, newfd); \
+               close(oldfd); \
+               oldfd = -1; \
+          } \
+          (void)0; \
+        })
+
+#define lowerfd(oldfd) \
+       ({ int newfd = dup(oldfd); \
+          if (newfd > oldfd) \
+               close(newfd); \
+          else { \
+               close(oldfd); \
+               oldfd = newfd; \
+          } \
+          (void)0; \
+        })
+
 /*! List of registered test definitions */
 static AST_LIST_HEAD_STATIC(tests, ast_test);
 
@@ -267,6 +313,207 @@ void ast_test_set_result(struct ast_test *test, enum ast_test_result_state state
        test->state = state;
 }
 
+void ast_test_capture_free(struct ast_test_capture *capture)
+{
+       if (capture) {
+               free(capture->outbuf);
+               capture->outbuf = NULL;
+               free(capture->errbuf);
+               capture->errbuf = NULL;
+       }
+       capture->pid = -1;
+       capture->exitcode = -1;
+}
+
+int ast_test_capture_command(struct ast_test_capture *capture, const char *file, char *const argv[], const char *data, unsigned datalen)
+{
+       int fd0[2] = { -1, -1 }, fd1[2] = { -1, -1 }, fd2[2] = { -1, -1 };
+       pid_t pid = -1;
+       int status = 0;
+
+       memset(capture, 0, sizeof(*capture));
+       capture->pid = capture->exitcode = -1;
+
+       if (data != NULL && datalen > 0) {
+               if (pipe(fd0) == -1) {
+                       ast_log(LOG_ERROR, "Couldn't open stdin pipe: %s\n", strerror(errno));
+                       goto cleanup;
+               }
+               fcntl(fd0[1], F_SETFL, fcntl(fd0[1], F_GETFL, 0) | O_NONBLOCK);
+       } else {
+               if ((fd0[0] = open("/dev/null", O_RDONLY)) == -1) {
+                       ast_log(LOG_ERROR, "Couldn't open /dev/null: %s\n", strerror(errno));
+                       goto cleanup;
+               }
+       }
+
+       if (pipe(fd1) == -1) {
+               ast_log(LOG_ERROR, "Couldn't open stdout pipe: %s\n", strerror(errno));
+               goto cleanup;
+       }
+
+       if (pipe(fd2) == -1) {
+               ast_log(LOG_ERROR, "Couldn't open stdout pipe: %s\n", strerror(errno));
+               goto cleanup;
+       }
+
+       /* we don't want anyone else reaping our children */
+       ast_replace_sigchld();
+
+       if ((pid = fork()) == -1) {
+               ast_log(LOG_ERROR, "Failed to fork(): %s\n", strerror(errno));
+               goto cleanup;
+
+       } else if (pid == 0) {
+               fclose(stdin);
+               zclose(fd0[1]);
+               zclose(fd1[0]);
+               zclose(fd2[0]);
+
+               movefd(fd0[0], 0);
+               movefd(fd1[1], 1);
+               movefd(fd2[1], 2);
+
+               execvp(file, argv);
+               ast_log(LOG_ERROR, "Failed to execv(): %s\n", strerror(errno));
+               exit(1);
+
+       } else {
+               FILE *cmd = NULL, *out = NULL, *err = NULL;
+
+               char buf[BUFSIZ];
+               int wstatus, n, nfds;
+               fd_set readfds, writefds;
+               unsigned i;
+
+               zclose(fd0[0]);
+               zclose(fd1[1]);
+               zclose(fd2[1]);
+
+               lowerfd(fd0[1]);
+               lowerfd(fd1[0]);
+               lowerfd(fd2[0]);
+
+               if ((cmd = fmemopen(buf, sizeof(buf), "w")) == NULL) {
+                       ast_log(LOG_ERROR, "Failed to open memory buffer: %s\n", strerror(errno));
+                       kill(pid, SIGKILL);
+                       goto cleanup;
+               }
+               for (i = 0; argv[i] != NULL; ++i) {
+                       if (i > 0) {
+                               fputc(' ', cmd);
+                       }
+                       fputs(argv[i], cmd);
+               }
+               zfclose(cmd);
+
+               ast_log(LOG_TRACE, "run: %.*s\n", (int)sizeof(buf), buf);
+
+               if ((out = open_memstream(&capture->outbuf, &capture->outlen)) == NULL) {
+                       ast_log(LOG_ERROR, "Failed to open output buffer: %s\n", strerror(errno));
+                       kill(pid, SIGKILL);
+                       goto cleanup;
+               }
+
+               if ((err = open_memstream(&capture->errbuf, &capture->errlen)) == NULL) {
+                       ast_log(LOG_ERROR, "Failed to open error buffer: %s\n", strerror(errno));
+                       kill(pid, SIGKILL);
+                       goto cleanup;
+               }
+
+               while (1) {
+                       n = waitpid(pid, &wstatus, WNOHANG);
+
+                       if (n == pid && WIFEXITED(wstatus)) {
+                               zclose(fd0[1]);
+                               zclose(fd1[0]);
+                               zclose(fd2[0]);
+                               zfclose(out);
+                               zfclose(err);
+
+                               capture->pid = pid;
+                               capture->exitcode = WEXITSTATUS(wstatus);
+
+                               ast_log(LOG_TRACE, "run: pid %d exits %d\n", capture->pid, capture->exitcode);
+
+                               break;
+                       }
+
+                       /* a function that does the opposite of ffs()
+                        * would be handy here for finding the highest
+                        * descriptor number.
+                        */
+                       nfds = MAX(fd0[1], MAX(fd1[0], fd2[0])) + 1;
+
+                       FD_ZERO(&readfds);
+                       FD_ZERO(&writefds);
+
+                       if (fd0[1] != -1) {
+                               if (data != NULL && datalen > 0)
+                                       FD_SET(fd0[1], &writefds);
+                       }
+                       if (fd1[0] != -1) {
+                               FD_SET(fd1[0], &readfds);
+                       }
+                       if (fd2[0] != -1) {
+                               FD_SET(fd2[0], &readfds);
+                       }
+
+                       /* not clear that exception fds are meaningful
+                        * with non-network descriptors.
+                        */
+                       n = select(nfds, &readfds, &writefds, NULL, NULL);
+
+                       if (FD_ISSET(fd0[1], &writefds)) {
+                               n = write(fd0[1], data, datalen);
+                               if (n > 0) {
+                                       data += n;
+                                       datalen -= MIN(datalen, n);
+                                       /* out of data, so close stdin */
+                                       if (datalen == 0)
+                                               zclose(fd0[1]);
+                               } else {
+                                       zclose(fd0[1]);
+                               }
+                       }
+
+                       if (FD_ISSET(fd1[0], &readfds)) {
+                               n = read(fd1[0], buf, sizeof(buf));
+                               if (n > 0) {
+                                       fwrite(buf, sizeof(char), n, out);
+                               } else {
+                                       zclose(fd1[0]);
+                               }
+                       }
+
+                       if (FD_ISSET(fd2[0], &readfds)) {
+                               n = read(fd2[0], buf, sizeof(buf));
+                               if (n > 0) {
+                                       fwrite(buf, sizeof(char), n, err);
+                               } else {
+                                       zclose(fd2[0]);
+                               }
+                       }
+               }
+               status = 1;
+
+cleanup:
+               ast_unreplace_sigchld();
+
+               zfclose(cmd);
+               zfclose(out);
+               zfclose(err);
+
+               zclose(fd0[1]);
+               zclose(fd1[0]);
+               zclose(fd1[1]);
+               zclose(fd2[0]);
+               zclose(fd2[1]);
+
+               return status;
+       }
+}
+
 /*
  * These are the Java reserved words we need to munge so Jenkins
  * doesn't barf on them.
@@ -1242,3 +1489,4 @@ int ast_test_init(void)
 
        return 0;
 }
+