Skip to content

Commit c5cdce4

Browse files
committed
initial commit? i wrote a lot for an initial commit lmao
0 parents  commit c5cdce4

File tree

4 files changed

+232
-0
lines changed

4 files changed

+232
-0
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
demo: demo.cpp subprocess.hpp
3+
g++ -g -std=c++14 demo.cpp -o demo -lpthread
4+

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# SubprocessCPP
2+
3+
A neat header-only library that allows you to execute processes either synchronously or asynchronously, whilst providing input and handling the generated input. No more calling `exec` in a C++ program!
4+
5+
This library uses some C++11 features, and tries to be as idiomatic to modern C++.
6+
7+
# Usage
8+
Simply include the `subprocess.hpp` file.
9+
10+
An example program is provided in `demo.cpp`, but here's some snippets:
11+
12+
```C++
13+
// execute bc and pass it some equations
14+
std::list<std::string> inputs = {"1+1\n", "2^333\n", "32-32\n"};
15+
subprocess::execute("/usr/bin/bc",
16+
{},
17+
inputs,
18+
echoString);
19+
20+
// grep over some inputs
21+
inputs = {"12232\n", "hello, world\n", "Hello, world\n\n", "line: Hello, world!\n"};
22+
subprocess::execute("/bin/grep", {"-i", "Hello, world"}, inputs, echoString);
23+
24+
// execute a process and extract all lines outputted
25+
inputs.clear(); // provide no stdin
26+
int status;
27+
std::vector<std::string> vec = subprocess::checkOutput("/usr/bin/time", {"sleep", "1"}, inputs, status);
28+
for (const std::string& s : vec) {
29+
std::cout << "output: " << s << '\t';
30+
std::cout << "line length:" << s.length() << std::endl;
31+
}
32+
std::cout << "process finished with an exit code of: " << status << std::endl;
33+
34+
// execute sleep asynchronously, and block when needing the output
35+
std::future<int> futureStatus = subprocess::async("/bin/sleep", {"3"}, inputs, [](std::string) {});
36+
// if this wasn't async, this wouldn't print until after the process finished!
37+
std::cout << "executing sleep..." << std::endl;
38+
std::cout << "sleep executed with exit status: " << futureStatus.get() << std::endl;
39+
```
40+
41+
# Requirements
42+
Not much:
43+
- A POSIX environment (Linux, Mac, \*BSD)
44+
- A modern >=C++11 compiler
45+
46+
Unfortunately, I am not as familiar with Windows to write code for it, if you want to provide some code that works for this platform, be my guest! But, don't forget to include appropriate header guards so that it works cross-platform.
47+
48+
# API
49+
### TODO: ....detail what each of the functions _should_ be used for.
50+
51+
# License
52+
This is dual-licensed under a MIT and GPLv3 license - so FOSS lovers can use it, whilst people restricted in companies to not open-source their program is also able to use this library :)
53+
54+
I don't know too much about licenses, so if I missed anything, please let me know.
55+
56+
# Future Features
57+
Some stuff that I haven't written yet, but I wanna:
58+
- [X] Output streaming. Provide an iterator to allow iteration over the output lines, such that we don't have to load all in memory at once.
59+
- [ ] Thread-safe async lambda interactions. Provide a method to launch a process in async, but still allow writing to the list of stdin without a race condition.
60+
- [ ] A ping-ponging interface. This should allow incrementally providing stdin, then invoking the functor if output is emitted. Note that will likely not be possible if there's not performed asynchronously, or without using select. Using select is a bit annoying, because how do we differentiate between a command taking a while and it providing no input?

demo.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#include <list>
2+
#include <string>
3+
#include <iostream>
4+
5+
#include "subprocess.hpp"
6+
7+
void echoString(std::string in) {
8+
std::cout << "output: " << in;
9+
}
10+
11+
int main() {
12+
// execute bc and pass it some equations
13+
std::list<std::string> inputs = {"1+1\n", "2^333\n", "32-32\n"};
14+
subprocess::execute("/usr/bin/bc",
15+
{},
16+
inputs,
17+
echoString);
18+
19+
// grep over some inputs
20+
inputs = {"12232\n", "hello, world\n", "Hello, world\n\n", "line: Hello, world!\n"};
21+
subprocess::execute("/bin/grep", {"-i", "Hello, world"}, inputs, echoString);
22+
23+
// execute a process and extract all lines outputted
24+
inputs.clear(); // provide no stdin
25+
int status;
26+
std::vector<std::string> vec = subprocess::checkOutput("/usr/bin/time", {"sleep", "1"}, inputs, status);
27+
for (const std::string& s : vec) {
28+
std::cout << "output: " << s << '\t';
29+
std::cout << "line length:" << s.length() << std::endl;
30+
}
31+
std::cout << "process finished with an exit code of: " << status << std::endl;
32+
33+
// execute sleep asynchronously, and block when needing the output
34+
// you will not be able to modify inputs dynamically from the functor, due to the possibility of concurrent modification of the list & line feeder
35+
std::future<int> futureStatus = subprocess::async("/bin/sleep", {"3"}, inputs, [](std::string) {});
36+
// if this wasn't async, this wouldn't print until after the process finished!
37+
std::cout << "executing sleep..." << std::endl;
38+
std::cout << "sleep executed with exit status: " << futureStatus.get() << std::endl;
39+
40+
41+
// simulate pipes between programs: lets launch echo to provide input into a grep process!
42+
}

subprocess.hpp

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#pragma once
2+
3+
#include <string>
4+
#include <stdexcept>
5+
#include <iostream>
6+
#include <algorithm>
7+
#include <functional>
8+
#include <tuple>
9+
#include <vector>
10+
#include <list>
11+
#include <future>
12+
13+
// unix process stuff
14+
#include <signal.h>
15+
#include <unistd.h>
16+
#include <sys/wait.h>
17+
#include <sys/prctl.h>
18+
#include <cstring>
19+
20+
21+
22+
namespace subprocess {
23+
24+
/* execute a process, inputting stdin and calling the functor with the stdout lines.
25+
* returns the status code of the executed program
26+
* */
27+
28+
int execute(const std::string& commandPath,
29+
const std::vector<std::string>& commandArgs,
30+
std::list<std::string>& stringInput /* what pumps into stdin */,
31+
std::function<void(std::string)> lambda) {
32+
// based off https://stackoverflow.com/a/6172578
33+
pid_t pid = 0;
34+
int inpipefd[2];
35+
int outpipefd[2];
36+
37+
38+
// construct the argument list (unfortunately, the C api wasn't defined with C++ in mind, so we have to abuse const_cast)
39+
// see: https://stackoverflow.com/a/190208
40+
std::vector<char*> cargs;
41+
// the process name must be first for execv
42+
cargs.push_back(const_cast<char*>(commandPath.c_str()));
43+
for (const std::string& arg : commandArgs) {
44+
cargs.push_back(const_cast<char*>(arg.c_str()));
45+
}
46+
// must be terminated with a nullptr for execv
47+
cargs.push_back(nullptr);
48+
49+
pipe(inpipefd);
50+
pipe(outpipefd);
51+
pid = fork();
52+
// child
53+
if (pid == 0) {
54+
dup2(outpipefd[0], STDIN_FILENO);
55+
dup2(inpipefd[1], STDOUT_FILENO);
56+
dup2(inpipefd[1], STDERR_FILENO);
57+
58+
// XXX: test (close the stdin..?)
59+
close(outpipefd[1]);
60+
61+
//ask kernel to deliver SIGTERM in case the parent dies
62+
prctl(PR_SET_PDEATHSIG, SIGTERM);
63+
64+
execv(commandPath.c_str(), cargs.data());
65+
// Nothing below this line should be executed by child process. If so,
66+
// it means that the execl function wasn't successfull, so lets exit:
67+
exit(1);
68+
}
69+
70+
close(outpipefd[0]);
71+
close(inpipefd[1]);
72+
73+
// while our string queue is working,
74+
while (!stringInput.empty()) {
75+
// write our input to the process's stdin pipe
76+
std::string newInput = stringInput.front();
77+
stringInput.pop_front();
78+
write(outpipefd[1], newInput.c_str(), newInput.size());
79+
}
80+
// now we finished chucking in the string, send an EOF
81+
close(outpipefd[1]);
82+
83+
// iterate over each line output by the child's stdout, and call the functor
84+
FILE* childStdout = fdopen(inpipefd[0], "r");
85+
char* line = nullptr;
86+
ssize_t nread;
87+
size_t len;
88+
while ((nread = getline(&line, &len, childStdout)) != -1) {
89+
lambda(std::string(line));
90+
91+
// free up the memory allocated by getline
92+
free(line);
93+
line = nullptr;
94+
}
95+
if (line != nullptr) free(line);
96+
97+
fclose(childStdout);
98+
int status;
99+
waitpid(pid, &status, 0);
100+
101+
return status;
102+
}
103+
104+
/* convenience fn to return a list of outputted strings */
105+
std::vector<std::string> checkOutput(const std::string& commandPath,
106+
const std::vector<std::string>& commandArgs,
107+
std::list<std::string>& stringInput /* what pumps into stdin */,
108+
int& status) {
109+
std::vector<std::string> retVec;
110+
status = execute(commandPath, commandArgs, stringInput, [&](std::string s) { retVec.push_back(std::move(s)); });
111+
return retVec;
112+
}
113+
114+
/* spawn the process in the background asynchronously, and return a future of the status code */
115+
std::future<int> async(const std::string commandPath, const std::vector<std::string> commandArgs, std::list<std::string> stringInput, std::function<void(std::string)> lambda) {
116+
// spawn the function async - we must pass the args by value into the async lambda
117+
// otherwise they may destruct before the execute fn executes!
118+
// whew, that was an annoying bug to find...
119+
return std::async(std::launch::async,
120+
[&](const std::string cp,
121+
const std::vector<std::string> ca,
122+
std::list<std::string> si,
123+
std::function<void(std::string)> l) { return execute(cp, ca, si, l); }, commandPath, commandArgs, stringInput, lambda);
124+
}
125+
126+
} // end namespace subprocess

0 commit comments

Comments
 (0)