// SPDX-License-Identifier: LGPL-2.1-or-later #include "review_service.h" #include #include #include #include #include #include namespace { constexpr const char *path = "/org/stormux/Cthulhu1/WineReview"; constexpr const char *xml = R"( )"; GVariant *bounds(const RECT &r) { return g_variant_new("(iiii)", r.left, r.top, r.right-r.left, r.bottom-r.top); } void field(GVariantBuilder &b, const char *key, const std::string &value) { g_variant_builder_add(&b, "{sv}", key, g_variant_new_string(value.c_str())); } void flag(GVariantBuilder &b, const char *key, bool value) { g_variant_builder_add(&b,"{sv}",key,g_variant_new_boolean(value)); } GVariant *serialize(const ReviewCapture &capture) { GVariantBuilder b, nodes; g_variant_builder_init(&b,G_VARIANT_TYPE_VARDICT); g_variant_builder_add(&b,"{sv}","version",g_variant_new_uint32(1)); field(b,"instance",capture.source.instance); field(b,"window",capture.source.window); field(b,"focus",capture.source.focus); field(b,"status",capture.status); g_variant_builder_add(&b,"{sv}","revision",g_variant_new_uint64(capture.source.revision)); g_variant_builder_add(&b,"{sv}","bounds",bounds(capture.source.bounds)); g_variant_builder_init(&nodes,G_VARIANT_TYPE("aa{sv}")); if (capture.status=="ok") for (const auto &node:capture.nodes) { GVariantBuilder n,ranges; g_variant_builder_init(&n,G_VARIANT_TYPE_VARDICT); field(n,"id",node.id); field(n,"parent",node.parent); field(n,"role",node.role); field(n,"name",node.name); flag(n,"protected",node.protected_text); flag(n,"visible",node.visible); flag(n,"text_known",node.text_known); flag(n,"caret_known",node.caret_known); g_variant_builder_add(&n,"{sv}","caret",g_variant_new_uint64(node.caret)); g_variant_builder_add(&n,"{sv}","caret_utf16",g_variant_new_uint64(node.caret_utf16)); g_variant_builder_add(&n,"{sv}","bounds",bounds(node.bounds)); g_variant_builder_init(&ranges,G_VARIANT_TYPE("a(tttts(iiii))")); if (!node.protected_text) for (const auto &range:node.ranges) g_variant_builder_add(&ranges,"(tttts(iiii))",range.start,range.end,range.utf16_start,range.utf16_end, range.text.c_str(),range.bounds.left,range.bounds.top,range.bounds.right-range.bounds.left,range.bounds.bottom-range.bounds.top); g_variant_builder_add(&n,"{sv}","ranges",g_variant_builder_end(&ranges)); g_variant_builder_add_value(&nodes,g_variant_builder_end(&n)); } g_variant_builder_add(&b,"{sv}","nodes",g_variant_builder_end(&nodes)); GVariant *result=g_variant_ref_sink(g_variant_builder_end(&b)); if (g_variant_get_size(result)>2097152) { g_variant_unref(result); ReviewCapture limited; limited.source=capture.source; limited.status="limited"; return serialize(limited); } return result; } bool same_notice(const ReviewFocus &a,const ReviewFocus &b) { return a.instance==b.instance && a.window==b.window && a.focus==b.focus && a.revision==b.revision && a.title==b.title && a.role==b.role && a.protected_text==b.protected_text && EqualRect(&a.bounds,&b.bounds); } struct Job { ReviewFocus focus; uint64_t deadline; GDBusMethodInvocation *invocation; bool ready=false, replied=false; ReviewCapture capture; ~Job() { g_object_unref(invocation); } }; } // namespace struct ReviewService::State { explicit State(GDBusConnection *c,ReviewIdentityStore &s,const ReaderEndpoint &reader):connection(G_DBUS_CONNECTION(g_object_ref(c))),store(s),endpoint(reader) { context=g_main_context_new(); loop=g_main_loop_new(context,FALSE); calls=g_cancellable_new(); } ~State() { g_object_unref(calls); g_main_loop_unref(loop); g_main_context_unref(context); g_object_unref(connection); } GDBusConnection *connection; ReviewIdentityStore store; const ReaderEndpoint endpoint; GMainContext *context; GMainLoop *loop; GCancellable *calls; std::mutex mutex; std::condition_variable changed; ReviewFocus focus, sent; std::deque> pending; std::shared_ptr active; std::atomic stopping{false}; bool worker_done=false, ready=false, exported=false; // These fields belong exclusively to the GLib dispatcher. std::string owner; bool same_user=false, registered=false, announcing=false; uint64_t next_registration=0; guint object_id=0, owner_subscription=0; static DWORD WINAPI work(LPVOID); static DWORD WINAPI dispatch(LPVOID); void reply(const std::shared_ptr &job,const ReviewCapture &capture) { if (job->replied) return; job->replied=true; GVariant *data=serialize(capture); g_dbus_method_invocation_return_value(job->invocation,g_variant_new("(@a{sv})",data)); } std::string lookup_owner() { GError *error=nullptr; GVariant *value=g_dbus_connection_call_sync(connection,"org.freedesktop.DBus","/org/freedesktop/DBus", "org.freedesktop.DBus","GetNameOwner",g_variant_new("(s)",endpoint.busName),G_VARIANT_TYPE("(s)"), G_DBUS_CALL_FLAGS_NONE,100,nullptr,&error); std::string result; if (value) { const gchar *name=nullptr; g_variant_get(value,"(&s)",&name); result=name; g_variant_unref(value); } if (error) g_error_free(error); return result; } void refresh_owner() { const auto current=lookup_owner(); if (current==owner) return; owner=current; registered=false; same_user=false; sent={}; next_registration=0; if (owner.empty()) return; GError *error=nullptr; GVariant *value=g_dbus_connection_call_sync(connection,"org.freedesktop.DBus","/org/freedesktop/DBus", "org.freedesktop.DBus","GetConnectionUnixUser",g_variant_new("(s)",owner.c_str()),G_VARIANT_TYPE("(u)"), G_DBUS_CALL_FLAGS_NONE,100,nullptr,&error); if (value) { guint uid=0; g_variant_get(value,"(u)",&uid); same_user=uid==getuid(); g_variant_unref(value); } if (error) g_error_free(error); } static void called(GObject *connection,GAsyncResult *result,gpointer data) { auto state=static_cast*>(data); GError *error=nullptr; GVariant *value=g_dbus_connection_call_finish(G_DBUS_CONNECTION(connection),result,&error); // Completion from an older owner may only allow a retry, never grant // access or retain that owner's focus as the new provider registration. (*state)->announcing=false; gboolean accepted=FALSE; if (value) { g_variant_get(value,"(b)",&accepted); g_variant_unref(value); } if (!accepted) (*state)->sent={}; if (error) g_error_free(error); delete state; } void announce(const std::shared_ptr &self) { if (announcing || owner.empty() || !same_user || stopping) return; const uint64_t now=GetTickCount64(); if (!registered) { if (now lock(mutex); notice=focus; } if (!registered || notice.focus.empty() || same_notice(notice,sent)) return; sent=notice; announcing=true; g_dbus_connection_call(connection,owner.c_str(),endpoint.objectPath,endpoint.interfaceName,"PresentWineReviewFocus", g_variant_new("(ssstssb(iiii))",notice.instance.c_str(),notice.window.c_str(),notice.focus.c_str(),notice.revision, notice.title.c_str(),notice.role.c_str(),notice.protected_text,notice.bounds.left,notice.bounds.top, notice.bounds.right-notice.bounds.left,notice.bounds.bottom-notice.bounds.top), G_VARIANT_TYPE("(b)"),G_DBUS_CALL_FLAGS_NONE,100,calls,called,new std::shared_ptr(self)); } static void method(GDBusConnection *,const gchar *sender,const gchar *,const gchar *,const gchar *, GVariant *parameters,GDBusMethodInvocation *invocation,gpointer data) { auto &state=*static_cast(data); const uint64_t received=GetTickCount64(); if (!state.same_user || state.owner!=sender || state.lookup_owner()!=sender) { g_dbus_method_invocation_return_dbus_error(invocation,"org.freedesktop.DBus.Error.AccessDenied","Only the current selected reader service may capture Wine review."); return; } const char *window=nullptr,*focus=nullptr; guint64 revision=0; g_variant_get(parameters,"(&s&st)",&window,&focus,&revision); if (strlen(window)>256 || strlen(focus)>256) { g_dbus_method_invocation_return_dbus_error(invocation,"org.freedesktop.DBus.Error.InvalidArgs","Review identities exceed their limit."); return; } auto job=std::make_shared(); job->deadline=received+500; job->invocation=G_DBUS_METHOD_INVOCATION(g_object_ref(invocation)); std::lock_guard lock(state.mutex); job->focus=state.focus; ReviewCapture rejected; rejected.source=job->focus; if (state.stopping) { state.reply(job,rejected); return; } if (job->focus.window!=window || job->focus.focus!=focus || job->focus.revision!=revision) { rejected.status="stale"; state.reply(job,rejected); return; } if (state.pending.size()>=1) { rejected.status="busy"; state.reply(job,rejected); return; } state.pending.push_back(job); state.changed.notify_one(); } void expire() { std::lock_guard lock(mutex); const auto now=GetTickCount64(); auto finish=[&](const std::shared_ptr &job) { if (!job || job->replied) return; if (job->ready && nowdeadline && !stopping) reply(job,job->capture); else if (now>=job->deadline || stopping) { ReviewCapture expired; expired.source=job->focus; reply(job,expired); } }; finish(active); for (auto &job:pending) finish(job); while (!pending.empty() && pending.front()->replied) pending.pop_front(); if (active && active->ready && active->replied) active.reset(); } }; ReviewService::~ReviewService() { stop(); } DWORD WINAPI ReviewService::State::work(LPVOID pointer) { std::unique_ptr> retained(static_cast*>(pointer)); auto state=*retained; const HRESULT com=CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED); for (;;) { std::shared_ptr job; { std::unique_lock lock(state->mutex); state->changed.wait(lock,[&] { return state->stopping || (!state->active && !state->pending.empty()); }); if (state->stopping) break; job=state->pending.front(); state->pending.pop_front(); state->active=job; } ReviewCapture result; result.source=job->focus; if (SUCCEEDED(com) && GetTickCount64()deadline) result=capture_review_window(state->store,job->focus,job->deadline); { std::lock_guard lock(state->mutex); job->capture=std::move(result); job->ready=true; } // Dispatcher returns or expires the result before allowing the next // native capture. The capture STA never handles D-Bus invocations. } if (SUCCEEDED(com)) CoUninitialize(); { std::lock_guard lock(state->mutex); state->worker_done=true; state->changed.notify_all(); } return 0; } bool ReviewService::start(GDBusConnection *connection,ReviewIdentityStore &store,const ReaderEndpoint &endpoint) { if (state_) return false; state_=std::make_shared(connection,store,endpoint); auto state=state_; auto worker_state=new std::shared_ptr(state); worker_=CreateThread(nullptr,0,State::work,worker_state,0,nullptr); if (!worker_) { delete worker_state; state_.reset(); return false; } auto dispatch_state=new std::shared_ptr(state); dispatcher_=CreateThread(nullptr,0,State::dispatch,dispatch_state,0,nullptr); if (!dispatcher_) { delete dispatch_state; { std::lock_guard lock(state->mutex); state->stopping=true; state->changed.notify_all(); } WaitForSingleObject(worker_,1000); CloseHandle(worker_); worker_=nullptr; state_.reset(); return false; } std::unique_lock lock(state->mutex); state->changed.wait_for(lock,std::chrono::seconds(1),[&] { return state->ready; }); return state->exported; } DWORD WINAPI ReviewService::State::dispatch(LPVOID pointer) { std::unique_ptr> retained(static_cast*>(pointer)); auto state=*retained; g_main_context_push_thread_default(state->context); GError *error=nullptr; GDBusNodeInfo *info=g_dbus_node_info_new_for_xml(xml,&error); static const GDBusInterfaceVTable vtable{State::method,nullptr,nullptr,{nullptr}}; if (info) state->object_id=g_dbus_connection_register_object(state->connection,path,info->interfaces[0],&vtable,state.get(),nullptr,&error); if (error) g_error_free(error); if (info) g_dbus_node_info_unref(info); { std::lock_guard lock(state->mutex); state->exported=state->object_id!=0; state->ready=true; state->changed.notify_all(); } GSource *tick=g_timeout_source_new(10); g_source_set_callback(tick,[](gpointer data)->gboolean { auto self=*static_cast*>(data); self->expire(); self->changed.notify_one(); static_cast(self); self->announce(self); return G_SOURCE_CONTINUE; },new std::shared_ptr(state),[](gpointer p) { delete static_cast*>(p); }); g_source_attach(tick,state->context); state->owner_subscription=g_dbus_connection_signal_subscribe(state->connection,"org.freedesktop.DBus","org.freedesktop.DBus", "NameOwnerChanged","/org/freedesktop/DBus",state->endpoint.busName,G_DBUS_SIGNAL_FLAGS_NONE, [](GDBusConnection*,const gchar*,const gchar*,const gchar*,const gchar*,GVariant*,gpointer p) { static_cast(p)->refresh_owner(); },state.get(),nullptr); state->refresh_owner(); g_main_loop_run(state->loop); g_cancellable_cancel(state->calls); while (state->announcing) g_main_context_iteration(state->context,TRUE); state->expire(); g_source_destroy(tick); g_source_unref(tick); if (state->owner_subscription) g_dbus_connection_signal_unsubscribe(state->connection,state->owner_subscription); if (state->object_id) g_dbus_connection_unregister_object(state->connection,state->object_id); g_main_context_pop_thread_default(state->context); return 0; } void ReviewService::focus_changed(const ReviewFocus &focus) { if (!state_) return; std::lock_guard lock(state_->mutex); state_->focus=focus; } void ReviewService::stop() { auto state=state_; if (!state) return; { std::lock_guard lock(state->mutex); state->stopping=true; state->changed.notify_all(); } g_main_context_invoke(state->context,[](gpointer p)->gboolean { auto state=static_cast*>(p); g_main_loop_quit((*state)->loop); delete state; return G_SOURCE_REMOVE; },new std::shared_ptr(state)); if (dispatcher_) { WaitForSingleObject(dispatcher_,1000); CloseHandle(dispatcher_); dispatcher_=nullptr; } std::unique_lock lock(state->mutex); state->changed.wait_for(lock,std::chrono::milliseconds(100),[&] { return state->worker_done; }); bool done=state->worker_done; lock.unlock(); if (worker_) { if (done) WaitForSingleObject(worker_,100); CloseHandle(worker_); worker_=nullptr; } state_.reset(); }