diff --git a/.github/Guidelines/Coding_MultiThreading.md b/.github/Guidelines/Coding_MultiThreading.md index fb1f77c5..c23ef6a3 100644 --- a/.github/Guidelines/Coding_MultiThreading.md +++ b/.github/Guidelines/Coding_MultiThreading.md @@ -45,3 +45,14 @@ - `QueueExitTask` does not cancel any pending `QueueTask`, all queued task will be executed. `ThreadVariable` can be used on global variables, different threads see different copy even when using the same `ThreadVariable` global variables. + +## Working with GacUI + +`GetCurrentController()->AsyncService()` and `GetApplicat5ion()` could communicate between UI threads and other threads: +- `InvokeInMainThread`: Push a task and it will be executed later in a UI thread. +- `InvokeInMainThreadAndWait`: Same as above but it blocks until the task is executed. DO NOT call this in a UI thread. +- `InvokeAsync`: Execute a task in a random non-UI thread. + +Non-UI thread could use `InvokeInMainThread` to run something that is required to be in a UI thread, e.g., access any GacUI objects. The whole GacUI is no thread-safe and everything should be touched in a UI thread, unless explicitly marked as thread-safe in the comment around the declaration. + +`InvokeInMainThread` could be used to raise an exception in a UI thread. DO NOT use `InvokeInMainThreadAndWait` for doing this. When running GacUI with remote protocol, this is an efficient way to create a fatal error. diff --git a/.github/KnowledgeBase/KB_GacUI_Design_RemoteProtocolRendererAndSerialization.md b/.github/KnowledgeBase/KB_GacUI_Design_RemoteProtocolRendererAndSerialization.md index be682eaf..c37e7c02 100644 --- a/.github/KnowledgeBase/KB_GacUI_Design_RemoteProtocolRendererAndSerialization.md +++ b/.github/KnowledgeBase/KB_GacUI_Design_RemoteProtocolRendererAndSerialization.md @@ -123,6 +123,8 @@ Protocol types are code-generated from `Protocol/*.txt` files into `GuiRemotePro Two projects in `Test/GacUISrc/` demonstrate a full remote protocol deployment. They are paired: one is the core side (console application) and the other is the renderer side (Windows application). +RemoteViewModelTest-specific remoting support is organized in `Test/RemotingHelpers/Rvmt/`. `ViewModelShared.*` owns the fixed RVM constants and concrete generated-RPC dispatcher initialization, `ViewModelHostClient.*` owns only the network host client, and `ViewModelHostServer.*` owns the protected `RpcServerHelpers` implementation and the application-facing `RemoteViewModelChannelServer`. The consolidated `Source_RemotingHelpers.vcxitems` inventory lists these files under its `Rvmt` filter and is imported only by remoting applications; standalone applications receive reusable automation endpoints through GacUI Core instead. + ### RemotingTest_Core (Console Application) Located at `Test/GacUISrc/RemotingTest_Core/`. Accepts `/Pipe` or `/Http` arguments to start either a named-pipe server or HTTP server. diff --git a/.github/KnowledgeBase/KB_Workflow_JsonRequestRouting.md b/.github/KnowledgeBase/KB_Workflow_JsonRequestRouting.md index 6e2c513a..63d7a250 100644 --- a/.github/KnowledgeBase/KB_Workflow_JsonRequestRouting.md +++ b/.github/KnowledgeBase/KB_Workflow_JsonRequestRouting.md @@ -39,7 +39,7 @@ Endpoint startup then happens through the reusable dispatcher client. A network ### Implementation -`IRpcJsonMessageDispatcher` is the transport boundary. `AllocateRequestId()` provides request ids for JSON envelopes, and `OnJsonRequest(message, requestType)` sends a direct, broadcast, or broadcast-and-drop request through the transport. `IRpcJsonMessageDispatcher::DefaultTranslate` is the receiver-side helper that routes JSON envelopes to local object ops, object event ops, or lifecycle service declaration handling. +`IRpcJsonMessageDispatcher` is the transport boundary. `AllocateRequestId()` provides request ids for JSON envelopes, `OnJsonRequest(message, requestType)` sends a direct, broadcast, or broadcast-and-drop request through the transport, and `InjectException(message)` persistently poisons dispatcher-controlled request execution. Injection is last-write-wins, an empty message is valid, and every later `OnJsonRequest` throws `RpcInjectedException` before sending. A transport-owned response wait must wake when injection occurs and linearize injection against response commitment under the same lock. `IRpcJsonMessageDispatcher::DefaultTranslate` is the receiver-side helper that routes JSON envelopes to local object ops, object event ops, or lifecycle service declaration handling. `RpcJsonObjectOps` and `RpcJsonObjectEventOps` adapt generated JSON ops to the shared envelopes declared by `Release/Rpc.d.ts`. They build request objects on the caller side, validate matching responses, and translate received requests back to `IRpcObjectOps` or `IRpcObjectEventOps`. @@ -49,6 +49,8 @@ Endpoint startup then happens through the reusable dispatcher client. A network `WfLibraryRpcJsonDispatcherClient` owns endpoint-side channel details that are not part of the generic RPC lifecycle: nested request processing while waiting for a response, response buffering by request id, pre-initialization service declaration caching, required-service waiting, and server-coordinator login/logout messages. +The channel client stores injected failure state with its received messages and buffered responses under one `CriticalSection`. Its `ConditionVariable` wakes response and startup waits. Waiters always test injection before messages or successful startup predicates; a matching response is committed under the same lock, so an earlier injection wins while an earlier response commitment may return normally. Injection does not asynchronously throw through a transport send or arbitrary service code; implementations check it at request entry and every dispatcher-controlled checkpoint. + `WfLibraryRpcJsonDispatcherServer` owns coordinator-side channel details: connected client tracking, broadcast request redirection, expected response tracking, response consolidation, service declaration replay to future clients, and client disconnect cleanup. The task-queue subclasses keep scheduling policy outside the core translation helpers. Every JSON RPC envelope has: @@ -204,3 +206,5 @@ A request kind mismatch is an error: object ops are direct, object events are br Only `Request:IRpcDispatcher_DeclareRemoteService` may be accepted before lifecycle initialization. Other RPC messages before initialization indicate a startup-order violation. Broadcast-and-drop requests must not produce responses. A client waiting for a response to this request will deadlock a correct implementation. + +`InjectException` is persistent rather than consumable. Custom dispatchers must keep a separate presence flag so an empty message remains distinguishable from no injection, replace the stored message on later injections, wake all dispatcher-owned waits, and throw `RpcInjectedException` on the original `OnJsonRequest` caller thread. A synchronous bridge that does not own a wait checks before dispatch and again before committing its result; it is not required to interrupt arbitrary code already executing. diff --git a/.github/KnowledgeBase/Learning.md b/.github/KnowledgeBase/Learning.md index 2fe8aa88..c92f24a8 100644 --- a/.github/KnowledgeBase/Learning.md +++ b/.github/KnowledgeBase/Learning.md @@ -6,7 +6,7 @@ - Verify generated artifacts with downstream consumer checks [19] - Crash early instead of adding error-tolerance fallbacks [14] - Port fixes from imports to source repositories [14] -- Proactively remove code made redundant by refactoring [13] +- Proactively remove code made redundant by refactoring [14] - Keep design documentation aligned with code after refactoring [12] - Fix behavior at the owning state instead of patching symptoms [10] - Extract abstractions only for real shared behavior [9] @@ -47,6 +47,7 @@ - Keep generated makefiles platform-invariant [1] - Group non-template C++ implementations by class in `.cpp` files [1] - Use reentrant POSIX date-time conversions [1] +- `IRpcJsonMessageDispatcher::InjectException` is persistent and last-write-wins [1] # Refinements @@ -329,3 +330,7 @@ Do not embed host-preprocessed `clang++ -MM` dependency output in tracked makefi ## Group non-template C++ implementations by class in `.cpp` files Move non-template method bodies out of headers into the matching `.cpp` file while leaving template definitions in headers. In each implementation file, keep a complete class definition and all methods belonging to that class together, and separate class groups with the repository's block-comment pattern. Place namespace-level forward declarations, variables, helper structs and other non-class items before the class groups. Apply the same organization consistently to platform-specific implementation files. + +## `IRpcJsonMessageDispatcher::InjectException` is persistent and last-write-wins + +Treat an injected RPC dispatcher exception as durable terminal state, separate from ordinary response messages. Serialize injection with response commitment, let the newest injected message replace the previous one, wake response and startup waits, and throw `RpcInjectedException` on the original caller thread at every dispatcher-controlled checkpoint. Do not consume or clear the failure after one request. diff --git a/.github/KnowledgeBase/manual/gacui/coding-agent/automation-service.md b/.github/KnowledgeBase/manual/gacui/coding-agent/automation-service.md index 21602ffa..6556b1a8 100644 --- a/.github/KnowledgeBase/manual/gacui/coding-agent/automation-service.md +++ b/.github/KnowledgeBase/manual/gacui/coding-agent/automation-service.md @@ -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`. The test-support implementation lives in `Test/RemotingHelpers/AutomationService/Windows`, outside the ordinary `GacUI.Windows` library pair. Test applications consume it through the shared `Source_RemotingHelpers` project. +`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`. 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. @@ -38,7 +38,7 @@ Each application owns the automation service and endpoint directly. After the se A normal Windows application can start the service before `GetApplication()->Run`: ```c++ -#include "../../RemotingHelpers/AutomationService/Windows/WindowsAutomationService.Windows.h" +#include "../../../Source/Utilities/AutomationService/Windows/WindowsAutomationService.Windows.h" using namespace vl; using namespace vl::presentation; @@ -123,4 +123,3 @@ The Windows HTTP wrapper is only one endpoint layer. Other platforms may expose Application-level automation depends on the GacUI UI thread. A native crash dialog, file dialog, or other modal native window can block the UI thread and keep the HTTP endpoint from answering. In that situation, inspect and operate native windows from another process using Win32 APIs, then return to the automation endpoint after the modal window is closed. Do not use operating-system UI Automation as the fallback for GacUI automation on Windows. It can fail when the screen is locked, and it is not the contract implemented by `AutomationService`. - diff --git a/.github/KnowledgeBase/manual/workflow/rpc.md b/.github/KnowledgeBase/manual/workflow/rpc.md index 152f0663..558ec444 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 and classifies them as direct, broadcast, or broadcast-and-drop messages. +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 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 0c0ee21e..552ed056 100644 --- a/.github/KnowledgeBase/manual/workflow/rpc/json-channel.md +++ b/.github/KnowledgeBase/manual/workflow/rpc/json-channel.md @@ -6,10 +6,13 @@ Use this page when wiring a JSON RPC endpoint to a transport. Implement IRpcJson ## IRpcJsonMessageDispatcher Contract -IRpcJsonMessageDispatcher has two runtime responsibilities: +IRpcJsonMessageDispatcher has three 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. @@ -31,6 +34,18 @@ 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 @@ -38,8 +53,24 @@ 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: