File indexing completed on 2024-12-29 05:17:37

0001 /*
0002  * Copyright 2023 David Edmundson <davidedmundson@kde.org>
0003  *
0004  * SPDX-License-Identifier: LicenseRef-KDE-Accepted-GPL
0005  * SPDX-FileCopyrightText: 2023 David Edmundson <kde@davidedmundson.co.uk>
0006  * SPDX-FileCopyrightText: 2023 Aleix Pol <aleixpol@kde.org>
0007  */
0008 
0009 #include "x11recordingnotifier.h"
0010 
0011 #include <cstdio>
0012 #include <cstdlib>
0013 #include <xcb/xcb.h>
0014 #include <xcb/xproto.h>
0015 #include <xcb/composite.h>
0016 #include <xcb/xcb_event.h>
0017 #include <xcb/xcbext.h>
0018 
0019 #include <xcb/record.h>
0020 #include <cstring>
0021 
0022 #include <QScopedPointer>
0023 #include <QDebug>
0024 #include <QSocketNotifier>
0025 #include <QScopeGuard>
0026 
0027 struct XCBResponse
0028 {
0029     ~XCBResponse();
0030     
0031     xcb_record_enable_context_reply_t *reply = nullptr;
0032     xcb_generic_error_t *error = nullptr;
0033 };
0034 
0035 XCBResponse::~XCBResponse() {
0036     std::free(this->reply);
0037     std::free(this->error);
0038 
0039     this->reply = nullptr;
0040     this->error = nullptr;
0041 }
0042 
0043 X11RecordingNotifier::X11RecordingNotifier(WId window, QObject *parent)
0044     : QObject(parent)
0045     , m_windowId(window)
0046 {
0047     // we use a separate connection as the X11 recording API blocks is super weird
0048     // and we get multiple replies to a request rather than events
0049     m_connection = xcb_connect(nullptr, nullptr);
0050     auto c = m_connection;
0051     xcb_generic_error_t *error;
0052 
0053     if (!c) {
0054         qWarning("Error to open local display. Auto activation will fail!\n");
0055         return;
0056     }
0057 
0058     int compositeExtensionOpCode = -1;
0059     {
0060         xcb_query_extension_cookie_t cookie = xcb_query_extension(c, strlen("Composite"), "Composite");
0061         QScopedPointer<xcb_query_extension_reply_t, QScopedPointerPodDeleter> reply(xcb_query_extension_reply(c, cookie, nullptr));
0062         compositeExtensionOpCode = reply->major_opcode;
0063     }
0064 
0065     // check the xcb_record extension exists
0066     {
0067         auto cookie = xcb_record_query_version(c, 0, 0);
0068         QScopedPointer<xcb_record_query_version_reply_t, QScopedPointerPodDeleter> reply(xcb_record_query_version_reply(c, cookie, &error));
0069         if (!reply) {
0070             qWarning() << ("Failed to create recording context");
0071         } else {
0072         }
0073     }
0074 
0075     xcb_record_range_t range = {};
0076     range.ext_requests.major.first = compositeExtensionOpCode;
0077     range.ext_requests.major.last = compositeExtensionOpCode;
0078     range.ext_requests.minor.first = XCB_COMPOSITE_REDIRECT_WINDOW;
0079     range.ext_requests.minor.last = XCB_COMPOSITE_UNREDIRECT_SUBWINDOWS;
0080     range.client_died = true;
0081     xcb_record_client_spec_t spec = XCB_RECORD_CS_ALL_CLIENTS;
0082 
0083     m_recordingContext = xcb_generate_id(c);
0084     auto cookie = xcb_record_create_context_checked(c, m_recordingContext, 0, 1, 1, &spec, &range);
0085     auto err = xcb_request_check(c, cookie);
0086     if (err) {
0087         qWarning() << ("Failed to create recording context");
0088     }
0089 
0090     auto enableCookie = xcb_record_enable_context(c, m_recordingContext).sequence;
0091     xcb_flush(c);
0092 
0093     auto notifier = new QSocketNotifier(xcb_get_file_descriptor(c), QSocketNotifier::Read, this);
0094     connect(notifier, &QSocketNotifier::activated, this, [this, enableCookie] {
0095         xcb_generic_event_t *event = nullptr;
0096         auto c = m_connection;
0097         while ((event = xcb_poll_for_event(m_connection))) {
0098             std::free(event);
0099         }
0100 
0101         XCBResponse record;
0102         while (enableCookie && xcb_poll_for_reply(c, enableCookie, (void **)&record.reply, &record.error)) {
0103             // xcb_poll_for_reply may set both reply and error to null if connection has error.
0104             // break if xcb_connection has error, no point to continue anyway.
0105             if (xcb_connection_has_error(c)) {
0106                 break;
0107             }
0108 
0109             if (record.error) {
0110                 break;
0111             }
0112 
0113             if (!record.reply) {
0114                 continue;
0115             }
0116 
0117             handleNewRecord(*record.reply);
0118             record = XCBResponse();
0119         }
0120     });
0121 }
0122 
0123 X11RecordingNotifier::~X11RecordingNotifier()
0124 {
0125     if (m_recordingContext) {
0126         xcb_record_free_context(m_connection, m_recordingContext);
0127     }
0128     if (m_connection) {
0129         xcb_disconnect(m_connection);
0130     }
0131 }
0132 
0133 bool X11RecordingNotifier::isRedirected() const
0134 {
0135     return !m_redirectionCount.isEmpty();
0136 }
0137 
0138 void X11RecordingNotifier::handleNewRecord(xcb_record_enable_context_reply_t &reply)
0139 {
0140     const bool wasRedirected = isRedirected();
0141     auto cleanup = qScopeGuard([wasRedirected, this] {
0142         if (isRedirected() != wasRedirected) {
0143             Q_EMIT isRedirectedChanged();
0144         }
0145     });
0146 
0147     if (reply.category == 3) {
0148         m_redirectionCount.remove(reply.xid_base);
0149         return;
0150     }
0151 
0152     if (reply.category != 1) {
0153         return;
0154     }
0155 
0156     xcb_composite_redirect_window_request_t *request = reinterpret_cast<xcb_composite_redirect_window_request_t *>(xcb_record_enable_context_data(&reply));
0157 
0158     if (!request) {
0159         return;
0160     }
0161 
0162     if (request->window != m_windowId) {
0163         return;
0164     }
0165 
0166     uint32_t caller = reply.xid_base;
0167     switch(request->minor_opcode) {
0168     case XCB_COMPOSITE_REDIRECT_WINDOW:
0169     case XCB_COMPOSITE_REDIRECT_SUBWINDOWS:
0170         m_redirectionCount[caller]++;
0171         break;
0172     case XCB_COMPOSITE_UNREDIRECT_WINDOW:
0173     case XCB_COMPOSITE_UNREDIRECT_SUBWINDOWS:
0174         if (m_redirectionCount[caller]-- == 0) {
0175             m_redirectionCount.remove(caller);
0176         }
0177         break;
0178     default:
0179         break;
0180     }
0181 }