summaryrefslogtreecommitdiff
path: root/src/buildtool/file_system/system_command.hpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/buildtool/file_system/system_command.hpp')
-rw-r--r--src/buildtool/file_system/system_command.hpp202
1 files changed, 202 insertions, 0 deletions
diff --git a/src/buildtool/file_system/system_command.hpp b/src/buildtool/file_system/system_command.hpp
new file mode 100644
index 00000000..66470ade
--- /dev/null
+++ b/src/buildtool/file_system/system_command.hpp
@@ -0,0 +1,202 @@
+#ifndef INCLUDED_SRC_BUILDTOOL_FILE_SYSTEM_EXECUTION_SYSTEM_HPP
+#define INCLUDED_SRC_BUILDTOOL_FILE_SYSTEM_EXECUTION_SYSTEM_HPP
+
+#include <array>
+#include <cstdio>
+#include <cstring> // for strerror()
+#include <iterator>
+#include <map>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "gsl-lite/gsl-lite.hpp"
+#include "src/buildtool/file_system/file_system_manager.hpp"
+#include "src/buildtool/logging/logger.hpp"
+
+/// \brief Execute system commands and obtain stdout, stderr and return value.
+/// Subsequent commands are context free and are not affected by previous
+/// commands. This class is not thread-safe.
+class SystemCommand {
+ public:
+ struct ExecOutput {
+ int return_value{};
+ std::filesystem::path stdout_file{};
+ std::filesystem::path stderr_file{};
+ };
+
+ /// \brief Create execution system with name.
+ explicit SystemCommand(std::string name) : logger_{std::move(name)} {}
+
+ /// \brief Execute command and arguments.
+ /// \param argv argv vector with the command to execute
+ /// \param env Environment variables set for execution.
+ /// \param cwd Working directory for execution.
+ /// \param tmpdir Temporary directory for storing stdout/stderr files.
+ /// \returns std::nullopt if there was an error in the execution setup
+ /// outside running the command itself, SystemCommand::ExecOutput otherwise.
+ [[nodiscard]] auto Execute(std::vector<std::string> argv,
+ std::map<std::string, std::string> env,
+ std::filesystem::path const& cwd,
+ std::filesystem::path const& tmpdir) noexcept
+ -> std::optional<ExecOutput> {
+ if (not FileSystemManager::IsDirectory(tmpdir)) {
+ logger_.Emit(LogLevel::Error,
+ "Temporary directory does not exist {}",
+ tmpdir.string());
+ return std::nullopt;
+ }
+
+ if (argv.empty()) {
+ logger_.Emit(LogLevel::Error, "Command cannot be empty.");
+ return std::nullopt;
+ }
+
+ std::vector<char*> cmd = UnwrapStrings(&argv);
+
+ std::vector<std::string> env_string{};
+ std::transform(std::begin(env),
+ std::end(env),
+ std::back_inserter(env_string),
+ [](auto& name_value) {
+ return name_value.first + "=" + name_value.second;
+ });
+ std::vector<char*> envp = UnwrapStrings(&env_string);
+ return ExecuteCommand(cmd.data(), envp.data(), cwd, tmpdir);
+ }
+
+ private:
+ Logger logger_;
+
+ /// \brief Open file exclusively as write-only.
+ [[nodiscard]] static auto OpenFile(
+ std::filesystem::path const& file_path) noexcept {
+ static auto file_closer = [](gsl::owner<FILE*> f) {
+ if (f != nullptr) {
+ std::fclose(f);
+ }
+ };
+ return std::unique_ptr<FILE, decltype(file_closer)>(
+ std::fopen(file_path.c_str(), "wx"), file_closer);
+ }
+
+ /// \brief Execute command and arguments.
+ /// \param cmd Command arguments as char pointer array.
+ /// \param envp Environment variables as char pointer array.
+ /// \param cwd Working directory for execution.
+ /// \param tmpdir Temporary directory for storing stdout/stderr files.
+ /// \returns ExecOutput if command was successfully submitted to the system.
+ /// \returns std::nullopt on internal failure.
+ [[nodiscard]] auto ExecuteCommand(
+ char* const* cmd,
+ char* const* envp,
+ std::filesystem::path const& cwd,
+ std::filesystem::path const& tmpdir) noexcept
+ -> std::optional<ExecOutput> {
+ auto stdout_file = tmpdir / "stdout";
+ auto stderr_file = tmpdir / "stderr";
+ if (auto const out = OpenFile(stdout_file)) {
+ if (auto const err = OpenFile(stderr_file)) {
+ if (auto retval = ForkAndExecute(
+ cmd, envp, cwd, fileno(out.get()), fileno(err.get()))) {
+ return ExecOutput{*retval,
+ std::move(stdout_file),
+ std::move(stderr_file)};
+ }
+ }
+ else {
+ logger_.Emit(LogLevel::Error,
+ "Failed to open stderr file '{}' with error: {}",
+ stderr_file.string(),
+ strerror(errno));
+ }
+ }
+ else {
+ logger_.Emit(LogLevel::Error,
+ "Failed to open stdout file '{}' with error: {}",
+ stdout_file.string(),
+ strerror(errno));
+ }
+
+ return std::nullopt;
+ }
+
+ /// \brief Fork process and exec command.
+ /// \param cmd Command arguments as char pointer array.
+ /// \param envp Environment variables as char pointer array.
+ /// \param cwd Working directory for execution.
+ /// \param out_fd File descriptor to standard output file.
+ /// \param err_fd File descriptor to standard erro file.
+ /// \returns return code if command was successfully submitted to system.
+ /// \returns std::nullopt if fork or exec failed.
+ [[nodiscard]] auto ForkAndExecute(char* const* cmd,
+ char* const* envp,
+ std::filesystem::path const& cwd,
+ int out_fd,
+ int err_fd) const noexcept
+ -> std::optional<int> {
+ // fork child process
+ pid_t pid = ::fork();
+ if (-1 == pid) {
+ logger_.Emit(LogLevel::Error,
+ "Failed to execute '{}': cannot fork a child process.",
+ *cmd);
+ return std::nullopt;
+ }
+
+ // dispatch child/parent process
+ if (pid == 0) {
+ // some executables require an open (possibly seekable) stdin, and
+ // therefore, we use an open temporary file that does not appear
+ // on the file system and will be removed automatically once the
+ // descriptor is closed.
+ gsl::owner<FILE*> in_file = std::tmpfile();
+ auto in_fd = fileno(in_file);
+
+ // redirect and close fds
+ ::dup2(in_fd, STDIN_FILENO);
+ ::dup2(out_fd, STDOUT_FILENO);
+ ::dup2(err_fd, STDERR_FILENO);
+ ::close(in_fd);
+ ::close(out_fd);
+ ::close(err_fd);
+
+ [[maybe_unused]] auto anchor =
+ FileSystemManager::ChangeDirectory(cwd);
+
+ // execute command in child process and exit
+ ::execvpe(*cmd, cmd, envp);
+
+ // report error and terminate child process if ::execvp did not exit
+ logger_.Emit(LogLevel::Error,
+ "Failed to execute '{}' with error: {}",
+ *cmd,
+ strerror(errno));
+
+ std::exit(EXIT_FAILURE);
+ }
+
+ // wait for child to finish and obtain return value
+ int status{};
+ ::waitpid(pid, &status, 0);
+ // NOLINTNEXTLINE(hicpp-signed-bitwise)
+ return WEXITSTATUS(status);
+ }
+
+ static auto UnwrapStrings(std::vector<std::string>* v) noexcept
+ -> std::vector<char*> {
+ std::vector<char*> raw{};
+ std::transform(std::begin(*v),
+ std::end(*v),
+ std::back_inserter(raw),
+ [](auto& str) { return str.data(); });
+ raw.push_back(nullptr);
+ return raw;
+ }
+};
+
+#endif // INCLUDED_SRC_BUILDTOOL_FILE_SYSTEM_EXECUTION_SYSTEM_HPP