feat: support application hooks

Signed-off-by: ComixHe <heyuming@deepin.org>
This commit is contained in:
ComixHe 2023-10-07 15:15:08 +08:00 committed by Comix
parent f233279466
commit fb0fc0a8ee
13 changed files with 317 additions and 56 deletions

29
docs/applicationHooks.md Normal file
View File

@ -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"]
}
```
需要注意的是,配置文件的键是大小写敏感的。

66
src/applicationHooks.cpp Normal file
View File

@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "applicationHooks.h"
#include <QFile>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonParseError>
std::optional<ApplicationHook> 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<ApplicationHook> &hooks) noexcept
{
QStringList ret;
for (const auto &hook : hooks) {
ret.append(hook.execPath());
ret.append(hook.args());
}
return ret;
}

39
src/applicationHooks.h Normal file
View File

@ -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 <optional>
#include <QStringList>
class ApplicationHook
{
public:
static std::optional<ApplicationHook> 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<ApplicationHook> &hooks) noexcept;
#endif

View File

@ -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

View File

@ -7,6 +7,7 @@
#include "dbus/AMobjectmanager1adaptor.h"
#include "systemdsignaldispatcher.h"
#include "propertiesForwarder.h"
#include "applicationHooks.h"
#include <QFile>
#include <QDBusMessage>
#include <unistd.h>
@ -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<QDir>(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<QDir>(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<QDir>{autostartDirs.cbegin(), autostartDirs.cend()}, [&needToLaunch](const QFileInfo &info) {
if (info.isSymbolicLink()) {
needToLaunch.emplace_back(info.symLinkTarget());
}
return false;
});
applyIteratively(
QList<QDir>{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<QString, ApplicationHook> hooks;
applyIteratively(
QList<QDir>(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<QDBusObjectPath> ApplicationManager1Service::list() const
{
return m_applicationList.keys();
@ -433,34 +472,39 @@ void ApplicationManager1Service::ReloadApplications()
auto apps = m_applicationList.keys();
applyIteratively(QList<QDir>(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<QDir>(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<ApplicationService> &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<ApplicationService> &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);

View File

@ -41,7 +41,9 @@ public:
void updateApplication(const QSharedPointer<ApplicationService> &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<Identifier> m_identifier;
std::weak_ptr<ApplicationManager1Storage> m_storage;
QScopedPointer<JobManager1Service> m_jobManager{nullptr};
QStringList m_hookElements;
QMap<QDBusObjectPath, QSharedPointer<ApplicationService>> 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);
};

View File

@ -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<ApplicationManager1Service *>(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 {

View File

@ -18,6 +18,7 @@
#include <memory>
#include <utility>
#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<DesktopEntry> &entry) noexcept;
[[nodiscard]] bool autostartCheck(const QString &linkPath) const noexcept;
[[nodiscard]] ApplicationManager1Service *parent() { return dynamic_cast<ApplicationManager1Service *>(QObject::parent()); }
[[nodiscard]] const ApplicationManager1Service *parent() const
{
return dynamic_cast<ApplicationManager1Service *>(QObject::parent());
}
};
#endif

View File

@ -79,7 +79,11 @@ template <typename T>
using remove_cvr_t = std::remove_reference_t<std::remove_cv_t<T>>;
template <typename T>
void applyIteratively(QList<QDir> dirs, T &&func)
void applyIteratively(QList<QDir> dirs,
T &&func,
QDir::Filters filter = QDir::NoFilter,
QStringList nameFilter = {},
QDir::SortFlags sortFlag = QDir::SortFlag::NoSort)
{
static_assert(std::is_invocable_v<T, const QFileInfo &>, "apply function should only accept one QFileInfo");
static_assert(std::is_same_v<decltype(func(QFileInfo{})), bool>,
@ -93,9 +97,7 @@ void applyIteratively(QList<QDir> 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;
}

View File

@ -11,12 +11,14 @@
QStringList generateCommand(const QVariantMap &props) noexcept
{
std::vector<std::unique_ptr<LaunchOption>> 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<setUserLaunchOption>(value));
options.emplace_back(std::make_unique<setUserLaunchOption>(value));
} else if (key == setEnvLaunchOption::key()) {
options.push_back(std::make_unique<setEnvLaunchOption>(value));
options.emplace_back(std::make_unique<setEnvLaunchOption>(value));
} else if (key == hookLaunchOption::key()) {
options.emplace_back(std::make_unique<hookLaunchOption>(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<LaunchOption> &lOption, const std::unique_ptr<LaunchOption> &rOption) {
if (lOption->type() == rOption->type()) {
return lOption->m_priority >= rOption->m_priority;
}
if (lOption->type() == AppExecOption and rOption->type() == systemdOption) {
return false;
}

View File

@ -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;

View File

@ -0,0 +1,6 @@
{
"Exec": "/usr/bin/echo",
"Args": [
"for test"
]
}

25
tests/ut_hook.cpp Normal file
View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "applicationHooks.h"
#include <QDir>
#include <QStringList>
#include <gtest/gtest.h>
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);
}