88 static std::fstream
null(
"/dev/null", std::ios::in | std::ios::out);
100inline std::unique_ptr<const char*[]>
to_carray(std::vector<std::string>
const& vec)
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;
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;
118 std::optional<pid_t> m_die_on_pid;
119 std::filesystem::path m_path_file_log;
120 ns_log::Level m_log_level;
125 void die_on_pid(pid_t pid);
127 std::vector<std::jthread> setup_pipes(pid_t child_pid
131 , std::filesystem::path
const& path_file_log);
132 [[noreturn]]
void exec_child();
135 template<ns_concept::StringRepresentable T>
148 template<ns_concept::StringRepresentable K, ns_concept::StringRepresentable V>
151 template<ns_concept::StringRepresentable K>
154 template<
typename... Args>
157 template<
typename... Args>
168 [[maybe_unused]] [[nodiscard]]
Subprocess&
with_streams(std::istream& stdin_stream, std::ostream& stdout_stream, std::ostream& stderr_stream);
178 [[maybe_unused]] [[nodiscard]] std::unique_ptr<Child>
spawn();
203template<ns_concept::StringRepresentable T>
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)
220 m_args.push_back(m_program);
222 for(
char** i = environ; *i !=
nullptr; ++i)
307template<ns_concept::StringRepresentable K, ns_concept::StringRepresentable V>
311 m_env.push_back(std::format(
"{}={}", k, v));
341template<ns_concept::StringRepresentable K>
345 auto it = std::ranges::find_if(m_env, [&](std::string
const& e)
348 return_if(vec.empty(),
false);
349 return vec.front() == k;
353 if ( it != std::ranges::end(m_env) )
355 logger(
"D::Erased var entry: {}", *it);
388template<
typename... Args>
392 auto add_arg = [
this]<
typename T>(T&& arg) ->
void
396 this->m_args.push_back(std::forward<T>(arg));
400 std::copy(arg.begin(), arg.end(), std::back_inserter(m_args));
408 static_assert(
false,
"Could not determine argument type");
413 (add_arg(std::forward<Args>(args)), ...);
447template<
typename... Args>
451 auto f_erase_existing = [
this](
auto&& entries)
453 for (
auto&& entry : entries)
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);
463 auto add_env = [
this, &f_erase_existing]<
typename T>(T&& arg) ->
void
467 f_erase_existing(std::vector<std::string>{arg});
468 this->m_env.push_back(std::forward<T>(arg));
472 f_erase_existing(arg);
473 std::ranges::copy(arg, std::back_inserter(m_env));
478 f_erase_existing(std::vector<std::string>{entry});
479 this->m_env.push_back(entry);
483 static_assert(
false,
"Could not determine argument type");
488 (add_env(std::forward<Args>(args)), ...);
552 m_stream_mode = mode;
586 m_path_file_log = path;
642inline void Subprocess::die_on_pid(pid_t pid)
645 return_if(prctl(PR_SET_PDEATHSIG, SIGKILL) < 0,,
"E::Failed to set PR_SET_PDEATHSIG: {}", strerror(errno));
647 if (::kill(pid, 0) < 0)
649 logger(
"E::Parent died, prctl will not have effect: {}", strerror(errno));
653 logger(
"D::{} dies with {}", getpid(), pid);
659inline void Subprocess::to_dev_null()
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));
682inline std::vector<std::jthread> Subprocess::setup_pipes(pid_t child_pid
686 , std::filesystem::path
const& log)
703[[noreturn]]
inline void Subprocess::exec_child()
710 execve(m_program.c_str(),
const_cast<char**
>(argv_custom.get()),
const_cast<char**
>(envp_custom.get()));
713 logger(
"E::execve() failed: {}", strerror(errno));
764 this->m_stdin = stdin_stream;
765 this->m_stdout = stdout_stream;
766 this->m_stderr = stderr_stream;
814 this->m_callback_child = std::forward<F>(f);
862 m_callback_parent = std::forward<F>(f);
892 m_daemon_mode =
true;
951 return_if(m_args.empty(), Child::create(-1, m_program),
"E::No arguments to spawn subprocess");
952 logger(
"D::Spawn command: {}", m_args);
959 if ( m_stream_mode == Stream::Pipe )
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));
967 pid_t* grandchild_pid_ptr =
nullptr;
970 grandchild_pid_ptr =
static_cast<pid_t*
>(mmap(
973 PROT_READ | PROT_WRITE,
974 MAP_SHARED | MAP_ANONYMOUS,
978 return_if(grandchild_pid_ptr == MAP_FAILED,
nullptr,
"E::mmap failed: {}", strerror(errno));
979 *grandchild_pid_ptr = -1;
988 if (grandchild_pid_ptr !=
nullptr)
990 munmap(grandchild_pid_ptr,
sizeof(pid_t));
992 logger(
"E::Failed to fork");
993 return Child::create(-1, m_program);
999 std::vector<std::jthread> pipe_threads;
1002 if ( m_daemon_mode )
1005 log_if(waitpid(pid, &status, 0) < 0,
"E::Waitpid failed: {}", strerror(errno));
1006 logger(
"D::Daemon mode: intermediate process exited");
1009 pid_t pid_grandchild = *grandchild_pid_ptr;
1012 munmap(grandchild_pid_ptr,
sizeof(pid_t));
1015 logger(
"D::Daemon grandchild PID: {}", pid_grandchild);
1016 return Child::create(pid_grandchild, m_program);
1019 else if ( m_stream_mode == Stream::Pipe)
1021 pipe_threads = this->setup_pipes(pid, pipestdin, pipestdout, pipestderr, m_path_file_log);
1022 logger(
"D::Parent pipes configured");
1026 if (m_callback_parent)
1029 m_callback_parent.value()(args);
1033 return Child::create(pid, m_program, std::move(pipe_threads));
1039 if ( m_daemon_mode )
1044 logger(
"E::setsid() failed: {}", strerror(errno));
1053 logger(
"E::Second fork failed: {}", strerror(errno));
1060 *grandchild_pid_ptr = pid;
1069 munmap(grandchild_pid_ptr,
sizeof(pid_t));
1072 if ( chdir(
"/") < 0 )
1074 logger(
"W::chdir() to / failed: {}", strerror(errno));
1081 else if ( m_stream_mode == Stream::Pipe )
1083 this->setup_pipes(pid, pipestdin, pipestdout, pipestderr, m_path_file_log);
1084 logger(
"D::child pipes configured", m_daemon_mode);
1091 if (m_stream_mode == Stream::Null or m_daemon_mode)
1093 this->to_dev_null();
1101 this->die_on_pid(m_die_on_pid.value());
1105 if (m_callback_child)
1109 .parent_pid = getppid(),
1110 .stdin_fd = STDIN_FILENO,
1111 .stdout_fd = STDOUT_FILENO,
1112 .stderr_fd = STDERR_FILENO
1114 m_callback_child.value()(args);
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.
Concept for iterable containers (has begin() and end())
Concept for types that can be represented as a string.
Handle for a spawned child process.
A library for file logging.
#define logger(fmt,...)
Compile-time log level dispatch macro with automatic location capture.
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)
String manipulation and conversion utilities.
std::string to_string(T &&t) noexcept
Converts a type to a string.
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.
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.
Pipe handling utilities for subprocess.
Arguments passed to child callback.
Arguments passed to parent callback.