Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#ifndef FLUTTER_INAPPWEBVIEW_PLUGIN_BROWSER_PROCESS_GATE_H_
#define FLUTTER_INAPPWEBVIEW_PLUGIN_BROWSER_PROCESS_GATE_H_

#include <algorithm>
#include <functional>
#include <map>
#include <optional>
#include <vector>

namespace flutter_inappwebview_plugin
{
// Coalesces browser-process responsiveness probing across every webview that
// shares a browser process: within one validity window at most one probe
// (bounded by its timeout) runs per process, no matter how many webviews
// have synchronous state waiting on it. Pure logic - the Win32 probe, clock
// and retry timer are injected, which also makes it unit-testable.
class BrowserProcessGateRegistry
{
public:
using Waiter = void*;
using ProbeFn = std::function<bool(unsigned long pid)>;
using ClockFn = std::function<unsigned long long()>;
using ScheduleRetryFn = std::function<void(unsigned long pid)>;
using CancelRetryFn = std::function<void(unsigned long pid)>;

BrowserProcessGateRegistry(ProbeFn probe, ClockFn clock,
ScheduleRetryFn scheduleRetry, CancelRetryFn cancelRetry,
const unsigned long long probeValidityMs)
: probe_(std::move(probe)), clock_(std::move(clock)),
scheduleRetry_(std::move(scheduleRetry)),
cancelRetry_(std::move(cancelRetry)),
probeValidityMs_(probeValidityMs)
{}

// True: the browser answered a probe at most probeValidityMs ago and the
// caller may issue its synchronous calls now. False: the caller is queued
// and handed back by takeWaitersIfResponsive on a retry tick.
bool tryAcquire(const unsigned long pid, const Waiter waiter)
{
auto& entry = entries_[pid];
if (verdict(pid, entry)) {
return true;
}
if (std::find(entry.waiters.begin(), entry.waiters.end(), waiter) ==
entry.waiters.end()) {
entry.waiters.push_back(waiter);
}
if (!entry.retryScheduled) {
entry.retryScheduled = true;
scheduleRetry_(pid);
}
return false;
}

// Runs on the retry timer. Returns the drained waiters once the browser
// answers again (and stops the timer); empty while it stays unresponsive.
std::vector<Waiter> takeWaitersIfResponsive(const unsigned long pid)
{
const auto it = entries_.find(pid);
if (it == entries_.end() || it->second.waiters.empty()) {
stopRetry(pid);
return {};
}
auto& entry = it->second;
if (!verdict(pid, entry)) {
return {};
}
stopRetry(pid);
std::vector<Waiter> drained;
drained.swap(entry.waiters);
return drained;
}

void removeWaiter(const Waiter waiter)
{
for (auto& [pid, entry] : entries_) {
entry.waiters.erase(
std::remove(entry.waiters.begin(), entry.waiters.end(), waiter),
entry.waiters.end());
}
}

// The browser process exited: its cached verdict no longer means anything.
void invalidate(const unsigned long pid)
{
const auto it = entries_.find(pid);
if (it != entries_.end()) {
it->second.lastVerdict.reset();
}
}

private:
struct Entry
{
std::optional<bool> lastVerdict;
unsigned long long verdictTick = 0;
std::vector<Waiter> waiters;
bool retryScheduled = false;
};

bool verdict(const unsigned long pid, Entry& entry)
{
const auto now = clock_();
if (!entry.lastVerdict.has_value() ||
now - entry.verdictTick >= probeValidityMs_) {
entry.lastVerdict = probe_(pid);
entry.verdictTick = now;
}
return entry.lastVerdict.value();
}

void stopRetry(const unsigned long pid)
{
const auto it = entries_.find(pid);
if (it != entries_.end() && it->second.retryScheduled) {
it->second.retryScheduled = false;
cancelRetry_(pid);
}
}

ProbeFn probe_;
ClockFn clock_;
ScheduleRetryFn scheduleRetry_;
CancelRetryFn cancelRetry_;
unsigned long long probeValidityMs_;
std::map<unsigned long, Entry> entries_;
};
}

#endif // FLUTTER_INAPPWEBVIEW_PLUGIN_BROWSER_PROCESS_GATE_H_
201 changes: 174 additions & 27 deletions flutter_inappwebview_windows/windows/in_app_webview/in_app_webview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include "../web_notification/web_notification_controller.h"
#include "../print_job/print_job_controller.h"
#include "../print_job/print_job_manager.h"
#include "browser_process_gate.h"
#include "in_app_webview.h"
#include "in_app_webview_manager.h"

Expand Down Expand Up @@ -76,7 +77,7 @@ namespace flutter_inappwebview_plugin
registerSurfaceEventHandlers();
}
else {
updateControllerVisibility();
applyPendingBrowserState();
// Resize WebView to fit the bounds of the parent window
RECT bounds;
GetClientRect(parentWindow, &bounds);
Expand Down Expand Up @@ -1077,6 +1078,7 @@ namespace flutter_inappwebview_plugin
COREWEBVIEW2_PROCESS_FAILED_KIND kind;
if (succeededOrLog(args->get_ProcessFailedKind(&kind))) {
if (kind == COREWEBVIEW2_PROCESS_FAILED_KIND_BROWSER_PROCESS_EXITED) {
invalidateBrowserProcessCache();
auto didCrash = reason == COREWEBVIEW2_PROCESS_FAILED_REASON_CRASHED;
auto detail = std::make_unique<RenderProcessGoneDetail>(
didCrash
Expand Down Expand Up @@ -3247,49 +3249,192 @@ namespace flutter_inappwebview_plugin
}


bool InAppWebView::updateControllerVisibility() const
namespace
{
if (!webViewController) {
return false;
constexpr UINT kBrowserProbeTimeoutMs = 50;
constexpr UINT kBrowserStateRetryIntervalMs = 250;

// Probe-window and retry-timer bookkeeping, keyed by browser process id.
// Platform-thread only, like every other call into this file.
std::map<unsigned long, HWND> gBrowserProbeWindows;
std::map<unsigned long, UINT_PTR> gGateRetryTimers;
std::map<UINT_PTR, unsigned long> gGateTimerPids;

HWND findBrowserProbeWindow(const unsigned long pid)
{
struct FindContext { unsigned long pid; HWND found; } context = { pid, nullptr };
EnumWindows([](HWND hwnd, LPARAM lparam) -> BOOL
{
auto* const ctx = reinterpret_cast<FindContext*>(lparam);
DWORD windowPid = 0;
GetWindowThreadProcessId(hwnd, &windowPid);
if (windowPid != ctx->pid) {
return TRUE;
}
wchar_t className[64] = L"";
GetClassNameW(hwnd, className, 64);
if (wcsncmp(className, L"Chrome_WidgetWin", 16) == 0) {
ctx->found = hwnd;
return FALSE;
}
return TRUE;
}, reinterpret_cast<LPARAM>(&context));
return context.found;
}

// Bounded by kBrowserProbeTimeoutMs. A missing probe window means the
// gate cannot judge; report responsive so behavior degrades to the old
// unconditional (possibly blocking) delivery rather than never delivering.
bool probeBrowserProcess(const unsigned long pid)
{
auto& window = gBrowserProbeWindows[pid];
if (window) {
DWORD windowPid = 0;
GetWindowThreadProcessId(window, &windowPid);
if (!IsWindow(window) || windowPid != pid) {
window = nullptr;
}
}
if (!window) {
window = findBrowserProbeWindow(pid);
}
if (!window) {
return true;
}
DWORD_PTR ignored = 0;
return SendMessageTimeoutW(window, WM_NULL, 0, 0,
SMTO_ABORTIFHUNG | SMTO_BLOCK, kBrowserProbeTimeoutMs, &ignored) != 0;
}

void CALLBACK GateRetryTimerProc(HWND, UINT, UINT_PTR timerId, DWORD);

BrowserProcessGateRegistry& browserGate()
{
static BrowserProcessGateRegistry gate(
&probeBrowserProcess,
[]() -> unsigned long long { return GetTickCount64(); },
[](const unsigned long pid)
{
if (map_contains(gGateRetryTimers, pid)) {
return;
}
const auto timerId = SetTimer(nullptr, 0, kBrowserStateRetryIntervalMs,
&GateRetryTimerProc);
if (timerId) {
gGateRetryTimers[pid] = timerId;
gGateTimerPids[timerId] = pid;
}
},
[](const unsigned long pid)
{
const auto it = gGateRetryTimers.find(pid);
if (it == gGateRetryTimers.end()) {
return;
}
KillTimer(nullptr, it->second);
gGateTimerPids.erase(it->second);
gGateRetryTimers.erase(it);
},
kBrowserStateRetryIntervalMs);
return gate;
}

void CALLBACK GateRetryTimerProc(HWND, UINT, UINT_PTR timerId, DWORD)
{
const auto it = gGateTimerPids.find(timerId);
if (it == gGateTimerPids.end()) {
KillTimer(nullptr, timerId);
return;
}
for (const auto waiter : browserGate().takeWaitersIfResponsive(it->second)) {
static_cast<InAppWebView*>(waiter)->applyPendingBrowserState();
}
}
}

DWORD InAppWebView::cachedBrowserProcessId()
{
if (!browserProcessId_ && webView) {
UINT32 pid = 0;
if (SUCCEEDED(webView->get_BrowserProcessId(&pid))) {
browserProcessId_ = pid;
}
}
return browserProcessId_;
}

void InAppWebView::invalidateBrowserProcessCache()
{
if (browserProcessId_) {
browserGate().invalidate(browserProcessId_);
gBrowserProbeWindows.erase(browserProcessId_);
}
browserProcessId_ = 0;
}

void InAppWebView::applyPendingBrowserState()
{
if (webViewController &&
(visibilityState_.needsApply() || pendingResume_ || pendingSuspend_)) {
const auto browserPid = cachedBrowserProcessId();
if (browserPid && !browserGate().tryAcquire(browserPid, this)) {
return;
}
if (pendingResume_) {
pendingResume_ = false;
wil::com_ptr<ICoreWebView2_3> webView3;
if (webView && SUCCEEDED(webView->QueryInterface(IID_PPV_ARGS(&webView3)))) {
failedLog(webView3->Resume());
}
}
if (visibilityState_.needsApply()) {
if (succeededOrLog(webViewController->put_IsVisible(
visibilityState_.shouldBeVisible() ? TRUE : FALSE))) {
visibilityState_.markApplied();
}
else {
pendingSuspend_ = false;
}
}
if (pendingSuspend_) {
pendingSuspend_ = false;
if (!visibilityState_.shouldBeVisible() && !visibilityState_.needsApply()) {
wil::com_ptr<ICoreWebView2_3> webView3;
if (webView && SUCCEEDED(webView->QueryInterface(IID_PPV_ARGS(&webView3)))) {
failedLog(webView3->TrySuspend(Callback<ICoreWebView2TrySuspendCompletedHandler>(
[this](HRESULT errorCode, BOOL isSuccessful) -> HRESULT
{
failedLog(errorCode);
return S_OK;
})
.Get()));
}
}
}
}
return succeededOrLog(webViewController->put_IsVisible(
visibilityState_.shouldBeVisible() ? TRUE : FALSE));
browserGate().removeWaiter(this);
}

void InAppWebView::setHostWindowMinimized(const bool minimized)
{
visibilityState_.setHostWindowMinimized(minimized);
updateControllerVisibility();
applyPendingBrowserState();
}

void InAppWebView::pause()
{
visibilityState_.setPaused(true);
if (!updateControllerVisibility()) {
return;
}

wil::com_ptr<ICoreWebView2_3> webView3;
if (webView && SUCCEEDED(webView->QueryInterface(IID_PPV_ARGS(&webView3)))) {
failedLog(webView3->TrySuspend(Callback<ICoreWebView2TrySuspendCompletedHandler>(
[this](HRESULT errorCode, BOOL isSuccessful) -> HRESULT
{
failedLog(errorCode);
return S_OK;
})
.Get()));
}
pendingResume_ = false;
pendingSuspend_ = true;
applyPendingBrowserState();
}

void InAppWebView::resume()
{
visibilityState_.setPaused(false);

wil::com_ptr<ICoreWebView2_3> webView3;
if (webView && SUCCEEDED(webView->QueryInterface(IID_PPV_ARGS(&webView3)))) {
failedLog(webView3->Resume());
}
updateControllerVisibility();
pendingSuspend_ = false;
pendingResume_ = true;
applyPendingBrowserState();
}


Expand Down Expand Up @@ -4023,6 +4168,7 @@ namespace flutter_inappwebview_plugin
surface_ = nullptr;
return false;
}
visibilityState_.markApplied();

// The Flutter view may detach while surface creation is still in flight.
if (plugin && plugin->registrar) {
Expand Down Expand Up @@ -4291,6 +4437,7 @@ namespace flutter_inappwebview_plugin
// Expire before tearing anything down: a Dart reply arriving from here on
// must find a dead token and drop the call.
aliveToken_.reset();
browserGate().removeWaiter(this);
WebViewDropTarget::UnregisterWebView(this);
userContentController = nullptr;
if (webView) {
Expand Down
Loading
Loading