: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>
109 lines
4.8 KiB
C++
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;
|
|
}
|