The app's end of the connection, independent of the transport, which reaches it through Detail::Exchange: one request's way back, safe from any thread and never blocking. - What needs only the session's own state (hello, status, list, log, pause, resume) is answered on the transport thread. What touches the app (calls, get, set, trace, screenshot) queues for the next Pump and runs on the app's thread between frames. Waits and steps reply from the Pump that sees them satisfied, events from the thread that emits them. No request ever holds a thread while it waits. - A request whose timeout passes before Pump is answered Timeout and is guaranteed not to have run, so an agent never wonders whether a command went through; an Async result arrives on the Pump it is ready; requests left when the Session ends are answered ShuttingDown. - Advance() for frame loops and Paused()/TakeSteps() for fixed-step loops hold what the app gates on them; pausing an app that gates nothing is refused instead of silently doing nothing. pause --when stops the frame its condition holds; pause --on-check stops before the next Advance in the frame a check fails, optionally raising SIGTRAP for an attached debugger. - A wait that times out says what its condition's paths were; a log follower gets matching entries in order, from the backlog on, and is dropped when it cannot keep up. - Request parameters go through Codec, so a mistyped request gets the same did-you-mean replies a mistyped command does. The session sets PR_SET_PTRACER_ANY so a debugger the agent starts can attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.8 KiB
C++
125 lines
4.8 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// Three configurations:
|
|
// Crafter.Debug.Core what the app and the client share (values, codecs)
|
|
// 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
|
|
// debug build compiles it in the same pass as the app; an executable adds no
|
|
// link flags to what depends on it. Consumers load this file through
|
|
// Debug::Use (tooling/), which also writes their launcher.
|
|
import std;
|
|
import Crafter.Build;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter;
|
|
|
|
namespace {
|
|
// The configurations this file makes besides the one it returns, at
|
|
// stable addresses for the pointers in `dependencies`. One set per load,
|
|
// not statics: a static would keep the first load's args for every load.
|
|
std::deque<Configuration> owned;
|
|
|
|
// This file's directory. A configuration a project file makes for itself
|
|
// keeps the path it is given, and "./" would later resolve against the
|
|
// consumer's directory instead of this one.
|
|
fs::path Root() {
|
|
return fs::path(std::source_location::current().file_name()).parent_path();
|
|
}
|
|
|
|
// Tests of the core, linked against it alone.
|
|
constexpr std::array<std::string_view, 4> coreTests = {
|
|
"ShouldRoundTripJson",
|
|
"ShouldRoundTripAggregates",
|
|
"ShouldReportDecodeErrorsWithPaths",
|
|
"ShouldDescribeDefaultsAndRequired",
|
|
};
|
|
|
|
// Tests of the app library.
|
|
constexpr std::array<std::string_view, 7> libraryTests = {
|
|
"ShouldInvokeCommands",
|
|
"ShouldResolveWatchPaths",
|
|
"ShouldRecordTimeline",
|
|
"ShouldEvaluateConditions",
|
|
"ShouldAnswerRequests",
|
|
"ShouldPauseAndStep",
|
|
"ShouldWaitTraceAndFollow",
|
|
};
|
|
}
|
|
|
|
extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> args) {
|
|
Configuration cfg;
|
|
cfg.path = "./";
|
|
cfg.name = "Crafter.Debug";
|
|
cfg.outputName = "Crafter.Debug";
|
|
cfg.type = ConfigurationType::LibraryStatic;
|
|
ApplyStandardArgs(cfg, args);
|
|
|
|
Configuration& core = owned.emplace_back();
|
|
core.path = Root();
|
|
core.name = "Crafter.Debug.Core";
|
|
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.
|
|
Configuration& client = owned.emplace_back();
|
|
client.path = Root();
|
|
client.name = "crafter.debug-cli";
|
|
client.outputName = "crafter-debug";
|
|
client.type = ConfigurationType::Executable;
|
|
client.target = cfg.target;
|
|
client.march = cfg.march;
|
|
client.mtune = cfg.mtune;
|
|
client.sysroot = cfg.sysroot;
|
|
client.debug = cfg.debug;
|
|
client.dependencies = { &core };
|
|
{
|
|
std::array<fs::path, 0> interfaces = {};
|
|
std::array<fs::path, 1> implementations = { "implementations/main" };
|
|
client.GetInterfacesAndImplementations(interfaces, implementations);
|
|
}
|
|
|
|
cfg.dependencies = { &core, &client };
|
|
{
|
|
std::array<fs::path, 5> interfaces = {
|
|
"interfaces/Crafter.Debug-Registry",
|
|
"interfaces/Crafter.Debug-Timeline",
|
|
"interfaces/Crafter.Debug-Condition",
|
|
"interfaces/Crafter.Debug-Session",
|
|
"interfaces/Crafter.Debug",
|
|
};
|
|
std::array<fs::path, 4> implementations = {
|
|
"implementations/Crafter.Debug-Registry",
|
|
"implementations/Crafter.Debug-Timeline",
|
|
"implementations/Crafter.Debug-Condition",
|
|
"implementations/Crafter.Debug-Session",
|
|
};
|
|
cfg.GetInterfacesAndImplementations(interfaces, implementations);
|
|
}
|
|
|
|
// Tests only when this is the project being built: a consumer's load of
|
|
// this file has no use for them.
|
|
if (IsTopLevelProject()) {
|
|
for (const std::string_view test : coreTests) cfg.AddTest(test).Dependencies({ &core });
|
|
for (const std::string_view test : libraryTests) cfg.AddTest(test).Dependencies({ &cfg });
|
|
}
|
|
return cfg;
|
|
}
|