1 // Copyright 2021 The Tint Authors.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 #include "src/utils/io/command.h"
16
17 #include <sys/poll.h>
18 #include <sys/stat.h>
19 #include <sys/wait.h>
20 #include <unistd.h>
21 #include <sstream>
22 #include <vector>
23
24 namespace tint {
25 namespace utils {
26
27 namespace {
28
29 /// File is a simple wrapper around a POSIX file descriptor
30 class File {
31 constexpr static const int kClosed = -1;
32
33 public:
34 /// Constructor
File()35 File() : handle_(kClosed) {}
36
37 /// Constructor
File(int handle)38 explicit File(int handle) : handle_(handle) {}
39
40 /// Destructor
~File()41 ~File() { Close(); }
42
43 /// Move assignment operator
operator =(File && rhs)44 File& operator=(File&& rhs) {
45 Close();
46 handle_ = rhs.handle_;
47 rhs.handle_ = kClosed;
48 return *this;
49 }
50
51 /// Closes the file (if it wasn't already closed)
Close()52 void Close() {
53 if (handle_ != kClosed) {
54 close(handle_);
55 }
56 handle_ = kClosed;
57 }
58
59 /// @returns the file handle
operator int()60 operator int() { return handle_; }
61
62 /// @returns true if the file is not closed
operator bool()63 operator bool() { return handle_ != kClosed; }
64
65 private:
66 File(const File&) = delete;
67 File& operator=(const File&) = delete;
68
69 int handle_ = kClosed;
70 };
71
72 /// Pipe is a simple wrapper around a POSIX pipe() function
73 class Pipe {
74 public:
75 /// Constructs the pipe
Pipe()76 Pipe() {
77 int pipes[2] = {};
78 if (pipe(pipes) == 0) {
79 read = File(pipes[0]);
80 write = File(pipes[1]);
81 }
82 }
83
84 /// Closes both the read and write files (if they're not already closed)
Close()85 void Close() {
86 read.Close();
87 write.Close();
88 }
89
90 /// @returns true if the pipe has an open read or write file
operator bool()91 operator bool() { return read || write; }
92
93 /// The reader end of the pipe
94 File read;
95
96 /// The writer end of the pipe
97 File write;
98 };
99
ExecutableExists(const std::string & path)100 bool ExecutableExists(const std::string& path) {
101 struct stat s {};
102 if (stat(path.c_str(), &s) != 0) {
103 return false;
104 }
105 return s.st_mode & S_IXUSR;
106 }
107
FindExecutable(const std::string & name)108 std::string FindExecutable(const std::string& name) {
109 if (ExecutableExists(name)) {
110 return name;
111 }
112 if (name.find("/") == std::string::npos) {
113 auto* path_env = getenv("PATH");
114 if (!path_env) {
115 return "";
116 }
117 std::istringstream path{path_env};
118 std::string dir;
119 while (getline(path, dir, ':')) {
120 auto test = dir + "/" + name;
121 if (ExecutableExists(test)) {
122 return test;
123 }
124 }
125 }
126 return "";
127 }
128
129 } // namespace
130
Command(const std::string & path)131 Command::Command(const std::string& path) : path_(path) {}
132
LookPath(const std::string & executable)133 Command Command::LookPath(const std::string& executable) {
134 return Command(FindExecutable(executable));
135 }
136
Found() const137 bool Command::Found() const {
138 return ExecutableExists(path_);
139 }
140
Exec(std::initializer_list<std::string> arguments) const141 Command::Output Command::Exec(
142 std::initializer_list<std::string> arguments) const {
143 if (!Found()) {
144 Output out;
145 out.err = "Executable not found";
146 return out;
147 }
148
149 // Pipes used for piping std[in,out,err] to / from the target process.
150 Pipe stdin_pipe;
151 Pipe stdout_pipe;
152 Pipe stderr_pipe;
153
154 if (!stdin_pipe || !stdout_pipe || !stderr_pipe) {
155 Output output;
156 output.err = "Command::Exec(): Failed to create pipes";
157 return output;
158 }
159
160 // execv() and friends replace the current process image with the target
161 // process image. To keep process that called this function going, we need to
162 // fork() this process into a child and parent process.
163 //
164 // The child process is responsible for hooking up the pipes to
165 // std[in,out,err]_pipes to STD[IN,OUT,ERR]_FILENO and then calling execv() to
166 // run the target command.
167 //
168 // The parent process is responsible for feeding any input to the stdin_pipe
169 // and collectting output from the std[out,err]_pipes.
170
171 int child_id = fork();
172 if (child_id < 0) {
173 Output output;
174 output.err = "Command::Exec(): fork() failed";
175 return output;
176 }
177
178 if (child_id > 0) {
179 // fork() - parent
180
181 // Close the stdout and stderr writer pipes.
182 // This is required for getting poll() POLLHUP events.
183 stdout_pipe.write.Close();
184 stderr_pipe.write.Close();
185
186 // Write the input to the child process
187 if (!input_.empty()) {
188 ssize_t n = write(stdin_pipe.write, input_.data(), input_.size());
189 if (n != static_cast<ssize_t>(input_.size())) {
190 Output output;
191 output.err = "Command::Exec(): write() for stdin failed";
192 return output;
193 }
194 }
195 stdin_pipe.write.Close();
196
197 // Accumulate the stdout and stderr output from the child process
198 pollfd poll_fds[2];
199 poll_fds[0].fd = stdout_pipe.read;
200 poll_fds[0].events = POLLIN;
201 poll_fds[1].fd = stderr_pipe.read;
202 poll_fds[1].events = POLLIN;
203
204 Output output;
205 bool stdout_open = true;
206 bool stderr_open = true;
207 while (stdout_open || stderr_open) {
208 if (poll(poll_fds, 2, -1) < 0) {
209 break;
210 }
211 char buf[256];
212 if (poll_fds[0].revents & POLLIN) {
213 auto n = read(stdout_pipe.read, buf, sizeof(buf));
214 if (n > 0) {
215 output.out += std::string(buf, buf + n);
216 }
217 }
218 if (poll_fds[0].revents & POLLHUP) {
219 stdout_open = false;
220 }
221 if (poll_fds[1].revents & POLLIN) {
222 auto n = read(stderr_pipe.read, buf, sizeof(buf));
223 if (n > 0) {
224 output.err += std::string(buf, buf + n);
225 }
226 }
227 if (poll_fds[1].revents & POLLHUP) {
228 stderr_open = false;
229 }
230 }
231
232 // Get the resulting error code
233 waitpid(child_id, &output.error_code, 0);
234
235 return output;
236 } else {
237 // fork() - child
238
239 // Redirect the stdin, stdout, stderr pipes for the execv process
240 if ((dup2(stdin_pipe.read, STDIN_FILENO) == -1) ||
241 (dup2(stdout_pipe.write, STDOUT_FILENO) == -1) ||
242 (dup2(stderr_pipe.write, STDERR_FILENO) == -1)) {
243 fprintf(stderr, "Command::Exec(): Failed to redirect pipes");
244 exit(errno);
245 }
246
247 // Close the pipes, once redirected above, we're now done with them.
248 stdin_pipe.Close();
249 stdout_pipe.Close();
250 stderr_pipe.Close();
251
252 // Run target executable
253 std::vector<const char*> args;
254 args.emplace_back(path_.c_str());
255 for (auto& arg : arguments) {
256 args.emplace_back(arg.c_str());
257 }
258 args.emplace_back(nullptr);
259 auto res = execv(path_.c_str(), const_cast<char* const*>(args.data()));
260 exit(res);
261 }
262 }
263
264 } // namespace utils
265 } // namespace tint
266