-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Our current API is a bit clunky and doesn't allow supporting all of the proposed features we wanted.
So, here's some newer endpoints I propose:
Executing a basic subprocess
/**
* Execute a subprocess and optionally call a function per line of stdout.
* @param commandPath - the path of the executable to execute, e.g. "/bin/cat"
* @param commandArgs - the extra arguments for an executable e.g. {"argument 1", "henlo"}
* @param stdinInput - a list of inputs that will be piped into the processes' stdin
* @param lambda - a function that is called with every line from the executed process (default NOP function)
* @param env - a list of environment variables that the process will execute with (default nothing)
*/
int execute(const std::string& commandPath, const std::vector<std::string>& commandArgs, const std::vector<std::string>& stdinInput, const std::function<void(std::string)>& lambda = [](std::string){}, const std::vector<std::string>& env = {});Also, another execute function will exist, except that instead of taking in a std::vector to supply its stdin, it takes two input iterators (the start and end of a range). It's signature would look like this:
template<class InputIt>
int execute(const std::string& commandPath, const std::vector<std::string>& commandArgs, InputIt stdioBegin, InputIt stdioEnd, const std::function<void(std::string)>& lambda = [](std::string){}, const std::vector<std::string>& env)Where the InputIt would require typical iterator methods implemented (similar to InputIterator defined here: https://en.cppreference.com/w/cpp/named_req/InputIterator).
Retrieving subprocess output
Similar to the execute function's parameters, the check_output function (name TBD, this name is just copied from the python3 subprocess library, we should check if they have better names (or there's internal conflict about how shit that name is)) simply returns a vector containing all lines of stdout.
std::vector<std::string> check_output(const std::string& commandPath, const std::vector<std::string>& commandArgs, const std::vector<std::string>& stdioInput, const std::vector<std::string>& env= {})
template<class InputIt>
std::vector<std::string> check_output(const std::string& commandPath, const std::vector<std::string>& commandArgs, InputIt stdioBegin, InputIt stdioEnd, const std::vector<std::string>& env = {});Input iterator is similar to the above restrictions in the execute function.
Interactive input/output
To enable dynamically setting what should be fed into the program based on the output, we introduce a newer ProcessStream class. This class has only a few methods available, but allows feeding a string into the process, and checking whether a line is available to read (there may not be one always available for each stdin, consider grep when the line doesn't match) and also extracting it.
I imagine it's use will be something along these lines:
// ctor
ProcessStream(const std::string& commandPath, const std::vector<std::string>& commandArgs, const std::vector<std::string>& env);
ProcessStream ps("/bin/cat", {});
ps << "henlo world\n";
if (ps.ready()) {
std::string output;
output << ps;
std::cout << output;
}There will naturally be non-operator overload equivalents to those endpoints (most likely something like .write(std::string) and std::string .read(timeout).
We require a timeout, because it's not really possible to check if there is a line available, without some kind of call to select (I think!).
Daemon spawning
People seem to like to spawn and forget processes (especially in some kind of management application), so providing a method to disown and mark them as daemons makes a whole lotta sense. I haven't written daemon code too often (it's been a couple years since I have I reckon), and that was in pure C. As this library has to use C to implement these features it's certainly doable, but would require different behaviour per OS, because this I imagine heavily differs between OSes. The problem is, I don't really know what a nice interface to doing so would be.
Any suggestions would be much appreciated.
Error Handling
As we're dealing with external applications, they may throw at any time and inconsistently (either due to the application, the OS/user killing them, or bugs), we need some kind of error handling.
Currently, the above APIs proposed return an error status, but I feel like this may not be very idiomatic, and that exceptions should be used instead. I suppose this requires a read up on C++ philosophy before being able to make a decision, but I'm thinking of mirroring how Python's subprocess library does it, which is pretty much to throw an exception whenever anything goes wrong, including non-zero return statuses.
Asynchronous Calls
Whether the user should be responsible for running this stuff in the background is again another philosophical question to ask, but as this is a library aimed at dumb-dumbs, its worth consideration if this is something we should hold their hand on. Currently, we provide a method to spawn a subprocess asynchronously, and return a std::future of the return code. Naturally, if we change the error handling to be a different strategy, this may not make sense, but I can't imagine asynchronous informing the main process about the status via exceptions can't be neat, so using futures on the return value seems like a good idea to cover this.
Final remarks
In my mind, these cover the current TODOs I propose in the current README, so it seems like a good direction to head. The only thing that I don't know and could cause problems in these interfaces is how Windows deals with processes. Some of these things may not be possible.
Any comments are appreciated (and likely only Cameron is gonna comment, but I'll try and hook some people to comment).
Cheers