From 5f6f364c81ff82337e220bcef485de2cacba6745 Mon Sep 17 00:00:00 2001 From: vczh Date: Sun, 9 Aug 2026 03:24:08 -0700 Subject: [PATCH] Sync published online manual --- .../manual/coding-agent/context.md | 2 +- .../KnowledgeBase/manual/coding-agent/jobs.md | 2 +- .../gacui/coding-agent/automation-service.md | 72 +++++++++------ .../manual/gacui/kb/osprovider.md | 2 +- .../KnowledgeBase/manual/gacui/modes/home.md | 6 +- .../manual/gacui/modes/remote_client.md | 9 +- .../manual/vlppos/using-inter-process.md | 8 +- .github/KnowledgeBase/manual/workflow/rpc.md | 2 +- .../manual/workflow/rpc/json-channel.md | 89 ++++++++----------- 9 files changed, 98 insertions(+), 94 deletions(-) diff --git a/.github/KnowledgeBase/manual/coding-agent/context.md b/.github/KnowledgeBase/manual/coding-agent/context.md index 5189877f..4c664c77 100644 --- a/.github/KnowledgeBase/manual/coding-agent/context.md +++ b/.github/KnowledgeBase/manual/coding-agent/context.md @@ -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, and archiving task logs. +- `Scripts`: Windows PowerShell wrappers for building, executing, debugging, 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. diff --git a/.github/KnowledgeBase/manual/coding-agent/jobs.md b/.github/KnowledgeBase/manual/coding-agent/jobs.md index 3d866367..2a1e69c6 100644 --- a/.github/KnowledgeBase/manual/coding-agent/jobs.md +++ b/.github/KnowledgeBase/manual/coding-agent/jobs.md @@ -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, run, and debugging instructions. +`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. ## investigate diff --git a/.github/KnowledgeBase/manual/gacui/coding-agent/automation-service.md b/.github/KnowledgeBase/manual/gacui/coding-agent/automation-service.md index 6556b1a8..2eb09866 100644 --- a/.github/KnowledgeBase/manual/gacui/coding-agent/automation-service.md +++ b/.github/KnowledgeBase/manual/gacui/coding-agent/automation-service.md @@ -2,13 +2,15 @@ AutomationService is the GacUI surface for programmatic inspection and input. It is exposed by `INativeAutomationService` through `GetCurrentController()->AutomationService()`. Coding agents use it to read the current UI structure and send input commands without depending on operating-system UI Automation. +**IMPORTANT:** This automation service is strong recommended to only use with debugging and testing. Security is not guaranteed totally. + The service is independent from the remote protocol. A normal Windows application, a remote protocol core application, and a remote protocol renderer can all expose an automation service, but each setup exposes the view of the UI that exists on that side. ## Service Interface `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. Endpoint lifetime is owned separately. +- `Stop`: turns off all features. Windows implementations also stop the HTTP listener. - `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 +24,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`. The reusable implementation lives in `Source/Utilities/AutomationService/Windows`, is compiled through `Source_GacUI_Core`, and is CodePacked into the ordinary `GacUI.Windows.h` and `GacUI.Windows.cpp` pair. The transport-neutral MiniHTTP endpoint beside it is CodePacked into `GacUI.h` and `GacUI.cpp`. +`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. 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,31 +36,40 @@ The window id is a path segment after `IO`, not a query parameter. All other met ## Starting The Service -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. +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. A normal Windows application can start the service before `GetApplication()->Run`: ```c++ -#include "../../../Source/Utilities/AutomationService/Windows/WindowsAutomationService.Windows.h" +#include "../../../Source/PlatformProviders/Windows/WinNativeWindow.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(); - windows::WindowsAutomationServiceHosted automationService; - GetNativeServiceSubstitution()->Substitute(&automationService, false); - windows::StartWindowsHttpAutomationService( + WindowsHttpAutomationServiceScope httpAutomationService( WString::Unmanaged(L"Automation/MyApp"), 8888); GetApplication()->Run(&window); - windows::StopWindowsHttpAutomationService(); - automationService.Stop(); - GetNativeServiceSubstitution()->Unsubstitute(&automationService); } int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int) @@ -67,42 +78,47 @@ int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int) } ``` -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. +Repeated calls do not create multiple listeners. The Windows implementation keeps one process-wide HTTP service until `StopWindowsHttpAutomationService` stops it. ## Setup Function Cases -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. +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. - `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`: 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. +- `SetupWGacRenderer`: [](https://github.com/vczh-libraries/wGac/blob/master/WGacShared/MiniHttpAutomationService.cpp) in **wGac** repo is the sample code to start such service. +- `SetupWGacHostedRenderer`: same requirement as `SetupWGacRenderer`. +- `SetupOSXCoreGraphicsRenderer`: [](https://github.com/vczh-libraries/iGac/blob/master/MacShared/MiniHttpAutomationService.cpp) in **iGac** repo is the sample code to start such service. +- `SetupOSXHostedCoreGraphicsRenderer`: same requirement as `SetupOSXCoreGraphicsRenderer`. ## 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 concrete service alive until the endpoint has stopped, `service.Stop()` has completed, and the service 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 substituted object alive until it is unsubstituted. -A remote protocol core owns its neutral service and endpoint directly: +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. ```c++ void GuiMain() { RemoteProtocolAutomationService automationService; GetNativeServiceSubstitution()->Substitute(&automationService, false); - windows::StartWindowsHttpAutomationService( - WString::Unmanaged(L"Automation/RemoteCore"), - 8888); - GetApplication()->Run(mainWindow); - windows::StopWindowsHttpAutomationService(); - automationService.Stop(); + + { + WindowsHttpAutomationServiceScope httpAutomationService( + WString::Unmanaged(L"Automation/RemoteCore"), + 8888); + GetApplication()->Run(mainWindow); + } + GetNativeServiceSubstitution()->Unsubstitute(&automationService); } ``` -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. +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. 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. diff --git a/.github/KnowledgeBase/manual/gacui/kb/osprovider.md b/.github/KnowledgeBase/manual/gacui/kb/osprovider.md index ed71bcc7..341983b3 100644 --- a/.github/KnowledgeBase/manual/gacui/kb/osprovider.md +++ b/.github/KnowledgeBase/manual/gacui/kb/osprovider.md @@ -57,5 +57,5 @@ When an **INativeController** is running as a native OS provider to **GuiHostedC ## Development Note -Implementing **INativeController** is a super complex topic. In order to port GacUI to a new platform, you must know everything about how GacUI interact with the OS. It is highly recommended to read the [Windows](https://github.com/vczh-libraries/GacUI/tree/master/Source/NativeWindow/Windows), [macOS](https://github.com/vczh-libraries/iGac), [Linux Wayland](https://github.com/vczh-libraries/wGac) and [Linux XWindow](https://github.com/vczh-libraries/gGac) implementation for **INativeController** before creating yours. Especially there are GDI and Direct2D support in Windows, it is a good example to know how to allow multiple rendering techniques in one OS. +Implementing **INativeController** is a super complex topic. In order to port GacUI to a new platform, you must know everything about how GacUI interact with the OS. It is highly recommended to read the [Windows](https://github.com/vczh-libraries/GacUI/tree/master/Source/NativeWindow/Windows), [macOS](https://github.com/vczh-libraries/iGac), [Linux Wayland](https://github.com/vczh-libraries/wGac) implementation for **INativeController** before creating yours. Especially there are GDI and Direct2D support in Windows, it is a good example to know how to allow multiple rendering techniques in one OS. diff --git a/.github/KnowledgeBase/manual/gacui/modes/home.md b/.github/KnowledgeBase/manual/gacui/modes/home.md index a9ae20d7..a5c909b7 100644 --- a/.github/KnowledgeBase/manual/gacui/modes/home.md +++ b/.github/KnowledgeBase/manual/gacui/modes/home.md @@ -1,6 +1,10 @@ # Hosted Mode and Remote Protocol -Using **SetupHostedWindowsGDIRenderer** or **SetupHostedWindowsDirect2DRenderer** instead of **SetupWindowsGDIRenderer** or **SetupWindowsDirect2DRenderer** runs a GacUI application in hosted mode. A hosted mode GacUI application will start only one OS native window, other windows are rendered inside it virtually. System dialogs will be replaced by predefined GacUI implemented dialogs by default, so that anything will be strictly inside the OS native window. +Using +- **SetupHostedWindowsDirect2DRenderer** +- **SetupHostedWindowsGDIRenderer** +- **SetupWGacHostedRenderer** +- **SetupOSXHostedCoreGraphicsRenderer** runs a GacUI application in hosted mode. A hosted mode GacUI application will start only one OS native window, other windows are rendered inside it virtually. System dialogs will be replaced by predefined GacUI implemented dialogs by default, so that anything will be strictly inside the OS native window. Using **SetupRemoteNativeController** runs a GacUI application with remote protocol (forced in hosted mode), which becomes headless, instead of render anything on the screen, it sends out rendering commands to a remote client. This part will be covered in [Remote Protocol Core Application](../.././gacui/modes/remote_core.md). diff --git a/.github/KnowledgeBase/manual/gacui/modes/remote_client.md b/.github/KnowledgeBase/manual/gacui/modes/remote_client.md index 9bcb4f60..2cc671f2 100644 --- a/.github/KnowledgeBase/manual/gacui/modes/remote_client.md +++ b/.github/KnowledgeBase/manual/gacui/modes/remote_client.md @@ -15,7 +15,7 @@ using namespace vl::presentation::remote_renderer; GuiRemoteRendererSingle* remoteRenderer = nullptr; GuiRemoteProtocolAsyncJsonChannelRenderer* asyncChannel = nullptr; -class GuiMainInvoker : public Object, public virtual IGuiRemoteProtocolAsyncRendererInvoker +class GuiMainInvoker : public IGuiRemoteProtocolAsyncRendererInvoker { public: void InvokeInMainThread(const Func& proc) override @@ -29,10 +29,9 @@ void GuiMain() auto mainWindow = GetCurrentController()->WindowService()->CreateNativeWindow(INativeWindow::Normal); mainWindow->SetTitle(L"Connecting ..."); - auto invoker = Ptr(new GuiMainInvoker); + GuiMainInvoker invoker; remoteRenderer->RegisterMainWindow(mainWindow); - asyncChannel->SetInvokeInMainThread(invoker); - asyncChannel->ProcessPendingMessages(); + asyncChannel->SetInvokeInMainThread(&invoker); GetCurrentController()->WindowService()->Run(mainWindow); @@ -67,4 +66,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` 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. +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. diff --git a/.github/KnowledgeBase/manual/vlppos/using-inter-process.md b/.github/KnowledgeBase/manual/vlppos/using-inter-process.md index f7eb2c67..b5a4fcf3 100644 --- a/.github/KnowledgeBase/manual/vlppos/using-inter-process.md +++ b/.github/KnowledgeBase/manual/vlppos/using-inter-process.md @@ -172,8 +172,6 @@ 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 @@ -238,7 +236,7 @@ SocketHttpClient( The server adapter uses the injected listener and never creates another one. The client uses the injected client for its first physical lane and calls **CreateSameEndpointClient** for the additional lanes required by full-duplex polling and recovery. The server name must select loopback, and the URL prefix is empty for the origin root or begins with **/**. -The adapter uses the same **/VlppInterProcess/Connect**, **/VlppInterProcess/Request/{token}** and **/VlppInterProcess/Response/{token}** routes as **windows_http::HttpServer** and **windows_http::HttpClient**. A successful nonempty Request response is provisional delivery: the next Request for the same token is its implicit acknowledgement and must arrive within five seconds. The deadline starts only when the server actually sends a message, so an idle connection has no heartbeat deadline. If the response fails, the server restores the message to the FIFO head; if the acknowledgement is missing, it reports a nonfatal local error. A raw callback may decline promotion, while an admitted **NetworkProtocolChannelServer** connection promotes either error, disconnects, unregisters the token and rejects any late Request carrying that stale token. The protocol still has no deduplication, heartbeat or explicit disconnect route and must not be treated as exactly-once delivery. +The adapter uses the same **/VlppInterProcess/Connect**, **/VlppInterProcess/Request/{token}** and **/VlppInterProcess/Response/{token}** routes as **windows_http::HttpServer** and **windows_http::HttpClient**. The protocol has no message-level delivery acknowledgement, deduplication, heartbeat or disconnect route, so it must not be treated as exactly-once delivery. ### Portable Mini HTTP Request Helpers @@ -267,9 +265,9 @@ 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. 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::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::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. A successful message response starts a five-second acknowledgement deadline; the next Request for the same GUID cancels it. A failed response retains the message, while a missing acknowledgement reports a recoverable **OnLocalError**. An admitted channel promotes either error, stops the logical connection and unregisters the GUID, so a late Request is rejected as unknown. No deadline runs while the server is idle. 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. +**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. ### Windows HTTP Helper APIs diff --git a/.github/KnowledgeBase/manual/workflow/rpc.md b/.github/KnowledgeBase/manual/workflow/rpc.md index 558ec444..152f0663 100644 --- a/.github/KnowledgeBase/manual/workflow/rpc.md +++ b/.github/KnowledgeBase/manual/workflow/rpc.md @@ -106,7 +106,7 @@ Dispatchers should schedule user callbacks through the host task queue or event ## JSON RPC Channel Layer -The JSON implementation uses RpcJsonDispatcher, RpcJsonLifecycle, RpcJsonObjectOps, and RpcJsonObjectEventOps. IRpcJsonMessageDispatcher accepts JSON requests, classifies them as direct, broadcast, or broadcast-and-drop messages, and supports persistent exception injection for waking dispatcher-owned waits. +The JSON implementation uses RpcJsonDispatcher, RpcJsonLifecycle, RpcJsonObjectOps, and RpcJsonObjectEventOps. IRpcJsonMessageDispatcher accepts JSON requests and classifies them as direct, broadcast, or broadcast-and-drop messages. The shared channel implementation connects the JSON dispatcher to [vl::inter_process](.././vlppos/using-inter-process.md) channels: - JsonPackage is a JSON node package. diff --git a/.github/KnowledgeBase/manual/workflow/rpc/json-channel.md b/.github/KnowledgeBase/manual/workflow/rpc/json-channel.md index c6fa2711..0c0ee21e 100644 --- a/.github/KnowledgeBase/manual/workflow/rpc/json-channel.md +++ b/.github/KnowledgeBase/manual/workflow/rpc/json-channel.md @@ -6,13 +6,10 @@ Use this page when wiring a JSON RPC endpoint to a transport. Implement IRpcJson ## IRpcJsonMessageDispatcher Contract -IRpcJsonMessageDispatcher has three runtime responsibilities: +IRpcJsonMessageDispatcher has two runtime responsibilities: - AllocateRequestId returns a unique request id for outgoing JSON RPC messages from this endpoint. -- InjectException persistently stores a last-write-wins failure and wakes dispatcher-owned waits. An empty message is valid. Every later OnJsonRequest throws RpcInjectedException before sending until the dispatcher is destroyed. - OnJsonRequest sends one JSON request according to the RequestType and returns the response JSON node when a response is expected. -InjectException must be safe to call concurrently with OnJsonRequest. A custom transport must guard the injected presence flag, message, response selection and response commitment with the same lock. Its response and startup waits must include injection in their wait predicate and be notified when the message changes. If injection commits before a matching response is committed, that call throws on its original caller thread. If the response commits first, that call may return normally, but the persistent injection still poisons every remaining and future call. Injection is observed only at dispatcher-controlled checkpoints; it cannot asynchronously throw through a blocking transport send or arbitrary service code. - RequestType describes the routing behavior: - Direct sends a request to the target client and waits for the matching response. - Broadcast sends a request through the server-side broker and waits for the broker response. @@ -34,18 +31,6 @@ private: IRpcObjectEventOps* objectEventOps = nullptr; IRpcDispatcher* rpcDispatcher = nullptr; IRpcLifecycle* lifecycle = nullptr; - CriticalSection lockState; - ConditionVariable cvState; - bool hasInjectedException = false; - WString injectedException; - - void ThrowInjectedExceptionLocked() - { - if (hasInjectedException) - { - throw RpcInjectedException(injectedException); - } - } public: vint AllocateRequestId() override @@ -53,24 +38,8 @@ public: return ++nextRequestId; } - void InjectException(const WString& message) override - { - CS_LOCK(lockState) - { - hasInjectedException = true; - injectedException = message; - } - cvState.WakeAllPendings(); - } - Ptr OnJsonRequest(Ptr message, RequestType requestType) override { - CS_LOCK(lockState) - { - ThrowInjectedExceptionLocked(); - } - // The transport-specific wait must use cvState and recheck - // ThrowInjectedExceptionLocked before committing a response. switch (requestType) { case RequestType::Direct: @@ -178,22 +147,29 @@ using JsonRpcLocalClient = JsonRpcChannelClient; ## Endpoint Dispatcher Client -RpcJsonDispatcherClientForTaskQueue already implements IRpcJsonMessageDispatcher by sending JSON packages through a JsonChannel. Keep this transport dispatcher generic. After connection assigns a client id, pass the dispatcher and id to a module-specific setup function that creates RpcJsonDispatcher and RpcJsonLifecycle, then registers the RPC metadata and helper operations for the loaded module. +RpcJsonDispatcherClientForTaskQueue already implements IRpcJsonMessageDispatcher by sending JSON packages through a JsonChannel. A project still needs a small subclass to create RpcJsonDispatcher and RpcJsonLifecycle, then register the RPC metadata and helper operations for the loaded module. The module-specific registration is represented below by ConfigureLifecycleForRpcModule. It should set the id map, serializer, object ops, event ops, event attachers and wrapper factory for the module being used. ```C++ void ConfigureLifecycleForRpcModule(RpcJsonLifecycle* lifecycle); -void InitializeRpcForModule( - RpcJsonDispatcherClient* dispatcher, - vint clientId) +class MyRpcDispatcherClient : public RpcJsonDispatcherClientForTaskQueue { - auto rpcDispatcher = Ptr(new RpcJsonDispatcher(clientId, dispatcher)); - auto lifecycle = Ptr(new RpcJsonLifecycle(clientId, rpcDispatcher.Obj())); +public: + MyRpcDispatcherClient(Ptr taskQueue) + : RpcJsonDispatcherClientForTaskQueue(taskQueue) + { + } - dispatcher->SetRpcObjects(rpcDispatcher, lifecycle); - ConfigureLifecycleForRpcModule(lifecycle.Obj()); -} + void InitializeRpc(vint clientId) + { + auto rpcDispatcher = Ptr(new RpcJsonDispatcher(clientId, this)); + auto lifecycle = Ptr(new RpcJsonLifecycle(clientId, rpcDispatcher.Obj())); + + SetRpcObjects(rpcDispatcher, lifecycle); + ConfigureLifecycleForRpcModule(lifecycle.Obj()); + } +}; ``` ## Server Channel Setup @@ -300,7 +276,7 @@ A process can host services through a local channel client connected to the same class JsonRpcServiceLocalClient : public JsonRpcLocalClient { private: - Ptr dispatcher; + Ptr dispatcher; public: JsonRpcServiceLocalClient(Ptr parser) @@ -308,6 +284,12 @@ public: { } + void OnConnected(vint clientId) override + { + CHECK_ERROR(dispatcher, L"The RPC dispatcher client is missing."); + dispatcher->InitializeRpc(clientId); + } + vint Connect( JsonChannelServer* channelServer, Ptr self, @@ -315,7 +297,7 @@ public: vint serverClientId, const List& waitingForServices) { - dispatcher = Ptr(new RpcJsonDispatcherClientForTaskQueue(taskQueue)); + dispatcher = Ptr(new MyRpcDispatcherClient(taskQueue)); auto clientId = dispatcher->ConnectLocalServer( channelServer, self, @@ -326,7 +308,7 @@ public: return clientId; } - RpcJsonDispatcherClient* GetDispatcher() + MyRpcDispatcherClient* GetDispatcher() { CHECK_ERROR(dispatcher, L"The RPC dispatcher client is not connected."); return dispatcher.Obj(); @@ -342,7 +324,7 @@ void HostLocalService( { auto serviceClient = Ptr(new JsonRpcServiceLocalClient(parser)); List waitingForServices; - auto clientId = serviceClient->Connect( + serviceClient->Connect( channelServer, serviceClient, taskQueue, @@ -350,7 +332,6 @@ void HostLocalService( waitingForServices); auto rpcClient = serviceClient->GetDispatcher(); - InitializeRpcForModule(rpcClient, clientId); auto lifecycle = rpcClient->GetRpcLifecycle(); auto typeId = lifecycle->GetTypeIdFromName(L"example::IExampleService"); CHECK_ERROR(typeId != RpcTypeId_NotFound, L"Unknown RPC service type."); @@ -366,34 +347,40 @@ A remote client uses JsonNetworkChannelClient over a raw transport such as **vl: ```C++ class JsonRpcNetworkEndpoint : public JsonRpcNetworkClient { +private: + Ptr dispatcher; + public: JsonRpcNetworkEndpoint( + Ptr _dispatcher, Ptr transport, Ptr parser) : JsonRpcNetworkClient(transport, parser) + , dispatcher(_dispatcher) { } - void OnConnected(vint) override + void OnConnected(vint clientId) override { + CHECK_ERROR(dispatcher, L"The RPC dispatcher client is missing."); + dispatcher->InitializeRpc(clientId); } }; -Ptr ConnectRemoteRpcClient( +Ptr ConnectRemoteRpcClient( Ptr parser, Ptr taskQueue, const List& waitingForServices) { - auto dispatcher = Ptr(new RpcJsonDispatcherClientForTaskQueue(taskQueue)); + auto dispatcher = Ptr(new MyRpcDispatcherClient(taskQueue)); auto transport = Ptr(new named_pipe::NamedPipeClient(L"WorkflowRpcPipe")); - auto channelClient = Ptr(new JsonRpcNetworkEndpoint(transport, parser)); + auto channelClient = Ptr(new JsonRpcNetworkEndpoint(dispatcher, transport, parser)); dispatcher->WaitForServer( channelClient.Obj(), channelClient->GetRpcChannel(), waitingForServices); - InitializeRpcForModule(dispatcher.Obj(), channelClient->GetClientId()); dispatcher->Initialize(); return dispatcher; }