Crafter.Debug/implementations/Crafter.Debug.Core-Discovery.cpp

171 lines
6.9 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: LGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
module;
#include <errno.h>
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
module Crafter.Debug.Core:Discovery_impl;
import std;
import :Value;
import :Codec;
import :Discovery;
using namespace Crafter::Debug;
namespace fs = std::filesystem;
namespace {
std::string RandomId() {
std::random_device random;
return std::format("{:08x}{:08x}", random(), random());
}
std::string ReadFile(const fs::path& path) {
std::ifstream in(path, std::ios::binary);
if (!in) return {};
return std::string((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
}
// The value of `key` in `section` of an INI-style file such as /.flatpak-info.
std::string IniValue(std::string_view text, std::string_view section, std::string_view key) {
bool inSection = false;
for (const auto part : std::views::split(text, '\n')) {
const std::string_view line(std::ranges::data(part), std::ranges::size(part));
if (line.starts_with('[')) {
inSection = line.size() == section.size() + 2 && line.substr(1, section.size()) == section && line.back() == ']';
continue;
}
if (inSection && line.size() > key.size() && line.starts_with(key) && line[key.size()] == '=') return std::string(line.substr(key.size() + 1));
}
return {};
}
// The host pid of the sandbox's init: child-pid in the bwrapinfo.json
// Flatpak keeps for the instance under $XDG_RUNTIME_DIR, or 0.
std::int64_t SandboxInitPid(std::string_view instance) {
if (instance.empty()) return 0;
const char* runtime = std::getenv("XDG_RUNTIME_DIR");
const fs::path base = runtime != nullptr && *runtime != 0 ? fs::path(runtime) : fs::path(std::format("/run/user/{}", ::getuid()));
const std::string text = ReadFile(base / ".flatpak" / std::string(instance) / "bwrapinfo.json");
if (text.empty()) return 0;
try {
const Value info = Value::Parse(text);
const Value* pid = info.Find("child-pid");
return pid != nullptr && pid->IsInteger() ? pid->AsInteger() : 0;
} catch (const ParseError&) {
return 0;
}
}
std::system_error Failure(std::string_view what) {
return std::system_error(errno, std::generic_category(), std::string(what));
}
// Through a temporary name and a rename, so a reader never sees half
// a file. Created with `mode`, before the umask.
void WriteAtomically(const fs::path& path, std::string_view text, std::uint32_t mode) {
fs::path temp = path;
temp += std::format(".{}.tmp", RandomId());
const std::int32_t fd = ::open(temp.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, mode);
if (fd < 0) throw Failure(std::format("creating {}", temp.string()));
std::size_t written = 0;
while (written < text.size()) {
const auto count = ::write(fd, text.data() + written, text.size() - written);
if (count < 0 && errno == EINTR) continue;
if (count <= 0) {
const std::system_error error = Failure(std::format("writing {}", temp.string()));
::close(fd);
::unlink(temp.c_str());
throw error;
}
written += static_cast<std::size_t>(count);
}
::close(fd);
fs::rename(temp, path);
}
}
fs::path Crafter::Debug::DiscoveryDirectory() {
if (const char* dir = std::getenv("CRAFTER_DEBUG_DIR"); dir != nullptr && *dir != 0) return fs::path(dir);
const char* home = std::getenv("HOME");
return fs::path(home != nullptr && *home != 0 ? home : "/tmp") / ".cache" / "crafter-debug" / "instances";
}
Registration::Registration(InstanceRecord given) : record(std::move(given)) {
const fs::path dir = DiscoveryDirectory();
fs::create_directories(dir);
std::error_code ignored;
fs::permissions(dir, fs::perms::owner_all, ignored);
record.instanceId = RandomId();
record.pid = ::getpid();
record.exe = fs::read_symlink("/proc/self/exe", ignored).string();
record.startedAt = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
const std::string info = ReadFile("/.flatpak-info");
if (!info.empty()) {
record.flatpakInstance = IniValue(info, "Instance", "instance-id");
record.hostInitPid = SandboxInitPid(record.flatpakInstance);
}
// The lock first, made under a temporary name, locked, then renamed into
// place, so nothing ever finds it unlocked while this instance lives;
// then the record, which a lister only reads once the lock is there.
lockPath = dir / std::format("{}.lock", record.instanceId);
recordPath = dir / std::format("{}.json", record.instanceId);
fs::path tempLock = lockPath;
tempLock += ".tmp";
lockFd = ::open(tempLock.c_str(), O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0600);
if (lockFd < 0) throw Failure(std::format("creating {}", tempLock.string()));
if (::flock(lockFd, LOCK_EX | LOCK_NB) != 0) {
const std::system_error error = Failure(std::format("locking {}", tempLock.string()));
::close(lockFd);
::unlink(tempLock.c_str());
throw error;
}
fs::rename(tempLock, lockPath);
WriteAtomically(recordPath, Encode(record).Dump(), 0600);
}
Registration::~Registration() {
std::error_code ignored;
fs::remove(recordPath, ignored);
fs::remove(lockPath, ignored);
if (lockFd >= 0) ::close(lockFd);
}
std::vector<Instance> Crafter::Debug::ListInstances() {
std::vector<Instance> live;
std::error_code ec;
for (const fs::directory_entry& entry : fs::directory_iterator(DiscoveryDirectory(), ec)) {
const fs::path path = entry.path();
if (path.extension() != ".json") continue;
fs::path lock = path;
lock.replace_extension(".lock");
bool alive = false;
const std::int32_t fd = ::open(lock.c_str(), O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
alive = ::flock(fd, LOCK_EX | LOCK_NB) != 0 && errno == EWOULDBLOCK;
::close(fd);
}
if (!alive) {
std::error_code ignored;
fs::remove(path, ignored);
fs::remove(lock, ignored);
continue;
}
Value parsed;
try {
parsed = Value::Parse(ReadFile(path));
} catch (const ParseError&) {
continue;
}
// A newer app may add fields; what this client knows still decodes.
InstanceRecord record;
DecodeContext context;
Decode(parsed, record, context);
live.push_back({ std::move(record), path });
}
std::ranges::sort(live, [](const Instance& a, const Instance& b) { return a.record.startedAt > b.record.startedAt; });
return live;
}