// SPDX-License-Identifier: LGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Discovery across processes: an app's Registration writes its record // (readable by its user only) and holds the lock while it lives, and // ListInstances lists it. An app killed with SIGKILL, which never runs its // destructor, leaves its files behind with the lock free, and the next // listing removes them instead of offering a dead instance; one that ends // normally removes its own. Pids cannot tell this apart: every `flatpak run` // sandbox has its own pid namespace. Also checks the generated build info. #include #include #include #include #include import std; import Crafter.Debug.Core; using namespace Crafter::Debug; namespace fs = std::filesystem; namespace { std::int32_t failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } std::uint32_t Mode(const fs::path& path) { struct stat info {}; ::stat(path.c_str(), &info); return info.st_mode & 0777; } } int main() { const fs::path dir = fs::temp_directory_path() / "crafter-debug-ShouldDiscoverLiveInstances"; fs::remove_all(dir); ::setenv("CRAFTER_DEBUG_DIR", dir.c_str(), 1); std::optional one; one.emplace(InstanceRecord{ .name = "one", .port = 4100, .token = "secret", .protocol = protocolVersion }); const std::vector first = ListInstances(); Check(first.size() == 1 && first[0].record.name == "one" && first[0].record.port == 4100 && first[0].record.token == "secret", "a registered instance is listed"); Check(first.size() == 1 && first[0].record.pid == ::getpid() && !first[0].record.instanceId.empty() && first[0].record.exe.ends_with("ShouldDiscoverLiveInstances"), "the record carries the pid, an instance id and the executable"); Check(Mode(dir) == 0700 && first.size() == 1 && Mode(first[0].recordPath) == 0600, "the directory and record are its user's only"); if (fs::exists("/.flatpak-info")) { Check(!one->Record().flatpakInstance.empty() && one->Record().hostInitPid > 0, "inside a Flatpak, the sandbox's instance id and host init pid are recorded"); } std::array ready{}; ::pipe(ready.data()); const ::pid_t child = ::fork(); if (child == 0) { ::close(ready[0]); Registration two(InstanceRecord{ .name = "two" }); const char signal = 'r'; (void)::write(ready[1], &signal, 1); while (true) ::pause(); } ::close(ready[1]); char signal = 0; (void)::read(ready[0], &signal, 1); const std::vector both = ListInstances(); Check(both.size() == 2, std::format("two live instances are listed ({})", both.size())); fs::path twoRecord; for (const Instance& instance : both) { if (instance.record.name == "two") twoRecord = instance.recordPath; } ::kill(child, SIGKILL); ::waitpid(child, nullptr, 0); Check(fs::exists(twoRecord), "a killed instance leaves its record behind"); const std::vector afterKill = ListInstances(); Check(afterKill.size() == 1 && afterKill[0].record.name == "one", "the next listing leaves out the killed instance"); Check(!fs::exists(twoRecord) && !fs::exists(fs::path(twoRecord).replace_extension(".lock")), "and removes its files"); one.reset(); Check(ListInstances().empty() && fs::is_empty(dir), "an instance that ends removes its own files"); Check(Detail::SourceId().size() == 16, std::format("the build has a source id ({})", Detail::SourceId())); Check(Detail::ClientPath().ends_with("/crafter-debug"), std::format("the build knows its client ({})", Detail::ClientPath())); fs::remove_all(dir); return failures == 0 ? 0 : 1; }