// SPDX-License-Identifier: LGPL-2.1-or-later #include "reader_events.h" #include #include #include #include #include struct ReaderEventQueue::State { State(GDBusConnection *bus, const ReaderEndpoint &reader) : connection(G_DBUS_CONNECTION(g_object_ref(bus))), endpoint(reader), calls(g_cancellable_new()) {} ~State() { clear(); g_object_unref(calls); g_object_unref(connection); } GDBusConnection *connection; const ReaderEndpoint endpoint; GCancellable *calls; std::mutex mutex; std::condition_variable changed; std::deque pending; bool stopping = false, overflowReported = false; void clear() { for (auto *parameters : pending) g_variant_unref(parameters); pending.clear(); } bool send(GVariant *parameters, unsigned &failures) { GError *error = nullptr; GVariant *reply = g_dbus_connection_call_sync( connection, endpoint.busName, endpoint.objectPath, endpoint.interfaceName, "PresentAccessibleEvent", parameters, G_VARIANT_TYPE("(b)"), G_DBUS_CALL_FLAGS_NONE, 500, calls, &error); // A false reply can simply mean filtered/duplicate feedback. Only a // failed exchange warrants diagnostics. Never include event contents. const bool delivered = reply != nullptr; if (!delivered && !g_cancellable_is_cancelled(calls)) { if (++failures == 1) std::fprintf(stderr, "wine-access: reader event reply failed (domain %s, code %d)\n", error ? g_quark_to_string(error->domain) : "unknown", error ? error->code : 0); } else if (delivered && failures) { std::fprintf(stderr, "wine-access: reader event replies resumed after %u failed calls\n", failures); failures = 0; } if (reply) g_variant_unref(reply); if (error) g_error_free(error); return delivered; } static DWORD WINAPI work(LPVOID pointer) { std::unique_ptr> retained( static_cast *>(pointer)); auto state = *retained; unsigned failures = 0; std::unique_ptr focus(nullptr, g_variant_unref); std::string focusSource; bool focusDelivered = false; for (;;) { GVariant *parameters; { std::unique_lock lock(state->mutex); state->changed.wait(lock, [&] { return state->stopping || !state->pending.empty(); }); if (state->stopping) break; parameters = state->pending.front(); state->pending.pop_front(); } const char *source = nullptr, *kind = nullptr; g_variant_get_child(parameters, 0, "&s", &source); g_variant_get_child(parameters, 1, "&s", &kind); if (std::string(kind) == "focus") { focus.reset(g_variant_ref(parameters)); focusSource = source; focusDelivered = state->send(parameters, failures); } else { // A lost focus exchange must not strand later editor actions. Replay // only the focus dependency, never a timed-out ordered text action. if (std::string(kind) == "text" && !focusDelivered && focus && focusSource == source) focusDelivered = state->send(focus.get(), failures); if (std::string(kind) != "text" || focusDelivered) { if (!state->send(parameters, failures)) focusDelivered = false; } } g_variant_unref(parameters); } return 0; } }; ReaderEventQueue::~ReaderEventQueue() { stop(); } bool ReaderEventQueue::start(GDBusConnection *connection, const ReaderEndpoint &endpoint) { if (state_) return false; state_ = std::make_shared(connection, endpoint); auto retained = new std::shared_ptr(state_); worker_ = CreateThread(nullptr, 0, State::work, retained, 0, nullptr); if (!worker_) { delete retained; state_.reset(); return false; } return true; } bool ReaderEventQueue::submit(GVariant *parameters) { parameters = g_variant_ref_sink(parameters); if (state_) { std::lock_guard lock(state_->mutex); // Keep memory bounded if the reader remains unavailable. Do not reorder // actions or evict the focus on which queued editor actions depend. if (!state_->stopping && state_->pending.size() < 128) { state_->pending.push_back(parameters); state_->changed.notify_one(); return true; } if (!state_->stopping && !state_->overflowReported) { std::fprintf(stderr, "wine-access: reader event queue full; feedback dropped\n"); state_->overflowReported = true; } } g_variant_unref(parameters); return false; } void ReaderEventQueue::stop() { if (!state_) return; { std::lock_guard lock(state_->mutex); state_->stopping = true; state_->clear(); state_->changed.notify_one(); } g_cancellable_cancel(state_->calls); // The worker retains its connection and queue state if cancellation has // not completed yet; it never references event-thread globals. WaitForSingleObject(worker_, 100); CloseHandle(worker_); worker_ = nullptr; state_.reset(); }