310 lines
16 KiB
C++
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();
|
|
}
|