mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-26 08:01:12 +01:00
271 lines
8.3 KiB
C++
271 lines
8.3 KiB
C++
// Copyright 2026 Mocktail Project Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "runtime/failure_dialog.h"
|
|
|
|
#include <spawn.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cerrno>
|
|
#include <filesystem>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
extern char** environ;
|
|
|
|
#ifndef MOCKTAIL_INSTALL_LIBDIR
|
|
#define MOCKTAIL_INSTALL_LIBDIR "lib"
|
|
#endif
|
|
|
|
namespace mocktail {
|
|
namespace runtime {
|
|
namespace {
|
|
|
|
constexpr char kMessagePacket = 'M';
|
|
constexpr char kFailurePacket = 'F';
|
|
constexpr char kSuccessPacket = 'S';
|
|
constexpr std::size_t kMaximumMessageBytes = 2048;
|
|
|
|
bool Enabled(const Environment& environment, std::string_view name) {
|
|
const std::optional<std::string> value = environment.Get(name);
|
|
return value.has_value() && !value->empty() && *value != "0";
|
|
}
|
|
|
|
bool HasNonEmpty(const Environment& environment, std::string_view name) {
|
|
const std::optional<std::string> value = environment.Get(name);
|
|
return value.has_value() && !value->empty();
|
|
}
|
|
|
|
bool IsExecutableRegularFile(const std::filesystem::path& path) {
|
|
std::error_code error;
|
|
return std::filesystem::is_regular_file(path, error) &&
|
|
!std::filesystem::is_symlink(path, error) &&
|
|
access(path.c_str(), X_OK) == 0;
|
|
}
|
|
|
|
std::filesystem::path DialogHelper(const Environment& environment) {
|
|
const std::optional<std::string> helper_override =
|
|
environment.Get("MOCKTAIL_FAILURE_DIALOG_HELPER");
|
|
if (helper_override.has_value()) {
|
|
const std::filesystem::path helper(*helper_override);
|
|
return helper.is_absolute() && IsExecutableRegularFile(helper)
|
|
? helper
|
|
: std::filesystem::path{};
|
|
}
|
|
|
|
const std::optional<std::string> project_override =
|
|
environment.Get("MOCKTAIL_PROJECT_ROOT");
|
|
if (project_override.has_value()) {
|
|
const std::filesystem::path root(*project_override);
|
|
if (root.is_absolute()) {
|
|
const std::filesystem::path helper =
|
|
root / "bin" / "mocktail_failure_dialog";
|
|
if (IsExecutableRegularFile(helper)) {
|
|
return helper;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::error_code error;
|
|
const std::filesystem::path executable =
|
|
std::filesystem::read_symlink("/proc/self/exe", error);
|
|
if (error || executable.empty()) {
|
|
return {};
|
|
}
|
|
const std::filesystem::path executable_dir = executable.parent_path();
|
|
const std::vector<std::filesystem::path> candidates = {
|
|
executable_dir / "mocktail_failure_dialog",
|
|
executable_dir.parent_path() / MOCKTAIL_INSTALL_LIBDIR / "mocktail" /
|
|
"mocktail_failure_dialog",
|
|
executable_dir.parent_path() / "libexec" / "mocktail" /
|
|
"mocktail_failure_dialog",
|
|
};
|
|
for (const std::filesystem::path& candidate : candidates) {
|
|
if (IsExecutableRegularFile(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void WaitForHelper(int helper_pid) {
|
|
if (helper_pid <= 0) {
|
|
return;
|
|
}
|
|
while (waitpid(helper_pid, nullptr, 0) < 0 && errno == EINTR) {
|
|
}
|
|
}
|
|
|
|
bool SpawnOneShot(const std::filesystem::path& helper,
|
|
std::string_view message) {
|
|
std::string helper_string = helper.string();
|
|
std::string message_string(message.substr(0, kMaximumMessageBytes));
|
|
char message_option[] = "--message";
|
|
char* arguments[] = {helper_string.data(), message_option,
|
|
message_string.data(), nullptr};
|
|
pid_t child = -1;
|
|
const int spawn_status =
|
|
posix_spawn(&child, helper.c_str(), nullptr, nullptr, arguments, environ);
|
|
if (spawn_status != 0) {
|
|
return false;
|
|
}
|
|
WaitForHelper(child);
|
|
return true;
|
|
}
|
|
|
|
std::pair<int, int> SpawnMonitor(const std::filesystem::path& helper) {
|
|
int sockets[2] = {-1, -1};
|
|
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sockets) != 0) {
|
|
return {-1, -1};
|
|
}
|
|
|
|
posix_spawn_file_actions_t actions;
|
|
if (posix_spawn_file_actions_init(&actions) != 0) {
|
|
close(sockets[0]);
|
|
close(sockets[1]);
|
|
return {-1, -1};
|
|
}
|
|
int action_status = 0;
|
|
if (sockets[0] == STDIN_FILENO) {
|
|
action_status = posix_spawn_file_actions_addclose(&actions, sockets[0]);
|
|
}
|
|
if (action_status == 0 && sockets[1] != STDIN_FILENO) {
|
|
action_status =
|
|
posix_spawn_file_actions_adddup2(&actions, sockets[1], STDIN_FILENO);
|
|
}
|
|
if (action_status == 0 && sockets[0] != STDIN_FILENO) {
|
|
action_status = posix_spawn_file_actions_addclose(&actions, sockets[0]);
|
|
}
|
|
if (action_status == 0 && sockets[1] != STDIN_FILENO) {
|
|
action_status = posix_spawn_file_actions_addclose(&actions, sockets[1]);
|
|
}
|
|
|
|
std::string helper_string = helper.string();
|
|
char monitor_option[] = "--monitor";
|
|
char* arguments[] = {helper_string.data(), monitor_option, nullptr};
|
|
pid_t child = -1;
|
|
int spawn_status = action_status;
|
|
if (spawn_status == 0) {
|
|
spawn_status = posix_spawn(&child, helper.c_str(), &actions, nullptr,
|
|
arguments, environ);
|
|
}
|
|
posix_spawn_file_actions_destroy(&actions);
|
|
close(sockets[1]);
|
|
if (spawn_status != 0) {
|
|
close(sockets[0]);
|
|
return {-1, -1};
|
|
}
|
|
return {sockets[0], static_cast<int>(child)};
|
|
}
|
|
|
|
bool SendPacket(int socket, char kind, std::string_view payload = {}) {
|
|
std::string packet(1, kind);
|
|
packet.append(payload.substr(0, kMaximumMessageBytes));
|
|
const ssize_t sent = send(socket, packet.data(), packet.size(), MSG_NOSIGNAL);
|
|
return sent == static_cast<ssize_t>(packet.size());
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool FailureDialogsEnabled(const Environment& environment) {
|
|
if (Enabled(environment, "MOCKTAIL_DISABLE_FAILURE_DIALOG") ||
|
|
Enabled(environment, "MOCKTAIL_ISOLATED_CANARY") ||
|
|
Enabled(environment, "MOCKTAIL_HEADLESS")) {
|
|
return false;
|
|
}
|
|
return HasNonEmpty(environment, "DISPLAY") ||
|
|
HasNonEmpty(environment, "WAYLAND_DISPLAY");
|
|
}
|
|
|
|
bool ShowFailureDialog(const Environment& environment,
|
|
std::string_view message) {
|
|
if (!FailureDialogsEnabled(environment)) {
|
|
return false;
|
|
}
|
|
const std::filesystem::path helper = DialogHelper(environment);
|
|
return !helper.empty() && SpawnOneShot(helper, message);
|
|
}
|
|
|
|
FailureDialogMonitor::FailureDialogMonitor(int socket, int helper_pid)
|
|
: socket_(socket), helper_pid_(helper_pid) {}
|
|
|
|
FailureDialogMonitor::~FailureDialogMonitor() { Finish(kFailurePacket); }
|
|
|
|
FailureDialogMonitor::FailureDialogMonitor(
|
|
FailureDialogMonitor&& other) noexcept
|
|
: socket_(std::exchange(other.socket_, -1)),
|
|
helper_pid_(std::exchange(other.helper_pid_, -1)) {}
|
|
|
|
FailureDialogMonitor& FailureDialogMonitor::operator=(
|
|
FailureDialogMonitor&& other) noexcept {
|
|
if (this == &other) {
|
|
return *this;
|
|
}
|
|
Finish(kFailurePacket);
|
|
socket_ = std::exchange(other.socket_, -1);
|
|
helper_pid_ = std::exchange(other.helper_pid_, -1);
|
|
return *this;
|
|
}
|
|
|
|
FailureDialogMonitor FailureDialogMonitor::Start(
|
|
const Environment& environment, std::string_view initial_message) {
|
|
if (!FailureDialogsEnabled(environment)) {
|
|
return {};
|
|
}
|
|
const std::filesystem::path helper = DialogHelper(environment);
|
|
if (helper.empty()) {
|
|
return {};
|
|
}
|
|
const auto [socket, helper_pid] = SpawnMonitor(helper);
|
|
FailureDialogMonitor monitor(socket, helper_pid);
|
|
if (monitor.active()) {
|
|
monitor.SetMessage(initial_message);
|
|
}
|
|
return monitor;
|
|
}
|
|
|
|
void FailureDialogMonitor::SetMessage(std::string_view message) {
|
|
if (!active() || message.empty()) {
|
|
return;
|
|
}
|
|
if (!SendPacket(socket_, kMessagePacket, message)) {
|
|
close(socket_);
|
|
socket_ = -1;
|
|
WaitForHelper(helper_pid_);
|
|
helper_pid_ = -1;
|
|
}
|
|
}
|
|
|
|
void FailureDialogMonitor::MarkSuccessful() { Finish(kSuccessPacket); }
|
|
|
|
void FailureDialogMonitor::Finish(char disposition) {
|
|
if (!active()) {
|
|
return;
|
|
}
|
|
(void)SendPacket(socket_, disposition);
|
|
(void)shutdown(socket_, SHUT_RDWR);
|
|
close(socket_);
|
|
socket_ = -1;
|
|
WaitForHelper(helper_pid_);
|
|
helper_pid_ = -1;
|
|
}
|
|
|
|
} // namespace runtime
|
|
} // namespace mocktail
|