From 5c96c38e3904904b22f32d86dcc8acd9ecf57603 Mon Sep 17 00:00:00 2001 From: Kai Uwe Broulik Date: Thu, 23 Nov 2023 14:48:17 +0100 Subject: [PATCH] Support kill prompt for XdgTopLevelWindows Instead of killing the window without asking, show the kill prompt like it's done for X11 windows. The window in question is exported through XDG foreign so the kill helper can parent itself to it, and an activation token is also provided. Also, the more contemporary desktop file name is now used for identification rather than window class. A no-display desktop file is installed for the kill helper so that it can get a proper window icon and suppress startup notification. --- autotests/integration/xdgshellwindow_test.cpp | 17 +- src/CMakeLists.txt | 2 + src/config-kwin.h.cmake | 2 +- src/helpers/killer/CMakeLists.txt | 6 + src/helpers/killer/killer.cpp | 145 +++++++++++++++--- .../killer/org.kde.kwin.killer.desktop.in | 9 ++ src/killprompt.cpp | 25 ++- src/killprompt.h | 2 +- src/xdgshellwindow.cpp | 17 +- src/xdgshellwindow.h | 2 + 10 files changed, 189 insertions(+), 38 deletions(-) create mode 100755 src/helpers/killer/org.kde.kwin.killer.desktop.in diff --git a/autotests/integration/xdgshellwindow_test.cpp b/autotests/integration/xdgshellwindow_test.cpp index 7f9d657f36..488cc264c2 100644 --- a/autotests/integration/xdgshellwindow_test.cpp +++ b/autotests/integration/xdgshellwindow_test.cpp @@ -822,19 +822,20 @@ void TestXdgShellWindow::testUnresponsiveWindow() QVERIFY(unresponsiveSpy.wait()); // window should be marked unresponsive but not killed auto elapsed1 = QDateTime::currentMSecsSinceEpoch() - startTime; - const int timeout = options->killPingTimeout() / 2; + const int timeout = options->killPingTimeout() / 2; // first timeout at half the time is for "unresponsive". QVERIFY(elapsed1 > timeout - 200 && elapsed1 < timeout + 200); // coarse timers on a test across two processes means we need a fuzzy compare QVERIFY(killWindow->unresponsive()); QVERIFY(killedSpy.isEmpty()); - QVERIFY(deletedSpy.wait()); - if (!socketMode) { - // process was killed - because we're across process this could happen in either order - QVERIFY(killedSpy.count() || killedSpy.wait()); - } + // TODO verify that kill prompt works. + killWindow->killWindow(); + process->kill(); - auto elapsed2 = QDateTime::currentMSecsSinceEpoch() - startTime; - QVERIFY(elapsed2 > timeout * 2 - 200); // second ping comes in later + QVERIFY(killedSpy.wait()); + + if (deletedSpy.isEmpty()) { + QVERIFY(deletedSpy.wait()); + } } void TestXdgShellWindow::testAppMenu() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cc981ef0bd..4d1ac5249b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -4,6 +4,8 @@ ecm_setup_version(${PROJECT_VERSION} SOVERSION 6 ) +set(KWIN_KILLER_BIN ${CMAKE_INSTALL_FULL_LIBEXECDIR}/kwin_killer_helper) + configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h) set(kwin_effects_dbus_xml ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.kwin.Effects.xml) diff --git a/src/config-kwin.h.cmake b/src/config-kwin.h.cmake index b350e91f79..ffbf1b32e9 100644 --- a/src/config-kwin.h.cmake +++ b/src/config-kwin.h.cmake @@ -12,7 +12,7 @@ constexpr QLatin1String KWIN_CONFIG("kwinrc"); constexpr QLatin1String KWIN_VERSION_STRING("${PROJECT_VERSION}"); constexpr QLatin1String XCB_VERSION_STRING("${XCB_VERSION}"); -constexpr QLatin1String KWIN_KILLER_BIN("${CMAKE_INSTALL_FULL_LIBEXECDIR}/kwin_killer_helper"); +constexpr QLatin1String KWIN_KILLER_BIN("${KWIN_KILLER_BIN}"); #cmakedefine01 HAVE_X11_XCB #cmakedefine01 HAVE_X11_XINPUT #cmakedefine01 HAVE_GBM_BO_GET_FD_FOR_PLANE diff --git a/src/helpers/killer/CMakeLists.txt b/src/helpers/killer/CMakeLists.txt index bca6afebee..bb6a5774a5 100644 --- a/src/helpers/killer/CMakeLists.txt +++ b/src/helpers/killer/CMakeLists.txt @@ -4,12 +4,18 @@ set(kwin_killer_helper_SRCS killer.cpp) add_executable(kwin_killer_helper ${kwin_killer_helper_SRCS}) +qt6_generate_wayland_protocol_client_sources(kwin_killer_helper FILES ${WaylandProtocols_DATADIR}/unstable/xdg-foreign/xdg-foreign-unstable-v2.xml) + target_link_libraries(kwin_killer_helper KF6::AuthCore KF6::I18n KF6::WidgetsAddons Qt::GuiPrivate + Qt::WaylandClient Qt::Widgets ) install(TARGETS kwin_killer_helper DESTINATION ${KDE_INSTALL_LIBEXECDIR}) + +configure_file(org.kde.kwin.killer.desktop.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kwin.killer.desktop @ONLY) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kwin.killer.desktop DESTINATION ${KDE_INSTALL_APPDIR}) diff --git a/src/helpers/killer/killer.cpp b/src/helpers/killer/killer.cpp index eea3c9cec9..fc8d2dd6cc 100644 --- a/src/helpers/killer/killer.cpp +++ b/src/helpers/killer/killer.cpp @@ -1,5 +1,6 @@ /* SPDX-FileCopyrightText: 2003 Lubos Lunak + SPDX-FileCopyrightText: 2023 Kai Uwe Broulik SPDX-License-Identifier: MIT @@ -8,26 +9,68 @@ #include #include #include +#include + #include #include #include #include +#include +#include + +#include + #include #include #include #include +#include + +#include "qwayland-xdg-foreign-unstable-v2.h" + +class XdgImported : public QtWayland::zxdg_imported_v2 +{ +public: + XdgImported(::zxdg_imported_v2 *object) + : QtWayland::zxdg_imported_v2(object) + { + } + ~XdgImported() override + { + destroy(); + } +}; + +class XdgImporter : public QWaylandClientExtensionTemplate, public QtWayland::zxdg_importer_v2 +{ +public: + XdgImporter() + : QWaylandClientExtensionTemplate(1) + { + } + ~XdgImporter() override + { + if (isActive()) { + destroy(); + } + } + XdgImported *import(const QString &handle) + { + return new XdgImported(import_toplevel(handle)); + } +}; int main(int argc, char *argv[]) { KLocalizedString::setApplicationDomain(QByteArrayLiteral("kwin")); - qputenv("QT_QPA_PLATFORM", QByteArrayLiteral("xcb")); QApplication app(argc, argv); QApplication::setWindowIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); QCoreApplication::setApplicationName(QStringLiteral("kwin_killer_helper")); QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); QApplication::setApplicationDisplayName(i18n("Window Manager")); QCoreApplication::setApplicationVersion(QStringLiteral("1.0")); + QApplication::setDesktopFileName(QStringLiteral("org.kde.kwin.killer")); QCommandLineOption pidOption(QStringLiteral("pid"), i18n("PID of the application to terminate"), i18n("pid")); @@ -55,16 +98,28 @@ int main(int argc, char *argv[]) parser.process(app); + const bool isX11 = app.platformName() == QLatin1String("xcb"); + QString hostname = parser.value(hostNameOption); bool pid_ok = false; pid_t pid = parser.value(pidOption).toULong(&pid_ok); QString caption = parser.value(windowNameOption); QString appname = parser.value(applicationNameOption); bool id_ok = false; - xcb_window_t id = parser.value(widOption).toULong(&id_ok); + xcb_window_t wid = XCB_WINDOW_NONE; + QString windowHandle; + if (isX11) { + wid = parser.value(widOption).toULong(&id_ok); + } else { + windowHandle = parser.value(widOption); + } + + // on Wayland XDG_ACTIVATION_TOKEN is set in the environment. bool time_ok = false; xcb_timestamp_t timestamp = parser.value(timestampOption).toULong(&time_ok); - if (!pid_ok || pid == 0 || !id_ok || id == XCB_WINDOW_NONE || !time_ok || timestamp == XCB_TIME_CURRENT_TIME + + if (!pid_ok || pid == 0 || ((!id_ok || wid == XCB_WINDOW_NONE) && windowHandle.isEmpty()) + || (isX11 && (!time_ok || timestamp == XCB_CURRENT_TIME)) || hostname.isEmpty() || caption.isEmpty() || appname.isEmpty()) { fprintf(stdout, "%s\n", qPrintable(i18n("This helper utility is not supposed to be called directly."))); parser.showHelp(1); @@ -88,26 +143,74 @@ int main(int argc, char *argv[]) KGuiItem continueButton = KGuiItem(i18n("&Terminate Application %1", appname), QStringLiteral("edit-bomb")); KGuiItem cancelButton = KGuiItem(i18n("Wait Longer"), QStringLiteral("chronometer")); - QX11Info::setAppUserTime(timestamp); - if (KMessageBox::warningContinueCancelWId(id, question, QString(), continueButton, cancelButton) == KMessageBox::Continue) { - if (!isLocal) { - QStringList lst; - lst << hostname << QStringLiteral("kill") << QString::number(pid); - QProcess::startDetached(QStringLiteral("xon"), lst); - } else { - if (::kill(pid, SIGKILL) && errno == EPERM) { - KAuth::Action killer(QStringLiteral("org.kde.ksysguard.processlisthelper.sendsignal")); - killer.setHelperId(QStringLiteral("org.kde.ksysguard.processlisthelper")); - killer.addArgument(QStringLiteral("pid0"), pid); - killer.addArgument(QStringLiteral("pidcount"), 1); - killer.addArgument(QStringLiteral("signal"), SIGKILL); - if (killer.isValid()) { - qDebug() << "Using KAuth to kill pid: " << pid; - killer.execute(); - } else { - qDebug() << "KWin process killer action not valid"; + + if (isX11) { + QX11Info::setAppUserTime(timestamp); + } + + auto *dialog = new KMessageDialog(KMessageDialog::WarningContinueCancel, question); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setCaption(QString()); // use default caption. + dialog->setIcon(QIcon()); // use default warning icon. + dialog->setButtons(continueButton, KGuiItem(), cancelButton); + dialog->winId(); + + std::unique_ptr xdgImporter; + std::unique_ptr importedParent; + + if (isX11) { + if (QWindow *foreignParent = QWindow::fromWinId(wid)) { + dialog->windowHandle()->setTransientParent(foreignParent); + } + } else { + xdgImporter = std::make_unique(); + } + + QObject::connect(dialog, &QDialog::finished, &app, [pid, hostname, isLocal](int result) { + if (result == KMessageBox::PrimaryAction) { + if (!isLocal) { + QStringList lst; + lst << hostname << QStringLiteral("kill") << QString::number(pid); + QProcess::startDetached(QStringLiteral("xon"), lst); + } else { + if (::kill(pid, SIGKILL) && errno == EPERM) { + KAuth::Action killer(QStringLiteral("org.kde.ksysguard.processlisthelper.sendsignal")); + killer.setHelperId(QStringLiteral("org.kde.ksysguard.processlisthelper")); + killer.addArgument(QStringLiteral("pid0"), pid); + killer.addArgument(QStringLiteral("pidcount"), 1); + killer.addArgument(QStringLiteral("signal"), SIGKILL); + if (killer.isValid()) { + qDebug() << "Using KAuth to kill pid: " << pid; + killer.execute(); + } else { + qDebug() << "KWin process killer action not valid"; + } } } } + + qApp->quit(); + }); + + dialog->show(); + + auto setTransientParent = [&xdgImporter, &importedParent, dialog, windowHandle] { + if (xdgImporter->isActive()) { + if (auto *waylandWindow = dialog->windowHandle()->nativeInterface()) { + importedParent.reset(xdgImporter->import(windowHandle)); + if (auto *surface = waylandWindow->surface()) { + importedParent->set_parent_of(surface); + } + } + } + }; + + if (xdgImporter) { + QObject::connect(xdgImporter.get(), &XdgImporter::activeChanged, dialog, setTransientParent); + setTransientParent(); } + + dialog->windowHandle()->requestActivate(); + + return app.exec(); } diff --git a/src/helpers/killer/org.kde.kwin.killer.desktop.in b/src/helpers/killer/org.kde.kwin.killer.desktop.in new file mode 100755 index 0000000000..974eeec16d --- /dev/null +++ b/src/helpers/killer/org.kde.kwin.killer.desktop.in @@ -0,0 +1,9 @@ +[Desktop Entry] +Type=Application +Name=KWin Kill Helper +Comment=Prompts whether to kill an unresponsive window +Exec=@KWIN_KILLER_BIN@ +Terminal=false +NoDisplay=true +Icon=tools-report-bug +StartupNotify=false diff --git a/src/killprompt.cpp b/src/killprompt.cpp index 0206d8edce..e1b4433108 100644 --- a/src/killprompt.cpp +++ b/src/killprompt.cpp @@ -7,7 +7,13 @@ #include "killprompt.h" #include "client_machine.h" +#include "wayland/display.h" +#include "wayland/seat.h" +#include "wayland/xdgforeign_v2.h" +#include "wayland_server.h" #include "x11window.h" +#include "xdgactivationv1.h" +#include "xdgshellwindow.h" #include #include @@ -19,7 +25,7 @@ namespace KWin KillPrompt::KillPrompt(Window *window) : m_window(window) { - Q_ASSERT(qobject_cast(window)); + Q_ASSERT(qobject_cast(window) || qobject_cast(window)); m_process.setProcessChannelMode(QProcess::ForwardedChannels); @@ -49,22 +55,37 @@ void KillPrompt::start(quint32 timestamp) QString wid; QString timestampString; QString hostname = QStringLiteral("localhost"); + QString appId = !m_window->desktopFileName().isEmpty() ? m_window->desktopFileName() : m_window->resourceClass(); + QString platform; if (auto *x11Window = qobject_cast(m_window)) { + platform = QStringLiteral("xcb"); wid = QString::number(x11Window->window()); timestampString = QString::number(timestamp); if (!x11Window->clientMachine()->isLocal()) { hostname = x11Window->clientMachine()->hostName(); } + } else if (auto *xdgToplevel = qobject_cast(m_window)) { + platform = QStringLiteral("wayland"); + auto *exported = waylandServer()->exportAsForeign(xdgToplevel->surface()); + wid = exported->handle(); + + auto *seat = waylandServer()->seat(); + const QString token = waylandServer()->xdgActivationIntegration()->requestPrivilegedToken(nullptr, seat->display()->serial(), seat, QStringLiteral("org.kde.kwin.killer")); + env.insert(QStringLiteral("XDG_ACTIVATION_TOKEN"), token); + + env.remove(QStringLiteral("QT_WAYLAND_RECONNECT")); } QStringList args{ + QStringLiteral("-platform"), + platform, QStringLiteral("--pid"), QString::number(m_window->pid()), QStringLiteral("--windowname"), m_window->captionNormal(), QStringLiteral("--applicationname"), - m_window->resourceClass(), + appId, QStringLiteral("--wid"), wid, QStringLiteral("--hostname"), diff --git a/src/killprompt.h b/src/killprompt.h index d75e2df56c..64f41f9016 100644 --- a/src/killprompt.h +++ b/src/killprompt.h @@ -18,7 +18,7 @@ class KillPrompt public: /** * @brief Creates a kill helper process. - * @param window The window to kill, must be an X11Window + * @param window The window to kill, must be an X11Window or XdgToplevelWindow. */ explicit KillPrompt(Window *window); diff --git a/src/xdgshellwindow.cpp b/src/xdgshellwindow.cpp index 4c8ba58e37..768a377122 100644 --- a/src/xdgshellwindow.cpp +++ b/src/xdgshellwindow.cpp @@ -14,6 +14,7 @@ #include "activities.h" #endif #include "decorations/decorationbridge.h" +#include "killprompt.h" #include "placement.h" #include "pointer_input.h" #include "touch_input.h" @@ -453,6 +454,9 @@ XdgToplevelWindow::XdgToplevelWindow(XdgToplevelInterface *shellSurface) XdgToplevelWindow::~XdgToplevelWindow() { + if (m_killPrompt) { + m_killPrompt->quit(); + } } void XdgToplevelWindow::handleRoleDestroyed() @@ -1085,11 +1089,11 @@ void XdgToplevelWindow::handlePingTimeout(quint32 serial) if (pingIt.value() == PingReason::CloseWindow) { qCDebug(KWIN_CORE) << "Final ping timeout on a close attempt, asking to kill:" << caption(); - // for internal windows, killing the window will delete this - QPointer guard(this); - killWindow(); - if (!guard) { - return; + if (!m_killPrompt) { + m_killPrompt = std::make_unique(this); + } + if (!m_killPrompt->isRunning()) { + m_killPrompt->start(); } } m_pings.erase(pingIt); @@ -1108,6 +1112,9 @@ void XdgToplevelWindow::handlePongReceived(quint32 serial) { if (m_pings.remove(serial)) { setUnresponsive(false); + if (m_killPrompt) { + m_killPrompt->quit(); + } } } diff --git a/src/xdgshellwindow.h b/src/xdgshellwindow.h index 9cec3fba29..cd58f8c8aa 100644 --- a/src/xdgshellwindow.h +++ b/src/xdgshellwindow.h @@ -23,6 +23,7 @@ namespace KWin { class AppMenuInterface; +class KillPrompt; class PlasmaShellSurfaceInterface; class ServerSideDecorationInterface; class ServerSideDecorationPaletteInterface; @@ -227,6 +228,7 @@ private: bool m_isTransient = false; QPointer m_fullScreenRequestedOutput; std::shared_ptr m_nextDecoration; + std::unique_ptr m_killPrompt; }; class XdgPopupWindow final : public XdgSurfaceWindow