[Asterisk-code-review] test: Add ability to capture child process output (asterisk[18])

Friendly Automation asteriskteam at digium.com
Mon Sep 12 09:43:43 CDT 2022


Friendly Automation has submitted this change. ( 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(-)

Approvals:
  George Joseph: Looks good to me, approved
  Friendly Automation: Approved for Submit




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: 2
Gerrit-Owner: Philip Prindeville <philipp at redfish-solutions.com>
Gerrit-Reviewer: Friendly Automation
Gerrit-Reviewer: George Joseph <gjoseph at digium.com>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20220912/22dd6c0d/attachment-0001.html>


More information about the asterisk-code-review mailing list