stc
Loading...
Searching...
No Matches
Process.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <filesystem>
4#ifdef _WIN32
5#error "Process.hpp is currently UNIX only, and does not support Windows. Feel free to open a PR to change this"
6#endif
7
18#include <array>
19#include <atomic>
20#include <cstdlib>
21#include <cstring>
22#include <fcntl.h>
23#include <format>
24#include <functional>
25#include <iostream>
26#include <map>
27#include <memory>
28#include <mutex>
29#include <optional>
30#include <pty.h>
31#include <sstream>
32#include <string>
33#include <string_view>
34#include <sys/poll.h>
35#include <sys/wait.h>
36#include <thread>
37#include <unistd.h>
38#include <variant>
39#include <vector>
40
41// TODO: this cannot be "unix", or the build inexplicably dies ("unexpected { before numeric constant")
42// It's probably a macro
43namespace stc::Unix {
44
46 ssize_t writeToFd(const std::string& data, int fd) {
47 ssize_t bytes = write(fd, data.data(), data.size());
48 if (bytes > 0) {
49 return bytes;
50 }
51 return 0;
52 }
53
54 ssize_t readFromFd(std::stringstream& out, int fd) {
55 std::array<char, 4096> buff;
56 ssize_t sum = 0;
57
58 nfds_t nfds = 1;
59 pollfd pdfs = {
60 .fd = fd,
61 .events = POLLIN,
62 .revents = 0
63 };
64
65 // we need a small timeout here to prevent race conditions
66 while (poll(&pdfs, nfds, 10)) {
67 ssize_t bytes = read(
68 fd,
69 buff.data(),
70 buff.size()
71 );
72 if (bytes > 0) {
73 out << std::string_view {
74 buff.begin(), buff.begin() + bytes
75 };
76 sum += bytes;
77 }
78 }
79 return sum;
80 }
81};
82
83struct Pipe : public LowLevelWrapper {
84 std::array<int, 2> fds;
85 Pipe() {
86 if (pipe(fds.data()) != 0) {
87 throw std::runtime_error("Failed to open pipe");
88 }
89 }
90
92 die();
93 }
94
95 void die() {
96 closeRead();
97 closeWrite();
98 }
99 void closeRead() {
100 if (fds[0] != -1) {
101 close(fds[0]);
102 }
103 fds[0] = -1;
104 }
105 void closeWrite() {
106 if (fds[1] != -1) {
107 close(fds[1]);
108 }
109
110 fds[1] = -1;
111 }
112
113 int readFd() {
114 return fds[0];
115 }
116 int writeFd() {
117 return fds[1];
118 }
119
120 ssize_t readData(std::stringstream& out) {
121 return readFromFd(out, readFd());
122 }
123};
124
125struct PTY : public LowLevelWrapper {
127
128 PTY() {
129 // TODO: figure out if it makes sense to:
130 // 1. store the name
131 // 2. Allow customising whatever the last two parameters are
132 if (openpty(&master, &slave, nullptr, nullptr, nullptr) == -1) {
133 throw std::runtime_error("Failed to open PTY");
134 }
135 }
137 die();
138 }
139
147 void die() {
150 }
151
153 if (master >= 0) {
154 close(master);
155 }
156 master = -1;
157 }
159 if (slave >= 0) {
160 close(slave);
161 }
162 slave = -1;
163 }
169 ssize_t writeToStdin(const std::string& data) {
170 return writeToFd(data, master);
171 }
172
173 ssize_t readData(std::stringstream& out) {
174 return readFromFd(out, master);
175 }
176};
177
181inline std::shared_ptr<Pipe> createPipe() {
182 return std::make_shared<Pipe>();
183}
184
185struct Pipes {
186 std::shared_ptr<Pipe> stdoutPipe = nullptr;
187 std::shared_ptr<Pipe> stderrPipe = nullptr;
188 std::shared_ptr<Pipe> stdinPipe = nullptr;
189
190 void die() {
191 if (stdoutPipe) stdoutPipe->die();
192 if (stderrPipe) stderrPipe->die();
193 if (stdinPipe) stdinPipe->die();
194 }
195
196 ssize_t writeToStdin(const std::string& data) {
197 if (stdinPipe == nullptr) {
198 throw std::runtime_error("Must open stdin to write to stdin");
199 }
200 return stdinPipe->writeToFd(data, stdinPipe->writeFd());
201 }
202
207 static Pipes separate(bool withStdin = true) {
208 return Pipes {
209 createPipe(),
210 createPipe(),
211 withStdin ? createPipe() : nullptr
212 };
213 }
218 static Pipes shared(bool withStdin = true) {
219 auto outPipe = createPipe();
220 return Pipes {
221 outPipe,
222 outPipe,
223 withStdin ? createPipe() : nullptr
224 };
225
226 }
227};
228
229
233inline std::shared_ptr<PTY> createPTY() {
234 return std::make_shared<PTY>();
235}
236
238 std::map<std::string, std::string> env = {};
242 bool extendEnviron = true;
243
248 std::optional<std::string> workingDirectory = std::nullopt;
249
250 void validate() const {
251 for (auto& [k, v] : env) {
252 if (k.find('=') != std::string::npos) {
253 throw std::runtime_error("Illegal key: " + k);
254 }
255 }
256
257 if (workingDirectory.has_value() && !std::filesystem::is_directory(*workingDirectory)) {
258 throw std::runtime_error(
259 std::format(
260 "Working directory set to {}, which does not exist or isn't a directory",
262 )
263 );
264 }
265 }
266};
267
268struct Config {
269 bool verboseUserOutput = false;
270};
271
272class Process {
273protected:
274 std::optional<decltype(fork())> pid = std::nullopt;
275
276 std::optional<
277 std::variant<Pipes, std::shared_ptr<PTY>>
279 std::stringstream stdoutBuff, stderrBuff;
280 std::mutex lock;
281
282 std::thread inputCollector;
283 std::atomic<int> statusCode = -1;
284 std::atomic<std::optional<bool>> exitedNormally;
285 bool running = true;
286
288
289 bool waitPid(int opts = 0) {
290 int wstatus;
291 if (!pid.has_value()) {
292 std::cerr << "waitPid called, but pid has no value. Something has gone very wrong" << std::endl;
293 exit(70);
294 }
295 if (waitpid(*pid, &wstatus, opts) > 0) {
296 if (WIFEXITED(wstatus)) {
297 statusCode = WEXITSTATUS(wstatus);
298 exitedNormally = true;
299 } else if (WIFSIGNALED(wstatus)) {
300 statusCode = WTERMSIG(wstatus);
301 exitedNormally = false;
302 } else if (WIFSTOPPED(wstatus)) {
303 statusCode = WSTOPSIG(wstatus);
304 exitedNormally = false;
305 } else {
306 std::cerr
307 << "WARNING: stc::Unix::Process got an unknown status: " << wstatus
308 << std::endl;
309 }
310 return true;
311 }
312 return false;
313 }
314
323 char* const* createEnviron(
324 const std::optional<Environment>& env
325 ) {
326 if (env == std::nullopt) {
327 return environ;
328 }
329
330 size_t size = 0;
331 for (char **env = environ; *env != nullptr; env++) {
332 ++size;
333 }
334
335 // This is disgusting, but it beats fucking around with the real raw types. This will technically leak a vector,
336 // but it does not matter because it's disappeared once exec is called:
337 // https://stackoverflow.com/a/3617385
338 // Would prefer to do this better, but I just don't want to
339 std::vector<char*>* data = new std::vector<char*>;
340 if (env->extendEnviron && size > 0) {
341 data->assign(
342 environ, environ + size
343 );
344 }
345
346 data->reserve(
347 // Existing data or 0
348 data->size()
349 // nullptr
350 + 1
351 // Extra envs
352 + env->env.size()
353 );
354 for (const auto& [k, v] : env->env) {
355 if (env->extendEnviron) {
356 // If we're extending environ, keys here can conflict with environ. They won't conflict internally,
357 // because env is a non-multimap, and we enforce keys not containing '=', so no weird injection shit
358 // resulting in identical strings.
359 data->erase(
360 std::remove_if(
361 data->begin(),
362 data->end(),
363 [&](const auto& v) -> bool {
364 return strncmp(v, k.data(), k.size()) == 0;
365 }
366 ), data->end()
367 );
368 }
369
370 std::string combined = std::format(
371 "{}={}", k, v
372 );
373 auto* newStr = strdup(combined.c_str());
374 if (newStr == nullptr) {
375 std::cerr << "Failed to copy string to env" << std::endl;
376 exit(69);
377 }
378 data->push_back(newStr);
379 }
380 data->push_back(nullptr);
381
382 return data->data();
383
384 }
385
387 const std::vector<std::string>& command,
388 const std::function<void()>& readImpl,
389 const std::function<void()>& prepDuping,
390 const std::optional<Environment>& env
391 ) {
392 if (command.size() == 0) {
393 throw std::runtime_error("Cannot run null command");
394 }
395 std::vector<const char*> convertedCommand;
396
397 if (env) {
398 env->validate();
399 }
400
402 std::cout << "Exec: ";
403 }
404 convertedCommand.reserve(command.size() + 1);
405 for (auto& str : command) {
407 std::cout << std::quoted(str);
408 // This isn't strictly speaking necessary, but avoids a situation where the tests need a .starts_with(),
409 // or it'll require the full string to contain a load-bearing linebreak AND a load-bearing trailing
410 // space.
411 if (convertedCommand.size() != command.size() - 1) {
412 std::cout << " ";
413 }
414 }
415 convertedCommand.push_back(str.c_str());
416 }
418 std::cout << "\n";
419 }
420 convertedCommand.push_back(nullptr);
421
422 pid = fork();
423 if (pid < 0) {
424 throw std::runtime_error("Failed to fork");
425 } else if (pid == 0) {
426 // Child process
427 if (prepDuping != nullptr) {
428 prepDuping();
429 }
430
431 // Close handles if they're opened
432 if (interface) {
433 std::visit([](auto& resolved) {
434 using T = std::decay_t<decltype(resolved)>;
435 if constexpr (std::is_same_v<T, std::shared_ptr<PTY>>) {
436 resolved->die();
437 } else {
438 resolved.die();
439 }
440 }, interface.value());
441 }
442
443 if (env.has_value()) {
444 if (env->workingDirectory.has_value()) {
445 // C++17 <3
446 // Avoids chdir() from <unistd.h> so there's one less thing to do if this file can be made portable.
447 std::filesystem::current_path(
448 env->workingDirectory.value()
449 );
450 }
451 }
452
453 execve(
454 convertedCommand.at(0),
455 (char**) convertedCommand.data(),
456 createEnviron(env)
457 );
458 } else {
459 // Parent process
460 if (readImpl != nullptr) {
461 this->inputCollector = std::thread(
462 std::bind(&Process::run, this, readImpl)
463 );
464 }
465 }
466 }
467
468 void run(const std::function<void()>& readImpl) {
469 // TODO: readImpl should bake in some timeout here, but is that enough? Is this thread going to be too busy?
470 do {
471 readImpl();
472 } while (!waitPid(WNOHANG));
473 }
474public:
475 [[nodiscard("Discarding immediately terminates the process. You probably don't want this")]]
477 const std::vector<std::string>& command,
478 const std::optional<Environment>& env = std::nullopt,
479 const Config& config = {}
480 ): config(config) {
481 doSpawnCommand(command, nullptr, nullptr, env);
482 }
483
484 [[nodiscard("Discarding immediately terminates the process. You probably don't want this")]]
486 const std::vector<std::string>& command,
487 const Pipes& pipes,
488 const std::optional<Environment>& env = std::nullopt,
489 const Config& config = {}
490 ): config(config) {
491 interface = pipes;
492
493 doSpawnCommand(command, [this]() {
494 auto& pipes = std::get<Pipes>(this->interface.value());
495 if (pipes.stdoutPipe != nullptr) {
496 std::lock_guard l(lock);
497 pipes.stdoutPipe->readData(
499 );
500 }
501 if (pipes.stderrPipe != nullptr) {
502 std::lock_guard l(lock);
503 pipes.stderrPipe->readData(
505 );
506 }
507 }, [&]() {
508 if (pipes.stdinPipe != nullptr) {
509 dup2(pipes.stdinPipe->readFd(), STDIN_FILENO);
510 }
511 if (pipes.stdoutPipe != nullptr) {
512 dup2(pipes.stdoutPipe->writeFd(), STDOUT_FILENO);
513 }
514 if (pipes.stderrPipe != nullptr) {
515 dup2(pipes.stderrPipe->writeFd(), STDERR_FILENO);
516 }
517 std::get<Pipes>(*interface).die();
518 }, env);
519 }
520
521 [[nodiscard("Discarding immediately terminates the process. You probably don't want this")]]
523 const std::vector<std::string>& command,
524 const std::shared_ptr<PTY>& pty,
525 const std::optional<Environment>& env = std::nullopt,
526 const Config& config = {}
527 ): config(config) {
528 if (pty == nullptr) {
529 throw std::runtime_error(
530 "pty cannot be null. If you don't want to attach anything, use the non-pipe/non-PTY constructor instead"
531 );
532 }
533 interface = pty;
534 doSpawnCommand(command, [this]() {
535 auto pty = std::get<std::shared_ptr<PTY>>(this->interface.value());
536
537 {
538 std::lock_guard l(lock);
539 pty->readData(
541 );
542 }
543
544 }, [&]() {
545 dup2(pty->slave, STDIN_FILENO);
546 dup2(pty->slave, STDOUT_FILENO);
547 dup2(pty->slave, STDERR_FILENO);
548 std::get<std::shared_ptr<PTY>>(*interface)->die();
549 }, env);
550
551
552 }
553
554 virtual ~Process() {
555 this->sigkill();
556 this->block();
557 }
558
570 std::string getStdoutBuffer(bool reset = false) {
571 std::lock_guard g(lock);
572 auto str = stdoutBuff.str();
573 if (reset) {
574 stdoutBuff = {};
575 }
576 return str;
577 }
578
589 std::string getStderrBuffer(bool reset = false) {
590 std::lock_guard g(lock);
591 auto str = stderrBuff.str();
592 if (reset) {
593 stderrBuff = {};
594 }
595 return str;
596 }
597
603 std::lock_guard g(lock);
604 stderrBuff = {};
605 stdoutBuff = {};
606 }
607
615 ssize_t writeToStdin(const std::string& data) {
616 if (interface) {
617 return std::visit([&](auto& resolved) -> ssize_t {
618 using T = std::decay_t<decltype(resolved)>;
619 if constexpr (std::is_same_v<T, std::shared_ptr<PTY>>) {
620 return resolved->writeToStdin(data);
621 } else {
622 return resolved.writeToStdin(data);
623 }
624 }, interface.value());
625 } else {
626 throw std::runtime_error("Must use pty or pipe mode to write to stdin");
627 }
628 }
629
635 int block() {
636 if (this->interface) {
637 if (inputCollector.joinable()) {
638 inputCollector.join();
639 }
640 return statusCode;
641 } else {
642 waitPid();
643 return statusCode;
644 }
645 }
646
647 void signal(int sig) {
648 if (statusCode == -1) {
649 if (pid.has_value() && *pid > 0) {
650 kill(*pid, sig);
651 }
652 }
653 }
654
658 void stop() {
659 signal(SIGTERM);
660 }
661
666 void sigkill() {
667 signal(SIGKILL);
668 }
669
670 void closeStdin() {
671 if (!this->interface.has_value()) {
672 throw std::runtime_error("Must use pipe or pty mode to use this function");
673 }
674 }
675
681 std::optional<bool> hasExitedNormally() {
682 return exitedNormally;
683 }
684
685 std::optional<int> getExitCode() {
686 if (statusCode != -1) {
687 return statusCode;
688 }
689 return std::nullopt;
690 }
691
692};
693
694}
Definition Process.hpp:272
char *const * createEnviron(const std::optional< Environment > &env)
Definition Process.hpp:323
void sigkill()
Definition Process.hpp:666
std::string getStderrBuffer(bool reset=false)
Definition Process.hpp:589
ssize_t writeToStdin(const std::string &data)
Definition Process.hpp:615
void run(const std::function< void()> &readImpl)
Definition Process.hpp:468
std::mutex lock
Definition Process.hpp:280
std::optional< decltype(fork())> pid
Definition Process.hpp:274
std::stringstream stderrBuff
Definition Process.hpp:279
Config config
Definition Process.hpp:287
void closeStdin()
Definition Process.hpp:670
std::optional< int > getExitCode()
Definition Process.hpp:685
std::thread inputCollector
Definition Process.hpp:282
std::stringstream stdoutBuff
Definition Process.hpp:279
void stop()
Definition Process.hpp:658
int block()
Definition Process.hpp:635
virtual ~Process()
Definition Process.hpp:554
bool running
Definition Process.hpp:285
bool waitPid(int opts=0)
Definition Process.hpp:289
void resetBuffers()
Definition Process.hpp:602
std::optional< std::variant< Pipes, std::shared_ptr< PTY > > > interface
Definition Process.hpp:278
void signal(int sig)
Definition Process.hpp:647
void doSpawnCommand(const std::vector< std::string > &command, const std::function< void()> &readImpl, const std::function< void()> &prepDuping, const std::optional< Environment > &env)
Definition Process.hpp:386
Process(const std::vector< std::string > &command, const std::shared_ptr< PTY > &pty, const std::optional< Environment > &env=std::nullopt, const Config &config={})
Definition Process.hpp:522
Process(const std::vector< std::string > &command, const std::optional< Environment > &env=std::nullopt, const Config &config={})
Definition Process.hpp:476
std::optional< bool > hasExitedNormally()
Definition Process.hpp:681
std::string getStdoutBuffer(bool reset=false)
Definition Process.hpp:570
Process(const std::vector< std::string > &command, const Pipes &pipes, const std::optional< Environment > &env=std::nullopt, const Config &config={})
Definition Process.hpp:485
std::atomic< std::optional< bool > > exitedNormally
Definition Process.hpp:284
std::atomic< int > statusCode
Definition Process.hpp:283
Definition Process.hpp:43
std::shared_ptr< PTY > createPTY()
Definition Process.hpp:233
std::shared_ptr< Pipe > createPipe()
Definition Process.hpp:181
Definition Process.hpp:268
bool verboseUserOutput
Definition Process.hpp:269
Definition Process.hpp:237
std::map< std::string, std::string > env
Definition Process.hpp:238
bool extendEnviron
Definition Process.hpp:242
void validate() const
Definition Process.hpp:250
std::optional< std::string > workingDirectory
Definition Process.hpp:248
Definition Process.hpp:45
ssize_t writeToFd(const std::string &data, int fd)
Definition Process.hpp:46
ssize_t readFromFd(std::stringstream &out, int fd)
Definition Process.hpp:54
Definition Process.hpp:125
~PTY()
Definition Process.hpp:136
int master
Definition Process.hpp:126
ssize_t writeToStdin(const std::string &data)
Definition Process.hpp:169
PTY()
Definition Process.hpp:128
void closeSlaveChannel()
Definition Process.hpp:158
ssize_t readData(std::stringstream &out)
Definition Process.hpp:173
void closeMasterChannel()
Definition Process.hpp:152
int slave
Definition Process.hpp:126
void die()
Definition Process.hpp:147
Definition Process.hpp:83
int readFd()
Definition Process.hpp:113
~Pipe()
Definition Process.hpp:91
Pipe()
Definition Process.hpp:85
void closeRead()
Definition Process.hpp:99
ssize_t readData(std::stringstream &out)
Definition Process.hpp:120
std::array< int, 2 > fds
Definition Process.hpp:84
void closeWrite()
Definition Process.hpp:105
void die()
Definition Process.hpp:95
int writeFd()
Definition Process.hpp:116
Definition Process.hpp:185
std::shared_ptr< Pipe > stderrPipe
Definition Process.hpp:187
ssize_t writeToStdin(const std::string &data)
Definition Process.hpp:196
std::shared_ptr< Pipe > stdoutPipe
Definition Process.hpp:186
std::shared_ptr< Pipe > stdinPipe
Definition Process.hpp:188
static Pipes separate(bool withStdin=true)
Definition Process.hpp:207
static Pipes shared(bool withStdin=true)
Definition Process.hpp:218
void die()
Definition Process.hpp:190