feat(discovery): find live instances across sandboxes; build info

Each app writes $HOME/.cache/crafter-debug/instances/<id>.json (0600,
in a 0700 directory) and holds an flock on <id>.lock while it lives.
Every `flatpak run` is its own pid namespace, so pids collide and can
neither name an instance nor tell whether it lives; a random id names
it and the lock says it lives, since flock works across sandboxes on
the shared filesystem. An app killed without its destructor leaves its
files with the lock free, and the next listing removes them. The lock
is created under a temporary name, locked, then renamed, as in
Crafter.Review's LifetimeLock, so it is never seen unlocked while its
app lives. Inside a Flatpak the record also carries the sandbox's
instance id (for `flatpak enter`) and the host pid of its init (to end
the whole sandbox from the host).

project.cpp now generates :BuildInfo per core variant: a hash of the
library's sources and the path of the client built with it. App and
client share the core, so they report the same id exactly when built
from the same sources; this is the safety net under building the
client in the same pass as the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-11-01 01:56:31 +01:00
commit 37e0d9b9ae
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
6 changed files with 433 additions and 18 deletions

View file

@ -2,7 +2,8 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Three configurations:
// Crafter.Debug.Core what the app and the client share (values, codecs)
// Crafter.Debug.Core what the app and the client share (values, codecs,
// the protocol, discovery, build info)
// crafter.debug-cli the client, crafter-debug
// Crafter.Debug what an app links (returned), depending on both
// The client is one of the library's dependencies only so that a consumer's
@ -28,11 +29,12 @@ namespace {
}
// Tests of the core, linked against it alone.
constexpr std::array<std::string_view, 4> coreTests = {
constexpr std::array<std::string_view, 5> coreTests = {
"ShouldRoundTripJson",
"ShouldRoundTripAggregates",
"ShouldReportDecodeErrorsWithPaths",
"ShouldDescribeDefaultsAndRequired",
"ShouldDiscoverLiveInstances",
};
// Tests of the app library.
@ -45,6 +47,65 @@ namespace {
"ShouldPauseAndStep",
"ShouldWaitTraceAndFollow",
};
std::string ReadFile(const fs::path& path) {
std::ifstream in(path, std::ios::binary);
return std::string((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
}
// FNV-1a over the path and bytes of every source, in a fixed order: what
// the app and the client compare to tell they came from the same code.
std::string SourceId() {
std::vector<fs::path> files;
for (const std::string_view dir : { "interfaces", "implementations" }) {
std::error_code ec;
for (const fs::directory_entry& entry : fs::recursive_directory_iterator(Root() / dir, ec)) {
if (entry.is_regular_file() && (entry.path().extension() == ".cppm" || entry.path().extension() == ".cpp")) files.push_back(entry.path());
}
}
std::ranges::sort(files);
std::uint64_t hash = 14695981039346656037ULL;
const auto mix = [&](std::string_view bytes) {
for (const char c : bytes) {
hash ^= static_cast<std::uint8_t>(c);
hash *= 1099511628211ULL;
}
};
for (const fs::path& file : files) {
mix(file.lexically_relative(Root()).generic_string());
mix(ReadFile(file));
}
return std::format("{:016x}", hash);
}
void WriteIfChanged(const fs::path& file, const std::string& text) {
if (fs::exists(file) && ReadFile(file) == text) return;
fs::create_directories(file.parent_path());
std::ofstream(file, std::ios::binary | std::ios::trunc) << text;
}
// Generates the definitions of :BuildInfo for this variant of the core and
// returns the source to compile, without its extension.
fs::path GenerateBuildInfo(const Configuration& core, const Configuration& client) {
const fs::path source = Root() / "build" / "generated" / core.VariantId() / "Crafter.Debug.Core-BuildInfo";
const fs::path clientPath = client.BinDir() / client.outputName;
WriteIfChanged(fs::path(source).concat(".cpp"), std::format(
"// Generated by Crafter.Debug's project.cpp whenever the sources or the client's\n"
"// path change. Not to be edited.\n"
"module Crafter.Debug.Core:BuildInfo_impl;\n"
"import std;\n"
"import :BuildInfo;\n"
"\n"
"std::string_view Crafter::Debug::Detail::SourceId() {{\n"
" return \"{}\";\n"
"}}\n"
"\n"
"std::string_view Crafter::Debug::Detail::ClientPath() {{\n"
" return R\"crafter-debug({})crafter-debug\";\n"
"}}\n",
SourceId(), clientPath.string()));
return source;
}
}
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
@ -61,22 +122,6 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
core.outputName = "Crafter.Debug.Core";
core.type = ConfigurationType::LibraryStatic;
ApplyStandardArgs(core, args);
{
std::array<fs::path, 6> interfaces = {
"interfaces/Crafter.Debug.Core-Value",
"interfaces/Crafter.Debug.Core-Reflect",
"interfaces/Crafter.Debug.Core-Codec",
"interfaces/Crafter.Debug.Core-Result",
"interfaces/Crafter.Debug.Core-Protocol",
"interfaces/Crafter.Debug.Core",
};
std::array<fs::path, 3> implementations = {
"implementations/Crafter.Debug.Core-Value",
"implementations/Crafter.Debug.Core-Codec",
"implementations/Crafter.Debug.Core-Protocol",
};
core.GetInterfacesAndImplementations(interfaces, implementations);
}
// The client, built for the library's target. Not through
// ApplyStandardArgs: a consumer's --lib would make it a library.
@ -91,6 +136,27 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> a
client.sysroot = cfg.sysroot;
client.debug = cfg.debug;
client.dependencies = { &core };
{
std::array<fs::path, 8> interfaces = {
"interfaces/Crafter.Debug.Core-Value",
"interfaces/Crafter.Debug.Core-Reflect",
"interfaces/Crafter.Debug.Core-Codec",
"interfaces/Crafter.Debug.Core-Result",
"interfaces/Crafter.Debug.Core-Protocol",
"interfaces/Crafter.Debug.Core-BuildInfo",
"interfaces/Crafter.Debug.Core-Discovery",
"interfaces/Crafter.Debug.Core",
};
std::array<fs::path, 5> implementations = {
"implementations/Crafter.Debug.Core-Value",
"implementations/Crafter.Debug.Core-Codec",
"implementations/Crafter.Debug.Core-Protocol",
"implementations/Crafter.Debug.Core-Discovery",
GenerateBuildInfo(core, client),
};
core.GetInterfacesAndImplementations(interfaces, implementations);
}
{
std::array<fs::path, 0> interfaces = {};
std::array<fs::path, 1> implementations = { "implementations/main" };