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>
108 lines
4.7 KiB
C++
108 lines
4.7 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// The session: the app's end of the debug connection.
|
|
//
|
|
// ThreadPool::Start();
|
|
// Debug::Session debug({ .name = "MyApp" });
|
|
// EventListener<void> pump(&window.onBeforeUpdate, [] { Debug::Pump(); });
|
|
//
|
|
// Requests arrive on transport threads and are never answered by blocking
|
|
// one: what needs only the session's own state (status, the command list,
|
|
// the log, pause, resume) is answered there and then; what touches the app
|
|
// (calls, watches, traces, screenshots) waits in a queue for the next Pump,
|
|
// which runs it on the app's own thread between frames; waits and steps are
|
|
// answered by the Pump that sees them satisfied. A request that cannot run
|
|
// before its timeout is answered Timeout and is guaranteed not to have run.
|
|
//
|
|
// Pause and step only stop what the app gates on them:
|
|
//
|
|
// while (acc >= dt && Debug::Advance()) { sim.Tick(); acc -= dt; } // frame loops
|
|
// if (Debug::Paused()) { for (auto n = Debug::TakeSteps(); n > 0; --n) sim.Tick(); acc = 0; }
|
|
// // fixed-step loops
|
|
//
|
|
// Rendering and Pump keep running while paused, so the agent can still read
|
|
// watches, take screenshots and step.
|
|
export module Crafter.Debug:Session;
|
|
import std;
|
|
import Crafter.Debug.Core;
|
|
|
|
export namespace Crafter::Debug::Detail {
|
|
// One request's way back to its client. A transport implements it; the
|
|
// session calls it from transport threads, the Pump thread and whatever
|
|
// thread logs, so it must be safe from any thread and must not block.
|
|
class Exchange {
|
|
public:
|
|
virtual ~Exchange() = default;
|
|
// The final reply, ending the exchange.
|
|
virtual void Reply(const Value& reply) = 0;
|
|
// One more object on a streaming exchange (`log --follow`). False
|
|
// when the client is gone or too far behind; the session then drops it.
|
|
virtual bool Stream(const Value& line) = 0;
|
|
// Reply, then call `afterSent` once the reply has left: what
|
|
// `quit --force` exits from.
|
|
virtual void ReplyThen(const Value& reply, std::function<void()> afterSent) {
|
|
Reply(reply);
|
|
afterSent();
|
|
}
|
|
};
|
|
}
|
|
|
|
export namespace Crafter::Debug {
|
|
struct SessionOptions {
|
|
// What `crafter-debug ls` calls this instance; the executable's
|
|
// file name when empty.
|
|
std::string name;
|
|
// 0 for any free port; the discovery file says which.
|
|
std::uint16_t port = 0;
|
|
// Also record what the app writes to stdout and stderr on the timeline.
|
|
bool captureStdio = false;
|
|
// Called on the Pump thread when an agent asks the app to quit,
|
|
// after QuitRequested() turned true. Don't call std::exit from it:
|
|
// close the window or end the loop, so the Session is destroyed
|
|
// before main returns.
|
|
std::function<void()> onQuit;
|
|
// False: no transport and no discovery file; requests come only
|
|
// through Handle. For tests.
|
|
bool listen = true;
|
|
};
|
|
|
|
// One per process; a second throws. Destroy it before
|
|
// ThreadPool::Stop() and before main returns: it closes its connections,
|
|
// and msquic waits for open ones at exit.
|
|
class Session {
|
|
public:
|
|
explicit Session(SessionOptions options = {});
|
|
~Session();
|
|
Session(const Session&) = delete;
|
|
Session& operator=(const Session&) = delete;
|
|
|
|
// One request, from a transport or a test. Never blocks; the reply
|
|
// goes through `exchange`.
|
|
void Handle(const Value& request, std::shared_ptr<Detail::Exchange> exchange);
|
|
// What every request must carry.
|
|
const std::string& Token() const;
|
|
|
|
struct State;
|
|
|
|
private:
|
|
std::shared_ptr<State> state;
|
|
};
|
|
|
|
// Once per frame, on the thread that owns the app's state: runs queued
|
|
// requests, polls Async results, evaluates waits and conditions, records
|
|
// traces, and counts the frame. Does nothing without a Session.
|
|
void Pump();
|
|
// For frame loops: false while paused, true once per step granted.
|
|
bool Advance();
|
|
// For fixed-step loops: whether the session is paused; see TakeSteps.
|
|
bool Paused();
|
|
// The steps granted since the last call, all at once.
|
|
std::uint32_t TakeSteps();
|
|
// Whether an agent asked the app to quit.
|
|
bool QuitRequested();
|
|
// Where `crafter-debug screenshot` gets a PNG: called on the Pump
|
|
// thread, returning the bytes when they are ready (a frame or two later
|
|
// for a GPU readback). See the README for the Crafter.Graphics version.
|
|
void SetCapture(std::function<Async<std::vector<std::byte>>()> capture);
|
|
}
|