FlatImage
A configurable Linux containerization system
Loading...
Searching...
No Matches
subprocess.hpp
Go to the documentation of this file.
1
8
9#pragma once
10
11#include <cstring>
12#include <functional>
13#include <sys/wait.h>
14#include <csignal>
15#include <vector>
16#include <string>
17#include <unistd.h>
18#include <sys/types.h>
19#include <sys/prctl.h>
20#include <sys/stat.h>
21#include <fcntl.h>
22#include <sys/mman.h>
23#include <ranges>
24#include <filesystem>
25#include <utility>
26#include <memory>
27
28#include "log.hpp"
29#include "../macro.hpp"
30#include "../std/vector.hpp"
31#include "subprocess/pipe.hpp"
32#include "subprocess/child.hpp"
33
43namespace ns_subprocess
44{
45
49enum class Stream
50{
51 Inherit, // Child inherits parent's stdin/stdout/stderr
52 Pipe, // Redirect to pipes with callbacks
53 Null, // Redirect to /dev/null (silent)
54};
55
60{
61 pid_t parent_pid; // Parent process PID
62 int stdin_fd; // Standard input file descriptor (usually 0)
63 int stdout_fd; // Standard output file descriptor (usually 1)
64 int stderr_fd; // Standard error file descriptor (usually 2)
65};
66
71{
72 pid_t child_pid; // Child process PID
73};
74
78namespace stream
79{
80
86inline std::fstream& null()
87{
88 static std::fstream null("/dev/null", std::ios::in | std::ios::out);
89 return null;
90}
91
92}
93
100inline std::unique_ptr<const char*[]> to_carray(std::vector<std::string> const& vec)
101{
102 auto arr = std::make_unique<const char*[]>(vec.size() + 1);
103 std::ranges::transform(vec, arr.get(), [](auto const& e) { return e.c_str(); });
104 arr[vec.size()] = nullptr;
105 return arr;
106}
107
109{
110 private:
111 std::filesystem::path m_program;
112 std::vector<std::string> m_args;
113 std::vector<std::string> m_env;
114 std::reference_wrapper<std::istream> m_stdin;
115 std::reference_wrapper<std::ostream> m_stdout;
116 std::reference_wrapper<std::ostream> m_stderr;
117 Stream m_stream_mode;
118 std::optional<pid_t> m_die_on_pid;
119 std::filesystem::path m_path_file_log;
120 ns_log::Level m_log_level;
121 std::optional<std::function<void(ArgsCallbackChild)>> m_callback_child;
122 std::optional<std::function<void(ArgsCallbackParent)>> m_callback_parent;
123 bool m_daemon_mode;
124
125 void die_on_pid(pid_t pid);
126 void to_dev_null();
127 std::vector<std::jthread> setup_pipes(pid_t child_pid
128 , int pipestdin[2]
129 , int pipestdout[2]
130 , int pipestderr[2]
131 , std::filesystem::path const& path_file_log);
132 [[noreturn]] void exec_child();
133
134 public:
135 template<ns_concept::StringRepresentable T>
136 [[nodiscard]] Subprocess(T&& t);
137 ~Subprocess();
138
139 Subprocess(Subprocess const&) = delete;
140 Subprocess& operator=(Subprocess const&) = delete;
141
142 Subprocess(Subprocess&&) = delete;
143 Subprocess& operator=(Subprocess&&) = delete;
144
145
146 [[maybe_unused]] [[nodiscard]] Subprocess& env_clear();
147
148 template<ns_concept::StringRepresentable K, ns_concept::StringRepresentable V>
149 [[maybe_unused]] [[nodiscard]] Subprocess& with_var(K&& k, V&& v);
150
151 template<ns_concept::StringRepresentable K>
152 [[maybe_unused]] [[nodiscard]] Subprocess& rm_var(K&& k);
153
154 template<typename... Args>
155 [[maybe_unused]] [[nodiscard]] Subprocess& with_args(Args&&... args);
156
157 template<typename... Args>
158 [[maybe_unused]] [[nodiscard]] Subprocess& with_env(Args&&... args);
159
160 [[maybe_unused]] [[nodiscard]] Subprocess& with_die_on_pid(pid_t pid);
161
162 [[maybe_unused]] [[nodiscard]] Subprocess& with_stdio(Stream mode);
163
164 [[maybe_unused]] [[nodiscard]] Subprocess& with_log_file(std::filesystem::path const& path);
165
166 [[maybe_unused]] [[nodiscard]] Subprocess& with_log_level(ns_log::Level const& level);
167
168 [[maybe_unused]] [[nodiscard]] Subprocess& with_streams(std::istream& stdin_stream, std::ostream& stdout_stream, std::ostream& stderr_stream);
169
170 template<typename F>
171 [[maybe_unused]] [[nodiscard]] Subprocess& with_callback_child(F&& f);
172
173 template<typename F>
174 [[maybe_unused]] [[nodiscard]] Subprocess& with_callback_parent(F&& f);
175
176 [[maybe_unused]] [[nodiscard]] Subprocess& with_daemon();
177
178 [[maybe_unused]] [[nodiscard]] std::unique_ptr<Child> spawn();
179};
180
203template<ns_concept::StringRepresentable T>
205 : m_program(ns_string::to_string(t))
206 , m_args()
207 , m_env()
208 , m_stdin(stream::null())
209 , m_stdout(stream::null())
210 , m_stderr(stream::null())
211 , m_stream_mode(Stream::Inherit)
212 , m_die_on_pid(std::nullopt)
213 , m_path_file_log("/dev/null")
214 , m_log_level(ns_log::get_level())
215 , m_callback_child(std::nullopt)
216 , m_callback_parent(std::nullopt)
217 , m_daemon_mode(false)
218{
219 // argv0 is program name
220 m_args.push_back(m_program);
221 // Copy environment
222 for(char** i = environ; *i != nullptr; ++i)
223 {
224 m_env.push_back(*i);
225 } // for
226}
227
244{
245}
246
271{
272 m_env.clear();
273 return *this;
274}
275
307template<ns_concept::StringRepresentable K, ns_concept::StringRepresentable V>
309{
310 std::ignore = rm_var(k);
311 m_env.push_back(std::format("{}={}", k, v));
312 return *this;
313}
314
341template<ns_concept::StringRepresentable K>
343{
344 // Find variable
345 auto it = std::ranges::find_if(m_env, [&](std::string const& e)
346 {
347 auto vec = ns_vector::from_string(e, '=');
348 return_if(vec.empty(), false);
349 return vec.front() == k;
350 });
351
352 // Erase if found
353 if ( it != std::ranges::end(m_env) )
354 {
355 logger("D::Erased var entry: {}", *it);
356 m_env.erase(it);
357 } // if
358
359 return *this;
360}
361
388template<typename... Args>
390{
391 // Helper lambda to add a single argument
392 auto add_arg = [this]<typename T>(T&& arg) -> void
393 {
395 {
396 this->m_args.push_back(std::forward<T>(arg));
397 }
398 else if constexpr ( ns_concept::Container<T> )
399 {
400 std::copy(arg.begin(), arg.end(), std::back_inserter(m_args));
401 }
402 else if constexpr ( ns_concept::StringRepresentable<T> )
403 {
404 this->m_args.push_back(ns_string::to_string(std::forward<T>(arg)));
405 }
406 else
407 {
408 static_assert(false, "Could not determine argument type");
409 }
410 };
411
412 // Process each argument
413 (add_arg(std::forward<Args>(args)), ...);
414
415 return *this;
416}
417
447template<typename... Args>
449{
450 // Helper to erase existing entries by key
451 auto f_erase_existing = [this](auto&& entries)
452 {
453 for (auto&& entry : entries)
454 {
455 auto parts = ns_vector::from_string(entry, '=');
456 continue_if(parts.size() < 2, "E::Entry '{}' is not valid", entry);
457 std::string key = parts.front();
458 std::ignore = this->rm_var(key);
459 }
460 };
461
462 // Helper lambda to add a single env entry or container
463 auto add_env = [this, &f_erase_existing]<typename T>(T&& arg) -> void
464 {
465 if constexpr ( ns_concept::Uniform<std::remove_cvref_t<T>, std::string> )
466 {
467 f_erase_existing(std::vector<std::string>{arg});
468 this->m_env.push_back(std::forward<T>(arg));
469 }
470 else if constexpr ( ns_concept::Iterable<T> )
471 {
472 f_erase_existing(arg);
473 std::ranges::copy(arg, std::back_inserter(m_env));
474 }
475 else if constexpr ( ns_concept::StringRepresentable<T> )
476 {
477 auto entry = ns_string::to_string(std::forward<T>(arg));
478 f_erase_existing(std::vector<std::string>{entry});
479 this->m_env.push_back(entry);
480 }
481 else
482 {
483 static_assert(false, "Could not determine argument type");
484 }
485 };
486
487 // Process each argument
488 (add_env(std::forward<Args>(args)), ...);
489
490 return *this;
491}
492
513{
514 m_die_on_pid = pid;
515 return *this;
516}
517
551{
552 m_stream_mode = mode;
553 return *this;
554}
555
584inline Subprocess& Subprocess::with_log_file(std::filesystem::path const& path)
585{
586 m_path_file_log = path;
587 // Set up pipe mode - pipe readers will configure logger sink to the specified path
588 return this->with_stdio(Stream::Pipe);
589}
590
628inline Subprocess& Subprocess::with_log_level(ns_log::Level const& level)
629{
630 m_log_level = level;
631 return *this;
632}
633
642inline void Subprocess::die_on_pid(pid_t pid)
643{
644 // Set death signal when pid dies
645 return_if(prctl(PR_SET_PDEATHSIG, SIGKILL) < 0,,"E::Failed to set PR_SET_PDEATHSIG: {}", strerror(errno));
646 // Abort if pid is not running
647 if (::kill(pid, 0) < 0)
648 {
649 logger("E::Parent died, prctl will not have effect: {}", strerror(errno));
650 _exit(1);
651 } // if
652 // Log pid and current pid
653 logger("D::{} dies with {}", getpid(), pid);
654}
655
659inline void Subprocess::to_dev_null()
660{
661 int fd = open("/dev/null", O_WRONLY);
662 return_if(fd < 0,,"E::Failed to open /dev/null: {}", strerror(errno));
663 return_if(dup2(fd, STDIN_FILENO) < 0,,"E::Failed to redirect stdin: {}", strerror(errno));
664 return_if(dup2(fd, STDOUT_FILENO) < 0,,"E::Failed to redirect stdout: {}", strerror(errno));
665 return_if(dup2(fd, STDERR_FILENO) < 0,,"E::Failed to redirect stderr: {}", strerror(errno));
666 close(fd);
667}
668
682inline std::vector<std::jthread> Subprocess::setup_pipes(pid_t child_pid
683 , int pipestdin[2]
684 , int pipestdout[2]
685 , int pipestderr[2]
686 , std::filesystem::path const& log)
687{
688 // Setup pipes for parent or child and return threads
689 return ns_pipe::setup(child_pid
690 , pipestdin
691 , pipestdout
692 , pipestderr
693 , m_stdin.get()
694 , m_stdout.get()
695 , m_stderr.get()
696 , log
697 );
698}
699
703[[noreturn]] inline void Subprocess::exec_child()
704{
705 // Create null-terminated arrays for execve
706 auto argv_custom = to_carray(m_args);
707 auto envp_custom = to_carray(m_env);
708
709 // Perform execve
710 execve(m_program.c_str(), const_cast<char**>(argv_custom.get()), const_cast<char**>(envp_custom.get()));
711
712 // Log error (will go to log_file if redirected)
713 logger("E::execve() failed: {}", strerror(errno));
714
715 // Child should stop here
716 _exit(1);
717}
718
762inline Subprocess& Subprocess::with_streams(std::istream& stdin_stream, std::ostream& stdout_stream, std::ostream& stderr_stream)
763{
764 this->m_stdin = stdin_stream;
765 this->m_stdout = stdout_stream;
766 this->m_stderr = stderr_stream;
767 return *this;
768}
769
811template<typename F>
813{
814 this->m_callback_child = std::forward<F>(f);
815 return *this;
816}
817
859template<typename F>
861{
862 m_callback_parent = std::forward<F>(f);
863 return *this;
864}
865
891{
892 m_daemon_mode = true;
893 return *this;
894}
895
948inline std::unique_ptr<Child> Subprocess::spawn()
949{
950 // Ignore on empty vec_argv
951 return_if(m_args.empty(), Child::create(-1, m_program), "E::No arguments to spawn subprocess");
952 logger("D::Spawn command: {}", m_args);
953
954 // Create pipes BEFORE fork (if needed for Stream::Pipe)
955 int pipestdin[2];
956 int pipestdout[2];
957 int pipestderr[2];
958
959 if ( m_stream_mode == Stream::Pipe )
960 {
961 return_if(pipe(pipestdin), Child::create(-1, m_program), "E::{}", strerror(errno));
962 return_if(pipe(pipestdout), Child::create(-1, m_program), "E::{}", strerror(errno));
963 return_if(pipe(pipestderr), Child::create(-1, m_program), "E::{}", strerror(errno));
964 }
965
966 // Create shared memory for daemon grandchild PID (if daemon mode)
967 pid_t* grandchild_pid_ptr = nullptr;
968 if ( m_daemon_mode )
969 {
970 grandchild_pid_ptr = static_cast<pid_t*>(mmap(
971 nullptr,
972 sizeof(pid_t),
973 PROT_READ | PROT_WRITE,
974 MAP_SHARED | MAP_ANONYMOUS,
975 -1,
976 0
977 ));
978 return_if(grandchild_pid_ptr == MAP_FAILED, nullptr, "E::mmap failed: {}", strerror(errno));
979 *grandchild_pid_ptr = -1; // Initialize
980 }
981
982 // Create child
983 pid_t pid = fork();
984
985 // Failed to fork
986 if (pid < 0)
987 {
988 if (grandchild_pid_ptr != nullptr)
989 {
990 munmap(grandchild_pid_ptr, sizeof(pid_t));
991 }
992 logger("E::Failed to fork");
993 return Child::create(-1, m_program);
994 }
995
996 // Parent returns here, child continues to execve
997 if ( pid > 0 )
998 {
999 std::vector<std::jthread> pipe_threads;
1000
1001 // If daemon mode, wait for intermediate child to exit and read grandchild PID
1002 if ( m_daemon_mode )
1003 {
1004 int status;
1005 log_if(waitpid(pid, &status, 0) < 0, "E::Waitpid failed: {}", strerror(errno));
1006 logger("D::Daemon mode: intermediate process exited");
1007
1008 // Read grandchild PID from shared memory
1009 pid_t pid_grandchild = *grandchild_pid_ptr;
1010
1011 // Cleanup shared memory
1012 munmap(grandchild_pid_ptr, sizeof(pid_t));
1013
1014 // Return Child handle with grandchild PID
1015 logger("D::Daemon grandchild PID: {}", pid_grandchild);
1016 return Child::create(pid_grandchild, m_program);
1017 }
1018 // Setup pipes for parent and capture threads
1019 else if ( m_stream_mode == Stream::Pipe)
1020 {
1021 pipe_threads = this->setup_pipes(pid, pipestdin, pipestdout, pipestderr, m_path_file_log);
1022 logger("D::Parent pipes configured");
1023 }
1024
1025 // Execute parent callback if provided
1026 if (m_callback_parent)
1027 {
1028 ArgsCallbackParent args{.child_pid = pid};
1029 m_callback_parent.value()(args);
1030 }
1031
1032 // Return Child handle with process and transfer pipe threads ownership
1033 return Child::create(pid, m_program, std::move(pipe_threads));
1034 }
1035
1036 // Child process continues here (intermediate child in daemon mode, final child otherwise)
1037
1038 // Daemon mode: perform second fork to create grandchild
1039 if ( m_daemon_mode )
1040 {
1041 // Create session leader to detach from controlling terminal
1042 if ( setsid() < 0 )
1043 {
1044 logger("E::setsid() failed: {}", strerror(errno));
1045 _exit(1);
1046 }
1047
1048 // Second fork - creates grandchild
1049 pid = fork();
1050
1051 if ( pid < 0 )
1052 {
1053 logger("E::Second fork failed: {}", strerror(errno));
1054 _exit(1);
1055 }
1056
1057 if ( pid > 0 )
1058 {
1059 // Intermediate child: write grandchild PID to shared memory
1060 *grandchild_pid_ptr = pid;
1061
1062 // Intermediate child exits immediately
1063 // This causes grandchild to be orphaned and adopted by init
1064 _exit(0);
1065 }
1066
1067 // Grandchild continues here - now a daemon
1068 // Cleanup shared memory in grandchild (no longer needed)
1069 munmap(grandchild_pid_ptr, sizeof(pid_t));
1070
1071 // Change working directory to root to avoid keeping directories busy
1072 if ( chdir("/") < 0 )
1073 {
1074 logger("W::chdir() to / failed: {}", strerror(errno));
1075 }
1076
1077 // Reset file mode creation mask
1078 umask(0);
1079 }
1080 // Setup pipes for child
1081 else if ( m_stream_mode == Stream::Pipe )
1082 {
1083 this->setup_pipes(pid, pipestdin, pipestdout, pipestderr, m_path_file_log);
1084 logger("D::child pipes configured", m_daemon_mode);
1085 }
1086
1087 ns_log::set_level(m_log_level);
1088
1089 // Handle stdio redirection based on mode
1090 // Daemon should not inherit the parent's IO
1091 if (m_stream_mode == Stream::Null or m_daemon_mode)
1092 {
1093 this->to_dev_null();
1094 }
1095 // Stream::Pipe: child's stdout/stderr redirected by pipes_child() via setup_pipes()
1096 // Stream::Inherit: do nothing, child keeps parent's stdio
1097
1098 // Die with pid
1099 if(m_die_on_pid)
1100 {
1101 this->die_on_pid(m_die_on_pid.value());
1102 }
1103
1104 // Execute child callback if provided
1105 if (m_callback_child)
1106 {
1108 {
1109 .parent_pid = getppid(),
1110 .stdin_fd = STDIN_FILENO,
1111 .stdout_fd = STDOUT_FILENO,
1112 .stderr_fd = STDERR_FILENO
1113 };
1114 m_callback_child.value()(args);
1115 }
1116
1117 // Execute child process (never returns)
1118 this->exec_child();
1119}
1120
1121} // namespace ns_subprocess
1122
1123/* vim: set expandtab fdm=marker ts=2 sw=2 tw=100 et :*/
Subprocess & with_die_on_pid(pid_t pid)
Configures the child process to die when the specified PID dies.
Subprocess & with_var(K &&k, V &&v)
Adds or replaces a single environment variable.
std::unique_ptr< Child > spawn()
Spawns (forks) the child process and begins execution.
Subprocess & with_stdio(Stream mode)
Sets the stdio redirection mode for the child process.
Subprocess & with_log_file(std::filesystem::path const &path)
Configures logging output for child process stdout/stderr.
Subprocess & with_callback_child(F &&f)
Sets a callback to run in the child process after fork() but before execve()
Subprocess & with_streams(std::istream &stdin_stream, std::ostream &stdout_stream, std::ostream &stderr_stream)
Configures stream handlers for stdin, stdout, and stderr of the child process.
Subprocess & with_env(Args &&... args)
Includes environment variables with the format 'NAME=VALUE' in the environment.
Subprocess & with_daemon()
Enable daemon mode using double fork pattern.
Subprocess & with_callback_parent(F &&f)
Sets a callback to run in the parent process after fork()
Subprocess & with_log_level(ns_log::Level const &level)
Sets the logging level for the pipe reader processes.
Subprocess(T &&t)
Construct a new Subprocess object with a program path.
Subprocess & env_clear()
Clears all environment variables before starting the process.
Subprocess & with_args(Args &&... args)
Arguments forwarded as the process' arguments.
~Subprocess()
Destroy the Subprocess object.
Subprocess & rm_var(K &&k)
Removes an environment variable from the process environment.
Concept for standard library container types.
Definition concept.hpp:319
Concept for iterable containers (has begin() and end())
Definition concept.hpp:227
Concept for types that can be represented as a string.
Definition concept.hpp:442
Concept to check if all types are the same.
Definition concept.hpp:198
Handle for a spawned child process.
A library for file logging.
#define logger(fmt,...)
Compile-time log level dispatch macro with automatic location capture.
Definition log.hpp:682
Simplified macros for common control flow patterns with optional logging.
Multi-level logging system with file and stdout sinks.
void set_level(Level level)
Sets the logging verbosity (CRITICAL,ERROR,INFO,DEBUG)
Definition log.hpp:339
String manipulation and conversion utilities.
std::string to_string(T &&t) noexcept
Converts a type to a string.
Definition string.hpp:97
std::vector< std::jthread > setup(pid_t pid, int pipestdin[2], int pipestdout[2], int pipestderr[2], std::istream &stdin, std::ostream &stdout, std::ostream &stderr, std::filesystem::path const &path_file_log)
Handle pipe setup for both parent and child processes.
Definition pipe.hpp:204
Custom stream redirection for child process stdio.
std::fstream & null()
Redirects to /dev/null (silent)
Child process management and execution.
Stream
Stream redirection modes for child process stdio.
std::unique_ptr< const char *[]> to_carray(std::vector< std::string > const &vec)
Converts a vector of strings to a null-terminated C-style array for execve.
R from_string(ns_concept::StringRepresentable auto &&t, char delimiter) noexcept
Creates a range from a string.
Definition vector.hpp:84
Pipe handling utilities for subprocess.
Arguments passed to child callback.
Arguments passed to parent callback.
Vector helpers.