]> git.localhorst.tv Git - blank.git/commitdiff
Process
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Thu, 17 Nov 2016 15:51:27 +0000 (16:51 +0100)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Thu, 17 Nov 2016 16:34:49 +0000 (17:34 +0100)
API for spawning and communicating with child processes

totally untested on win32; I'd be surprised if it even compiles

src/app/Process.hpp [new file with mode: 0644]
src/app/proc.cpp [new file with mode: 0644]
tst/app/ProcessTest.cpp [new file with mode: 0644]
tst/app/ProcessTest.hpp [new file with mode: 0644]

diff --git a/src/app/Process.hpp b/src/app/Process.hpp
new file mode 100644 (file)
index 0000000..985b661
--- /dev/null
@@ -0,0 +1,50 @@
+#ifndef BLANK_APP_PROCESS_HPP_
+#define BLANK_APP_PROCESS_HPP_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+
+namespace blank {
+
+class Process {
+
+public:
+       /// launch process executing the file at given path with
+       /// given arguments and environment
+       Process(
+               const std::string &path,
+               const std::vector<std::string> &args,
+               const std::vector<std::string> &env);
+       ~Process();
+
+public:
+       /// write to the process' input stream
+       /// data is taken from given buffer, at most max_len bytes
+       /// @return the number of bytes written
+       std::size_t WriteIn(const void *buffer, std::size_t max_len);
+       /// read from the process' output stream
+       /// data is stored in the given buffer, at most max_len bytes
+       /// @return the number of bytes read
+       std::size_t ReadOut(void *buffer, std::size_t max_len);
+       /// read from the process' error stream
+       /// data is stored in the given buffer, at most max_len bytes
+       /// @return the number of bytes read
+       std::size_t ReadErr(void *buffer, std::size_t max_len);
+
+       /// wait until the process exits and fetch its exit status
+       int Join();
+
+private:
+       struct Impl;
+       std::unique_ptr<Impl> impl;
+
+       bool joined;
+       int status;
+
+};
+
+}
+
+#endif
diff --git a/src/app/proc.cpp b/src/app/proc.cpp
new file mode 100644 (file)
index 0000000..8809856
--- /dev/null
@@ -0,0 +1,302 @@
+#include "Process.hpp"
+
+#ifdef __WIN32
+#  error "TODO: windows implementation of Process"
+#else
+#  include <cstdio>
+#  include <fcntl.h>
+#  include <unistd.h>
+#  include <sys/wait.h>
+#endif
+
+#include <stdexcept>
+
+using namespace std;
+
+
+namespace blank {
+
+struct Process::Impl {
+
+       Impl(
+               const string &path_in,
+               const vector<string> &args,
+               const vector<string> &env);
+       ~Impl();
+
+       size_t WriteIn(const void *buffer, size_t max_len);
+       size_t ReadOut(void *buffer, size_t max_len);
+       size_t ReadErr(void *buffer, size_t max_len);
+
+       int Join();
+
+#ifdef __WIN32
+       PROCESS_INFORMATION pi;
+       HANDLE fd_in[2];
+       HANDLE fd_out[2];
+       HANDLE fd_err[2];
+#else
+       int pid;
+       int fd_in[2];
+       int fd_out[2];
+       int fd_err[2];
+#endif
+
+};
+
+
+Process::Process(
+       const string &path,
+       const vector<string> &args,
+       const vector<string> &env)
+: impl(new Impl(path, args, env))
+, joined(false)
+, status(0) {
+
+}
+
+Process::~Process() {
+       Join();
+}
+
+
+size_t Process::WriteIn(const void *buffer, size_t max_len) {
+       return impl->WriteIn(buffer, max_len);
+}
+
+size_t Process::ReadOut(void *buffer, size_t max_len) {
+       return impl->ReadOut(buffer, max_len);
+}
+
+size_t Process::ReadErr(void *buffer, size_t max_len) {
+       return impl->ReadErr(buffer, max_len);
+}
+
+int Process::Join() {
+       if (joined) {
+               return status;
+       } else {
+               status = impl->Join();
+               joined = true;
+               return status;
+       }
+}
+
+
+Process::Impl::Impl(
+       const string &path_in,
+       const vector<string> &args,
+       const vector<string> &env
+) {
+       const char *path = path_in.c_str();
+       char *envp[env.size() + 1];
+       for (size_t i = 0; i < env.size(); ++i) {
+               envp[i] = const_cast<char *>(env[i].c_str());
+       }
+       envp[env.size()] = nullptr;
+#ifdef __WIN32
+       string cmdline;
+       for (const auto &arg : args) {
+               cmdline += '"';
+               cmdline += arg;
+               cmdline += '"';
+       }
+
+       SECURITY_ATTRIBUTES sa;
+       sa.nLength = sizeof(SECURITY_ATTRIBUTES);
+       sa.bInheritHandle = true;
+       sa.lpSecurityDescriptor = nullptr;
+       if (!CreatePipe(&fd_in[0], &fd_in[1], &sa, 0)) {
+               throw runtime_error("failed to open pipe for child process' stdin");
+       }
+       if (!SetHandleInformation(fd_in[1], HANDLE_FLAG_INHERIT, 0)) {
+               throw runtime_error("failed to stop child process from inheriting stdin write handle");
+       }
+       if (!CreatePipe(&fd_out[0], &fd_out[1], &sa, 0)) {
+               throw runtime_error("failed to open pipe for child process' stdout");
+       }
+       if (!SetHandleInformation(fd_out[0], HANDLE_FLAG_INHERIT, 0)) {
+               throw runtime_error("failed to stop child process from inheriting stdout read handle");
+       }
+       if (!CreatePipe(&fd_err[0], &fd_err[1], &sa, 0)) {
+               throw runtime_error("failed to open pipe for child process' stderr");
+       }
+       if (!SetHandleInformation(fd_err[0], HANDLE_FLAG_INHERIT, 0)) {
+               throw runtime_error("failed to stop child process from inheriting stderr read handle");
+       }
+
+       STARTUPINFO si;
+       ZeroMemory(&si, sizeof(STARTUPINFO));
+       si.cb = sizeof(STARTUPINFO);
+       si.hStdError = fd_err[1];
+       si.hStdOutput = fd_out[1];
+       si.hStdInput = fd_in[0];
+       si.dwFlags |= STARTF_USESTDHANDLES;
+       ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
+
+       if (!CreateProcess(
+               path,
+               cmdline.c_str(),
+               nullptr,
+               nullptr,
+               true,
+               0,
+               envp,
+               nullptr,
+               &si,
+               &pi
+       )) {
+               throw runtime_error("CreateProcess");
+       }
+#else
+       char *argv[args.size() + 1];
+       for (size_t i = 0; i < args.size(); ++i) {
+               // I was promised exec won't modify my characters
+               argv[i] = const_cast<char *>(args[i].c_str());
+       }
+       argv[args.size()] = nullptr;
+
+       if (pipe(fd_in) != 0) {
+               throw runtime_error("failed to open pipe for child process' stdin");
+       }
+       if (pipe(fd_out) != 0) {
+               throw runtime_error("failed to open pipe for child process' stdout");
+       }
+       if (pipe(fd_err) != 0) {
+               throw runtime_error("failed to open pipe for child process' stderr");
+       }
+
+       pid = fork();
+       if (pid == -1) {
+               throw runtime_error("fork");
+       } else if (pid == 0) {
+
+               if (dup2(fd_in[0], STDIN_FILENO) == -1) {
+                       exit(EXIT_FAILURE);
+               }
+               if (dup2(fd_out[1], STDOUT_FILENO) == -1) {
+                       exit(EXIT_FAILURE);
+               }
+               if (dup2(fd_err[1], STDERR_FILENO) == -1) {
+                       exit(EXIT_FAILURE);
+               }
+
+               close(fd_in[0]);
+               close(fd_in[1]);
+               close(fd_out[0]);
+               close(fd_out[1]);
+               close(fd_err[0]);
+               close(fd_err[1]);
+
+               execve(path, argv, envp);
+               // if execve returns, something bad happened
+               exit(EXIT_FAILURE);
+
+       } else {
+
+               close(fd_in[0]);
+               close(fd_out[1]);
+               close(fd_err[1]);
+
+       }
+}
+#endif
+
+Process::Impl::~Impl() {
+
+}
+
+size_t Process::Impl::WriteIn(const void *buffer, size_t max_len) {
+#ifdef __WIN32
+       DWORD written;
+       if (!WriteFile(fd_in[1], buffer, max_len, &written, nullptr)) {
+               throw runtime_error("failed to write to child process' input stream");
+       }
+       return written;
+#else
+       int written = write(fd_in[1], buffer, max_len);
+       if (written < 0) {
+               if (errno == EAGAIN) {
+                       return 0;
+               } else {
+                       throw runtime_error("failed to write to child process' input stream");
+               }
+       }
+       return written;
+#endif
+}
+
+size_t Process::Impl::ReadOut(void *buffer, size_t max_len) {
+#ifdef __WIN32
+       DWORD ret;
+       if (!ReadFile(fd_out[0], buffer, max_len, &ret, nullptr)) {
+               throw runtime_error("failed to read from child process' output stream");
+       }
+       return ret;
+#else
+       int ret = read(fd_out[0], buffer, max_len);
+       if (ret < 0) {
+               if (errno == EAGAIN) {
+                       return 0;
+               } else {
+                       throw runtime_error("failed to read from child process' output stream");
+               }
+       }
+       return ret;
+#endif
+}
+
+size_t Process::Impl::ReadErr(void *buffer, size_t max_len) {
+#ifdef __WIN32
+       DWORD ret;
+       if (!ReadFile(fd_err[0], buffer, max_len, &ret, nullptr)) {
+               throw runtime_error("failed to read from child process' error stream");
+       }
+       return ret;
+#else
+       int ret = read(fd_err[0], buffer, max_len);
+       if (ret < 0) {
+               if (errno == EAGAIN) {
+                       return 0;
+               } else {
+                       throw runtime_error("failed to read from child process' error stream");
+               }
+       }
+       return ret;
+#endif
+}
+
+int Process::Impl::Join() {
+#ifdef __WIN32
+       CloseHandle(fd_in[1]);
+       CloseHandle(fd_out[0]);
+       CloseHandle(fd_err[0]);
+
+       DWORD exit_code;
+       WaitForSingleObject(pi.hProcess, INFINITE);
+       GetExitCodeProcess(pi.hProcess, &exit_code);
+       CloseHandle(pi.hProcess);
+       CloseHandle(pi.hThread);
+       return exit_code;
+#else
+       // close streams before waiting on child termination
+       close(fd_in[1]);
+       close(fd_out[0]);
+       close(fd_err[0]);
+
+       while (true) {
+               int status;
+               int result = waitpid(pid, &status, 0);
+               if (result == -1) {
+                       throw runtime_error("error waiting on child process");
+               }
+               if (result == pid && WIFEXITED(status)) {
+                       return WEXITSTATUS(status);
+               }
+               // otherwise, child probably signalled, which we don't care
+               // about (please don't tell youth welfare), so try again
+       }
+#endif
+}
+
+}
diff --git a/tst/app/ProcessTest.cpp b/tst/app/ProcessTest.cpp
new file mode 100644 (file)
index 0000000..778b7f6
--- /dev/null
@@ -0,0 +1,118 @@
+#include "ProcessTest.hpp"
+
+#include "app/Process.hpp"
+
+CPPUNIT_TEST_SUITE_REGISTRATION(blank::test::ProcessTest);
+
+using namespace std;
+
+
+namespace blank {
+namespace test {
+
+void ProcessTest::setUp() {
+
+}
+
+void ProcessTest::tearDown() {
+
+}
+
+
+void ProcessTest::testExit() {
+#ifdef __WIN32
+#  error "TODO: implemente Process tests for windows"
+#else
+
+       {
+               Process proc("/usr/bin/env", { "env", "true" }, { });
+               int status = proc.Join();
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "exit status of true assumed 0",
+                       0, status);
+       }
+
+       {
+               Process proc("/usr/bin/env", { "env", "false" }, { });
+               int status = proc.Join();
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "exit status of false assumed 1",
+                       1, status);
+       }
+
+#endif
+}
+
+void ProcessTest::testStream() {
+#ifdef __WIN32
+#  error "TODO: implemente Process tests for windows"
+#else
+
+       {
+               const string test_input("hello, world");
+               const string expected_output("hello, world\n");
+               Process proc("/usr/bin/env", { "env", "echo", test_input.c_str() }, { });
+               char buffer[expected_output.length() + 1];
+               size_t len = proc.ReadOut(buffer, sizeof(buffer));
+               const string output(buffer, len);
+               int status = proc.Join();
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "exit status of echo assumed 0",
+                       0, status);
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "unexpected length of echo output",
+                       expected_output.size(), len);
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "unexpected output of echo",
+                       expected_output, output);
+       }
+
+       {
+               const string test_input("hello, error");
+               const string expected_output("hello, error\n");
+               Process proc("/usr/bin/env", { "env", "sh", "-c", "echo $1 >&2", "echo", test_input.c_str() }, { });
+               char buffer[expected_output.length() + 1];
+               size_t len = proc.ReadErr(buffer, sizeof(buffer));
+               const string output(buffer, len);
+               int status = proc.Join();
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "exit status of echo assumed 0",
+                       0, status);
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "unexpected length of echo output",
+                       expected_output.size(), len);
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "unexpected error output of echo",
+                       expected_output, output);
+       }
+
+
+       {
+               const string test_input("dog");
+               const string expected_output("dog");
+               Process proc("/usr/bin/env", { "env", "cat" }, { });
+               size_t len = proc.WriteIn(test_input.c_str(), test_input.size());
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "unexpected length of input to cat",
+                       test_input.size(), len);
+
+               char buffer[expected_output.length() + 1];
+               len = proc.ReadOut(buffer, sizeof(buffer));
+               const string output(buffer, len);
+               int status = proc.Join();
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "exit status of cat assumed 0",
+                       0, status);
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "unexpected length of cat output",
+                       expected_output.size(), len);
+               CPPUNIT_ASSERT_EQUAL_MESSAGE(
+                       "unexpected output of cat",
+                       expected_output, output);
+       }
+
+#endif
+}
+
+}
+}
diff --git a/tst/app/ProcessTest.hpp b/tst/app/ProcessTest.hpp
new file mode 100644 (file)
index 0000000..972fbe5
--- /dev/null
@@ -0,0 +1,32 @@
+#ifndef BLANK_TEST_APP_PROCESSTEST_HPP_
+#define BLANK_TEST_APP_PROCESSTEST_HPP_
+
+#include <cppunit/extensions/HelperMacros.h>
+
+
+namespace blank {
+namespace test {
+
+class ProcessTest
+: public CppUnit::TestFixture {
+
+CPPUNIT_TEST_SUITE(ProcessTest);
+
+CPPUNIT_TEST(testExit);
+CPPUNIT_TEST(testStream);
+
+CPPUNIT_TEST_SUITE_END();
+
+public:
+       void setUp();
+       void tearDown();
+
+       void testExit();
+       void testStream();
+
+};
+
+}
+}
+
+#endif