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