diff --git a/docs/applicationHooks.md b/docs/applicationHooks.md new file mode 100644 index 0000000..203b58b --- /dev/null +++ b/docs/applicationHooks.md @@ -0,0 +1,29 @@ +# Application Hooks + +本文档描述了 dde-application-manager 的hook机制。 + +## 功能描述 + +hook 允许系统组件在应用启动前对应用的运行时环境做出配置(如cgroups)。 + +## 配置文件 + +hook 的配置文件需要放在'/usr/share/deepin/dde-application-manager/hook.d/'下,文件名必须符合以下规范: + - 以数字开头,作为hook的顺序标识。 + - 以`-`分割顺序和hook名。 + - 文件格式需要是`json`,文件扩展名同样以`json`结尾。 + +例如: `1-proxy.json`就是一个符合要求的hook配置文件。 + +### 文件格式 + +文件中需要写明hook二进制的绝对位置和所需要的参数,例如: + +```json +{ + "Exec": "/usr/bin/proxy", + "Args": ["--protocol=https","--port=12345"] +} +``` + +需要注意的是,配置文件的键是大小写敏感的。 diff --git a/src/applicationHooks.cpp b/src/applicationHooks.cpp new file mode 100644 index 0000000..4130799 --- /dev/null +++ b/src/applicationHooks.cpp @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "applicationHooks.h" +#include +#include +#include +#include +#include + +std::optional ApplicationHook::loadFromFile(const QString &filePath) noexcept +{ + auto index = filePath.lastIndexOf(QDir::separator()); + auto fileName = filePath.last(filePath.size() - index - 1); + + QFile file{filePath}; + if (!file.open(QFile::Text | QFile::ReadOnly | QFile::ExistingOnly)) { + qWarning() << "open hook file:" << filePath << "failed:" << file.errorString() << ", skip."; + return std::nullopt; + } + + auto content = file.readAll(); + QJsonParseError err; + auto json = QJsonDocument::fromJson(content, &err); + if (err.error != QJsonParseError::NoError) { + qWarning() << "parse hook failed:" << err.errorString(); + return std::nullopt; + } + if (json.isEmpty()) { + qWarning() << "empty hook, skip."; + return std::nullopt; + } + auto obj = json.object(); + + auto it = obj.constFind("Exec"); + if (it == obj.constEnd()) { + qWarning() << "invalid hook: lack of Exec."; + return std::nullopt; + } + auto exec = it->toString(); + QFileInfo info{exec}; + if (!(info.exists() and info.isExecutable())) { + qWarning() << "exec maybe doesn't exists or be executed."; + return std::nullopt; + } + + it = obj.constFind("Args"); + if (it == obj.constEnd()) { + qWarning() << "invalid hook: lack of Args."; + return std::nullopt; + } + auto args = it->toVariant().toStringList(); + + return ApplicationHook{std::move(fileName), std::move(exec), std::move(args)}; +} + +QStringList generateHooks(const QList &hooks) noexcept +{ + QStringList ret; + for (const auto &hook : hooks) { + ret.append(hook.execPath()); + ret.append(hook.args()); + } + return ret; +} diff --git a/src/applicationHooks.h b/src/applicationHooks.h new file mode 100644 index 0000000..6d5b83a --- /dev/null +++ b/src/applicationHooks.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef APPLICATIONHOOKS_H +#define APPLICATIONHOOKS_H + +#include +#include + +class ApplicationHook +{ +public: + static std::optional loadFromFile(const QString &filePath) noexcept; + + [[nodiscard]] const QString &hookName() const noexcept { return m_hookName; } + [[nodiscard]] const QString &execPath() const noexcept { return m_execPath; } + [[nodiscard]] const QStringList &args() const noexcept { return m_args; } + + friend bool operator>(const ApplicationHook &lhs, const ApplicationHook &rhs) { return lhs.m_hookName > rhs.m_hookName; }; + friend bool operator<(const ApplicationHook &lhs, const ApplicationHook &rhs) { return lhs.m_hookName < rhs.m_hookName; }; + friend bool operator==(const ApplicationHook &lhs, const ApplicationHook &rhs) { return lhs.m_hookName == rhs.m_hookName; }; + friend bool operator!=(const ApplicationHook &lhs, const ApplicationHook &rhs) { return !(lhs == rhs); }; + +private: + ApplicationHook(QString &&hookName, QString &&execPath, QStringList &&args) + : m_hookName(hookName) + , m_execPath(std::move(execPath)) + , m_args(std::move(args)) + { + } + QString m_hookName; + QString m_execPath; + QStringList m_args; +}; + +QStringList generateHooks(const QList &hooks) noexcept; + +#endif diff --git a/src/constant.h b/src/constant.h index 9c70113..b510eb8 100644 --- a/src/constant.h +++ b/src/constant.h @@ -51,4 +51,6 @@ constexpr auto STORAGE_VERSION = 0; constexpr auto ApplicationPropertiesGroup = u8"Application Properties"; constexpr auto LastLaunchedTime = u8"LastLaunchedTime"; +constexpr auto ApplicationManagerHookDir = u8"/deepin/dde-application-manager/hooks.d"; + #endif diff --git a/src/dbus/applicationmanager1service.cpp b/src/dbus/applicationmanager1service.cpp index 7409a57..f68731b 100644 --- a/src/dbus/applicationmanager1service.cpp +++ b/src/dbus/applicationmanager1service.cpp @@ -7,6 +7,7 @@ #include "dbus/AMobjectmanager1adaptor.h" #include "systemdsignaldispatcher.h" #include "propertiesForwarder.h" +#include "applicationHooks.h" #include #include #include @@ -70,6 +71,8 @@ void ApplicationManager1Service::initService(QDBusConnection &connection) noexce scanInstances(); + loadHooks(); + if (auto *ptr = new (std::nothrow) PropertiesForwarder{DDEApplicationManager1ObjectPath, this}; ptr == nullptr) { qCritical() << "new PropertiesForwarder of Application Manager failed."; } @@ -209,18 +212,23 @@ void ApplicationManager1Service::scanApplications() noexcept { const auto &desktopFileDirs = getDesktopFileDirs(); - applyIteratively(QList(desktopFileDirs.cbegin(), desktopFileDirs.cend()), [this](const QFileInfo &info) -> bool { - DesktopErrorCode err{DesktopErrorCode::NoError}; - auto ret = DesktopFile::searchDesktopFileByPath(info.absoluteFilePath(), err); - if (!ret.has_value()) { - qWarning() << "failed to search File:" << err; - return false; - } - if (!this->addApplication(std::move(ret).value())) { - qWarning() << "add Application failed, skip..."; - } - return false; // means to apply this function to the rest of the files - }); + applyIteratively( + QList(desktopFileDirs.cbegin(), desktopFileDirs.cend()), + [this](const QFileInfo &info) -> bool { + DesktopErrorCode err{DesktopErrorCode::NoError}; + auto ret = DesktopFile::searchDesktopFileByPath(info.absoluteFilePath(), err); + if (!ret.has_value()) { + qWarning() << "failed to search File:" << err; + return false; + } + if (!this->addApplication(std::move(ret).value())) { + qWarning() << "add Application failed, skip..."; + } + return false; // means to apply this function to the rest of the files + }, + QDir::Readable | QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, + {"*.desktop"}, + QDir::Name | QDir::DirsLast); } void ApplicationManager1Service::scanInstances() noexcept @@ -250,12 +258,17 @@ void ApplicationManager1Service::scanAutoStart() noexcept { auto autostartDirs = getAutoStartDirs(); QStringList needToLaunch; - applyIteratively(QList{autostartDirs.cbegin(), autostartDirs.cend()}, [&needToLaunch](const QFileInfo &info) { - if (info.isSymbolicLink()) { - needToLaunch.emplace_back(info.symLinkTarget()); - } - return false; - }); + applyIteratively( + QList{autostartDirs.cbegin(), autostartDirs.cend()}, + [&needToLaunch](const QFileInfo &info) { + if (info.isSymbolicLink()) { + needToLaunch.emplace_back(info.symLinkTarget()); + } + return false; + }, + QDir::Readable | QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, + {"*.desktop"}, + QDir::Name | QDir::DirsLast); while (!needToLaunch.isEmpty()) { const auto &filePath = needToLaunch.takeFirst(); @@ -269,6 +282,32 @@ void ApplicationManager1Service::scanAutoStart() noexcept } } +void ApplicationManager1Service::loadHooks() noexcept +{ + auto hookDirs = getXDGDataDirs(); + std::for_each(hookDirs.begin(), hookDirs.end(), [](QString &str) { str.append(ApplicationManagerHookDir); }); + QHash hooks; + + applyIteratively( + QList(hookDirs.begin(), hookDirs.end()), + [&hooks](const QFileInfo &info) -> bool { + auto fileName = info.fileName(); + if (!hooks.contains(fileName)) { + if (auto hook = ApplicationHook::loadFromFile(info.absoluteFilePath()); hook) { + hooks.insert(fileName, std::move(hook).value()); + } + } + return false; + }, + QDir::Files | QDir::NoDotAndDotDot | QDir::NoSymLinks | QDir::Readable, + {"*.json"}, + QDir::Name); + + auto hookList = hooks.values(); + std::sort(hookList.begin(), hookList.end()); + m_hookElements = generateHooks(hookList); +} + QList ApplicationManager1Service::list() const { return m_applicationList.keys(); @@ -433,34 +472,39 @@ void ApplicationManager1Service::ReloadApplications() auto apps = m_applicationList.keys(); - applyIteratively(QList(desktopFileDirs.cbegin(), desktopFileDirs.cend()), [this, &apps](const QFileInfo &info) -> bool { - DesktopErrorCode err{DesktopErrorCode::NoError}; - auto ret = DesktopFile::searchDesktopFileByPath(info.absoluteFilePath(), err); - if (!ret.has_value()) { + applyIteratively( + QList(desktopFileDirs.cbegin(), desktopFileDirs.cend()), + [this, &apps](const QFileInfo &info) -> bool { + DesktopErrorCode err{DesktopErrorCode::NoError}; + auto ret = DesktopFile::searchDesktopFileByPath(info.absoluteFilePath(), err); + if (!ret.has_value()) { + return false; + } + + auto file = std::move(ret).value(); + + auto destApp = + std::find_if(m_applicationList.cbegin(), + m_applicationList.cend(), + [&file](const QSharedPointer &app) { return file.desktopId() == app->id(); }); + + if (err != DesktopErrorCode::NoError) { + qWarning() << "error occurred:" << err << " skip this application."; + return false; + } + + if (destApp != m_applicationList.cend() and apps.contains(destApp.key())) { + apps.removeOne(destApp.key()); + updateApplication(destApp.value(), std::move(file)); + return false; + } + + addApplication(std::move(file)); return false; - } - - auto file = std::move(ret).value(); - - auto destApp = - std::find_if(m_applicationList.cbegin(), - m_applicationList.cend(), - [&file](const QSharedPointer &app) { return file.desktopId() == app->id(); }); - - if (err != DesktopErrorCode::NoError) { - qWarning() << "error occurred:" << err << " skip this application."; - return false; - } - - if (destApp != m_applicationList.cend() and apps.contains(destApp.key())) { - apps.removeOne(destApp.key()); - updateApplication(destApp.value(), std::move(file)); - return false; - } - - addApplication(std::move(file)); - return false; - }); + }, + QDir::Readable | QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, + {"*.desktop"}, + QDir::Name | QDir::DirsLast); for (const auto &key : apps) { removeOneApplication(key); diff --git a/src/dbus/applicationmanager1service.h b/src/dbus/applicationmanager1service.h index 8021943..11cd57f 100644 --- a/src/dbus/applicationmanager1service.h +++ b/src/dbus/applicationmanager1service.h @@ -41,7 +41,9 @@ public: void updateApplication(const QSharedPointer &destApp, DesktopFile desktopFile) noexcept; - JobManager1Service &jobManager() noexcept { return *m_jobManager; } + [[nodiscard]] JobManager1Service &jobManager() noexcept { return *m_jobManager; } + [[nodiscard]] const JobManager1Service &jobManager() const noexcept { return *m_jobManager; } + [[nodiscard]] const QStringList &applicationHooks() const noexcept { return m_hookElements; } public Q_SLOTS: QString Identify(const QDBusUnixFileDescriptor &pidfd, @@ -59,11 +61,13 @@ private: std::unique_ptr m_identifier; std::weak_ptr m_storage; QScopedPointer m_jobManager{nullptr}; + QStringList m_hookElements; QMap> m_applicationList; void scanApplications() noexcept; void scanInstances() noexcept; void scanAutoStart() noexcept; + void loadHooks() noexcept; void addInstanceToApplication(const QString &unitName, const QDBusObjectPath &systemdUnitPath); void removeInstanceFromApplication(const QString &unitName, const QDBusObjectPath &systemdUnitPath); }; diff --git a/src/dbus/applicationservice.cpp b/src/dbus/applicationservice.cpp index c13c65d..4aa7090 100644 --- a/src/dbus/applicationservice.cpp +++ b/src/dbus/applicationservice.cpp @@ -180,6 +180,10 @@ QDBusObjectPath ApplicationService::Launch(const QString &action, const QStringL } } + optionsMap.remove("_hooks"); // this is internal property, user shouldn't pass it to Application Manager + if (const auto &hooks = parent()->applicationHooks(); !hooks.isEmpty()) { + optionsMap.insert("_hooks", hooks); + } auto cmds = generateCommand(optionsMap); auto [bin, execCmds, res] = unescapeExec(execStr, fields); @@ -190,7 +194,7 @@ QDBusObjectPath ApplicationService::Launch(const QString &action, const QStringL } cmds.append(std::move(execCmds)); - auto &jobManager = static_cast(parent())->jobManager(); + auto &jobManager = parent()->jobManager(); return jobManager.addJob( m_applicationPath.path(), [this, binary = std::move(bin), commands = std::move(cmds)](const QVariant &variantValue) mutable -> QVariant { diff --git a/src/dbus/applicationservice.h b/src/dbus/applicationservice.h index cc98ca6..986d6a1 100644 --- a/src/dbus/applicationservice.h +++ b/src/dbus/applicationservice.h @@ -18,6 +18,7 @@ #include #include #include "applicationmanagerstorage.h" +#include "dbus/applicationmanager1service.h" #include "dbus/instanceservice.h" #include "global.h" #include "desktopentry.h" @@ -151,6 +152,11 @@ private: void updateAfterLaunch(bool isLaunch) noexcept; static bool shouldBeShown(const std::unique_ptr &entry) noexcept; [[nodiscard]] bool autostartCheck(const QString &linkPath) const noexcept; + [[nodiscard]] ApplicationManager1Service *parent() { return dynamic_cast(QObject::parent()); } + [[nodiscard]] const ApplicationManager1Service *parent() const + { + return dynamic_cast(QObject::parent()); + } }; #endif diff --git a/src/global.h b/src/global.h index 7514dfa..0e93d58 100644 --- a/src/global.h +++ b/src/global.h @@ -79,7 +79,11 @@ template using remove_cvr_t = std::remove_reference_t>; template -void applyIteratively(QList dirs, T &&func) +void applyIteratively(QList dirs, + T &&func, + QDir::Filters filter = QDir::NoFilter, + QStringList nameFilter = {}, + QDir::SortFlags sortFlag = QDir::SortFlag::NoSort) { static_assert(std::is_invocable_v, "apply function should only accept one QFileInfo"); static_assert(std::is_same_v, @@ -93,9 +97,7 @@ void applyIteratively(QList dirs, T &&func) qWarning() << "apply function to an non-existent directory:" << dir.absolutePath() << ", skip."; continue; } - - const auto &infoList = dir.entryInfoList( - {"*.desktop"}, QDir::Readable | QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot, QDir::Name | QDir::DirsLast); + const auto &infoList = dir.entryInfoList(nameFilter, filter, sortFlag); for (const auto &info : infoList) { if (info.isFile() and func(info)) { @@ -374,7 +376,7 @@ inline QString getXDGDataHome() return XDGDataHome; } -inline QStringList getDesktopFileDirs() +inline QStringList getXDGDataDirs() { auto XDGDataDirs = QString::fromLocal8Bit(qgetenv("XDG_DATA_DIRS")).split(':', Qt::SkipEmptyParts); @@ -384,14 +386,18 @@ inline QStringList getDesktopFileDirs() } XDGDataDirs.push_front(getXDGDataHome()); + return XDGDataDirs; +} +inline QStringList getDesktopFileDirs() +{ + auto XDGDataDirs = getXDGDataDirs(); std::for_each(XDGDataDirs.begin(), XDGDataDirs.end(), [](QString &str) { if (!str.endsWith(QDir::separator())) { str.append(QDir::separator()); } str.append("applications"); }); - return XDGDataDirs; } diff --git a/src/launchoptions.cpp b/src/launchoptions.cpp index 8ed8498..bd88e0a 100644 --- a/src/launchoptions.cpp +++ b/src/launchoptions.cpp @@ -11,12 +11,14 @@ QStringList generateCommand(const QVariantMap &props) noexcept { std::vector> options; - for (auto it = props.constKeyValueBegin(); it!= props.constKeyValueEnd(); ++it) { + for (auto it = props.constKeyValueBegin(); it != props.constKeyValueEnd(); ++it) { const auto &[key, value] = *it; if (key == setUserLaunchOption::key()) { - options.push_back(std::make_unique(value)); + options.emplace_back(std::make_unique(value)); } else if (key == setEnvLaunchOption::key()) { - options.push_back(std::make_unique(value)); + options.emplace_back(std::make_unique(value)); + } else if (key == hookLaunchOption::key()) { + options.emplace_back(std::make_unique(value)); } else { qWarning() << "unsupported options" << key; } @@ -27,6 +29,10 @@ QStringList generateCommand(const QVariantMap &props) noexcept std::sort(options.begin(), options.end(), [](const std::unique_ptr &lOption, const std::unique_ptr &rOption) { + if (lOption->type() == rOption->type()) { + return lOption->m_priority >= rOption->m_priority; + } + if (lOption->type() == AppExecOption and rOption->type() == systemdOption) { return false; } diff --git a/src/launchoptions.h b/src/launchoptions.h index 35bc0ae..e4ba7bb 100644 --- a/src/launchoptions.h +++ b/src/launchoptions.h @@ -20,7 +20,11 @@ struct LaunchOption }; [[nodiscard]] virtual const QString &type() const noexcept = 0; + uint32_t m_priority{0}; QVariant m_val; + +protected: + LaunchOption() = default; }; struct setUserLaunchOption : public LaunchOption @@ -70,4 +74,24 @@ struct splitLaunchOption : public LaunchOption [[nodiscard]] QStringList generateCommandLine() const noexcept override; }; +struct hookLaunchOption : public LaunchOption +{ + explicit hookLaunchOption(QVariant v) + : LaunchOption(std::move(v)) + { + m_priority = 1; + } + [[nodiscard]] const QString &type() const noexcept override + { + static QString tp{AppExecOption}; + return tp; + } + [[nodiscard]] static const QString &key() noexcept + { + static QString hook{"hook"}; + return hook; + } + [[nodiscard]] QStringList generateCommandLine() const noexcept override { return m_val.toStringList(); }; +}; + QStringList generateCommand(const QVariantMap &props) noexcept; diff --git a/tests/data/hooks.d/1-test.json b/tests/data/hooks.d/1-test.json new file mode 100644 index 0000000..e1953e5 --- /dev/null +++ b/tests/data/hooks.d/1-test.json @@ -0,0 +1,6 @@ +{ + "Exec": "/usr/bin/echo", + "Args": [ + "for test" + ] +} diff --git a/tests/ut_hook.cpp b/tests/ut_hook.cpp new file mode 100644 index 0000000..d4a75f3 --- /dev/null +++ b/tests/ut_hook.cpp @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "applicationHooks.h" +#include +#include +#include + +TEST(ApplicationHookTest, load) +{ + auto file = + QDir::currentPath() + QDir::separator() + "data" + QDir::separator() + "hooks.d" + QDir::separator() + "1-test.json"; + auto hook = ApplicationHook::loadFromFile(file); + EXPECT_TRUE(hook); + EXPECT_EQ(hook->hookName(), QString{"1-test.json"}); + EXPECT_EQ(hook->execPath(), QString{"/usr/bin/echo"}); + + QStringList tmp{"for test"}; + EXPECT_EQ(hook->args(), tmp); + + tmp.push_front("/usr/bin/echo"); + auto elem = generateHooks({std::move(hook).value()}); + EXPECT_EQ(elem, tmp); +}