kwin/src/main_wayland.cpp
2022-06-19 05:49:20 +00:00

650 lines
24 KiB
C++

/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "main_wayland.h"
#include <config-kwin.h>
#include "composite.h"
#include "effects.h"
#include "inputmethod.h"
#include "platform.h"
#include "tabletmodemanager.h"
#include "utils/realtime.h"
#include "wayland/display.h"
#include "wayland/seat_interface.h"
#include "wayland_server.h"
#include "workspace.h"
#include "xwayland/xwayland.h"
#include "xwayland/xwaylandlauncher.h"
// KDE
#include <KCrash>
#include <KDesktopFile>
#include <KLocalizedString>
#include <KPluginMetaData>
#include <KShell>
// Qt
#include <QCommandLineParser>
#include <QDBusInterface>
#include <QDebug>
#include <QFileInfo>
#include <QProcess>
#include <QWindow>
#include <qplatformdefs.h>
#include <sched.h>
#include <sys/resource.h>
#include <iomanip>
#include <iostream>
Q_IMPORT_PLUGIN(KWinIntegrationPlugin)
Q_IMPORT_PLUGIN(KGlobalAccelImpl)
Q_IMPORT_PLUGIN(KWindowSystemKWinPlugin)
Q_IMPORT_PLUGIN(KWinIdleTimePoller)
#if PipeWire_FOUND
Q_IMPORT_PLUGIN(ScreencastManagerFactory)
#endif
namespace KWin
{
static rlimit originalNofileLimit = {
.rlim_cur = 0,
.rlim_max = 0,
};
static bool bumpNofileLimit()
{
if (getrlimit(RLIMIT_NOFILE, &originalNofileLimit) == -1) {
std::cerr << "Failed to bump RLIMIT_NOFILE limit, getrlimit() failed: " << strerror(errno) << std::endl;
return false;
}
rlimit limit = originalNofileLimit;
limit.rlim_cur = limit.rlim_max;
if (setrlimit(RLIMIT_NOFILE, &limit) == -1) {
std::cerr << "Failed to bump RLIMIT_NOFILE limit, setrlimit() failed: " << strerror(errno) << std::endl;
return false;
}
return true;
}
static void restoreNofileLimit()
{
if (setrlimit(RLIMIT_NOFILE, &originalNofileLimit) == -1) {
std::cerr << "Failed to restore RLIMIT_NOFILE limit, legacy apps might be broken" << std::endl;
}
}
static void sighandler(int)
{
QApplication::exit();
}
void disableDrKonqi()
{
KCrash::setDrKonqiEnabled(false);
}
// run immediately, before Q_CORE_STARTUP functions
// that would enable drkonqi
Q_CONSTRUCTOR_FUNCTION(disableDrKonqi)
//************************************
// ApplicationWayland
//************************************
ApplicationWayland::ApplicationWayland(int &argc, char **argv)
: ApplicationWaylandAbstract(OperationModeWaylandOnly, argc, argv)
{
}
ApplicationWayland::~ApplicationWayland()
{
setTerminating();
if (!waylandServer()) {
return;
}
// need to unload all effects prior to destroying X connection as they might do X calls
if (effects) {
static_cast<EffectsHandlerImpl *>(effects)->unloadAllEffects();
}
delete m_xwayland;
m_xwayland = nullptr;
destroyWorkspace();
destroyInputMethod();
destroyCompositor();
destroyInput();
}
void ApplicationWayland::performStartup()
{
if (m_startXWayland) {
setOperationMode(OperationModeXwayland);
setXwaylandScale(config()->group("Xwayland").readEntry("Scale", 1.0));
}
// first load options - done internally by a different thread
createOptions();
if (!platform()->initialize()) {
std::exit(1);
}
waylandServer()->initPlatform();
createColorManager();
createInput();
createInputMethod();
TabletModeManager::create(this);
createPlugins();
createScreens();
WaylandCompositor::create();
connect(Compositor::self(), &Compositor::sceneCreated, platform(), &Platform::sceneInitialized);
connect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::continueStartupWithScene);
}
void ApplicationWayland::continueStartupWithScene()
{
disconnect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::continueStartupWithScene);
// Note that we start accepting client connections after creating the Workspace.
createWorkspace();
if (!waylandServer()->start()) {
qFatal("Failed to initialze the Wayland server, exiting now");
}
if (operationMode() == OperationModeWaylandOnly) {
finalizeStartup();
return;
}
m_xwayland = new Xwl::Xwayland(this);
m_xwayland->xwaylandLauncher()->setListenFDs(m_xwaylandListenFds);
m_xwayland->xwaylandLauncher()->setDisplayName(m_xwaylandDisplay);
m_xwayland->xwaylandLauncher()->setXauthority(m_xwaylandXauthority);
connect(m_xwayland, &Xwl::Xwayland::errorOccurred, this, &ApplicationWayland::finalizeStartup);
connect(m_xwayland, &Xwl::Xwayland::started, this, &ApplicationWayland::finalizeStartup);
m_xwayland->start();
}
void ApplicationWayland::finalizeStartup()
{
if (m_xwayland) {
disconnect(m_xwayland, &Xwl::Xwayland::errorOccurred, this, &ApplicationWayland::finalizeStartup);
disconnect(m_xwayland, &Xwl::Xwayland::started, this, &ApplicationWayland::finalizeStartup);
}
startSession();
notifyStarted();
}
void ApplicationWayland::refreshSettings(const KConfigGroup &group, const QByteArrayList &names)
{
if (group.name() == "Wayland" && names.contains("InputMethod")) {
KDesktopFile file(group.readPathEntry("InputMethod", QString()));
InputMethod::self()->setInputMethodCommand(file.desktopGroup().readEntry("Exec", QString()));
}
if (m_startXWayland && group.name() == "Xwayland" && names.contains("Scale")) {
setXwaylandScale(group.readEntry("Scale", 1.0));
}
if (group.name() == "Wayland" && names.contains("EnablePrimarySelection")) {
waylandServer()->setEnablePrimarySelection(group.readEntry("EnablePrimarySelection", true));
}
}
void ApplicationWayland::startSession()
{
KSharedConfig::Ptr kwinSettings = kwinApp()->config();
m_settingsWatcher = KConfigWatcher::create(kwinSettings);
connect(m_settingsWatcher.data(), &KConfigWatcher::configChanged, this, &ApplicationWayland::refreshSettings);
if (!m_inputMethodServerToStart.isEmpty()) {
InputMethod::self()->setInputMethodCommand(m_inputMethodServerToStart);
} else {
refreshSettings(kwinSettings->group("Wayland"), {"InputMethod"});
}
// start session
if (!m_sessionArgument.isEmpty()) {
QStringList arguments = KShell::splitArgs(m_sessionArgument);
if (!arguments.isEmpty()) {
QString program = arguments.takeFirst();
QProcess *p = new QProcess(this);
p->setProcessChannelMode(QProcess::ForwardedErrorChannel);
p->setProcessEnvironment(processStartupEnvironment());
connect(p, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [p](int code, QProcess::ExitStatus status) {
p->deleteLater();
if (status == QProcess::CrashExit) {
qWarning() << "Session process has crashed";
QCoreApplication::exit(-1);
return;
}
if (code) {
qWarning() << "Session process exited with code" << code;
}
QCoreApplication::exit(code);
});
p->setProgram(program);
p->setArguments(arguments);
p->start();
} else {
qWarning("Failed to launch the session process: %s is an invalid command",
qPrintable(m_sessionArgument));
}
}
// start the applications passed to us as command line arguments
if (!m_applicationsToStart.isEmpty()) {
for (const QString &application : qAsConst(m_applicationsToStart)) {
QStringList arguments = KShell::splitArgs(application);
if (arguments.isEmpty()) {
qWarning("Failed to launch application: %s is an invalid command",
qPrintable(application));
continue;
}
QString program = arguments.takeFirst();
// note: this will kill the started process when we exit
// this is going to happen anyway as we are the wayland and X server the app connects to
QProcess *p = new QProcess(this);
p->setProcessChannelMode(QProcess::ForwardedErrorChannel);
p->setProcessEnvironment(processStartupEnvironment());
p->setProgram(program);
p->setArguments(arguments);
p->startDetached();
p->deleteLater();
}
}
}
static const QString s_waylandPlugin = QStringLiteral("KWinWaylandWaylandBackend");
static const QString s_x11Plugin = QStringLiteral("KWinWaylandX11Backend");
static const QString s_drmPlugin = QStringLiteral("KWinWaylandDrmBackend");
static const QString s_virtualPlugin = QStringLiteral("KWinWaylandVirtualBackend");
static QString automaticBackendSelection()
{
if (qEnvironmentVariableIsSet("WAYLAND_DISPLAY")) {
return s_waylandPlugin;
}
if (qEnvironmentVariableIsSet("DISPLAY")) {
return s_x11Plugin;
}
return s_drmPlugin;
}
} // namespace
int main(int argc, char *argv[])
{
KWin::Application::setupMalloc();
KWin::Application::setupLocalizedString();
KWin::gainRealTime();
if (signal(SIGTERM, KWin::sighandler) == SIG_IGN) {
signal(SIGTERM, SIG_IGN);
}
if (signal(SIGINT, KWin::sighandler) == SIG_IGN) {
signal(SIGINT, SIG_IGN);
}
if (signal(SIGHUP, KWin::sighandler) == SIG_IGN) {
signal(SIGHUP, SIG_IGN);
}
signal(SIGPIPE, SIG_IGN);
// It's easy to exceed the file descriptor limit because many things are backed using fds
// nowadays, e.g. dmabufs, shm buffers, etc. Bump the RLIMIT_NOFILE limit to handle that.
// Some apps may still use select(), so we reset the limit to its original value in fork().
if (KWin::bumpNofileLimit()) {
pthread_atfork(nullptr, nullptr, KWin::restoreNofileLimit);
}
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
// enforce our internal qpa plugin, unfortunately command line switch has precedence
setenv("QT_QPA_PLATFORM", "wayland-org.kde.kwin.qpa", true);
qunsetenv("QT_DEVICE_PIXEL_RATIO");
qputenv("QSG_RENDER_LOOP", "basic");
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
KWin::ApplicationWayland a(argc, argv);
a.setupTranslator();
// reset QT_QPA_PLATFORM so we don't propagate it to our children (e.g. apps launched from the overview effect)
qunsetenv("QT_QPA_PLATFORM");
KWin::Application::createAboutData();
const auto availablePlugins = KPluginMetaData::findPlugins(QStringLiteral("org.kde.kwin.waylandbackends"));
auto hasPlugin = [&availablePlugins](const QString &name) {
return std::any_of(availablePlugins.begin(), availablePlugins.end(),
[name](const KPluginMetaData &plugin) {
return plugin.pluginId() == name;
});
};
const bool hasSizeOption = hasPlugin(KWin::s_x11Plugin) || hasPlugin(KWin::s_virtualPlugin);
const bool hasOutputCountOption = hasPlugin(KWin::s_x11Plugin);
const bool hasX11Option = hasPlugin(KWin::s_x11Plugin);
const bool hasVirtualOption = hasPlugin(KWin::s_virtualPlugin);
const bool hasWaylandOption = hasPlugin(KWin::s_waylandPlugin);
const bool hasDrmOption = hasPlugin(KWin::s_drmPlugin);
QCommandLineOption xwaylandOption(QStringLiteral("xwayland"),
i18n("Start a rootless Xwayland server."));
QCommandLineOption waylandSocketOption(QStringList{QStringLiteral("s"), QStringLiteral("socket")},
i18n("Name of the Wayland socket to listen on. If not set \"wayland-0\" is used."),
QStringLiteral("socket"));
QCommandLineOption x11DisplayOption(QStringLiteral("x11-display"),
i18n("The X11 Display to use in windowed mode on platform X11."),
QStringLiteral("display"));
QCommandLineOption waylandDisplayOption(QStringLiteral("wayland-display"),
i18n("The Wayland Display to use in windowed mode on platform Wayland."),
QStringLiteral("display"));
QCommandLineOption virtualFbOption(QStringLiteral("virtual"), i18n("Render to a virtual framebuffer."));
QCommandLineOption widthOption(QStringLiteral("width"),
i18n("The width for windowed mode. Default width is 1024."),
QStringLiteral("width"));
widthOption.setDefaultValue(QString::number(1024));
QCommandLineOption heightOption(QStringLiteral("height"),
i18n("The height for windowed mode. Default height is 768."),
QStringLiteral("height"));
heightOption.setDefaultValue(QString::number(768));
QCommandLineOption scaleOption(QStringLiteral("scale"),
i18n("The scale for windowed mode. Default value is 1."),
QStringLiteral("scale"));
scaleOption.setDefaultValue(QString::number(1));
QCommandLineOption outputCountOption(QStringLiteral("output-count"),
i18n("The number of windows to open as outputs in windowed mode. Default value is 1"),
QStringLiteral("count"));
outputCountOption.setDefaultValue(QString::number(1));
QCommandLineOption waylandSocketFdOption(QStringLiteral("wayland-fd"),
i18n("Wayland socket to use for incoming connections. This can be combined with --socket to name the socket"),
QStringLiteral("wayland-fd"));
QCommandLineOption xwaylandListenFdOption(QStringLiteral("xwayland-fd"),
i18n("XWayland socket to use for Xwayland's incoming connections. This can be set multiple times"),
QStringLiteral("xwayland-fds"));
QCommandLineOption xwaylandDisplayOption(QStringLiteral("xwayland-display"),
i18n("Name of the xwayland display that has been pre-set up"),
"xwayland-display");
QCommandLineOption xwaylandXAuthorityOption(QStringLiteral("xwayland-xauthority"),
i18n("Name of the xauthority file "),
"xwayland-xauthority");
QCommandLineOption replaceOption(QStringLiteral("replace"),
i18n("Exits this instance so it can be restarted by kwin_wayland_wrapper."));
QCommandLineParser parser;
a.setupCommandLine(&parser);
parser.addOption(xwaylandOption);
parser.addOption(waylandSocketOption);
parser.addOption(waylandSocketFdOption);
parser.addOption(xwaylandListenFdOption);
parser.addOption(xwaylandDisplayOption);
parser.addOption(xwaylandXAuthorityOption);
parser.addOption(replaceOption);
if (hasX11Option) {
parser.addOption(x11DisplayOption);
}
if (hasWaylandOption) {
parser.addOption(waylandDisplayOption);
}
if (hasVirtualOption) {
parser.addOption(virtualFbOption);
}
if (hasSizeOption) {
parser.addOption(widthOption);
parser.addOption(heightOption);
parser.addOption(scaleOption);
}
if (hasOutputCountOption) {
parser.addOption(outputCountOption);
}
QCommandLineOption drmOption(QStringLiteral("drm"), i18n("Render through drm node."));
if (hasDrmOption) {
parser.addOption(drmOption);
}
QCommandLineOption inputMethodOption(QStringLiteral("inputmethod"),
i18n("Input method that KWin starts."),
QStringLiteral("path/to/imserver"));
parser.addOption(inputMethodOption);
QCommandLineOption listBackendsOption(QStringLiteral("list-backends"),
i18n("List all available backends and quit."));
parser.addOption(listBackendsOption);
#if KWIN_BUILD_SCREENLOCKER
QCommandLineOption screenLockerOption(QStringLiteral("lockscreen"),
i18n("Starts the session in locked mode."));
parser.addOption(screenLockerOption);
QCommandLineOption noScreenLockerOption(QStringLiteral("no-lockscreen"),
i18n("Starts the session without lock screen support."));
parser.addOption(noScreenLockerOption);
#endif
QCommandLineOption noGlobalShortcutsOption(QStringLiteral("no-global-shortcuts"),
i18n("Starts the session without global shortcuts support."));
parser.addOption(noGlobalShortcutsOption);
#if KWIN_BUILD_ACTIVITIES
QCommandLineOption noActivitiesOption(QStringLiteral("no-kactivities"),
i18n("Disable KActivities integration."));
parser.addOption(noActivitiesOption);
#endif
QCommandLineOption exitWithSessionOption(QStringLiteral("exit-with-session"),
i18n("Exit after the session application, which is started by KWin, closed."),
QStringLiteral("/path/to/session"));
parser.addOption(exitWithSessionOption);
parser.addPositionalArgument(QStringLiteral("applications"),
i18n("Applications to start once Wayland and Xwayland server are started"),
QStringLiteral("[/path/to/application...]"));
parser.process(a);
a.processCommandLine(&parser);
#if KWIN_BUILD_ACTIVITIES
if (parser.isSet(noActivitiesOption)) {
a.setUseKActivities(false);
}
#endif
if (parser.isSet(replaceOption)) {
QDBusMessage msg = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"), QStringLiteral("/KWin"),
QStringLiteral("org.kde.KWin"), QStringLiteral("replace"));
QDBusConnection::sessionBus().call(msg, QDBus::NoBlock);
return 0;
}
if (parser.isSet(listBackendsOption)) {
for (const auto &plugin : availablePlugins) {
std::cout << std::setw(40) << std::left << qPrintable(plugin.name()) << qPrintable(plugin.description()) << std::endl;
}
return 0;
}
if (parser.isSet(exitWithSessionOption)) {
a.setSessionArgument(parser.value(exitWithSessionOption));
}
QString pluginName;
QSize initialWindowSize;
QByteArray deviceIdentifier;
int outputCount = 1;
qreal outputScale = 1;
if (hasDrmOption && parser.isSet(drmOption)) {
pluginName = KWin::s_drmPlugin;
}
if (hasSizeOption) {
bool ok = false;
const int width = parser.value(widthOption).toInt(&ok);
if (!ok) {
std::cerr << "FATAL ERROR incorrect value for width" << std::endl;
return 1;
}
const int height = parser.value(heightOption).toInt(&ok);
if (!ok) {
std::cerr << "FATAL ERROR incorrect value for height" << std::endl;
return 1;
}
const qreal scale = parser.value(scaleOption).toDouble(&ok);
if (!ok || scale <= 0) {
std::cerr << "FATAL ERROR incorrect value for scale" << std::endl;
return 1;
}
outputScale = scale;
initialWindowSize = QSize(width, height);
}
if (hasOutputCountOption) {
bool ok = false;
const int count = parser.value(outputCountOption).toInt(&ok);
if (ok) {
outputCount = qMax(1, count);
}
}
if (hasX11Option && parser.isSet(x11DisplayOption)) {
deviceIdentifier = parser.value(x11DisplayOption).toUtf8();
pluginName = KWin::s_x11Plugin;
} else if (hasWaylandOption && parser.isSet(waylandDisplayOption)) {
deviceIdentifier = parser.value(waylandDisplayOption).toUtf8();
pluginName = KWin::s_waylandPlugin;
}
if (hasVirtualOption && parser.isSet(virtualFbOption)) {
pluginName = KWin::s_virtualPlugin;
}
if (pluginName.isEmpty()) {
std::cerr << "No backend specified through command line argument, trying auto resolution" << std::endl;
pluginName = KWin::automaticBackendSelection();
}
auto pluginIt = std::find_if(availablePlugins.begin(), availablePlugins.end(),
[&pluginName](const KPluginMetaData &plugin) {
return plugin.pluginId() == pluginName;
});
if (pluginIt == availablePlugins.end()) {
std::cerr << "FATAL ERROR: could not find a backend" << std::endl;
return 1;
}
// TODO: create backend without having the server running
KWin::WaylandServer *server = KWin::WaylandServer::create(&a);
KWin::WaylandServer::InitializationFlags flags;
#if KWIN_BUILD_SCREENLOCKER
if (parser.isSet(screenLockerOption)) {
flags = KWin::WaylandServer::InitializationFlag::LockScreen;
} else if (parser.isSet(noScreenLockerOption)) {
flags = KWin::WaylandServer::InitializationFlag::NoLockScreenIntegration;
}
#endif
if (parser.isSet(noGlobalShortcutsOption)) {
flags |= KWin::WaylandServer::InitializationFlag::NoGlobalShortcuts;
}
const QString socketName = parser.value(waylandSocketOption);
if (parser.isSet(waylandSocketFdOption)) {
bool ok;
int fd = parser.value(waylandSocketFdOption).toInt(&ok);
if (ok) {
// make sure we don't leak this FD to children
fcntl(fd, F_SETFD, O_CLOEXEC);
server->display()->addSocketFileDescriptor(fd, socketName);
} else {
std::cerr << "FATAL ERROR: could not parse socket FD" << std::endl;
return 1;
}
} else {
// socketName empty is fine here, addSocketName will automatically pick one
if (!server->display()->addSocketName(socketName)) {
std::cerr << "FATAL ERROR: could not add wayland socket " << qPrintable(socketName) << std::endl;
return 1;
}
}
if (!server->init(flags)) {
std::cerr << "FATAL ERROR: could not create Wayland server" << std::endl;
return 1;
}
a.initPlatform(*pluginIt);
if (!a.platform()) {
std::cerr << "FATAL ERROR: could not instantiate a backend" << std::endl;
return 1;
}
if (!deviceIdentifier.isEmpty()) {
a.platform()->setDeviceIdentifier(deviceIdentifier);
}
if (initialWindowSize.isValid()) {
a.platform()->setInitialWindowSize(initialWindowSize);
}
a.platform()->setInitialOutputScale(outputScale);
a.platform()->setInitialOutputCount(outputCount);
QObject::connect(&a, &KWin::Application::workspaceCreated, server, &KWin::WaylandServer::initWorkspace);
if (!server->socketName().isEmpty()) {
environment.insert(QStringLiteral("WAYLAND_DISPLAY"), server->socketName());
qputenv("WAYLAND_DISPLAY", server->socketName().toUtf8());
}
a.setProcessStartupEnvironment(environment);
if (parser.isSet(xwaylandOption)) {
a.setStartXwayland(true);
if (parser.isSet(xwaylandListenFdOption)) {
const QStringList fdStrings = parser.values(xwaylandListenFdOption);
for (const QString &fdString : fdStrings) {
bool ok;
int fd = fdString.toInt(&ok);
if (ok) {
// make sure we don't leak this FD to children
fcntl(fd, F_SETFD, O_CLOEXEC);
a.addXwaylandSocketFileDescriptor(fd);
}
}
if (parser.isSet(xwaylandDisplayOption)) {
a.setXwaylandDisplay(parser.value(xwaylandDisplayOption));
} else {
std::cerr << "Using xwayland-fd without xwayland-display is undefined" << std::endl;
return 1;
}
if (parser.isSet(xwaylandXAuthorityOption)) {
a.setXwaylandXauthority(parser.value(xwaylandXAuthorityOption));
}
}
}
a.setApplicationsToStart(parser.positionalArguments());
a.setInputMethodServerToStart(parser.value(inputMethodOption));
a.start();
return a.exec();
}