process: Provide an abstraction to spawn child processes with redirected I/O
authorMartin Willi <martin@revosec.ch>
Thu, 2 Oct 2014 14:17:46 +0000 (16:17 +0200)
committerMartin Willi <martin@revosec.ch>
Mon, 6 Oct 2014 16:24:39 +0000 (18:24 +0200)
src/libstrongswan/Android.mk
src/libstrongswan/Makefile.am
src/libstrongswan/tests/Makefile.am
src/libstrongswan/tests/suites/test_process.c [new file with mode: 0644]
src/libstrongswan/tests/tests.h
src/libstrongswan/utils/process.c [new file with mode: 0644]
src/libstrongswan/utils/process.h [new file with mode: 0644]

index 3ddd42f..9b775f9 100644 (file)
@@ -37,7 +37,7 @@ selectors/traffic_selector.c settings/settings.c settings/settings_types.c \
 settings/settings_parser.c settings/settings_lexer.c \
 utils/utils.c utils/chunk.c utils/debug.c utils/enum.c utils/identification.c \
 utils/lexparser.c utils/optionsfrom.c utils/capabilities.c utils/backtrace.c \
-utils/parser_helper.c utils/test.c utils/utils/strerror.c
+utils/parser_helper.c utils/test.c utils/process.c utils/utils/strerror.c
 
 libstrongswan_la_SOURCES += \
     threading/thread.c \
index 3fb57de..0083ffe 100644 (file)
@@ -35,7 +35,7 @@ selectors/traffic_selector.c settings/settings.c settings/settings_types.c \
 settings/settings_parser.y settings/settings_lexer.l \
 utils/utils.c utils/chunk.c utils/debug.c utils/enum.c utils/identification.c \
 utils/lexparser.c utils/optionsfrom.c utils/capabilities.c utils/backtrace.c \
-utils/parser_helper.c utils/test.c utils/utils/strerror.c
+utils/parser_helper.c utils/test.c utils/process.c utils/utils/strerror.c
 
 if !USE_WINDOWS
   libstrongswan_la_SOURCES += \
@@ -102,7 +102,7 @@ utils/lexparser.h utils/optionsfrom.h utils/capabilities.h utils/backtrace.h \
 utils/leak_detective.h utils/printf_hook/printf_hook.h \
 utils/printf_hook/printf_hook_vstr.h utils/printf_hook/printf_hook_builtin.h \
 utils/parser_helper.h utils/test.h utils/integrity_checker.h utils/windows.h \
-utils/utils/strerror.h
+utils/process.h utils/utils/strerror.h
 endif
 
 library.lo :   $(top_builddir)/config.status
index e8e8090..7ecba19 100644 (file)
@@ -30,6 +30,7 @@ tests_SOURCES = tests.h tests.c \
   suites/test_hashtable.c \
   suites/test_identification.c \
   suites/test_threading.c \
+  suites/test_process.c \
   suites/test_watcher.c \
   suites/test_stream.c \
   suites/test_fetch_http.c \
diff --git a/src/libstrongswan/tests/suites/test_process.c b/src/libstrongswan/tests/suites/test_process.c
new file mode 100644 (file)
index 0000000..e41b015
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2014 Martin Willi
+ * Copyright (C) 2014 revosec AG
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+#include "test_suite.h"
+
+#include <unistd.h>
+
+#include <utils/process.h>
+
+START_TEST(test_retval_true)
+{
+       process_t *process;
+       char *argv[] = {
+               "/bin/true",
+               NULL
+       };
+       int retval;
+
+       process = process_start(argv, NULL, NULL, NULL, NULL, TRUE);
+       ck_assert(process != NULL);
+       ck_assert(process->wait(process, &retval));
+       ck_assert_int_eq(retval, 0);
+}
+END_TEST
+
+START_TEST(test_retval_false)
+{
+       process_t *process;
+       char *argv[] = {
+               "/bin/false",
+               NULL
+       };
+       int retval;
+
+       process = process_start(argv, NULL, NULL, NULL, NULL, TRUE);
+       ck_assert(process != NULL);
+       ck_assert(process->wait(process, &retval));
+       ck_assert(retval != 0);
+}
+END_TEST
+
+START_TEST(test_not_found)
+{
+       process_t *process;
+       char *argv[] = {
+               "/bin/does-not-exist",
+               NULL
+       };
+
+       process = process_start(argv, NULL, NULL, NULL, NULL, TRUE);
+       /* both is acceptable behavior */
+       ck_assert(process == NULL || !process->wait(process, NULL));
+}
+END_TEST
+
+START_TEST(test_echo)
+{
+       process_t *process;
+       char *argv[] = {
+               "/bin/cat",
+               NULL
+       };
+       int retval, in, out;
+       char *msg = "test";
+       char buf[strlen(msg) + 1];
+
+       memset(buf, 0, strlen(msg) + 1);
+
+       process = process_start(argv, NULL, &in, &out, NULL, TRUE);
+       ck_assert(process != NULL);
+       ck_assert_int_eq(write(in, msg, strlen(msg)), strlen(msg));
+       ck_assert(close(in) == 0);
+       ck_assert_int_eq(read(out, buf, strlen(msg) + 1), strlen(msg));
+       ck_assert_str_eq(buf, msg);
+       ck_assert(close(out) == 0);
+       ck_assert(process->wait(process, &retval));
+       ck_assert_int_eq(retval, 0);
+}
+END_TEST
+
+START_TEST(test_echo_err)
+{
+       process_t *process;
+       char *argv[] = {
+               "/bin/sh",
+               "-c",
+               "1>&2 /bin/cat",
+               NULL
+       };
+       int retval, in, err;
+       char *msg = "a longer test message";
+       char buf[strlen(msg) + 1];
+
+       memset(buf, 0, strlen(msg) + 1);
+
+       process = process_start(argv, NULL, &in, NULL, &err, TRUE);
+       ck_assert(process != NULL);
+       ck_assert_int_eq(write(in, msg, strlen(msg)), strlen(msg));
+       ck_assert(close(in) == 0);
+       ck_assert_int_eq(read(err, buf, strlen(msg) + 1), strlen(msg));
+       ck_assert_str_eq(buf, msg);
+       ck_assert(close(err) == 0);
+       ck_assert(process->wait(process, &retval));
+       ck_assert_int_eq(retval, 0);
+}
+END_TEST
+
+START_TEST(test_env)
+{
+       process_t *process;
+       char *argv[] = {
+               "/bin/sh",
+               "-c",
+               "echo -n $A $B",
+               NULL
+       };
+       char *envp[] = {
+               "A=atest",
+               "B=bstring",
+               NULL
+       };
+       int retval, out;
+       char buf[64] = {};
+
+       process = process_start(argv, envp, NULL, &out, NULL, TRUE);
+       ck_assert(process != NULL);
+       ck_assert(read(out, buf, sizeof(buf)) > 0);
+       ck_assert_str_eq(buf, "atest bstring");
+       ck_assert(close(out) == 0);
+       ck_assert(process->wait(process, &retval));
+       ck_assert_int_eq(retval, 0);
+}
+END_TEST
+
+Suite *process_suite_create()
+{
+       Suite *s;
+       TCase *tc;
+
+       s = suite_create("process");
+
+       tc = tcase_create("return values");
+       tcase_add_test(tc, test_retval_true);
+       tcase_add_test(tc, test_retval_false);
+       suite_add_tcase(s, tc);
+
+       tc = tcase_create("not found");
+       tcase_add_test(tc, test_not_found);
+       suite_add_tcase(s, tc);
+
+       tc = tcase_create("echo");
+       tcase_add_test(tc, test_echo);
+       tcase_add_test(tc, test_echo_err);
+       suite_add_tcase(s, tc);
+
+       tc = tcase_create("env");
+       tcase_add_test(tc, test_env);
+       suite_add_tcase(s, tc);
+
+       return s;
+}
index ab0f642..5862278 100644 (file)
@@ -24,6 +24,7 @@ TEST_SUITE(hashtable_suite_create)
 TEST_SUITE(array_suite_create)
 TEST_SUITE(identification_suite_create)
 TEST_SUITE(threading_suite_create)
+TEST_SUITE(process_suite_create)
 TEST_SUITE(watcher_suite_create)
 TEST_SUITE(stream_suite_create)
 TEST_SUITE(utils_suite_create)
diff --git a/src/libstrongswan/utils/process.c b/src/libstrongswan/utils/process.c
new file mode 100644 (file)
index 0000000..bcd8c76
--- /dev/null
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2014 Martin Willi
+ * Copyright (C) 2014 revosec AG
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+#include "process.h"
+
+#include <utils/debug.h>
+
+#include <fcntl.h>
+
+typedef struct private_process_t private_process_t;
+
+/**
+ * Ends of a pipe()
+ */
+enum {
+       PIPE_READ = 0,
+       PIPE_WRITE = 1,
+       PIPE_ENDS,
+};
+
+#ifndef WIN32
+
+#include <unistd.h>
+#include <errno.h>
+#include <sys/wait.h>
+
+/**
+ * Private data of an process_t object.
+ */
+struct private_process_t {
+
+       /**
+        * Public process_t interface.
+        */
+       process_t public;
+
+       /**
+        * child stdin pipe
+        */
+       int in[PIPE_ENDS];
+
+       /**
+        * child stdout pipe
+        */
+       int out[PIPE_ENDS];
+
+       /**
+        * child stderr pipe
+        */
+       int err[PIPE_ENDS];
+
+       /**
+        * child process
+        */
+       int pid;
+};
+
+/**
+ * Close a file descriptor if it is not -1
+ */
+static void close_if(int *fd)
+{
+       if (*fd != -1)
+       {
+               close(*fd);
+               *fd = -1;
+       }
+}
+
+/**
+ * Destroy a process structure, close all pipes
+ */
+static void process_destroy(private_process_t *this)
+{
+       close_if(&this->in[PIPE_READ]);
+       close_if(&this->in[PIPE_WRITE]);
+       close_if(&this->out[PIPE_READ]);
+       close_if(&this->out[PIPE_WRITE]);
+       close_if(&this->err[PIPE_READ]);
+       close_if(&this->err[PIPE_WRITE]);
+       free(this);
+}
+
+METHOD(process_t, wait_, bool,
+       private_process_t *this, int *code)
+{
+       int status, ret;
+
+       ret = waitpid(this->pid, &status, 0);
+       process_destroy(this);
+       if (ret == -1)
+       {
+               return FALSE;
+       }
+       if (!WIFEXITED(status))
+       {
+               return FALSE;
+       }
+       if (code)
+       {
+               *code = WEXITSTATUS(status);
+       }
+       return TRUE;
+}
+
+/**
+ * See header
+ */
+process_t* process_start(char *const argv[], char *const envp[],
+                                                int *in, int *out, int *err, bool close_all)
+{
+       private_process_t *this;
+       char *empty[] = { NULL };
+
+       INIT(this,
+               .public = {
+                       .wait = _wait_,
+               },
+               .in = { -1, -1 },
+               .out = { -1, -1 },
+               .err = { -1, -1 },
+       );
+
+       if (in && pipe(this->in) != 0)
+       {
+               DBG1(DBG_LIB, "creating stdin pipe failed: %s", strerror(errno));
+               process_destroy(this);
+               return NULL;
+       }
+       if (out && pipe(this->out) != 0)
+       {
+               DBG1(DBG_LIB, "creating stdout pipe failed: %s", strerror(errno));
+               process_destroy(this);
+               return NULL;
+       }
+       if (err && pipe(this->err) != 0)
+       {
+               DBG1(DBG_LIB, "creating stderr pipe failed: %s", strerror(errno));
+               process_destroy(this);
+               return NULL;
+       }
+
+       this->pid = fork();
+       switch (this->pid)
+       {
+               case -1:
+                       DBG1(DBG_LIB, "forking process failed: %s", strerror(errno));
+                       process_destroy(this);
+                       return NULL;
+               case 0:
+                       /* child */
+                       close_if(&this->in[PIPE_WRITE]);
+                       close_if(&this->out[PIPE_READ]);
+                       close_if(&this->err[PIPE_READ]);
+                       if (this->in[PIPE_READ] != -1)
+                       {
+                               if (dup2(this->in[PIPE_READ], 0) == -1)
+                               {
+                                       raise(SIGKILL);
+                               }
+                       }
+                       if (this->out[PIPE_WRITE] != -1)
+                       {
+                               if (dup2(this->out[PIPE_WRITE], 1) == -1)
+                               {
+                                       raise(SIGKILL);
+                               }
+                       }
+                       if (this->err[PIPE_WRITE] != -1)
+                       {
+                               if (dup2(this->err[PIPE_WRITE], 2) == -1)
+                               {
+                                       raise(SIGKILL);
+                               }
+                       }
+                       if (close_all)
+                       {
+                               closefrom(3);
+                       }
+                       if (execve(argv[0], argv, envp ?: empty) == -1)
+                       {
+                               raise(SIGKILL);
+                       }
+                       /* not reached */
+               default:
+                       /* parent */
+                       close_if(&this->in[PIPE_READ]);
+                       close_if(&this->out[PIPE_WRITE]);
+                       close_if(&this->err[PIPE_WRITE]);
+                       if (in)
+                       {
+                               *in = this->in[PIPE_WRITE];
+                               this->in[PIPE_WRITE] = -1;
+                       }
+                       if (out)
+                       {
+                               *out = this->out[PIPE_READ];
+                               this->out[PIPE_READ] = -1;
+                       }
+                       if (err)
+                       {
+                               *err = this->err[PIPE_READ];
+                               this->err[PIPE_READ] = -1;
+                       }
+                       return &this->public;
+       }
+}
+
+#else /* WIN32 */
+
+/**
+ * See header
+ */
+process_t* process_start(char *const argv[], char *const envp[],
+                                                int *in, int *out, int *err, bool close_all)
+{
+       return NULL;
+}
+
+#endif /* WIN32 */
diff --git a/src/libstrongswan/utils/process.h b/src/libstrongswan/utils/process.h
new file mode 100644 (file)
index 0000000..62d2ce7
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2014 Martin Willi
+ * Copyright (C) 2014 revosec AG
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+/**
+ * @defgroup process process
+ * @{ @ingroup utils
+ */
+
+#ifndef PROCESS_H_
+#define PROCESS_H_
+
+#include <utils/utils.h>
+
+typedef struct process_t process_t;
+
+/**
+ * Child process spawning abstraction
+ */
+struct process_t {
+
+       /**
+        * Wait for a started process to terminate.
+        *
+        * The process object gets destroyed by this call, regardless of the
+        * return value.
+        *
+        * The returned code is the exit code, not the status returned by waitpid().
+        * If the program could not be executed or has terminated abnormally
+        * (by signals etc.), FALSE is returned.
+        *
+        * @param code  process exit code, set only if TRUE returned
+        * @return              TRUE if program exited normally through exit()
+        */
+       bool (*wait)(process_t *this, int *code);
+};
+
+/**
+ * Spawn a child process with redirected I/O.
+ *
+ * Forks the current process, optionally redirects stdin/out/err to the current
+ * process, and executes the provided program with arguments.
+ *
+ * The process to execute is specified as argv[0], followed by the process
+ * arguments, followed by NULL. envp[] has a NULL terminated list of arguments
+ * to invoke the process with.
+ *
+ * If any of in/out/err is given, stdin/out/err from the child process get
+ * connected over pipe()s to the caller. If close_all is TRUE, all other
+ * open file descriptors get closed, regardless of any CLOEXEC setting.
+ *
+ * A caller must close all of the returned file descriptors to avoid file
+ * descriptor leaks.
+ *
+ * A non-NULL return value does not guarantee that the process has been
+ * invoked successfully.
+ *
+ * @param argv         NULL terminated process arguments, with argv[0] as program
+ * @param envp         NULL terminated list of environment variables
+ * @param in           pipe fd returned for redirecting data to child stdin
+ * @param out          pipe fd returned to redirect child stdout data to
+ * @param err          pipe fd returned to redirect child stderr data to
+ * @param close_all    close all open file descriptors above 2 before execve()
+ * @return                     process, NULL on failure
+ */
+process_t* process_start(char *const argv[], char *const envp[],
+                                                int *in, int *out, int *err, bool close_all);
+
+#endif /** PROCESS_H_ @}*/