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.
This commit is contained in:
Kai Uwe Broulik 2023-11-23 14:48:17 +01:00 committed by Vlad Zahorodnii
parent 9b7718459e
commit 5c96c38e39
10 changed files with 189 additions and 38 deletions

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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})

View file

@ -1,5 +1,6 @@
/*
SPDX-FileCopyrightText: 2003 Lubos Lunak <l.lunak@kde.org>
SPDX-FileCopyrightText: 2023 Kai Uwe Broulik <kde@broulik.de>
SPDX-License-Identifier: MIT
@ -8,26 +9,68 @@
#include <KAuth/Action>
#include <KLocalizedString>
#include <KMessageBox>
#include <KMessageDialog>
#include <QApplication>
#include <QCommandLineParser>
#include <QDebug>
#include <QProcess>
#include <QWaylandClientExtensionTemplate>
#include <QWindow>
#include <qpa/qplatformwindow_p.h>
#include <private/qtx11extras_p.h>
#include <xcb/xcb.h>
#include <cerrno>
#include <csignal>
#include <memory>
#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<XdgImporter>, 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> xdgImporter;
std::unique_ptr<XdgImported> importedParent;
if (isX11) {
if (QWindow *foreignParent = QWindow::fromWinId(wid)) {
dialog->windowHandle()->setTransientParent(foreignParent);
}
} else {
xdgImporter = std::make_unique<XdgImporter>();
}
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<QNativeInterface::Private::QWaylandWindow>()) {
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();
}

View file

@ -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

View file

@ -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 <QDir>
#include <QFileInfo>
@ -19,7 +25,7 @@ namespace KWin
KillPrompt::KillPrompt(Window *window)
: m_window(window)
{
Q_ASSERT(qobject_cast<X11Window *>(window));
Q_ASSERT(qobject_cast<X11Window *>(window) || qobject_cast<XdgToplevelWindow *>(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<X11Window *>(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<XdgToplevelWindow *>(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"),

View file

@ -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);

View file

@ -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<QObject> guard(this);
killWindow();
if (!guard) {
return;
if (!m_killPrompt) {
m_killPrompt = std::make_unique<KillPrompt>(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();
}
}
}

View file

@ -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<Output> m_fullScreenRequestedOutput;
std::shared_ptr<KDecoration2::Decoration> m_nextDecoration;
std::unique_ptr<KillPrompt> m_killPrompt;
};
class XdgPopupWindow final : public XdgSurfaceWindow