diff --git a/IntelPresentMon/AppCef/CMakeLists.txt b/IntelPresentMon/AppCef/CMakeLists.txt
index b2d4e3282..9cf00b4ee 100644
--- a/IntelPresentMon/AppCef/CMakeLists.txt
+++ b/IntelPresentMon/AppCef/CMakeLists.txt
@@ -15,6 +15,8 @@ add_executable(
source/util/FontEnumerator.cpp
source/util/IpcInvocationManager.cpp
source/util/KernelActionRegistration.cpp
+ source/util/KernelLink.cpp
+ source/util/KernelRelay.cpp
source/util/LogSetup.cpp
source/util/PathSanitaryCheck.cpp
source/util/SignalManager.cpp
diff --git a/IntelPresentMon/AppCef/CefNano.vcxproj b/IntelPresentMon/AppCef/CefNano.vcxproj
index 5cd52fa55..1df05bbb0 100644
--- a/IntelPresentMon/AppCef/CefNano.vcxproj
+++ b/IntelPresentMon/AppCef/CefNano.vcxproj
@@ -25,6 +25,8 @@
+
+
@@ -52,6 +54,8 @@
+
+
diff --git a/IntelPresentMon/AppCef/source/DataBindAccessor.cpp b/IntelPresentMon/AppCef/source/DataBindAccessor.cpp
index 296b69535..6f9ef8f41 100644
--- a/IntelPresentMon/AppCef/source/DataBindAccessor.cpp
+++ b/IntelPresentMon/AppCef/source/DataBindAccessor.cpp
@@ -2,16 +2,6 @@
// SPDX-License-Identifier: MIT
#include "DataBindAccessor.h"
#include "util/Logging.h"
-#include
-#include
-#include
-#include "util/CefValues.h"
-#include
-#include "util/cact/TargetLostAction.h"
-#include "util/cact/OverlayDiedAction.h"
-#include "util/cact/PresentmonInitFailedAction.h"
-#include "util/cact/StalePidAction.h"
-#include "util/SignalManager.h"
namespace p2c::client::cef
diff --git a/IntelPresentMon/AppCef/source/NanoCefBrowserClient.cpp b/IntelPresentMon/AppCef/source/NanoCefBrowserClient.cpp
index 070beb83a..2a469f394 100644
--- a/IntelPresentMon/AppCef/source/NanoCefBrowserClient.cpp
+++ b/IntelPresentMon/AppCef/source/NanoCefBrowserClient.cpp
@@ -8,6 +8,7 @@
#include
#include "util/AsyncEndpointManager.h"
#include "NanoCefProcessHandler.h"
+#include "util/KernelRelay.h"
// implemented in winmain.cpp
void AppQuitMessageLoop();
@@ -35,6 +36,11 @@ namespace p2c::client::cef
pContextMenuHandler = new NullMenuHandler{};
}
+ void NanoCefBrowserClient::SetKernelCallRelay(util::kernel_relay::CallRelay* pRelay)
+ {
+ pKernelCallRelay = pRelay;
+ }
+
CefRefPtr NanoCefBrowserClient::GetBrowser()
{
return pBrowser;
@@ -66,6 +72,9 @@ namespace p2c::client::cef
bool NanoCefBrowserClient::OnProcessMessageReceived(CefRefPtr browser, CefRefPtr frame, CefProcessId source_process, CefRefPtr message)
{
+ if (pKernelCallRelay && pKernelCallRelay->HandleMessage(*message, frame)) {
+ return true;
+ }
if (message->GetName() == util::AsyncEndpointManager::GetDispatchMessageName())
{
if (auto pEndpoint = endpoints.Find(message->GetArgumentList()->GetString(0)))
diff --git a/IntelPresentMon/AppCef/source/NanoCefBrowserClient.h b/IntelPresentMon/AppCef/source/NanoCefBrowserClient.h
index cfa0b5f87..a6c5f098f 100644
--- a/IntelPresentMon/AppCef/source/NanoCefBrowserClient.h
+++ b/IntelPresentMon/AppCef/source/NanoCefBrowserClient.h
@@ -8,6 +8,10 @@
#include
#include "util/AsyncEndpointCollection.h"
+namespace p2c::client::util::kernel_relay
+{
+ class CallRelay;
+}
namespace p2c::client::cef
{
@@ -18,6 +22,9 @@ namespace p2c::client::cef
{
public:
NanoCefBrowserClient();
+ // Kernel calls from the page are handed to this relay. Set before the browser is created,
+ // since only the page sends them; null leaves them unhandled.
+ void SetKernelCallRelay(util::kernel_relay::CallRelay* pRelay);
CefRefPtr GetBrowser();
CefRefPtr GetLifeSpanHandler() override;
CefRefPtr GetDisplayHandler() override;
@@ -40,6 +47,7 @@ namespace p2c::client::cef
CefRefPtr pContextMenuHandler;
CefRefPtr pBrowser;
util::AsyncEndpointCollection endpoints;
+ util::kernel_relay::CallRelay* pKernelCallRelay = nullptr;
// Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(NanoCefBrowserClient);
diff --git a/IntelPresentMon/AppCef/source/NanoCefProcessHandler.cpp b/IntelPresentMon/AppCef/source/NanoCefProcessHandler.cpp
index 1c8f340a0..ca05ab8ae 100644
--- a/IntelPresentMon/AppCef/source/NanoCefProcessHandler.cpp
+++ b/IntelPresentMon/AppCef/source/NanoCefProcessHandler.cpp
@@ -14,14 +14,26 @@
#include "util/AsyncEndpointManager.h"
#include "util/CefValues.h"
#include "util/CliOptions.h"
+#include "util/KernelRelay.h"
+#include
using namespace pmon::util;
-using namespace std::chrono_literals;
namespace rn = std::ranges;
namespace vi = std::views;
namespace p2c::client::cef
{
+ void NanoCefProcessHandler::OnBeforeCommandLineProcessing(const CefString& processType, CefRefPtr pCommandLine)
+ {
+ // An elevated kernel launches the UI at medium integrity with a token restricted from its
+ // own. Chromium reads that token as elevated, relaunches itself without the kernel pipe,
+ // and exits. Opt out only when we really are below high integrity, so a UI that did run
+ // elevated is still de-elevated by Chromium.
+ if (processType.empty() && ::pmon::util::win::WeAreBelowHighIntegrity()) {
+ pCommandLine->AppendSwitch("do-not-de-elevate");
+ }
+ }
+
void NanoCefProcessHandler::OnContextInitialized()
{
#ifdef NDEBUG
@@ -112,11 +124,14 @@ namespace p2c::client::cef
void NanoCefProcessHandler::OnContextCreated(CefRefPtr browser, CefRefPtr frame, CefRefPtr context)
{
- std::this_thread::sleep_for(50ms);
- pKernelWrapper->pClient = std::make_unique(
- *util::cli::Options::Get().actName, util::cact::CefExecutionContext{.pSignalManager = &pKernelWrapper->signals}
- );
- pKernelWrapper->pInvocationManager = std::make_unique(*pKernelWrapper->pClient);
+ // Only the main frame gets the kernel bridge. A subframe's context must not replace the
+ // invocation manager, which would drop the main frame's pending calls, and it has no
+ // business calling the kernel. A main-frame reload does start fresh: its old page is gone.
+ if (!frame->IsMain()) {
+ return;
+ }
+ // kernel calls are relayed through the browser process, which holds the kernel connection
+ pKernelWrapper->pInvocationManager = std::make_unique();
pAccessor = new DataBindAccessor{ pBrowser, pKernelWrapper.get() };
auto core = CefV8Value::CreateObject(nullptr, nullptr);
@@ -138,11 +153,33 @@ namespace p2c::client::cef
pBrowser = browser_;
+ // versions the kernel reported to the browser process when it opened its session
+ if (extra_info) {
+ namespace kr = util::kernel_relay;
+ auto& versions = pKernelWrapper->kernelVersions;
+ versions.serviceBuildId = extra_info->GetString(kr::kServiceBuildIdKey).ToString();
+ versions.serviceBuildTime = extra_info->GetString(kr::kServiceBuildTimeKey).ToString();
+ versions.serviceVersion = extra_info->GetString(kr::kServiceVersionKey).ToString();
+ versions.middlewareApiVersion = extra_info->GetString(kr::kMiddlewareApiVersionKey).ToString();
+ }
+
CefRenderProcessHandler::OnBrowserCreated(std::move(browser_), std::move(extra_info));
}
bool NanoCefProcessHandler::OnProcessMessageReceived(CefRefPtr browser, CefRefPtr frame, CefProcessId source_process, CefRefPtr message)
{
+ if (pKernelWrapper->pInvocationManager && pKernelWrapper->pInvocationManager->HandleReply(*message)) {
+ return true;
+ }
+ if (message->GetName() == util::kernel_relay::kSignalMessage) {
+ const auto args = message->GetArgumentList();
+ std::optional arg;
+ if (args->GetType(1) == VTYPE_INT) {
+ arg = (uint32_t)args->GetInt(1);
+ }
+ pKernelWrapper->signals.SignalFromBrowser(args->GetString(0).ToString(), arg);
+ return true;
+ }
if (message->GetName() == util::AsyncEndpointManager::GetResolveMessageName())
{
pAccessor->ResolveAsyncEndpoint(
diff --git a/IntelPresentMon/AppCef/source/NanoCefProcessHandler.h b/IntelPresentMon/AppCef/source/NanoCefProcessHandler.h
index 82da332ad..c5fee62d2 100644
--- a/IntelPresentMon/AppCef/source/NanoCefProcessHandler.h
+++ b/IntelPresentMon/AppCef/source/NanoCefProcessHandler.h
@@ -15,6 +15,7 @@ namespace p2c::client::cef
public CefBrowserProcessHandler
{
public:
+ void OnBeforeCommandLineProcessing(const CefString& processType, CefRefPtr pCommandLine) override;
void OnContextInitialized() override;
CefRefPtr GetBrowserProcessHandler() override;
CefRefPtr GetRenderProcessHandler() override;
diff --git a/IntelPresentMon/AppCef/source/util/ActionClientServer.cpp b/IntelPresentMon/AppCef/source/util/ActionClientServer.cpp
index f3f9d7c5e..b020f429a 100644
--- a/IntelPresentMon/AppCef/source/util/ActionClientServer.cpp
+++ b/IntelPresentMon/AppCef/source/util/ActionClientServer.cpp
@@ -3,9 +3,9 @@
namespace p2c::client::util
{
- CefClient::CefClient(std::string pipeName, cact::CefExecutionContext context)
+ CefClient::CefClient(::pmon::util::win::Handle connectedPipe, cact::CefExecutionContext context)
:
- SymmetricActionClient{ std::move(pipeName), std::move(context) }
+ SymmetricActionClient{ std::move(connectedPipe), "kernel-ui", std::move(context) }
{
auto res = DispatchSync(kproc::kact::OpenSession::Params{ GetCurrentProcessId() });
serviceBuildId_ = std::move(res.serviceBuildId);
diff --git a/IntelPresentMon/AppCef/source/util/ActionClientServer.h b/IntelPresentMon/AppCef/source/util/ActionClientServer.h
index 489ebb534..aca6423eb 100644
--- a/IntelPresentMon/AppCef/source/util/ActionClientServer.h
+++ b/IntelPresentMon/AppCef/source/util/ActionClientServer.h
@@ -8,7 +8,8 @@ namespace p2c::client::util
class CefClient : public act::SymmetricActionClient {
public:
- CefClient(std::string pipeName, cact::CefExecutionContext context = {});
+ // connectedPipe: the client end of the pipe pair the kernel created for this UI
+ CefClient(::pmon::util::win::Handle connectedPipe, cact::CefExecutionContext context);
const std::string& GetServiceBuildId() const;
const std::string& GetServiceBuildTime() const;
const std::string& GetServiceVersion() const;
diff --git a/IntelPresentMon/AppCef/source/util/CliOptions.h b/IntelPresentMon/AppCef/source/util/CliOptions.h
index 8b686c6f3..4e4cc039e 100644
--- a/IntelPresentMon/AppCef/source/util/CliOptions.h
+++ b/IntelPresentMon/AppCef/source/util/CliOptions.h
@@ -64,14 +64,15 @@ namespace p2c::client::util::cli
private: Group gi_{ this, "Internal", "Internal options, do not supply manually" }; public:
Option cefType{ this, "--type", "", "Type of the current chromium process" };
Option logPipeName{ this, "--p2c-log-pipe-name", "", "The postfix used to create the named pipe for logging source server" };
- Option actName{ this, "--p2c-act-name", "", "Base name for the named pipes connecting CEF and the overlay Kernel process" };
+ Option actHandle{ this, "--p2c-act-handle", 0, "Inherited handle of the pipe connecting CEF and the overlay Kernel process" };
static constexpr const char* description = "PresentMon control UI";
static constexpr const char* name = "PresentMonUI.exe";
private:
MutualExclusion excl_{ logDenyList, logAllowList };
- NoForward noForward_{ cefType, logPipeName, logVerboseModules };
+ // actHandle is meaningful only in the browser process that inherited it
+ NoForward noForward_{ cefType, logPipeName, logVerboseModules, actHandle };
AllowExtras ext_{ this };
};
}
diff --git a/IntelPresentMon/AppCef/source/util/IpcInvocationManager.cpp b/IntelPresentMon/AppCef/source/util/IpcInvocationManager.cpp
index 0a8fe1eee..5600dde8e 100644
--- a/IntelPresentMon/AppCef/source/util/IpcInvocationManager.cpp
+++ b/IntelPresentMon/AppCef/source/util/IpcInvocationManager.cpp
@@ -1,9 +1,46 @@
-#pragma once
#include "IpcInvocationManager.h"
namespace p2c::client::util
{
- std::unordered_map, CefClient&)>>
+ std::unordered_map)>>
IpcInvocationManager::dispatchBindings_;
+ int IpcInvocationManager::nextCallId_ = 0;
+
+ bool IpcInvocationManager::HandleReply(CefProcessMessage& message)
+ {
+ CEF_REQUIRE_RENDERER_THREAD();
+ if (message.GetName() != kernel_relay::kReplyMessage) {
+ return false;
+ }
+ const auto args = message.GetArgumentList();
+ const auto callId = args->GetInt(0);
+ const auto i = pendingCalls_.find(callId);
+ if (i == pendingCalls_.end()) {
+ pmlog_dbg("Dropping kernel call reply for an unknown call").pmwatch(callId);
+ return true;
+ }
+ auto onReply = std::move(i->second);
+ pendingCalls_.erase(i);
+ // no binary value means the Response serialized to no bytes
+ onReply(args->GetBool(1), args->GetType(2) == VTYPE_BINARY ? args->GetBinary(2) : nullptr);
+ return true;
+ }
+
+ void IpcInvocationManager::SendCall_(const std::string& action, CefRefPtr pParams,
+ CefRefPtr frame, ReplyHandler onReply)
+ {
+ CEF_REQUIRE_RENDERER_THREAD();
+ const auto callId = nextCallId_++;
+ auto message = CefProcessMessage::Create(kernel_relay::kCallMessage);
+ const auto args = message->GetArgumentList();
+ args->SetString(0, action);
+ args->SetInt(1, callId);
+ // null when Params serialized to no bytes, see kernel_relay::ToBinary
+ if (pParams) {
+ args->SetBinary(2, std::move(pParams));
+ }
+ pendingCalls_.emplace(callId, std::move(onReply));
+ frame->SendProcessMessage(PID_BROWSER, std::move(message));
+ }
}
\ No newline at end of file
diff --git a/IntelPresentMon/AppCef/source/util/IpcInvocationManager.h b/IntelPresentMon/AppCef/source/util/IpcInvocationManager.h
index 24744d5fe..5b7e13e04 100644
--- a/IntelPresentMon/AppCef/source/util/IpcInvocationManager.h
+++ b/IntelPresentMon/AppCef/source/util/IpcInvocationManager.h
@@ -4,27 +4,24 @@
#include
#include
#include
-#include
-#include
-#include
#include
#include
#include
-#include
-#include
-#include
#include "AsyncEndpointCollection.h"
-#include "ActionClientServer.h"
+#include "KernelRelay.h"
#include "V8Transfer.h"
#include
namespace p2c::client::util
{
- // this class exists to:
+ // Renderer: the page's calls to kernel actions. This class exists to:
// A) bridge from key string to action
// B) encode conversion from V8 to Params{}
// C) encode conversion from Response{} to V8
+ // The browser process owns the connection to the kernel, so each call is sent there as a
+ // kernel_relay::kCallMessage and resolved when its kReplyMessage comes back. Everything
+ // here runs on the renderer thread, so nothing is locked.
class IpcInvocationManager
{
public:
@@ -39,12 +36,14 @@ namespace p2c::client::util
void DispatchInvocation(const std::string& key, CallbackContext&& ctx, CefRefPtr pArgs)
{
// TODO: explicit lookup and handle missing error
- dispatchBindings_[key](std::move(ctx), std::move(pArgs), client_);
+ dispatchBindings_[key](*this, std::move(ctx), std::move(pArgs));
}
bool HasHandler(const std::string& key) const
{
return dispatchBindings_.contains(key);
}
+ // Handles a kernel_relay::kReplyMessage. False for any other message.
+ bool HandleReply(CefProcessMessage& message);
template
static void RegisterDispatchBinding()
{
@@ -52,30 +51,40 @@ namespace p2c::client::util
using Response = typename A::Response;
// register handler function for the endpoint identifer (e.g. SetCapture) that takes in V8 context and V8 action args
// this handler will be called when the DBA receives an Execute call on the invokeEndpoint channel
- dispatchBindings_[A::Identifier] = [](CallbackContext ctx, CefRefPtr pObj, CefClient& client) {
- // convert the V8 args to the Params struct associated with the action A
- // create a lambda which accepts the incoming result of this action and stores the V8 context
- // lamba will be invoked after the response is received from the server, causing the response
- // to be returned to the original V8 caller in js land
- client.DispatchWithContinuation(FromV8(*pObj), [ctx = std::move(ctx)](Response&& res, std::exception_ptr pex) {
- CefPostTask(TID_RENDERER, base::BindOnce(
- &IpcInvocationManager::ResolveInvocation_,
- std::move(ctx), std::move(res), std::move(pex)
- ));
- });
+ dispatchBindings_[A::Identifier] = [](IpcInvocationManager& manager, CallbackContext ctx, CefRefPtr pObj) {
+ // the call goes out from the frame that made it, so the reply comes back to it
+ auto frame = ctx.pV8Context->GetFrame();
+ manager.SendCall_(A::Identifier, kernel_relay::ToBinary(FromV8(*pObj)), std::move(frame),
+ [ctx = std::move(ctx)](bool succeeded, CefRefPtr pResponse) {
+ // a failed call reaches the page as a rejection
+ if (succeeded) {
+ try {
+ ResolveInvocation_(ctx, kernel_relay::FromBinary(pResponse.get()), true);
+ return;
+ }
+ catch (...) {
+ pmlog_error(::pmon::util::ReportException("Malformed kernel call reply"));
+ }
+ }
+ ResolveInvocation_(ctx, Response{}, false);
+ });
};
}
- IpcInvocationManager(CefClient& client) : client_{ client } {}
private:
- // function
+ // types
+ // succeeded, and the Response payload (null when it serialized to no bytes)
+ using ReplyHandler = std::function)>;
+ // functions
+ void SendCall_(const std::string& action, CefRefPtr pParams, CefRefPtr frame,
+ ReplyHandler onReply);
template
- static void ResolveInvocation_(CallbackContext ctx, Response res, std::exception_ptr pex)
+ static void ResolveInvocation_(const CallbackContext& ctx, const Response& res, bool succeeded)
{
CEF_REQUIRE_RENDERER_THREAD();
// TODO: give error details in the failure path
ctx.pV8Context->Enter();
- if (!pex) {
+ if (succeeded) {
ctx.pV8ResolveFunction->ExecuteFunction(nullptr, { ToV8(res) });
}
else {
@@ -84,8 +93,11 @@ namespace p2c::client::util
ctx.pV8Context->Exit();
}
// data
- static std::unordered_map, CefClient&)>> dispatchBindings_;
- CefClient& client_;
+ static std::unordered_map)>> dispatchBindings_;
+ // unique across the renderer process, so a reply meant for a page that has since
+ // reloaded cannot match a call made by the new one
+ static int nextCallId_;
+ std::unordered_map pendingCalls_;
};
template<>
diff --git a/IntelPresentMon/AppCef/source/util/KernelActionRegistration.cpp b/IntelPresentMon/AppCef/source/util/KernelActionRegistration.cpp
index 48b968b38..f7d2b9a8e 100644
--- a/IntelPresentMon/AppCef/source/util/KernelActionRegistration.cpp
+++ b/IntelPresentMon/AppCef/source/util/KernelActionRegistration.cpp
@@ -1,9 +1,16 @@
#include "IpcInvocationManager.h"
+#include "KernelRelay.h"
#include "../../../KernelProcess/kact/AllActions.h"
namespace p2c::client::util::kact {
+ // registers both halves of a kernel action: the renderer binding called from the page,
+ // and the browser-side relay that forwards it to the kernel
template
struct IpcActionRegistrator {
- IpcActionRegistrator() { ::p2c::client::util::IpcInvocationManager::RegisterDispatchBinding(); }
+ IpcActionRegistrator()
+ {
+ ::p2c::client::util::IpcInvocationManager::RegisterDispatchBinding();
+ ::p2c::client::util::kernel_relay::CallRelay::RegisterAction();
+ }
};
IpcActionRegistrator reg_ibind_Introspect_;
diff --git a/IntelPresentMon/AppCef/source/util/KernelLink.cpp b/IntelPresentMon/AppCef/source/util/KernelLink.cpp
new file mode 100644
index 000000000..faa63fa0e
--- /dev/null
+++ b/IntelPresentMon/AppCef/source/util/KernelLink.cpp
@@ -0,0 +1,59 @@
+#include "KernelLink.h"
+#include
+
+namespace p2c::client::util
+{
+ ::pmon::util::win::Handle TakeKernelPipeHandle(uint64_t pipeHandleValue)
+ {
+ if (!pipeHandleValue) {
+ pmlog_error("No kernel pipe handle was passed; the UI has no kernel link");
+ return {};
+ }
+ ::pmon::util::win::Handle pipe{ reinterpret_cast((uintptr_t)pipeHandleValue) };
+ // an inherited handle stays inheritable; it must never reach a process this one launches
+ if (!SetHandleInformation(pipe, HANDLE_FLAG_INHERIT, 0)) {
+ pmlog_error("Failed to clear inheritance on the kernel pipe handle; the UI has no kernel link").hr();
+ return {};
+ }
+ return pipe;
+ }
+
+ KernelLink::KernelLink(::pmon::util::win::Handle pipe, kernel_relay::SignalRelay::BrowserGetter getBrowser)
+ :
+ signals_{ getBrowser },
+ pClient_{ OpenSession_(std::move(pipe), signals_) },
+ calls_{ pClient_.get() }
+ {}
+
+ kernel_relay::CallRelay& KernelLink::GetCallRelay()
+ {
+ return calls_;
+ }
+
+ CefRefPtr KernelLink::MakeBrowserExtraInfo() const
+ {
+ const std::string unknown = "unknown";
+ auto info = CefDictionaryValue::Create();
+ info->SetString(kernel_relay::kServiceBuildIdKey, pClient_ ? pClient_->GetServiceBuildId() : unknown);
+ info->SetString(kernel_relay::kServiceBuildTimeKey, pClient_ ? pClient_->GetServiceBuildTime() : unknown);
+ info->SetString(kernel_relay::kServiceVersionKey, pClient_ ? pClient_->GetServiceVersion() : unknown);
+ info->SetString(kernel_relay::kMiddlewareApiVersionKey, pClient_ ? pClient_->GetMiddlewareApiVersion() : unknown);
+ return info;
+ }
+
+ std::unique_ptr KernelLink::OpenSession_(::pmon::util::win::Handle pipe, const kernel_relay::SignalRelay& signals)
+ {
+ if (!pipe) {
+ // TakeKernelPipeHandle has logged why
+ return {};
+ }
+ try {
+ return std::make_unique(std::move(pipe),
+ cact::CefExecutionContext{ .pSignalRelay = &signals });
+ }
+ catch (...) {
+ pmlog_error(::pmon::util::ReportException("Failed opening the kernel session; the UI has no kernel link"));
+ return {};
+ }
+ }
+}
diff --git a/IntelPresentMon/AppCef/source/util/KernelLink.h b/IntelPresentMon/AppCef/source/util/KernelLink.h
new file mode 100644
index 000000000..0f3d708df
--- /dev/null
+++ b/IntelPresentMon/AppCef/source/util/KernelLink.h
@@ -0,0 +1,32 @@
+#pragma once
+#include "KernelRelay.h"
+#include
+#include
+
+namespace p2c::client::util
+{
+ // Browser process: takes ownership of the kernel pipe handle passed on the command line and
+ // clears its inheritance. Call before CefInitialize, so no process CEF launches can inherit it.
+ // Returns an empty handle, and logs why, when the value is 0 or inheritance cannot be cleared;
+ // the UI then runs with no kernel link.
+ ::pmon::util::win::Handle TakeKernelPipeHandle(uint64_t pipeHandleValue);
+
+ // Browser process: the UI's one connection to the kernel. It holds the client end of the pipe
+ // pair the kernel created for this UI, relays the page's calls to the kernel, and forwards the
+ // kernel's events to the page. Lives from just after CefInitialize until just before CefShutdown.
+ class KernelLink
+ {
+ public:
+ // pipe comes from TakeKernelPipeHandle. If it is empty or the session cannot be opened, the
+ // UI still runs with no kernel link and calls are refused.
+ KernelLink(::pmon::util::win::Handle pipe, kernel_relay::SignalRelay::BrowserGetter getBrowser);
+ kernel_relay::CallRelay& GetCallRelay();
+ // service and middleware versions for the renderer, passed with browser creation
+ CefRefPtr MakeBrowserExtraInfo() const;
+ private:
+ static std::unique_ptr OpenSession_(::pmon::util::win::Handle pipe, const kernel_relay::SignalRelay& signals);
+ kernel_relay::SignalRelay signals_;
+ std::unique_ptr pClient_;
+ kernel_relay::CallRelay calls_;
+ };
+}
diff --git a/IntelPresentMon/AppCef/source/util/KernelRelay.cpp b/IntelPresentMon/AppCef/source/util/KernelRelay.cpp
new file mode 100644
index 000000000..c39ebaf1e
--- /dev/null
+++ b/IntelPresentMon/AppCef/source/util/KernelRelay.cpp
@@ -0,0 +1,88 @@
+#include "KernelRelay.h"
+#include
+#include
+#include
+#include
+
+namespace p2c::client::util::kernel_relay
+{
+ bool CallRelay::HandleMessage(CefProcessMessage& message, CefRefPtr frame)
+ {
+ if (message.GetName() != kCallMessage) {
+ return false;
+ }
+ const auto args = message.GetArgumentList();
+ const auto action = args->GetString(0).ToString();
+ const auto callId = args->GetInt(1);
+ if (!pClient_) {
+ pmlog_error("Kernel call refused, the UI has no kernel link").pmwatch(action);
+ PostReply_(frame, callId, false, nullptr);
+ return true;
+ }
+ const auto i = Forwarders_().find(action);
+ if (i == Forwarders_().end()) {
+ pmlog_error("Kernel call refused, unknown action").pmwatch(action);
+ PostReply_(frame, callId, false, nullptr);
+ return true;
+ }
+ // no binary value means Params serialized to no bytes
+ const auto pParams = args->GetType(2) == VTYPE_BINARY ? args->GetBinary(2) : nullptr;
+ try {
+ i->second(*pClient_, callId, pParams, frame);
+ }
+ catch (...) {
+ pmlog_error(::pmon::util::ReportException(std::format("Failed relaying kernel call [{}]", action)));
+ PostReply_(frame, callId, false, nullptr);
+ }
+ return true;
+ }
+
+ std::unordered_map& CallRelay::Forwarders_()
+ {
+ static std::unordered_map forwarders;
+ return forwarders;
+ }
+
+ void CallRelay::PostReply_(CefRefPtr frame, int callId, bool succeeded, CefRefPtr pResponse)
+ {
+ CefPostTask(TID_UI, base::BindOnce(&CallRelay::SendReply_, std::move(frame), callId, succeeded, std::move(pResponse)));
+ }
+
+ void CallRelay::SendReply_(CefRefPtr frame, int callId, bool succeeded, CefRefPtr pResponse)
+ {
+ // the page that made the call may have reloaded or closed since
+ if (!frame || !frame->IsValid()) {
+ pmlog_dbg("Dropping kernel call reply, its frame is gone").pmwatch(callId);
+ return;
+ }
+ auto message = CefProcessMessage::Create(kReplyMessage);
+ const auto args = message->GetArgumentList();
+ args->SetInt(0, callId);
+ args->SetBool(1, succeeded);
+ if (pResponse) {
+ args->SetBinary(2, std::move(pResponse));
+ }
+ frame->SendProcessMessage(PID_RENDERER, std::move(message));
+ }
+
+ void SignalRelay::Forward(const std::string& key, std::optional arg) const
+ {
+ CefPostTask(TID_UI, base::BindOnce(&SignalRelay::Send_, getBrowser_, key, arg.has_value(), arg.value_or(0)));
+ }
+
+ void SignalRelay::Send_(BrowserGetter getBrowser, std::string key, bool hasArg, uint32_t arg)
+ {
+ const auto browser = getBrowser();
+ if (!browser) {
+ pmlog_warn("Dropping kernel event, no browser yet").pmwatch(key);
+ return;
+ }
+ auto message = CefProcessMessage::Create(kSignalMessage);
+ const auto args = message->GetArgumentList();
+ args->SetString(0, key);
+ if (hasArg) {
+ args->SetInt(1, (int)arg);
+ }
+ browser->GetMainFrame()->SendProcessMessage(PID_RENDERER, std::move(message));
+ }
+}
diff --git a/IntelPresentMon/AppCef/source/util/KernelRelay.h b/IntelPresentMon/AppCef/source/util/KernelRelay.h
new file mode 100644
index 000000000..f0c12d112
--- /dev/null
+++ b/IntelPresentMon/AppCef/source/util/KernelRelay.h
@@ -0,0 +1,117 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "ActionClientServer.h"
+#include "Logging.h"
+
+// The browser process owns the pipe to the kernel; the page runs in a renderer process.
+// Kernel calls and kernel events cross between the two as these CEF process messages.
+namespace p2c::client::util::kernel_relay
+{
+ // Payloads are cereal bytes in a binary value. CEF has no empty binary value, so a payload
+ // that serializes to nothing (an action whose Params or Response has no fields) is sent as
+ // no value at that index; see ToBinary and FromBinary.
+ // renderer -> browser. 0: action identifier (string), 1: call id (int), 2: Params (binary)
+ inline constexpr const char* kCallMessage = "kern-call";
+ // browser -> renderer. 0: call id (int), 1: succeeded (bool), 2: Response (binary, on success)
+ inline constexpr const char* kReplyMessage = "kern-reply";
+ // browser -> renderer. 0: signal key (string), 1: argument (int, only for signals that take one)
+ inline constexpr const char* kSignalMessage = "kern-signal";
+ // keys of the browser-creation extra_info dictionary, which carries the versions the kernel
+ // reported when the browser process opened its session
+ inline constexpr const char* kServiceBuildIdKey = "serviceBuildId";
+ inline constexpr const char* kServiceBuildTimeKey = "serviceBuildTime";
+ inline constexpr const char* kServiceVersionKey = "serviceVersion";
+ inline constexpr const char* kMiddlewareApiVersionKey = "middlewareApiVersion";
+
+ // Params and Response types already serialize with cereal for the pipe; the relay reuses that.
+ // Null when value serializes to no bytes: CEF cannot hold an empty binary value.
+ template
+ CefRefPtr ToBinary(const T& value)
+ {
+ std::ostringstream stream;
+ {
+ cereal::BinaryOutputArchive archive{ stream };
+ archive(value);
+ }
+ const auto bytes = std::move(stream).str();
+ if (bytes.empty()) {
+ return nullptr;
+ }
+ return CefBinaryValue::Create(bytes.data(), bytes.size());
+ }
+ // pBinary may be null, meaning the value serialized to no bytes
+ template
+ T FromBinary(CefBinaryValue* pBinary)
+ {
+ std::string bytes;
+ if (pBinary) {
+ bytes.resize(pBinary->GetSize());
+ pBinary->GetData(bytes.data(), bytes.size(), 0);
+ }
+ std::istringstream stream{ std::move(bytes) };
+ cereal::BinaryInputArchive archive{ stream };
+ T value;
+ archive(value);
+ return value;
+ }
+
+ // Browser process: carries kernel calls from the renderer to the kernel, and the replies back
+ // to the frame that made the call.
+ class CallRelay
+ {
+ public:
+ // pClient is null when the UI has no kernel link; every call is then refused.
+ explicit CallRelay(CefClient* pClient) : pClient_{ pClient } {}
+ // Handles a kCallMessage from frame. False for any other message.
+ bool HandleMessage(CefProcessMessage& message, CefRefPtr frame);
+ // Registered once per kernel action, beside its renderer-side binding.
+ template
+ static void RegisterAction()
+ {
+ Forwarders_()[A::Identifier] = [](CefClient& client, int callId,
+ CefRefPtr pParams, CefRefPtr frame) {
+ client.DispatchWithContinuation(FromBinary(pParams.get()),
+ [callId, frame](typename A::Response&& res, std::exception_ptr pex) {
+ if (pex) {
+ PostReply_(frame, callId, false, nullptr);
+ }
+ else {
+ PostReply_(frame, callId, true, ToBinary(res));
+ }
+ });
+ };
+ }
+ private:
+ using Forwarder = std::function, CefRefPtr)>;
+ static std::unordered_map& Forwarders_();
+ // any thread; the reply is sent on the CEF UI thread. pResponse may be null (see ToBinary).
+ static void PostReply_(CefRefPtr frame, int callId, bool succeeded, CefRefPtr pResponse);
+ static void SendReply_(CefRefPtr frame, int callId, bool succeeded, CefRefPtr pResponse);
+ CefClient* pClient_;
+ };
+
+ // Browser process: forwards kernel events to the page. getBrowser runs on the CEF UI thread and
+ // returns the current browser, or null before it exists; events with no browser are dropped.
+ class SignalRelay
+ {
+ public:
+ using BrowserGetter = CefRefPtr(*)();
+ explicit SignalRelay(BrowserGetter getBrowser) : getBrowser_{ getBrowser } {}
+ // any thread
+ void Forward(const std::string& key, std::optional arg = {}) const;
+ private:
+ static void Send_(BrowserGetter getBrowser, std::string key, bool hasArg, uint32_t arg);
+ BrowserGetter getBrowser_;
+ };
+}
diff --git a/IntelPresentMon/AppCef/source/util/KernelWrapper.h b/IntelPresentMon/AppCef/source/util/KernelWrapper.h
index 874c86fb2..c57414947 100644
--- a/IntelPresentMon/AppCef/source/util/KernelWrapper.h
+++ b/IntelPresentMon/AppCef/source/util/KernelWrapper.h
@@ -2,8 +2,8 @@
#include
#include "SignalManager.h"
#include "AsyncEndpointManager.h"
-#include "ActionClientServer.h"
#include "IpcInvocationManager.h"
+#include
namespace p2c::client::util
@@ -12,7 +12,14 @@ namespace p2c::client::util
{
util::SignalManager signals;
util::AsyncEndpointManager asyncEndpoints;
- std::unique_ptr pClient;
std::unique_ptr pInvocationManager;
+ // versions the kernel reported to the browser process, received with browser creation
+ struct KernelVersions
+ {
+ std::string serviceBuildId = "unknown";
+ std::string serviceBuildTime = "unknown";
+ std::string serviceVersion = "unknown";
+ std::string middlewareApiVersion = "unknown";
+ } kernelVersions;
};
}
diff --git a/IntelPresentMon/AppCef/source/util/LogSetup.cpp b/IntelPresentMon/AppCef/source/util/LogSetup.cpp
index afc62bc6c..7966dea73 100644
--- a/IntelPresentMon/AppCef/source/util/LogSetup.cpp
+++ b/IntelPresentMon/AppCef/source/util/LogSetup.cpp
@@ -166,7 +166,7 @@ namespace p2c::client::util
try {
const auto fullPipeName = R"(\\.\pipe\)" + pipePrefix;
// wait maximum 1.5sec for pipe to be created
- if (!pipe::DuplexPipe::WaitForAvailability(fullPipeName, 1500, true)) {
+ if (!pipe::DuplexPipe::WaitForAvailability(fullPipeName, 1500)) {
pmlog_warn(std::format("Failed to connect to logging source server {} after waiting 1.5s", pipePrefix));
return;
}
diff --git a/IntelPresentMon/AppCef/source/util/SignalManager.cpp b/IntelPresentMon/AppCef/source/util/SignalManager.cpp
index a25ee56d2..ccb664af1 100644
--- a/IntelPresentMon/AppCef/source/util/SignalManager.cpp
+++ b/IntelPresentMon/AppCef/source/util/SignalManager.cpp
@@ -35,31 +35,17 @@ namespace p2c::client::util
}
}
- // List of Signal-firing functions (call from C++ to invoke a callback registered by JS side)
- //
- // targetLost(pid:number)
- void SignalManager::SignalTargetLost(uint32_t pid)
+ void SignalManager::SignalFromBrowser(const std::string& key, std::optional arg)
{
- ImplementSignal_(Keys::targetLost(), pid);
- }
- // hotkeyFired(action:number)
- void SignalManager::SignalHotkeyFired(uint32_t action)
- {
- ImplementSignal_(Keys::hotkeyFired(), action);
- }
- // presentmonInitFailed()
- void SignalManager::SignalPresentmonInitFailed()
- {
- ImplementSignal_(Keys::presentmonInitFailed());
- }
- // overlayDied()
- void SignalManager::SignalOverlayDied()
- {
- ImplementSignal_(Keys::overlayDied());
- }
- // stalePid()
- void SignalManager::SignalStalePid()
- {
- ImplementSignal_(Keys::stalePid());
+ if (std::find(std::begin(keyList), std::end(keyList), key) == std::end(keyList)) {
+ pmlog_warn(std::format("Kernel event [{}] is not a valid signal key", key));
+ return;
+ }
+ if (arg) {
+ ImplementSignal_(key, *arg);
+ }
+ else {
+ ImplementSignal_(key);
+ }
}
}
\ No newline at end of file
diff --git a/IntelPresentMon/AppCef/source/util/SignalManager.h b/IntelPresentMon/AppCef/source/util/SignalManager.h
index 22eed52d6..14e6aa8f5 100644
--- a/IntelPresentMon/AppCef/source/util/SignalManager.h
+++ b/IntelPresentMon/AppCef/source/util/SignalManager.h
@@ -3,6 +3,8 @@
#pragma once
#include
#include
+#include
+#include
#include
#include
@@ -40,12 +42,11 @@ namespace p2c::client::util
};
// functions
void RegisterCallback(const std::string& key, CallbackContext ctx);
- // -- Signal-firing (call from C++ to invoke a callback registered by JS side)
- void SignalTargetLost(uint32_t pid); // targetLost(pid:number)
- void SignalHotkeyFired(uint32_t action); // hotkeyFired(action:number)
- void SignalPresentmonInitFailed(); // presentmonInitFailed()
- void SignalOverlayDied(); // overlayDied()
- void SignalStalePid(); // stalePid()
+ // Fires the callback the page registered for a kernel event forwarded by the browser
+ // process; key must be one of keyList. The signals and their arguments:
+ // targetLost(pid:number), hotkeyFired(action:number), presentmonInitFailed(),
+ // overlayDied(), stalePid()
+ void SignalFromBrowser(const std::string& key, std::optional arg);
private:
// functions
template
diff --git a/IntelPresentMon/AppCef/source/util/async/GetAppInfo.h b/IntelPresentMon/AppCef/source/util/async/GetAppInfo.h
index 61bd5a773..a511249a0 100644
--- a/IntelPresentMon/AppCef/source/util/async/GetAppInfo.h
+++ b/IntelPresentMon/AppCef/source/util/async/GetAppInfo.h
@@ -111,14 +111,14 @@ namespace p2c::client::util::async
constexpr bool isDebugBuild = true;
#endif
const auto pKernelWrapper = accessor.GetKernelWrapper();
- const auto pClient = pKernelWrapper ? pKernelWrapper->pClient.get() : nullptr;
+ const auto versions = pKernelWrapper ? pKernelWrapper->kernelVersions : KernelWrapper::KernelVersions{};
auto info = MakeCefObject(
CefProp{ "productName", std::string{ "Intel(R) PresentMon" } },
CefProp{ "productVersion", std::string{ PM_VER_PRODUCT_STR } },
CefProp{ "fileVersion", std::string{ PM_VER_FILE_STR } },
CefProp{ "apiVersion", FormatApiHeaderVersion_() },
- CefProp{ "middlewareApiVersion", pClient ? pClient->GetMiddlewareApiVersion() : std::string{ "unknown" } },
+ CefProp{ "middlewareApiVersion", versions.middlewareApiVersion },
CefProp{ "buildId", std::string{ ::pmon::bid::BuildIdUid() } },
CefProp{ "buildHash", std::string{ ::pmon::bid::BuildIdLongHash() } },
CefProp{ "buildHashShort", std::string{ ::pmon::bid::BuildIdShortHash() } },
@@ -128,9 +128,9 @@ namespace p2c::client::util::async
CefProp{ "isDebugBuild", isDebugBuild },
CefProp{ "compileDate", std::string{ __DATE__ } },
CefProp{ "compileTime", std::string{ __TIME__ } },
- CefProp{ "serviceBuildId", pClient ? pClient->GetServiceBuildId() : std::string{ "unknown" } },
- CefProp{ "serviceBuildTime", pClient ? pClient->GetServiceBuildTime() : std::string{ "unknown" } },
- CefProp{ "serviceVersion", pClient ? pClient->GetServiceVersion() : std::string{ "unknown" } },
+ CefProp{ "serviceBuildId", versions.serviceBuildId },
+ CefProp{ "serviceBuildTime", versions.serviceBuildTime },
+ CefProp{ "serviceVersion", versions.serviceVersion },
CefProp{ "cefVersion", std::string{ CEF_VERSION } },
CefProp{ "cefVersionMajor", CEF_VERSION_MAJOR },
CefProp{ "cefVersionMinor", CEF_VERSION_MINOR },
diff --git a/IntelPresentMon/AppCef/source/util/cact/CefExecutionContext.h b/IntelPresentMon/AppCef/source/util/cact/CefExecutionContext.h
index 0d09250e3..bfda96d8f 100644
--- a/IntelPresentMon/AppCef/source/util/cact/CefExecutionContext.h
+++ b/IntelPresentMon/AppCef/source/util/cact/CefExecutionContext.h
@@ -1,31 +1,18 @@
#pragma once
-#include
-#include
-#include
-#include
-#include
-#include
-#include
#include
#include
-#include
-namespace p2c::client::util
+namespace p2c::client::util::kernel_relay
{
- class SignalManager;
+ class SignalRelay;
}
namespace p2c::client::util::cact
{
- using namespace ::pmon;
- struct CefExecutionContext;
-
struct CefSessionContext
{
- // common session context items
- std::unique_ptr> pConn;
+ // required by the transport, see TransportSessionContext
uint32_t remotePid = 0;
- uint32_t nextCommandToken = 0;
};
struct CefExecutionContext
@@ -34,7 +21,8 @@ namespace p2c::client::util::cact
using SessionContextType = CefSessionContext;
// data
- SignalManager* pSignalManager = nullptr;
+ // kernel events arrive in the browser process and are forwarded to the page through this
+ const kernel_relay::SignalRelay* pSignalRelay = nullptr;
std::optional responseWriteTimeoutMs;
};
-}
\ No newline at end of file
+}
diff --git a/IntelPresentMon/AppCef/source/util/cact/HotkeyFiredAction.h b/IntelPresentMon/AppCef/source/util/cact/HotkeyFiredAction.h
index 1d6edd037..c189d9e4b 100644
--- a/IntelPresentMon/AppCef/source/util/cact/HotkeyFiredAction.h
+++ b/IntelPresentMon/AppCef/source/util/cact/HotkeyFiredAction.h
@@ -32,9 +32,7 @@ namespace p2c::client::util::cact
}
#ifdef PM_ASYNC_ACTION_REGISTRATION_
-#include
-#include
-#include
+#include "../KernelRelay.h"
#include "../SignalManager.h"
namespace p2c::client::util::cact
{
@@ -45,8 +43,7 @@ namespace p2c::client::util::cact
// while also enabling dependency decoupling more easily
void ACT_NAME::Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in)
{
- CefPostTask(TID_RENDERER, base::BindOnce(&SignalManager::SignalHotkeyFired,
- base::Unretained(ctx.pSignalManager), in.actionId));
+ ctx.pSignalRelay->Forward(SignalManager::Keys::hotkeyFired(), (uint32_t)in.actionId);
}
}
#endif
diff --git a/IntelPresentMon/AppCef/source/util/cact/OverlayDiedAction.h b/IntelPresentMon/AppCef/source/util/cact/OverlayDiedAction.h
index ea6c68308..6621f5d91 100644
--- a/IntelPresentMon/AppCef/source/util/cact/OverlayDiedAction.h
+++ b/IntelPresentMon/AppCef/source/util/cact/OverlayDiedAction.h
@@ -28,9 +28,7 @@ namespace p2c::client::util::cact
}
#ifdef PM_ASYNC_ACTION_REGISTRATION_
-#include
-#include
-#include
+#include "../KernelRelay.h"
#include "../SignalManager.h"
namespace p2c::client::util::cact
{
@@ -41,8 +39,7 @@ namespace p2c::client::util::cact
// while also enabling dependency decoupling more easily
void ACT_NAME::Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in)
{
- CefPostTask(TID_RENDERER, base::BindOnce(&SignalManager::SignalOverlayDied,
- base::Unretained(ctx.pSignalManager)));
+ ctx.pSignalRelay->Forward(SignalManager::Keys::overlayDied());
}
}
#endif
diff --git a/IntelPresentMon/AppCef/source/util/cact/PresentmonInitFailedAction.h b/IntelPresentMon/AppCef/source/util/cact/PresentmonInitFailedAction.h
index e6c2683d2..fce8fb957 100644
--- a/IntelPresentMon/AppCef/source/util/cact/PresentmonInitFailedAction.h
+++ b/IntelPresentMon/AppCef/source/util/cact/PresentmonInitFailedAction.h
@@ -28,9 +28,7 @@ namespace p2c::client::util::cact
}
#ifdef PM_ASYNC_ACTION_REGISTRATION_
-#include
-#include
-#include
+#include "../KernelRelay.h"
#include "../SignalManager.h"
namespace p2c::client::util::cact
{
@@ -41,8 +39,7 @@ namespace p2c::client::util::cact
// while also enabling dependency decoupling more easily
void ACT_NAME::Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in)
{
- CefPostTask(TID_RENDERER, base::BindOnce(&SignalManager::SignalPresentmonInitFailed,
- base::Unretained(ctx.pSignalManager)));
+ ctx.pSignalRelay->Forward(SignalManager::Keys::presentmonInitFailed());
}
}
#endif
diff --git a/IntelPresentMon/AppCef/source/util/cact/StalePidAction.h b/IntelPresentMon/AppCef/source/util/cact/StalePidAction.h
index feba84899..c149f5567 100644
--- a/IntelPresentMon/AppCef/source/util/cact/StalePidAction.h
+++ b/IntelPresentMon/AppCef/source/util/cact/StalePidAction.h
@@ -28,9 +28,7 @@ namespace p2c::client::util::cact
}
#ifdef PM_ASYNC_ACTION_REGISTRATION_
-#include
-#include
-#include
+#include "../KernelRelay.h"
#include "../SignalManager.h"
namespace p2c::client::util::cact
{
@@ -41,8 +39,7 @@ namespace p2c::client::util::cact
// while also enabling dependency decoupling more easily
void ACT_NAME::Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in)
{
- CefPostTask(TID_RENDERER, base::BindOnce(&SignalManager::SignalStalePid,
- base::Unretained(ctx.pSignalManager)));
+ ctx.pSignalRelay->Forward(SignalManager::Keys::stalePid());
}
}
#endif
diff --git a/IntelPresentMon/AppCef/source/util/cact/TargetLostAction.h b/IntelPresentMon/AppCef/source/util/cact/TargetLostAction.h
index 85a50464e..eee53608b 100644
--- a/IntelPresentMon/AppCef/source/util/cact/TargetLostAction.h
+++ b/IntelPresentMon/AppCef/source/util/cact/TargetLostAction.h
@@ -32,9 +32,7 @@ namespace p2c::client::util::cact
}
#ifdef PM_ASYNC_ACTION_REGISTRATION_
-#include
-#include
-#include
+#include "../KernelRelay.h"
#include "../SignalManager.h"
namespace p2c::client::util::cact
{
@@ -45,8 +43,7 @@ namespace p2c::client::util::cact
// while also enabling dependency decoupling more easily
void ACT_NAME::Execute_(const ACT_EXEC_CTX& ctx, SessionContext& stx, Params&& in)
{
- CefPostTask(TID_RENDERER, base::BindOnce(&SignalManager::SignalTargetLost,
- base::Unretained(ctx.pSignalManager), in.pid));
+ ctx.pSignalRelay->Forward(SignalManager::Keys::targetLost(), in.pid);
}
}
#endif
diff --git a/IntelPresentMon/AppCef/source/winmain.cpp b/IntelPresentMon/AppCef/source/winmain.cpp
index 77d8b9f62..d4ec56578 100644
--- a/IntelPresentMon/AppCef/source/winmain.cpp
+++ b/IntelPresentMon/AppCef/source/winmain.cpp
@@ -17,6 +17,7 @@
#include
#include
#include "util/CefLog.h"
+#include "util/KernelLink.h"
#pragma comment(lib, "Dwmapi.lib")
@@ -33,16 +34,22 @@ constexpr const char* BrowserWindowClassName = "BrowserWindowClass";
constexpr const char* MessageWindowClassName = "MessageWindowClass";
constexpr const int quitCefCode = 0xABAD1DEA;
CefRefPtr pBrowserClient;
+// the UI's connection to the kernel, see KernelLink
+std::unique_ptr pKernelLink;
HWND hwndAppMsg = nullptr;
+// runs on the CEF UI thread; null until the browser has been created
+CefRefPtr GetBrowserForKernelEvents()
+{
+ return pBrowserClient ? pBrowserClient->GetBrowser() : nullptr;
+}
+
LRESULT CALLBACK BrowserWindowWndProc(HWND window_handle, UINT message, WPARAM w_param, LPARAM l_param)
{
switch (message)
{
case WM_CREATE:
{
- pBrowserClient = new ccef::NanoCefBrowserClient{};
-
RECT rect = { 0 };
GetClientRect(window_handle, &rect);
CefRect cefRect;
@@ -60,9 +67,10 @@ LRESULT CALLBACK BrowserWindowWndProc(HWND window_handle, UINT message, WPARAM w
if (Options::Get().url) {
url = *Options::Get().url;
}
+ // the renderer reads the kernel's reported versions from extra_info
CefBrowserHost::CreateBrowser(
info, pBrowserClient.get(), url,
- settings, {}, {}
+ settings, pKernelLink->MakeBrowserExtraInfo(), {}
);
break;
}
@@ -221,6 +229,9 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi
}
// code from here on is only executed by the root process (browser window process)
+
+ // take the kernel pipe first: its inheritance must be cleared before CEF can launch any process
+ auto kernelPipe = client::util::TakeKernelPipeHandle(*opt.actHandle);
auto uiInstanceMutex = client::util::TryAcquireUiBrowserProcessMutex(*opt.uiMutexName);
if (!uiInstanceMutex.first) {
return -1;
@@ -249,8 +260,19 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi
cefLogSeverity = ToCefLogLevel(util::log::Level::Debug);
}
settings.log_severity = cefLogSeverity;
- CefInitialize(main_args, settings, app.get(), nullptr);
+ // on failure CEF must not be used further; without this check the window
+ // would open with nothing rendering in it
+ if (!CefInitialize(main_args, settings, app.get(), nullptr)) {
+ const auto cefExitCode = CefGetExitCode();
+ pmlog_error("CEF failed to initialize; the UI cannot start").pmwatch(cefExitCode);
+ return cefExitCode ? cefExitCode : -1;
+ }
}
+ // The browser client exists before the kernel link, so a kernel event finds it; the
+ // link exists before the browser, so the page's first call finds the relay.
+ pBrowserClient = new ccef::NanoCefBrowserClient{};
+ pKernelLink = std::make_unique(std::move(kernelPipe), &GetBrowserForKernelEvents);
+ pBrowserClient->SetKernelCallRelay(&pKernelLink->GetCallRelay());
auto hwndBrowser = CreateBrowserWindow(hInstance, nCmdShow);
client::util::SetUiBrowserWindowMutexSuffix(hwndBrowser, *opt.uiMutexName);
hwndAppMsg = CreateMessageWindow(hInstance);
@@ -263,6 +285,9 @@ int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLi
}
DestroyWindow(hwndAppMsg);
+ // close the kernel session while CEF can still run the tasks it posts
+ pBrowserClient->SetKernelCallRelay(nullptr);
+ pKernelLink.reset();
CefShutdown();
client::util::ClearUiBrowserWindowMutexSuffix(hwndBrowser);
DestroyWindow(hwndBrowser);
diff --git a/IntelPresentMon/CommonUtilities/pipe/CoroMutex.cpp b/IntelPresentMon/CommonUtilities/pipe/CoroMutex.cpp
index 246c58403..39880e059 100644
--- a/IntelPresentMon/CommonUtilities/pipe/CoroMutex.cpp
+++ b/IntelPresentMon/CommonUtilities/pipe/CoroMutex.cpp
@@ -1,20 +1,40 @@
#include "CoroMutex.h"
+#include
namespace pmon::util::pipe
{
- as::awaitable CoroMutex::Lock()
+ as::awaitable CoroMutex::Lock(std::optional timeout)
{
if (counter_++ == 0) {
- co_return;
+ co_return LockResult::Acquired;
}
auto ec = boost::system::error_code{};
+ auto granted = std::make_shared(false);
Timer timer{ ctx_ };
- timer.expires_after(Timer::duration::max());
- waiters_.push_back(&timer);
+ if (timeout) {
+ timer.expires_after(*timeout);
+ }
+ else {
+ timer.expires_after(Timer::duration::max());
+ }
+ waiters_.push_back(Waiter{ &timer, granted });
co_await timer.async_wait(as::redirect_error(as::use_awaitable, ec));
- holdoff_ = false;
+ // Granted wins even when the timeout fired too: Unlock already removed this waiter and
+ // passed it the count, so it owns the mutex and its guard must release it.
+ if (*granted) {
+ holdoff_ = false;
+ co_return LockResult::Acquired;
+ }
+ // Still queued means nobody removed it, so the timeout woke it: remove it and undo its
+ // count here. Not queued means CancelWaiters removed it and already undid its count.
+ if (const auto i = std::ranges::find(waiters_, &timer, &Waiter::timer); i != waiters_.end()) {
+ waiters_.erase(i);
+ --counter_;
+ co_return LockResult::TimedOut;
+ }
+ co_return LockResult::Cancelled;
}
bool CoroMutex::TryLock()
{
@@ -38,17 +58,42 @@ namespace pmon::util::pipe
}
holdoff_ = true;
- waiters_.front()->cancel();
+ if (waiters_.front().granted) {
+ *waiters_.front().granted = true;
+ }
+ waiters_.front().timer->cancel();
waiters_.pop_front();
}
+ void CoroMutex::CancelWaiters()
+ {
+ auto pending = std::move(waiters_);
+ waiters_.clear();
+ for (auto& waiter : pending) {
+ if (counter_ > 0) {
+ --counter_;
+ }
+ if (waiter.timer) {
+ waiter.timer->cancel();
+ }
+ }
+ }
CoroLockGuard::CoroLockGuard(CoroLockGuard&& other)
- : pMtx_{ std::exchange(other.pMtx_, nullptr) }
+ : pMtx_{ std::exchange(other.pMtx_, nullptr) },
+ result_{ other.result_ }
{}
CoroLockGuard& CoroLockGuard::operator=(CoroLockGuard&& rhs)
{
+ if (this == &rhs) {
+ return *this;
+ }
+ // release any mutex already held before taking ownership of rhs
+ if (pMtx_) {
+ pMtx_->Unlock();
+ }
pMtx_ = std::exchange(rhs.pMtx_, nullptr);
+ result_ = rhs.result_;
return *this;
}
CoroLockGuard::~CoroLockGuard()
@@ -59,9 +104,12 @@ namespace pmon::util::pipe
}
- as::awaitable CoroLock(CoroMutex& mtx)
+ as::awaitable CoroLock(CoroMutex& mtx, std::optional timeout)
{
- co_await mtx.Lock();
+ const auto result = co_await mtx.Lock(timeout);
+ if (result != CoroMutex::LockResult::Acquired) {
+ co_return CoroLockGuard{ result };
+ }
co_return CoroLockGuard{ mtx };
}
}
\ No newline at end of file
diff --git a/IntelPresentMon/CommonUtilities/pipe/CoroMutex.h b/IntelPresentMon/CommonUtilities/pipe/CoroMutex.h
index d90f0bfe6..cebfb1191 100644
--- a/IntelPresentMon/CommonUtilities/pipe/CoroMutex.h
+++ b/IntelPresentMon/CommonUtilities/pipe/CoroMutex.h
@@ -3,7 +3,10 @@
#include
#include
#include
+#include
#include
+#include
+#include
namespace pmon::util::pipe
{
@@ -21,12 +24,32 @@ namespace pmon::util::pipe
CoroMutex& operator=(CoroMutex&&) = default;
~CoroMutex() = default;
- as::awaitable Lock();
+ enum class LockResult
+ {
+ // the caller owns the mutex and must Unlock it (CoroLockGuard does)
+ Acquired,
+ // CancelWaiters woke the wait (the pipe was closed); the caller does not own it
+ Cancelled,
+ // the timeout passed first; the caller does not own it
+ TimedOut,
+ };
+ // Waits for the mutex, up to timeout when one is given.
+ // counter_ counts the holder plus every queued waiter. A waiter is removed from waiters_
+ // exactly once, and whoever removes it accounts for its count: Unlock hands the count to
+ // it as the new holder, CancelWaiters and a timed-out waiter decrement it.
+ as::awaitable Lock(std::optional timeout = {});
bool TryLock();
void Unlock();
+ // Wakes every coroutine blocked in Lock without granting the mutex.
+ void CancelWaiters();
private:
+ struct Waiter
+ {
+ Timer* timer = nullptr;
+ std::shared_ptr granted;
+ };
as::io_context& ctx_;
- std::deque waiters_;
+ std::deque waiters_;
int counter_ = 0;
// used to check for the the same coro unlocking 2+ times
bool holdoff_ = false;
@@ -34,20 +57,28 @@ namespace pmon::util::pipe
class CoroLockGuard
{
- friend as::awaitable CoroLock(CoroMutex& mtx);
+ friend as::awaitable CoroLock(CoroMutex& mtx, std::optional timeout);
using CoroMutexType = CoroMutex;
public:
+ CoroLockGuard() = default;
CoroLockGuard(const CoroLockGuard&) = delete;
CoroLockGuard& operator=(const CoroLockGuard&) = delete;
+ // true when the mutex is held, that is when the result was Acquired
+ explicit operator bool() const { return pMtx_ != nullptr; }
+ CoroMutex::LockResult GetResult() const { return result_; }
CoroLockGuard(CoroLockGuard&& other);
CoroLockGuard& operator=(CoroLockGuard&& rhs);
~CoroLockGuard();
private:
// functions
- CoroLockGuard(CoroMutexType& mtx) : pMtx_{ &mtx } {}
+ CoroLockGuard(CoroMutexType& mtx) : pMtx_{ &mtx }, result_{ CoroMutex::LockResult::Acquired } {}
+ CoroLockGuard(CoroMutex::LockResult failure) : result_{ failure } {}
// data
CoroMutexType* pMtx_ = nullptr;
+ CoroMutex::LockResult result_ = CoroMutex::LockResult::Cancelled;
};
- as::awaitable CoroLock(CoroMutex& mtx);
+ // The guard owns the mutex only when GetResult() is Acquired, and then releases it when
+ // destroyed. With a timeout the wait gives up at that deadline (TimedOut).
+ as::awaitable CoroLock(CoroMutex& mtx, std::optional timeout = {});
}
\ No newline at end of file
diff --git a/IntelPresentMon/CommonUtilities/pipe/ManualAsyncEvent.h b/IntelPresentMon/CommonUtilities/pipe/ManualAsyncEvent.h
index 693b3fc99..48297bafc 100644
--- a/IntelPresentMon/CommonUtilities/pipe/ManualAsyncEvent.h
+++ b/IntelPresentMon/CommonUtilities/pipe/ManualAsyncEvent.h
@@ -3,6 +3,8 @@
#include "../win/Event.h"
#include
#include
+#include
+#include
namespace pmon::util::pipe
@@ -17,16 +19,39 @@ namespace pmon::util::pipe
:
timer_{ ctx, Timer::duration::max() }
{}
- as::awaitable AsyncWait()
+ // Returns true once signalled, false only if the optional timeout elapsed first.
+ // Returns immediately if already signalled, so a waiter cannot miss a signal
+ // raised before it started waiting.
+ as::awaitable AsyncWait(std::optional timeout = {})
{
- auto ec = boost::system::error_code{};
- co_await timer_.async_wait(as::redirect_error(as::use_awaitable, ec));
+ if (timeout) {
+ timer_.expires_after(*timeout);
+ }
+ while (!signalled_) {
+ auto ec = boost::system::error_code{};
+ co_await timer_.async_wait(as::redirect_error(as::use_awaitable, ec));
+ // Signal() cancels the timer; an abort with signalled_ set is success.
+ if (signalled_) {
+ co_return true;
+ }
+ if (ec) {
+ throw boost::system::system_error{ ec };
+ }
+ if (timeout) {
+ co_return false;
+ }
+ // without a deadline the timer is parked at time max, so a clean
+ // completion is a spurious wake and is simply waited out again
+ }
+ co_return true;
}
void Signal()
{
+ signalled_ = true;
timer_.cancel();
}
private:
Timer timer_;
+ bool signalled_ = false;
};
}
\ No newline at end of file
diff --git a/IntelPresentMon/CommonUtilities/pipe/Pipe.cpp b/IntelPresentMon/CommonUtilities/pipe/Pipe.cpp
index f51ac2701..4ac2ccdb6 100644
--- a/IntelPresentMon/CommonUtilities/pipe/Pipe.cpp
+++ b/IntelPresentMon/CommonUtilities/pipe/Pipe.cpp
@@ -1,5 +1,6 @@
#include "Pipe.h"
#include "../win/Security.h"
+#include "../win/HrError.h"
#include "../win/ServiceProcess.h"
#include
@@ -17,6 +18,22 @@ namespace pmon::util::pipe
std::atomic DuplexPipe::nextUid_ = 0;
+ namespace
+ {
+ // A client is free to connect and vanish before the accept completes. That costs
+ // the listener its pipe instance, but it is ordinary traffic on a public pipe
+ // rather than a failure, so it is reported as benign and the caller re-arms.
+ [[noreturn]] void ThrowAcceptError_(DWORD error)
+ {
+ if (error == ERROR_NO_DATA /* pipe is being closed */ || error == ERROR_BROKEN_PIPE) {
+ pmlog_dbg("Client vanished before the pipe connection was accepted").hr(error);
+ throw Except("Client vanished before the pipe connection was accepted");
+ }
+ pmlog_error("Failure accepting pipe connection").hr(error);
+ throw Except("Failure accepting pipe connection");
+ }
+ }
+
as::awaitable DuplexPipe::Accept()
{
assert(!asioPipeHandle_.is_open());
@@ -32,8 +49,7 @@ namespace pmon::util::pipe
}
if (const auto error = GetLastError(); error == ERROR_IO_PENDING) {
// async operation is in-flight and not yet complete, do async wait while not complete
- bool completed = false;
- while (!completed) {
+ for (;;) {
co_await connEvt.async_wait(as::use_awaitable);
// after completion signal, get result to A) make sure not a spurious wake,
// B) make sure there was no error, and C) conclude the overlapped operation cleanly
@@ -41,9 +57,8 @@ namespace pmon::util::pipe
if (GetOverlappedResult(rawPipeHandle_, &over, &dummyBytes, FALSE)) {
break;
}
- if (GetLastError() != ERROR_IO_INCOMPLETE) {
- pmlog_error("Failure accepting pipe connection").hr();
- throw Except("Failure accepting pipe connection");
+ if (const auto waitError = GetLastError(); waitError != ERROR_IO_INCOMPLETE) {
+ ThrowAcceptError_(waitError);
}
}
// now we have connected, so transfer pipe ownership to asio
@@ -56,39 +71,97 @@ namespace pmon::util::pipe
}
else {
// some error has occurred during connection
- pmlog_error("Failure accepting pipe connection").hr();
- throw Except("Failure accepting pipe connection");
+ ThrowAcceptError_(error);
}
pmlog_dbg(std::format("{}:{} has received a connection", name_, uid_));
}
- DuplexPipe DuplexPipe::Connect(const std::string& name, as::io_context& ioctx)
+ DuplexPipe DuplexPipe::Connect(const std::string& name, as::io_context& ioctx, PipeLimits limits)
{
- return DuplexPipe{ ioctx, Connect_(name), name, true };
+ return DuplexPipe{ ioctx, Connect_(name), name, true, limits };
}
- DuplexPipe DuplexPipe::Make(const std::string& name, as::io_context& ioctx, const std::string& security)
+ std::unique_ptr DuplexPipe::ConnectAsPtr(const std::string& name, as::io_context& ioctx, PipeLimits limits)
{
- return DuplexPipe{ ioctx, Make_(name), name, false };
+ return std::unique_ptr(new DuplexPipe{ ioctx, Connect_(name), name, true, limits });
}
- std::unique_ptr DuplexPipe::ConnectAsPtr(const std::string& name, as::io_context& ioctx)
+ std::unique_ptr DuplexPipe::MakeAsPtr(const std::string& name, as::io_context& ioctx, const std::string& security, PipeLimits limits)
{
- return std::unique_ptr(new DuplexPipe{ ioctx, Connect_(name), name, true });
+ return std::unique_ptr(new DuplexPipe{ ioctx, Make_(name, security), name, false, limits });
}
- std::unique_ptr DuplexPipe::MakeAsPtr(const std::string& name, as::io_context& ioctx,
- const std::string& security)
+ DuplexPipe::ConnectedPair DuplexPipe::MakeConnectedPair(const std::string& name, as::io_context& ioctx,
+ const std::string& security, PipeLimits limits)
{
- return std::unique_ptr(new DuplexPipe{ ioctx, Make_(name, security), name, false });
+ // one instance only, and creation fails if the name already exists
+ win::Handle server{ Make_(name, security, true) };
+ // fails if any other process connected first, since there is only one instance
+ win::Handle client{ Connect_(name) };
+ // the client is already connected, so this completes at once
+ win::Event connectEvent;
+ OVERLAPPED over{ .hEvent = connectEvent.Get() };
+ const auto connected = ConnectNamedPipe(server, &over);
+ const auto error = GetLastError();
+ if (connected || error != ERROR_PIPE_CONNECTED) {
+ // a started wait must end before the OVERLAPPED goes out of scope
+ if (!connected && error == ERROR_IO_PENDING) {
+ CancelIoEx(server, &over);
+ DWORD dummyBytes = 0;
+ GetOverlappedResult(server, &over, &dummyBytes, TRUE);
+ }
+ pmlog_error("Pipe pair was not connected on creation").pmwatch(name).hr(error);
+ throw Except("Pipe pair was not connected on creation");
+ }
+ // the only client must be this process
+ ULONG clientPid = 0;
+ if (!GetNamedPipeClientProcessId(server, &clientPid) || clientPid != GetCurrentProcessId()) {
+ pmlog_error("Pipe pair client is not this process").pmwatch(name).pmwatch(clientPid);
+ throw Except("Pipe pair client is not this process");
+ }
+ pmlog_dbg(std::format("Created connected pipe pair [{}]", name));
+ return ConnectedPair{
+ .pServer = std::unique_ptr(new DuplexPipe{ ioctx, server.Release(), name, true, limits }),
+ .client = std::move(client),
+ };
}
- size_t DuplexPipe::GetWriteBufferPending() const
+ std::unique_ptr DuplexPipe::AdoptConnectedClientAsPtr(HANDLE clientHandle, as::io_context& ioctx, PipeLimits limits)
{
- return writeBuf_.size();
+ return std::unique_ptr(new DuplexPipe{ ioctx, clientHandle, "adopted-client", true, limits });
+ }
+ void DuplexPipe::DiscardPacketPayload()
+ {
+ readBuf_.consume(readBuf_.size());
}
- void DuplexPipe::ClearWriteBuffer()
+ void DuplexPipe::Cancel_()
{
- return writeBuf_.consume(GetWriteBufferPending());
+ if (!asioPipeHandle_.is_open()) {
+ return;
+ }
+ boost::system::error_code ec;
+ asioPipeHandle_.cancel(ec);
+ if (ec) {
+ pmlog_warn("Failure cancelling pipe operations").pmwatch(ec.message());
+ }
}
- bool DuplexPipe::WaitForAvailability(const std::string& baseName, uint32_t timeoutMs, bool noSuffix, uint32_t pollPeriodMs)
+ void DuplexPipe::Close()
+ {
+ Cancel_();
+ boost::system::error_code ec;
+ if (asioPipeHandle_.is_open()) {
+ asioPipeHandle_.close(ec);
+ if (ec) {
+ pmlog_warn("Failure closing pipe").pmwatch(ec.message());
+ }
+ }
+ rawPipeHandle_.Clear();
+ // After the handle is closed, wake CoroMutex waiters so they observe
+ // the closed pipe and return instead of resuming into a write.
+ writeMtx_.CancelWaiters();
+ readMtx_.CancelWaiters();
+ }
+ size_t DuplexPipe::GetWriteBufferPending() const
+ {
+ return writeBuf_.size();
+ }
+ bool DuplexPipe::WaitForAvailability(const std::string& name, uint32_t timeoutMs, uint32_t pollPeriodMs)
{
- const auto name = baseName + (noSuffix ? "" : "-in");
const auto start = std::chrono::high_resolution_clock::now();
while (std::chrono::high_resolution_clock::now() - start < 1ms * timeoutMs) {
if (WaitNamedPipeA(name.c_str(), 0)) {
@@ -100,9 +173,8 @@ namespace pmon::util::pipe
}
return false;
}
- bool DuplexPipe::WaitForVacancy(const std::string& baseName, uint32_t timeoutMs, bool noSuffix, uint32_t pollPeriodMs)
+ bool DuplexPipe::WaitForVacancy(const std::string& name, uint32_t timeoutMs, uint32_t pollPeriodMs)
{
- const auto name = baseName + (noSuffix ? "" : "-out");
const auto start = std::chrono::high_resolution_clock::now();
while (std::chrono::high_resolution_clock::now() - start < 1ms * timeoutMs) {
if (!WaitNamedPipeA(name.c_str(), 0)) {
@@ -124,6 +196,16 @@ namespace pmon::util::pipe
{
return name_;
}
+ uint32_t DuplexPipe::GetClientProcessId() const
+ {
+ // asio only exposes native_handle() as non-const; reading it does not modify the handle
+ const auto handle = const_cast(asioPipeHandle_).native_handle();
+ ULONG pid = 0;
+ if (!GetNamedPipeClientProcessId(handle, &pid)) {
+ throw Except("Failed getting pipe client process id");
+ }
+ return (uint32_t)pid;
+ }
bool DuplexPipe::TryGetConnectedServerProcessId(uint32_t& serverProcessId) noexcept
{
if (!asioPipeHandle_.is_open()) {
@@ -135,14 +217,14 @@ namespace pmon::util::pipe
{
switch (mode) {
default:case SecurityMode::None: return {};
- case SecurityMode::Service: return "D:P(A;;GA;;;AU)S:(ML;;NW;;;LW)"s;
- case SecurityMode::Child: return "D:(A;OICI;GA;;;WD)"s;
}
}
std::string DuplexPipe::GetServiceControlPipeSecurityString()
{
- // SY: create/listen; AU: client read/write without FILE_CREATE_PIPE_INSTANCE or DACL change
- return std::format("D:P(A;;GA;;;SY)(A;;0x{:x};;;AU)S:(ML;;NW;;;LW)", kClientPipeConnectAccess);
+ // SY: create/listen; AU: client read/write without FILE_CREATE_PIPE_INSTANCE or DACL change.
+ // ME label: no write (so no connect) from processes below medium integrity; AU alone does not
+ // imply an integrity level.
+ return std::format("D:P(A;;GA;;;SY)(A;;0x{:x};;;AU)S:(ML;;NW;;;ME)", kClientPipeConnectAccess);
}
DWORD DuplexPipe::GetClientPipeConnectAccessMask() noexcept
{
@@ -160,9 +242,10 @@ namespace pmon::util::pipe
return std::format("D:(A;;GA;;;{})", userSid);
}
- DuplexPipe::DuplexPipe(as::io_context& ioctx, HANDLE pipeHandle, std::string name, bool asClient)
+ DuplexPipe::DuplexPipe(as::io_context& ioctx, HANDLE pipeHandle, std::string name, bool connected, PipeLimits limits)
:
name_{ std::move(name) },
+ limits_{ limits },
rawPipeHandle_{ pipeHandle },
asioPipeHandle_{ ioctx },
readStream_{ &readBuf_ },
@@ -172,8 +255,8 @@ namespace pmon::util::pipe
writeArchive_{ writeStream_ },
writeMtx_{ ioctx }
{
- if (asClient) {
- // client is automatically connected upon creation, so immediatly transfer pipe to asio
+ if (connected) {
+ // a client end, or either end of a connected pair, needs no accept, so transfer the pipe to asio now
asioPipeHandle_.assign(rawPipeHandle_.Release());
}
}
@@ -193,7 +276,7 @@ namespace pmon::util::pipe
}
return handle.Release();
}
- HANDLE DuplexPipe::Make_(const std::string& name, const std::string& security)
+ HANDLE DuplexPipe::Make_(const std::string& name, const std::string& security, bool singleInstance)
{
pmlog_dbg(std::format("Creating instance of [{}] with security [{}]", name, security));
// structure required for creating named pipe, create with placeholder pointer for descriptor
@@ -210,11 +293,12 @@ namespace pmon::util::pipe
}
// if we have a security string, call create pipe with above structure, else call with nullptr
SECURITY_ATTRIBUTES* pSecurityAttributes = security.empty() ? nullptr : &securityAttributes;
+ // a single instance cannot be joined by a second server, and creating it fails if the name exists
win::Handle handle(CreateNamedPipeA(
name.c_str(),
- PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
+ PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | (singleInstance ? FILE_FLAG_FIRST_PIPE_INSTANCE : 0),
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_REJECT_REMOTE_CLIENTS,
- PIPE_UNLIMITED_INSTANCES,
+ singleInstance ? 1 : PIPE_UNLIMITED_INSTANCES,
4096,
4096,
0,
@@ -231,9 +315,12 @@ namespace pmon::util::pipe
if (timeoutMs) {
const auto result = co_await(as::async_read(asioPipeHandle_, readBuf_, as::transfer_exactly(byteCount),
as::as_tuple(as::use_awaitable)) || Timeout_(*timeoutMs));
- // 2nd index active means timed out
+ // 2nd index active means the timer finished first: expired, or cancelled from outside
if (result.index() == 1) {
- throw Except("Timeout during read");
+ if (std::get<1>(result)) {
+ throw Except("Timeout during read");
+ }
+ throw Except("Read canceled");
}
// otherwise 1st index active => extract error code and transform
auto&& [ec, n] = std::get<0>(result);
@@ -250,9 +337,12 @@ namespace pmon::util::pipe
if (timeoutMs) {
const auto result = co_await(as::async_write(asioPipeHandle_, writeBuf_, as::as_tuple(as::use_awaitable))
|| Timeout_(*timeoutMs));
- // 2nd index active means timed out
+ // 2nd index active means the timer finished first: expired, or cancelled from outside
if (result.index() == 1) {
- throw Except("Timeout during write");
+ if (std::get<1>(result)) {
+ throw Except("Timeout during write");
+ }
+ throw Except("Write canceled");
}
// otherwise 1st index active => extract error code and transform
auto&& [ec, n] = std::get<0>(result);
@@ -263,11 +353,15 @@ namespace pmon::util::pipe
TransformError_(ec);
}
}
- as::awaitable DuplexPipe::Timeout_(uint32_t ms)
+ as::awaitable DuplexPipe::Timeout_(uint32_t ms)
{
as::deadline_timer timer{ co_await as::this_coro::executor };
timer.expires_from_now(boost::posix_time::millisec{ ms });
- co_await timer.async_wait(as::use_awaitable);
+ // Losing the race to the I/O cancels this wait. That is the normal case, so it
+ // completes quietly instead of throwing; only a real expiry reports true.
+ auto ec = boost::system::error_code{};
+ co_await timer.async_wait(as::redirect_error(as::use_awaitable, ec));
+ co_return !ec;
}
void DuplexPipe::TransformError_(const boost::system::error_code& ec)
diff --git a/IntelPresentMon/CommonUtilities/pipe/Pipe.h b/IntelPresentMon/CommonUtilities/pipe/Pipe.h
index 4506cbd3e..c00540743 100644
--- a/IntelPresentMon/CommonUtilities/pipe/Pipe.h
+++ b/IntelPresentMon/CommonUtilities/pipe/Pipe.h
@@ -12,6 +12,8 @@
#include "../log/Log.h"
#include "SecurityMode.h"
#include "CoroMutex.h"
+#include
+#include
#include
namespace pmon::util::pipe
@@ -20,6 +22,17 @@ namespace pmon::util::pipe
using namespace std::literals;
PM_DEFINE_EX(PipeError);
+ // A read that had a deadline and missed it. Distinct from a peer disconnect so
+ // the session owner can report SessionEndReason::PeerStalled.
+ PM_DEFINE_EX_FROM(PipeError, PipeReadTimeout);
+ // A write that had a deadline and missed it: the peer stopped reading. Part of the packet
+ // may already be on the wire, so the stream can no longer be trusted and the session owner
+ // must end the session (SessionEndReason::PeerStalled).
+ PM_DEFINE_EX_FROM(PipeError, PipeWriteTimeout);
+ // A write whose deadline passed while it waited for the write lock (another write held it),
+ // or that had no time left once it got the lock. Nothing was written and the stream is
+ // intact, so this fails only that write; the writer holding the lock answers for the stall.
+ PM_DEFINE_EX_FROM(PipeError, PipeSendQueueTimeout);
// pipe errors that are often part of acceptable program flow
PM_DEFINE_EX_FROM(PipeError, BenignPipeError);
// pipe was broken (closed by remote)
@@ -27,6 +40,20 @@ namespace pmon::util::pipe
// pipe operation was canceled (e.g. by operator ||, ioctx.stop)
PM_DEFINE_EX_FROM(BenignPipeError, PipeOperationCanceled);
+ // Bounds imposed on the length prefix of an incoming packet. The declared body
+ // length is remote input, so it is validated before the receive buffer is grown
+ // to hold it. Owners with a known packet shape pass tighter limits at construction.
+ struct PipeLimits
+ {
+ uint32_t minBodyBytes = 1;
+ uint32_t maxBodyBytes = 1024 * 1024;
+ // once a length has been declared the sender has committed, so a stall is abuse
+ uint32_t bodyReadTimeoutMs = 2000;
+ };
+
+ // Written into the stream before the body is serialized, then overwritten in place.
+ inline constexpr uint32_t kPacketBodyLengthPlaceholder = 0x54454D50u;
+
class DuplexPipe
{
public:
@@ -36,21 +63,48 @@ namespace pmon::util::pipe
DuplexPipe& operator=(DuplexPipe&&) = delete;
~DuplexPipe() = default;
as::awaitable Accept();
- static DuplexPipe Connect(const std::string& name, as::io_context& ioctx);
- static DuplexPipe Make(const std::string& name, as::io_context& ioctx, const std::string& security = {});
- static std::unique_ptr ConnectAsPtr(const std::string& name, as::io_context& ioctx);
- static std::unique_ptr MakeAsPtr(const std::string& name, as::io_context& ioctx,
- const std::string& security = {});
+ static DuplexPipe Connect(const std::string& name, as::io_context& ioctx, PipeLimits limits = {});
+ static std::unique_ptr ConnectAsPtr(const std::string& name, as::io_context& ioctx, PipeLimits limits = {});
+ static std::unique_ptr MakeAsPtr(const std::string& name, as::io_context& ioctx, const std::string& security = {}, PipeLimits limits = {});
+ // Both ends of a single-instance pipe, created and connected by this process, so no
+ // other process can ever connect to it. The client end stays a plain handle, to be
+ // handed to another process; it must not be given to asio here, because the I/O
+ // completion port association belongs to the file object both handles share.
+ struct ConnectedPair
+ {
+ std::unique_ptr pServer;
+ win::Handle client;
+ };
+ // Throws PipeError if the name is already in use or the pair cannot be verified;
+ // the caller retries with a fresh name.
+ static ConnectedPair MakeConnectedPair(const std::string& name, as::io_context& ioctx,
+ const std::string& security, PipeLimits limits = {});
+ // Wraps the client end of a connected pair received from the process that created it.
+ static std::unique_ptr AdoptConnectedClientAsPtr(HANDLE clientHandle, as::io_context& ioctx, PipeLimits limits = {});
+ // timeoutMs, when given, is one deadline covering the wait for the write lock and the
+ // write: PipeSendQueueTimeout if it passes before any byte is sent, PipeWriteTimeout if
+ // it passes during the write.
template
as::awaitable WritePacket(const H& header, const P& payload, std::optional timeoutMs = {})
{
+ std::optional deadline;
+ std::optional lockTimeout;
+ if (timeoutMs) {
+ lockTimeout = std::chrono::milliseconds{ *timeoutMs };
+ deadline = std::chrono::steady_clock::now() + *lockTimeout;
+ }
// lock while this coro is running to prevent other coros from causing an overlapped operation fault
- auto lk = co_await CoroLock(writeMtx_);
- // some sanity checks
+ auto lk = co_await CoroLock(writeMtx_, lockTimeout);
+ if (lk.GetResult() == CoroMutex::LockResult::TimedOut) {
+ throw Except("Timeout waiting to write");
+ }
+ // Close cancels a waiter. Do not write on the closed handle.
+ if (!lk || !asioPipeHandle_.is_open()) {
+ throw Except("Write on closed pipe");
+ }
assert(writeBuf_.size() == 0);
- assert(asioPipeHandle_.is_open());
// first we directly write bytes for the size of the body as a placeholder until we know how many are serialized
- const uint32_t placeholderSize = 'TEMP';
+ const uint32_t placeholderSize = kPacketBodyLengthPlaceholder;
writeStream_.write(reinterpret_cast(&placeholderSize), sizeof(placeholderSize));
// record how many bytes used for the serialization of the size
const auto sizeSize = writeBuf_.size();
@@ -58,29 +112,61 @@ namespace pmon::util::pipe
writeArchive_(header, payload);
// calculate size of body
const auto payloadSize = uint32_t(writeBuf_.size() - sizeSize);
+ if (payloadSize < limits_.minBodyBytes || payloadSize > limits_.maxBodyBytes) {
+ pmlog_error("Packet body size out of range").pmwatch(payloadSize);
+ writeBuf_.consume(writeBuf_.size());
+ throw Except("Packet body size out of range");
+ }
// replace the placeholder with the actual body size
const auto pSizeInPlace = const_cast(&*as::buffers_begin(writeBuf_.data()));
auto replacement = std::string_view{ reinterpret_cast(&payloadSize), sizeof(payloadSize) };
std::ranges::copy(replacement, pSizeInPlace);
- // transmit the packet
- co_await Write_(timeoutMs);
+ // what is left of the deadline, measured after serialization, right before the I/O
+ std::optional writeTimeoutMs;
+ if (deadline) {
+ const auto remaining = std::chrono::duration_cast