Crafter.Review/project.cpp
Jorijn van der Graaf f94d895701
feat(platform): processes that never outlive their pass, shared files, the board identity
:Process makes sure no child outlives what it was started for. SIGKILL to
the direct child alone would leave the Playwright server and Chromium
running after a timeout, and every pass running after a SIGKILLed driver:

- every child is forked by one long-lived spawner thread, the reaper's
  registry locked across fork, so PR_SET_PDEATHSIG holds and a fast exit
  is never reaped unregistered; one reaper thread owns waitpid(-1);
- the child gets an empty signal mask, SIGPIPE and SIGXFSZ at their
  defaults, and nothing but its three streams (close_range); the driver
  ignores SIGPIPE and takes SIGINT/SIGTERM/SIGHUP on a thread of its own;
- `crafter-review __supervise <argv>` is a pass's subreaper. It kills the
  pass's whole tree, Chromium's detached process group included, when the
  target exits, on SIGTERM, and when the driver dies, by its death signal.
- Run() captures both outputs without the deadlock.

The shared state the detached threads wait on is never destroyed: a
condition variable destroyed under a waiter makes glibc's
pthread_cond_destroy, and with it exit(), wait forever.

:Platform: the Flatpak sandbox decided once, the host's XDG directories
restored there; WriteAtomic and LockedUpdate, the read-modify-write of
the board, ledger, memory.json and unit-state.json every driver on the
machine shares, under flock(2) on the same side files; /proc readers; and
the identity other drivers can check: natively pid and start time, in the
sandbox the instance's host pid from bwrapinfo.json with /proc/1's start
time. The lifetime lock is created under a temporary name and renamed into
place, so no prober ever finds it unlocked while its driver lives.

Tests: Process (the three ways a pass's tree must die, return codes, a
missing program, capture, timeouts, fds, the child's signal state),
Platform (atomic writes, LockedUpdate against a damaged file, two writers,
and another process holding flock on the side file; /proc on a fake
tree; both identities; the lifetime lock; the XDG restore).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:49:46 +01:00

109 lines
4.8 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
import std;
import Crafter.Build;
// Pinned: the Flatpak builds with the base app's crafter-build, which can be
// older than the rules' master. Move the pin together with the base.
#pragma crafter dependency "https://forgejo.catcrafts.net/Catcrafts/Crafter.LintRules.git" commit "7d932892017800b4e1f1a15fe7aa49b3daee9ceb"
import Crafter.LintRules;
namespace fs = std::filesystem;
using namespace Crafter;
namespace {
// The umbrella, then the partitions in import order: each after the ones
// it imports, which is also the order the module map in AGENTS.md uses.
constexpr std::array<std::string_view, 9> Interfaces = {
"interfaces/Crafter.Review",
"interfaces/Crafter.Review-Unicode",
"interfaces/Crafter.Review-Json",
"interfaces/Crafter.Review-Text",
"interfaces/Crafter.Review-Crypto",
"interfaces/Crafter.Review-Platform",
"interfaces/Crafter.Review-Process",
"interfaces/Crafter.Review-Core",
"interfaces/Crafter.Review-Driver",
};
constexpr std::array<std::string_view, 6> Implementations = {
"implementations/Crafter.Review-Text",
"implementations/Crafter.Review-Crypto",
"implementations/Crafter.Review-Platform",
"implementations/Crafter.Review-Process",
"implementations/Crafter.Review-Core",
"implementations/Crafter.Review-Driver",
};
// One per directory under tests/, each an ordinary main() that links the
// library: exit 0 passes, nonzero fails, 77 skips.
constexpr std::array<std::string_view, 4> Tests = {
"Crypto",
"Text",
"Platform",
"Process",
};
// The version the binary reports: CI sets it from the tag, a checkout
// describes itself, anything else is a development build. A define on
// the library, so each version is its own library variant.
std::string Version() {
if (const char* env = std::getenv("CRAFTER_REVIEW_VERSION"); env && *env) return env;
auto described = RunCommandWithTimeout("git describe --tags --always --dirty --exclude=latest 2>/dev/null", std::chrono::seconds(10));
std::string version = described.output;
while (!version.empty() && (version.back() == '\n' || version.back() == '\r')) version.pop_back();
if (described.exitCode != 0 || version.empty()) return "0.0.0-dev";
return version;
}
}
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
// Crafter.Network for its TCP and HTTP/1.1 clients and listener, without
// QUIC: msquic is most of a fresh build and nothing here speaks HTTP/3.
// From forgejo at a pinned commit, or with --local from the checkout next
// to this one (which is also how Crafter.Network then finds
// Crafter.Thread).
std::vector<std::string> depArgs(args.begin(), args.end());
depArgs.emplace_back("--no-quic");
const bool useLocal = std::ranges::find(args, std::string_view("--local")) != args.end();
Configuration* network = useLocal
? LocalProject({ .projectFile = "../Crafter.Network/project.cpp", .args = depArgs })
: GitProject({
.source = { .url = "https://forgejo.catcrafts.net/Catcrafts/Crafter.Network.git", .commit = "f68321685006be65f6bc0436fd0175611f1e0a55" },
.args = depArgs,
});
static auto ReviewLib = std::make_unique<Configuration>();
ReviewLib->path = "./";
ReviewLib->name = "crafter.review-lib";
ReviewLib->outputName = "crafter-review";
ApplyStandardArgs(*ReviewLib, args);
ReviewLib->type = ConfigurationType::LibraryStatic;
ReviewLib->dependencies = { network };
// Backslash-escaped quotes: the define travels through the shell before
// it reaches clang, which must still see a string literal.
ReviewLib->defines.push_back({"CRAFTER_REVIEW_VERSION", std::format("\\\"{}\\\"", Version())});
{
std::vector<fs::path> interfaces(Interfaces.begin(), Interfaces.end());
std::vector<fs::path> implementations(Implementations.begin(), Implementations.end());
ReviewLib->GetInterfacesAndImplementations(interfaces, implementations);
}
Configuration cfg;
cfg.path = "./";
cfg.name = "crafter.review-exe";
cfg.outputName = "crafter-review";
ApplyStandardArgs(cfg, args);
cfg.type = ConfigurationType::Executable;
cfg.dependencies = { ReviewLib.get() };
{
std::array<fs::path, 0> interfaces = {};
std::array<fs::path, 1> implementations = { "implementations/main" };
cfg.GetInterfacesAndImplementations(interfaces, implementations);
}
for (const std::string_view test : Tests) {
cfg.AddTest(test).Dependencies({ ReviewLib.get() });
}
LintRules::AddLiteRules(cfg);
return cfg;
}