Skip to content

Commit

Permalink
add support for sub-process management (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcumming authored Oct 3, 2024
1 parent 434ab9c commit 175ed37
Show file tree
Hide file tree
Showing 4 changed files with 322 additions and 0 deletions.
2 changes: 2 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ lib_src = [
'src/uenv/repository.cpp',
'src/util/shell.cpp',
'src/util/strings.cpp',
'src/util/subprocess.cpp',
'src/uenv/uenv.cpp',
]
lib_inc = include_directories('src')
Expand Down Expand Up @@ -79,6 +80,7 @@ unit_src = [
'test/unit/main.cpp',
'test/unit/parse.cpp',
'test/unit/repository.cpp',
'test/unit/subprocess.cpp',
]

unit = executable('unit',
Expand Down
140 changes: 140 additions & 0 deletions src/util/subprocess.cpp
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
97 changes: 97 additions & 0 deletions src/util/subprocess.h
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
83 changes: 83 additions & 0 deletions test/unit/subprocess.cpp
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());
}
}

0 comments on commit 175ed37

Please sign in to comment.