From bc56159d8eb62473b3e957cc2a58aed6741698e0 Mon Sep 17 00:00:00 2001 From: Matthew Fernandez Date: Sat, 22 Jun 2019 17:58:00 -0700 Subject: [PATCH] add support for running an external command Github: related to #130 "interact with SMT solver" --- rumur/CMakeLists.txt | 1 + rumur/src/process.cc | 329 +++++++++++++++++++++++++++++++++++++++++++ rumur/src/process.h | 12 ++ 3 files changed, 342 insertions(+) create mode 100644 rumur/src/process.cc create mode 100644 rumur/src/process.h diff --git a/rumur/CMakeLists.txt b/rumur/CMakeLists.txt index 557e8163..73d073f5 100644 --- a/rumur/CMakeLists.txt +++ b/rumur/CMakeLists.txt @@ -50,6 +50,7 @@ add_executable(rumur src/max-simple-width.cc src/options.cc src/output.cc + src/process.cc src/symmetry-reduction.cc src/utils.cc src/ValueType.cc) diff --git a/rumur/src/process.cc b/rumur/src/process.cc new file mode 100644 index 00000000..6fa97c45 --- /dev/null +++ b/rumur/src/process.cc @@ -0,0 +1,329 @@ +#include +#include +#include +#include +#include +#include +#include "process.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// compile this file with -DTEST_PROCESS to build a test harness +#ifdef TEST_PROCESS + #define debug (&std::cerr) +#else + #include "log.h" +#endif + +#ifdef __APPLE__ + #include +#endif + +enum { READ_FD = 0, WRITE_FD = 1 }; + +// pipe through which we'll redirect SIGCHLD notifications +static int sigchld_pipe[2] = { -1, -1 }; + +// signal handler to redirect SIGCHLD into the above pipe +static void handler(int sigchld __attribute__((unused))) { + + assert(sigchld == SIGCHLD && "SIGCHLD handler received something other than " + "SIGCHLD"); + + assert(sigchld_pipe[WRITE_FD] != -1 && "SIGCHLD handler called before " + "SIGCHLD pipe has been setup"); + + (void)write(sigchld_pipe[WRITE_FD], "\0", 1); +} + +static bool inited = false; + +static int init(void) { + + if (pipe(sigchld_pipe) < 0) { + *debug << "failed to create SIGCHLD pipe\n"; + goto fail; + } + + // set the SIGCHLD pipe not to block on reading or writing + { + int r = sigchld_pipe[READ_FD]; + int w = sigchld_pipe[WRITE_FD]; + if (fcntl(r, F_SETFD, fcntl(r, F_GETFD) | O_NONBLOCK) == -1 || + fcntl(w, F_SETFD, fcntl(w, F_GETFD) | O_NONBLOCK) == -1) { + *debug << "failed to set SIGCHLD pipe non-blocking\n"; + goto fail; + } + } + + // register our redirecting signal handler + if (signal(SIGCHLD, handler) == SIG_ERR) { + *debug << "failed to set SIGCHLD handler: " << strerror(errno) << "\n"; + goto fail; + } + + inited = true; + + return 0; + +fail: + for (int &fd : sigchld_pipe) { + if (fd != -1) { + (void)close(fd); + fd = -1; + } + } + return -1; +} + +static char **get_environ() { +#if __APPLE__ + /* Bizarrely Apple don't give programs a symbol for environ, but have an + * indirect way of accessing it. + */ + return *_NSGetEnviron(); +#else + return environ; +#endif +} + +static int max(int a, int b) { + return a > b ? a : b; +} + +int run(const std::vector &args, const std::string &input, + std::string &output) { + + if (!inited) { + if (init() < 0) + return -1; + } + + // setup an argument vector + std::vector argv; + for (const std::string &a : args) + argv.push_back(const_cast(a.c_str())); + argv.push_back(nullptr); + + + posix_spawn_file_actions_t fa; + int in[2] = { -1, -1 }; + int out[2] = { -1, -1 }; + int ret = -1; + std::ostringstream buffered_output; + off_t input_offset = 0; + + + if (posix_spawn_file_actions_init(&fa) < 0) { + *debug << "failed file_actions_init: " << strerror(errno) << "\n"; + return -1; + } + + // create some pipes we'll use to communicate with the child + if (pipe(in) < 0 || pipe(out) < 0) { + *debug<< "failed pipe: " << strerror(errno) << "\n"; + goto done; + } + + // set the ends the parent (us) will use as non-blocking + if (fcntl(in[WRITE_FD], F_SETFD, fcntl(in[WRITE_FD], F_GETFD) | O_NONBLOCK) == -1 || + fcntl(out[READ_FD], F_SETFD, fcntl(out[READ_FD], F_GETFD) | O_NONBLOCK) == -1) { + *debug << "failed to set O_NONBLOCK: " << strerror(errno) << "\n"; + goto done; + } + + // have the child close the ends it won't need + if (posix_spawn_file_actions_addclose(&fa, in[WRITE_FD]) < 0 || + posix_spawn_file_actions_addclose(&fa, out[READ_FD]) < 0) { + *debug << "failed file_actions_addclose: " << strerror(errno) << "\n"; + goto done; + } + + // replace the child's stdin, stdout and stderr with the pipes + if (posix_spawn_file_actions_adddup2(&fa, in[READ_FD], STDIN_FILENO) < 0 || + posix_spawn_file_actions_adddup2(&fa, out[WRITE_FD], STDOUT_FILENO) < 0 || + posix_spawn_file_actions_adddup2(&fa, out[WRITE_FD], STDERR_FILENO) < 0) { + *debug << "failed file_actions_adddup2: " << strerror(errno) << "\n"; + goto done; + } + + // spawn the child + pid_t pid; + if (posix_spawnp(&pid, argv[0], &fa, nullptr, argv.data(), get_environ()) != 0) { + *debug << "failed posix_spawnp: " << strerror(errno) << "\n"; + goto done; + } + + // close the ends of the pipes we (the parent) don't need + (void)close(in[READ_FD]); + in[READ_FD] = -1; + (void)close(out[WRITE_FD]); + out[WRITE_FD] = -1; + + // now we're ready to sit in an event loop interacting with the child + for (;;) { + + int nfds; + + // create a set of the out pipe and SIGCHLD to monitor for reading + fd_set readfds; + FD_ZERO(&readfds); + FD_SET(out[READ_FD], &readfds); + FD_SET(sigchld_pipe[READ_FD], &readfds); + nfds = max(out[READ_FD], sigchld_pipe[READ_FD]); + + // create a set of only the in pipe (if still open) to monitor for writing + fd_set writefds; + FD_ZERO(&writefds); + if (in[WRITE_FD] != -1) { + FD_SET(in[WRITE_FD], &writefds); + nfds = max(nfds, in[WRITE_FD]); + } + + // wait for an event + if (select(nfds, &readfds, &writefds, nullptr, nullptr) < 0) { + // if our select call is correct, and any "error" should be an interrupt + assert(errno == EAGAIN || errno == EINTR); + } + + // clear any SIGCHLD notification + if (FD_ISSET(sigchld_pipe[READ_FD], &readfds)) { + char ignored[BUFSIZ]; + (void)read(sigchld_pipe[READ_FD], ignored, sizeof(ignored)); + } + + // read any data available from the child + if (FD_ISSET(out[READ_FD], &readfds)) { + ssize_t r; + do { + + char buffer[BUFSIZ]; + do { + r = read(out[READ_FD], buffer, sizeof(buffer)); + } while (r == -1 && errno == EINTR); + + if (r == -1) { + if (errno == EAGAIN) + break; + *debug << "failed to read from child: " << strerror(errno) << "\n"; + goto done; + } + + // retain anything we read + for (size_t i = 0; i < (size_t)r; i++) + buffered_output << buffer[i]; + + } while (r > 0); + } + + // write remaining data if the input pipe is ready + if (in[WRITE_FD] != -1 && FD_ISSET(in[WRITE_FD], &writefds)) { + if ((size_t)input_offset < input.size()) { + + ssize_t w; + do { + w = write(in[WRITE_FD], input.c_str() + input_offset, + input.size() - input_offset); + } while (w == -1 && errno == EINTR); + + if (w == -1 && errno != EAGAIN) { + *debug << "failed to write to child: " << strerror(errno) << "\n"; + goto done; + } + + if (w > 0) { + input_offset += (off_t)w; + if ((size_t)input_offset == input.size()) { + // exhausted the input; send the child EOF + (void)close(in[WRITE_FD]); + in[WRITE_FD] = -1; + } + } + } + } + + // check if the child has exited + int status; + if (waitpid(pid, &status, WNOHANG) == pid) { + if (WIFEXITED(status)) + break; + if (WIFSIGNALED(status)) { + *debug << "child exited due to signal " << WTERMSIG(status) << "\n"; + break; + } + } + } + + // success if we've reached here + + output = buffered_output.str(); + + ret = 0; + +done: + + for (int &fd : out) { + if (fd != -1) { + (void)close(fd); + fd = -1; + } + } + + for (int &fd : in) { + if (fd != -1) { + (void)close(fd); + fd = -1; + } + } + + (void)posix_spawn_file_actions_destroy(&fa); + + return ret; +} + +static int __attribute__((unused)) test_process(int argc, char **argv) { + + if (argc < 2 || strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0 + || strcmp(argv[1], "-?") == 0) { + std::cerr + << "Rumur Process.cc tester\n" + << "\n" + << " usage: " << argv[0] << " cmd args...\n"; + return EXIT_SUCCESS; + } + + // construct argument list + std::vector args; + for (size_t i = 1; i < (size_t)argc; i++) { + assert(argv[i] != nullptr); + args.emplace_back(argv[i]); + } + + // read stdin until EOF + std::ostringstream input; + for (std::string line; std::getline(std::cin, line); ) { + input << line << "\n"; + } + + // run our designated process + std::string output; + int r = run(args, input.str(), output); + + if (r == 0) + std::cout << output; + + return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} + +#ifdef TEST_PROCESS +int main(int argc, char **argv) { + return test_process(argc, argv); +} +#endif diff --git a/rumur/src/process.h b/rumur/src/process.h new file mode 100644 index 00000000..d636dd72 --- /dev/null +++ b/rumur/src/process.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +/* Run an external process, pass it the given input on stdin and wait for it to + * finish. Its stdout data is reported via the output parameter. Note that there + * is currently no reporting of the exit status of the process. + */ +int run(const std::vector &args, const std::string &input, + std::string &output);