Commit 93f9ef03 authored by Henri Chain's avatar Henri Chain 🛰
Browse files

[startplasma] Migrate autostart scripts to .desktop files

parent 6f754187
......@@ -31,8 +31,10 @@
<note><para>Please note that in this module all changes are immediately applied.</para></note>
<para>The program scans <filename>$HOME/.config/autostart/</filename>,
<filename class="directory">$HOME/.config/plasma-workspace/env</filename> and <filename class="directory">$HOME/.config/plasma-workspace/shutdown</filename> folders to check what programs and scripts are already there and displays them. It allows you to manage them easily.
<para>The program scans <filename>$HOME/.config/autostart/</filename> for applications and login scripts, <filename class="directory">$HOME/.config/plasma-workspace/env</filename> for pre-startup scripts and <filename class="directory">$HOME/.config/plasma-workspace/shutdown</filename> for logout scripts to check what programs and scripts are already there and displays them. It allows you to manage them easily.
</para>
<para>Login scripts are <filename class="extension">.desktop</filename> files with a <literal>X-KDE-AutostartScript=true</literal> key. Pre-startup scripts are run earlier and can be used to set environment variables.
</para>
<note><para>Note that you can change the location of your <filename class="directory">Autostart</filename>
......@@ -87,10 +89,10 @@ This column shows the name of the program or script you want to start with &plas
<varlistentry><term><guilabel>Properties</guilabel></term>
<listitem>
<para>
This button is only shown when you hover the item with the mouse pointer. The button (only enabled for programs &ie; <filename class="extension">.desktop</filename> files) allows you to change the properties of the program or script. You have general properties, permissions properties, a preview when applicable, and properties related to the application for programs. The default command is extracted from the <filename class="extension">.desktop</filename> file from the <literal>Exec</literal> key.
This button is only shown when you hover the item with the mouse pointer. The button (only enabled for programs and login scripts &ie; <filename class="extension">.desktop</filename> files) allows you to change the properties of the program or script. You have general properties, permissions properties, a preview when applicable, and properties related to the application or login script. The default command is extracted from the <filename class="extension">.desktop</filename> file from the <literal>Exec</literal> key.
</para>
<para>
For a script, the command is the path to the script and can not be modified.
For a logout script, the command is the path to the script and can not be modified.
</para>
</listitem>
</varlistentry>
......@@ -126,7 +128,7 @@ This will copy the program <filename class="extension">.desktop</filename> file
<varlistentry><term><guimenuitem>Add Login Script...</guimenuitem></term>
<listitem>
<para>
This item opens a dialog that asks you for the location of the script you want to add. Scripts set to run on login are copied or symlinked in <filename class="directory">$HOME/.config/autostart</filename> and will be run during Plasma startup.
This item opens a dialog that asks you for the location of the script you want to add. Scripts set to run on login will have a corresponding <filename class="extension">.desktop</filename> file created in your <filename class="directory">Autostart</filename> folder and will be run during Plasma startup.
</para>
</listitem>
</varlistentry>
......
......@@ -7,7 +7,7 @@ set(kcm_autostart_PART_SRCS
add_library(kcm_autostart MODULE ${kcm_autostart_PART_SRCS})
target_link_libraries(kcm_autostart KF5::I18n KF5::KIOCore KF5::KIOWidgets KF5::QuickAddons)
target_link_libraries(kcm_autostart KF5::I18n KF5::KIOCore KF5::KIOWidgets KF5::QuickAddons PW::KWorkspace)
kcoreaddons_desktop_to_json(kcm_autostart "package/metadata.desktop")
......
......@@ -34,16 +34,15 @@
#include <KLocalizedString>
#include <KOpenWithDialog>
#include <KPropertiesDialog>
#include <optional>
#include <autostartscriptdesktopfile.h>
// FDO user autostart directories are
// .config/autostart which has .desktop files executed by klaunch
// .config/autostart which has .desktop files executed by klaunch or systemd, some of which might be scripts
// Then we have Plasma-specific locations which run scripts
// .config/autostart-scripts which has scripts executed by ksmserver
// .config/plasma-workspace/shutdown which has scripts executed by startkde
// .config/plasma-workspace/env which has scripts executed by startkde
// .config/autostart-scripts which has scripts executed by plasma_session (now migrated to .desktop files)
// .config/plasma-workspace/shutdown which has scripts executed by plasma-shutdown
// .config/plasma-workspace/env which has scripts executed by startplasma
// in the case of pre-startup they have to end in .sh
// everywhere else it doesn't matter
......@@ -52,7 +51,7 @@
// share/autostart shouldn't be an option as this should be reserved for global autostart entries
static std::optional<AutostartEntry> loadDesktopEntry(const QString &fileName)
std::optional<AutostartEntry> AutostartModel::loadDesktopEntry(const QString &fileName)
{
KDesktopFile config(fileName);
const KConfigGroup grp = config.desktopGroup();
......@@ -75,7 +74,7 @@ static std::optional<AutostartEntry> loadDesktopEntry(const QString &fileName)
const auto lstEntry = grp.readXdgListEntry("OnlyShowIn");
const bool onlyInPlasma = lstEntry.contains(QLatin1String("KDE"));
const QString iconName = config.readIcon();
const auto kind = AutostartScriptDesktopFile::isAutostartScript(config) ? XdgScripts : XdgAutoStart; // .config/autostart load desktop at startup
const QString tryCommand = grp.readEntry("TryExec");
// Try to filter out entries that point to nonexistant programs
......@@ -85,38 +84,28 @@ static std::optional<AutostartEntry> loadDesktopEntry(const QString &fileName)
return {};
}
return std::optional<AutostartEntry>({name,
AutostartModel::AutostartEntrySource::XdgAutoStart, // .config/autostart load desktop at startup
enabled,
fileName,
onlyInPlasma,
iconName});
return AutostartEntry{name, kind, enabled, fileName, onlyInPlasma, iconName};
}
AutostartModel::AutostartModel(QObject *parent)
: QAbstractListModel(parent)
, m_xdgConfigPath(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation))
, m_xdgAutoStartPath(m_xdgConfigPath.filePath(QStringLiteral("autostart")))
{
}
QString AutostartModel::XdgAutoStartPath() const
{
return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/autostart/");
}
void AutostartModel::load()
{
beginResetModel();
m_entries.clear();
QDir autostartdir(XdgAutoStartPath());
if (!autostartdir.exists()) {
autostartdir.mkpath(XdgAutoStartPath());
}
autostartdir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
// Creates if doesn't already exist
m_xdgAutoStartPath.mkpath(QStringLiteral("."));
const auto filesInfo = autostartdir.entryInfoList();
// Needed to add all script entries after application entries
QVector<AutostartEntry> scriptEntries;
const auto filesInfo = m_xdgAutoStartPath.entryInfoList(QDir::Files);
for (const QFileInfo &fi : filesInfo) {
if (!KDesktopFile::isDesktopFile(fi.fileName())) {
continue;
......@@ -128,12 +117,16 @@ void AutostartModel::load()
continue;
}
m_entries.push_back(entry.value());
if (entry->source == XdgScripts) {
scriptEntries.push_back(entry.value());
} else {
m_entries.push_back(entry.value());
}
}
loadScriptsFromDir(QStringLiteral("/autostart-scripts/"), AutostartModel::AutostartEntrySource::XdgScripts);
// Treat them as XdgScripts so they appear together in the UI
loadScriptsFromDir(QStringLiteral("/plasma-workspace/env/"), AutostartModel::AutostartEntrySource::XdgScripts);
m_entries.append(scriptEntries);
loadScriptsFromDir(QStringLiteral("/plasma-workspace/env/"), AutostartModel::AutostartEntrySource::PlasmaEnvScripts);
loadScriptsFromDir(QStringLiteral("/plasma-workspace/shutdown/"), AutostartModel::AutostartEntrySource::PlasmaShutdown);
......@@ -142,15 +135,11 @@ void AutostartModel::load()
void AutostartModel::loadScriptsFromDir(const QString &subDir, AutostartModel::AutostartEntrySource kind)
{
const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + subDir;
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(path);
}
QDir dir(m_xdgConfigPath.filePath(subDir));
// Creates if doesn't already exist
dir.mkpath(QStringLiteral("."));
dir.setFilter(QDir::Files | QDir::NoDotAndDotDot);
const auto autostartDirFilesInfo = dir.entryInfoList();
const auto autostartDirFilesInfo = dir.entryInfoList(QDir::Files);
for (const QFileInfo &fi : autostartDirFilesInfo) {
QString fileName = fi.absoluteFilePath();
const bool isSymlink = fi.isSymLink();
......@@ -223,7 +212,7 @@ void AutostartModel::addApplication(const KService::Ptr &service)
// https://bugs.launchpad.net/ubuntu/+source/kde-workspace/+bug/923360
if (service->desktopEntryName().isEmpty() || service->entryPath().isEmpty()) {
// create a new desktop file in s_desktopPath
desktopPath = XdgAutoStartPath() + service->name() + QStringLiteral(".desktop");
desktopPath = m_xdgAutoStartPath.filePath(service->name() + QStringLiteral(".desktop"));
KDesktopFile desktopFile(desktopPath);
KConfigGroup kcg = desktopFile.desktopGroup();
......@@ -236,7 +225,7 @@ void AutostartModel::addApplication(const KService::Ptr &service)
desktopFile.sync();
} else {
desktopPath = XdgAutoStartPath() + service->desktopEntryName() + QStringLiteral(".desktop");
desktopPath = m_xdgAutoStartPath.filePath(service->desktopEntryName() + QStringLiteral(".desktop"));
QFile::remove(desktopPath);
......@@ -320,8 +309,6 @@ void AutostartModel::addScript(const QUrl &url, AutostartModel::AutostartEntrySo
}
const QString fileName = url.fileName();
int index = 0;
QString folder;
if (kind == AutostartModel::AutostartEntrySource::XdgScripts) {
int lastLoginScript = -1;
......@@ -332,44 +319,44 @@ void AutostartModel::addScript(const QUrl &url, AutostartModel::AutostartEntrySo
++lastLoginScript;
}
index = lastLoginScript + 1;
folder = QStringLiteral("/autostart-scripts/");
AutostartScriptDesktopFile desktopFile(fileName, file.filePath());
insertScriptEntry(lastLoginScript + 1, fileName, desktopFile.fileName(), kind);
} else if (kind == AutostartModel::AutostartEntrySource::PlasmaShutdown) {
index = m_entries.size();
folder = QStringLiteral("/plasma-workspace/shutdown/");
const QUrl destinationScript = QUrl::fromLocalFile(QDir(m_xdgConfigPath.filePath(QStringLiteral("/plasma-workspace/shutdown/"))).filePath(fileName));
KIO::CopyJob *job = KIO::link(url, destinationScript, KIO::HideProgressInfo);
job->setAutoRename(true);
job->setProperty("finalUrl", destinationScript);
connect(job, &KIO::CopyJob::renamed, this, [](KIO::Job *job, const QUrl &from, const QUrl &to) {
Q_UNUSED(from)
// in case the destination filename had to be renamed
job->setProperty("finalUrl", to);
});
connect(job, &KJob::finished, this, [this, url, kind](KJob *theJob) {
if (theJob->error()) {
qWarning() << "Could not add script entry" << theJob->errorString();
return;
}
const QUrl dest = theJob->property("finalUrl").toUrl();
insertScriptEntry(m_entries.size(), dest.fileName(), dest.path(), kind);
});
job->start();
} else {
Q_ASSERT(0);
}
}
QUrl destinationScript = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + folder + fileName);
KIO::CopyJob *job = KIO::link(url, destinationScript, KIO::HideProgressInfo);
job->setAutoRename(true);
job->setProperty("finalUrl", destinationScript);
connect(job, &KIO::CopyJob::renamed, this, [](KIO::Job *job, const QUrl &from, const QUrl &to) {
Q_UNUSED(from)
// in case the destination filename had to be renamed
job->setProperty("finalUrl", to);
});
connect(job, &KJob::finished, this, [this, index, url, kind](KJob *theJob) {
if (theJob->error()) {
qWarning() << "Could add script entry" << theJob->errorString();
return;
}
beginInsertRows(QModelIndex(), index, index);
const QUrl dest = theJob->property("finalUrl").toUrl();
AutostartEntry entry = AutostartEntry{dest.fileName(), kind, true, dest.path(), false, QStringLiteral("dialog-scripts")};
void AutostartModel::insertScriptEntry(int index, const QString &name, const QString &path, AutostartEntrySource kind)
{
beginInsertRows(QModelIndex(), index, index);
m_entries.insert(index, entry);
AutostartEntry entry = AutostartEntry{name, kind, true, path, false, QStringLiteral("dialog-scripts")};
endInsertRows();
});
m_entries.insert(index, entry);
job->start();
endInsertRows();
}
void AutostartModel::removeEntry(int row)
......
......@@ -20,8 +20,10 @@
#define AUTOSTARTMODEL_H
#include <QAbstractListModel>
#include <QDir>
#include <KService>
#include <optional>
struct AutostartEntry;
class QQuickItem;
......@@ -46,7 +48,7 @@ public:
XdgAutoStart = 0,
XdgScripts = 1,
PlasmaShutdown = 2,
PlasmaStart = 3,
PlasmaEnvScripts = 3,
};
Q_ENUM(AutostartEntrySource)
......@@ -69,8 +71,11 @@ Q_SIGNALS:
private:
void addApplication(const KService::Ptr &service);
void loadScriptsFromDir(const QString &subDir, AutostartEntrySource kind);
QString XdgAutoStartPath() const;
void insertScriptEntry(int index, const QString &name, const QString &path, AutostartModel::AutostartEntrySource kind);
static std::optional<AutostartEntry> loadDesktopEntry(const QString &fileName);
QDir m_xdgConfigPath;
QDir m_xdgAutoStartPath;
QVector<AutostartEntry> m_entries;
};
......
......@@ -75,7 +75,7 @@ KCM.ScrollViewKCM {
text: i18n("Properties")
icon.name: "document-properties"
onTriggered: kcm.model.editApplication(model.index, root)
visible: model.source === AutostartModel.XdgAutoStart
visible: model.source === AutostartModel.XdgAutoStart || model.source === AutostartModel.XdgScripts
},
Kirigami.Action {
text: i18n("Remove")
......@@ -91,9 +91,12 @@ KCM.ScrollViewKCM {
if (section == AutostartModel.XdgAutoStart) {
return i18n("Applications")
}
if (section == AutostartModel.XdgScripts || section == AutostartModel.PlasmaStart) {
if (section == AutostartModel.XdgScripts) {
return i18n("Login Scripts")
}
if (section == AutostartModel.PlasmaEnvScripts) {
return i18n("Pre-startup Scripts")
}
if (section == AutostartModel.PlasmaShutdown) {
return i18n("Logout Scripts")
}
......
......@@ -4,6 +4,7 @@ set(kworkspace_LIB_SRCS kdisplaymanager.cpp
sessionmanagement.cpp
sessionmanagementbackend.cpp
updatelaunchenvjob.cpp
autostartscriptdesktopfile.cpp
)
add_definitions(-DTRANSLATION_DOMAIN=\"libkworkspace\")
......@@ -73,6 +74,7 @@ install( FILES kdisplaymanager.h
kworkspace.h
sessionmanagement.h
updatelaunchenvjob.h
autostartscriptdesktopfile.h
${CMAKE_CURRENT_BINARY_DIR}/config-libkworkspace.h
${CMAKE_CURRENT_BINARY_DIR}/kworkspace_export.h
DESTINATION ${KDE_INSTALL_INCLUDEDIR}/kworkspace5 COMPONENT Devel )
......
/*
* SPDX-FileCopyrightText: 2021 Henri Chain <henri.chain@enioka.com>
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#include "autostartscriptdesktopfile.h"
#include <KConfigGroup>
#include <KDesktopFile>
#include <QDir>
#include <QStandardPaths>
static const auto autostartScriptKey = QStringLiteral("X-KDE-AutostartScript");
QDir AutostartScriptDesktopFile::autostartLocation()
{
return QDir(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation)).filePath("autostart");
}
AutostartScriptDesktopFile::AutostartScriptDesktopFile(const QString &name, const QString &execPath)
: KDesktopFile(autostartLocation().absoluteFilePath(name + QStringLiteral(".desktop")))
{
KConfigGroup kcg = desktopGroup();
kcg.writeEntry("Type", "Application");
kcg.writeEntry("Name", name);
kcg.writeEntry("Exec", execPath);
kcg.writeEntry("Icon", "dialog-scripts");
kcg.writeEntry(autostartScriptKey, "true");
kcg.writeEntry("Path", "");
}
bool AutostartScriptDesktopFile::isAutostartScript(const KDesktopFile &file)
{
return file.desktopGroup().readEntry<bool>(autostartScriptKey, false);
}
/*
* SPDX-FileCopyrightText: 2021 Henri Chain <henri.chain@enioka.com>
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
#pragma once
#include "kworkspace_export.h"
#include <KDesktopFile>
#include <QDir>
#include <QString>
/**
* Corresponds to a .desktop file in $XDG_CONFIG_HOME/autostart that points to
* an autostart script and has X-KDE-AutostartScript=true
*/
class KWORKSPACE_EXPORT AutostartScriptDesktopFile : public KDesktopFile
{
public:
explicit AutostartScriptDesktopFile(const QString &name, const QString &execPath);
/**
* Checks whether this KDesktopFile has X-KDE-AutostartScript=true
*/
static bool isAutostartScript(const KDesktopFile &file);
/**
* The location of autostart .desktop application and script files
* ($XDG_CONFIG_HOME/autostart)
*/
static QDir autostartLocation();
};
......@@ -132,15 +132,14 @@ public:
: Phase(autostart, parent)
{
}
void runUserAutostart();
bool migrateKDE4Autostart(const QString &folder);
void migrateKDE4Autostart();
void start() override
{
qCDebug(PLASMA_SESSION) << "Phase 2";
migrateKDE4Autostart();
addSubjob(new AutoStartAppsJob(m_autostart, 2));
addSubjob(new KDEDInitJob());
runUserAutostart();
}
};
......@@ -352,48 +351,21 @@ void RestoreSessionJob::start()
connect(watcher, &QDBusPendingCallWatcher::finished, watcher, &QObject::deleteLater);
}
void StartupPhase2::runUserAutostart()
void StartupPhase2::migrateKDE4Autostart()
{
// Now let's execute the scripts in the KDE-specific autostart-scripts folder.
// Migrate user autostart from kde4
Kdelibs4Migration migration;
if (!migration.kdeHomeFound()) {
return;
}
const QString autostartFolder =
QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QDir::separator() + QStringLiteral("autostart-scripts");
QDir dir(autostartFolder);
if (!dir.exists()) {
// Create dir in all cases, so that users can find it :-)
dir.mkpath(QStringLiteral("."));
if (!migrateKDE4Autostart(autostartFolder)) {
return;
}
}
const QStringList entries = dir.entryList(QDir::Files);
for (const QString &file : entries) {
// Don't execute backup files
if (!file.endsWith(QLatin1Char('~')) && !file.endsWith(QLatin1String(".bak")) && (file[0] != QLatin1Char('%') || !file.endsWith(QLatin1Char('%')))
&& (file[0] != QLatin1Char('#') || !file.endsWith(QLatin1Char('#')))) {
const QString fullPath = dir.absolutePath() + QLatin1Char('/') + file;
qCInfo(PLASMA_SESSION) << "Starting autostart script " << fullPath;
auto p = new KProcess; // deleted in onFinished lambda
p->setProgram(fullPath);
p->start();
connect(p, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), [p](int exitCode) {
qCInfo(PLASMA_SESSION) << "autostart script" << p->program() << "finished with exit code " << exitCode;
p->deleteLater();
});
}
}
}
bool StartupPhase2::migrateKDE4Autostart(const QString &autostartFolder)
{
// Migrate user autostart from kde4
Kdelibs4Migration migration;
if (!migration.kdeHomeFound()) {
return false;
}
// KDEHOME/Autostart was the default value for KGlobalSettings::autostart()
QString oldAutostart = migration.kdeHome() + QStringLiteral("/Autostart");
// That path could be customized in kdeglobals
......@@ -420,7 +392,7 @@ bool StartupPhase2::migrateKDE4Autostart(const QString &autostartFolder)
qCWarning(PLASMA_SESSION) << "Error copying" << src << "to" << dest;
}
}
return true;
return;
}
AutoStartAppsJob::AutoStartAppsJob(const AutoStart &autostart, int phase)
......
......@@ -34,6 +34,7 @@
#include <unistd.h>
#include <autostartscriptdesktopfile.h>
#include <updatelaunchenvjob.h>
#include "startplasma.h"
......@@ -538,6 +539,9 @@ bool startPlasmaSession(bool wayland)
}
});
// Create .desktop files for the scripts in .config/autostart-scripts
migrateUserScriptsAutostart();
if (!useSystemdBoot()) {
qCDebug(PLASMA_STARTUP) << "Using classic boot";
QProcess startPlasmaSession;
......@@ -611,3 +615,47 @@ void waitForKonqi()
}
}
}
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);
}
......@@ -54,6 +54,7 @@ void waitForKonqi();
static void resetSystemdFailedUnits();
static bool hasSystemdService(const QString &serviceName);
static bool useSystemdBoot();
static void migrateUserScriptsAutostart();
struct KillBeforeDeleter {
static inline void cleanup(QProcess *pointer)
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment