Skip to content

Commit 4c0d407

Browse files
committed
fix(connectivity_plus): retry Windows Advise when the platform thread is input-synchronous
INetworkListManagerEvents Advise fails with RPC_E_CANTCALLOUT_ININPUTSYNCCALL when OnListen runs while the platform thread is inside an input-synchronous call (e.g. a SendMessage-driven window callback). Failing OnListen surfaces in Dart as an uncatchable FlutterError, so instead retry the subscription via a message-loop timer and report only persistent failures through the event sink, where stream onError handlers can observe them.
1 parent 2b61441 commit 4c0d407

1 file changed

Lines changed: 92 additions & 8 deletions

File tree

packages/connectivity_plus/connectivity_plus/windows/connectivity_plus_plugin.cpp

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
#include <flutter/plugin_registrar_windows.h>
1111
#include <flutter/standard_method_codec.h>
1212

13+
#include <algorithm>
1314
#include <functional>
15+
#include <map>
1416
#include <memory>
1517

1618
namespace {
@@ -56,10 +58,24 @@ class ConnectivityStreamHandler : public FlStreamHandler {
5658
OnCancelInternal(const flutter::EncodableValue *arguments) override;
5759

5860
private:
61+
bool TryStartListen();
62+
void ScheduleStartListenRetry();
63+
void CancelStartListenRetry();
64+
static void CALLBACK RetryTimerProc(HWND hwnd, UINT message, UINT_PTR id,
65+
DWORD time);
66+
67+
// Thread-affine (platform thread), like the stream handler itself.
68+
static std::map<UINT_PTR, ConnectivityStreamHandler *> retry_handlers_;
69+
5970
std::shared_ptr<NetworkManager> manager;
6071
std::unique_ptr<FlEventSink> sink;
72+
UINT_PTR retry_timer_id_ = 0;
73+
int retry_attempts_ = 0;
6174
};
6275

76+
std::map<UINT_PTR, ConnectivityStreamHandler *>
77+
ConnectivityStreamHandler::retry_handlers_;
78+
6379
ConnectivityPlusWindowsPlugin::ConnectivityPlusWindowsPlugin() {
6480
manager = std::make_shared<NetworkManager>();
6581
manager->Init();
@@ -146,25 +162,92 @@ ConnectivityStreamHandler::ConnectivityStreamHandler(
146162
std::shared_ptr<NetworkManager> manager)
147163
: manager(manager) {}
148164

149-
ConnectivityStreamHandler::~ConnectivityStreamHandler() {}
165+
ConnectivityStreamHandler::~ConnectivityStreamHandler() {
166+
CancelStartListenRetry();
167+
}
150168

151169
void ConnectivityStreamHandler::AddConnectivityEvent() {
152170
sink->Success(EncodeConnectivityTypes(manager->GetConnectivityTypes()));
153171
}
154172

173+
bool ConnectivityStreamHandler::TryStartListen() {
174+
auto callback =
175+
std::bind(&ConnectivityStreamHandler::AddConnectivityEvent, this);
176+
return manager->StartListen(callback);
177+
}
178+
179+
void ConnectivityStreamHandler::ScheduleStartListenRetry() {
180+
// 100ms, 200ms, 400ms, ... — enough to escape whatever input-synchronous
181+
// window the platform thread was inside.
182+
UINT delay = 100u << (std::min)(retry_attempts_, 4);
183+
retry_timer_id_ = SetTimer(nullptr, 0, delay, RetryTimerProc);
184+
if (retry_timer_id_ != 0) {
185+
retry_handlers_[retry_timer_id_] = this;
186+
}
187+
}
188+
189+
void ConnectivityStreamHandler::CancelStartListenRetry() {
190+
if (retry_timer_id_ != 0) {
191+
KillTimer(nullptr, retry_timer_id_);
192+
retry_handlers_.erase(retry_timer_id_);
193+
retry_timer_id_ = 0;
194+
}
195+
}
196+
197+
void CALLBACK ConnectivityStreamHandler::RetryTimerProc(HWND hwnd,
198+
UINT message,
199+
UINT_PTR id,
200+
DWORD time) {
201+
KillTimer(hwnd, id);
202+
auto it = retry_handlers_.find(id);
203+
if (it == retry_handlers_.end()) {
204+
return;
205+
}
206+
ConnectivityStreamHandler *self = it->second;
207+
retry_handlers_.erase(it);
208+
self->retry_timer_id_ = 0;
209+
210+
if (!self->sink) {
211+
return; // Cancelled while the retry was pending.
212+
}
213+
214+
if (self->TryStartListen()) {
215+
self->AddConnectivityEvent();
216+
return;
217+
}
218+
219+
if (self->manager->GetError() == RPC_E_CANTCALLOUT_ININPUTSYNCCALL &&
220+
++self->retry_attempts_ < 5) {
221+
self->ScheduleStartListenRetry();
222+
return;
223+
}
224+
225+
// Persistent failure: report through the sink, where Dart-side stream
226+
// onError handlers can observe it (unlike an OnListen error, which the
227+
// framework can only surface as an uncatchable FlutterError).
228+
self->sink->Error(std::to_string(self->manager->GetError()),
229+
"NetworkManager::StartListen", nullptr);
230+
}
231+
155232
std::unique_ptr<FlStreamHandlerError>
156233
ConnectivityStreamHandler::OnListenInternal(
157234
const flutter::EncodableValue *arguments,
158235
std::unique_ptr<FlEventSink> &&events) {
159236
sink = std::move(events);
160237

161-
auto callback =
162-
std::bind(&ConnectivityStreamHandler::AddConnectivityEvent, this);
163-
164-
if (!manager->StartListen(callback)) {
165-
return std::make_unique<FlStreamHandlerError>(
166-
std::to_string(manager->GetError()), "NetworkManager::StartListen",
167-
nullptr);
238+
if (!TryStartListen()) {
239+
if (manager->GetError() == RPC_E_CANTCALLOUT_ININPUTSYNCCALL) {
240+
// The platform thread is inside an input-synchronous call (e.g. a
241+
// SendMessage-driven window callback), where COM rejects the outgoing
242+
// Advise. The subscription itself is fine — retry once the message
243+
// loop spins, and still emit the initial snapshot below.
244+
retry_attempts_ = 0;
245+
ScheduleStartListenRetry();
246+
} else {
247+
return std::make_unique<FlStreamHandlerError>(
248+
std::to_string(manager->GetError()), "NetworkManager::StartListen",
249+
nullptr);
250+
}
168251
}
169252

170253
AddConnectivityEvent();
@@ -174,6 +257,7 @@ ConnectivityStreamHandler::OnListenInternal(
174257
std::unique_ptr<FlStreamHandlerError>
175258
ConnectivityStreamHandler::OnCancelInternal(
176259
const flutter::EncodableValue *arguments) {
260+
CancelStartListenRetry();
177261
manager->StopListen();
178262
sink.reset();
179263
return nullptr;

0 commit comments

Comments
 (0)