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_ = std::thread(&BackgroundCommandTask::WorkerMain, this, command,
72 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 (void)chdir(directory.c_str());
145 }
146 execl("/bin/sh", "sh", "-lc", command.c_str(),
147 static_cast<char*>(nullptr));
148 _exit(127);
149 }
150
151 setpgid(pid, pid);
152 close(pipe_fds[1]);
153 {
154 std::lock_guard<std::mutex> lock(mutex_);
155 child_pid_ = pid;
156 }
157
158 std::array<char, 512> buffer{};
159 bool sent_sigterm = false;
160 bool sent_sigkill = false;
161 auto cancel_requested_at = std::chrono::steady_clock::time_point{};
162 bool pipe_closed = false;
163 int wait_status = 0;
164 bool child_reaped = false;
165
166 while (!pipe_closed || !child_reaped) {
167 struct pollfd pfd {
168 pipe_fds[0], POLLIN | POLLHUP, 0
169 };
170 const int poll_result = poll(&pfd, 1, kPollIntervalMs);
171 if (poll_result > 0 && (pfd.revents & (POLLIN | POLLHUP))) {
172 const ssize_t bytes_read = read(pipe_fds[0], buffer.data(), buffer.size());
173 if (bytes_read > 0) {
174 AppendOutput(buffer.data(), static_cast<size_t>(bytes_read));
175 } else if (bytes_read == 0) {
176 pipe_closed = true;
177 } else if (errno != EINTR) {
178 pipe_closed = true;
179 }
180 }
181
182 const bool cancel_requested = [this]() {
183 std::lock_guard<std::mutex> lock(mutex_);
184 return cancel_requested_;
185 }();
186 if (cancel_requested) {
187 if (cancel_requested_at == std::chrono::steady_clock::time_point{}) {
188 cancel_requested_at = std::chrono::steady_clock::now();
189 }
190 if (!sent_sigterm) {
191 kill(-pid, SIGTERM);
192 kill(pid, SIGTERM);
193 sent_sigterm = true;
194 } else if (!sent_sigkill &&
195 std::chrono::steady_clock::now() - cancel_requested_at >=
196 kCancelEscalationDelay) {
197 kill(-pid, SIGKILL);
198 kill(pid, SIGKILL);
199 sent_sigkill = true;
200 }
201 }
202
203 const pid_t wait_result = waitpid(pid, &wait_status, WNOHANG);
204 if (wait_result == pid) {
205 child_reaped = true;
206 }
207 }
208
209 close(pipe_fds[0]);
210 {
211 std::lock_guard<std::mutex> lock(mutex_);
212 child_pid_ = -1;
213 }
214
215 const bool cancel_requested = [this]() {
216 std::lock_guard<std::mutex> lock(mutex_);
217 return cancel_requested_;
218 }();
219 if (cancel_requested) {
220 Finalize(absl::CancelledError("Background command cancelled"), -1);
221 return;
222 }
223
224 if (!WIFEXITED(wait_status) || WEXITSTATUS(wait_status) != 0) {
225 const int exit_code = WIFEXITED(wait_status) ? WEXITSTATUS(wait_status) : -1;
226 Finalize(absl::InternalError("Background command failed"), exit_code);
227 return;
228 }
229
230 Finalize(absl::OkStatus(), WEXITSTATUS(wait_status));
231#endif
232}
233
234void BackgroundCommandTask::AppendOutput(const char* data, size_t size) {
235 std::lock_guard<std::mutex> lock(mutex_);
236 output_.append(data, size);
238}
239
240void BackgroundCommandTask::Finalize(absl::Status status, int exit_code) {
241 std::lock_guard<std::mutex> lock(mutex_);
242 running_ = false;
243 finished_ = true;
244 exit_code_ = exit_code;
245 status_ = std::move(status);
247}
248
250 if (worker_.joinable()) {
251 worker_.join();
252 }
253}
254
255std::string BackgroundCommandTask::ComputeOutputTail(const std::string& output) {
256 if (output.empty()) {
257 return "";
258 }
259
260 std::vector<std::string> lines;
261 for (absl::string_view line_view : absl::StrSplit(output, '\n')) {
262 std::string line(line_view);
263 absl::StripAsciiWhitespace(&line);
264 if (!line.empty()) {
265 lines.push_back(std::move(line));
266 }
267 }
268 if (lines.empty()) {
269 return "";
270 }
271 const size_t start =
272 lines.size() > kTailLineLimit ? lines.size() - kTailLineLimit : 0;
273 std::string tail;
274 for (size_t i = start; i < lines.size(); ++i) {
275 if (!tail.empty()) {
276 tail += "\n";
277 }
278 tail += lines[i];
279 }
280 return tail;
281}
282
283} // 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.