/* * synergy -- mouse and keyboard sharing utility * Copyright (C) 2022 Red Hat, Inc. * Copyright (C) 2024 Symless Ltd. * * This package is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * found in the file LICENSE that should have accompanied this file. * * This package is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "config.h" #if HAVE_LIBPORTAL_INPUTCAPTURE #include "base/Event.h" #include "base/Log.h" #include "base/TMethodJob.h" #include "platform/PortalInputCapture.h" #include // for EIS fd hack, remove #include // for EIS fd hack, remove namespace synergy { enum signals { SESSION_CLOSED, DISABLED, ACTIVATED, DEACTIVATED, ZONES_CHANGED, _N_SIGNALS, }; PortalInputCapture::PortalInputCapture(EiScreen *screen, IEventQueue *events) : screen_(screen), events_(events), portal_(xdp_portal_new()), signals_(_N_SIGNALS) { glib_main_loop_ = g_main_loop_new(nullptr, true); glib_thread_ = new Thread(new TMethodJob( this, &PortalInputCapture::glib_thread)); auto init_capture_cb = [](gpointer data) -> gboolean { return reinterpret_cast(data) ->init_input_capture_session(); }; g_idle_add(init_capture_cb, this); } PortalInputCapture::~PortalInputCapture() { if (g_main_loop_is_running(glib_main_loop_)) g_main_loop_quit(glib_main_loop_); if (glib_thread_) { glib_thread_->cancel(); glib_thread_->wait(); glib_thread_ = nullptr; g_main_loop_unref(glib_main_loop_); glib_main_loop_ = nullptr; } if (session_) { XdpSession *parent_session = xdp_input_capture_session_get_session(session_); g_signal_handler_disconnect( G_OBJECT(parent_session), signals_[SESSION_CLOSED]); g_signal_handler_disconnect(session_, signals_[DISABLED]); g_signal_handler_disconnect(session_, signals_[ACTIVATED]); g_signal_handler_disconnect(session_, signals_[DEACTIVATED]); g_signal_handler_disconnect(session_, signals_[ZONES_CHANGED]); g_object_unref(session_); } for (auto b : barriers_) { g_object_unref(b); } barriers_.clear(); g_object_unref(portal_); } gboolean PortalInputCapture::timeout_handler() { return true; // keep re-triggering } int PortalInputCapture::fake_eis_fd() { auto path = std::getenv("LIBEI_SOCKET"); if (!path) { LOG_DEBUG("cannot fake eis socket, env var not set: LIBEI_SOCKET"); return -1; } auto sock = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0); // Dealing with the socket directly because nothing in lib/... supports // AF_UNIX and I'm too lazy to fix all this for a temporary hack int fd = sock; struct sockaddr_un addr = { .sun_family = AF_UNIX, .sun_path = {0}, }; std::snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", path); auto result = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); if (result != 0) { LOG_DEBUG("faked eis fd failed: %s", strerror(errno)); } return sock; } void PortalInputCapture::cb_session_closed(XdpSession *session) { LOG_ERR("portal input capture session was closed, exiting"); g_main_loop_quit(glib_main_loop_); events_->addEvent(Event::kQuit); g_signal_handler_disconnect(session, signals_[SESSION_CLOSED]); signals_[SESSION_CLOSED] = 0; } void PortalInputCapture::cb_init_input_capture_session( GObject *object, GAsyncResult *res) { LOG_DEBUG("portal session ready"); g_autoptr(GError) error = nullptr; auto session = xdp_portal_create_input_capture_session_finish( XDP_PORTAL(object), res, &error); if (!session) { LOG_ERR( "failed to initialize input capture session, quitting: %s", error->message); g_main_loop_quit(glib_main_loop_); events_->addEvent(Event::kQuit); return; } session_ = session; auto fd = xdp_input_capture_session_connect_to_eis(session, &error); if (fd < 0) { LOG_ERR("failed to connect to eis: %s", error->message); // FIXME: Development hack to avoid having to assemble all parts just for // testing this code. fd = fake_eis_fd(); if (fd < 0) { g_main_loop_quit(glib_main_loop_); events_->addEvent(Event::kQuit); return; } } // Socket ownership is transferred to the EiScreen events_->addEvent(Event( events_->forEi().connected(), screen_->getEventTarget(), EiScreen::EiConnectInfo::alloc(fd))); // FIXME: the lambda trick doesn't work here for unknown reasons, we need // the static function signals_[DISABLED] = g_signal_connect( G_OBJECT(session), "disabled", G_CALLBACK(cb_disabled_cb), this); signals_[ACTIVATED] = g_signal_connect( G_OBJECT(session_), "activated", G_CALLBACK(cb_activated_cb), this); signals_[DEACTIVATED] = g_signal_connect( G_OBJECT(session_), "deactivated", G_CALLBACK(cb_deactivated_cb), this); signals_[ZONES_CHANGED] = g_signal_connect( G_OBJECT(session_), "zones-changed", G_CALLBACK(cb_zones_changed_cb), this); XdpSession *parent_session = xdp_input_capture_session_get_session(session); signals_[SESSION_CLOSED] = g_signal_connect( G_OBJECT(parent_session), "closed", G_CALLBACK(cb_session_closed_cb), this); cb_zones_changed(session_, nullptr); } void PortalInputCapture::cb_set_pointer_barriers( GObject *object, GAsyncResult *res) { g_autoptr(GError) error = nullptr; auto failed_list = xdp_input_capture_session_set_pointer_barriers_finish( session_, res, &error); if (failed_list) { auto it = failed_list; while (it) { guint id; g_object_get(it->data, "id", &id, nullptr); for (auto elem = barriers_.begin(); elem != barriers_.end(); elem++) { if (*elem == it->data) { int x1, x2, y1, y2; g_object_get( G_OBJECT(*elem), "x1", &x1, "x2", &x2, "y1", &y1, "y2", &y2, nullptr); LOG_WARN( "failed to apply barrier %d (%d/%d-%d/%d)", id, x1, y1, x2, y2); g_object_unref(*elem); barriers_.erase(elem); break; } } it = it->next; } } g_list_free_full(failed_list, g_object_unref); enable(); } gboolean PortalInputCapture::init_input_capture_session() { LOG_DEBUG("setting up input capture session"); xdp_portal_create_input_capture_session( portal_, nullptr, // parent static_cast( XDP_INPUT_CAPABILITY_KEYBOARD | XDP_INPUT_CAPABILITY_POINTER), nullptr, // cancellable [](GObject *obj, GAsyncResult *res, gpointer data) { reinterpret_cast(data) ->cb_init_input_capture_session(obj, res); }, this); return false; } void PortalInputCapture::enable() { if (!enabled_) { LOG_DEBUG("enabling the portal input capture session"); xdp_input_capture_session_enable(session_); enabled_ = true; } } void PortalInputCapture::disable() { if (enabled_) { LOG_DEBUG("disabling the portal input capture session"); xdp_input_capture_session_disable(session_); enabled_ = false; } } void PortalInputCapture::release() { LOG_DEBUG("releasing input capture session, id=%d", activation_id_); xdp_input_capture_session_release(session_, activation_id_); is_active_ = false; } void PortalInputCapture::release(double x, double y) { LOG_DEBUG( "releasing input capture session, id=%d x=%.1f y=%.1f", activation_id_, x, y); xdp_input_capture_session_release_at(session_, activation_id_, x, y); is_active_ = false; } void PortalInputCapture::cb_disabled(XdpInputCaptureSession *session) { LOG_DEBUG("portal cb disabled"); if (!enabled_) return; // Nothing to do enabled_ = false; is_active_ = false; // FIXME: need some better heuristics here of when we want to enable again // But we don't know *why* we got disabled (and it's doubtfull we ever // will), so we just assume that the zones will change or something and we // can re-enable again // ... very soon g_timeout_add( 1000, [](gpointer data) -> gboolean { reinterpret_cast(data)->enable(); return false; }, this); } void PortalInputCapture::cb_activated( XdpInputCaptureSession *session, std::uint32_t activation_id, GVariant *options) { LOG_DEBUG("portal cb activated, id=%d", activation_id); if (options) { gdouble x, y; if (g_variant_lookup(options, "cursor_position", "(dd)", &x, &y)) { screen_->warpCursor((int)x, (int)y); } else { LOG_WARN("failed to get cursor position"); } } else { LOG_WARN("activation has no options"); } activation_id_ = activation_id; is_active_ = true; } void PortalInputCapture::cb_deactivated( XdpInputCaptureSession *session, std::uint32_t activation_id, GVariant *options) { LOG_DEBUG("cb deactivated, id=%i", activation_id); is_active_ = false; } void PortalInputCapture::cb_zones_changed( XdpInputCaptureSession *session, GVariant *options) { for (auto b : barriers_) g_object_unref(b); barriers_.clear(); auto zones = xdp_input_capture_session_get_zones(session); while (zones != nullptr) { guint w, h; gint x, y; g_object_get( zones->data, "width", &w, "height", &h, "x", &x, "y", &y, nullptr); LOG_DEBUG("input capture zone, %dx%d@%d,%d", w, h, x, y); int x1, x2, y1, y2; // Hardcoded behaviour: our pointer barriers are always at the edges of // all zones. Since the implementation is supposed to reject the ones in // the wrong place, we can just install barriers everywhere and let EIS // figure it out. Also a lot easier to implement for now though it doesn't // cover differently-sized screens... auto id = barriers_.size() + 1; x1 = x; y1 = y; x2 = x + w - 1; y2 = y; LOG_DEBUG("barrier (top) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2); barriers_.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new( XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr))); id = barriers_.size() + 1; x1 = x + w; y1 = y; x2 = x + w; y2 = y + h - 1; LOG_DEBUG("barrier (right) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2); barriers_.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new( XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr))); id = barriers_.size() + 1; x1 = x; y1 = y; x2 = x; y2 = y + h - 1; LOG_DEBUG("barrier (left) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2); barriers_.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new( XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr))); id = barriers_.size() + 1; x1 = x; y1 = y + h; x2 = x + w - 1; y2 = y + h; LOG_DEBUG("barrier (bottom) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2); barriers_.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new( XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr))); zones = zones->next; } GList *list = nullptr; for (auto const &b : barriers_) { list = g_list_append(list, b); } xdp_input_capture_session_set_pointer_barriers( session_, list, nullptr, // cancellable [](GObject *obj, GAsyncResult *res, gpointer data) { reinterpret_cast(data)->cb_set_pointer_barriers( obj, res); }, this); } void PortalInputCapture::glib_thread(void *) { auto context = g_main_loop_get_context(glib_main_loop_); LOG_DEBUG("glib thread running"); while (g_main_loop_is_running(glib_main_loop_)) { Thread::testCancel(); g_main_context_iteration(context, true); } LOG_DEBUG("shutting down glib thread"); } } // namespace synergy #endif