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"
23#elif !defined(__EMSCRIPTEN__)
30#include <TargetConditionals.h>
32#include <crt_externs.h>
48absl::Status WindowsError(
const std::string& action, DWORD error) {
49 return absl::InternalError(
50 absl::StrCat(action,
" (Windows error ", error,
")"));
53absl::StatusOr<std::wstring> ToWide(
const std::string& text) {
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");
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);
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());
72absl::StatusOr<std::string> ToUtf8(
const std::wstring& text) {
75 if (text.size() >
static_cast<size_t>(std::numeric_limits<int>::max())) {
76 return absl::InvalidArgumentError(
77 "Background environment text is too large");
79 const int text_size =
static_cast<int>(text.size());
81 WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, text.data(), text_size,
82 nullptr, 0,
nullptr,
nullptr);
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());
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) ==
100class ScopedWindowsHandle {
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) {
110 CloseHandle(handle_);
120 const std::map<std::string, std::string>& overrides) {
121 std::vector<std::string> environment;
123 std::map<std::wstring, std::wstring, EnvironmentNameLess> values;
124 const auto block = GetEnvironmentStringsW();
126 return WindowsError(
"Cannot read inherited environment", GetLastError());
127 absl::Cleanup free_environment = [block] {
128 FreeEnvironmentStringsW(block);
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);
137 std::map<std::wstring, bool, EnvironmentNameLess> seen;
138 for (
const auto& [name, value] : overrides) {
139 auto wide_name = ToWide(name);
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");
149 values[*wide_name] = *wide_value;
151 for (
const auto& [name, value] : values) {
152 auto entry = ToUtf8(name + L
"=" + value);
154 return entry.status();
155 environment.push_back(std::move(*entry));
157#elif !defined(__EMSCRIPTEN__) && !(defined(__APPLE__) && TARGET_OS_IPHONE)
158#if defined(__APPLE__)
159 char** inherited = *_NSGetEnviron();
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);
170 for (
const auto& [name, value] : overrides) {
171 environment.push_back(name +
"=" + value);
178#if defined(__EMSCRIPTEN__) || (defined(__APPLE__) && TARGET_OS_IPHONE)
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_);
197 return absl::FailedPreconditionError(
"Task is already running");
200 if (!SupportsNativeBackgroundCommands()) {
201 return absl::UnimplementedError(
202 "Background commands are not supported on this platform");
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");
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");
219 auto child_environment = ChildEnvironment(environment);
220 if (!child_environment.ok())
221 return child_environment.status();
234 status_ = absl::UnknownError(
"Task is running");
236 directory, std::move(*child_environment));
237 return absl::OkStatus();
243 std::lock_guard<std::mutex> lock(
mutex_);
248#if !defined(_WIN32) && !defined(__EMSCRIPTEN__) && \
249 !(defined(__APPLE__) && TARGET_OS_IPHONE)
258 std::lock_guard<std::mutex> lock(
mutex_);
274 std::lock_guard<std::mutex> lock(
mutex_);
279 std::string directory,
280 std::vector<std::string> environment) {
282 WorkerMainWindows(std::move(command), std::move(directory),
283 std::move(environment));
285#elif defined(__EMSCRIPTEN__) || (defined(__APPLE__) && TARGET_OS_IPHONE)
287 "Background commands are not supported on this platform"),
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());
298 environment_pointers.push_back(
nullptr);
301 if (pipe(pipe_fds) != 0) {
302 Finalize(absl::InternalError(
"Failed to create process pipe"), -1);
306 const pid_t pid = fork();
310 Finalize(absl::InternalError(
"Failed to fork background process"), -1);
316 dup2(pipe_fds[1], STDOUT_FILENO);
317 dup2(pipe_fds[1], STDERR_FILENO);
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);
330 execle(
"/bin/sh",
"sh",
"-lc", command.c_str(),
static_cast<char*
>(
nullptr),
331 environment_pointers.data());
338 std::lock_guard<std::mutex> lock(
mutex_);
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;
348 bool child_reaped =
false;
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) {
360 }
else if (errno != EINTR) {
365 const bool cancel_requested = [
this]() {
366 std::lock_guard<std::mutex> lock(
mutex_);
369 if (cancel_requested) {
370 if (cancel_requested_at == std::chrono::steady_clock::time_point{}) {
371 cancel_requested_at = std::chrono::steady_clock::now();
377 }
else if (!sent_sigkill &&
378 std::chrono::steady_clock::now() - cancel_requested_at >=
379 kCancelEscalationDelay) {
386 const pid_t wait_result = waitpid(pid, &wait_status, WNOHANG);
387 if (wait_result == pid) {
394 std::lock_guard<std::mutex> lock(
mutex_);
398 const bool cancel_requested = [
this]() {
399 std::lock_guard<std::mutex> lock(
mutex_);
402 if (cancel_requested) {
403 Finalize(absl::CancelledError(
"Background command cancelled"), -1);
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)) +
")";
418 std::lock_guard<std::mutex> lock(
mutex_);
421 const auto last_newline = detail.rfind(
'\n');
422 if (last_newline != std::string::npos) {
423 detail.erase(0, last_newline + 1);
425 if (detail.size() > kFailureDetailLimit) {
426 size_t end = kFailureDetailLimit;
429 (
static_cast<unsigned char>(detail[end]) & 0xC0) == 0x80) {
435 if (!detail.empty()) {
436 message +=
": " + detail;
438 Finalize(absl::InternalError(message), exit_code);
442 Finalize(absl::OkStatus(), WEXITSTATUS(wait_status));
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_);
454 const auto fail = [
this](
const std::string& action, DWORD error) {
455 Finalize(WindowsError(action, error), -1);
457 auto wide_command = ToWide(command);
458 if (!wide_command.ok()) {
459 Finalize(wide_command.status(), -1);
462 auto wide_directory = ToWide(directory);
463 if (!wide_directory.ok()) {
464 Finalize(wide_directory.status(), -1);
467 if (!wide_directory->empty()) {
468 const DWORD required =
469 GetFullPathNameW(wide_directory->c_str(), 0,
nullptr,
nullptr);
471 fail(
"Cannot resolve command working directory", GetLastError());
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());
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
488 fail(
"Cannot enter command working directory: " + directory, error);
492 std::vector<wchar_t> environment_block;
493 for (
const auto& entry : environment) {
494 auto wide_entry = ToWide(entry);
495 if (!wide_entry.ok()) {
499 environment_block.insert(environment_block.end(), wide_entry->begin(),
501 environment_block.push_back(L
'\0');
503 if (environment_block.empty())
504 environment_block.push_back(L
'\0');
505 environment_block.push_back(L
'\0');
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);
513 GetSystemDirectoryW(system_directory.data(), system_directory.size());
515 if (!system_length || system_length >= system_directory.size()) {
516 fail(
"Cannot resolve Windows command interpreter", GetLastError());
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"),
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());
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());
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());
552 ScopedWindowsHandle job(CreateJobObjectW(
nullptr,
nullptr));
554 fail(
"Cannot create background command job", GetLastError());
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());
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());
571 std::vector<unsigned char> attribute_storage(attribute_bytes);
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());
578 absl::Cleanup delete_attributes = [attributes] {
579 DeleteProcThreadAttributeList(attributes);
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());
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{};
597 Finalize(absl::CancelledError(
"Background command cancelled"), -1);
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());
610 ScopedWindowsHandle process(process_info.hProcess);
611 ScopedWindowsHandle primary_thread(process_info.hThread);
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),
622 WaitForSingleObject(process.get(), INFINITE);
623 fail(
"Cannot assign background command to its job", assignment_error);
627 if (!TerminateJobObject(job.get(), 1))
629 WaitForSingleObject(process.get(), INFINITE);
630 Finalize(absl::CancelledError(
"Background command cancelled"), -1);
633 if (ResumeThread(primary_thread.get()) ==
static_cast<DWORD
>(-1)) {
634 const DWORD resume_error = GetLastError();
635 if (!TerminateJobObject(job.get(), 1))
637 WaitForSingleObject(process.get(), INFINITE);
638 fail(
"Cannot resume background command", resume_error);
641 primary_thread.Reset();
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;
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",
656 WaitForSingleObject(process.get(), INFINITE);
659 job_terminated =
true;
661 bool read_output =
false;
664 if (PeekNamedPipe(pipe_read.get(),
nullptr, 0,
nullptr, &available,
667 DWORD bytes_read = 0;
668 if (
ReadFile(pipe_read.get(), buffer.data(),
669 std::min<DWORD>(available, buffer.size()), &bytes_read,
675 }
else if (GetLastError() == ERROR_BROKEN_PIPE) {
678 transport_status = WindowsError(
679 "Cannot read background command output", GetLastError());
683 }
else if (GetLastError() == ERROR_BROKEN_PIPE) {
686 transport_status = WindowsError(
687 "Cannot inspect background command output", GetLastError());
691 if (!process_exited) {
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());
699 process_exited =
true;
700 }
else if (wait == WAIT_FAILED) {
702 WindowsError(
"Cannot wait for background command", GetLastError());
703 process_exited =
true;
705 }
else if (!read_output) {
706 std::this_thread::sleep_for(std::chrono::milliseconds(kPollIntervalMs));
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",
714 WaitForSingleObject(process.get(), INFINITE);
717 job_terminated =
true;
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());
727 WaitForSingleObject(process.get(), INFINITE);
730 job_empty = accounting.ActiveProcesses == 0;
733 Finalize(absl::CancelledError(
"Background command cancelled"), -1);
736 if (!transport_status.ok()) {
740 if (exit_code != 0) {
741 std::string message =
742 absl::StrCat(
"Background command failed (exit ", exit_code,
")");
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) {
750 (
static_cast<unsigned char>(detail[end]) & 0xC0) == 0x80)
756 message +=
": " + detail;
757 Finalize(absl::InternalError(message),
static_cast<int>(exit_code));
765 std::lock_guard<std::mutex> lock(
mutex_);
771 std::lock_guard<std::mutex> lock(
mutex_);
786 const std::string& output) {
787 if (output.empty()) {
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);
796 lines.push_back(std::move(line));
803 lines.size() > kTailLineLimit ? lines.size() - kTailLineLimit : 0;
805 for (
size_t i = start; i < lines.size(); ++i) {
void Finalize(absl::Status status, int exit_code)
Snapshot GetSnapshot() const
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)
constexpr auto kCancelEscalationDelay
constexpr size_t kTailLineLimit
constexpr size_t kFailureDetailLimit
bool SupportsNativeBackgroundCommands()
absl::StatusOr< std::vector< std::string > > ChildEnvironment(const std::map< std::string, std::string > &overrides)
constexpr int kPollIntervalMs
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.