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:
parent
9b7718459e
commit
5c96c38e39
10 changed files with 189 additions and 38 deletions
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
9
src/helpers/killer/org.kde.kwin.killer.desktop.in
Executable file
9
src/helpers/killer/org.kde.kwin.killer.desktop.in
Executable 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
|
|
@ -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"),
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue