Commits

Greg Ward committed 36e17ce

Add capture_child() to replace use of system() and popen().

Comments (0)

Files changed (3)

 vcprompt: $(objects)
 	$(CC) -o $@ $(objects)
 
+# build a standalone version of capture_child() library for testing
+src/capture: src/capture.c src/capture.h src/common.c src/common.h
+	$(CC) -DTEST_CAPTURE $(CFLAGS) -o $@ src/capture.c src/common.c
+
 # Maximally pessimistic view of header dependencies.
 $(objects): $(headers)
 
+#include "capture.h"
+#include "common.h"
+
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/wait.h>
+#include <sys/select.h>
+#include <sys/types.h>
+
+static void
+init_dynbuf(dynbuf *dbuf, int bufsize)
+{
+    dbuf->size = bufsize;
+    dbuf->len = 0;
+    dbuf->buf = malloc(bufsize); /* caller handles NULL */
+    dbuf->eof = 0;
+}
+
+static ssize_t
+read_dynbuf(int fd, dynbuf *dbuf)
+{
+    size_t avail = dbuf->size - dbuf->len;
+    if (avail < 1024) {
+        dbuf->size *= 2;
+        dbuf->buf = realloc(dbuf->buf, dbuf->size);
+        avail = dbuf->size - dbuf->len;
+    }
+    /* read avail-1 bytes to leave room for termininating \0 */
+    ssize_t nread = read(fd, dbuf->buf + dbuf->len, avail - 1);
+    if (nread < 0)
+        return nread;
+    else if (nread == 0) {
+        dbuf->buf[dbuf->len] = '\0';
+        dbuf->eof = 1;
+        //debug("capture: eof on fd %d; total read = %d bytes", fd, dbuf->len);
+        return 0;
+    }
+    //debug("capture: read %d bytes from child via fd %d", nread, fd);
+    dbuf->len += nread;
+    return nread;
+}
+
+capture_t *
+new_capture()
+{
+    int bufsize = 4096;
+    capture_t *result = malloc(sizeof(capture_t));
+    if (result == NULL)
+        goto err;
+    init_dynbuf(&result->stdout, bufsize);
+    if (result->stdout.buf == NULL)
+        goto err;
+
+    init_dynbuf(&result->stderr, bufsize);
+    if (result->stderr.buf == NULL)
+        goto err;
+
+    return result;
+
+ err:
+    free_capture(result);
+    return NULL;
+}
+
+void
+free_capture(capture_t *result)
+{
+    if (result != NULL) {
+        if (result->stdout.buf != NULL)
+            free(result->stdout.buf);
+        if (result->stderr.buf != NULL)
+            free(result->stderr.buf);
+        free(result);
+    }
+}
+
+capture_t *
+capture_child(const char *file, char *const argv[])
+{
+    int stdout_pipe[] = {-1, -1};
+    int stderr_pipe[] = {-1, -1};
+    capture_t *result = NULL;
+    if (pipe(stdout_pipe) < 0)
+        goto err;
+    if (pipe(stderr_pipe) < 0)
+        goto err;
+
+    pid_t pid = fork();
+    if (pid < 0) {
+        goto err;
+    }
+    if (pid == 0) {             /* in the child */
+        close(stdout_pipe[0]);  /* don't need the read ends of the pipes */
+        close(stderr_pipe[0]);
+        if (dup2(stdout_pipe[1], STDOUT_FILENO) < 0)
+            _exit(1);
+        if (dup2(stderr_pipe[1], STDERR_FILENO) < 0)
+            _exit(1);
+
+        execvp(file, argv);
+        debug("error executing %s: %s\n", file, strerror(errno));
+        _exit(127);
+    }
+
+    /* parent: don't need write ends of the pipes */
+    close(stdout_pipe[1]);
+    close(stderr_pipe[1]);
+
+    result = new_capture();
+    if (result == NULL)
+        goto err;
+
+    int cstdout = stdout_pipe[0];
+    int cstderr = stderr_pipe[0];
+
+    int done = 0;
+    while (!done) {
+        int maxfd = -1;
+        fd_set child_fds;
+        FD_ZERO(&child_fds);
+        if (!result->stdout.eof) {
+            FD_SET(cstdout, &child_fds);
+            maxfd = cstdout;
+        }
+        if (!result->stderr.eof) {
+            FD_SET(cstderr, &child_fds);
+            maxfd = cstderr;
+        }
+        int numavail = select(maxfd+1, &child_fds, NULL, NULL, NULL);
+        if (numavail < 0)
+            goto err;
+        else if (numavail == 0) /* EOF on both pipes */
+            break;
+
+        if (FD_ISSET(cstdout, &child_fds)) {
+            if (read_dynbuf(cstdout, &result->stdout) < 0)
+                goto err;
+        }
+        if (FD_ISSET(cstderr, &child_fds)) {
+            if (read_dynbuf(cstderr, &result->stderr) < 0)
+                goto err;
+        }
+        done = result->stdout.eof && result->stderr.eof;
+    }
+
+    waitpid(pid, &result->status, 0);
+    if (result->status != 0)
+        debug("child process %s exited with status %d",
+              file, WEXITSTATUS(result->status));
+    if (result->stderr.len > 0)
+        debug("child process %s wrote to stderr:\n%s",
+              file, result->stderr.buf);
+
+    return result;
+ err:
+    if (stdout_pipe[0] > -1)
+        close(stdout_pipe[0]);
+    if (stdout_pipe[1] > -1)
+        close(stdout_pipe[1]);
+    if (stderr_pipe[0] > -1)
+        close(stderr_pipe[0]);
+    if (stderr_pipe[1] > -1)
+        close(stderr_pipe[1]);
+    free_capture(result);
+    return NULL;
+}
+
+/*
+ * To build a standalone executable for testing:
+ *    make src/capture
+ *
+ * Then to actually test, commands like this are useful:
+ *    ./src/capture ls -l              # stdout only
+ *    ./src/capture ls -l asdf fsda    # stderr only
+ *    ./src/capture ls -l . fdsa / asdf # mix of stdout and stderr
+ *    ./src/capture sh -c "echo -n foobar ; echo -n bipbop >&2; echo whee ; echo fnorb >&2"
+ *    ./src/capture find /etc -type f
+ * ...and so forth.
+ */
+#ifdef TEST_CAPTURE
+#include <stdio.h>
+
+int
+main(int argc, char *argv[])
+{
+    if (argc < 2) {
+        fprintf(stderr, "usage: %s prog arg...\n", argv[0]);
+        return 2;
+    }
+    options_t options = {debug: 1};
+    set_options(&options);
+
+    capture_t *result = capture_child(argv[1], argv+1);
+    int status;
+    if (result == NULL) {
+        perror("capture failed");
+        return 1;
+    }
+    printf("read %ld bytes from child stdout: >%s<\n",
+           result->stdout.len, result->stdout.buf);
+    printf("read %ld bytes from child stderr: >%s<\n",
+           result->stderr.len, result->stderr.buf);
+    status = WEXITSTATUS(result->status);
+    printf("child exit status: %d\n", status);
+    free_capture(result);
+    return status;
+}
+#endif
+#ifndef CAPTURE_H
+#define CAPTURE_H
+
+#include <sys/types.h>
+
+typedef struct {
+    size_t size;                /* bytes allocated */
+    size_t len;                 /* bytes filled */
+    char *buf;
+    int eof;
+} dynbuf;
+
+typedef struct {
+    dynbuf stdout;
+    dynbuf stderr;
+    int status;
+} capture_t;
+
+/* fork() and exec() a child process, capturing its entire stdout and
+ * stderr to a capture object. capture->stdout.buf is the child's
+ * stdout, and capture->stdout.len the number of bytes read (buf is
+ * null terminated, so as long as the child's output is textual, you
+ * can use buf as a string). Similarly, child's stderr is in
+ * capture->stderr.buf and capture->stderr.len.
+ */
+capture_t *
+capture_child(const char *file, char *const argv[]);
+
+/* free all resources in the object returned by capture_child() */
+void
+free_capture(capture_t *capture);
+
+#endif
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.