Crafter.Debug/tests/ShouldDiscoverLiveInstances/main.cpp

87 lines
3.8 KiB
C++
Raw Normal View History

// 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 <signal.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
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<Registration> one;
one.emplace(InstanceRecord{ .name = "one", .port = 4100, .token = "secret", .protocol = protocolVersion });
const std::vector<Instance> 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<std::int32_t, 2> 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<Instance> 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<Instance> 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;
}