/** @addtogroup Utilities * @{ */ /** @file process.hh * * @brief Portable extensions to process handling. * * Provides POSIX and Windows abstractions for process creation and more. * * @copyright See COPYING.md in the project tree for further information. */ #ifndef OSTD_PROCESS_HH #define OSTD_PROCESS_HH #include #include #include #include #include #include #include #include #include namespace ostd { /** @addtogroup Utilities * @{ */ /** @brief Thrown on errors in ostd::split_args(). */ struct OSTD_EXPORT word_error: std::runtime_error { using std::runtime_error::runtime_error; /* empty, for vtable placement */ virtual ~word_error(); }; namespace detail { OSTD_EXPORT void split_args_impl( string_range const &str, void (*func)(string_range, void *), void *data ); } /** @brief Splits command line argument string into individual arguments. * * The split is done in a platform specific manner, using `wordexp` on POSIX * systems and `CommandLineToArgvW` on Windows. On Windows, the input string * is assumed to be UTF-8 and the output strings are always UTF-8, on POSIX * the wordexp implementation is followed (so it's locale specific). * * The `out` argument can be an output range that takes a single argument * at a time. The value type is an ostd::string_range. If it's not an output * range, it has to be a function that takes the ostd::string_range. However, * the string range that is used internally during the conversions is just * temporary and freed at the end of this function, so it's important that * it's copied in the range or the lambda. * * The ostd::word_error exception is used to handle failures of this * function itself. It may also throw other exceptions though, particularly * those thrown by `out` when putting the strings into it and also * std::bad_alloc on allocation failures. * * @returns The forwarded `out`. * * @throws ostd::word_error on failure or anything thrown by `out`. * @throws std::bad_alloc on alloation failures. */ template Sink &&split_args(Sink &&out, string_range str) { detail::split_args_impl(str, [](string_range val, void *outp) { if constexpr(is_output_range>) { static_cast *>(outp)->put(val); } else { (*static_cast *>(outp))(val); } }, &out); return std::forward(out); } /** @brief Thrown on errors in ostd::subprocess. */ struct OSTD_EXPORT subprocess_error: std::runtime_error { using std::runtime_error::runtime_error; /* empty, for vtable placement */ virtual ~subprocess_error(); }; /** @brief The mode used for standard streams in ostd::subprocess. * * This way you can turn stdin, stdout or stderr of any subprocess into * a stadnard ostd::file_stream or keep them as they are. You can also * redirect stderr into stdout, if stdout itself is redirected then * stderr will point to the newly redirected stdout. * * Only use the `STDOUT` value for stderr stream. */ enum class subprocess_stream { DEFAULT = 0, ///< Do not perform any redirection. PIPE, ///< Capture the stream as an ostd::file_stream. STDOUT ///< Writes to stderr will be written to stdout. }; /** @brief Implements portable subprocess handling. * * This is a universal API with which you can freely manipulate standard * streams of the child process (therefore manipulate its I/O) as well as * get the return code of the child process. It portably covers the role * of `popen` (but it is also more powerful, as it can be bidirectional) * as well as the `exec` family of functions. */ struct OSTD_EXPORT subprocess { private: struct nat {}; std::aligned_storage_t<2 * sizeof(void *), alignof(void *)> p_data; void *p_current = nullptr; public: /** @brief The standard input stream when redirected. * * If no redirection is done (see ostd::subprocess::use_in) then * this stream will not be opened. * * @see ostd::subprocess::out, ostd::subprocess::err */ file_stream in = file_stream{}; /** @brief The standard output stream when redirected. * * If no redirection is done (see ostd::subprocess::use_out) then * this stream will not be opened. * * @see ostd::subprocess::in, ostd::subprocess::err */ file_stream out = file_stream{}; /** @brief The standard error stream when redirected. * * If no redirection is done (see ostd::subprocess::use_err) then * this stream will not be opened. * * @see ostd::subprocess::in, ostd::subprocess::out */ file_stream err = file_stream{}; /** @brief The standard input redirection mode. * * The value is one of ostd::subprocess_stream. Set this before opening * the subprocess. If it's set to `PIPE`, you will be able to write * into the standard input of the child process using the `in` member, * which is a standard ostd::file_stream. Never set it to `STDOUT` * as that will make process opening throw an error. By default * no redirection is done. * * @see ostd::subprocess::in, ostd::subprocess::use_out, * ostd::subprocess::use_err */ subprocess_stream use_in = subprocess_stream::DEFAULT; /** @brief The standard output redirection mode. * * The value is one of ostd::subprocess_stream. Set this before opening * the subprocess. If it's set to `PIPE`, you will be able to read * from the standard output of the child process using the `out` member, * which is a standard ostd::file_stream. Setting this to `STDOUT` has * the same effect as `DEFAULT`. * * @see ostd::subprocess::out, ostd::subprocess::use_in, * ostd::subprocess::use_err */ subprocess_stream use_out = subprocess_stream::DEFAULT; /** @brief The standard error redirection mode. * * The value is one of ostd::subprocess_stream. Set this before opening * the subprocess. If it's set to `PIPE`, you will be able to read * from the standard error of the child process using the `err` member, * which is a standard ostd::file_stream. Setting this to `STDOUT` * redirects the child process standard error into its stanard output, * no matter what the redirection mode of the standard output is, so * they will be effectively the same stream - if you redirect the * standard output you will also read standard error using `err`. * * @see ostd::subprocess::err, ostd::subprocess::use_in, * ostd::subprocess::use_out */ subprocess_stream use_err = subprocess_stream::DEFAULT; /** @brief Initializes the structure with the given stream redirections. */ subprocess( subprocess_stream in_use = subprocess_stream::DEFAULT, subprocess_stream out_use = subprocess_stream::DEFAULT, subprocess_stream err_use = subprocess_stream::DEFAULT ) noexcept: use_in(in_use), use_out(out_use), use_err(err_use) {} /** @brief Initializes the structure and opens a subprocess. * * This is similar to calling either open_command() or open_path() * after constructing the object depending on `use_path`. It may * throw the same exceptions as the respective two methods. */ template subprocess( string_range cmd, InputRange1 &&args, InputRange2 &&envs, bool use_path = true, subprocess_stream in_use = subprocess_stream::DEFAULT, subprocess_stream out_use = subprocess_stream::DEFAULT, subprocess_stream err_use = subprocess_stream::DEFAULT ): use_in(in_use), use_out(out_use), use_err(err_use) { open_full( cmd, std::forward(args), std::forward(envs), use_path ); } /** @brief Initializes the structure and opens a subprocess. * * This is similar to calling either open_command() or open_path() * after constructing the object depending on `use_path`. It may * throw the same exceptions as the respective two methods. */ template subprocess( string_range cmd, InputRange1 &&args, bool use_path = true, subprocess_stream in_use = subprocess_stream::DEFAULT, subprocess_stream out_use = subprocess_stream::DEFAULT, subprocess_stream err_use = subprocess_stream::DEFAULT ): use_in(in_use), use_out(out_use), use_err(err_use) { open_full(cmd, std::forward(args), nullptr, use_path); } /** @brief Moves the subprocess data. */ subprocess(subprocess &&i) noexcept: in(std::move(i.in)), out(std::move(i.out)), err(std::move(i.err)), use_in(i.use_in), use_out(i.use_out), use_err(i.use_err) { move_data(i); } /** @brief Move assigns the subprocess data. * * Effectively equivalent to swap(). */ subprocess &operator=(subprocess &&i) noexcept { swap(i); return *this; } /** @brief Swaps the data of two subprocess structures. */ void swap(subprocess &i) noexcept { using std::swap; swap(use_in, i.use_in); swap(use_out, i.use_out); swap(use_err, i.use_err); swap(in, i.in); swap(out, i.out); swap(err, i.err); swap_data(i); } /** @brief Destroys the subprocess. * * If a child process assigned to this structure currently * exists, it will wait for it to finish first by calling close(). */ ~subprocess() { if (!p_current) { return; } try { close(); } catch (subprocess_error const &) {} reset(); } /** @brief Waits for a currently running child process to be done. * * If there isn't any child process assigned to this, it will throw * ostd::subprocess_error. It will also throw the same exception if * some other error has occured. It will not throw if the command has * executed but exited with a non-zero code. This code will be * returned instead. * * @returns The child process return code on success. * * @throws ostd::subprocess_error on failure of any kind. * * @see open_path(), open_command(), valid() */ int close(); /** @brief Opens a subprocess using the given arguments. * * The `path` is an actual absolute or relative path to an executable * file (as in POSIX `execv` or Windows `CreateProcess`) and `args` * is a range of string-like types (any works as long as a string can * be constructed from it). The first element of `args` is `argv[0]`. * * If `path` is empty, the first element of `args` is used. * * The `envs` argument represents a range of string-like types just like * `args` and each string represents an environment variable of the * subprocess, in format `name=value`. If you do not want to specify * custom environment variables, use an overload without the `envs` * argument - the child process will inherit its parent's environment. * * If this fails for any reason, ostd::subprocess_error will be thrown. * Having another child process running is considered a failure, so * use close() or detach() before opening another. * * On success, a new subprocess will be created and this will return * without waiting for it to finish. Use close() to wait and get the * return code or detach() to give up ownership of the child process. * * @throws ostd::subprocess_error on failure of any kind. * * @see open_command(), close() */ template void open_path(string_range path, InputRange1 &&args, InputRange2 &&envs) { open_full( path, std::forward(args), std::forward(envs), false ); } /** @brief See the main open_path() documentation. */ template void open_path(string_range path, InputRange &&args) { open_full(path, std::forward(args), nullptr, false); } /** @brief Opens a subprocess using the given arguments. * * The `cmd` is a command name looked up in the `PATH` environment * variable when it contains no slash and an ordinary executable * path when it contains one (as in POSIX `execvp` or Windows * `CreateProcess`) and `args` is a range of string-like types * (any works as long as a string can be constructed from it). * The first element of `args` is `argv[0]`. * * If `cmd` is empty, the first element of `args` is used. * * The `envs` argument represents a range of string-like types just like * `args` and each string represents an environment variable of the * subprocess, in format `name=value`. If you do not want to specify * custom environment variables, use an overload without the `envs` * argument - the child process will inherit its parent's environment. * * If this fails for any reason, ostd::subprocess_error will be thrown. * Having another child process running is considered a failure, so * use close() or detach() before opening another. * * On success, a new subprocess will be created and this will return * without waiting for it to finish. Use close() to wait and get the * return code or detach() to give up ownership of the child process. * * @throws ostd::subprocess_error on failure of any kind. * * @see open_path(), close() */ template void open_command(string_range cmd, InputRange1 &&args, InputRange2 &&envs) { open_full( cmd, std::forward(args), std::forward(envs), true ); } /** @brief See the main open_command() documentation. */ template void open_command(string_range cmd, InputRange &&args) { open_full(cmd, std::forward(args), nullptr, true); } /** @brief Detaches the child process. * * If there is a running child process, makes it so that it's no * longer owned by this subprocess object. The child process will * keep running even after the subprocess object has died. * * @throws ostd::subprocess_error if there is no child process. * * @see valid() */ void detach() { if (!p_current) { throw subprocess_error{"no child process"}; } reset(); } /** @brief Checks if there is a running child process. * * @returns If there is one, true, otherwise false. */ bool valid() const noexcept { return !!p_current; } private: template void open_full( string_range cmd, InputRange1 args, [[maybe_unused]] InputRange2 env, bool use_path ) { static_assert( std::is_constructible_v< string_range, range_reference_t >, "The arguments must be strings" ); if constexpr(!std::is_null_pointer_v) { static_assert( std::is_constructible_v< string_range, range_reference_t >, "The environment variables must be strings" ); } if (p_current) { throw subprocess_error{"another child process already running"}; } auto argf = [](string_range &arg, void *data) { InputRange1 &argr = *static_cast(data); if (argr.empty()) { return false; } arg = string_range{argr.front()}; argr.pop_front(); return true; }; bool (*envf)(string_range &, void *) = nullptr; void *argp = &args, *envp = nullptr; if constexpr(!std::is_null_pointer_v) { envf = [](string_range envv, void *data) { InputRange2 &envr = *static_cast(data); if (envr.empty()) { return false; } envv = string_range{envr.front()}; envr.pop_front(); return true; }; envp = &env; } open_impl(use_path, cmd, argf, argp, envf, envp); } void open_impl( bool use_path, string_range cmd, bool (*func)(string_range &, void *), void *data, bool (*efunc)(string_range &, void *), void *edatap ); void reset(); void move_data(subprocess &i); void swap_data(subprocess &i); }; /** @} */ } /* namespace ostd */ #endif /** @} */