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 <cwchar>
8#include <limits>
9#include <string>
10#include <vector>
11
12#include "absl/cleanup/cleanup.h"
13#include "absl/status/statusor.h"
14#include "absl/strings/ascii.h"
15#include "absl/strings/str_cat.h"
16#include "absl/strings/str_split.h"
17
18#ifdef _WIN32
19#ifndef NOMINMAX
20#define NOMINMAX
21#endif
22#include <windows.h>
23#elif !defined(__EMSCRIPTEN__)
24#include <poll.h>
25#include <signal.h>
26#include <sys/types.h>
27#include <sys/wait.h>
28#include <unistd.h>
29#if defined(__APPLE__)
30#include <TargetConditionals.h>
31#if !TARGET_OS_IPHONE
32#include <crt_externs.h>
33#endif
34#else
35extern char** environ;
36#endif
37#endif
38
39namespace yaze::editor {
40namespace {
41
42constexpr size_t kTailLineLimit = 4;
43constexpr size_t kFailureDetailLimit = 512;
44constexpr int kPollIntervalMs = 50;
45constexpr auto kCancelEscalationDelay = std::chrono::seconds(2);
46
47#ifdef _WIN32
48absl::Status WindowsError(const std::string& action, DWORD error) {
49 return absl::InternalError(
50 absl::StrCat(action, " (Windows error ", error, ")"));
51}
52
53absl::StatusOr<std::wstring> ToWide(const std::string& text) {
54 if (text.empty())
55 return std::wstring();
56 if (text.size() > static_cast<size_t>(std::numeric_limits<int>::max())) {
57 return absl::InvalidArgumentError("Background command text is too large");
58 }
59 const int text_size = static_cast<int>(text.size());
60 const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
61 text.data(), text_size, nullptr, 0);
62 if (length == 0)
63 return WindowsError("Cannot decode UTF-8 command text", GetLastError());
64 std::wstring result(length, L'\0');
65 if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, text.data(), text_size,
66 result.data(), length) == 0) {
67 return WindowsError("Cannot decode UTF-8 command text", GetLastError());
68 }
69 return result;
70}
71
72absl::StatusOr<std::string> ToUtf8(const std::wstring& text) {
73 if (text.empty())
74 return std::string();
75 if (text.size() > static_cast<size_t>(std::numeric_limits<int>::max())) {
76 return absl::InvalidArgumentError(
77 "Background environment text is too large");
78 }
79 const int text_size = static_cast<int>(text.size());
80 const int length =
81 WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text.data(), text_size,
82 nullptr, 0, nullptr, nullptr);
83 if (length == 0)
84 return WindowsError("Cannot encode inherited environment", GetLastError());
85 std::string result(length, '\0');
86 if (WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text.data(), text_size,
87 result.data(), length, nullptr, nullptr) == 0) {
88 return WindowsError("Cannot encode inherited environment", GetLastError());
89 }
90 return result;
91}
92
93struct EnvironmentNameLess {
94 bool operator()(const std::wstring& left, const std::wstring& right) const {
95 return CompareStringOrdinal(left.c_str(), -1, right.c_str(), -1, TRUE) ==
96 CSTR_LESS_THAN;
97 }
98};
99
100class ScopedWindowsHandle {
101 public:
102 explicit ScopedWindowsHandle(HANDLE handle = nullptr) : handle_(handle) {}
103 ~ScopedWindowsHandle() { Reset(); }
104 ScopedWindowsHandle(const ScopedWindowsHandle&) = delete;
105 ScopedWindowsHandle& operator=(const ScopedWindowsHandle&) = delete;
106 HANDLE get() const { return handle_; }
107 bool valid() const { return handle_ && handle_ != INVALID_HANDLE_VALUE; }
108 void Reset(HANDLE handle = nullptr) {
109 if (valid())
110 CloseHandle(handle_);
111 handle_ = handle;
112 }
113
114 private:
115 HANDLE handle_;
116};
117#endif
118
119absl::StatusOr<std::vector<std::string>> ChildEnvironment(
120 const std::map<std::string, std::string>& overrides) {
121 std::vector<std::string> environment;
122#ifdef _WIN32
123 std::map<std::wstring, std::wstring, EnvironmentNameLess> values;
124 const auto block = GetEnvironmentStringsW();
125 if (!block)
126 return WindowsError("Cannot read inherited environment", GetLastError());
127 absl::Cleanup free_environment = [block] {
128 FreeEnvironmentStringsW(block);
129 };
130 for (const wchar_t* entry = block; *entry; entry += std::wcslen(entry) + 1) {
131 const std::wstring text(entry);
132 const auto separator = text.find(L'=', text.front() == L'=' ? 1 : 0);
133 if (separator != std::wstring::npos) {
134 values[text.substr(0, separator)] = text.substr(separator + 1);
135 }
136 }
137 std::map<std::wstring, bool, EnvironmentNameLess> seen;
138 for (const auto& [name, value] : overrides) {
139 auto wide_name = ToWide(name);
140 if (!wide_name.ok())
141 return wide_name.status();
142 auto wide_value = ToWide(value);
143 if (!wide_value.ok())
144 return wide_value.status();
145 if (!seen.emplace(*wide_name, true).second) {
146 return absl::InvalidArgumentError(
147 "Windows child environment overrides contain duplicate names");
148 }
149 values[*wide_name] = *wide_value;
150 }
151 for (const auto& [name, value] : values) {
152 auto entry = ToUtf8(name + L"=" + value);
153 if (!entry.ok())
154 return entry.status();
155 environment.push_back(std::move(*entry));
156 }
157#elif !defined(__EMSCRIPTEN__) && !(defined(__APPLE__) && TARGET_OS_IPHONE)
158#if defined(__APPLE__)
159 char** inherited = *_NSGetEnviron();
160#else
161 char** inherited = environ;
162#endif
163 for (char** entry = inherited; entry && *entry; ++entry) {
164 const std::string value(*entry);
165 const auto separator = value.find('=');
166 if (!overrides.contains(value.substr(0, separator))) {
167 environment.push_back(value);
168 }
169 }
170 for (const auto& [name, value] : overrides) {
171 environment.push_back(name + "=" + value);
172 }
173#endif
174 return environment;
175}
176
178#if defined(__EMSCRIPTEN__) || (defined(__APPLE__) && TARGET_OS_IPHONE)
179 return false;
180#else
181 return true;
182#endif
183}
184
185} // namespace
186
191
193 const std::string& command, const std::string& directory,
194 const std::map<std::string, std::string>& environment) {
195 std::lock_guard<std::mutex> lock(mutex_);
196 if (started_ && !finished_) {
197 return absl::FailedPreconditionError("Task is already running");
198 }
199
200 if (!SupportsNativeBackgroundCommands()) {
201 return absl::UnimplementedError(
202 "Background commands are not supported on this platform");
203 }
204
205 for (const auto& [name, value] : environment) {
206 if (name.empty() || name.find('=') != std::string::npos ||
207 name.find('\0') != std::string::npos ||
208 value.find('\0') != std::string::npos) {
209 return absl::InvalidArgumentError("Invalid child environment override");
210 }
211 }
212#ifdef _WIN32
213 if (command.find('\0') != std::string::npos ||
214 directory.find('\0') != std::string::npos) {
215 return absl::InvalidArgumentError(
216 "Invalid background command or directory");
217 }
218#endif
219 auto child_environment = ChildEnvironment(environment);
220 if (!child_environment.ok())
221 return child_environment.status();
222
223 JoinIfNeeded();
224 started_ = true;
225 running_ = true;
226 finished_ = false;
227 cancel_requested_ = false;
228 exit_code_ = -1;
229 child_pid_ = -1;
230 command_ = command;
231 directory_ = directory;
232 output_.clear();
233 output_tail_.clear();
234 status_ = absl::UnknownError("Task is running");
235 worker_ = std::thread(&BackgroundCommandTask::WorkerMain, this, command,
236 directory, std::move(*child_environment));
237 return absl::OkStatus();
238}
239
241 int pid = -1;
242 {
243 std::lock_guard<std::mutex> lock(mutex_);
244 cancel_requested_ = true;
245 pid = child_pid_;
246 }
247
248#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && \
249 !(defined(__APPLE__) && TARGET_OS_IPHONE)
250 if (pid > 0) {
251 kill(-pid, SIGTERM);
252 kill(pid, SIGTERM);
253 }
254#endif
255}
256
258 std::lock_guard<std::mutex> lock(mutex_);
259 return {.started = started_,
260 .running = running_,
261 .finished = finished_,
262 .cancel_requested = cancel_requested_,
263 .can_cancel = running_,
264 .exit_code = exit_code_,
265 .command = command_,
266 .directory = directory_,
267 .output = output_,
268 .output_tail = output_tail_,
269 .status = status_};
270}
271
273 JoinIfNeeded();
274 std::lock_guard<std::mutex> lock(mutex_);
275 return status_;
276}
277
278void BackgroundCommandTask::WorkerMain(std::string command,
279 std::string directory,
280 std::vector<std::string> environment) {
281#if defined(_WIN32)
282 WorkerMainWindows(std::move(command), std::move(directory),
283 std::move(environment));
284 return;
285#elif defined(__EMSCRIPTEN__) || (defined(__APPLE__) && TARGET_OS_IPHONE)
286 Finalize(absl::UnimplementedError(
287 "Background commands are not supported on this platform"),
288 -1);
289 return;
290#else
291 // All storage and pointers are owned before fork; the child only performs
292 // process setup and exec, without mutating the parent's environment.
293 std::vector<char*> environment_pointers;
294 environment_pointers.reserve(environment.size() + 1);
295 for (auto& entry : environment) {
296 environment_pointers.push_back(entry.data());
297 }
298 environment_pointers.push_back(nullptr);
299
300 int pipe_fds[2];
301 if (pipe(pipe_fds) != 0) {
302 Finalize(absl::InternalError("Failed to create process pipe"), -1);
303 return;
304 }
305
306 const pid_t pid = fork();
307 if (pid < 0) {
308 close(pipe_fds[0]);
309 close(pipe_fds[1]);
310 Finalize(absl::InternalError("Failed to fork background process"), -1);
311 return;
312 }
313
314 if (pid == 0) {
315 setpgid(0, 0);
316 dup2(pipe_fds[1], STDOUT_FILENO);
317 dup2(pipe_fds[1], STDERR_FILENO);
318 close(pipe_fds[0]);
319 close(pipe_fds[1]);
320 if (!directory.empty()) {
321 if (chdir(directory.c_str()) != 0) {
322 constexpr char kPrefix[] = "Cannot enter command working directory: ";
323 constexpr char kNewline[] = "\n";
324 (void)write(STDERR_FILENO, kPrefix, sizeof(kPrefix) - 1);
325 (void)write(STDERR_FILENO, directory.data(), directory.size());
326 (void)write(STDERR_FILENO, kNewline, sizeof(kNewline) - 1);
327 _exit(127);
328 }
329 }
330 execle("/bin/sh", "sh", "-lc", command.c_str(), static_cast<char*>(nullptr),
331 environment_pointers.data());
332 _exit(127);
333 }
334
335 setpgid(pid, pid);
336 close(pipe_fds[1]);
337 {
338 std::lock_guard<std::mutex> lock(mutex_);
339 child_pid_ = pid;
340 }
341
342 std::array<char, 512> buffer{};
343 bool sent_sigterm = false;
344 bool sent_sigkill = false;
345 auto cancel_requested_at = std::chrono::steady_clock::time_point{};
346 bool pipe_closed = false;
347 int wait_status = 0;
348 bool child_reaped = false;
349
350 while (!pipe_closed || !child_reaped) {
351 struct pollfd pfd{pipe_fds[0], POLLIN | POLLHUP, 0};
352 const int poll_result = poll(&pfd, 1, kPollIntervalMs);
353 if (poll_result > 0 && (pfd.revents & (POLLIN | POLLHUP))) {
354 const ssize_t bytes_read =
355 read(pipe_fds[0], buffer.data(), buffer.size());
356 if (bytes_read > 0) {
357 AppendOutput(buffer.data(), static_cast<size_t>(bytes_read));
358 } else if (bytes_read == 0) {
359 pipe_closed = true;
360 } else if (errno != EINTR) {
361 pipe_closed = true;
362 }
363 }
364
365 const bool cancel_requested = [this]() {
366 std::lock_guard<std::mutex> lock(mutex_);
367 return cancel_requested_;
368 }();
369 if (cancel_requested) {
370 if (cancel_requested_at == std::chrono::steady_clock::time_point{}) {
371 cancel_requested_at = std::chrono::steady_clock::now();
372 }
373 if (!sent_sigterm) {
374 kill(-pid, SIGTERM);
375 kill(pid, SIGTERM);
376 sent_sigterm = true;
377 } else if (!sent_sigkill &&
378 std::chrono::steady_clock::now() - cancel_requested_at >=
379 kCancelEscalationDelay) {
380 kill(-pid, SIGKILL);
381 kill(pid, SIGKILL);
382 sent_sigkill = true;
383 }
384 }
385
386 const pid_t wait_result = waitpid(pid, &wait_status, WNOHANG);
387 if (wait_result == pid) {
388 child_reaped = true;
389 }
390 }
391
392 close(pipe_fds[0]);
393 {
394 std::lock_guard<std::mutex> lock(mutex_);
395 child_pid_ = -1;
396 }
397
398 const bool cancel_requested = [this]() {
399 std::lock_guard<std::mutex> lock(mutex_);
400 return cancel_requested_;
401 }();
402 if (cancel_requested) {
403 Finalize(absl::CancelledError("Background command cancelled"), -1);
404 return;
405 }
406
407 if (!WIFEXITED(wait_status) || WEXITSTATUS(wait_status) != 0) {
408 const int exit_code =
409 WIFEXITED(wait_status) ? WEXITSTATUS(wait_status) : -1;
410 std::string message = "Background command failed";
411 if (WIFEXITED(wait_status)) {
412 message += " (exit " + std::to_string(exit_code) + ")";
413 } else if (WIFSIGNALED(wait_status)) {
414 message += " (signal " + std::to_string(WTERMSIG(wait_status)) + ")";
415 }
416 std::string detail;
417 {
418 std::lock_guard<std::mutex> lock(mutex_);
419 detail = output_tail_;
420 }
421 const auto last_newline = detail.rfind('\n');
422 if (last_newline != std::string::npos) {
423 detail.erase(0, last_newline + 1);
424 }
425 if (detail.size() > kFailureDetailLimit) {
426 size_t end = kFailureDetailLimit;
427 // Do not cut a UTF-8 diagnostic in the middle of a code point.
428 while (end > 0 &&
429 (static_cast<unsigned char>(detail[end]) & 0xC0) == 0x80) {
430 --end;
431 }
432 detail.resize(end);
433 detail += "...";
434 }
435 if (!detail.empty()) {
436 message += ": " + detail;
437 }
438 Finalize(absl::InternalError(message), exit_code);
439 return;
440 }
441
442 Finalize(absl::OkStatus(), WEXITSTATUS(wait_status));
443#endif
444}
445
446#ifdef _WIN32
447void BackgroundCommandTask::WorkerMainWindows(
448 std::string command, std::string directory,
449 std::vector<std::string> environment) {
450 const auto cancelled = [this] {
451 std::lock_guard<std::mutex> lock(mutex_);
452 return cancel_requested_;
453 };
454 const auto fail = [this](const std::string& action, DWORD error) {
455 Finalize(WindowsError(action, error), -1);
456 };
457 auto wide_command = ToWide(command);
458 if (!wide_command.ok()) {
459 Finalize(wide_command.status(), -1);
460 return;
461 }
462 auto wide_directory = ToWide(directory);
463 if (!wide_directory.ok()) {
464 Finalize(wide_directory.status(), -1);
465 return;
466 }
467 if (!wide_directory->empty()) {
468 const DWORD required =
469 GetFullPathNameW(wide_directory->c_str(), 0, nullptr, nullptr);
470 if (!required) {
471 fail("Cannot resolve command working directory", GetLastError());
472 return;
473 }
474 std::vector<wchar_t> absolute_directory(required);
475 const DWORD copied = GetFullPathNameW(wide_directory->c_str(), required,
476 absolute_directory.data(), nullptr);
477 if (!copied || copied >= required) {
478 fail("Cannot resolve command working directory", GetLastError());
479 return;
480 }
481 *wide_directory = std::wstring(absolute_directory.data(), copied);
482 const DWORD attributes = GetFileAttributesW(wide_directory->c_str());
483 if (attributes == INVALID_FILE_ATTRIBUTES ||
484 !(attributes & FILE_ATTRIBUTE_DIRECTORY)) {
485 const DWORD error = attributes == INVALID_FILE_ATTRIBUTES
486 ? GetLastError()
487 : ERROR_DIRECTORY;
488 fail("Cannot enter command working directory: " + directory, error);
489 return;
490 }
491 }
492 std::vector<wchar_t> environment_block;
493 for (const auto& entry : environment) {
494 auto wide_entry = ToWide(entry);
495 if (!wide_entry.ok()) {
496 Finalize(wide_entry.status(), -1);
497 return;
498 }
499 environment_block.insert(environment_block.end(), wide_entry->begin(),
500 wide_entry->end());
501 environment_block.push_back(L'\0');
502 }
503 if (environment_block.empty())
504 environment_block.push_back(L'\0');
505 environment_block.push_back(L'\0');
506
507 std::vector<wchar_t> system_directory(MAX_PATH);
508 DWORD system_length =
509 GetSystemDirectoryW(system_directory.data(), system_directory.size());
510 if (system_length >= system_directory.size()) {
511 system_directory.resize(system_length + 1);
512 system_length =
513 GetSystemDirectoryW(system_directory.data(), system_directory.size());
514 }
515 if (!system_length || system_length >= system_directory.size()) {
516 fail("Cannot resolve Windows command interpreter", GetLastError());
517 return;
518 }
519 const std::wstring shell =
520 std::wstring(system_directory.data(), system_length) + L"\\cmd.exe";
521 std::wstring command_line =
522 L"\"" + shell + L"\" /d /s /c \"" + *wide_command + L"\"";
523 if (command_line.size() >= 32767) {
524 Finalize(absl::InvalidArgumentError(
525 "Windows command line exceeds 32766 characters"),
526 -1);
527 return;
528 }
529
530 SECURITY_ATTRIBUTES security{};
531 security.nLength = sizeof(security);
532 security.bInheritHandle = TRUE;
533 HANDLE raw_read = nullptr;
534 HANDLE raw_write = nullptr;
535 if (!CreatePipe(&raw_read, &raw_write, &security, 0)) {
536 fail("Failed to create process pipe", GetLastError());
537 return;
538 }
539 ScopedWindowsHandle pipe_read(raw_read);
540 ScopedWindowsHandle pipe_write(raw_write);
541 if (!SetHandleInformation(pipe_read.get(), HANDLE_FLAG_INHERIT, 0)) {
542 fail("Cannot protect parent process pipe", GetLastError());
543 return;
544 }
545 ScopedWindowsHandle input(
546 CreateFileW(L"NUL", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
547 &security, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
548 if (!input.valid()) {
549 fail("Cannot open background command input", GetLastError());
550 return;
551 }
552 ScopedWindowsHandle job(CreateJobObjectW(nullptr, nullptr));
553 if (!job.valid()) {
554 fail("Cannot create background command job", GetLastError());
555 return;
556 }
557 JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
558 limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
559 if (!SetInformationJobObject(job.get(), JobObjectExtendedLimitInformation,
560 &limits, sizeof(limits))) {
561 fail("Cannot contain background command job", GetLastError());
562 return;
563 }
564
565 SIZE_T attribute_bytes = 0;
566 InitializeProcThreadAttributeList(nullptr, 1, 0, &attribute_bytes);
567 if (!attribute_bytes) {
568 fail("Cannot size background command handle list", GetLastError());
569 return;
570 }
571 std::vector<unsigned char> attribute_storage(attribute_bytes);
572 auto* attributes =
573 reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attribute_storage.data());
574 if (!InitializeProcThreadAttributeList(attributes, 1, 0, &attribute_bytes)) {
575 fail("Cannot initialize background command handle list", GetLastError());
576 return;
577 }
578 absl::Cleanup delete_attributes = [attributes] {
579 DeleteProcThreadAttributeList(attributes);
580 };
581 HANDLE inherited[] = {pipe_write.get(), input.get()};
582 if (!UpdateProcThreadAttribute(attributes, 0,
583 PROC_THREAD_ATTRIBUTE_HANDLE_LIST, inherited,
584 sizeof(inherited), nullptr, nullptr)) {
585 fail("Cannot restrict background command handles", GetLastError());
586 return;
587 }
588 STARTUPINFOEXW startup{};
589 startup.StartupInfo.cb = sizeof(startup);
590 startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
591 startup.StartupInfo.hStdInput = input.get();
592 startup.StartupInfo.hStdOutput = pipe_write.get();
593 startup.StartupInfo.hStdError = pipe_write.get();
594 startup.lpAttributeList = attributes;
595 PROCESS_INFORMATION process_info{};
596 if (cancelled()) {
597 Finalize(absl::CancelledError("Background command cancelled"), -1);
598 return;
599 }
600 if (!CreateProcessW(
601 shell.c_str(), command_line.data(), nullptr, nullptr, TRUE,
602 CREATE_SUSPENDED | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT |
603 EXTENDED_STARTUPINFO_PRESENT,
604 environment_block.data(),
605 wide_directory->empty() ? nullptr : wide_directory->c_str(),
606 &startup.StartupInfo, &process_info)) {
607 fail("Cannot start background command", GetLastError());
608 return;
609 }
610 ScopedWindowsHandle process(process_info.hProcess);
611 ScopedWindowsHandle primary_thread(process_info.hThread);
612 pipe_write.Reset();
613 input.Reset();
614 if (!AssignProcessToJobObject(job.get(), process.get())) {
615 const DWORD assignment_error = GetLastError();
616 if (!TerminateProcess(process.get(), 1)) {
617 fail(absl::StrCat("Cannot stop uncontained suspended command PID ",
618 process_info.dwProcessId),
619 GetLastError());
620 return;
621 }
622 WaitForSingleObject(process.get(), INFINITE);
623 fail("Cannot assign background command to its job", assignment_error);
624 return;
625 }
626 if (cancelled()) {
627 if (!TerminateJobObject(job.get(), 1))
628 job.Reset();
629 WaitForSingleObject(process.get(), INFINITE);
630 Finalize(absl::CancelledError("Background command cancelled"), -1);
631 return;
632 }
633 if (ResumeThread(primary_thread.get()) == static_cast<DWORD>(-1)) {
634 const DWORD resume_error = GetLastError();
635 if (!TerminateJobObject(job.get(), 1))
636 job.Reset();
637 WaitForSingleObject(process.get(), INFINITE);
638 fail("Cannot resume background command", resume_error);
639 return;
640 }
641 primary_thread.Reset();
642
643 std::array<char, 4096> buffer{};
644 bool process_exited = false;
645 bool pipe_closed = false;
646 bool job_terminated = false;
647 bool job_empty = false;
648 DWORD exit_code = 0;
649 absl::Status transport_status = absl::OkStatus();
650 while (!process_exited || !pipe_closed || !job_empty) {
651 if (cancelled() && !job_terminated) {
652 if (!TerminateJobObject(job.get(), 1)) {
653 transport_status = WindowsError("Cannot cancel background command job",
654 GetLastError());
655 job.Reset();
656 WaitForSingleObject(process.get(), INFINITE);
657 break;
658 }
659 job_terminated = true;
660 }
661 bool read_output = false;
662 if (!pipe_closed) {
663 DWORD available = 0;
664 if (PeekNamedPipe(pipe_read.get(), nullptr, 0, nullptr, &available,
665 nullptr)) {
666 if (available) {
667 DWORD bytes_read = 0;
668 if (ReadFile(pipe_read.get(), buffer.data(),
669 std::min<DWORD>(available, buffer.size()), &bytes_read,
670 nullptr)) {
671 if (bytes_read) {
672 AppendOutput(buffer.data(), bytes_read);
673 read_output = true;
674 }
675 } else if (GetLastError() == ERROR_BROKEN_PIPE) {
676 pipe_closed = true;
677 } else {
678 transport_status = WindowsError(
679 "Cannot read background command output", GetLastError());
680 pipe_closed = true;
681 }
682 }
683 } else if (GetLastError() == ERROR_BROKEN_PIPE) {
684 pipe_closed = true;
685 } else {
686 transport_status = WindowsError(
687 "Cannot inspect background command output", GetLastError());
688 pipe_closed = true;
689 }
690 }
691 if (!process_exited) {
692 const DWORD wait =
693 WaitForSingleObject(process.get(), read_output ? 0 : kPollIntervalMs);
694 if (wait == WAIT_OBJECT_0) {
695 if (!GetExitCodeProcess(process.get(), &exit_code)) {
696 transport_status = WindowsError(
697 "Cannot read background command exit code", GetLastError());
698 }
699 process_exited = true;
700 } else if (wait == WAIT_FAILED) {
701 transport_status =
702 WindowsError("Cannot wait for background command", GetLastError());
703 process_exited = true;
704 }
705 } else if (!read_output) {
706 std::this_thread::sleep_for(std::chrono::milliseconds(kPollIntervalMs));
707 }
708 // A completed shell must not leave descendants or inherited pipe writers.
709 if ((process_exited || !transport_status.ok()) && !job_terminated) {
710 if (!TerminateJobObject(job.get(), exit_code)) {
711 transport_status = WindowsError("Cannot finish background command job",
712 GetLastError());
713 job.Reset();
714 WaitForSingleObject(process.get(), INFINITE);
715 break;
716 }
717 job_terminated = true;
718 }
719 JOBOBJECT_BASIC_ACCOUNTING_INFORMATION accounting{};
720 if (!QueryInformationJobObject(job.get(),
721 JobObjectBasicAccountingInformation,
722 &accounting, sizeof(accounting), nullptr)) {
723 transport_status = WindowsError(
724 "Cannot verify background command job cleanup", GetLastError());
725 // Closing the sole kill-on-close handle still terminates the owned tree.
726 job.Reset();
727 WaitForSingleObject(process.get(), INFINITE);
728 break;
729 }
730 job_empty = accounting.ActiveProcesses == 0;
731 }
732 if (cancelled()) {
733 Finalize(absl::CancelledError("Background command cancelled"), -1);
734 return;
735 }
736 if (!transport_status.ok()) {
737 Finalize(transport_status, -1);
738 return;
739 }
740 if (exit_code != 0) {
741 std::string message =
742 absl::StrCat("Background command failed (exit ", exit_code, ")");
743 std::string detail = GetSnapshot().output_tail;
744 const auto last_newline = detail.rfind('\n');
745 if (last_newline != std::string::npos)
746 detail.erase(0, last_newline + 1);
747 if (detail.size() > kFailureDetailLimit) {
748 size_t end = kFailureDetailLimit;
749 while (end > 0 &&
750 (static_cast<unsigned char>(detail[end]) & 0xC0) == 0x80)
751 --end;
752 detail.resize(end);
753 detail += "...";
754 }
755 if (!detail.empty())
756 message += ": " + detail;
757 Finalize(absl::InternalError(message), static_cast<int>(exit_code));
758 return;
759 }
760 Finalize(absl::OkStatus(), 0);
761}
762#endif
763
764void BackgroundCommandTask::AppendOutput(const char* data, size_t size) {
765 std::lock_guard<std::mutex> lock(mutex_);
766 output_.append(data, size);
768}
769
770void BackgroundCommandTask::Finalize(absl::Status status, int exit_code) {
771 std::lock_guard<std::mutex> lock(mutex_);
772 running_ = false;
773 finished_ = true;
774 exit_code_ = exit_code;
775 status_ = std::move(status);
777}
778
780 if (worker_.joinable()) {
781 worker_.join();
782 }
783}
784
786 const std::string& output) {
787 if (output.empty()) {
788 return "";
789 }
790
791 std::vector<std::string> lines;
792 for (absl::string_view line_view : absl::StrSplit(output, '\n')) {
793 std::string line(line_view);
794 absl::StripAsciiWhitespace(&line);
795 if (!line.empty()) {
796 lines.push_back(std::move(line));
797 }
798 }
799 if (lines.empty()) {
800 return "";
801 }
802 const size_t start =
803 lines.size() > kTailLineLimit ? lines.size() - kTailLineLimit : 0;
804 std::string tail;
805 for (size_t i = start; i < lines.size(); ++i) {
806 if (!tail.empty()) {
807 tail += "\n";
808 }
809 tail += lines[i];
810 }
811 return tail;
812}
813
814} // namespace yaze::editor
char ** environ
void Finalize(absl::Status status, int exit_code)
absl::Status Start(const std::string &command, const std::string &directory, const std::map< std::string, std::string > &environment={})
void WorkerMain(std::string command, std::string directory, std::vector< std::string > environment)
static std::string ComputeOutputTail(const std::string &output)
void AppendOutput(const char *data, size_t size)
absl::StatusOr< std::vector< std::string > > ChildEnvironment(const std::map< std::string, std::string > &overrides)
absl::StatusOr< std::string > ReadFile(const std::filesystem::path &path, bool sheet=false, size_t maximum=64 *1024 *1024)
Editors are the view controllers for the application.
char ** environ