-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add support for sub-process management (#11)
- Loading branch information
Showing
4 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
#include <string> | ||
#include <vector> | ||
|
||
#include <sys/wait.h> | ||
#include <unistd.h> | ||
|
||
#include <util/expected.h> | ||
#include <util/subprocess.h> | ||
|
||
#include <fmt/core.h> | ||
#include <fmt/ranges.h> | ||
|
||
namespace util { | ||
|
||
expected<pipe, int> make_pipe() { | ||
pipe p; | ||
if (auto rval = ::pipe(p.state); rval == -1) { | ||
return unexpected(rval); | ||
} | ||
return p; | ||
} | ||
|
||
expected<subprocess, std::string> run(const std::vector<std::string>& argv) { | ||
if (argv.empty()) { | ||
return unexpected("need at least one argument"); | ||
} | ||
|
||
// TODO: error handling | ||
pipe inpipe = *make_pipe(); | ||
pipe outpipe = *make_pipe(); | ||
pipe errpipe = *make_pipe(); | ||
|
||
auto pid = ::fork(); | ||
|
||
if (pid == 0) { | ||
// TODO: error handling | ||
::dup2(outpipe.write(), STDOUT_FILENO); | ||
::dup2(errpipe.write(), STDERR_FILENO); | ||
::dup2(inpipe.read(), STDIN_FILENO); | ||
|
||
outpipe.close(); | ||
errpipe.close(); | ||
inpipe.close(); | ||
|
||
// child(argv); | ||
std::vector<char*> args; | ||
args.reserve(argv.size() + 1); | ||
for (auto& arg : argv) { | ||
args.push_back(const_cast<char*>(arg.data())); | ||
} | ||
args.push_back(nullptr); | ||
|
||
execvp(args[0], &args[0]); | ||
|
||
// this code only executes if the attempt to launch the subprocess | ||
// fails to launch. | ||
std::perror( | ||
fmt::format("subprocess error running '{}'", fmt::join(argv, " ")) | ||
.c_str()); | ||
exit(1); | ||
} | ||
|
||
outpipe.close_write(); | ||
errpipe.close_write(); | ||
inpipe.close_read(); | ||
|
||
return subprocess{outpipe, errpipe, inpipe, pid}; | ||
} | ||
|
||
void subprocess::setrcode(int status) { | ||
if (WIFEXITED(status)) { | ||
rcode_ = WEXITSTATUS(status); | ||
} else if (WIFSIGNALED(status)) { | ||
rcode_ = WTERMSIG(status); | ||
} else { | ||
rcode_ = 255; | ||
} | ||
} | ||
|
||
int subprocess::wait() { | ||
if (!finished_) { | ||
int status = 0; | ||
waitpid(pid, &status, 0); | ||
finished_ = true; | ||
setrcode(status); | ||
} | ||
return *rcode_; | ||
} | ||
|
||
bool subprocess::finished() { | ||
if (finished_) { | ||
return true; | ||
} | ||
|
||
int status; | ||
auto rc = waitpid(pid, &status, WNOHANG); | ||
if (rc == 0) { | ||
return false; | ||
} | ||
|
||
wait(); | ||
|
||
return true; | ||
} | ||
|
||
void subprocess::kill(int signal) { | ||
if (!finished_) { | ||
::kill(pid, signal); | ||
wait(); | ||
} | ||
finished_ = true; | ||
} | ||
|
||
int subprocess::rvalue() { | ||
if (!finished_) { | ||
return wait(); | ||
} | ||
return *rcode_; | ||
} | ||
|
||
std::istream& buffered_istream::stream() { | ||
return *stream_; | ||
} | ||
|
||
std::optional<std::string> buffered_istream::getline() { | ||
if (std::string line; std::getline(stream(), line)) { | ||
return line; | ||
} | ||
return {}; | ||
} | ||
|
||
std::ostream& buffered_ostream::stream() { | ||
return *stream_; | ||
} | ||
|
||
void buffered_ostream::putline(std::string_view line) { | ||
stream() << line << std::endl; | ||
} | ||
|
||
} // namespace util |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
#pragma once | ||
|
||
#include <iostream> | ||
#include <memory> | ||
#include <string> | ||
#include <vector> | ||
|
||
#include <ext/stdio_filebuf.h> | ||
|
||
#include <util/expected.h> | ||
|
||
namespace util { | ||
|
||
struct pipe { | ||
int state[2]; | ||
int read() const { | ||
return state[0]; | ||
} | ||
int write() const { | ||
return state[1]; | ||
} | ||
void close() { | ||
::close(read()); | ||
::close(write()); | ||
} | ||
void close_read() { | ||
::close(read()); | ||
} | ||
void close_write() { | ||
::close(write()); | ||
} | ||
}; | ||
|
||
class buffered_istream { | ||
std::unique_ptr<__gnu_cxx::stdio_filebuf<char>> buffer_; | ||
std::unique_ptr<std::istream> stream_; | ||
|
||
public: | ||
buffered_istream() = delete; | ||
buffered_istream(const pipe& p) | ||
: buffer_(new __gnu_cxx::stdio_filebuf<char>(p.read(), | ||
std::ios_base::in, 1)), | ||
stream_(new std::istream(buffer_.get())) { | ||
} | ||
|
||
std::istream& stream(); | ||
|
||
std::optional<std::string> getline(); | ||
}; | ||
|
||
class buffered_ostream { | ||
std::unique_ptr<__gnu_cxx::stdio_filebuf<char>> buffer_; | ||
std::unique_ptr<std::ostream> stream_; | ||
|
||
public: | ||
buffered_ostream() = delete; | ||
buffered_ostream(const pipe& p) | ||
: buffer_(new __gnu_cxx::stdio_filebuf<char>(p.write(), | ||
std::ios_base::out, 1)), | ||
stream_(new std::ostream(buffer_.get())) { | ||
} | ||
|
||
std::ostream& stream(); | ||
void putline(std::string_view line); | ||
}; | ||
|
||
enum class proc_status { running, finished }; | ||
|
||
class subprocess { | ||
public: | ||
buffered_istream out; | ||
buffered_istream err; | ||
buffered_ostream in; | ||
pid_t pid; | ||
|
||
subprocess() = delete; | ||
|
||
subprocess(buffered_istream out, buffered_istream err, buffered_ostream in, | ||
pid_t pid) | ||
: out(std::move(out)), err(std::move(err)), in(std::move(in)), | ||
pid(pid) { | ||
} | ||
|
||
int wait(); | ||
bool finished(); | ||
int rvalue(); | ||
void kill(int signal = 9); | ||
|
||
private: | ||
bool finished_ = false; | ||
std::optional<int> rcode_; | ||
void setrcode(int); | ||
}; | ||
|
||
expected<subprocess, std::string> run(const std::vector<std::string>& argv); | ||
|
||
} // namespace util |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
#include <catch2/catch_all.hpp> | ||
#include <fmt/core.h> | ||
|
||
#include <util/subprocess.h> | ||
|
||
namespace matchers = Catch::Matchers; | ||
|
||
TEST_CASE("error", "[subprocess]") { | ||
// test that execve error is handled correctly by running an binary that | ||
// does not exist | ||
{ | ||
auto proc = util::run({"/wombat/soup", "--garbage"}); | ||
REQUIRE(proc->wait() == 1); | ||
auto line = proc->err.getline(); | ||
REQUIRE(line); | ||
REQUIRE_THAT(*line, | ||
matchers::ContainsSubstring("No such file or directory")); | ||
} | ||
// check that errors are handled correctly when an application launches, | ||
// then fails with an error | ||
{ | ||
auto proc = util::run({"ls", "--garbage"}); | ||
// ls returns 2 for "serious trouble, e.g. can't access comand line | ||
// argument" | ||
REQUIRE(proc->wait() == 2); | ||
auto line = proc->err.getline(); | ||
REQUIRE(line); | ||
REQUIRE_THAT(*line, | ||
matchers::Equals("ls: unrecognized option '--garbage'")); | ||
} | ||
} | ||
|
||
TEST_CASE("wait", "[subprocess]") { | ||
Catch::Timer t; | ||
// sleep for 100 ms | ||
t.start(); | ||
// wait 50 ms | ||
auto proc = util::run({"sleep", "0.1s"}); | ||
while (t.getElapsedMilliseconds() < 50) { | ||
} | ||
REQUIRE(!proc->finished()); | ||
REQUIRE(proc->wait() == 0); | ||
REQUIRE(proc->finished()); | ||
REQUIRE(t.getElapsedMicroseconds() > 100'000); | ||
} | ||
|
||
TEST_CASE("kill", "[subprocess]") { | ||
// sleep 100 ms | ||
auto proc = util::run({"sleep", "0.1s"}); | ||
// the following will be called while the process is running | ||
proc->kill(); | ||
REQUIRE(proc->finished()); | ||
// se set return value to -1 when the process is killed | ||
REQUIRE(proc->rvalue() == 9); | ||
} | ||
|
||
TEST_CASE("stdout", "[subprocess]") { | ||
{ | ||
auto proc = util::run({"echo", "hello world"}); | ||
REQUIRE(proc); | ||
REQUIRE(proc->wait() == 0); | ||
auto line = proc->out.getline(); | ||
REQUIRE(line); | ||
REQUIRE_THAT(*line, matchers::Equals("hello world")); | ||
// only one line of ourput is expected | ||
REQUIRE(!proc->out.getline()); | ||
REQUIRE(!proc->err.getline()); | ||
} | ||
{ | ||
auto proc = util::run({"echo", "-e", "hello\nworld"}); | ||
REQUIRE(proc); | ||
REQUIRE(proc->wait() == 0); | ||
auto line = proc->out.getline(); | ||
REQUIRE(line); | ||
REQUIRE_THAT(*line, matchers::Equals("hello")); | ||
line = proc->out.getline(); | ||
REQUIRE(line); | ||
REQUIRE_THAT(*line, matchers::Equals("world")); | ||
// only one line of ourput is expected | ||
REQUIRE(!proc->out.getline()); | ||
REQUIRE(!proc->err.getline()); | ||
} | ||
} |