Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions plugins/application-tray/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,26 @@
return reply && !replyError;
}

void Util::setX11WindowInputShape(const xcb_window_t& window, const QSize& size)

Check warning on line 270 in plugins/application-tray/util.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'setX11WindowInputShape' is never used.
{
// Idempotency guard: if this window's input shape is already the value we
// set last time, return early. Setting a shape (especially a 0x0 input
// shape) triggers Xwayland's miSetShape -> WindowsRestructured ->
// enter/leave recomputation, which broadcasts events to clients that
// selected LEAVE_NOTIFY. If nativeEventFilter receives that event and
// sets the same shape again, it forms a storm of hundreds of thousands
// of events per second and spins the CPU.
auto it = m_inputShapes.find(window);
if (it != m_inputShapes.end() && it.value() == size) {
// The stack mode is derived from the shape (non-empty -> ABOVE,
// empty -> BELOW), so an unchanged shape also means an unchanged
// stack mode. There is no other code path that restacks this window
// independently of its shape, hence no separate stacking cache is
// required.
return;
Comment thread
wineee marked this conversation as resolved.
}
m_inputShapes.insert(window, size);

xcb_rectangle_t rectangle;
rectangle.x = 0;
rectangle.y = 0;
Expand All @@ -282,6 +300,11 @@
xcb_flush(m_x11connection);
}

void Util::removeX11WindowInputShapeRecord(const xcb_window_t& window)

Check warning on line 303 in plugins/application-tray/util.cpp

View workflow job for this annotation

GitHub Actions / cppcheck

The function 'removeX11WindowInputShapeRecord' is never used.
{
m_inputShapes.remove(window);
}

uint8_t Util::getWindowVisualDepth(const xcb_window_t& window) const
{
auto attrCookie = xcb_get_window_attributes(m_x11connection, window);
Expand Down
7 changes: 7 additions & 0 deletions plugins/application-tray/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

#pragma once

#include <QByteArray>

Check warning on line 7 in plugins/application-tray/util.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QByteArray> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QHash>

Check warning on line 8 in plugins/application-tray/util.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QHash> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QImage>

Check warning on line 9 in plugins/application-tray/util.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QImage> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QSize>

Check warning on line 10 in plugins/application-tray/util.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QSize> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QSharedPointer>

Check warning on line 11 in plugins/application-tray/util.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QSharedPointer> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QSet>

Check warning on line 12 in plugins/application-tray/util.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QSet> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QObject>

Check warning on line 13 in plugins/application-tray/util.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QObject> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QMutex>

#include <cstdint>
Expand Down Expand Up @@ -44,6 +45,7 @@
QString getX11WindowName(const xcb_window_t& window);
bool isValidX11Window(const xcb_window_t& window) const;
void setX11WindowInputShape(const xcb_window_t& widnow, const QSize& size);
void removeX11WindowInputShapeRecord(const xcb_window_t& window);
uint8_t getWindowVisualDepth(const xcb_window_t& window) const;
QImage getX11WindowImageNonComposite(const xcb_window_t& window);
bool getX11WindowPixmapData(const xcb_window_t& window, QByteArray *data);
Expand Down Expand Up @@ -84,6 +86,11 @@

QSet<QString> m_currentIds;
QMutex m_idMutex;
// Records the last input shape set per window, used for idempotency:
// setting the same shape again triggers Xwayland to recompute enter/leave
// and emit new LEAVE_NOTIFY events, forming an event storm (see
// XembedProtocol::nativeEventFilter).
QHash<xcb_window_t, QSize> m_inputShapes;
};

}
75 changes: 74 additions & 1 deletion plugins/application-tray/xembedprotocolhandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,44 @@ bool XembedProtocol::nativeEventFilter(const QByteArray &eventType, void *messag
const auto responseType = XCB_EVENT_RESPONSE_TYPE(ev);
if (responseType == XCB_LEAVE_NOTIFY) {
xcb_leave_notify_event_t *lE = (xcb_leave_notify_event_t *)ev;
UTIL->setX11WindowInputShape(lE->event, QSize(0, 0));

// Only handle LEAVE_NOTIFY for windows we actually manage (created or
// reparented). Previously every client window's LEAVE_NOTIFY was handled,
// so a leave event from any X11 client (wine WeCom, nm-applet, etc.)
// triggered setX11WindowInputShape, which in turn caused another
// enter/leave recomputation, forming an event-storm infinite loop.
const xcb_window_t eventWindow = lE->event;
bool managed = false;
for (auto it = m_registedItem.cbegin(); it != m_registedItem.cend(); ++it) {
auto handler = it.value().dynamicCast<XembedProtocolHandler>();
if (handler && handler->ownsX11Window(eventWindow)) {
managed = true;
break;
}
}
if (!managed) {
return false;
}

UTIL->setX11WindowInputShape(eventWindow, QSize(0, 0));
return true;
} else if (responseType == XCB_DESTROY_NOTIFY) {
// The embedded client may destroy its icon window asynchronously; the
// DBus notification that removes the handler can lag behind by up to
// 200 ms. During that window X11 may recycle the destroyed window ID,
// so drop ownership synchronously here instead of waiting for the
// delayed tray-change callback.
auto *dE = reinterpret_cast<xcb_destroy_notify_event_t *>(ev);
const xcb_window_t destroyedWindow = dE->window;
for (auto it = m_registedItem.cbegin(); it != m_registedItem.cend(); ++it) {
auto handler = it.value().dynamicCast<XembedProtocolHandler>();
if (handler && handler->ownsX11Window(destroyedWindow)) {
handler->invalidate();
break;
}
}
// Fall through without consuming the event so FdoSelectionManager can
// still observe the destroy and undock the icon.
}

return false;
Expand Down Expand Up @@ -102,6 +138,10 @@ void XembedProtocol::onTrayIconsChanged()
for (auto alreadyRegistedItem : m_registedItem.keys()) {
if (!currentRegistedItems.contains(alreadyRegistedItem)) {
if (auto value = m_registedItem.value(alreadyRegistedItem, nullptr)) {
auto handler = value.dynamicCast<XembedProtocolHandler>();
if (handler) {
handler->invalidate();
}
uint pid = m_item2Pid[alreadyRegistedItem];
registeredMap.remove(pid);
m_item2Pid.remove(alreadyRegistedItem);
Expand All @@ -118,6 +158,9 @@ void XembedProtocol::onRemoveItemByPid(uint pid)
const auto keys = m_registedItem.keys();
auto it = std::find_if(keys.begin(), keys.end(), [this, pid] (uint id) { return pid == m_item2Pid[id]; });
if (it != keys.end()) {
if (auto handler = m_registedItem.value(*it).dynamicCast<XembedProtocolHandler>()) {
handler->invalidate();
}
m_item2Pid.remove(*it);
m_registedItem.remove(*it);
}
Expand All @@ -126,6 +169,7 @@ void XembedProtocol::onRemoveItemByPid(uint pid)
XembedProtocolHandler::XembedProtocolHandler(const uint32_t& id, QObject* parent)
: AbstractTrayProtocolHandler(parent)
, m_enabled(false)
, m_owned(true)
, m_windowId(id)
, m_containerWid(0)
, m_hoverTimer(new QTimer(this))
Expand Down Expand Up @@ -162,7 +206,12 @@ XembedProtocolHandler::~XembedProtocolHandler()
{
if (m_containerWid) {
xcb_destroy_window(Util::instance()->getX11Connection(), m_containerWid);
Util::instance()->removeX11WindowInputShapeRecord(m_containerWid);
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
}
// The icon window itself also gets its input shape cached when
// nativeEventFilter processes its LEAVE_NOTIFY. Drop that record too, so
// a recycled X11 window ID is never mistaken for an already-shaped window.
Util::instance()->removeX11WindowInputShapeRecord(m_windowId);
UTIL->removeUniqueId(m_id);
}

Expand All @@ -177,6 +226,30 @@ uint32_t XembedProtocolHandler::windowId() const
return m_windowId;
}

bool XembedProtocolHandler::ownsX11Window(const xcb_window_t& window) const
{
// Only recognize the tray icon window we reparented and the container
// window we created for it, and only while the handler still owns them.
// After invalidate() the embedded icon is gone and X11 may recycle the
// window ID, so we must stop claiming ownership.
return m_owned && (window == m_windowId || window == m_containerWid);
Comment thread
wineee marked this conversation as resolved.
}

void XembedProtocolHandler::invalidate()
{
if (!m_owned) {
return;
}
m_owned = false;
// Drop the cached input shapes now: X11 may reuse these window IDs, and a
// stale "already 0x0" record would make the idempotency guard skip real
// shape requests for a new, unrelated window.
Util::instance()->removeX11WindowInputShapeRecord(m_windowId);
if (m_containerWid) {
Util::instance()->removeX11WindowInputShapeRecord(m_containerWid);
}
}

QString XembedProtocolHandler::id() const
{
return m_id;
Expand Down
14 changes: 13 additions & 1 deletion plugins/application-tray/xembedprotocolhandler.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2024-2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

Expand Down Expand Up @@ -52,6 +52,17 @@ class XembedProtocolHandler : public AbstractTrayProtocolHandler

virtual uint32_t windowId() const override;

// Checks whether an X11 window is managed by this handler (the reparented
// icon window or its container). Used by nativeEventFilter to only handle
// LEAVE_NOTIFY of its own windows, avoiding shape event storms triggered by
// other clients' windows.
bool ownsX11Window(const xcb_window_t& window) const;

// Invalidates ownership when the embedded icon window is destroyed. X11 may
// reuse the window ID, so we must stop claiming it and drop the cached
// input shape to avoid mismatching a recycled ID.
void invalidate();

virtual QString id() const override;

virtual QString title() const override;
Expand Down Expand Up @@ -88,6 +99,7 @@ public Q_SLOTS:
};

bool m_enabled;
bool m_owned;
uint32_t m_windowId;
xcb_window_t m_containerWid;
QPixmap m_icon;
Expand Down
Loading