yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
background_command_task.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <cerrno>
6#include <chrono>
7#include <string>
8#include <vector>
9
10#include "absl/strings/ascii.h"
11#include "absl/strings/str_split.h"
12
13#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
14#include <poll.h>
15#include <signal.h>
16#include <sys/types.h>
17#include <sys/wait.h>
18#include <unistd.h>
19#if defined(__APPLE__)
20#include <TargetConditionals.h>
21#endif
22#endif
23
24namespace yaze::editor {
25namespace {
26
27constexpr size_t kTailLineLimit = 4;
28constexpr int kPollIntervalMs = 50;
29constexpr auto kCancelEscalationDelay = std::chrono::seconds(2);
30
32#if defined(_WIN32) || defined(__EMSCRIPTEN__) || \
33 (defined(__APPLE__) && TARGET_OS_IPHONE)
34 return false;
35#else
36 return true;
37#endif
38}
39
40} // namespace
41
46
47absl::Status BackgroundCommandTask::Start(const std::string& command,
48 const std::string& directory) {
49 std::lock_guard<std::mutex> lock(mutex_);
50 if (started_ && !finished_) {
51 return absl::FailedPreconditionError("Task is already running");
52 }
53
54 if (!SupportsNativeBackgroundCommands()) {
55 return absl::UnimplementedError(
56 "Background commands are not supported on this platform");
57 }
58
60 started_ = true;
61 running_ = true;
62 finished_ = false;
63 cancel_requested_ = false;
64 exit_code_ = -1;
65 child_pid_ = -1;
66 command_ = command;
67 directory_ = directory;
68 output_.clear();
69 output_tail_.clear();
70 status_ = absl::UnknownError("Task is running");
71 worker_ =
72 std::thread(&BackgroundCommandTask::WorkerMain, this, command, directory);
73 return absl::OkStatus();
74}
75
77 int pid = -1;
78 {
79 std::lock_guard<std::mutex> lock(mutex_);
80 cancel_requested_ = true;
81 pid = child_pid_;
82 }
83
84#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && \
85 !(defined(__APPLE__) && TARGET_OS_IPHONE)
86 if (pid > 0) {
87 kill(-pid, SIGTERM);
88 kill(pid, SIGTERM);
89 }
90#endif
91}
92
94 std::lock_guard<std::mutex> lock(mutex_);
95 return {.started = started_,
96 .running = running_,
97 .finished = finished_,
98 .cancel_requested = cancel_requested_,
99 .can_cancel = running_,
100 .exit_code = exit_code_,
101 .command = command_,
102 .directory = directory_,
103 .output = output_,
104 .output_tail = output_tail_,
105 .status = status_};
106}
107
109 JoinIfNeeded();
110 std::lock_guard<std::mutex> lock(mutex_);
111 return status_;
112}
113
114void BackgroundCommandTask::WorkerMain(std::string command,
115 std::string directory) {
116#if defined(_WIN32) || defined(__EMSCRIPTEN__) || \
117 (defined(__APPLE__) && TARGET_OS_IPHONE)
118 Finalize(absl::UnimplementedError(
119 "Background commands are not supported on this platform"),
120 -1);
121 return;
122#else
123 int pipe_fds[2];
124 if (pipe(pipe_fds) != 0) {
125 Finalize(absl::InternalError("Failed to create process pipe"), -1);
126 return;
127 }
128
129 const pid_t pid = fork();
130 if (pid < 0) {
131 close(pipe_fds[0]);
132 close(pipe_fds[1]);
133 Finalize(absl::InternalError("Failed to fork background process"), -1);
134 return;
135 }
136
137 if (pid == 0) {
138 setpgid(0, 0);
139 dup2(pipe_fds[1], STDOUT_FILENO);
140 dup2(pipe_fds[1], STDERR_FILENO);
141 close(pipe_fds[0]);
142 close(pipe_fds[1]);
143 if (!directory.empty()) {
144 if (chdir(directory.c_str()) != 0) {
145 constexpr char kPrefix[] = "Cannot enter command working directory: ";
146 constexpr char kNewline[] = "\n";
147 (void)write(STDERR_FILENO, kPrefix, sizeof(kPrefix) - 1);
148 (void)write(STDERR_FILENO, directory.data(), directory.size());
149 (void)write(STDERR_FILENO, kNewline, sizeof(kNewline) - 1);
150 _exit(127);
151 }
152 }
153 execl("/bin/sh", "sh", "-lc", command.c_str(), static_cast<char*>(nullptr));
154 _exit(127);
155 }
156
157 setpgid(pid, pid);
158 close(pipe_fds[1]);
159 {
160 std::lock_guard<std::mutex> lock(mutex_);
161 child_pid_ = pid;
162 }
163
164 std::array<char, 512> buffer{};
165 bool sent_sigterm = false;
166 bool sent_sigkill = false;
167 auto cancel_requested_at = std::chrono::steady_clock::time_point{};
168 bool pipe_closed = false;
169 int wait_status = 0;
170 bool child_reaped = false;
171
172 while (!pipe_closed || !child_reaped) {
173 struct pollfd pfd{pipe_fds[0], POLLIN | POLLHUP, 0};
174 const int poll_result = poll(&pfd, 1, kPollIntervalMs);
175 if (poll_result > 0 && (pfd.revents & (POLLIN | POLLHUP))) {
176 const ssize_t bytes_read =
177 read(pipe_fds[0], buffer.data(), buffer.size());
178 if (bytes_read > 0) {
179 AppendOutput(buffer.data(), static_cast<size_t>(bytes_read));
180 } else if (bytes_read == 0) {
181 pipe_closed = true;
182 } else if (errno != EINTR) {
183 pipe_closed = true;
184 }
185 }
186
187 const bool cancel_requested = [this]() {
188 std::lock_guard<std::mutex> lock(mutex_);
189 return cancel_requested_;
190 }();
191 if (cancel_requested) {
192 if (cancel_requested_at == std::chrono::steady_clock::time_point{}) {
193 cancel_requested_at = std::chrono::steady_clock::now();
194 }
195 if (!sent_sigterm) {
196 kill(-pid, SIGTERM);
197 kill(pid, SIGTERM);
198 sent_sigterm = true;
199 } else if (!sent_sigkill &&
200 std::chrono::steady_clock::now() - cancel_requested_at >=
201 kCancelEscalationDelay) {
202 kill(-pid, SIGKILL);
203 kill(pid, SIGKILL);
204 sent_sigkill = true;
205 }
206 }
207
208 const pid_t wait_result = waitpid(pid, &wait_status, WNOHANG);
209 if (wait_result == pid) {
210 child_reaped = true;
211 }
212 }
213
214 close(pipe_fds[0]);
215 {
216 std::lock_guard<std::mutex> lock(mutex_);
217 child_pid_ = -1;
218 }
219
220 const bool cancel_requested = [this]() {
221 std::lock_guard<std::mutex> lock(mutex_);
222 return cancel_requested_;
223 }();
224 if (cancel_requested) {
225 Finalize(absl::CancelledError("Background command cancelled"), -1);
226 return;
227 }
228
229 if (!WIFEXITED(wait_status) || WEXITSTATUS(wait_status) != 0) {
230 const int exit_code =
231 WIFEXITED(wait_status) ? WEXITSTATUS(wait_status) : -1;
232 Finalize(absl::InternalError("Background command failed"), exit_code);
233 return;
234 }
235
236 Finalize(absl::OkStatus(), WEXITSTATUS(wait_status));
237#endif
238}
239
240void BackgroundCommandTask::AppendOutput(const char* data, size_t size) {
241 std::lock_guard<std::mutex> lock(mutex_);
242 output_.append(data, size);
244}
245
246void BackgroundCommandTask::Finalize(absl::Status status, int exit_code) {
247 std::lock_guard<std::mutex> lock(mutex_);
248 running_ = false;
249 finished_ = true;
250 exit_code_ = exit_code;
251 status_ = std::move(status);
253}
254
256 if (worker_.joinable()) {
257 worker_.join();
258 }
259}
260
262 const std::string& output) {
263 if (output.empty()) {
264 return "";
265 }
266
267 std::vector<std::string> lines;
268 for (absl::string_view line_view : absl::StrSplit(output, '\n')) {
269 std::string line(line_view);
270 absl::StripAsciiWhitespace(&line);
271 if (!line.empty()) {
272 lines.push_back(std::move(line));
273 }
274 }
275 if (lines.empty()) {
276 return "";
277 }
278 const size_t start =
279 lines.size() > kTailLineLimit ? lines.size() - kTailLineLimit : 0;
280 std::string tail;
281 for (size_t i = start; i < lines.size(); ++i) {
282 if (!tail.empty()) {
283 tail += "\n";
284 }
285 tail += lines[i];
286 }
287 return tail;
288}
289
290} // namespace yaze::editor
void Finalize(absl::Status status, int exit_code)
void WorkerMain(std::string command, std::string directory)
static std::string ComputeOutputTail(const std::string &output)
absl::Status Start(const std::string &command, const std::string &directory)
void AppendOutput(const char *data, size_t size)
Editors are the view controllers for the application.