Brooklyn/plasma/workspace/startkde/startplasma.cpp
2022-03-05 22:41:29 +05:00

792 lines
32 KiB
C++

/*
SPDX-FileCopyrightText: 2019 Aleix Pol Gonzalez <aleixpol@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include <config-startplasma.h>
#include <QDir>
#include <QEventLoop>
#include <QProcess>
#include <QStandardPaths>
#include <QTextStream>
#include <QDBusConnectionInterface>
#include <QDBusServiceWatcher>
#include <KConfig>
#include <KConfigGroup>
#include <KNotifyConfig>
#include <KPackage/Package>
#include <KPackage/PackageLoader>
#include <KSharedConfig>
#include <phonon/audiooutput.h>
#include <phonon/mediaobject.h>
#include <phonon/mediasource.h>
#include <unistd.h>
#include <autostartscriptdesktopfile.h>
#include <updatelaunchenvjob.h>
#include "startplasma.h"
#include "../config-workspace.h"
#include "../kcms/lookandfeel/lookandfeelmanager.h"
#include "debug.h"
QTextStream out(stderr);
void sigtermHandler(int signalNumber)
{
Q_UNUSED(signalNumber)
if (QCoreApplication::instance()) {
QCoreApplication::instance()->exit(-1);
}
}
void messageBox(const QString &text)
{
out << text;
runSync(QStringLiteral("xmessage"), {QStringLiteral("-geometry"), QStringLiteral("500x100"), text});
}
QStringList allServices(const QLatin1String &prefix)
{
QDBusConnectionInterface *bus = QDBusConnection::sessionBus().interface();
const QStringList services = bus->registeredServiceNames();
QMap<QString, QStringList> servicesWithAliases;
for (const QString &serviceName : services) {
QDBusReply<QString> reply = bus->serviceOwner(serviceName);
QString owner = reply;
if (owner.isEmpty())
owner = serviceName;
servicesWithAliases[owner].append(serviceName);
}
QStringList names;
for (auto it = servicesWithAliases.constBegin(); it != servicesWithAliases.constEnd(); ++it) {
if (it.value().startsWith(prefix))
names << it.value();
}
names.removeDuplicates();
names.sort();
return names;
}
void gentleTermination(QProcess *p)
{
if (p->state() != QProcess::Running) {
return;
}
p->close();
p->terminate();
// Wait longer for a session than a greeter
if (!p->waitForFinished(5000)) {
p->kill();
if (!p->waitForFinished(5000)) {
qCWarning(PLASMA_STARTUP) << "Could not fully finish the process" << p->program();
}
}
}
int runSync(const QString &program, const QStringList &args, const QStringList &env)
{
QProcess p;
if (!env.isEmpty())
p.setEnvironment(QProcess::systemEnvironment() << env);
p.setProcessChannelMode(QProcess::ForwardedChannels);
p.start(program, args);
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &p, [&p] {
gentleTermination(&p);
});
// qCDebug(PLASMA_STARTUP) << "started..." << program << args;
p.waitForFinished(-1);
if (p.exitCode()) {
qCWarning(PLASMA_STARTUP) << program << args << "exited with code" << p.exitCode();
}
return p.exitCode();
}
bool isShellVariable(const QByteArray &name)
{
return name == "_" || name.startsWith("SHLVL");
}
bool isSessionVariable(const QByteArray &name)
{
// Check is variable is specific to session.
return name == "DISPLAY" || name == "XAUTHORITY" || //
name == "WAYLAND_DISPLAY" || name == "WAYLAND_SOCKET" || //
name.startsWith("XDG_");
}
void setEnvironmentVariable(const QByteArray &name, const QByteArray &value)
{
if (qgetenv(name) != value) {
qputenv(name, value);
}
}
void sourceFiles(const QStringList &files)
{
QStringList filteredFiles;
std::copy_if(files.begin(), files.end(), std::back_inserter(filteredFiles), [](const QString &i) {
return QFileInfo(i).isReadable();
});
if (filteredFiles.isEmpty())
return;
filteredFiles.prepend(QStringLiteral(CMAKE_INSTALL_FULL_LIBEXECDIR "/plasma-sourceenv.sh"));
QProcess p;
p.start(QStringLiteral("/bin/sh"), filteredFiles);
p.waitForFinished(-1);
const auto fullEnv = p.readAllStandardOutput();
auto envs = fullEnv.split('\0');
for (auto &env : envs) {
const int idx = env.indexOf('=');
if (Q_UNLIKELY(idx <= 0)) {
continue;
}
const auto name = env.left(idx);
if (isShellVariable(name)) {
continue;
}
setEnvironmentVariable(name, env.mid(idx + 1));
}
}
void createConfigDirectory()
{
const QString configDir = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation);
if (!QDir().mkpath(configDir))
out << "Could not create config directory XDG_CONFIG_HOME: " << configDir << '\n';
}
void runStartupConfig()
{
// export LC_* variables set by kcmshell5 formats into environment
// so it can be picked up by QLocale and friends.
KConfig config(QStringLiteral("plasma-localerc"));
KConfigGroup formatsConfig = KConfigGroup(&config, "Formats");
const auto lcValues = {"LANG", "LC_NUMERIC", "LC_TIME", "LC_MONETARY", "LC_MEASUREMENT", "LC_COLLATE", "LC_CTYPE"};
for (auto lc : lcValues) {
const QString value = formatsConfig.readEntry(lc, QString());
if (!value.isEmpty()) {
qputenv(lc, value.toUtf8());
}
}
KConfigGroup languageConfig = KConfigGroup(&config, "Translations");
const QString value = languageConfig.readEntry("LANGUAGE", QString());
if (!value.isEmpty()) {
qputenv("LANGUAGE", value.toUtf8());
}
if (!formatsConfig.hasKey("LANG") && !qEnvironmentVariableIsEmpty("LANG")) {
formatsConfig.writeEntry("LANG", qgetenv("LANG"));
formatsConfig.sync();
}
}
void setupCursor(bool wayland)
{
const KConfig cfg(QStringLiteral("kcminputrc"));
const KConfigGroup inputCfg = cfg.group("Mouse");
const auto kcminputrc_mouse_cursorsize = inputCfg.readEntry("cursorSize", 24);
const auto kcminputrc_mouse_cursortheme = inputCfg.readEntry("cursorTheme", QStringLiteral("breeze_cursors"));
if (!kcminputrc_mouse_cursortheme.isEmpty()) {
#ifdef XCURSOR_PATH
QByteArray path(XCURSOR_PATH);
path.replace("$XCURSOR_PATH", qgetenv("XCURSOR_PATH"));
qputenv("XCURSOR_PATH", path);
#endif
}
// TODO: consider linking directly
const int applyMouseStatus =
wayland ? 0 : runSync(QStringLiteral("kapplymousetheme"), {kcminputrc_mouse_cursortheme, QString::number(kcminputrc_mouse_cursorsize)});
if (applyMouseStatus == 10) {
qputenv("XCURSOR_THEME", "breeze_cursors");
} else if (!kcminputrc_mouse_cursortheme.isEmpty()) {
qputenv("XCURSOR_THEME", kcminputrc_mouse_cursortheme.toUtf8());
}
qputenv("XCURSOR_SIZE", QByteArray::number(kcminputrc_mouse_cursorsize));
}
std::optional<QProcessEnvironment> getSystemdEnvironment()
{
QStringList list;
auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.DBus.Properties"),
QStringLiteral("Get"));
msg << QStringLiteral("org.freedesktop.systemd1.Manager") << QStringLiteral("Environment");
auto reply = QDBusConnection::sessionBus().call(msg);
if (reply.type() == QDBusMessage::ErrorMessage) {
return std::nullopt;
}
// Make sure the returned type is correct.
auto arguments = reply.arguments();
if (arguments.isEmpty() || arguments[0].userType() != qMetaTypeId<QDBusVariant>()) {
return std::nullopt;
}
auto variant = qdbus_cast<QVariant>(arguments[0]);
if (variant.type() != QVariant::StringList) {
return std::nullopt;
}
const auto assignmentList = variant.toStringList();
QProcessEnvironment ret;
for (auto &env : assignmentList) {
const int idx = env.indexOf(QLatin1Char('='));
if (Q_LIKELY(idx > 0)) {
ret.insert(env.left(idx), env.mid(idx + 1));
}
}
return ret;
}
// Import systemd user environment.
//
// Systemd read ~/.config/environment.d which applies to all systemd user unit.
// But it won't work if plasma is not started by systemd.
void importSystemdEnvrionment()
{
const auto environment = getSystemdEnvironment();
if (!environment) {
return;
}
for (auto &nameStr : environment.value().keys()) {
const auto name = nameStr.toLocal8Bit();
if (!isShellVariable(name) && !isSessionVariable(name)) {
setEnvironmentVariable(name, environment.value().value(nameStr).toLocal8Bit());
}
}
}
// Source scripts found in <config locations>/plasma-workspace/env/*.sh
// (where <config locations> correspond to the system and user's configuration
// directory.
//
// Scripts are sourced in reverse order of priority of their directory, as defined
// by `QStandardPaths::standardLocations`. This ensures that high-priority scripts
// (such as those in the user's home directory) are sourced last and take precedence
// over lower-priority scripts (such as system defaults). Scripts in the same
// directory are sourced in lexical order of their filename.
//
// This is where you can define environment variables that will be available to
// all KDE programs, so this is where you can run agents using e.g. eval `ssh-agent`
// or eval `gpg-agent --daemon`.
// Note: if you do that, you should also put "ssh-agent -k" as a shutdown script
//
// (see end of this file).
// For anything else (that doesn't set env vars, or that needs a window manager),
// better use the Autostart folder.
void runEnvironmentScripts()
{
QStringList scripts;
auto locations = QStandardPaths::standardLocations(QStandardPaths::GenericConfigLocation);
//`standardLocations()` returns locations sorted by "order of priority". We iterate in reverse
// order so that high-priority scripts are sourced last and their modifications take precedence.
for (auto loc = locations.crbegin(); loc != locations.crend(); loc++) {
QDir dir(*loc);
if (!dir.cd(QStringLiteral("./plasma-workspace/env"))) {
// Skip location if plasma-workspace/env subdirectory does not exist
continue;
}
const auto dirScripts = dir.entryInfoList({QStringLiteral("*.sh")}, QDir::Files, QDir::Name);
for (const auto &script : dirScripts) {
scripts << script.absoluteFilePath();
}
}
sourceFiles(scripts);
}
// Mark that full KDE session is running (e.g. Konqueror preloading works only
// with full KDE running). The KDE_FULL_SESSION property can be detected by
// any X client connected to the same X session, even if not launched
// directly from the KDE session but e.g. using "ssh -X", kdesu. $KDE_FULL_SESSION
// however guarantees that the application is launched in the same environment
// like the KDE session and that e.g. KDE utilities/libraries are available.
// KDE_FULL_SESSION property is also only available since KDE 3.5.5.
// The matching tests are:
// For $KDE_FULL_SESSION:
// if test -n "$KDE_FULL_SESSION"; then ... whatever
// For KDE_FULL_SESSION property (on X11):
// xprop -root | grep "^KDE_FULL_SESSION" >/dev/null 2>/dev/null
// if test $? -eq 0; then ... whatever
//
// Additionally there is $KDE_SESSION_UID with the uid
// of the user running the KDE session. It should be rarely needed (e.g.
// after sudo to prevent desktop-wide functionality in the new user's kded).
//
// Since KDE4 there is also KDE_SESSION_VERSION, containing the major version number.
//
void setupPlasmaEnvironment()
{
// Manually disable auto scaling because we are scaling above
// otherwise apps that manually opt in for high DPI get auto scaled by the developer AND manually scaled by us
qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "0");
qputenv("KDE_FULL_SESSION", "true");
qputenv("KDE_SESSION_VERSION", "5");
qputenv("KDE_SESSION_UID", QByteArray::number(getuid()));
qputenv("XDG_CURRENT_DESKTOP", "KDE");
qputenv("KDE_APPLICATIONS_AS_SCOPE", "1");
// Add kdedefaults dir to allow config defaults overriding from a writable location
QByteArray currentConfigDirs = qgetenv("XDG_CONFIG_DIRS");
if (currentConfigDirs.isEmpty()) {
currentConfigDirs = "/etc/xdg";
}
const QString extraConfigDir = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/kdedefaults");
QDir().mkpath(extraConfigDir);
qputenv("XDG_CONFIG_DIRS", QFile::encodeName(extraConfigDir) + ':' + currentConfigDirs);
const KConfig globals;
const QString currentLnf = KConfigGroup(&globals, QStringLiteral("KDE")).readEntry("LookAndFeelPackage", QStringLiteral("org.kde.breeze.desktop"));
QFile activeLnf(extraConfigDir + QLatin1String("/package"));
activeLnf.open(QIODevice::ReadOnly);
if (activeLnf.readLine() != currentLnf.toUtf8()) {
KPackage::Package package = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/LookAndFeel"), currentLnf);
LookAndFeelManager lnfManager;
lnfManager.setMode(LookAndFeelManager::Mode::Defaults);
lnfManager.save(package, KPackage::Package());
}
// check if colors changed, if so apply them and discard palsma cache
{
LookAndFeelManager lnfManager;
lnfManager.setMode(LookAndFeelManager::Mode::Apply);
KConfig globals(QStringLiteral("kdeglobals")); // Reload the config
KConfigGroup generalGroup(&globals, QStringLiteral("General"));
const QString colorScheme = generalGroup.readEntry("ColorScheme", QStringLiteral("BreezeLight"));
QString path = lnfManager.colorSchemeFile(colorScheme);
if (!path.isEmpty()) {
QFile f(path);
QCryptographicHash hash(QCryptographicHash::Sha1);
if (f.open(QFile::ReadOnly) && hash.addData(&f)) {
const QString fileHash = QString::fromUtf8(hash.result().toHex());
if (fileHash != generalGroup.readEntry("ColorSchemeHash", QString())) {
lnfManager.setColors(colorScheme, path);
generalGroup.writeEntry("ColorSchemeHash", fileHash);
generalGroup.sync();
const QString svgCache = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QLatin1Char('/') + QStringLiteral("plasma-svgelements");
if (!svgCache.isEmpty()) {
QFile::remove(svgCache);
}
}
}
}
}
}
void setupX11()
{
// Set a left cursor instead of the standard X11 "X" cursor, since I've heard
// from some users that they're confused and don't know what to do. This is
// especially necessary on slow machines, where starting KDE takes one or two
// minutes until anything appears on the screen.
//
// If the user has overwritten fonts, the cursor font may be different now
// so don't move this up.
runSync(QStringLiteral("xsetroot"), {QStringLiteral("-cursor_name"), QStringLiteral("left_ptr")});
}
void cleanupPlasmaEnvironment(const std::optional<QProcessEnvironment> &oldSystemdEnvironment)
{
qunsetenv("KDE_FULL_SESSION");
qunsetenv("KDE_SESSION_VERSION");
qunsetenv("KDE_SESSION_UID");
if (!oldSystemdEnvironment) {
return;
}
auto currentEnv = getSystemdEnvironment();
if (!currentEnv) {
return;
}
// According to systemd documentation:
// If a variable is listed in both, the variable is set after this method returns, i.e. the set list overrides the unset list.
// So this will effectively restore the state to the values in oldSystemdEnvironment.
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QStringLiteral("UnsetAndSetEnvironment"));
message.setArguments({currentEnv.value().keys(), oldSystemdEnvironment.value().toStringList()});
// The session program gonna quit soon, ensure the message is flushed.
auto reply = QDBusConnection::sessionBus().asyncCall(message);
reply.waitForFinished();
}
// Drop session-specific variables from the systemd environment.
// Those can be leftovers from previous sessions, which can interfere with the session
// we want to start now, e.g. $DISPLAY might break kwin_wayland.
static void dropSessionVarsFromSystemdEnvironment()
{
const auto environment = getSystemdEnvironment();
if (!environment) {
return;
}
QStringList varsToDrop;
for (auto &nameStr : environment.value().keys()) {
// If it's set in this process, it'll be overwritten by the following UpdateLaunchEnvJob
const auto name = nameStr.toLocal8Bit();
if (!qEnvironmentVariableIsSet(name) && isSessionVariable(name)) {
varsToDrop.append(nameStr);
}
}
auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QStringLiteral("UnsetEnvironment"));
msg << varsToDrop;
auto reply = QDBusConnection::sessionBus().call(msg);
if (reply.type() == QDBusMessage::ErrorMessage) {
qCWarning(PLASMA_STARTUP) << "Failed to unset systemd environment variables:" << reply.errorName() << reply.errorMessage();
}
}
// kwin_wayland can possibly also start dbus-activated services which need env variables.
// In that case, the update in startplasma might be too late.
bool syncDBusEnvironment()
{
dropSessionVarsFromSystemdEnvironment();
// At this point all environment variables are set, let's send it to the DBus session server to update the activation environment
auto job = new UpdateLaunchEnvJob(QProcessEnvironment::systemEnvironment());
return job->exec();
}
static bool desktopLockedAtStart = false;
QProcess *setupKSplash()
{
const auto dlstr = qgetenv("DESKTOP_LOCKED");
desktopLockedAtStart = dlstr == "true" || dlstr == "1";
qunsetenv("DESKTOP_LOCKED"); // Don't want it in the environment
QProcess *p = nullptr;
if (!desktopLockedAtStart) {
const KConfig cfg(QStringLiteral("ksplashrc"));
// the splashscreen and progress indicator
KConfigGroup ksplashCfg = cfg.group("KSplash");
if (ksplashCfg.readEntry("Engine", QStringLiteral("KSplashQML")) == QLatin1String("KSplashQML")) {
p = new QProcess;
p->setProcessChannelMode(QProcess::ForwardedChannels);
p->start(QStringLiteral("ksplashqml"), {ksplashCfg.readEntry("Theme", QStringLiteral("Breeze"))});
}
}
return p;
}
// If something went on an endless restart crash loop it will get blacklisted, as this is a clean login we will want to reset those counters
// This is independent of whether we use the Plasma systemd boot
void resetSystemdFailedUnits()
{
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QStringLiteral("ResetFailed"));
QDBusConnection::sessionBus().call(message);
}
bool hasSystemdService(const QString &serviceName)
{
qDBusRegisterMetaType<QPair<QString, QString>>();
qDBusRegisterMetaType<QList<QPair<QString, QString>>>();
auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QStringLiteral("ListUnitFilesByPatterns"));
msg << QStringList({QStringLiteral("enabled"),
QStringLiteral("static"),
QStringLiteral("linked"),
QStringLiteral("linked-runtime")});
msg << QStringList({serviceName});
QDBusReply<QList<QPair<QString, QString>>> reply = QDBusConnection::sessionBus().call(msg);
if (!reply.isValid()) {
return false;
}
// if we have a service returned then it must have found it
return !reply.value().isEmpty();
}
bool useSystemdBoot()
{
auto config = KSharedConfig::openConfig(QStringLiteral("startkderc"), KConfig::NoGlobals);
const QString configValue = config->group(QStringLiteral("General")).readEntry("systemdBoot", QStringLiteral("true")).toLower();
if (configValue == QLatin1String("false")) {
return false;
}
if (configValue == QLatin1String("force")) {
qInfo() << "Systemd boot forced";
return true;
}
if (!hasSystemdService(QStringLiteral("plasma-workspace.target"))) {
return false;
}
// xdg-desktop-autostart.target is shipped with an systemd 246 and provides a generator
// for creating units out of existing autostart files
// only enable our systemd boot if that exists, unless the user has forced the systemd boot above
return hasSystemdService(QStringLiteral("xdg-desktop-autostart.target"));
}
void startKSplashViaSystemd()
{
const KConfig cfg(QStringLiteral("ksplashrc"));
// the splashscreen and progress indicator
KConfigGroup ksplashCfg = cfg.group("KSplash");
if (ksplashCfg.readEntry("Engine", QStringLiteral("KSplashQML")) == QLatin1String("KSplashQML")) {
auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QStringLiteral("StartUnit"));
msg << QStringLiteral("plasma-ksplash.service") << QStringLiteral("fail");
QDBusReply<QDBusObjectPath> reply = QDBusConnection::sessionBus().call(msg);
}
}
static void migrateUserScriptsAutostart()
{
QDir configLocation(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation));
QDir autostartScriptsLocation(configLocation.filePath(QStringLiteral("autostart-scripts")));
if (!autostartScriptsLocation.exists()) {
return;
}
const QDir autostartScriptsMovedLocation(configLocation.filePath(QStringLiteral("old-autostart-scripts")));
const auto entries = autostartScriptsLocation.entryInfoList(QDir::Files);
for (const auto &info : entries) {
const auto scriptName = info.fileName();
const auto scriptPath = info.absoluteFilePath();
const auto scriptMovedPath = autostartScriptsMovedLocation.filePath(scriptName);
// Don't migrate backup files
if (scriptName.endsWith(QLatin1Char('~')) || scriptName.endsWith(QLatin1String(".bak"))
|| (scriptName[0] == QLatin1Char('%') && scriptName.endsWith(QLatin1Char('%')))
|| (scriptName[0] == QLatin1Char('#') && scriptName.endsWith(QLatin1Char('#')))) {
qCDebug(PLASMA_STARTUP) << "Not migrating backup autostart script" << scriptName;
continue;
}
// Migrate autostart script to a standard .desktop autostart file
AutostartScriptDesktopFile desktopFile(scriptName, info.isSymLink() ? info.symLinkTarget() : scriptMovedPath);
qCInfo(PLASMA_STARTUP) << "Migrated legacy autostart script" << scriptPath << "to" << desktopFile.fileName();
if (info.isSymLink() && QFile::remove(scriptPath)) {
qCInfo(PLASMA_STARTUP) << "Removed legacy autostart script" << scriptPath << "that pointed to" << info.symLinkTarget();
}
}
// Delete or rename autostart-scripts to old-autostart-scripts to avoid running the migration again
if (autostartScriptsLocation.entryInfoList(QDir::Files).empty()) {
autostartScriptsLocation.removeRecursively();
} else {
configLocation.rename(autostartScriptsLocation.dirName(), autostartScriptsMovedLocation.dirName());
}
// Reload systemd so that the XDG autostart generator is run again to pick up the new .desktop files
QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QStringLiteral("Reload"));
QDBusConnection::sessionBus().call(message);
}
bool startPlasmaSession(bool wayland)
{
resetSystemdFailedUnits();
OrgKdeKSplashInterface iface(QStringLiteral("org.kde.KSplash"), QStringLiteral("/KSplash"), QDBusConnection::sessionBus());
iface.setStage(QStringLiteral("startPlasma"));
// finally, give the session control to the session manager
// see kdebase/ksmserver for the description of the rest of the startup sequence
// if the KDEWM environment variable has been set, then it will be used as KDE's
// window manager instead of kwin.
// if KDEWM is not set, ksmserver will ensure kwin is started.
// kwrapper5 is used to reduce startup time and memory usage
// kwrapper5 does not return useful error codes such as the exit code of ksmserver.
// We only check for 255 which means that the ksmserver process could not be
// started, any problems thereafter, e.g. ksmserver failing to initialize,
// will remain undetected.
// If the session should be locked from the start (locked autologin),
// lock now and do the rest of the KDE startup underneath the locker.
bool rc = true;
QEventLoop e;
QDBusServiceWatcher serviceWatcher;
serviceWatcher.setConnection(QDBusConnection::sessionBus());
// We want to exit when both ksmserver and plasma-session-shutdown have finished
// This also closes if ksmserver crashes unexpectedly, as in those cases plasma-shutdown is not running
if (wayland) {
serviceWatcher.addWatchedService(QStringLiteral("org.kde.KWinWrapper"));
} else {
serviceWatcher.addWatchedService(QStringLiteral("org.kde.ksmserver"));
}
serviceWatcher.addWatchedService(QStringLiteral("org.kde.Shutdown"));
serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
QObject::connect(&serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, [&]() {
const QStringList watchedServices = serviceWatcher.watchedServices();
bool plasmaSessionRunning = std::any_of(watchedServices.constBegin(), watchedServices.constEnd(), [](const QString &service) {
return QDBusConnection::sessionBus().interface()->isServiceRegistered(service);
});
if (!plasmaSessionRunning) {
e.quit();
}
});
// Create .desktop files for the scripts in .config/autostart-scripts
migrateUserScriptsAutostart();
QScopedPointer<QProcess, KillBeforeDeleter> startPlasmaSession;
if (!useSystemdBoot()) {
startPlasmaSession.reset(new QProcess);
qCDebug(PLASMA_STARTUP) << "Using classic boot";
QStringList plasmaSessionOptions;
if (wayland) {
plasmaSessionOptions << QStringLiteral("--no-lockscreen");
} else {
if (desktopLockedAtStart) {
plasmaSessionOptions << QStringLiteral("--lockscreen");
}
}
startPlasmaSession->setProcessChannelMode(QProcess::ForwardedChannels);
QObject::connect(startPlasmaSession.data(), &QProcess::finished, &e, [&rc](int exitCode, QProcess::ExitStatus) {
if (exitCode == 255) {
// Startup error
messageBox(QStringLiteral("startkde: Could not start plasma_session. Check your installation.\n"));
rc = false;
}
});
startPlasmaSession->start(QStringLiteral(CMAKE_INSTALL_FULL_BINDIR "/plasma_session"), plasmaSessionOptions);
} else {
qCDebug(PLASMA_STARTUP) << "Using systemd boot";
const QString platform = wayland ? QStringLiteral("wayland") : QStringLiteral("x11");
auto msg = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QStringLiteral("StartUnit"));
msg << QStringLiteral("plasma-workspace-%1.target").arg(platform) << QStringLiteral("fail");
QDBusReply<QDBusObjectPath> reply = QDBusConnection::sessionBus().call(msg);
if (!reply.isValid()) {
qCWarning(PLASMA_STARTUP) << "Could not start systemd managed Plasma session:" << reply.error().name() << reply.error().message();
messageBox(QStringLiteral("startkde: Could not start Plasma session.\n"));
rc = false;
} else {
playStartupSound(&e);
}
if (wayland) {
startKSplashViaSystemd();
}
}
if (rc) {
QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, &e, &QEventLoop::quit);
e.exec();
}
return rc;
}
void waitForKonqi()
{
const KConfig cfg(QStringLiteral("startkderc"));
const KConfigGroup grp = cfg.group("WaitForDrKonqi");
bool wait_drkonqi = grp.readEntry("Enabled", true);
if (wait_drkonqi) {
// wait for remaining drkonqi instances with timeout (in seconds)
const int wait_drkonqi_timeout = grp.readEntry("Timeout", 900) * 1000;
QElapsedTimer wait_drkonqi_counter;
wait_drkonqi_counter.start();
QStringList services = allServices(QLatin1String("org.kde.drkonqi-"));
while (!services.isEmpty()) {
sleep(5);
services = allServices(QLatin1String("org.kde.drkonqi-"));
if (wait_drkonqi_counter.elapsed() >= wait_drkonqi_timeout) {
// ask remaining drkonqis to die in a graceful way
for (const auto &service : qAsConst(services)) {
QDBusInterface iface(service, QStringLiteral("/MainApplication"));
iface.call(QStringLiteral("quit"));
}
break;
}
}
}
}
void playStartupSound(QObject *parent)
{
KNotifyConfig notifyConfig(QStringLiteral("plasma_workspace"), QList<QPair<QString, QString>>(), QStringLiteral("startkde"));
const QString action = notifyConfig.readEntry(QStringLiteral("Action"));
if (action.isEmpty() || !action.split(QLatin1Char('|')).contains(QLatin1String("Sound"))) {
// no startup sound configured
return;
}
Phonon::AudioOutput *audioOutput = new Phonon::AudioOutput(Phonon::NotificationCategory, parent);
QString soundFilename = notifyConfig.readEntry(QStringLiteral("Sound"));
if (soundFilename.isEmpty()) {
qCWarning(PLASMA_STARTUP) << "Audio notification requested, but no sound file provided in notifyrc file, aborting audio notification";
audioOutput->deleteLater();
return;
}
QUrl soundURL;
const auto dataLocations = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
for (const QString &dataLocation : dataLocations) {
soundURL = QUrl::fromUserInput(soundFilename, dataLocation + QStringLiteral("/sounds"), QUrl::AssumeLocalFile);
if (soundURL.isLocalFile() && QFile::exists(soundURL.toLocalFile())) {
break;
} else if (!soundURL.isLocalFile() && soundURL.isValid()) {
break;
}
soundURL.clear();
}
if (soundURL.isEmpty()) {
qCWarning(PLASMA_STARTUP) << "Audio notification requested, but sound file from notifyrc file was not found, aborting audio notification";
audioOutput->deleteLater();
return;
}
Phonon::MediaObject *mediaObject = new Phonon::MediaObject(parent);
Phonon::createPath(mediaObject, audioOutput);
QObject::connect(mediaObject, &Phonon::MediaObject::finished, audioOutput, &QObject::deleteLater);
QObject::connect(mediaObject, &Phonon::MediaObject::finished, mediaObject, &QObject::deleteLater);
mediaObject->setCurrentSource(soundURL);
mediaObject->play();
}