108 lines
4.7 KiB
Text
108 lines
4.7 KiB
Text
|
|
// 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);
|
||
|
|
}
|