Files
wine-access/review_service.cpp

310 lines
16 KiB
C++

// SPDX-License-Identifier: LGPL-2.1-or-later
#include "review_service.h"
#include <condition_variable>
#include <chrono>
#include <deque>
#include <atomic>
#include <cstring>
#include <unistd.h>
namespace {
constexpr const char *path = "/org/stormux/Cthulhu1/WineReview";
constexpr const char *xml = R"(<node><interface name="org.stormux.Cthulhu1.WineReview">
<method name="CaptureWindow"><arg type="s" direction="in"/><arg type="s" direction="in"/>
<arg type="t" direction="in"/><arg type="a{sv}" direction="out"/></method></interface></node>)";
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<std::shared_ptr<Job>> pending;
std::shared_ptr<Job> active;
std::atomic<bool> 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> &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<std::shared_ptr<State>*>(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<State> &self) {
if (announcing || owner.empty() || !same_user || stopping) return;
const uint64_t now=GetTickCount64();
if (!registered) {
if (now<next_registration) return;
next_registration=now+1000;
GError *error=nullptr;
GVariant *value=g_dbus_connection_call_sync(connection,owner.c_str(),endpoint.objectPath,endpoint.interfaceName,"RegisterWineReviewProvider",
g_variant_new("(ou)",path,1u),G_VARIANT_TYPE("(b)"),G_DBUS_CALL_FLAGS_NONE,100,nullptr,&error);
gboolean accepted=FALSE;
if (value) { g_variant_get(value,"(b)",&accepted); g_variant_unref(value); }
if (error) g_error_free(error);
registered=accepted;
}
ReviewFocus notice;
{ std::lock_guard<std::mutex> 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<State>(self));
}
static void method(GDBusConnection *,const gchar *sender,const gchar *,const gchar *,const gchar *,
GVariant *parameters,GDBusMethodInvocation *invocation,gpointer data) {
auto &state=*static_cast<State*>(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>(); job->deadline=received+500;
job->invocation=G_DBUS_METHOD_INVOCATION(g_object_ref(invocation));
std::lock_guard<std::mutex> 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<std::mutex> lock(mutex);
const auto now=GetTickCount64();
auto finish=[&](const std::shared_ptr<Job> &job) {
if (!job || job->replied) return;
if (job->ready && now<job->deadline && !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<std::shared_ptr<State>> retained(static_cast<std::shared_ptr<State>*>(pointer));
auto state=*retained;
const HRESULT com=CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
for (;;) {
std::shared_ptr<Job> job;
{
std::unique_lock<std::mutex> 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()<job->deadline) result=capture_review_window(state->store,job->focus,job->deadline);
{ std::lock_guard<std::mutex> 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<std::mutex> 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<State>(connection,store,endpoint);
auto state=state_;
auto worker_state=new std::shared_ptr<State>(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>(state);
dispatcher_=CreateThread(nullptr,0,State::dispatch,dispatch_state,0,nullptr);
if (!dispatcher_) {
delete dispatch_state;
{ std::lock_guard<std::mutex> lock(state->mutex); state->stopping=true; state->changed.notify_all(); }
WaitForSingleObject(worker_,1000); CloseHandle(worker_); worker_=nullptr;
state_.reset(); return false;
}
std::unique_lock<std::mutex> 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<std::shared_ptr<State>> retained(static_cast<std::shared_ptr<State>*>(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<std::mutex> 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<std::shared_ptr<State>*>(data);
self->expire(); self->changed.notify_one();
static_cast<void>(self);
self->announce(self);
return G_SOURCE_CONTINUE;
},new std::shared_ptr<State>(state),[](gpointer p) { delete static_cast<std::shared_ptr<State>*>(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<State*>(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<std::mutex> lock(state_->mutex);
state_->focus=focus;
}
void ReviewService::stop() {
auto state=state_;
if (!state) return;
{
std::lock_guard<std::mutex> lock(state->mutex);
state->stopping=true; state->changed.notify_all();
}
g_main_context_invoke(state->context,[](gpointer p)->gboolean {
auto state=static_cast<std::shared_ptr<State>*>(p);
g_main_loop_quit((*state)->loop); delete state; return G_SOURCE_REMOVE;
},new std::shared_ptr<State>(state));
if (dispatcher_) { WaitForSingleObject(dispatcher_,1000); CloseHandle(dispatcher_); dispatcher_=nullptr; }
std::unique_lock<std::mutex> 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();
}