diff --git a/AGENTS.template.md b/AGENTS.template.md index 97c0a8b..cc076dc 100644 --- a/AGENTS.template.md +++ b/AGENTS.template.md @@ -9,12 +9,14 @@ Crafter.Debug lets an agent inspect and drive an app while it runs. `project.cpp | Configuration | Holds | |---|---| | `Crafter.Debug.Core` (static) | What the app and the client share. Partitions:
• `:Value`: JSON value with its own parser and printer
• `:Reflect`: aggregate members and scoped-enum names, without ``
• `:Codec`: encode, decode, describe
• `:Result`: Error, Result, Async
• `:Protocol`: envelope, codes, versions, base64
• `:BuildInfo`: source id and client path, generated
• `:Discovery`: instance files and the lifetime lock | -| `Crafter.Debug.Cli` (static) | The client's logic: `Cli::Run`, with the transport injected. | +| `Crafter.Debug.Cli` (static) | The client's logic:
• `:Client`: `Cli::Run`, with the transport injected
• `:Quic`: `ConnectQuic`, the transport `main` injects | | `crafter.debug-cli` (executable) | `crafter-debug`: `main.cpp` plus the transport. | -| `Crafter.Debug` (static, returned) | What an app links. Partitions:
• `:Registry`: commands, watches, samples, Group
• `:Timeline`: Log, Check, Emit
• `:Condition`: what wait and pause --when evaluate
• `:Session`: serving requests, Pump, Advance | +| `Crafter.Debug` (static, returned) | What an app links. Partitions:
• `:Registry`: commands, watches, samples, Group
• `:Timeline`: Log, Check, Emit
• `:Condition`: what wait and pause --when evaluate
• `:Session`: serving requests, Pump, Advance
• `:Transport`: the QUIC listener, exporting nothing | The client is one of the library's dependencies only so that a consumer's debug build compiles it in the same pass. +**The wire:** QUIC on 127.0.0.1, ALPN `crafter-debug/1`, through Crafter.Network. One request per stream the client opens: the request object and a FIN; back come zero or more stream lines (`{"entry": ...}`), then the reply (`{"result"}` or `{"error"}`) and a FIN. Every object is one line of JSON. `implementations/Crafter.Debug-Transport.cpp` opens with the rules on who may hold which lock. + `tooling/` is a separate project, `Crafter.Debug.Build`. Consumers load it with `#pragma crafter dependency ... project "tooling/project.cpp"`. Its `Debug::Use` adds the library under the consumer's flag and writes the consumer's `build/crafter-debug` launcher at load time. `tooling/AGENTS.consumer.md` is the only notes file consumers get, and it covers both driving the app and coding with it. Crafter.Build merges a dependency's notes only when that dependency is in the build: @@ -34,12 +36,17 @@ Tests run inside the Flatpak sandbox, where: Every `flatpak run` is its own sandbox with its own pid namespace. +**Crafter.Network** comes from forgejo, unpinned, so every load `git pull`s it. `--local` takes `../Crafter.Network`, and `CRAFTER_DEBUG_NETWORK=` takes any checkout, a worktree say. The environment reaches the build inside the Flatpak. + +The transport needs Network's callback API (`OnRecieve`, `SendAsync`, `OnClosed`, `ListenerQUIC` with a bind address and `Port()`) and its fix that lets stopped streams finish before `~ClientQUIC` closes. Until those reach Network's `master`, build with `CRAFTER_DEBUG_NETWORK` pointing at a checkout of the branch that has them. + ## Build and test commands ```bash flatpak run --die-with-parent net.catcrafts.CrafterBuild # the library, core, client flatpak run --die-with-parent net.catcrafts.CrafterBuild test # every test flatpak run --die-with-parent net.catcrafts.CrafterBuild test 'Should*Client*' +CRAFTER_DEBUG_NETWORK=../Crafter.Network-quic-callbacks/project.cpp flatpak run --die-with-parent net.catcrafts.CrafterBuild test # Network from a worktree cd examples/Basic && flatpak run --die-with-parent net.catcrafts.CrafterBuild --debug-api # a consumer's debug build ``` @@ -50,7 +57,7 @@ A test's own output appears only when it fails. To see its PASS lines, run the b **Registration:** tests are declared in `project.cpp`, and only when this is the top-level project: - `coreTests` link the core; - `libraryTests` link the library; -- `ShouldRunClientCommands` links the library and the client. +- `ShouldRunClientCommands` and `ShouldServeOverQuic` link the library and the client. The second listens for real and drives the session over loopback with the client's own transport. **Shape of a test:** - Name it `Should`. @@ -59,6 +66,8 @@ A test's own output appears only when it fails. To see its PASS lines, run the b **Session tests** use `Session({ .listen = false })` and hand requests to `Session::Handle` through a fake `Detail::Exchange`. Pump from the test's own thread, or from one thread playing the app. +**A listening session** needs `ThreadPool::Start()` with a worker first, `CRAFTER_DEBUG_DIR` set, and `std::_Exit` at the end after `std::fflush(stdout)`: msquic's teardown at exit waits for connections. Run it 30 times in a row before trusting it; one transport race showed in 4 runs of 30. + **Files and discovery:** - Temporary files go under `fs::temp_directory_path() / "crafter-debug-"`. - Discovery tests set `CRAFTER_DEBUG_DIR` there. @@ -87,7 +96,7 @@ A test's own output appears only when it fails. To see its PASS lines, run the b ## Design rules 1. **Release builds never link Crafter.Debug.** Consumers gate it through `Debug::Use` and their own define. The library has no no-op mode. -2. **No thread of Crafter.Debug's own, and nothing ever blocks one.** Transport threads only queue work or answer from the session's own state; the Pump thread runs app code; `Detail::Exchange::Reply` and `Stream` never block. The thread pool belongs to the host app. +2. **No thread of Crafter.Debug's own, and nothing ever blocks one.** Transport threads only queue work or answer from the session's own state; the Pump thread runs app code; `Detail::Exchange::Reply` and `Stream` never block. The thread pool belongs to the host app. Network's own threads are ListenerQUIC's accept loop and msquic's workers. The one thread Crafter.Debug starts is the timer that ends the process 200 ms after `quit --force` has sent its reply. 3. **App code runs only on the Pump thread:** handlers, getters, setters, and condition evaluation. Never hold the session mutex while calling it, since app code logs, and logging re-enters the session. 4. **Nothing the Flatpak compiles runs on the host.** The launcher is POSIX sh and starts the client with `flatpak run`. On older hosts, the bundled libc++ and the runtime's glibc do not load. 5. **The client is built with the app.** `debug.hello`, the ALPN `crafter-debug/1` and the discovery record's `protocol` and `sourceId` are frozen. Bump `protocolVersion` on any wire change. @@ -105,6 +114,8 @@ A test's own output appears only when it fails. To see its PASS lines, run the b ## Security - **Debug builds only.** The session listens on 127.0.0.1, and every request must carry the instance's 128-bit token. +- **TLS** uses Crafter.Network's per-user self-signed certificate, which the client does not check. The token is what proves who asks. +- **Size limits:** a request is at most 1 MiB, answered with an error past that. A `log --follow` client more than 8 MiB behind is reset. - **Discovery files** are 0600 in a 0700 directory. - **Debuggers:** the session calls `prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY)`, so a debugger the agent starts can attach. That is another reason it never ships in a release build. @@ -117,3 +128,6 @@ A test's own output appears only when it fails. To see its PASS lines, run the b - **A consumer's `#ifdef`'d `import Crafter.Debug;`** is harmless in its plain builds. - **`project.cpp` regenerates `build/generated//Crafter.Debug.Core-BuildInfo.cpp`** when the sources or the client's path change. `crafter-build clean` removes it, and the next load writes it again. - **The launcher is written by `Debug::Use` while crafter-build loads the consumer,** not by the build. A plain build keeps an existing launcher; a fresh plain build writes the stub. +- **A consumer that depends on Crafter.Network itself** must name it exactly as `project.cpp` here does (same URL, no pin, same args). Otherwise the graph holds two Crafter.Network configurations, and crafter-build refuses with "module … built twice". +- **`debug.log`'s `last` of 0 means no backlog**, as `tail -n 0` does. A `TimelineQuery`'s `last` of 0 means all. +- **Answer before you raise a flag the main loop acts on.** The loop may end the Session at once. A reply given before the connections close is delivered; one given after is not. `quit` replies first, then sets `QuitRequested`. diff --git a/README.md b/README.md index ade6e0d..604cc32 100644 --- a/README.md +++ b/README.md @@ -68,11 +68,13 @@ extern "C" Configuration CrafterBuildProject(std::span a A dependency that two paths in the build graph share must get the **same arg list on both**. Otherwise crafter-build refuses the graph with "module … built twice". Build one `depArgs` and give it to every dependency, as above. +That includes Crafter.Network, which Crafter.Debug takes from `https://forgejo.catcrafts.net/Catcrafts/Crafter.Network.git`, unpinned (with `--local`, from `../Crafter.Network`). If your project depends on Crafter.Network too, name it the same way. A project that pins its own Crafter.Network to a commit can't add Crafter.Debug yet. + **2. In `main`**, start a session and pump it once per frame. Gate the parts the agent should be able to pause: ```cpp import MyApp.DebugShim; // see "Without #ifdefs" below -ThreadPool::Start(); // the session's transport runs on the pool +ThreadPool::Start(); // the session's transport needs a pool worker #ifdef MYAPP_DEBUG_API Debug::Session debug({ .name = "MyApp" }); MyApp::RegisterDebug(); // your commands and watches @@ -84,7 +86,7 @@ while (running && !Debug::QuitRequested()) { } ``` -The `Session` must be destroyed before `main` returns and before `ThreadPool::Stop()`, because msquic waits for open connections at exit. A stack object in `main`, as above, does that. +The `Session` must be destroyed before `main` returns and before `ThreadPool::Stop()`, because msquic waits for open connections at exit. A stack object in `main`, as above, does that. It needs the pool started with at least one worker (`Start(1)` starts none) and throws without one. **3. Register what the agent needs** in debug-only source files, which `project.cpp` adds only under the flag. See "The app side" below. @@ -319,6 +321,8 @@ cd examples/Basic && crafter-build --debug-api -r - **Library and core tests** run in-process, against a `Session` built with `listen = false` and a fake transport. - **`ShouldRunClientCommands`** runs the real client against a real session that way. +- **`ShouldServeOverQuic`** runs a listening session and drives it over loopback with the client's own QUIC transport. It covers discovery, calls, `log --follow`, wrong tokens, oversized requests, parallel requests, and shutdown with a client connected. +- **Crafter.Network** comes from forgejo, or from `../Crafter.Network` with `--local`. To build against another checkout, a worktree say, set `CRAFTER_DEBUG_NETWORK` to its `project.cpp`. - **`AGENTS.template.md`** has the conventions. Licensed LGPL-3.0-only; the examples are MIT.