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 if (fd < 0) {
48 throw std::runtime_error("Illegal write on closed or invalid fd");
49 }
50 ssize_t bytes = write(fd, data.data(), data.size());
51 if (bytes > 0) {
52 return bytes;
53 }
54 return 0;
55 }
56
57 ssize_t readFromFd(std::stringstream& out, int fd) {
58 std::array<char, 4096> buff;
59 ssize_t sum = 0;
60
61 nfds_t nfds = 1;
62 pollfd pdfs = {
63 .fd = fd,
64 .events = POLLIN,
65 .revents = 0
66 };
67
68 // we need a small timeout here to prevent race conditions
69 while (poll(&pdfs, nfds, 10)) {
70 ssize_t bytes = read(
71 fd,
72 buff.data(),
73 buff.size()
74 );
75 if (bytes > 0) {
76 out << std::string_view {
77 buff.begin(), buff.begin() + bytes
78 };
79 sum += bytes;
80 }
81 }
82 return sum;
83 }
84};
85
86struct Pipe : public LowLevelWrapper {
87 std::array<int, 2> fds;
88 Pipe() {
89 if (pipe(fds.data()) != 0) {
90 throw std::runtime_error("Failed to open pipe");
91 }
92 }
93
95 die();
96 }
97
98 void die() {
99 closeRead();
100 closeWrite();
101 }
102 void closeRead() {
103 if (fds[0] != -1) {
104 close(fds[0]);
105 }
106 fds[0] = -1;
107 }
108 void closeWrite() {
109 if (fds[1] != -1) {
110 close(fds[1]);
111 }
112
113 fds[1] = -1;
114 }
115
116 int readFd() {
117 return fds[0];
118 }
119 int writeFd() {
120 return fds[1];
121 }
122
123 ssize_t readData(std::stringstream& out) {
124 return readFromFd(out, readFd());
125 }
126};
127
128struct PTY : public LowLevelWrapper {
130
131 PTY() {
132 // TODO: figure out if it makes sense to:
133 // 1. store the name
134 // 2. Allow customising whatever the last two parameters are
135 if (openpty(&master, &slave, nullptr, nullptr, nullptr) == -1) {
136 throw std::runtime_error("Failed to open PTY");
137 }
138 }
140 die();
141 }
142
150 void die() {
153 }
154
156 if (master >= 0) {
157 close(master);
158 }
159 master = -1;
160 }
162 if (slave >= 0) {
163 close(slave);
164 }
165 slave = -1;
166 }
172 ssize_t writeToStdin(const std::string& data) {
173 return writeToFd(data, master);
174 }
175
176 ssize_t readData(std::stringstream& out) {
177 return readFromFd(out, master);
178 }
179};
180
184inline std::shared_ptr<Pipe> createPipe() {
185 return std::make_shared<Pipe>();
186}
187
188struct Pipes {
189 std::shared_ptr<Pipe> stdoutPipe = nullptr;
190 std::shared_ptr<Pipe> stderrPipe = nullptr;
191 std::shared_ptr<Pipe> stdinPipe = nullptr;
192
193 void die() {
194 if (stdoutPipe) stdoutPipe->die();
195 if (stderrPipe) stderrPipe->die();
196 if (stdinPipe) stdinPipe->die();
197 }
198
199 ssize_t writeToStdin(const std::string& data) {
200 if (stdinPipe == nullptr) {
201 throw std::runtime_error("Must open stdin to write to stdin");
202 }
203 return stdinPipe->writeToFd(data, stdinPipe->writeFd());
204 }
205
210 static Pipes separate(bool withStdin = true) {
211 return Pipes {
212 createPipe(),
213 createPipe(),
214 withStdin ? createPipe() : nullptr
215 };
216 }
221 static Pipes shared(bool withStdin = true) {
222 auto outPipe = createPipe();
223 return Pipes {
224 outPipe,
225 outPipe,
226 withStdin ? createPipe() : nullptr
227 };
228
229 }
230};
231
232
236inline std::shared_ptr<PTY> createPTY() {
237 return std::make_shared<PTY>();
238}
239
241 std::map<std::string, std::string> env = {};
245 bool extendEnviron = true;
246
251 std::optional<std::string> workingDirectory = std::nullopt;
252
253 void validate() const {
254 for (auto& [k, v] : env) {
255 if (k.find('=') != std::string::npos) {
256 throw std::runtime_error("Illegal key: " + k);
257 }
258 }
259
260 if (workingDirectory.has_value() && !std::filesystem::is_directory(*workingDirectory)) {
261 throw std::runtime_error(
262 std::format(
263 "Working directory set to {}, which does not exist or isn't a directory",
265 )
266 );
267 }
268 }
269};
270
271struct Config {
272 bool verboseUserOutput = false;
273};
274
275class Process {
276protected:
277 std::optional<decltype(fork())> pid = std::nullopt;
278
279 std::optional<
280 std::variant<Pipes, std::shared_ptr<PTY>>
282 std::stringstream stdoutBuff, stderrBuff;
283 std::mutex lock;
284
285 std::thread inputCollector;
286 std::atomic<int> statusCode = -1;
287 std::atomic<std::optional<bool>> exitedNormally;
288 bool running = true;
289
291
292 bool waitPid(int opts = 0) {
293 int wstatus;
294 if (!pid.has_value()) {
295 std::cerr << "waitPid called, but pid has no value. Something has gone very wrong" << std::endl;
296 exit(70);
297 }
298 if (waitpid(*pid, &wstatus, opts) > 0) {
299 if (WIFEXITED(wstatus)) {
300 statusCode = WEXITSTATUS(wstatus);
301 exitedNormally = true;
302 } else if (WIFSIGNALED(wstatus)) {
303 statusCode = WTERMSIG(wstatus);
304 exitedNormally = false;
305 } else if (WIFSTOPPED(wstatus)) {
306 statusCode = WSTOPSIG(wstatus);
307 exitedNormally = false;
308 } else {
309 std::cerr
310 << "WARNING: stc::Unix::Process got an unknown status: " << wstatus
311 << std::endl;
312 }
313 this->running = false;
314 return true;
315 }
316 return false;
317 }
318
327 char* const* createEnviron(
328 const std::optional<Environment>& env
329 ) {
330 if (env == std::nullopt) {
331 return environ;
332 }
333
334 size_t size = 0;
335 for (char **env = environ; *env != nullptr; env++) {
336 ++size;
337 }
338
339 // This is disgusting, but it beats fucking around with the real raw types. This will technically leak a vector,
340 // but it does not matter because it's disappeared once exec is called:
341 // https://stackoverflow.com/a/3617385
342 // Would prefer to do this better, but I just don't want to
343 std::vector<char*>* data = new std::vector<char*>;
344 if (env->extendEnviron && size > 0) {
345 data->assign(
346 environ, environ + size
347 );
348 }
349
350 data->reserve(
351 // Existing data or 0
352 data->size()
353 // nullptr
354 + 1
355 // Extra envs
356 + env->env.size()
357 );
358 for (const auto& [k, v] : env->env) {
359 if (env->extendEnviron) {
360 // If we're extending environ, keys here can conflict with environ. They won't conflict internally,
361 // because env is a non-multimap, and we enforce keys not containing '=', so no weird injection shit
362 // resulting in identical strings.
363 data->erase(
364 std::remove_if(
365 data->begin(),
366 data->end(),
367 [&](const auto& v) -> bool {
368 return strncmp(v, k.data(), k.size()) == 0;
369 }
370 ), data->end()
371 );
372 }
373
374 std::string combined = std::format(
375 "{}={}", k, v
376 );
377 auto* newStr = strdup(combined.c_str());
378 if (newStr == nullptr) {
379 std::cerr << "Failed to copy string to env" << std::endl;
380 exit(69);
381 }
382 data->push_back(newStr);
383 }
384 data->push_back(nullptr);
385
386 return data->data();
387
388 }
389
391 const std::vector<std::string>& command,
392 const std::function<void()>& readImpl,
393 const std::function<void()>& prepDuping,
394 const std::optional<Environment>& env
395 ) {
396 if (command.size() == 0) {
397 throw std::runtime_error("Cannot run null command");
398 }
399 std::vector<const char*> convertedCommand;
400
401 if (env) {
402 env->validate();
403 }
404
406 std::cout << "Exec: ";
407 }
408 convertedCommand.reserve(command.size() + 1);
409 for (auto& str : command) {
411 std::cout << std::quoted(str);
412 // This isn't strictly speaking necessary, but avoids a situation where the tests need a .starts_with(),
413 // or it'll require the full string to contain a load-bearing linebreak AND a load-bearing trailing
414 // space.
415 if (convertedCommand.size() != command.size() - 1) {
416 std::cout << " ";
417 }
418 }
419 convertedCommand.push_back(str.c_str());
420 }
422 std::cout << "\n";
423 }
424 convertedCommand.push_back(nullptr);
425
426 pid = fork();
427 if (pid < 0) {
428 throw std::runtime_error("Failed to fork");
429 } else if (pid == 0) {
430 // Child process
431 if (prepDuping != nullptr) {
432 prepDuping();
433 }
434
435 // Close handles if they're opened
436 if (interface) {
437 std::visit([](auto& resolved) {
438 using T = std::decay_t<decltype(resolved)>;
439 if constexpr (std::is_same_v<T, std::shared_ptr<PTY>>) {
440 resolved->die();
441 } else {
442 resolved.die();
443 }
444 }, interface.value());
445 }
446
447 if (env.has_value()) {
448 if (env->workingDirectory.has_value()) {
449 // C++17 <3
450 // Avoids chdir() from <unistd.h> so there's one less thing to do if this file can be made portable.
451 std::filesystem::current_path(
452 env->workingDirectory.value()
453 );
454 }
455 }
456
457 execve(
458 convertedCommand.at(0),
459 (char**) convertedCommand.data(),
460 createEnviron(env)
461 );
462 } else {
463 // Parent process
464 if (readImpl != nullptr) {
465 this->inputCollector = std::thread(
466 std::bind(&Process::run, this, readImpl)
467 );
468 }
469 }
470 }
471
472 void run(const std::function<void()>& readImpl) {
473 // TODO: readImpl should bake in some timeout here, but is that enough? Is this thread going to be too busy?
474 do {
475 readImpl();
476 } while (!waitPid(WNOHANG));
477 // Read anything left in the buffer at exit time
478 readImpl();
479 }
480public:
481 [[nodiscard("Discarding immediately terminates the process. You probably don't want this")]]
483 const std::vector<std::string>& command,
484 const std::optional<Environment>& env = std::nullopt,
485 const Config& config = {}
486 ): config(config) {
487 doSpawnCommand(command, nullptr, nullptr, env);
488 }
489
490 [[nodiscard("Discarding immediately terminates the process. You probably don't want this")]]
492 const std::vector<std::string>& command,
493 const Pipes& pipes,
494 const std::optional<Environment>& env = std::nullopt,
495 const Config& config = {}
496 ): config(config) {
497 interface = pipes;
498
499 doSpawnCommand(command, [this]() {
500 auto& pipes = std::get<Pipes>(this->interface.value());
501 if (pipes.stdoutPipe != nullptr) {
502 std::lock_guard l(lock);
503 pipes.stdoutPipe->readData(
505 );
506 }
507 if (pipes.stderrPipe != nullptr) {
508 std::lock_guard l(lock);
509 pipes.stderrPipe->readData(
511 );
512 }
513 }, [&]() {
514 if (pipes.stdinPipe != nullptr) {
515 dup2(pipes.stdinPipe->readFd(), STDIN_FILENO);
516 }
517 if (pipes.stdoutPipe != nullptr) {
518 dup2(pipes.stdoutPipe->writeFd(), STDOUT_FILENO);
519 }
520 if (pipes.stderrPipe != nullptr) {
521 dup2(pipes.stderrPipe->writeFd(), STDERR_FILENO);
522 }
523 std::get<Pipes>(*interface).die();
524 }, env);
525 }
526
527 [[nodiscard("Discarding immediately terminates the process. You probably don't want this")]]
529 const std::vector<std::string>& command,
530 const std::shared_ptr<PTY>& pty,
531 const std::optional<Environment>& env = std::nullopt,
532 const Config& config = {}
533 ): config(config) {
534 if (pty == nullptr) {
535 throw std::runtime_error(
536 "pty cannot be null. If you don't want to attach anything, use the non-pipe/non-PTY constructor instead"
537 );
538 }
539 interface = pty;
540 doSpawnCommand(command, [this]() {
541 auto pty = std::get<std::shared_ptr<PTY>>(this->interface.value());
542
543 {
544 std::lock_guard l(lock);
545 pty->readData(
547 );
548 }
549 // TODO: a random python-related question I stumbled into suggested using two PTYs so the output and input
550 // can be handled separately. This was in relation to closing stdin. In theory, three separate PTYs could be
551 // used to achieve the same system as pipes. I imagine this is what some terminals use to highlight error
552 // output separately from standard output?
553 }, [&]() {
554 dup2(pty->slave, STDIN_FILENO);
555 dup2(pty->slave, STDOUT_FILENO);
556 dup2(pty->slave, STDERR_FILENO);
557 std::get<std::shared_ptr<PTY>>(*interface)->die();
558 }, env);
559
560
561 }
562
563 virtual ~Process() {
564 this->sigkill();
565 this->block();
566 }
567
579 std::string getStdoutBuffer(bool reset = false) {
580 std::lock_guard g(lock);
581 auto str = stdoutBuff.str();
582 if (reset) {
583 stdoutBuff = {};
584 }
585 return str;
586 }
587
598 std::string getStderrBuffer(bool reset = false) {
599 std::lock_guard g(lock);
600 auto str = stderrBuff.str();
601 if (reset) {
602 stderrBuff = {};
603 }
604 return str;
605 }
606
612 std::lock_guard g(lock);
613 stderrBuff = {};
614 stdoutBuff = {};
615 }
616
624 ssize_t writeToStdin(const std::string& data) {
625 if (!this->running) {
626 throw std::runtime_error("Cannot write to closed proc");
627 }
628 if (interface) {
629 return std::visit([&](auto& resolved) -> ssize_t {
630 using T = std::decay_t<decltype(resolved)>;
631 if constexpr (std::is_same_v<T, std::shared_ptr<PTY>>) {
632 return resolved->writeToStdin(data);
633 } else {
634 return resolved.writeToStdin(data);
635 }
636 }, interface.value());
637 } else {
638 throw std::runtime_error("Must use pty or pipe mode to write to stdin");
639 }
640 }
641
647 int block() {
648 if (this->interface) {
649 if (inputCollector.joinable()) {
650 inputCollector.join();
651 }
652 return statusCode;
653 } else {
654 waitPid();
655 return statusCode;
656 }
657 }
658
659 void signal(int sig) {
660 if (statusCode == -1) {
661 if (pid.has_value() && *pid > 0) {
662 kill(*pid, sig);
663 }
664 }
665 }
666
670 void stop() {
671 signal(SIGTERM);
672 }
673
678 void sigkill() {
679 signal(SIGKILL);
680 }
681
682 void closeStdin() {
683 if (!this->interface.has_value()) {
684 throw std::runtime_error("Must use pipe or pty mode to use this function");
685 }
686
687 if (std::holds_alternative<Pipes>(*this->interface)) {
688 auto& ptr = std::get<Pipes>(*this->interface).stdinPipe;
689 if (ptr) {
690 ptr->closeWrite();
691 }
692 }
693 }
694
700 std::optional<bool> hasExitedNormally() {
701 return exitedNormally;
702 }
703
704 std::optional<int> getExitCode() {
705 if (statusCode != -1) {
706 return statusCode;
707 }
708 return std::nullopt;
709 }
710
711};
712
713}
Definition Process.hpp:275
char *const * createEnviron(const std::optional< Environment > &env)
Definition Process.hpp:327
void sigkill()
Definition Process.hpp:678
std::string getStderrBuffer(bool reset=false)
Definition Process.hpp:598
ssize_t writeToStdin(const std::string &data)
Definition Process.hpp:624
void run(const std::function< void()> &readImpl)
Definition Process.hpp:472
std::mutex lock
Definition Process.hpp:283
std::optional< decltype(fork())> pid
Definition Process.hpp:277
std::stringstream stderrBuff
Definition Process.hpp:282
Config config
Definition Process.hpp:290
void closeStdin()
Definition Process.hpp:682
std::optional< int > getExitCode()
Definition Process.hpp:704
std::thread inputCollector
Definition Process.hpp:285
std::stringstream stdoutBuff
Definition Process.hpp:282
void stop()
Definition Process.hpp:670
int block()
Definition Process.hpp:647
virtual ~Process()
Definition Process.hpp:563
bool running
Definition Process.hpp:288
bool waitPid(int opts=0)
Definition Process.hpp:292
void resetBuffers()
Definition Process.hpp:611
std::optional< std::variant< Pipes, std::shared_ptr< PTY > > > interface
Definition Process.hpp:281
void signal(int sig)
Definition Process.hpp:659
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:390
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:528
Process(const std::vector< std::string > &command, const std::optional< Environment > &env=std::nullopt, const Config &config={})
Definition Process.hpp:482
std::optional< bool > hasExitedNormally()
Definition Process.hpp:700
std::string getStdoutBuffer(bool reset=false)
Definition Process.hpp:579
Process(const std::vector< std::string > &command, const Pipes &pipes, const std::optional< Environment > &env=std::nullopt, const Config &config={})
Definition Process.hpp:491
std::atomic< std::optional< bool > > exitedNormally
Definition Process.hpp:287
std::atomic< int > statusCode
Definition Process.hpp:286
Definition Process.hpp:43
std::shared_ptr< PTY > createPTY()
Definition Process.hpp:236
std::shared_ptr< Pipe > createPipe()
Definition Process.hpp:184
Definition Process.hpp:271
bool verboseUserOutput
Definition Process.hpp:272
Definition Process.hpp:240
std::map< std::string, std::string > env
Definition Process.hpp:241
bool extendEnviron
Definition Process.hpp:245
void validate() const
Definition Process.hpp:253
std::optional< std::string > workingDirectory
Definition Process.hpp:251
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:57
Definition Process.hpp:128
~PTY()
Definition Process.hpp:139
int master
Definition Process.hpp:129
ssize_t writeToStdin(const std::string &data)
Definition Process.hpp:172
PTY()
Definition Process.hpp:131
void closeSlaveChannel()
Definition Process.hpp:161
ssize_t readData(std::stringstream &out)
Definition Process.hpp:176
void closeMasterChannel()
Definition Process.hpp:155
int slave
Definition Process.hpp:129
void die()
Definition Process.hpp:150
Definition Process.hpp:86
int readFd()
Definition Process.hpp:116
~Pipe()
Definition Process.hpp:94
Pipe()
Definition Process.hpp:88
void closeRead()
Definition Process.hpp:102
ssize_t readData(std::stringstream &out)
Definition Process.hpp:123
std::array< int, 2 > fds
Definition Process.hpp:87
void closeWrite()
Definition Process.hpp:108
void die()
Definition Process.hpp:98
int writeFd()
Definition Process.hpp:119
Definition Process.hpp:188
std::shared_ptr< Pipe > stderrPipe
Definition Process.hpp:190
ssize_t writeToStdin(const std::string &data)
Definition Process.hpp:199
std::shared_ptr< Pipe > stdoutPipe
Definition Process.hpp:189
std::shared_ptr< Pipe > stdinPipe
Definition Process.hpp:191
static Pipes separate(bool withStdin=true)
Definition Process.hpp:210
static Pipes shared(bool withStdin=true)
Definition Process.hpp:221
void die()
Definition Process.hpp:193