171 lines
6.9 KiB
C++
171 lines
6.9 KiB
C++
|
|
// 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;
|
||
|
|
}
|