aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAvatar Ciaran McCreesh <ciaran.mccreesh@googlemail.com> 2010-08-19 17:02:21 +0100
committerAvatar Ciaran McCreesh <ciaran.mccreesh@googlemail.com> 2010-08-21 14:58:15 +0100
commitef7126699addb2a5a3bf74dfba5fd5c1f44b5b93 (patch)
tree6e6f5c1b1ccfc5c395ed254083a8e5f61bcb76f2
parentadda76229c6a59054e137fcc3120891d2c1aa9c8 (diff)
downloadpaludis-ef7126699addb2a5a3bf74dfba5fd5c1f44b5b93.tar.gz
paludis-ef7126699addb2a5a3bf74dfba5fd5c1f44b5b93.tar.xz
New, less horrible way of running processes
-rw-r--r--.gitignore1
-rw-r--r--paludis/util/files.m41
-rw-r--r--paludis/util/process-fwd.hh35
-rw-r--r--paludis/util/process.cc355
-rw-r--r--paludis/util/process.hh96
-rw-r--r--paludis/util/process_TEST.cc149
6 files changed, 637 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 7c93ae1..2c0ee71 100644
--- a/.gitignore
+++ b/.gitignore
@@ -455,6 +455,7 @@ paludis-*.*.*.tar.bz2
/paludis/util/output_wrapper_TEST
/paludis/util/outputwrapper
/paludis/util/pretty_print_TEST
+/paludis/util/process_TEST
/paludis/util/pty_TEST
/paludis/util/random_TEST
/paludis/util/realpath_TEST
diff --git a/paludis/util/files.m4 b/paludis/util/files.m4
index 9f8e241..11ca69c 100644
--- a/paludis/util/files.m4
+++ b/paludis/util/files.m4
@@ -62,6 +62,7 @@ add(`output_wrapper', `test', `testscript')
add(`pimp', `hh', `impl')
add(`pipe', `hh', `cc')
add(`pretty_print', `hh', `cc', `test')
+add(`process', `hh', `cc', `fwd', `test')
add(`pty', `hh', `cc', `test')
add(`random', `hh', `cc', `test')
add(`realpath', `hh', `cc', `test', `testscript')
diff --git a/paludis/util/process-fwd.hh b/paludis/util/process-fwd.hh
new file mode 100644
index 0000000..35fcd2c
--- /dev/null
+++ b/paludis/util/process-fwd.hh
@@ -0,0 +1,35 @@
+/* vim: set sw=4 sts=4 et foldmethod=syntax : */
+
+/*
+ * Copyright (c) 2010 Ciaran McCreesh
+ *
+ * This file is part of the Paludis package manager. Paludis is free software;
+ * you can redistribute it and/or modify it under the terms of the GNU General
+ * Public License version 2, as published by the Free Software Foundation.
+ *
+ * Paludis 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.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#ifndef PALUDIS_GUARD_PALUDIS_UTIL_PROCESS_FWD_HH
+#define PALUDIS_GUARD_PALUDIS_UTIL_PROCESS_FWD_HH 1
+
+namespace paludis
+{
+ struct ProcessError;
+
+ struct ProcessCommand;
+
+ struct Process;
+
+ struct RunningProcessThread;
+ struct RunningProcessHandle;
+}
+
+#endif
diff --git a/paludis/util/process.cc b/paludis/util/process.cc
new file mode 100644
index 0000000..b6d83f3
--- /dev/null
+++ b/paludis/util/process.cc
@@ -0,0 +1,355 @@
+/* vim: set sw=4 sts=4 et foldmethod=syntax : */
+
+/*
+ * Copyright (c) 2010 Ciaran McCreesh
+ *
+ * This file is part of the Paludis package manager. Paludis is free software;
+ * you can redistribute it and/or modify it under the terms of the GNU General
+ * Public License version 2, as published by the Free Software Foundation.
+ *
+ * Paludis 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.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include <paludis/util/process.hh>
+#include <paludis/util/pimp-impl.hh>
+#include <paludis/util/thread.hh>
+#include <paludis/util/pipe.hh>
+
+#include <iostream>
+#include <functional>
+#include <algorithm>
+#include <vector>
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/select.h>
+
+using namespace paludis;
+
+ProcessError::ProcessError(const std::string & s) throw () :
+ Exception(s)
+{
+}
+
+namespace paludis
+{
+ template <>
+ struct Imp<ProcessCommand>
+ {
+ std::vector<std::string> args;
+
+ Imp(std::vector<std::string> && i) :
+ args(i)
+ {
+ }
+ };
+}
+
+ProcessCommand::ProcessCommand(const std::initializer_list<std::string> & i) :
+ Pimp<ProcessCommand>(std::vector<std::string>(i))
+{
+}
+
+ProcessCommand::ProcessCommand(ProcessCommand && other) :
+ Pimp<ProcessCommand>(std::move(other._imp->args))
+{
+}
+
+ProcessCommand::~ProcessCommand() = default;
+
+void
+ProcessCommand::exec()
+{
+ if (_imp->args.size() < 1)
+ throw ProcessError("No command specified");
+
+ /* no need to worry about free()ing this lot, since if our execvp fails we
+ * call _exit() shortly afterwards */
+
+ char ** argv(new char * [_imp->args.size() + 1]);
+ argv[_imp->args.size()] = 0;
+ for (auto v_begin(_imp->args.begin()), v(v_begin), v_end(_imp->args.end()) ;
+ v != v_end ; ++v)
+ {
+ argv[v - v_begin] = new char [v->length() + 1];
+ argv[v - v_begin][v->length()] = '\0';
+ std::copy(v->begin(), v->end(), argv[v - v_begin]);
+ }
+
+ execvp(_imp->args[0].c_str(), argv);
+
+ throw ProcessError("execvp failed");
+}
+
+namespace paludis
+{
+ struct RunningProcessThread
+ {
+ Pipe ctl_pipe;
+
+ std::ostream * capture_stdout;
+ std::unique_ptr<Pipe> capture_stdout_pipe;
+
+ std::ostream * capture_stderr;
+ std::unique_ptr<Pipe> capture_stderr_pipe;
+
+ /* must be last, so the thread gets join()ed before its FDs vanish */
+ std::unique_ptr<Thread> thread;
+
+ RunningProcessThread() :
+ ctl_pipe(true),
+ capture_stdout(0),
+ capture_stderr(0)
+ {
+ }
+
+ void thread_func();
+
+ void start();
+ };
+}
+
+void
+RunningProcessThread::thread_func()
+{
+ bool done(false);
+ while (! done)
+ {
+ fd_set read_fds, write_fds;
+ int max_fd(0);
+ FD_ZERO(&read_fds);
+ FD_ZERO(&write_fds);
+
+ FD_SET(ctl_pipe.read_fd(), &read_fds);
+ max_fd = std::max(max_fd, ctl_pipe.read_fd());
+
+ if (capture_stdout_pipe)
+ {
+ FD_SET(capture_stdout_pipe->read_fd(), &read_fds);
+ max_fd = std::max(max_fd, capture_stdout_pipe->read_fd());
+ }
+
+ if (capture_stderr_pipe)
+ {
+ FD_SET(capture_stderr_pipe->read_fd(), &read_fds);
+ max_fd = std::max(max_fd, capture_stderr_pipe->read_fd());
+ }
+
+ int retval(::pselect(max_fd + 1, &read_fds, &write_fds, 0, 0, 0));
+ if (-1 == retval)
+ throw ProcessError("pselect() failed");
+
+ bool done_anything(false);
+ char buf[4096];
+
+ if (capture_stdout_pipe && FD_ISSET(capture_stdout_pipe->read_fd(), &read_fds))
+ {
+ int n(::read(capture_stdout_pipe->read_fd(), &buf, sizeof(buf)));
+ if (-1 == n)
+ throw ProcessError("read() capture_stdout_pipe read_fd failed");
+ else if (0 != n)
+ capture_stdout->write(buf, n);
+ done_anything = true;
+ }
+
+ if (capture_stderr_pipe && FD_ISSET(capture_stderr_pipe->read_fd(), &read_fds))
+ {
+ int n(::read(capture_stderr_pipe->read_fd(), &buf, sizeof(buf)));
+ if (-1 == n)
+ throw ProcessError("read() capture_stderr_pipe read_fd failed");
+ else if (0 != n)
+ capture_stderr->write(buf, n);
+ done_anything = true;
+ }
+
+ if (done_anything)
+ continue;
+
+ /* don't do this until nothing else has anything to do */
+ if (FD_ISSET(ctl_pipe.read_fd(), &read_fds))
+ {
+ char c('?');
+ if (1 != ::read(ctl_pipe.read_fd(), &c, 1))
+ throw ProcessError("read() on our ctl pipe failed");
+ else if (c != 'x')
+ throw ProcessError("read() on our ctl pipe gave '" + std::string(1, c) + "' not 'x'");
+ done = true;
+ }
+ }
+}
+
+void
+RunningProcessThread::start()
+{
+ thread.reset(new Thread(std::bind(&RunningProcessThread::thread_func, this)));
+}
+
+namespace paludis
+{
+ template <>
+ struct Imp<Process>
+ {
+ ProcessCommand command;
+
+ bool need_thread;
+ std::ostream * capture_stdout;
+ std::ostream * capture_stderr;
+
+ Imp(ProcessCommand && c) :
+ command(std::move(c)),
+ need_thread(false),
+ capture_stdout(0),
+ capture_stderr(0)
+ {
+ }
+ };
+}
+
+Process::Process(ProcessCommand && c) :
+ Pimp<Process>(c)
+{
+}
+
+Process::~Process()
+{
+}
+
+RunningProcessHandle
+Process::run()
+{
+ std::unique_ptr<RunningProcessThread> thread;
+ if (_imp->need_thread)
+ {
+ thread.reset(new RunningProcessThread{});
+
+ if (_imp->capture_stdout)
+ {
+ thread->capture_stdout = _imp->capture_stdout;
+ thread->capture_stdout_pipe.reset(new Pipe(true));
+ }
+
+ if (_imp->capture_stderr)
+ {
+ thread->capture_stderr = _imp->capture_stderr;
+ thread->capture_stderr_pipe.reset(new Pipe(true));
+ }
+ }
+
+ pid_t child(fork());
+ if (-1 == child)
+ throw ProcessError("fork() failed");
+ else if (0 == child)
+ {
+ try
+ {
+ if (thread && thread->capture_stdout_pipe)
+ {
+ if (-1 == ::dup2(thread->capture_stdout_pipe->write_fd(), STDOUT_FILENO))
+ throw ProcessError("dup2() failed");
+ }
+
+ if (thread && thread->capture_stderr_pipe)
+ {
+ if (-1 == ::dup2(thread->capture_stderr_pipe->write_fd(), STDERR_FILENO))
+ throw ProcessError("dup2() failed");
+ }
+
+ _imp->command.exec();
+ }
+ catch (const ProcessError & e)
+ {
+ std::cerr << "Eek! child thread got error '" << e.message() << "'" << std::endl;
+ }
+ _exit(1);
+ }
+ else
+ {
+ if (thread)
+ thread->start();
+ return RunningProcessHandle(child, thread);
+ }
+}
+
+Process &
+Process::capture_stdout(std::ostream & s)
+{
+ _imp->need_thread = true;
+ _imp->capture_stdout = &s;
+ return *this;
+}
+
+Process &
+Process::capture_stderr(std::ostream & s)
+{
+ _imp->need_thread = true;
+ _imp->capture_stderr = &s;
+ return *this;
+}
+
+namespace paludis
+{
+ template <>
+ struct Imp<RunningProcessHandle>
+ {
+ pid_t pid;
+ std::unique_ptr<RunningProcessThread> thread;
+
+ Imp(pid_t p, std::unique_ptr<RunningProcessThread> && t) :
+ pid(p),
+ thread(std::move(t))
+ {
+ }
+ };
+}
+
+RunningProcessHandle::RunningProcessHandle(pid_t p, std::unique_ptr<RunningProcessThread> && t) :
+ Pimp<RunningProcessHandle>(p, t)
+{
+}
+
+RunningProcessHandle::~RunningProcessHandle()
+{
+ if (-1 != _imp->pid)
+ {
+ int dummy PALUDIS_ATTRIBUTE((unused)) (wait());
+ throw ProcessError("Didn't wait()");
+ }
+}
+
+RunningProcessHandle::RunningProcessHandle(RunningProcessHandle && other) :
+ Pimp<RunningProcessHandle>(other._imp->pid, other._imp->thread)
+{
+ _imp->pid = -1;
+}
+
+int
+RunningProcessHandle::wait()
+{
+ if (-1 == _imp->pid)
+ throw ProcessError("Already wait()ed");
+
+ int status(0);
+ if (-1 == ::waitpid(_imp->pid, &status, 0))
+ throw ProcessError("waitpid() returned -1");
+ _imp->pid = -1;
+
+ if (_imp->thread)
+ {
+ char c('x');
+ if (1 != ::write(_imp->thread->ctl_pipe.write_fd(), &c, 1))
+ throw ProcessError("write() on our ctl pipe failed");
+
+ _imp->thread.reset();
+ }
+
+ return (WIFSIGNALED(status) ? WTERMSIG(status) + 128 : WEXITSTATUS(status));
+}
+
diff --git a/paludis/util/process.hh b/paludis/util/process.hh
new file mode 100644
index 0000000..b9515f8
--- /dev/null
+++ b/paludis/util/process.hh
@@ -0,0 +1,96 @@
+/* vim: set sw=4 sts=4 et foldmethod=syntax : */
+
+/*
+ * Copyright (c) 2010 Ciaran McCreesh
+ *
+ * This file is part of the Paludis package manager. Paludis is free software;
+ * you can redistribute it and/or modify it under the terms of the GNU General
+ * Public License version 2, as published by the Free Software Foundation.
+ *
+ * Paludis 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.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#ifndef PALUDIS_GUARD_PALUDIS_UTIL_PROCESS_HH
+#define PALUDIS_GUARD_PALUDIS_UTIL_PROCESS_HH 1
+
+#include <paludis/util/process-fwd.hh>
+#include <paludis/util/attributes.hh>
+#include <paludis/util/pimp.hh>
+#include <paludis/util/exception.hh>
+
+#include <string>
+#include <iosfwd>
+#include <memory>
+#include <initializer_list>
+
+#include <unistd.h>
+
+namespace paludis
+{
+ class PALUDIS_VISIBLE ProcessError :
+ public Exception
+ {
+ public:
+ ProcessError(const std::string &) throw ();
+ };
+
+ class PALUDIS_VISIBLE ProcessCommand :
+ private Pimp<ProcessCommand>
+ {
+ public:
+ /**
+ * List of arguments, one string per argv value.
+ **/
+ explicit ProcessCommand(const std::initializer_list<std::string> &);
+
+ ProcessCommand(ProcessCommand &&);
+ ~ProcessCommand();
+
+ ProcessCommand(const ProcessCommand &) = delete;
+ ProcessCommand & operator= (const ProcessCommand &) = delete;
+
+ void exec() PALUDIS_ATTRIBUTE((noreturn));
+ };
+
+ class PALUDIS_VISIBLE Process :
+ private Pimp<Process>
+ {
+ public:
+ explicit Process(ProcessCommand &&);
+ ~Process();
+
+ Process(const Process &) = delete;
+ Process & operator= (const Process &) = delete;
+
+ RunningProcessHandle run() PALUDIS_ATTRIBUTE((warn_unused_result));
+
+ Process & capture_stdout(std::ostream &);
+ Process & capture_stderr(std::ostream &);
+ };
+
+ class PALUDIS_VISIBLE RunningProcessHandle :
+ private Pimp<RunningProcessHandle>
+ {
+ public:
+ RunningProcessHandle(
+ const pid_t,
+ std::unique_ptr<RunningProcessThread> &&);
+
+ ~RunningProcessHandle();
+ RunningProcessHandle(RunningProcessHandle &&);
+
+ RunningProcessHandle(const RunningProcessHandle &) = delete;
+ RunningProcessHandle & operator= (const RunningProcessHandle &) = delete;
+
+ int wait() PALUDIS_ATTRIBUTE((warn_unused_result));
+ };
+}
+
+#endif
diff --git a/paludis/util/process_TEST.cc b/paludis/util/process_TEST.cc
new file mode 100644
index 0000000..9bf0666
--- /dev/null
+++ b/paludis/util/process_TEST.cc
@@ -0,0 +1,149 @@
+/* vim: set sw=4 sts=4 et foldmethod=syntax : */
+
+/*
+ * Copyright (c) 2010 Ciaran McCreesh
+ *
+ * This file is part of the Paludis package manager. Paludis is free software;
+ * you can redistribute it and/or modify it under the terms of the GNU General
+ * Public License version 2, as published by the Free Software Foundation.
+ *
+ * Paludis 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.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
+ * Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+
+#include <paludis/util/process.hh>
+#include <test/test_framework.hh>
+#include <test/test_runner.hh>
+#include <sstream>
+
+using namespace paludis;
+using namespace test;
+
+
+namespace test_cases
+{
+ struct TrueTest : TestCase
+ {
+ TrueTest() : TestCase("true") { }
+
+ void run()
+ {
+ Process true_process(ProcessCommand({"true"}));
+ TEST_CHECK_EQUAL(true_process.run().wait(), 0);
+ }
+ } test_true;
+
+ struct FalseTest : TestCase
+ {
+ FalseTest() : TestCase("false") { }
+
+ void run()
+ {
+ Process false_process(ProcessCommand({"false"}));
+ TEST_CHECK_EQUAL(false_process.run().wait(), 1);
+ }
+ } test_false;
+
+ struct NoWaitTest : TestCase
+ {
+ NoWaitTest() : TestCase("no wait") { }
+
+ void run()
+ {
+ Process true_process(ProcessCommand({"true"}));
+ TEST_CHECK_THROWS({ RunningProcessHandle handle(true_process.run()); }, ProcessError);
+ }
+ } test_no_wait;
+
+ struct TwoWaitTest : TestCase
+ {
+ TwoWaitTest() : TestCase("two wait") { }
+
+ void run()
+ {
+ Process true_process(ProcessCommand({"true"}));
+
+ RunningProcessHandle handle(true_process.run());
+ TEST_CHECK_EQUAL(handle.wait(), 0);
+ TEST_CHECK_THROWS(int PALUDIS_ATTRIBUTE((unused)) x(handle.wait()), ProcessError);
+ }
+ } test_two_wait;
+
+ struct GrabStdoutTest : TestCase
+ {
+ GrabStdoutTest() : TestCase("grab stdout") { }
+
+ void run()
+ {
+ std::stringstream stdout_stream;
+ Process echo_process(ProcessCommand({"echo", "monkey"}));
+ echo_process.capture_stdout(stdout_stream);
+
+ TEST_CHECK_EQUAL(echo_process.run().wait(), 0);
+ TEST_CHECK_EQUAL(stdout_stream.str(), "monkey\n");
+ }
+ } test_grab_stdout;
+
+ struct GrabStderrTest : TestCase
+ {
+ GrabStderrTest() : TestCase("grab stderr") { }
+
+ void run()
+ {
+ std::stringstream stderr_stream;
+ Process echo_process(ProcessCommand({"sh", "-c", "echo monkey 1>&2"}));
+ echo_process.capture_stderr(stderr_stream);
+
+ TEST_CHECK_EQUAL(echo_process.run().wait(), 0);
+ TEST_CHECK_EQUAL(stderr_stream.str(), "monkey\n");
+ }
+ } test_grab_stderr;
+
+ struct GrabStdoutStderrTest : TestCase
+ {
+ GrabStdoutStderrTest() : TestCase("grab stdout stderr") { }
+
+ void run()
+ {
+ std::stringstream stdout_stream, stderr_stream;
+ Process echo_process(ProcessCommand({"sh", "-c", "echo monkey 1>&2 ; echo chimp"}));
+ echo_process.capture_stdout(stdout_stream);
+ echo_process.capture_stderr(stderr_stream);
+
+ TEST_CHECK_EQUAL(echo_process.run().wait(), 0);
+
+ TEST_CHECK_EQUAL(stdout_stream.str(), "chimp\n");
+ TEST_CHECK_EQUAL(stderr_stream.str(), "monkey\n");
+ }
+ } test_grab_stdout_stderr;
+
+ struct GrabStdoutLongTest : TestCase
+ {
+ GrabStdoutLongTest() : TestCase("grab stdout long") { }
+
+ void run()
+ {
+ std::stringstream stdout_stream;
+ Process echo_process(ProcessCommand({"seq", "1", "100000"}));
+ echo_process.capture_stdout(stdout_stream);
+
+ TEST_CHECK_EQUAL(echo_process.run().wait(), 0);
+
+ std::string s;
+ for (int x(1) ; x <= 100000 ; ++x)
+ {
+ TEST_CHECK(std::getline(stdout_stream, s));
+ TEST_CHECK_STRINGIFY_EQUAL(s, stringify(x));
+ }
+
+ TEST_CHECK(! std::getline(stdout_stream, s));
+ }
+ } test_grab_stdout_long;
+}
+