Inspector transport internals
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Checked by reading modules/inspector, the diagnostics provider and the CMake registration at the snapshot; the 27 tests were located, not executed. A live browser session, any renderer-side preview provider (none exists at the snapshot), macOS runtime behaviour and remote (non-loopback) deployment are not established by this page.
modules/inspector/ is not part of the Game loop or of the CNA umbrella. It is three things a game opts into explicitly: an application-side agent (CNA::Inspector::Agent) that answers an authenticated, versioned binary protocol on one background thread; a synchronous protocol client; and a separate cna-inspector process that turns that protocol into a loopback-only HTTP API and an embedded browser UI. This page is for maintainers who change the agent, the wire format, the bridge or the preview seam, or who have to explain why the Inspector shows nothing. The user guide is Inspector; the data it transports comes from Diagnostics and profiler internals.
Opt-in composition and host scope
modules/inspector/CMakeLists.txt is always entered by modules/CMakeLists.txt (right after diagnostics), but it defines nothing unless CNA_BUILD_INSPECTOR is ON; the option is declared in the root CMakeLists.txt with default OFF, and no CMake preset or CI workflow at this snapshot turns it on. With the option ON it creates:
| Target | What it is | Condition |
|---|---|---|
cna_inspector / CNA::Inspector | Static library made by cna_add_module from every src/*.cpp except InspectorMain.cpp; links cna_diagnostics and cna_core_headers publicly, Threads::Threads privately, plus ws2_32 and bcrypt on Windows | CNA_BUILD_INSPECTOR=ON |
cna-inspector | The bridge executable (InspectorMain.cpp); no output-directory override, so it lands in the module's build directory | CNA_BUILD_INSPECTOR=ON |
cna_inspector_demo | inspector_demo.cpp, linked with CNA CNA::Inspector SHARP_RUNTIME; output directory forced to the build root | plus CNA_BUILD_EXAMPLES (default ON) |
cna_inspector_benchmark | InspectorBenchmark.cpp | plus CNA_BUILD_BENCHMARKS (default OFF) |
cna_inspector_compiled_out_benchmark | A control loop with no Inspector include or link (InspectorCompiledOutBenchmark.cpp) | CNA_BUILD_BENCHMARKS alone, with or without the Inspector |
The option is a configure-time FATAL_ERROR on Emscripten, Android and iOS; desktop Windows, Linux, macOS and other POSIX desktops are the declared scope. cna_inspector is absent from the list of runtime parts that make up the CNA interface target in modules/CMakeLists.txt, so a game gets Inspector code only by naming CNA::Inspector on its own link line, and the agent exists only after that game calls Agent::Start. There is no static initializer, environment hook or Game integration. The session identity it reports uses the project-wide CNA_PLATFORM_<NAME> definition from PlatformSelection.cmake and NDEBUG (Release/Debug) unless the configuration overrides them.
game process (explicitly links CNA::Inspector)
Diagnostics process provider (v1) <- pulled only on request
Agent::Start(configuration, error) -> one worker thread, one TCP listener
^ binary protocol v1.0, 24-byte header, token in ClientHello
| plaintext TCP, loopback unless allowRemote=true
cna-inspector process
Client (one connection, serialized by a mutex)
WebBridge: HTTP/1.1 on 127.0.0.1 only, one detached thread per connection (max 32)
^ x-cna-inspector-ui-token header on /api/*, exact Host header
browser: embedded HTML/CSS/JS; never receives the agent token
This boundary is the first thing to check when “the Inspector does not appear”: CNA_DIAGNOSTICS=FULL alone starts no agent, CNA_BUILD_INSPECTOR=ON alone links nothing into a game, and a game that links the module but never calls Agent::Start has no thread and no listener. Architecture: build-time pieces outside the runtime closure places the module among the optional development pieces.
Agent ownership, negotiation and shutdown
Start validates everything before a socket exists
Agent.hpp offers two factories. Agent::Start(configuration, error) forwards to the second with Diagnostics::GetProvider(), a function-static provider over diagnostics state that is deliberately allocated once and never freed, so the borrowed reference cannot dangle. Agent::Start(provider, configuration, error) takes any version-1 IDiagnosticsProvider&, which must outlive the agent; the tests use it with a fake. Agent::Impl::Start in Agent.cpp then runs, in order:
- rejects an empty
bindAddress, and a non-loopback one unlessallowRemoteis true. “Loopback” is a literal string test inInternalSocket.cpp(IsLoopbackAddress): exactly127.0.0.1,::1orlocalhost; any other spelling,127.0.0.2included, counts as remote; - rejects more than 64 metadata entries and a token longer than 256 bytes, then generates a missing token: 32 bytes from
BCryptGenRandomor/dev/urandom, hex-encoded to 64 characters; - clamps
maximumRequestsPerSecondto 1–1024,maximumPendingPreviewsto 1–16 andpreviewCooldownMillisecondsto 50–60,000; - encodes a throw-away
HelloRequestandHelloResponse, so an application name over 4,096 bytes, a metadata key over 128 or value over 1,024 bytes, or invalid UTF-8 fails here as “invalid Inspector identity configuration” rather than later on the wire; - creates the listener (
SO_REUSEADDR, listen backlog 4, ephemeral port whenportis 0) and only then starts the workerstd::thread. A thread-creation failure closes the listener again.
Every failure returns nullptr with error set; nothing is left running. The returned std::unique_ptr<Agent> owns the implementation, the listener socket and the worker. It also co-owns the optional previewProvider through its std::shared_ptr, but not anything that provider refers to.
Stop, destruction and the order relative to the Game
Stop is idempotent and noexcept: it clears running_, sets stopping_, shuts down the active client socket under socketMutex_ so that a blocked receive returns, joins the worker, and closes the listener. The destructor calls it. Three consequences matter to a maintainer:
- The idle worker waits in
AcceptTcpwith a 200 ms timeout and re-checksstopping_, so stopping an idle agent takes at most about that long. A provider call already in progress is not interrupted:joinwaits for it to return. - There is a narrow window in which the worker has accepted a connection but not yet published it as
activeClient_; aStoplanding there shuts down nothing, and the join then waits for that connection's handshake reads to finish or time out (3 s each for header and payload). - In the demo the agent is created in
mainbefore theGameand destroyed after it, so it keeps serving requests while the game tears down. That is harmless for the process provider, whose state is never freed, but a future preview provider that touches aGraphicsDeviceneeds the opposite order or its own teardown guard, because the worker may callPollPreviewuntilStopreturns.
The ownership and lifetime master map records the agent as an application-owned object outside the Game's service graph.
Handshake: authenticate first, then look at the provider
The worker serves one connection at a time. The first packet is read with a 3 s timeout, and the checks run in this order: a header that fails ProtocolCodec::DecodeHeader ends the connection silently (no Error packet); an envelope whose major version is not 1 or whose minor version is above 0 gets UnsupportedVersion; anything other than a decodable ClientHello gets InvalidRequest; a hello whose [minimumMajorVersion, maximumMajorVersion] range excludes 1 gets UnsupportedVersion; a token mismatch gets Unauthorized. The token comparison (TokensEqual) walks the longer of the two strings and ORs byte differences, so it does not stop at the first mismatch. Each rejection closes the connection.
Only after authentication does the agent call provider_.CaptureSnapshot(), once, to derive capabilities. Malformed, wrong-version and unauthenticated clients therefore cause no diagnostics collection at all, which two tests pin down (RejectsBadAuthenticationWithoutProviderAccess, InvalidIdentityStringsFailBeforeProviderAccess).
| Capability bit | Granted when |
|---|---|
Snapshots, Accuracy, Discontinuities (bits 0, 4, 5) | always |
ResourceMetadata (bit 2) | snapshot.buildMode >= Stats |
Events, CpuZones (bits 1, 3) | snapshot.buildMode >= Full |
ResourcePreviews (bit 6) | a previewProvider was configured |
The result is ANDed with the client's requestedCapabilities (all bits by default) and enforced per request afterwards. Note that the derivation reads buildMode, the compile-time CNA_DIAGNOSTICS level, not runtimeMode: a FULL build whose runtime mode was lowered still advertises Events, and the runtime mode is visible only inside each snapshot. The ServerHello also carries the three wire limits, localOnly, the application name and metadata, and the identity strings described under the failure model.
Threads, locks and what the Game thread can feel
| Thread | Created by | Runs | Shares state with |
|---|---|---|---|
| Agent worker (one) | Agent::Impl::Start | accept loop, handshake, every provider call, every preview-provider call, all socket I/O | diagnostics internals through the provider; Stop through socketMutex_ and two atomics |
| Game and instrumented threads | the application | instrumentation macros, frame completion | the same diagnostics mutexes; never the agent itself |
| Bridge accept loop | WebBridge::Run in cna-inspector | accepts, counts, spawns | connectionMutex_ |
| Bridge connection threads (≤ 32, detached) | the accept loop | one HTTP request each | clientMutex_ around the single Client |
No Game, render or instrumentation thread ever calls into the agent, and the agent never touches a GPU object. The coupling is lock contention inside the diagnostics module: the process provider's CaptureSnapshot takes the metric, frame, resource, thread and history mutexes (in FULL it first drains every thread's producer ring into the history under threadMutex and historyMutex), and the frame-completion path on the Game thread takes the frame and metric mutexes. A large snapshot copied on the agent thread can therefore delay a frame end by the length of the copy, but a slow browser or a stalled socket cannot, because network backpressure stops at the agent thread. The diagnostics side of this is described in Diagnostics internals: ownership and concurrency of records; the process-wide map is the thread and callback map.
Demand-driven data path and bounded work
After the ServerHello, the worker loops on ReceivePacket with a 30 s socket timeout, so an authenticated client that goes silent for 30 s is dropped. Each packet's envelope version is re-checked (a change ends the session), then a fixed one-second window counts requests: once maximumRequestsPerSecond is exceeded (default 64) the request is answered with Busy and skipped, but the connection stays open. The window is reset, not sliding, and the Busy reply itself is still sent, so the limit bounds provider work rather than network traffic. RequestRateBackpressureIsBounded checks exactly this with a limit of 2.
| Request | Provider work on the agent thread | Notes |
|---|---|---|
SnapshotRequest | one full CaptureSnapshot(), then unrequested parts are cleared | parts: Metrics, Frames (default), Resources; asking for Resources without the negotiated capability is Unavailable. The response carries total metric, frame and resource counts even for omitted parts. |
EventsRequest | ReadEvents(afterSequence, maximumEvents), then ResolveName per event | maximumEvents must be 1–4,096 (default 512); the response keeps oldestAvailableSequence, newestAvailableSequence, eventsDroppedBeforeStart and producerEventsDropped so loss is reported, not hidden |
PreviewRequest / PreviewPollRequest | one CaptureSnapshot() plus the preview provider | see Resource previews |
Ping | none | payload must be empty; answered with Pong |
Selecting parts therefore reduces bytes on the wire, not the in-process copy: even a metrics-only request copies the whole resource registry inside the provider, as the user guide's performance notes say. Nothing is pushed: an agent with a connected but idle client, or with no client, makes no provider calls. EnabledWithoutClientDoesNotPollProvider observes 75 ms after start; the guarantee itself is structural, because Run calls the provider only inside HandleClient. A capture is a point-in-time copy, not a recording, and a client must advance its event cursor and read the loss fields.
Where the bounds live and what exceeding them does
All limits are constants in Protocol.hpp and the anonymous namespace of Protocol.cpp: payload 8 MiB; 4,096 events per response; 4 MiB of encoded preview data; strings 4,096 bytes unless a field says otherwise (client name 128, token 256, metadata key 128 and value 1,024, error and preview messages 1,024, MIME type 64); 512 metrics; frames up to Diagnostics::FrameHistoryCapacity (240) with at most 64 metrics each; 65,536 resource records; 64 metadata entries; preview dimensions 1–4,096. The encoder enforces the same bounds by throwing std::length_error (or std::invalid_argument for invalid UTF-8), and HandleRequest maps a length_error to LimitExceeded and anything else to InternalError, keeping the connection open.
Maintainer note: the two modules bound names differently. Diagnostics sanitizes names and resource labels to valid UTF-8 but accepts metric and event names up to 1 MiB (MaximumTraceNameBytes in Diagnostics.cpp) and does not bound label length, while the protocol caps every such string at 4,096 bytes. One oversized metric name therefore makes every snapshot that includes metrics or frames (frame samples repeat the metric names) fail with LimitExceeded, one oversized resource label does the same for resource snapshots, and an oversized event name fails the events batch that contains it. Nothing truncates. The metric (512) and per-frame metric (64) limits do match on both sides.
Resource previews: an extension seam, not GPU access
IResourcePreviewProvider has two methods, RequestPreview and PollPreview, whose documented contract is to start or poll asynchronous readback and return promptly, never waiting for a device or queue. The agent cannot enforce that; it only calls them on its worker thread. No renderer or other module implements the interface at this snapshot (only the benchmark does), so real sessions never negotiate ResourcePreviews. HandlePreviewRequest checks, in this order: capability and provider present (else Unavailable); request decodes, with resource id non-zero, dimensions 1–4,096 and byte limit 1–4 MiB (else InvalidRequest); fewer than maximumPendingPreviews tickets outstanding (else Busy); cooldown since the previous accepted request elapsed (else Busy); the id is present in a fresh snapshot and is a Texture2D, TextureCube, RenderTarget2D or RenderTargetCube (else Unavailable). Only then is the provider called and its answer validated: a pending result needs a non-zero ticket, a non-ready result may carry no image fields, and a ready result needs non-zero dimensions within the request, 1 byte to the request's byte limit, and a PNG, JPEG or WebP MIME type. A ticket the provider reuses is an InternalError.
Pending tickets are kept in pendingPreviews_ together with the original request, and every poll is validated against those original bounds rather than the defaults (PreviewPollHonorsTheOriginalRequestBounds). A ticket that ends (ready, failed, unavailable or invalid) is erased. When a client disconnects, the whole map is cleared without telling the provider; the interface has no cancel call, so a provider must bound its own outstanding readbacks. On the client side, Client::RequestPreview turns an Unavailable error into a normal response with PreviewStatus::Unavailable, which is why OptionalPreviewUnavailableIsANormalResponse expects success. Writing a real provider is a renderer and threading project: resource lifetime, render-thread or queue affinity and device teardown all have to hold while the network worker calls in. A synchronous readback in RequestPreview would put GPU latency on the agent thread and, through the diagnostics locks, near the frame loop.
Wire envelope and compatibility rules
Every packet is a 24-byte little-endian header — magic 0x49414E43 (“CNAI”), major, minor, message type, flags, payload length, request id — followed by the payload. DecodeHeader rejects a wrong magic, an unknown message type, non-zero flags and a payload length above 8 MiB; the receive path allocates the payload buffer from that length before reading, so even an unauthenticated peer can make the agent allocate at most 8 MiB. Payload decoders reject truncation, trailing bytes, out-of-range enumerations, Booleans other than 0 or 1, over-long strings and collections, and invalid UTF-8 (overlong forms, surrogates and code points above U+10FFFF).
The version rule is effectively “exactly 1.0 at both ends”. The agent refuses any envelope with a minor version above 0; Client.cpp refuses any response envelope with a minor above 0 or a mismatched request id, and refuses a session whose providerInterfaceVersion differs from IDiagnosticsProvider::InterfaceVersion. A new message type is not a compatible addition either: an older peer's DecodeHeader treats the unknown type as a malformed header and drops the connection. Any change to a message layout, a bound or a type therefore has to move the agent, the client and the bridge together, and a new JSON field in the bridge does not by itself change the binary contract.
| Outcome | Connection | Examples |
|---|---|---|
Structured Error during handshake | closed by the agent | UnsupportedVersion, InvalidRequest, Unauthorized, InternalError (provider threw during negotiation) |
Structured Error after negotiation | kept | InvalidRequest, Unavailable, LimitExceeded, Busy, InternalError |
| Transport failure, undecodable header, 30 s silence, send failure | closed; the agent returns to accept | MalformedClientDoesNotPreventReconnect sends 24 zero bytes and then connects normally |
| Client: protocol error in a response | kept; the call returns false with the message | rate limit, unavailable capability |
| Client: envelope, type or decode mismatch, transport error | client disconnects itself | next bridge request reconnects |
Separate browser bridge and its trust boundary
InspectorMain.cpp parses --agent-host (default 127.0.0.1), --agent-port (required, non-zero), --http-port (0 means ephemeral), --token, --token-file and --allow-remote-agent. The token precedence is: a token file (at most 256 bytes, trailing CR/LF stripped, empty refused) overrides --token, and CNA_INSPECTOR_TOKEN is read only when neither gave one. A non-loopback agent host, by the same literal test as the agent, needs --allow-remote-agent. The URL line is flushed explicitly because Run never returns.
WebBridge.cpp binds its HTTP listener to 127.0.0.1 unconditionally; there is no option to change that, whatever the agent does. Beyond the size and time limits the user guide lists, the parser accepts only an HTTP/1.1 request line with a target of at most 2,048 bytes, refuses any request with a non-zero Content-Length (preview ids travel in the query string), and compares the Host header with 127.0.0.1:<bound port> byte for byte, so a page opened as localhost:<port> gets 400 “invalid Inspector Host header”. That exact-Host rule is the DNS-rebinding defence. Every response is sent with Connection: close, so each request is its own TCP connection.
| Route | Needs UI token | Agent call |
|---|---|---|
GET /, /style.css, /app.js | no | none; assets are embedded strings in WebAssets.cpp, and / has the per-process UI token substituted for __CNA_UI_TOKEN__ |
GET /api/session | yes | none if already connected (cached ServerHello), else Connect |
GET /api/snapshot [?resources=1] | yes | CaptureSnapshot: metrics and frames, or resources only |
GET /api/events?after=&max= | yes | ReadEvents, max clamped to 4,096 |
POST /api/preview?id=, GET /api/preview?ticket= | yes | RequestPreview, PollPreview |
An /api/ request without the exact x-cna-inspector-ui-token value is answered 404 “not found”, not 401: a browser tab kept from an earlier bridge run (with a fixed --http-port) has a stale token and sees only 404s until it is reloaded. API handlers take clientMutex_, so however many tabs or connections exist, the agent sees one request at a time; static assets do not take the lock. EnsureConnected reconnects lazily on the next API call after the client dropped its connection; a client failure becomes HTTP 503 with the message, which the UI's 500 ms poll loop shows as “reconnecting” (it then refetches /api/session).
Connections are counted under connectionMutex_; the 33rd concurrent one receives a 503 and is closed rather than queued, and each accepted one is served on a detached thread that decrements the count when done. WebBridge::Impl::~Impl waits on a condition variable until that count reaches zero before closing the listener, because the detached threads reference the object. In the shipped cna-inspector, however, Run is an endless accept loop and InspectorMain.cpp installs no signal handler, so the process ends by termination (Ctrl+C) and that destructor path is not reached; it matters only to code that embeds WebBridge differently.
The trust boundary is therefore “processes on this machine that can open the loopback port”: the UI token is in the page served at /, which needs no token. The agent token never leaves the bridge process. See the Inspector security model for the user-facing summary; enabling a remote agent sends that token over plaintext TCP, and remote deployment behaviour is not established by anything on this page.
Failure model and human debugging route
Agent::Start reports configuration, token-generation, identity-encoding, bind/listen and thread-creation failures as nullptr plus an error string. After that, nothing in the module mutates the Game: protocol problems become one of the seven ErrorCode values (InvalidRequest, UnsupportedVersion, Unauthorized, Unavailable, LimitExceeded, Busy, InternalError), every handler is wrapped so a provider exception becomes InternalError, and transport errors end one connection and return the worker to accept. A blank Inspector view has several possible layers, and they are worth testing in this order before touching rendering code:
- Build data. Was the game compiled with
CNA_DIAGNOSTICS=STATSorFULL(Diagnostics modes)? With OFF the session negotiates but snapshots are nearly empty; events and CPU zones need FULL. - Agent. Is
CNA::Inspectorlinked, didAgent::Startreturn non-null, and is the returned pointer still alive? A temporary that is destroyed immediately stops the agent. - Handshake. Same port, same token (regenerated on every start unless configured), protocol 1.0 on both sides. Only one client is served at a time: a second bridge connects at TCP level but, with the client's default 3 s timeout, its
Connectfails with a receive timeout while the first session lasts. - Capability. Does
/api/sessionshow the capability the view needs? Resources need STATS, events and zones FULL, previews an installed provider. - Provider or wire limits. A persistent
LimitExceededorInternalErroron snapshots points at the provider or at an oversized name or label (see the bounds note). - Bridge. 400 on every request means the Host header (use
127.0.0.1, notlocalhost); 404 on/api/means a missing or stale UI token; 503 carries the agent-side message. - Actual loss. Only after all of the above, read
producerEventsDropped,eventsDroppedBeforeStartand the history-overwrite counters.
The session's renderer is the compiled default. ServerHello::graphicsRenderer is CNA::getCurrentGraphicsRendererName() from GraphicsRendererType.hpp, a compile-time constant. In a multi-renderer build only the default identity's macros are defined project-wide (RendererSelection.cmake), so the Session view names the default even when the game selected, or fell back to, another renderer at run time. The runtime answer is GraphicsRendererSelection::GetActive (see default, selected and active renderers and Core: renderer selection). targetPlatform is getCurrentPlatformName() from TargetPlatform.hpp (“Linux”, “Windows”, “macOS”, …), which is the OS target, not the platform backend.
The general tools for the process side (debuggers, sanitizers, logging) are in the debugging cookbook.
What the tests cover, and what they do not
InspectorTests.cpp holds 27 TEST definitions, all against a FakeProvider and an optional FakePreviewProvider over real loopback sockets. UnitTests.cmake drops the file from the test sources unless CNA_BUILD_INSPECTOR is ON; when it is, the tests are compiled into CnaTests, whose cases CTest discovers, and into the focused CnaInspectorTests executable, which is an iteration target and not a separate CTest registration. No CI workflow enables the option. The list below was checked by reading the source at 009d40f5; none of it was executed for this page.
| Suite | Tests | What they establish |
|---|---|---|
InspectorProtocolTests (7) | HeaderRoundTripsExactly, RejectsInvalidAndOversizedHeaders, SnapshotRoundTripPreservesAccuracyAndLossCounters, EventRoundTripPreservesCursorDiscontinuity, MutatedPayloadsAreRejectedOrDecodeIntoEncodableValues, RandomHeadersNeverAdmitAnOversizedPayload, RejectsTruncationTrailingBytesAndInvalidBounds | codec round trips; seeded mutation of 7,000 payloads (every accepted value must re-encode) and 20,000 headers (no decoded header above 8 MiB or with flags set); truncation, trailing bytes, unknown parts, image data on a pending preview |
InspectorFrontendTests (1) | AssetsAreOfflineBoundedAndDemandDriven | string checks on the embedded assets: token placeholder, no external URLs or CDN, no WebSocket, 1,000-event retention, the UI-token header and POST for previews |
InspectorAgentTests (19) | EnabledWithoutClientDoesNotPollProvider, GeneratesA256BitAuthenticationTokenWhenOmitted, NegotiatesProviderVersionAndCapabilities, EnforcesTheNegotiatedResourceCapability, InvalidIdentityStringsFailBeforeProviderAccess, RejectsUnsupportedProtocolVersion, RejectsBadAuthenticationWithoutProviderAccess, SnapshotAndEventRequestsUseProviderVersionOne, ReconnectsAfterAClientDisconnects, MalformedClientDoesNotPreventReconnect, RequestRateBackpressureIsBounded, ResourceLifetimeChangesAreObservedOnlyOnRequest, OptionalPreviewUnavailableIsANormalResponse, PreviewIsExplicitAsynchronousAndBounded, InvalidPreviewSourceCannotBreakTheConnection, PreviewPollHonorsTheOriginalRequestBounds, PendingPreviewCountIsBounded, ServesClientsWhenSocketsExceedTheDescriptorSetLimit, NonLoopbackBindingRequiresExplicitAuthorization | the agent contract end to end over loopback. The descriptor test is compiled only off Windows and skips itself when the process cannot open descriptors past FD_SETSIZE; it guards the poll()-based wait in InternalSocket.cpp |
Gaps a maintainer should know before relying on the suite: no test drives WebBridge at all, so the Host check, UI-token enforcement, the 32-connection cap, request parsing and response headers have no automated coverage; no test calls Agent::Stop directly, stops an agent with a client attached, or restarts one (each test's agent is simply destroyed); nothing exercises a real preview provider, a live browser, macOS, or a remote bind. CNA's own docs/inspector.md records a Linux end-to-end run with Chrome and a native MSVC run on Windows and lists macOS runtime validation as pending; those are CNA's records and were not reproduced here.
| If you change… | Run at least | Then also |
|---|---|---|
the agent loop, handshake or Stop | all InspectorAgentTests, especially bad authentication, malformed-client reconnect, rate backpressure | a manual start/stop with a client attached; there is no automated Stop test |
| the codec or a bound | all InspectorProtocolTests (the seeded mutation tests reproduce from their seed) | the agent tests, because Client and agent share the codec |
| preview limits or ticket handling | PendingPreviewCountIsBounded, PreviewPollHonorsTheOriginalRequestBounds, OptionalPreviewUnavailableIsANormalResponse, InvalidPreviewSourceCannotBreakTheConnection | a real renderer with asynchronous readback, if one is ever installed |
| the bridge or the UI | AssetsAreOfflineBoundedAndDemandDriven | a local browser smoke against cna_inspector_demo; HTTP security has no automated test |
Where these tests fit in the wider suite is covered by test architecture and change recipes and the test target index; the general procedure for adding one is I need to add a regression test.
Curated implementation route
modules/inspector/CMakeLists.txtandinspector_demo.cpp: the opt-in targets, the refused hosts, and how a game links, starts and orders the agent relative to itsGame.Agent.hppthenAgent.cpp: provider borrowing,Startvalidation order, the worker andStop, authentication before provider access, capability derivation, the rate window and preview validation.Protocol.hppandProtocol.cpp: message types, capability and error enumerations, every bound, strict UTF-8, and the decoder/encoder symmetry the mutation tests rely on.InternalSocket.cpp: loopback test, secure random, listener and connect,poll()-based waits that survive descriptors pastFD_SETSIZE, and exact send/receive with deadlines.Client.hppandClient.cpp: request ids, envelope checks, which failures disconnect, and theUnavailable-to-status mapping for previews.InspectorMain.cppandWebBridge.cpp: option and token precedence, the loopback HTTP listener, Host and UI-token checks, the client mutex, detached connections and JSON translation; thenWebAssets.cppfor the UI's polling and event-cursor logic.Diagnostics.hpp(IDiagnosticsProvider) and theProcessProviderinDiagnostics.cpp: what one provider call copies and which locks it takes.InspectorTests.cpp: which of the claims above are pinned by deterministic tests and which still need a live application, browser or renderer.
For the module's place among its siblings see the module index; for choosing a first subsystem to own and the general maintainer workflow, start from the Maintainer Handbook.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Verification tiers: evidence forms, oracle authority and CI reporting — A decoding key for CNA verification claims: claim labels, what each evidence form establishes, which authority decides which question, what CI runs at 009d40f5 and how to report it.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-209: Inspector ServerHello.graphicsRenderer reports the compile-time default renderer, not the one GraphicsRendererSelection actually created — The Inspector session names the build's default renderer identity, so in a multi-renderer build where a preference, the environment variable or a fallback selects another renderer, the Session view shows the wrong one.
- CNA-BUG-210: A diagnostics name or resource label longer than 4,096 bytes makes every Inspector snapshot or event batch that contains it fail with LimitExceeded — Diagnostics accepts names up to 1 MiB and labels of any length, but the Inspector protocol caps strings at 4,096 bytes and refuses the whole response, so one long name breaks the affected Inspector views for the rest of
- CNA-BUG-211: Agent::Stop can wait up to about six seconds for a connection that the accept loop takes around the time Stop runs — Stop shuts down only a client already published as activeClient_, and the worker never re-checks stopping_ after accepting. A connection accepted just before Stop, or during the accept poll of up to 200 ms that follows S
- CNA-GAP-061: No renderer implements IResourcePreviewProvider, and the seam cannot cancel a preview ticket the agent abandons — Resource previews are unavailable in every real Inspector session because no CNA renderer installs a preview provider, and when a client disconnects the agent drops its pending tickets without telling the provider.
- CNA-VGAP-051: No CI configuration builds diagnostics at STATS or FULL or enables CNA_BUILD_INSPECTOR, so most Diagnostics tests and every Inspector test are never compiled in CI — CNA_DIAGNOSTICS and CNA_BUILD_INSPECTOR default to OFF and no workflow or preset changes either, so 38 of 40 DiagnosticsTests, the seven audio and graphics diagnostics tests and all 27 Inspector tests run only in local b
- CNA-VGAP-052: The Inspector's WebBridge security checks and Agent::Stop paths have no automated test — No test drives the HTTP bridge (Host check, UI-token gate, 32-connection cap, request parsing, response headers), and no test calls Agent::Stop directly, stops an agent with a client attached, or starts an agent again af
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Inspector: architecture · Inspector: security model · Inspector: wire protocol v1 · Diagnostics: modes
- Maintainer workflow
- Thread and callback map · Ownership and lifetime master map · Debugging cookbook · Maintainer Handbook
- Tests and validation
- Test architecture and change recipes · I need to add a regression test
- Reference
- Module index · Test target index · CMake option index