From b9df2c481bcbd776741ad22b9e3943e1a339f2f3 Mon Sep 17 00:00:00 2001 From: Philip Prindeville Date: Mon, 2 May 2022 22:49:54 -0600 Subject: [PATCH] test: Add ability to capture child process output ASTERISK-30037 Change-Id: Icbf84ce05addb197a458361c35d784e460d8d6c2 --- include/asterisk/test.h | 56 +++++++++ main/Makefile | 3 + main/test.c | 248 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 307 insertions(+) diff --git a/include/asterisk/test.h b/include/asterisk/test.h index 78d9788f7e..12aed650be 100644 --- a/include/asterisk/test.h +++ b/include/asterisk/test.h @@ -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 */ diff --git a/main/Makefile b/main/Makefile index 9f31a3a011..ac4742307f 100644 --- a/main/Makefile +++ b/main/Makefile @@ -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 diff --git a/main/test.c b/main/test.c index 5135803234..747262c7bf 100644 --- a/main/test.c +++ b/main/test.c @@ -48,6 +48,16 @@ #include "asterisk/astobj2.h" #include "asterisk/stasis.h" #include "asterisk/json.h" +#include "asterisk/app.h" /* for ast_replace_sigchld(), etc. */ + +#include +#include +#include +#include +#include +#include +#include +#include /*! \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; } +