Run CDB directly

This commit is contained in:
vczh
2026-08-02 00:54:37 -07:00
parent 92f80aa5e3
commit 5cb97f351e
21 changed files with 222 additions and 164 deletions
@@ -28,6 +28,17 @@ Runtime type information retrieval and manipulation through the reflection syste
[API Explanation](./KB_VlppReflection_TypeMetadata.md)
#### Metaonly Metadata Generation and Loading
Generate binary reflection metadata as either an independently loadable file or a layer that depends on types registered from previous files.
- Use `CollectRegisteredTypes` after loading a base type manager to capture descriptors supplied by previous layers
- Pass an empty exclusion list to `GenerateMetaonlyTypes` for a self-contained file
- Pass a captured descriptor list to omit foreign metadata records while preserving references to those types
- Load and activate every dependency before reading a dependent file with `LoadMetaonlyTypes`
[API Explanation](./KB_VlppReflection_GenerateMetaonlyTypes.md)
#### Type Registration Structure
Organized approach for registering types with proper file organization and macro usage.
@@ -117,7 +117,7 @@ Protocol types are code-generated from `Protocol/*.txt` files into `GuiRemotePro
`GuiRemoteProtocolAsyncJsonChannel` is the core-side async wrapper around an `IJsonChannel`. It queues outgoing packages, queues incoming events for `ProcessRemoteEvents()`, stores incoming responses by request id, and blocks `BatchWrite(disconnected)` until the current `PendingRequestGroup` is satisfied or disconnected. `connectionCounter` and `connectionClientId` protect pending requests when channel events arrive after disconnect/reconnect boundaries.
`GuiRemoteProtocolAsyncJsonChannelRenderer` is the renderer-side async wrapper. It queues received packages and schedules `ProcessRemoteMessages()` through an `IGuiRemoteProtocolAsyncRendererInvoker`. Before `SetInvokeInMainThread(...)` is called by renderer `GuiMain`, packages are cached. After the invoker is installed, they are drained on the renderer UI thread. `Initialize(reader)` requires a non-null reader; `Detach()` explicitly clears it, increments `messageVersion`, and drops queued work. The version check prevents callbacks queued before `Detach()` from running after detachment or a later reader installation.
`GuiRemoteProtocolAsyncJsonChannelRenderer` is the renderer-side async wrapper. It queues received packages and main-thread tasks, then schedules `ProcessPendingMessages()` through an `IGuiRemoteProtocolAsyncRendererInvoker`. Before `SetInvokeInMainThread(...)` is called by renderer `GuiMain`, work is cached. After the invoker is installed, it is drained in FIFO order on the renderer UI thread. `Initialize(reader)` requires a non-null reader; `Detach()` explicitly clears it, increments `messageVersion`, and drops queued work. The version check prevents callbacks queued before `Detach()` from running after detachment or a later reader installation.
## Demo Project Pair
@@ -146,10 +146,10 @@ Located at `Test/GacUISrc/RemotingTest_Rendering_Win32/`. Accepts `/Pipe` or `/H
2. Creates `GuiRemoteProtocolAsyncJsonChannelRenderer` over the client's protocol channel.
3. Creates `GuiRemoteRendererSingle` and `GuiRemoteProtocolRendererChannel(&asyncRendererChannel, &remoteRenderer)`.
4. Waits for the server, then calls `SetupRawWindowsDirect2DRenderer()` to run the native window event loop.
5. In `GuiMain()`, creates the native window, registers it with `GuiRemoteRendererSingle`, installs `GuiMainAsyncRendererInvoker` through `asyncChannel->SetInvokeInMainThread(&invoker)`, and runs the window service.
5. In `GuiMain()`, creates the native window, registers it with `GuiRemoteRendererSingle`, creates a retained `Ptr<GuiMainAsyncRendererInvoker>`, installs it through `asyncChannel->SetInvokeInMainThread(invoker)`, drains startup work with `ProcessPendingMessages()`, and runs the window service.
6. On exit, clears the invoker, unregisters the main window, stops the network connection, and clears stack-owned renderer/channel pointers.
`RemotingTestChannelClient` records only the first fatal error and queues a native Yes/No prompt asking whether to close the renderer. A core read error uses the core-error title. A fatal local transport error uses the renderer-transport title and first calls `GuiRemoteRendererSingle::RequestCoreForceExitByFatalError()`. Choosing Yes calls `ForceExitByFatelError()`; choosing No calls `RetainByFatalError(message)`, keeps the native renderer window open with a `[STOPPED]` title and fatal overlay, and exposes the error through the renderer automation service. On disconnect, the client calls `GuiRemoteProtocolAsyncJsonChannelRenderer::Detach()` and forces renderer exit only when no fatal error has already been claimed.
`RemotingTestChannelClient` queues both protocol packages and terminal actions through the async renderer's ordered main-thread FIFO. A Core-authored `!Error` arrives through `OnReadError`, claims the first fatal error, and opens the native Yes/No prompt. Choosing Yes calls `ForceExitByFatelError()`; choosing No calls `RetainByFatalError(message)`, keeps the native renderer window open with a `[STOPPED]` title and fatal overlay, and exposes the error through renderer automation. A fatal local channel error has different UI semantics: after VlppOS's `IChannelClient` promotes a post-connection protocol error, `OnLocalError(..., true)` queues the ordinary disconnected transition directly, without showing a fatal prompt and without waiting for `OnDisconnected`. `OnDisconnected` queues the same idempotent transition when it is delivered. FIFO ordering lets an earlier `ControllerConnectionStopped` or Core `!Error` win before detach.
### Protocol Stack Direction
@@ -36,7 +36,7 @@ For an accepted connection:
3. Return `WaitForClientResult::Accept`, or return `Reject` without using the connection.
4. Exchange nonempty `WString` messages with `SendString`.
The most-derived server destructor must call `Stop` before destroying fields that connection callbacks can access. From outside callbacks, `Stop` is the callback-draining shutdown boundary. A callback-reentrant `Stop` prevents further work but does not unwind the current callback, whose visible state must survive until it returns.
The most-derived server destructor must call `Stop` before destroying fields that connection callbacks can access and must suppress any shutdown exception. From outside callbacks, `Stop` is the callback-draining shutdown boundary and reports a recorded completion exception at most once. A callback-reentrant `Stop` prevents further work but does not unwind the current callback, whose visible state must survive until it returns.
`INetworkProtocolClient` owns one logical connection. The normal client sequence is:
@@ -113,7 +113,13 @@ public:
~ChannelServer()
{
Stop();
try
{
Stop();
}
catch (...)
{
}
}
WaitForClientResult OnClientConnected(
@@ -182,10 +188,14 @@ channels[L"Events"]->BroadcastFromClient(package);
channels[L"Events"]->BatchWrite(disconnected);
```
The derived client can also handle `OnConnected`, `OnDisconnected`, `OnReadError` and `OnLocalError`.
The derived client can also handle `OnConnected`, `OnDisconnected`, `OnReadError` and `OnLocalError`. A raw protocol callback returns `true` from `INetworkProtocolCallback::OnLocalError` when it needs to promote a recoverable transport error to fatal; the protocol then stops only after the callback returns. `NetworkProtocolChannelClient` uses this hook after its channel reaches `Connected`: it reports every local error to its `IChannelClient` user with `fatal == true`, transitions the channel to disconnected, and asks the raw transport to stop. Before the channel is connected, raw retry policy remains in control.
`IChannelServer<TPackage>` routes messages but is not itself a channel participant. When server-side code must send ordinary channel messages, connect a `NetworkProtocolLocalChannelClient<TPackage, TSerialization>` with `ConnectLocalClient`; this gives the local participant a normal positive client id.
`BroadcastError` is an idempotent terminal admission boundary: the first error wins, it snapshots existing recipients, and it rejects new admissions. A client whose application admission callback was already in flight cannot commit after that snapshot; if the application accepted it, the server delivers the retained first terminal error before disconnecting it. A committed client becomes eligible for the snapshot only after its network client-id response or local `OnConnected` callback completes, so retained delivery preserves `connected -> fatal -> disconnected` ordering.
The server keeps its underlying transport alive until raw protocol callbacks, admission callbacks and fatal-delivery work have left a shared stop barrier. Concurrent `Stop` calls wait for the same physical shutdown and cannot overtake retained delivery. If an admission, fatal, or disconnection callback itself calls `BroadcastError` or `Stop`, terminal state is published synchronously and physical stop is deferred until the protected callback unwinds so it cannot wait on itself. When the last barrier is a raw protocol callback, completion runs on a separate thread so the underlying transport's `Stop` can drain that callback. Recipient exceptions are recorded without bypassing best-effort terminal delivery and shutdown; a later non-reentrant `Stop` reports the recorded completion exception once.
## How Socket HTTP Implements the Protocol
`async_tcp_socket::SocketHttpServer`, defined in `Source/InterProcess/AsyncSocket/AsyncSocket_HttpServer.h`, derives from `async_tcp_socket::SocketHttpServerApi` and implements `INetworkProtocolServer`. `async_tcp_socket::SocketHttpClient`, defined in `Source/InterProcess/AsyncSocket/AsyncSocket_HttpClient.h`, implements both `INetworkProtocolClient` and its single logical `INetworkProtocolConnection`.
@@ -270,6 +280,6 @@ Use `VlppOS.Windows.h` on Windows; Winsock initialization and `Ws2_32.lib` linka
`urlPrefix` is empty for the origin root or an ASCII origin-form prefix such as `/example`. Both adapters remove trailing slashes, so `/` becomes the origin root. A nonempty normalized prefix must start with `/` and must not contain a query, fragment, backslash, NUL, malformed escape, or encoded path separator. Server and client must use the same normalized prefix, and their injected sockets must report the same port.
The portable `HttpRequestClient` treats a 404 response as `HttpResponseFailure::NotFound`, reports a fatal error, and stops its socket. `SocketHttpClientApi` exposes this as `SocketHttpClientErrorCode::ResponseNotFound`; the logical `SocketHttpClient` reports one fatal local error immediately instead of retrying `/Connect`, `/Request`, or `/Response`.
The portable `HttpRequestClient` treats a 404 response as `HttpResponseFailure::NotFound`, reports a terminal physical-lane error, and stops that socket. `SocketHttpClientApi` exposes this as `SocketHttpClientErrorCode::ResponseNotFound`. The logical `SocketHttpClient` does not inherit that physical fatal classification: it reports the endpoint failure as nonfatal and applies the endpoint's normal retry policy. `/Connect` and `/Response` retry up to their normal attempt limit, replacing a failed physical lane when needed; `/Request` replaces its receive lane and continues polling. If a connected `NetworkProtocolChannelClient` owns the callback, its promotion response stops the logical client after the first reported failure instead.
For the lower socket, HTTP request and Mini HTTP API startup rules on each platform, see [Inter-Process Async-Socket-Based Mini HTTP API](./KB_VlppOS_InterProcessAsyncSocketBasedMiniHttpApi.md).
@@ -69,6 +69,8 @@ Use `IAttributeInfo` to inspect an attribute:
Attributes are serialized into metaonly binary metadata by `GenerateMetaonlyTypes` and deserialized by `LoadMetaonlyTypes`.
Ordinary attribute values are serialized through `ISerializableType::Serialize` and deserialized through `ISerializableType::Deserialize`.
`ITypeDescriptor*` attribute values are encoded separately as referenced type-descriptor indices with empty serialized data.
For general metadata generation, dependency layers, and loading order, see [GenerateMetaonlyTypes](./KB_VlppReflection_GenerateMetaonlyTypes.md).
Attributes appear in the logged text output (`.txt` baseline files) in the format:
```
@Attribute:<AttributeTypeName>(<ArgTypeName>:<SerializedData>, ...)
@@ -0,0 +1,95 @@
# GenerateMetaonlyTypes
## Generate and load binary reflection metadata
`GenerateMetaonlyTypes` serializes the registered reflection metadata in a loaded global type manager.
The output can be a self-contained file or a dependent layer whose previously registered types are referenced without repeating their metadata records.
The related APIs are:
```cpp
void CollectRegisteredTypes(collections::List<ITypeDescriptor*>& types);
void GenerateMetaonlyTypes(
const collections::List<ITypeDescriptor*>& excludedTypes,
stream::IStream& outputStream
);
Ptr<ITypeLoader> LoadMetaonlyTypes(
stream::IStream& inputStream,
const collections::Dictionary<WString, Ptr<ISerializableType>>& serializableTypes
);
```
## Self-contained metadata
Pass an empty exclusion list to create an independently loadable file:
```cpp
collections::List<ITypeDescriptor*> dependencies;
stream::FileStream output(fileName, stream::FileStream::WriteOnly);
GenerateMetaonlyTypes(dependencies, output);
```
The global type manager must already be loaded before generation.
An empty dependency list is serialized at the beginning of the file, and `LoadMetaonlyTypes` does not require pre-existing registered types when reading it.
## Dependent metadata layers
Use a snapshot of all currently registered descriptors when a later group of types should be stored in a separate file:
1. Register and load every type in the base layer.
2. Call `CollectRegisteredTypes` to capture the base descriptors. The function replaces the output list instead of appending to it.
3. Register the types for the dependent layer. Adding a loader to an already loaded type manager applies it immediately; do not call `ITypeManager::Load` again.
4. Pass the captured base descriptors to `GenerateMetaonlyTypes`.
```cpp
auto manager = GetGlobalTypeManager();
manager->Load();
collections::List<ITypeDescriptor*> baseTypes;
CollectRegisteredTypes(baseTypes);
manager->AddTypeLoader(CreateDependentTypeLoader());
stream::FileStream output(layerFileName, stream::FileStream::WriteOnly);
GenerateMetaonlyTypes(baseTypes, output);
```
The exclusion snapshot identifies descriptors supplied by previous layers.
Those descriptors can still be referenced by local base types, signatures, generic arguments, properties, events, attributes, and other metadata, but their own records are not emitted again.
## Loading order
Load and activate every dependency before even calling `LoadMetaonlyTypes` for a dependent file:
```cpp
auto manager = GetGlobalTypeManager();
auto baseLoader = LoadMetaonlyTypes(baseStream, serializableTypes);
manager->AddTypeLoader(baseLoader);
manager->Load();
auto dependentLoader = LoadMetaonlyTypes(dependentStream, serializableTypes);
manager->AddTypeLoader(dependentLoader);
```
Dependency validation happens while `LoadMetaonlyTypes` reads the file.
If any required registered name is missing, it raises an error that includes that name.
When the manager is already loaded, adding the dependent loader registers its local descriptors immediately.
## Dependency identity and ordering
- Dependencies are identified by `ITypeDescriptor::GetTypeName()`, which is the registered reflection name. Do not use the C++ full name.
- `GenerateMetaonlyTypes` sorts dependency names in ascending order. The result does not depend on caller order or `ITypeManager::GetTypeDescriptor(vint)` enumeration order.
- The file begins with exactly one serialized `List<WString>` containing the sorted dependency names.
- Foreign descriptor indices occupy the dependency prefix in serialized-name order. Local descriptors follow in deterministic type-name order.
- Type-descriptor references use the combined foreign-and-local table. Method, property, and event indices remain local to the current layer.
## Preconditions and lifetime
- The global type manager must be loaded before calling `CollectRegisteredTypes` or `GenerateMetaonlyTypes`.
- Every excluded descriptor must be non-null, unique, and the exact descriptor currently registered under its name.
- Captured descriptors must remain registered and unchanged until generation finishes. Adding the new local types is expected, but do not replace captured descriptors, reset the manager, or mutate it concurrently with collection or generation.
- Keep dependency layers loaded while their dependent layers are in use. The dependent loader retains resolved foreign descriptors so its local metadata references remain valid.
Attribute values participate in the same combined descriptor table.
For the attribute-specific representation of serializable values and `ITypeDescriptor*`, see [Attribute Registration](./KB_VlppReflection_AttributeRegistration.md).
+20 -12
View File
@@ -3,20 +3,20 @@
# Orders
- Process staged tasks one by one with verification [19]
- Verify generated artifacts with downstream consumer checks [15]
- Port fixes from imports to source repositories [13]
- Crash early instead of adding error-tolerance fallbacks [9]
- Keep design documentation aligned with code after refactoring [8]
- Proactively remove code made redundant by refactoring [8]
- Fix behavior at the owning state instead of patching symptoms [7]
- Verify generated artifacts with downstream consumer checks [17]
- Crash early instead of adding error-tolerance fallbacks [14]
- Port fixes from imports to source repositories [14]
- Proactively remove code made redundant by refactoring [12]
- Keep design documentation aligned with code after refactoring [10]
- Fix behavior at the owning state instead of patching symptoms [10]
- Verify and localize portability on every target OS [7]
- Extract abstractions only for real shared behavior [7]
- Make `Stop()` drain asynchronous work before returning [6]
- Validate expectations against implementation and existing tests [5]
- Verify and localize portability on every target OS [5]
- Use `WString::IndexOf` with `wchar_t` (not `const wchar_t*`) [4]
- Use `collections::BinarySearchLambda` on contiguous buffers (guard empty) [4]
- Do not assume async callback owners are heap allocated [4]
- Use `vl::Exception` for expected semantic failures and `CHECK_ERROR` for invariants [3]
- Extract abstractions only for real shared behavior [3]
- Do not assume async callback owners are heap allocated [3]
- Capture dependent lambdas explicitly [2]
- Don't assume observable changes are batched [2]
- Use `ERROR_MESSAGE_PREFIX` for meaningful `CHECK_ERROR` / `CHECK_FAIL` messages [2]
@@ -25,6 +25,9 @@
- Prefer well-defined tests over ambiguous edge cases [2]
- Prefer raw pointers unless shared ownership is required [2]
- Start async callbacks after most-derived construction [2]
- Sort serialization metadata by deterministic keys, not pointer addresses [2]
- Use RAII scope cleanup instead of manual catch cleanup [2]
- Treat environment correlation as evidence, not a cause [2]
- Prefer `operator<=> = default` for lexicographic key structs [1]
- Prefer two-pointer merge for sorted range maps [1]
- Use named sentinel constants instead of raw values [1]
@@ -32,9 +35,7 @@
- Avoid references into containers when mutating them [1]
- Prefer designated initializers for aggregate-like structs [1]
- Construct `Nullable<WString>` explicitly in function calls [1]
- Sort serialization metadata by deterministic keys, not pointer addresses [1]
- Do not rely on `Event<T>` handler invocation order [1]
- Use RAII scope cleanup instead of manual catch cleanup [1]
- `collections::Dictionary` copy assignment is deleted (use move/swap) [1]
- Dereference `Ptr<T>` via `.Obj()` (not `*ptr`) [1]
- `vl::regex` separator regex: `L"[\\/\\\\]+"` [1]
@@ -46,7 +47,6 @@
- Keep generated makefiles platform-invariant [1]
- Group non-template C++ implementations by class in `.cpp` files [1]
- Use reentrant POSIX date-time conversions [1]
- Treat environment correlation as evidence, not a cause [1]
# Refinements
@@ -66,6 +66,8 @@ When an invariant says a value must exist or a conversion must succeed, prefer u
Build-generation scripts must also preserve failures from compiler or dependency probes across command substitutions and formatting pipelines. Stop before emitting tracked output when a required header or package is missing, and declare the dependency in the canonical environment bootstrap so the original diagnostic is reported instead of a malformed generated rule.
For HTTP automation endpoints, return protocol errors such as 404 only for recognized request rejection. Unexpected parser, automation, callback, or invariant failures should reach a process-terminating boundary instead of being translated into a healthy-looking endpoint response.
## Process staged tasks one by one with verification
When a request is split into explicit tasks, complete and verify each task before starting the next one. This keeps commits easy to understand and review, limits side effects to the current task, and avoids having to diagnose many unrelated issues at the same time. If a task has its own finishing instructions, finish that task properly before moving on.
@@ -204,6 +206,8 @@ When multiple handlers attached to the same `Event<T>` can both observe, mutate,
When a helper temporarily suppresses callbacks, changes ownership flags, or otherwise establishes scoped state, use a small scope object whose destructor restores state during normal return and exception unwinding. Avoid manual `try`/`catch` blocks that only restore state and rethrow; C++ stack unwinding should own that cleanup.
Do not add a scope wrapper solely to run normal-path finalization after an exception in a fail-fast test app. When the exception is intended to terminate the process and the OS releases the endpoint, keep ordinary shutdown calls straight-line at the end. Retain scoped ownership where destruction order prevents callbacks from reaching expired stack objects.
## `collections::Dictionary` copy assignment is deleted (use move/swap)
`collections::Dictionary` does not support copy assignment. When you need to replace one dictionary with another, use move semantics (when appropriate), or rebuild/swap explicitly instead of `a = b`.
@@ -232,6 +236,8 @@ When generated build metadata must be platform-invariant, generate it with nativ
When a generator produces runnable sample applications, verify the generated output through the actual app workflow too. For example, generated ChatBot RPC code should be checked by running the server and multiple clients through joins, chat messages, client exit, and server shutdown, not only by confirming generation succeeds.
When CodePack ownership is split, inspect the exact generated code pairs and their dependency boundaries. Confirm neutral output excludes platform-only dependencies, ordinary library pairs do not absorb the helper layer, and generated formatting is preserved rather than hand-edited.
When a shared dispatcher schema such as `Rpc.d.ts` changes, type-check the shared schema itself as well as generated fixtures so envelope changes are caught even before concrete generated values instantiate every request shape.
For a released VlppOS namespace change, validate Workflow through the ChatBot SOP and validate GacUI through `RemotingTest_Core /RPT /Http` with `RemotingTest_Rendering_Win32 /Http`, plus GacJS against the HTTP remoting core. An upstream build alone does not prove the imported public surface works.
@@ -302,6 +308,8 @@ When a layered transport needs a stricter response policy than its general parse
When a bug reproduces on one machine but not another, do not assume hardware speed or timing is causal merely because the machines differ. Reproduce the smallest differing state or response shape deterministically, then identify the missing transition or dependency that the other environment happens to mask.
The same standard applies when severe CPU usage coincides with a debugger or terminal session: treat the tool/output feedback path as a hypothesis until resource evidence identifies the consuming process or thread. Do not turn an unobserved incident into a durable application-thread-leak diagnosis.
## Verify and localize portability on every target OS
Run the relevant tests on every target operating system whose behavior is being claimed, and report only the platforms actually exercised. Use contrasts between passing and failing platforms to narrow investigation toward the failing platform's implementation before changing shared code, while retaining cross-platform regression verification. Instructions may be prepared for an untested operating system only when they are clearly labeled untested; never report that platform as verified.
@@ -10,7 +10,7 @@ The copied `.github` folder contains several kinds of files:
- `copilot-instructions.md`: the main instruction file. It tells the agent to read `Project.md`, use the knowledge base, and prefer provided scripts.
- `Guidelines`: build, run, debug, source-file, coding, and GacUI resource instructions.
- `prompts`: job prompts such as `ask.prompt.md`, `investigate.prompt.md`, `refine.prompt.md`, and `kb.prompt.md`.
- `Scripts`: Windows PowerShell wrappers for building, executing, debugging, and archiving task logs.
- `Scripts`: Windows PowerShell wrappers for building, executing, and archiving task logs.
- `Ubuntu`: Linux build wrapper and helper commands.
- `KnowledgeBase`: copied API, design, manual, and learning documents that the agent can read without network access.
- `Learning`: project-local lessons that refine future agent behavior.
@@ -1,15 +1,16 @@
# Installing Required Tools on Windows
The Windows agent scripts build with MSBuild through Visual Studio's developer environment and debug with CDB. Install Visual Studio with the C++ desktop workload, a Windows SDK, and the Windows debugging tools that provide `cdb.exe`. If CDB is not available, install the Windows Driver Kit or the Windows debugging tools component from the Visual Studio Installer.
The Windows agent scripts build with MSBuild through Visual Studio's developer environment, and the agent debugs directly with CDB. Install Visual Studio with the C++ desktop workload, a Windows SDK, and the Windows debugging tools that provide `cdb.exe`. If CDB is not available, install the Windows Driver Kit or the Windows debugging tools component from the Visual Studio Installer.
Define these environment variables before asking the agent to build or debug:
- `VLPP_VSDEVCMD_PATH`: absolute path to `VsDevCmd.bat`, for example `C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat`.
- `CDBPATH`: absolute path to `cdb.exe`, for example `C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\cdb.exe`.
After copying the Release `.github` folder, the agent should use these scripts instead of calling tools directly:
After copying the Release `.github` folder, the agent should use these scripts instead of calling build or execution tools directly:
- `.github\Scripts\copilotBuild.ps1`: builds the solution found from `Project.md` context and writes `Build.log`.
- `.github\Scripts\copilotExecute.ps1`: runs unit-test or CLI projects and writes `Execute.log` for unit tests.
- `.github\Scripts\copilotDebug_Start.ps1` and `copilotDebug_RunCommand.ps1`: start CDB and send debugger commands.
Run CDB directly in an interactive terminal when debugging.
If the application uses the tools shipped by Release, build them from the Release repository:
- Open `Tools\Executables\Executables.sln` in Visual Studio.
@@ -17,5 +18,5 @@ If the application uses the tools shipped by Release, build them from the Releas
- Run `Tools\CopyExecutables.ps1`.
- Confirm `CodePack.exe`, `CppMerge.exe`, `GacGen.exe`, and `GlrParserGen.exe` exist in `Release\Tools`.
For debugger readability, copy `Import\vlpp.natvis` from Release to Visual Studio's visualizers folder. The CDB startup script also loads the natvis file for debugger commands such as `dx`.
For debugger readability, copy `Import\vlpp.natvis` from Release to Visual Studio's visualizers folder. Load the natvis file in CDB for debugger commands such as `dx`.
@@ -1,6 +1,6 @@
# Investigate and Refine Jobs
`AGENTS.md` and `CLAUDE.md` route short request keywords to prompt files in `.github/prompts`. These jobs make agent work repeatable because the agent writes durable task documents, uses the copied knowledge base, and follows the repository's build and debug scripts.
`AGENTS.md` and `CLAUDE.md` route short request keywords to prompt files in `.github/prompts`. These jobs make agent work repeatable because the agent writes durable task documents, uses the copied knowledge base, and follows the repository's build, run, and debugging instructions.
## investigate
@@ -8,7 +8,7 @@ The service is independent from the remote protocol. A normal Windows applicatio
`INativeAutomationService` has one service-level availability flag and three feature groups:
- `Available`: returns false when no automation service exists for the current controller.
- `Stop`: turns off all features. Windows implementations also stop the HTTP listener.
- `Stop`: turns off all features. Endpoint lifetime is owned separately.
- `CanDumpControlTree` and `DumpControlTree`: expose visible GacUI windows, popups, controls and compositions.
- `CanDumpDomTree` and `DumpDomTree`: expose the remote protocol renderer DOM.
- `CanRunIOCommands` and `RunIOCommand`: send a textual IO command to the main window or to a selected native window.
@@ -22,7 +22,7 @@ Feature availability is checked separately. A real service returns true from `Av
## Windows HTTP Layer
`StartWindowsHttpAutomationService` creates a localhost HTTP wrapper around the current `INativeAutomationService`. It is declared in `PlatformProviders/Windows/WinNativeWindow.h` and implemented by the Windows platform provider. If `GetCurrentController()->AutomationService()->Available()` is false, the function returns without starting a listener.
`StartWindowsHttpAutomationService` creates a localhost HTTP wrapper around the current `INativeAutomationService`. The test-support implementation lives in `Source/RemotingHelpers/AutomationService/Windows`, outside the ordinary `GacUI.Windows` library pair. Test applications consume it through the shared `Source_RemotingHelpers` project.
The function takes `applicationName` as a URL path fragment and `port` as the localhost port. Given `applicationName == L"Automation/MyApp"` and `port == 8888`, the listener prefix is `http://localhost:8888/Automation/MyApp/`. The service offers exactly these HTTP URLs:
- `GET http://localhost:8888/Automation/MyApp/Controls`: calls `DumpControlTree` on the UI thread when `CanDumpControlTree` is true.
@@ -34,40 +34,31 @@ The window id is a path segment after `IO`, not a query parameter. All other met
## Starting The Service
Call `StartWindowsHttpAutomationService` from `GuiMain`, after the setup function has installed the current native controller and before entering the application event loop. Every code path that calls `StartWindowsHttpAutomationService` must later call `StopWindowsHttpAutomationService` before the native controller or substituted automation service is torn down. Skipping the stop leaks the process-wide HTTP service. Use a local guard or equivalent try/catch so the stop runs after the start on normal returns and exceptions.
Each application owns the automation service and endpoint directly. After the setup function has installed the current native controller, construct the concrete service matching that controller, substitute it, start an endpoint, and enter the application event loop. On exit, reverse the dependency order: stop the endpoint, stop the service, and finally unsubstitute it.
A normal Windows application can start the service before `GetApplication()->Run`:
```c++
#include "../../../Source/PlatformProviders/Windows/WinNativeWindow.h"
#include "../../../Source/RemotingHelpers/AutomationService/Windows/WindowsAutomationService.Windows.h"
using namespace vl;
using namespace vl::presentation;
using namespace vl::presentation::controls;
class WindowsHttpAutomationServiceScope
{
public:
WindowsHttpAutomationServiceScope(const WString& applicationName, vint port)
{
windows::StartWindowsHttpAutomationService(applicationName, port);
}
~WindowsHttpAutomationServiceScope()
{
windows::StopWindowsHttpAutomationService();
}
};
void GuiMain()
{
demo::MainWindow window;
window.ForceCalculateSizeImmediately();
window.MoveToScreenCenter();
WindowsHttpAutomationServiceScope httpAutomationService(
windows::WindowsAutomationServiceHosted automationService;
GetNativeServiceSubstitution()->Substitute(&automationService, false);
windows::StartWindowsHttpAutomationService(
WString::Unmanaged(L"Automation/MyApp"),
8888);
GetApplication()->Run(&window);
windows::StopWindowsHttpAutomationService();
automationService.Stop();
GetNativeServiceSubstitution()->Unsubstitute(&automationService);
}
int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
@@ -76,46 +67,42 @@ int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
}
```
Repeated calls do not create multiple listeners. The Windows implementation keeps one process-wide HTTP service until `StopWindowsHttpAutomationService` stops it.
Only one substituted automation service and one endpoint may be active in a process. Duplicate starts fail immediately. `StartMiniHttpAutomationService(socketServer, applicationName)` follows the same lifecycle and URL contract, but requires the application to pass the exact `IAsyncSocketServer` that should host its automation prefix.
## Setup Function Cases
The setup function decides which controller and service are active while `GuiMain` runs. Use the following cases when deciding whether user code must substitute another automation service:
- `SetupWindowsGDIRenderer`: installs `WindowsAutomationService`. Use `StartWindowsHttpAutomationService` directly for multi-window control tree and IO. The DOM route is normally unavailable.
- `SetupWindowsDirect2DRenderer`: same automation behavior as `SetupWindowsGDIRenderer`, with the Direct2D renderer.
- `SetupHostedWindowsGDIRenderer`: installs `WindowsAutomationServiceHosted`. Use the HTTP service directly. IO should use `/IO` without a window id because hosted sub windows and popups are represented under the main window.
- `SetupHostedWindowsDirect2DRenderer`: same automation behavior as `SetupHostedWindowsGDIRenderer`, with the Direct2D renderer.
- `SetupRawWindowsGDIRenderer`: installs `WindowsAutomationService` for the raw native window controller. If the raw process is a remote protocol renderer and owns a `GuiRemoteRendererSingle`, substitute `WindowsAutomationServiceRenderer` to expose renderer DOM and renderer-side IO.
- `SetupRawWindowsDirect2DRenderer`: same automation behavior as `SetupRawWindowsGDIRenderer`, with the Direct2D renderer.
The setup function decides which controller is active while `GuiMain` runs; it no longer installs test automation implicitly. Select the matching helper service explicitly:
- `SetupWindowsGDIRenderer` and `SetupWindowsDirect2DRenderer`: construct `WindowsAutomationService` for multi-window control tree and IO. The DOM route is normally unavailable.
- `SetupHostedWindowsGDIRenderer` and `SetupHostedWindowsDirect2DRenderer`: construct `WindowsAutomationServiceHosted`. IO should use `/IO` without a window id because hosted sub windows and popups are represented under the main window.
- `SetupRawWindowsGDIRenderer` and `SetupRawWindowsDirect2DRenderer`: construct `WindowsAutomationService` for an ordinary raw native controller. A remote protocol renderer that owns a `GuiRemoteRendererSingle` constructs `WindowsAutomationServiceRenderer` with the renderer pointer to expose renderer DOM and renderer-side IO.
- `SetupRemoteNativeController`: the remote controller itself does not return an automation service. Substitute `RemoteProtocolAutomationService` around `GetApplication()->Run` when the remote core should expose the hosted control tree and core-side IO.
- `SetupGacGenNativeController`: this setup is for generation-time resource processing, not an interactive UI session. It does not provide an automation service for HTTP control.
- `SetupGtkRenderer`: do not call the Windows HTTP helper. A Gtk port must provide its own endpoint layer and automation service implementation if it needs coding-agent automation.
- `SetupWGacRenderer`: do not call the Windows HTTP helper. A Wayland port must provide its own endpoint layer and automation service implementation if it needs coding-agent automation.
- `SetupWGacHostedRenderer`: same requirement as `SetupWGacRenderer`, but the service should follow hosted-mode window-id behavior if it exposes hosted windows.
- `SetupWGacRenderer`: construct the Wayland platform service such as `WGacAutomationServiceRenderer` and expose it through MiniHTTP.
- `SetupWGacHostedRenderer`: use the corresponding hosted Wayland service and hosted-mode window-id behavior.
- A macOS remote renderer constructs `CocoaAutomationServiceRenderer` and exposes it through MiniHTTP.
## Substituting a Service
Use `GetNativeServiceSubstitution()->Substitute(service, false)` before the automation service is first requested. The substitution layer rejects a late substitution after a service has already been used. Keep the substituted object alive until it is unsubstituted.
Use `GetNativeServiceSubstitution()->Substitute(service, false)` before the automation service is first requested. The substitution layer rejects a late substitution after a service has already been used. Keep the concrete service alive until the endpoint has stopped, `service.Stop()` has completed, and the service is unsubstituted.
A remote protocol core can expose the core-side automation surface like this. The sample uses the same `WindowsHttpAutomationServiceScope` guard from the normal Windows application example.
A remote protocol core owns its neutral service and endpoint directly:
```c++
void GuiMain()
{
RemoteProtocolAutomationService automationService;
GetNativeServiceSubstitution()->Substitute(&automationService, false);
{
WindowsHttpAutomationServiceScope httpAutomationService(
WString::Unmanaged(L"Automation/RemoteCore"),
8888);
GetApplication()->Run(mainWindow);
}
windows::StartWindowsHttpAutomationService(
WString::Unmanaged(L"Automation/RemoteCore"),
8888);
GetApplication()->Run(mainWindow);
windows::StopWindowsHttpAutomationService();
automationService.Stop();
GetNativeServiceSubstitution()->Unsubstitute(&automationService);
}
```
A remote protocol renderer that owns a `GuiRemoteRendererSingle` can substitute `WindowsAutomationServiceRenderer` in the same scope before starting the HTTP service. This is the case where `GET /Dom` becomes meaningful.
A Windows remote protocol renderer constructs `WindowsAutomationServiceRenderer` with its `GuiRemoteRendererSingle`, substitutes it, and owns either the Windows HTTP or MiniHTTP endpoint directly. Linux uses `WGacAutomationServiceRenderer`; macOS uses `CocoaAutomationServiceRenderer`; both use MiniHTTP. These renderer cases are where `GET /Dom` becomes meaningful.
When a remote renderer is retained after a fatal remote-protocol error, renderer automation keeps `GET /Dom` available. The DOM response is still an HTTP success containing the frozen renderer DOM, and it adds a lowercase top-level `fatalError` string with the original error. Renderer IO switches to `ExitOnly`: ordinary IO returns exactly `!Application stopped responding.`, while exact `!Exit` is still accepted so automation can close the retained renderer.
@@ -15,7 +15,7 @@ using namespace vl::presentation::remote_renderer;
GuiRemoteRendererSingle* remoteRenderer = nullptr;
GuiRemoteProtocolAsyncJsonChannelRenderer* asyncChannel = nullptr;
class GuiMainInvoker : public IGuiRemoteProtocolAsyncRendererInvoker
class GuiMainInvoker : public Object, public virtual IGuiRemoteProtocolAsyncRendererInvoker
{
public:
void InvokeInMainThread(const Func<void()>& proc) override
@@ -29,9 +29,10 @@ void GuiMain()
auto mainWindow = GetCurrentController()->WindowService()->CreateNativeWindow(INativeWindow::Normal);
mainWindow->SetTitle(L"Connecting ...");
GuiMainInvoker invoker;
auto invoker = Ptr(new GuiMainInvoker);
remoteRenderer->RegisterMainWindow(mainWindow);
asyncChannel->SetInvokeInMainThread(&invoker);
asyncChannel->SetInvokeInMainThread(invoker);
asyncChannel->ProcessPendingMessages();
GetCurrentController()->WindowService()->Run(mainWindow);
@@ -66,5 +67,4 @@ GuiRemoteProtocolAsyncJsonChannelRenderer queues messages from the channel and r
The portable **/MiniHttp** path creates a default loopback TCP client with **vl::inter_process::async_tcp_socket::CreateDefaultAsyncSocketClient(port)**, wraps it in **vl::inter_process::async_tcp_socket::SocketHttpClient(socketClient, L"localhost", urlPrefix)**, and passes that object to **GuiRemoteProtocolChannelClient**. The renderer application shown by the linked test remains Win32 even though the VlppOS transport itself is available on Windows, Linux and macOS.
Override GuiRemoteProtocolChannelClient::OnReadError, OnLocalError, or OnDisconnected when the renderer should show a fatal error dialog or call GuiRemoteRendererSingle::ForceExitByFatelError. See [RemotingTest_Rendering_Win32](https://github.com/vczh-libraries/GacUI/tree/master/Test/GacUISrc/RemotingTest_Rendering_Win32) for the complete named-pipe (**/Pipe**), Windows HTTP.sys/WinHTTP (**/Http**) and portable Mini HTTP (**/MiniHttp**) implementations.
Override `GuiRemoteProtocolChannelClient::OnReadError` when a Core-authored `!Error` should show fatal UI. Treat `OnLocalError(..., true)` as an independently complete, prompt-free disconnected transition and do not wait for `OnDisconnected`; use `OnDisconnected` as the idempotent fallback when it is delivered. See [RemotingTest_Rendering_Win32](https://github.com/vczh-libraries/GacUI/tree/master/Test/GacUISrc/RemotingTest_Rendering_Win32) for the complete named-pipe (**/Pipe**), Windows HTTP.sys/WinHTTP (**/Http**) and portable Mini HTTP (**/MiniHttp**) implementations.
@@ -172,6 +172,8 @@ The bridge uses **NetworkPackage** as the raw text envelope. Its string shape is
A network channel client connects by sending one handshake package with an empty client id, an empty channel name and all supported channel names joined by **!**. The server validates the names, assigns a positive client id, records channel membership and sends the id back in an empty-channel response. After that, normal channel packages are delivered only when sender and receiver membership matches the channel name.
**BroadcastError** is a terminal admission boundary. The first broadcast error is retained, later broadcasts are ignored, and new network or local admissions are rejected. If an application **OnClientConnected** callback was already running, the server keeps the underlying transport alive until an accepted client receives that retained error and disconnects. A committed client is not included in the broadcast snapshot until its client-id response or local **OnConnected** callback completes, preserving connected-before-fatal ordering. Concurrent **Stop** calls share this barrier and cannot stop the transport early. Callback-reentrant terminal calls never wait on themselves; when broadcast owns the terminal boundary, physical stop is deferred until protected raw protocol, admission, fatal-delivery and disconnection callbacks unwind. If the last barrier is a raw protocol callback, completion moves to another thread so the underlying transport can drain that callback. A non-reentrant **Stop** reports a recorded shutdown exception once; a most-derived server destructor must call **Stop** before destroying callback-visible fields and catch any exception.
A channel server over a Windows named pipe can be declared like this:
```C++
#include <vlppos.h>
@@ -265,7 +267,7 @@ The raw HTTP protocol uses these routes under the configured base URL:
- **POST /VlppInterProcess/Request/GUID** is the client-maintained long-poll request for server-to-client messages.
- **POST /VlppInterProcess/Response/GUID** sends client-to-server messages and may also receive one queued server-to-client message.
**vl::inter_process::windows_http::HttpClient::WaitForServer** sends the connect request, validates the returned URLs, records them and reports connection. **BeginReadingLoopUnsafe** starts the long-poll request loop. **SendString** posts to the response URL. Connect and response failures retry a limited number of times; request failures retry while the client is still running.
**vl::inter_process::windows_http::HttpClient::WaitForServer** sends the connect request, validates the returned URLs, records them and reports connection. **BeginReadingLoopUnsafe** starts the long-poll request loop. **SendString** posts to the response URL. Each recoverable failed Connect, Request or Response exchange calls **INetworkProtocolCallback::OnLocalError** with **fatal == false** before retrying; Connect and Response retry a limited number of times, while Request retries while the client is still running. Bounded retry exhaustion remains raw-fatal. Returning **true** from **OnLocalError** promotes a recoverable failure and makes the raw client stop after the callback returns. **NetworkProtocolChannelClient** does this for every local error after its channel reaches **Connected**, forwards **fatal == true** to its **IChannelClient** user and disconnects the channel; pre-handshake Connect retries remain under raw-client policy.
**vl::inter_process::windows_http::HttpServer** creates a **vl::inter_process::windows_http::HttpServerConnection** for each connect request. Server-to-client messages are returned through a pending long-poll request when possible, or queued until the next request. Client-to-server request bodies are dispatched as inbound strings. When the server stops, pending long-poll requests are cancelled and connection callbacks receive disconnection.