| // Copyright 2021 The Dawn & Tint Authors |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // 1. Redistributions of source code must retain the above copyright notice, this |
| // list of conditions and the following disclaimer. |
| // |
| // 2. Redistributions in binary form must reproduce the above copyright notice, |
| // this list of conditions and the following disclaimer in the documentation |
| // and/or other materials provided with the distribution. |
| // |
| // 3. Neither the name of the copyright holder nor the names of its |
| // contributors may be used to endorse or promote products derived from |
| // this software without specific prior written permission. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| // GEN_BUILD:CONDITION(tint_build_is_linux || tint_build_is_mac) |
| |
| #include "src/tint/utils/command/command.h" |
| |
| #include <sys/poll.h> |
| #include <sys/stat.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| #include <sstream> |
| #include <vector> |
| |
| namespace tint { |
| |
| namespace { |
| |
| /// File is a simple wrapper around a POSIX file descriptor |
| class File { |
| constexpr static const int kClosed = -1; |
| |
| public: |
| /// Constructor |
| File() : handle_(kClosed) {} |
| |
| /// Constructor |
| explicit File(int handle) : handle_(handle) {} |
| |
| /// Destructor |
| ~File() { Close(); } |
| |
| /// Move assignment operator |
| File& operator=(File&& rhs) { |
| Close(); |
| handle_ = rhs.handle_; |
| rhs.handle_ = kClosed; |
| return *this; |
| } |
| |
| /// Closes the file (if it wasn't already closed) |
| void Close() { |
| if (handle_ != kClosed) { |
| close(handle_); |
| } |
| handle_ = kClosed; |
| } |
| |
| /// @returns the file handle |
| operator int() { return handle_; } |
| |
| /// @returns true if the file is not closed |
| operator bool() { return handle_ != kClosed; } |
| |
| private: |
| File(const File&) = delete; |
| File& operator=(const File&) = delete; |
| |
| int handle_ = kClosed; |
| }; |
| |
| /// Pipe is a simple wrapper around a POSIX pipe() function |
| class Pipe { |
| public: |
| /// Constructs the pipe |
| Pipe() { |
| int pipes[2] = {}; |
| if (pipe(pipes) == 0) { |
| read = File(pipes[0]); |
| write = File(pipes[1]); |
| } |
| } |
| |
| /// Closes both the read and write files (if they're not already closed) |
| void Close() { |
| read.Close(); |
| write.Close(); |
| } |
| |
| /// @returns true if the pipe has an open read or write file |
| operator bool() { return read || write; } |
| |
| /// The reader end of the pipe |
| File read; |
| |
| /// The writer end of the pipe |
| File write; |
| }; |
| |
| bool ExecutableExists(const std::string& path) { |
| struct stat s {}; |
| if (stat(path.c_str(), &s) != 0) { |
| return false; |
| } |
| return s.st_mode & S_IXUSR; |
| } |
| |
| std::string FindExecutable(const std::string& name) { |
| if (ExecutableExists(name)) { |
| return name; |
| } |
| if (name.find("/") == std::string::npos) { |
| auto* path_env = getenv("PATH"); |
| if (!path_env) { |
| return ""; |
| } |
| std::istringstream path{path_env}; |
| std::string dir; |
| while (getline(path, dir, ':')) { |
| auto test = dir + "/" + name; |
| if (ExecutableExists(test)) { |
| return test; |
| } |
| } |
| } |
| return ""; |
| } |
| |
| } // namespace |
| |
| Command::Command(const std::string& path) : path_(path) {} |
| |
| Command Command::LookPath(const std::string& executable) { |
| return Command(FindExecutable(executable)); |
| } |
| |
| bool Command::Found() const { |
| return ExecutableExists(path_); |
| } |
| |
| Command::Output Command::Exec(std::initializer_list<std::string> arguments) const { |
| if (!Found()) { |
| Output out; |
| out.err = "Executable not found"; |
| return out; |
| } |
| |
| // Pipes used for piping std[in,out,err] to / from the target process. |
| Pipe stdin_pipe; |
| Pipe stdout_pipe; |
| Pipe stderr_pipe; |
| |
| if (!stdin_pipe || !stdout_pipe || !stderr_pipe) { |
| Output output; |
| output.err = "Command::Exec(): Failed to create pipes"; |
| return output; |
| } |
| |
| // execv() and friends replace the current process image with the target |
| // process image. To keep process that called this function going, we need to |
| // fork() this process into a child and parent process. |
| // |
| // The child process is responsible for hooking up the pipes to |
| // std[in,out,err]_pipes to STD[IN,OUT,ERR]_FILENO and then calling execv() to |
| // run the target command. |
| // |
| // The parent process is responsible for feeding any input to the stdin_pipe |
| // and collecting output from the std[out,err]_pipes. |
| |
| int child_id = fork(); |
| if (child_id < 0) { |
| Output output; |
| output.err = "Command::Exec(): fork() failed"; |
| return output; |
| } |
| |
| if (child_id > 0) { |
| // fork() - parent |
| |
| // Close the stdout and stderr writer pipes. |
| // This is required for getting poll() POLLHUP events. |
| stdout_pipe.write.Close(); |
| stderr_pipe.write.Close(); |
| |
| // Write the input to the child process |
| if (!input_.empty()) { |
| ssize_t n = write(stdin_pipe.write, input_.data(), input_.size()); |
| if (n != static_cast<ssize_t>(input_.size())) { |
| Output output; |
| output.err = "Command::Exec(): write() for stdin failed"; |
| return output; |
| } |
| } |
| stdin_pipe.write.Close(); |
| |
| // Accumulate the stdout and stderr output from the child process |
| pollfd poll_fds[2]; |
| poll_fds[0].fd = stdout_pipe.read; |
| poll_fds[0].events = POLLIN; |
| poll_fds[1].fd = stderr_pipe.read; |
| poll_fds[1].events = POLLIN; |
| |
| Output output; |
| bool stdout_open = true; |
| bool stderr_open = true; |
| while (stdout_open || stderr_open) { |
| if (poll(poll_fds, 2, -1) < 0) { |
| break; |
| } |
| char buf[256]; |
| if (poll_fds[0].revents & POLLIN) { |
| auto n = read(stdout_pipe.read, buf, sizeof(buf)); |
| if (n > 0) { |
| output.out += std::string(buf, buf + n); |
| } |
| } |
| if (poll_fds[0].revents & POLLHUP) { |
| stdout_open = false; |
| } |
| if (poll_fds[1].revents & POLLIN) { |
| auto n = read(stderr_pipe.read, buf, sizeof(buf)); |
| if (n > 0) { |
| output.err += std::string(buf, buf + n); |
| } |
| } |
| if (poll_fds[1].revents & POLLHUP) { |
| stderr_open = false; |
| } |
| } |
| |
| // Get the resulting error code |
| waitpid(child_id, &output.error_code, 0); |
| |
| return output; |
| } else { |
| // fork() - child |
| |
| // Redirect the stdin, stdout, stderr pipes for the execv process |
| if ((dup2(stdin_pipe.read, STDIN_FILENO) == -1) || |
| (dup2(stdout_pipe.write, STDOUT_FILENO) == -1) || |
| (dup2(stderr_pipe.write, STDERR_FILENO) == -1)) { |
| fprintf(stderr, "Command::Exec(): Failed to redirect pipes"); |
| exit(errno); |
| } |
| |
| // Close the pipes, once redirected above, we're now done with them. |
| stdin_pipe.Close(); |
| stdout_pipe.Close(); |
| stderr_pipe.Close(); |
| |
| // Run target executable |
| std::vector<const char*> args; |
| args.emplace_back(path_.c_str()); |
| for (auto& arg : arguments) { |
| if (!arg.empty()) { |
| args.emplace_back(arg.c_str()); |
| } |
| } |
| args.emplace_back(nullptr); |
| auto res = execv(path_.c_str(), const_cast<char* const*>(args.data())); |
| exit(res); |
| } |
| } |
| |
| } // namespace tint |