// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #include #include #include #include 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(in)), std::istreambuf_iterator()); } // 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(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::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 Crafter::Debug::ListInstances() { std::vector 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; }