[Asterisk-code-review] test: Add ability to capture child process output (asterisk[18])
Philip Prindeville
asteriskteam at digium.com
Wed Sep 7 14:23:48 CDT 2022
Philip Prindeville has uploaded this change for review. ( https://gerrit.asterisk.org/c/asterisk/+/19110 )
Change subject: test: Add ability to capture child process output
......................................................................
test: Add ability to capture child process output
ASTERISK-30037
Change-Id: Icbf84ce05addb197a458361c35d784e460d8d6c2
---
M include/asterisk/test.h
M main/Makefile
M main/test.c
3 files changed, 318 insertions(+), 0 deletions(-)
git pull ssh://gerrit.asterisk.org:29418/asterisk refs/changes/10/19110/1
diff --git a/include/asterisk/test.h b/include/asterisk/test.h
index 78d9788..12aed65 100644
--- a/include/asterisk/test.h
+++ b/include/asterisk/test.h
@@ -209,6 +209,27 @@
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
*/
struct ast_test_info {
@@ -417,5 +438,40 @@
} \
})
+/*!
+ * \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 9f31a3a..ac47423 100644
--- a/main/Makefile
+++ b/main/Makefile
@@ -167,6 +167,9 @@
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 5135803..747262c 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 <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 @@
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 @@
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 @@
return 0;
}
+
--
To view, visit https://gerrit.asterisk.org/c/asterisk/+/19110
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings
Gerrit-Project: asterisk
Gerrit-Branch: 18
Gerrit-Change-Id: Icbf84ce05addb197a458361c35d784e460d8d6c2
Gerrit-Change-Number: 19110
Gerrit-PatchSet: 1
Gerrit-Owner: Philip Prindeville <philipp at redfish-solutions.com>
Gerrit-MessageType: newchange
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20220907/4a5ff3c5/attachment-0001.html>
More information about the asterisk-code-review
mailing list