diff --git a/.gitignore b/.gitignore index 378eac2..5acb669 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build +.vscode diff --git a/CMakeLists.txt b/CMakeLists.txt index d2b9d19..51067f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,9 +44,9 @@ foreach(obj IN LISTS AdaptorLib) endforeach() add_subdirectory(src) -# add_subdirectory(docs) -# add_subdirectory(plugin) +add_subdirectory(plugin) +# add_subdirectory(docs) include(CTest) if(BUILD_TESTING) diff --git a/api/dbus/org.desktopspec.ApplicationManager1.Application.xml b/api/dbus/org.desktopspec.ApplicationManager1.Application.xml index e516d7f..9298ca7 100644 --- a/api/dbus/org.desktopspec.ApplicationManager1.Application.xml +++ b/api/dbus/org.desktopspec.ApplicationManager1.Application.xml @@ -3,9 +3,7 @@ diff --git a/api/dbus/org.desktopspec.ApplicationManager1.xml b/api/dbus/org.desktopspec.ApplicationManager1.xml index e0e26e5..e3fd60b 100644 --- a/api/dbus/org.desktopspec.ApplicationManager1.xml +++ b/api/dbus/org.desktopspec.ApplicationManager1.xml @@ -2,6 +2,15 @@ + + + + diff --git a/plugin/CMakeLists.txt b/plugin/CMakeLists.txt new file mode 100644 index 0000000..4067e52 --- /dev/null +++ b/plugin/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(appLauncher) diff --git a/plugin/appLauncher/CMakeLists.txt b/plugin/appLauncher/CMakeLists.txt new file mode 100644 index 0000000..bd33540 --- /dev/null +++ b/plugin/appLauncher/CMakeLists.txt @@ -0,0 +1,13 @@ +include(GNUInstallDirs) + +set(APP_LAUNCHER_BIN_NAME am-launcher) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(SYSTEMD REQUIRED IMPORTED_TARGET libsystemd) + +add_executable(${APP_LAUNCHER_BIN_NAME} launcher.cpp) +target_link_libraries(${APP_LAUNCHER_BIN_NAME} PRIVATE + PkgConfig::SYSTEMD +) + +install(TARGETS ${APP_LAUNCHER_BIN_NAME} DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}/deepin/application-manager/) diff --git a/plugin/appLauncher/launcher.cpp b/plugin/appLauncher/launcher.cpp new file mode 100644 index 0000000..b2cbe59 --- /dev/null +++ b/plugin/appLauncher/launcher.cpp @@ -0,0 +1,374 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum class ExitCode { SystemdError = -3, InvalidInput = -2, InternalError = -1, Done = 0, Waiting = 1 }; + +struct JobRemoveResult +{ + std::string_view id; + int removedFlag{0}; + ExitCode result{ExitCode::Waiting}; +}; + +constexpr static auto SystemdService = "org.freedesktop.systemd1"; +constexpr static auto SystemdObjectPath = "/org/freedesktop/systemd1"; +constexpr static auto SystemdInterfaceName = "org.freedesktop.systemd1.Manager"; + +using msg_ptr = sd_bus_message *; +using bus_ptr = sd_bus *; + +static ExitCode fromString(const std::string &str) +{ + if (str == "done") { + return ExitCode::Done; + } else if (str == "canceled" or str == "timeout" or str == "failed" or str == "dependency" or str == "skipped") { + return ExitCode::SystemdError; + } else if (str == "internalError") { + return ExitCode::InternalError; + } else if (str == "invalidInput") { + return ExitCode::InvalidInput; + } + __builtin_unreachable(); +} + +static ExitCode fromString(const char *str) +{ + if (!str) { + return ExitCode::Waiting; + } + std::string tmp{str}; + return fromString(tmp); +} + +[[noreturn]] static void releaseRes(sd_bus_error &error, msg_ptr &msg, bus_ptr &bus, ExitCode ret) +{ + sd_bus_error_free(&error); + sd_bus_message_unref(msg); + sd_bus_unref(bus); + + std::exit(static_cast(ret)); +} + +static int processExecStart(msg_ptr &msg, const std::vector &execArgs) +{ + int ret; + if (ret = sd_bus_message_append(msg, "s", "ExecStart"); ret < 0) { + sd_journal_perror("append ExecStart failed."); + return ret; + } + + if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_VARIANT, "a(sasb)"); ret < 0) { + sd_journal_perror("open variant of execStart failed."); + if (auto tmp = sd_bus_message_close_container(msg)) + return ret; + } + + if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_ARRAY, "(sasb)"); ret < 0) { + sd_journal_perror("open array of execStart failed."); + return ret; + } + + if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_STRUCT, "sasb"); ret < 0) { + sd_journal_perror("open struct of execStart failed."); + return ret; + } + + if (ret = sd_bus_message_append(msg, "s", execArgs[0].data()); ret < 0) { + sd_journal_perror("append binary of execStart failed."); + return ret; + } + + if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_ARRAY, "s"); ret < 0) { + sd_journal_perror("open array of execStart variant failed."); + return ret; + } + for (std::size_t i = 0; i < execArgs.size(); ++i) { + if (ret = sd_bus_message_append(msg, "s", execArgs[i].data()); ret < 0) { + sd_journal_perror("append args of execStart failed."); + return ret; + } + } + + if (ret = sd_bus_message_close_container(msg); ret < 0) { + sd_journal_perror("close array of execStart failed."); + return ret; + } + + if (ret = sd_bus_message_append(msg, "b", 0); + ret < 0) { // this value indicate that systemd should be considered a failure if the process exits uncleanly + sd_journal_perror("append boolean of execStart failed."); + return ret; + } + + if (ret = sd_bus_message_close_container(msg); ret < 0) { + sd_journal_perror("close struct of execStart failed."); + return ret; + } + + if (ret = sd_bus_message_close_container(msg); ret < 0) { + sd_journal_perror("close array of execStart failed."); + return ret; + } + + if (ret = sd_bus_message_close_container(msg); ret < 0) { + sd_journal_perror("close variant of execStart failed."); + return ret; + } + + return 0; +} + +static int processKVPair(msg_ptr &msg, const std::map &props) +{ + int ret; + if (!props.empty()) { + for (auto [key, value] : props) { + // NOTE: append if necessary + // usage: + // sd_bus_message_append(msg,"s",key.data()); + // sd_bus_message_open_container(msg,SD_BUS_TYPE_VARIANT,"[corresponding type]"); + // append content.... + // sd_bus_message_close_container(msg); + } + } + return 0; +} + +static std::string cmdParse(msg_ptr &msg, const std::vector cmdLines) +{ + std::string serviceName{"internalError"}; + std::map props; + for (auto str : cmdLines) { // avoid stl exception + if (str.size() < 2) { + sd_journal_print(LOG_WARNING, "invalid option %s.", str.data()); + continue; + } + if (str.substr(0, 2) != "--") { + sd_journal_print(LOG_INFO, "unknown option %s.", str.data()); + continue; + } + + auto kvStr = str.substr(2); + if (!kvStr.empty()) [[likely]] { + auto it = kvStr.cbegin(); + if (it = std::find(it, kvStr.cend(), '='); it == kvStr.cend()) { + sd_journal_print(LOG_WARNING, "invalid k-v pair: %s", kvStr.data()); + continue; + } + auto splitIndex = std::distance(kvStr.cbegin(), it); + if (++it == kvStr.cend()) { + sd_journal_print(LOG_WARNING, "invalid k-v pair: %s", kvStr.data()); + continue; + } + + auto key = kvStr.substr(0, splitIndex); + if (key == "Type") { // type must be exec + continue; + } + props[key] = kvStr.substr(splitIndex + 1); + continue; + } + + // Processing of the binary file and its parameters that am want to launch + auto it = std::find(cmdLines.cbegin(), cmdLines.cend(), str); + std::vector execArgs(++it, cmdLines.cend()); + if (execArgs.empty()) { + sd_journal_print(LOG_ERR, "param exec is empty."); + serviceName = "invalidInput"; + return serviceName; + } + int ret; + if (props.find("unitName") == props.cend()) { + sd_journal_perror("unitName doesn't exists."); + serviceName = "invalidInput"; + return serviceName; + } + if (ret = sd_bus_message_append(msg, "s", props["unitName"].data()); ret < 0) { // unitName + sd_journal_perror("append unitName failed."); + return serviceName; + } else { + serviceName = props["unitName"]; + props.erase("unitName"); + } + + if (ret = sd_bus_message_append(msg, "s", "replace"); ret < 0) { // start mode + sd_journal_perror("append startMode failed."); + return serviceName; + } + + // process properties: a(sv) + if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_ARRAY, "(sv)"); ret < 0) { + sd_journal_perror("open array failed."); + return serviceName; + } + + if (ret = sd_bus_message_append(msg, "(sv)", "Type", "s", "exec"); ret < 0) { + sd_journal_perror("append type failed."); + return serviceName; + } + + if (ret = sd_bus_message_open_container(msg, SD_BUS_TYPE_STRUCT, "sv"); ret < 0) { + sd_journal_perror("open struct failed."); + return serviceName; + } + + if (ret = processKVPair(msg, props); ret < 0) { // process props + serviceName = "invalidInput"; + return serviceName; + } + + if (ret = processExecStart(msg, execArgs); ret < 0) { + serviceName = "invalidInput"; + return serviceName; + } + + if (ret = sd_bus_message_close_container(msg); ret < 0) { + sd_journal_perror("close struct failed."); + return serviceName; + } + if (ret = sd_bus_message_close_container(msg); ret < 0) { + sd_journal_perror("close array failed."); + return serviceName; + } + + // append aux, it's unused for now + if (ret = sd_bus_message_append(msg, "a(sa(sv))", 0); ret < 0) { + sd_journal_perror("append aux failed."); + return serviceName; + } + + break; + } + return serviceName; +} + +int jobRemovedReceiver(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) +{ + int ret; + if (ret = sd_bus_error_is_set(ret_error); ret != 0) { + sd_journal_print(LOG_ERR, "JobRemoved error: [%s,%s]", ret_error->name, ret_error->message); + } else { + const char *serviceId{nullptr}, *jobResult{nullptr}; + if (ret = sd_bus_message_read(m, "uoss", nullptr, nullptr, &serviceId, &jobResult); ret < 0) { + sd_journal_perror("read from JobRemoved failed."); + } else { + auto ptr = reinterpret_cast(userdata); + if (ptr->id == serviceId) { + ptr->removedFlag = 1; + ptr->result = fromString(jobResult); + } + } + } + + if (ret_error and ret_error->_need_free) { + sd_bus_error_free(ret_error); + } + + return ret; +} + +static int process_dbus_message(sd_bus *bus) +{ + int ret; + ret = sd_bus_process(bus, nullptr); + if (ret < 0) { + sd_journal_print(LOG_ERR, "event loop error."); + return ret; + } + + if (ret > 0) { + return 0; + } + + ret = sd_bus_wait(bus, std::numeric_limits::max()); + if (ret < 0) { + sd_journal_print(LOG_ERR, "sd-bus event loop error:%s", strerror(-ret)); + return ret; + } + + return ret; +} + +int main(int argc, const char *argv[]) +{ + sd_bus_error error{SD_BUS_ERROR_NULL}; + sd_bus_message *msg{nullptr}; + sd_bus *bus{nullptr}; + std::string serviceId; + int ret; + + if (ret = sd_bus_open_user(&bus); ret < 0) { + sd_journal_perror("Failed to connect to user bus."); + releaseRes(error, msg, bus, ExitCode::InternalError); + } + + if (ret = sd_bus_message_new_method_call( + bus, &msg, SystemdService, SystemdObjectPath, SystemdInterfaceName, "StartTransientUnit"); + ret < 0) { + sd_journal_perror("Failed to create D-Bus call message"); + releaseRes(error, msg, bus, ExitCode::InternalError); + } + + std::vector args; + for (std::size_t i = 1; i < argc; ++i) { + args.emplace_back(argv[i]); + } + + serviceId = cmdParse(msg, args); + if (serviceId == "internalError") { + releaseRes(error, msg, bus, ExitCode::InternalError); + } else if (serviceId == "invalidInput") { + releaseRes(error, msg, bus, ExitCode::InvalidInput); + } + + const char *path{nullptr}; + JobRemoveResult resultData{serviceId}; + + if (ret = sd_bus_match_signal( + bus, nullptr, SystemdService, SystemdObjectPath, SystemdInterfaceName, "JobRemoved", jobRemovedReceiver, &resultData); + ret < 0) { + sd_journal_perror("add signal matcher failed."); + releaseRes(error, msg, bus, ExitCode::InternalError); + } + + sd_bus_message *reply{nullptr}; + if (ret = sd_bus_call(bus, msg, 0, &error, &reply); ret < 0) { + sd_bus_error_get_errno(&error); + sd_journal_print(LOG_ERR, "launch failed: [%s,%s]", error.name, error.message); + releaseRes(error, msg, bus, ExitCode::InternalError); + } + + if (ret = sd_bus_message_read(reply, "o", &path); ret < 0) { + sd_journal_perror("failed to parse response message."); + sd_bus_message_unref(reply); + releaseRes(error, msg, bus, ExitCode::InternalError); + } + + sd_bus_message_unref(reply); + + while (true) { // wait for jobRemoved + int ret = process_dbus_message(bus); + if (ret < 0) { + releaseRes(error, msg, bus, ExitCode::InternalError); + } + + if (resultData.removedFlag) { + break; + } + } + + releaseRes(error, msg, bus, resultData.result); +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6dae734..a12f538 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -19,8 +19,13 @@ target_link_libraries(${LIB_NAME} PUBLIC ) add_executable(${BIN_NAME} main.cpp utils.cpp) + target_link_libraries(${BIN_NAME} PRIVATE ${LIB_NAME} ) +target_include_directories(${BIN_NAME} PRIVATE + ${PROJECT_BINARY_DIR} +) + install(TARGETS ${BIN_NAME} DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/cgroupsidentifier.cpp b/src/cgroupsidentifier.cpp new file mode 100644 index 0000000..c860a70 --- /dev/null +++ b/src/cgroupsidentifier.cpp @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "cgroupsidentifier.h" +#include "global.h" +#include +#include +#include + +IdentifyRet CGroupsIdentifier::Identify(pid_t pid) +{ + auto AppCgroupPath = QString("/proc/%1/cgroup").arg(pid); + QFile AppCgroupFile{AppCgroupPath}; + if (!AppCgroupFile.open(QFile::ExistingOnly | QFile::ReadOnly | QFile::Text)) { + qWarning() << "open " << AppCgroupPath << "failed: " << AppCgroupFile.errorString(); + return {}; + } + auto appInstance = parseCGroupsPath( + AppCgroupFile.readAll().split(':').last().trimmed()); // FIXME: support CGroup version detection and multi-line parsing + auto appInstanceComponent = appInstance.split('@'); + if (appInstanceComponent.count() != 2) { + qWarning() << "Application Instance format is invalid." << appInstanceComponent; + return {}; + } + return {appInstanceComponent.first(), appInstanceComponent.last()}; +} + +QString CGroupsIdentifier::parseCGroupsPath(QString CGP) const noexcept +{ + if (CGP.isEmpty()) { + qWarning() << "CGroupPath is empty."; + return {}; + } + auto unescapedCGP = unescapeString(CGP); + auto CGPSlices = unescapedCGP.split('/'); + auto curUID = getCurrentUID(); + + if (CGPSlices.first() != "user.slice") { + qWarning() << "unrecognized process."; + return {}; + } + auto processUIDStr = CGPSlices[1].split('.').first().split('-').last(); + + if (processUIDStr.isEmpty()) { + qWarning() << "no uid found."; + return {}; + } + + if (auto processUID = processUIDStr.toInt(); processUID != getCurrentUID()) { + qWarning() << "process is not in CGroups of current user, ignore...."; + return {}; + } + + auto appInstance = CGPSlices.last().split('.').first(); + if (appInstance.isEmpty()) { + qWarning() << "get AppId failed."; + return {}; + } + return appInstance; +} diff --git a/src/dbus/applicationmanager1service.cpp b/src/dbus/applicationmanager1service.cpp index f9f95fd..7068a9c 100644 --- a/src/dbus/applicationmanager1service.cpp +++ b/src/dbus/applicationmanager1service.cpp @@ -2,12 +2,27 @@ // // SPDX-License-Identifier: LGPL-3.0-or-later #include "applicationmanager1service.h" -#include "applicationadaptor.h" -#include "global.h" +#include "applicationmanager1adaptor.h" +#include +#include ApplicationManager1Service::~ApplicationManager1Service() = default; -ApplicationManager1Service::ApplicationManager1Service() = default; +ApplicationManager1Service::ApplicationManager1Service(std::unique_ptr ptr, QDBusConnection &connection) + : m_identifier(std::move(ptr)) +{ + if (!connection.registerService(DDEApplicationManager1ServiceName)) { + qFatal() << connection.lastError(); + } + + if (!registerObjectToDBus(new ApplicationManager1Adaptor{this}, + DDEApplicationManager1ObjectPath, + getDBusInterface())) { + std::terminate(); + } + + m_jobManager.reset(new JobManager1Service(this)); +} QList ApplicationManager1Service::list() const { @@ -24,9 +39,18 @@ void ApplicationManager1Service::removeAllApplication() m_applicationList.clear(); } -QDBusObjectPath ApplicationManager1Service::Application(const QString &id) +QDBusObjectPath ApplicationManager1Service::Application(const QString &id) const { - // TODO: impl + auto ret = std::find_if(m_applicationList.cbegin(), m_applicationList.cend(), [&id](const auto &app) { + if (app->id() == id) { + return true; + } + return false; + }); + + if (ret != m_applicationList.cend()) { + return ret.key(); + } return {}; } @@ -34,8 +58,52 @@ QString ApplicationManager1Service::Identify(const QDBusUnixFileDescriptor &pidf QDBusObjectPath &application, QDBusObjectPath &application_instance) { - // TODO: impl - return {}; + if (!pidfd.isValid()) { + qWarning() << "pidfd isn't a valid unix file descriptor"; + return {}; + } + + Q_ASSERT_X(static_cast(m_identifier), "Identify", "Broken Identifier."); + + QString fdFilePath = QString{"/proc/self/fdinfo/%1"}.arg(pidfd.fileDescriptor()); + QFile fdFile{fdFilePath}; + if (!fdFile.open(QFile::ExistingOnly | QFile::ReadOnly | QFile::Text)) { + qWarning() << "open " << fdFilePath << "failed: " << fdFile.errorString(); + return {}; + } + auto content = fdFile.readAll(); + QTextStream stream{content}; + QString appPid; + while (!stream.atEnd()) { + auto line = stream.readLine(); + if (line.startsWith("Pid")) { + appPid = line.split(":").last().trimmed(); + break; + } + } + if (appPid.isEmpty()) { + qWarning() << "can't find pid which corresponding with the instance of this application."; + return {}; + } + bool ok; + auto pid = appPid.toUInt(&ok); + if (!ok) { + qWarning() << "AppId is failed to convert to uint."; + return {}; + } + + const auto ret = m_identifier->Identify(pid); + + auto app = std::find_if(m_applicationList.cbegin(), m_applicationList.cend(), [&ret](const auto &appPtr) { + return appPtr->id() == ret.ApplicationId; + }); + if (app == m_applicationList.cend()) { + qWarning() << "can't find application:" << ret.ApplicationId; + return {}; + } + application = m_applicationList.key(*app); + application_instance = (*app)->findInstance(ret.InstanceId); + return ret.ApplicationId; } QDBusObjectPath ApplicationManager1Service::Launch(const QString &id, @@ -43,18 +111,13 @@ QDBusObjectPath ApplicationManager1Service::Launch(const QString &id, const QStringList &fields, const QVariantMap &options) { - // TODO: impl reset of Launch - QString objectPath; - if (id.contains('.')) { - objectPath = id.split('.').first(); + auto app = Application(id); + if (app.path().isEmpty()) { + qWarning() << "No such application."; + return {}; } - objectPath.prepend(DDEApplicationManager1ApplicationObjectPath); - QSharedPointer app{new ApplicationService{id}}; - auto *ptr = app.data(); - if (registerObjectToDbus(new ApplicationAdaptor(ptr), objectPath, getDBusInterface())) { - QDBusObjectPath path{objectPath}; - m_applicationList.insert(path, app); - return path; - } - return {}; + const auto &value = m_applicationList.value(app); + return value->Launch(actions, fields, options); } + +void ApplicationManager1Service::UpdateApplicationInfo(const QStringList &update_path) {} diff --git a/src/dbus/applicationmanager1service.h b/src/dbus/applicationmanager1service.h index c6b6b01..243b28c 100644 --- a/src/dbus/applicationmanager1service.h +++ b/src/dbus/applicationmanager1service.h @@ -9,14 +9,19 @@ #include #include #include +#include +#include #include +#include "jobmanager1service.h" #include "applicationservice.h" +#include "applicationadaptor.h" +#include "identifier.h" class ApplicationManager1Service final : public QObject { Q_OBJECT public: - ApplicationManager1Service(); + explicit ApplicationManager1Service(std::unique_ptr ptr, QDBusConnection &connection); ~ApplicationManager1Service() override; ApplicationManager1Service(const ApplicationManager1Service &) = delete; ApplicationManager1Service(ApplicationManager1Service &&) = delete; @@ -25,16 +30,35 @@ public: Q_PROPERTY(QList List READ list) QList list() const; - void addApplication(const QString &ID, const QStringList &actions, bool AutoStart = false); + template + bool addApplication(T &&desktopFileSource) + { + QSharedPointer application{new ApplicationService{std::forward(desktopFileSource), this}}; + if (!application) { + return false; + } + if (!registerObjectToDBus(new ApplicationAdaptor{application.data()}, + application->m_applicationPath.path(), + getDBusInterface())) { + return false; + } + m_applicationList.insert(application->m_applicationPath, application); + return true; + } bool removeOneApplication(const QDBusObjectPath &application); void removeAllApplication(); + JobManager1Service &jobManager() noexcept { return *m_jobManager; } + public Q_SLOTS: - QDBusObjectPath Application(const QString &id); + QDBusObjectPath Application(const QString &id) const; QString Identify(const QDBusUnixFileDescriptor &pidfd, QDBusObjectPath &application, QDBusObjectPath &application_instance); QDBusObjectPath Launch(const QString &id, const QString &action, const QStringList &fields, const QVariantMap &options); + void UpdateApplicationInfo(const QStringList &update_path); private: + std::unique_ptr m_identifier; + QScopedPointer m_jobManager{nullptr}; QMap> m_applicationList; }; diff --git a/src/dbus/applicationservice.cpp b/src/dbus/applicationservice.cpp index 0d46000..a5218cb 100644 --- a/src/dbus/applicationservice.cpp +++ b/src/dbus/applicationservice.cpp @@ -3,61 +3,220 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include "applicationservice.h" -#include "applicationadaptor.h" +#include "applicationmanager1service.h" #include "instanceadaptor.h" +#include "pwd.h" #include +#include +#include +#include +#include +#include +#include -ApplicationService::ApplicationService(QString ID) - : m_AutoStart(false) - , m_ID(std::move(ID)) - , m_applicationPath(DDEApplicationManager1ApplicationObjectPath + m_ID) +ApplicationService::~ApplicationService() { + m_desktopSource.destruct(m_isPersistence); } -ApplicationService::~ApplicationService() = default; - QString ApplicationService::GetActionName(const QString &identifier, const QStringList &env) { - // TODO: impl - return {}; + const auto &supportedActions = actions(); + if (supportedActions.isEmpty()) { + return {}; + } + if (auto index = supportedActions.indexOf(identifier); index == -1) { + qWarning() << "can't find " << identifier << " in supported actions List."; + return {}; + } + + const auto &actionHeader = QString{"%1%2"}.arg(DesktopFileActionKey, identifier); + const auto &actionName = m_entry->value(actionHeader, "Name"); + if (!actionName) { + return {}; + } + + QString locale{""}; + bool ok; + if (!env.isEmpty()) { + QString lcStr; + for (const auto &lc : env) { + if (lc.startsWith("LANG")) { + locale = lc.split('=').last(); + } + + if (lc.startsWith("LC_ALL")) { + locale = lc.split('=').last(); + break; + } + } + } + + QLocale lc = locale.isEmpty() ? getUserLocale() : QLocale{locale}; + + const auto &name = actionName->toLocaleString(lc, ok); + if (!ok) { + qWarning() << "convert to locale string failed, dest locale:" << lc; + return {}; + } + return name; } -QDBusObjectPath ApplicationService::Launch(const QString &action, const QStringList &fields, const QVariantMap &options) +QDBusObjectPath ApplicationService::Launch(QString action, QStringList fields, QVariantMap options) { - // TODO: impl launch app from systemd. - QString objectPath{m_applicationPath.path() + "/" + QUuid::createUuid().toString()}; - QSharedPointer ins{new InstanceService{objectPath, ""}}; - auto *ptr = ins.data(); - if (registerObjectToDbus(new InstanceAdaptor(ptr), objectPath, getDBusInterface())) { - m_Instances.insert(QDBusObjectPath{objectPath}, ins); - return QDBusObjectPath{objectPath}; + QString execStr; + bool ok; + const auto &supportedActions = actions(); + + while (!action.isEmpty() and !supportedActions.isEmpty()) { // break trick + if (auto index = supportedActions.indexOf(action); index == -1) { + qWarning() << "can't find " << action << " in supported actions List. application will use default action to launch."; + break; + } + + const auto &actionHeader = QString{"%1%2"}.arg(DesktopFileEntryKey, action); + const auto &actionExec = m_entry->value(actionHeader, "Exec"); + if (!actionExec) { + break; + } + execStr = actionExec->toString(ok); + if (!ok) { + qWarning() << "exec value to string failed, try default action."; + break; + } + break; } - return {}; + + if (execStr.isEmpty()) { + auto Actions = m_entry->value(DesktopFileEntryKey, "Exec"); + if (!Actions) { + qWarning() << "application has isn't executable."; + return {}; + } + execStr = Actions->toString(ok); + if (!ok) { + qWarning() << "default action value to string failed. abort..."; + return {}; + } + } + + auto [bin, cmds, res] = unescapeExec(execStr, fields); + + uid_t uid; + auto it = options.find("uid"); + if (it != options.cend()) { + uid = it->toUInt(&ok); + if (!ok) { + qWarning() << "convert uid string to uint failed: " << *it; + return {}; + } + + if (uid != getCurrentUID()) { + cmds.prepend(userNameLookup(uid)); + cmds.prepend("--user"); + cmds.prepend("pkexec"); + } + } else { + uid = getCurrentUID(); + } + + cmds.prepend("--"); + + auto &jobManager = m_parent->jobManager(); + + return jobManager.addJob( + m_applicationPath.path(), + [this, binary = std::move(bin), commands = std::move(cmds)](QVariant variantValue) mutable -> QVariant { + auto resourceFile = variantValue.toString(); + if (resourceFile.isEmpty()) { + auto instanceRandomUUID = QUuid::createUuid().toString(QUuid::Id128); + commands.push_front(QString{R"(--unitName=DDE-%1@%2.service)"}.arg( + this->id(), instanceRandomUUID)); // launcher should use this instanceId + QProcess process; + process.start(m_launcher, commands); + process.waitForFinished(); + if (auto code = process.exitCode(); code != 0) { + qWarning() << "Launch Application Failed. exitCode:" << code; + return false; + } + return addOneInstance(instanceRandomUUID, m_applicationPath.path()); // TODO: pass correct Systemd Unit Path + } + + int location{0}; + location = commands.indexOf(R"(%f)"); + if (location != -1) { // due to std::move, there only remove once + commands.remove(location); + } + + auto url = QUrl::fromUserInput(resourceFile); + if (!url.isValid()) { // if url is invalid, passing to launcher directly + auto scheme = url.scheme(); + if (!scheme.isEmpty()) { + // TODO: resourceFile = processRemoteFile(resourceFile); + } + } + + // resourceFile must be available in the following contexts + auto tmp = commands; + tmp.insert(location, resourceFile); + auto instanceRandomUUID = QUuid::createUuid().toString(QUuid::Id128); + tmp.push_front(QString{R"(--unitName=DDE-%1@%2.service)"}.arg(this->id(), instanceRandomUUID)); + QProcess process; + process.start(ApplicationLauncherBinary, tmp); + process.waitForFinished(); + auto exitCode = process.exitCode(); + if (exitCode != 0) { + qWarning() << "Launch Application Failed:" << binary << tmp; + } + return addOneInstance(instanceRandomUUID, m_applicationPath.path()); + }, + std::move(res)); } QStringList ApplicationService::actions() const noexcept { - return m_actions; + if (m_entry.isNull()) { + qWarning() << "desktop entry is empty, isPersistence:" << m_isPersistence; + return {}; + } + + const auto &actions = m_entry->value(DesktopFileEntryKey, "Actions"); + if (!actions) { + return {}; + } + + bool ok{false}; + const auto &str = actions->toString(ok); + if (!ok) { + qWarning() << "Actions convert to String failed."; + return {}; + } + + auto actionList = str.split(";"); + actionList.removeAll(""); + + return actionList; } -QStringList &ApplicationService::actionsRef() noexcept +QString ApplicationService::id() const noexcept { - return m_actions; -} - -QString ApplicationService::iD() const noexcept -{ - return m_ID; + if (m_isPersistence) { + return m_desktopSource.m_file.desktopId(); + } + return {}; } IconMap ApplicationService::icons() const { - return m_Icons; + if (m_Icons) { + return m_Icons->icons(); + } + return {}; } IconMap &ApplicationService::iconsRef() { - return m_Icons; + return m_Icons->iconsRef(); } bool ApplicationService::isAutoStart() const noexcept @@ -75,12 +234,168 @@ QList ApplicationService::instances() const noexcept return m_Instances.keys(); } -bool ApplicationService::removeOneInstance(const QDBusObjectPath &instance) +bool ApplicationService::addOneInstance(const QString &instanceId, const QString &application, const QString &systemdUnitPath) { - return m_Instances.remove(instance) != 0; + auto service = new InstanceService{instanceId, application, systemdUnitPath}; + auto adaptor = new InstanceAdaptor(service); + QString objectPath{DDEApplicationManager1InstanceObjectPath + instanceId}; + + if (registerObjectToDBus(adaptor, objectPath, getDBusInterface())) { + m_Instances.insert(QDBusObjectPath{objectPath}, QSharedPointer{service}); + service->moveToThread(this->thread()); + adaptor->moveToThread(this->thread()); + return true; + } + + adaptor->deleteLater(); + service->deleteLater(); + return false; +} + +void ApplicationService::removeOneInstance(const QDBusObjectPath &instance) +{ + m_Instances.remove(instance); + unregisterObjectFromDBus(instance.path()); } void ApplicationService::removeAllInstance() { - m_Instances.clear(); + for (const auto &instance : m_Instances.keys()) { + removeOneInstance(instance); + } +} + +QDBusObjectPath ApplicationService::findInstance(const QString &instanceId) const +{ + for (const auto &[path, ptr] : m_Instances.asKeyValueRange()) { + if (ptr->instanceId() == instanceId) { + return path; + } + } + return {}; +} + +QString ApplicationService::userNameLookup(uid_t uid) +{ + auto pws = getpwuid(uid); + return pws->pw_name; +} + +LaunchTask ApplicationService::unescapeExec(const QString &str, const QStringList &fields) +{ + LaunchTask task; + QStringList execList = str.split(' '); + task.LaunchBin = execList.first(); + + QRegularExpression re{"%[fFuUickdDnNvm]"}; + auto matcher = re.match(str); + if (!matcher.hasMatch()) { + task.command.append(std::move(execList)); + return task; + } + + auto list = matcher.capturedTexts(); + if (list.count() > 1) { + qWarning() << "invalid exec format, all filed code will be ignored."; + for (const auto &code : list) { + execList.removeOne(code); + } + task.command.append(std::move(execList)); + return task; + } + + auto filesCode = list.first().back().toLatin1(); + auto codeStr = QString(R"(%%1)").arg(filesCode); + auto location = execList.indexOf(codeStr); + + switch (filesCode) { + case 'f': { // Defer to async job + task.command.append(std::move(execList)); + for (auto &field : fields) { + task.Resources.emplace_back(std::move(field)); + } + break; + } + case 'u': { + execList.removeAt(location); + if (fields.count() > 1) { + qDebug() << R"(fields count is greater than one, %u will only take first element.)"; + } + execList.insert(location, fields.first()); + task.command.append(execList); + break; + } + case 'F': + [[fallthrough]]; + case 'U': { + execList.removeAt(location); + auto it = execList.begin() + location; + for (const auto &field : fields) { + it = execList.insert(it, field); + } + task.command.append(std::move(execList)); + break; + } + case 'i': { + execList.removeAt(location); + auto val = m_entry->value(DesktopFileEntryKey, "Icon"); + if (!val) { + qDebug() << R"(Application Icons can't be found. %i will be ignored.)"; + task.command.append(std::move(execList)); + return task; + } + bool ok; + auto iconStr = val->toIconString(ok); + if (!ok) { + qDebug() << R"(Icons Convert to string failed. %i will be ignored.)"; + task.command.append(std::move(execList)); + return task; + } + auto it = execList.insert(location, iconStr); + execList.insert(it, "--icon"); + task.command.append(std::move(execList)); + break; + } + case 'c': { + execList.removeAt(location); + auto val = m_entry->value(DesktopFileEntryKey, "Name"); + if (!val) { + qDebug() << R"(Application Name can't be found. %c will be ignored.)"; + task.command.append(std::move(execList)); + return task; + } + bool ok; + auto NameStr = val->toLocaleString(getUserLocale(), ok); + if (!ok) { + qDebug() << R"(Name Convert to locale string failed. %c will be ignored.)"; + task.command.append(std::move(execList)); + return task; + } + execList.insert(location, NameStr); + task.command.append(std::move(execList)); + break; + } + case 'k': { // ignore all desktop file location for now. + execList.removeAt(location); + task.command.append(std::move(execList)); + break; + } + case 'd': + case 'D': + case 'n': + case 'N': + case 'v': + [[fallthrough]]; // Deprecated field codes should be removed from the command line and ignored. + case 'm': { + execList.removeAt(location); + task.command.append(std::move(execList)); + break; + } + } + + if (task.Resources.isEmpty()) { + task.Resources.emplace_back(QString{""}); // mapReduce should run once at least + } + + return task; } diff --git a/src/dbus/applicationservice.h b/src/dbus/applicationservice.h index 4b4c5d3..89ea3b9 100644 --- a/src/dbus/applicationservice.h +++ b/src/dbus/applicationservice.h @@ -11,8 +11,14 @@ #include #include #include +#include +#include +#include #include "instanceservice.h" #include "global.h" +#include "desktopentry.h" +#include "desktopicons.h" +#include "jobmanager1service.h" class ApplicationService : public QObject { @@ -26,10 +32,9 @@ public: Q_PROPERTY(QStringList Actions READ actions CONSTANT) QStringList actions() const noexcept; - QStringList &actionsRef() noexcept; - Q_PROPERTY(QString ID READ iD CONSTANT) - QString iD() const noexcept; + Q_PROPERTY(QString ID READ id CONSTANT) + QString id() const noexcept; Q_PROPERTY(IconMap Icons READ icons) IconMap icons() const; @@ -42,23 +47,93 @@ public: Q_PROPERTY(QList Instances READ instances) QList instances() const noexcept; + QDBusObjectPath findInstance(const QString &instanceId) const; + + const QString &getLauncher() const noexcept { return m_launcher; } + void setLauncher(const QString &launcher) noexcept { m_launcher = launcher; } + + bool addOneInstance(const QString &instanceId, const QString &application, const QString &systemdUnitPath = "/"); void recoverInstances(const QList) noexcept; - bool removeOneInstance(const QDBusObjectPath &instance); + void removeOneInstance(const QDBusObjectPath &instance); // TODO: remove instance when app closed void removeAllInstance(); public Q_SLOTS: QString GetActionName(const QString &identifier, const QStringList &env); - QDBusObjectPath Launch(const QString &action, const QStringList &fields, const QVariantMap &options); + QDBusObjectPath Launch(QString action, QStringList fields, QVariantMap options); private: friend class ApplicationManager1Service; - explicit ApplicationService(QString ID); - bool m_AutoStart; - QString m_ID; + template + ApplicationService(T &&source, ApplicationManager1Service *parent) + : m_parent(parent) + , m_desktopSource(std::forward(source)) + { + static_assert(std::is_same_v or std::is_same_v, "param type must be QString or DesktopFile."); + QString objectPath{DDEApplicationManager1ApplicationObjectPath}; + QTextStream sourceStream; + QFile sourceFile; + auto dbusAppid = m_desktopSource.m_file.desktopId(); + + if constexpr (std::is_same_v) { + m_applicationPath = QDBusObjectPath{objectPath + escapeToObjectPath(dbusAppid)}; + m_isPersistence = true; + sourceFile.setFileName(m_desktopSource.m_file.filePath()); + if (!sourceFile.open(QFile::ExistingOnly | QFile::ReadOnly | QFile::Text)) { + qCritical() << "desktop file can't open:" << m_desktopSource.m_file.filePath() << sourceFile.errorString(); + return; + } + sourceStream.setDevice(&sourceFile); + } else if (std::is_same_v) { + m_applicationPath = QDBusObjectPath{objectPath + QUuid::createUuid().toString(QUuid::Id128)}; + m_isPersistence = false; + sourceStream.setString(&m_desktopSource.m_temp, QTextStream::ReadOnly | QTextStream::Text); + } + m_entry.reset(new DesktopEntry()); + if (auto error = m_entry->parse(sourceStream); error != ParseError::NoError) { + if (error != ParseError::EntryKeyInvalid) { + m_entry.reset(nullptr); + return; + } + } + // TODO: icon lookup + } + + bool m_AutoStart{false}; + bool m_isPersistence; + ApplicationManager1Service *m_parent{nullptr}; QDBusObjectPath m_applicationPath; - QStringList m_actions; + QString m_launcher{ApplicationLauncherBinary}; + union DesktopSource + { + template , bool> = true> + DesktopSource(T &&source) + : m_file(std::forward(source)) + { + } + + template , bool> = true> + DesktopSource(T &&source) + : m_temp(std::forward(source)) + { + } + + void destruct(bool isPersistence) + { + if (isPersistence) { + m_file.~DesktopFile(); + } else { + m_temp.~QString(); + } + } + ~DesktopSource(){}; + QString m_temp; + DesktopFile m_file; + } m_desktopSource; + QSharedPointer m_entry{nullptr}; + QSharedPointer m_Icons{nullptr}; QMap> m_Instances; - IconMap m_Icons; + QString userNameLookup(uid_t uid); + [[nodiscard]] LaunchTask unescapeExec(const QString &str, const QStringList &fields); }; #endif diff --git a/src/dbus/instanceservice.cpp b/src/dbus/instanceservice.cpp index 77ede63..6c7ed6c 100644 --- a/src/dbus/instanceservice.cpp +++ b/src/dbus/instanceservice.cpp @@ -4,8 +4,9 @@ #include "instanceservice.h" -InstanceService::InstanceService(QString application, QString systemdUnitPath) - : m_Application(std::move(application)) +InstanceService::InstanceService(QString instanceId, QString application, QString systemdUnitPath) + : m_instanceId(std::move(instanceId)) + , m_Application(std::move(application)) , m_SystemdUnitPath(std::move(systemdUnitPath)) { } diff --git a/src/dbus/instanceservice.h b/src/dbus/instanceservice.h index 8fd6f52..7ebfed6 100644 --- a/src/dbus/instanceservice.h +++ b/src/dbus/instanceservice.h @@ -18,15 +18,18 @@ public: InstanceService &operator=(const InstanceService &) = delete; InstanceService &operator=(InstanceService &&) = delete; - Q_PROPERTY(QDBusObjectPath Application READ application CONSTANT) + Q_PROPERTY(QDBusObjectPath Application READ application) QDBusObjectPath application() const; - Q_PROPERTY(QDBusObjectPath SystemdUnitPath READ systemdUnitPath CONSTANT) + Q_PROPERTY(QDBusObjectPath SystemdUnitPath READ systemdUnitPath) QDBusObjectPath systemdUnitPath() const; + const QString &instanceId() const noexcept { return m_instanceId; } + private: friend class ApplicationService; - InstanceService(QString application, QString systemdUnitPath); + InstanceService(QString instanceId, QString application, QString systemdUnitPath); + QString m_instanceId; const QDBusObjectPath m_Application; const QDBusObjectPath m_SystemdUnitPath; }; diff --git a/src/dbus/jobmanager1service.cpp b/src/dbus/jobmanager1service.cpp index 6478ce1..3f8a2ce 100644 --- a/src/dbus/jobmanager1service.cpp +++ b/src/dbus/jobmanager1service.cpp @@ -3,7 +3,38 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include "jobmanager1service.h" +#include "jobmanager1adaptor.h" -JobManager1Service::JobManager1Service() = default; +LaunchTask::LaunchTask() +{ + qRegisterMetaType(); +} + +JobManager1Service::JobManager1Service(ApplicationManager1Service *parent) + : m_parent(parent) +{ + if (!registerObjectToDBus( + new JobManager1Adaptor{this}, DDEApplicationManager1JobManagerObjectPath, getDBusInterface())) { + std::terminate(); + } +} JobManager1Service::~JobManager1Service() = default; + +bool JobManager1Service::removeOneJob(const QDBusObjectPath &path) +{ + decltype(m_jobs)::size_type removeCount{0}; + { + QMutexLocker locker{&m_mutex}; + removeCount = m_jobs.remove(path); + } + // removeCount means m_jobs can't find value which corresponding with path + // and we shouldn't emit jobRemoved signal because this signal may already has been emit + if (removeCount == 0) { + qWarning() << "Job already has been removed: " << path.path(); + return false; + } + + unregisterObjectFromDBus(path.path()); + return true; +} diff --git a/src/dbus/jobmanager1service.h b/src/dbus/jobmanager1service.h index 85b2101..ce32aec 100644 --- a/src/dbus/jobmanager1service.h +++ b/src/dbus/jobmanager1service.h @@ -14,14 +14,30 @@ #include #include #include -#include "jobadaptor.h" #include "global.h" +#include "jobadaptor.h" + +class ApplicationManager1Service; + +struct LaunchTask +{ + LaunchTask(); + ~LaunchTask() = default; + LaunchTask(const LaunchTask &) = default; + LaunchTask(LaunchTask &&) = default; + LaunchTask &operator=(const LaunchTask &) = default; + LaunchTask &operator=(LaunchTask &&) = default; + QString LaunchBin; + QStringList command; + QVariantList Resources; +}; + +Q_DECLARE_METATYPE(LaunchTask) class JobManager1Service final : public QObject { Q_OBJECT public: - JobManager1Service(); JobManager1Service(const JobManager1Service &) = delete; JobManager1Service(JobManager1Service &&) = delete; JobManager1Service &operator=(const JobManager1Service &) = delete; @@ -29,48 +45,48 @@ public: ~JobManager1Service() override; template - void addJob(const QDBusObjectPath &source, F func, QVariantList args) + QDBusObjectPath addJob(QString source, F func, QVariantList args) { static_assert(std::is_invocable_v, "param type must be QVariant."); QString objectPath{DDEApplicationManager1JobObjectPath + QUuid::createUuid().toString(QUuid::Id128)}; - auto future = QtConcurrent::mappedReduced(args.begin(), - args.end(), - func, - qOverload(&QVariantList::append), - QVariantList{}, - QtConcurrent::ReduceOption::OrderedReduce); + QFuture future = QtConcurrent::mappedReduced(std::move(args), + func, + qOverload(&QVariantList::append), + QVariantList{}, + QtConcurrent::ReduceOption::OrderedReduce); QSharedPointer job{new JobService{future}}; + if (!registerObjectToDBus(new JobAdaptor(job.data()), objectPath, getDBusInterface())) { + qCritical() << "can't register job to dbus."; + future.cancel(); + return {}; + } + auto path = QDBusObjectPath{objectPath}; { QMutexLocker locker{&m_mutex}; - m_jobs.insert(path, job); // Insertion is always successful + m_jobs.insert(path, std::move(job)); // Insertion is always successful } - emit JobNew(path, source); - registerObjectToDbus(new JobAdaptor(job.data()), objectPath, getDBusInterface()); + emit JobNew(path, QDBusObjectPath{source}); + auto emitRemove = [this, job, path, future](QVariantList value) { - decltype(m_jobs)::size_type removeCount{0}; - { - QMutexLocker locker{&m_mutex}; - removeCount = m_jobs.remove(path); - } - // removeCount means m_jobs can't find value which corresponding with path - // and we shouldn't emit jobRemoved signal because this signal may already has been emit - if (removeCount == 0) { - qCritical() << "Job already has been removed: " << path.path(); + if (!removeOneJob(path)) { return value; } + QString result{job->status()}; for (const auto &val : future.result()) { - if (val.template canConvert()) { + if (val.canConvert()) { result = "failed"; } break; } - emit this->JobRemoved(path, result, future.result()); + emit JobRemoved(path, result, future.result()); return value; }; - future.then(emitRemove); + + future.then(this, emitRemove); + return path; } Q_SIGNALS: @@ -78,8 +94,12 @@ Q_SIGNALS: void JobRemoved(const QDBusObjectPath &job, const QString &status, const QVariantList &result); private: + bool removeOneJob(const QDBusObjectPath &path); + friend class ApplicationManager1Service; + JobManager1Service(ApplicationManager1Service *parent); QMutex m_mutex; QMap> m_jobs; + ApplicationManager1Service *m_parent{nullptr}; }; #endif diff --git a/src/desktopentry.cpp b/src/desktopentry.cpp index f758e1d..5e0094d 100644 --- a/src/desktopentry.cpp +++ b/src/desktopentry.cpp @@ -24,8 +24,10 @@ auto DesktopEntry::parserGroupHeader(const QString &str) noexcept ParseError DesktopEntry::parseEntry(const QString &str, decltype(m_entryMap)::iterator ¤tGroup) noexcept { - if (str.startsWith("#")) + if (str.startsWith("#")) { return ParseError::NoError; + } + auto splitCharIndex = str.indexOf(']'); if (splitCharIndex != -1) { for (; splitCharIndex < str.size(); ++splitCharIndex) { @@ -60,7 +62,6 @@ ParseError DesktopEntry::parseEntry(const QString &str, decltype(m_entryMap)::it if (auto locale = matcher.captured("LOCALE"); !locale.isEmpty()) { valueKey = locale; } - qDebug() << valueKey << valueStr; if (auto cur = currentGroup->find(key); cur == currentGroup->end()) { currentGroup->insert(keyStr, {{valueKey, valueStr}}); return ParseError::NoError; @@ -93,8 +94,10 @@ std::optional DesktopFile::searchDesktopFile(const QString &desktop qDebug() << "Current Application Dirs:" << XDGDataDirs; for (const auto &d : XDGDataDirs) { auto dirPath = QDir::cleanPath(d); - QDirIterator it{ - dirPath, {desktopFile}, QDir::AllEntries | QDir::NoSymLinks | QDir::NoDotAndDotDot, QDirIterator::Subdirectories}; + QDirIterator it{dirPath, + {desktopFile}, + QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Readable, + QDirIterator::Subdirectories}; if (it.hasNext()) { path = it.next(); break; @@ -107,8 +110,8 @@ std::optional DesktopFile::searchDesktopFile(const QString &desktop err = ParseError::NotFound; return std::nullopt; } - - auto components = path.split(QDir::separator()).toList(); + auto tmp = path.chopped(8); // remove ".desktop" + auto components = tmp.split(QDir::separator()).toList(); auto it = std::find(components.cbegin(), components.cend(), "applications"); if (it == components.cend()) { qWarning() << "custom location detected, Id wouldn't be generated."; @@ -117,12 +120,24 @@ std::optional DesktopFile::searchDesktopFile(const QString &desktop ++it; while (it != components.cend()) FileId += (*(it++) + "-"); - id = FileId.chopped(1); + id = FileId.chopped(1); // remove extra "-"" } err = ParseError::NoError; return DesktopFile{std::move(path), std::move(id)}; } +ParseError DesktopEntry::parse(const DesktopFile &desktopFile) noexcept +{ + auto file = QFile(desktopFile.filePath()); + if (!file.open(QFile::ExistingOnly | QFile::ReadOnly | QFile::Text)) { + qWarning() << desktopFile.filePath() << "can't open."; + return ParseError::OpenFailed; + } + + QTextStream in{&file}; + return parse(in); +} + ParseError DesktopEntry::parse(QTextStream &stream) noexcept { if (stream.atEnd()) @@ -135,6 +150,10 @@ ParseError DesktopEntry::parse(QTextStream &stream) noexcept while (!stream.atEnd()) { auto line = stream.readLine().trimmed(); + if (line.isEmpty()) { + continue; + } + if (line.startsWith("[")) { if (!line.endsWith("]")) return ParseError::GroupHeaderInvalid; @@ -150,11 +169,27 @@ ParseError DesktopEntry::parse(QTextStream &stream) noexcept return err; } -QMap DesktopEntry::group(const QString &key) const noexcept +std::optional> DesktopEntry::group(const QString &key) const noexcept { if (auto group = m_entryMap.find(key); group != m_entryMap.cend()) return *group; - return {}; + return std::nullopt; +} + +std::optional DesktopEntry::value(const QString &groupKey, const QString &valueKey) const noexcept +{ + const auto &destGroup = group(groupKey); + if (!destGroup) { + qWarning() << "group " << groupKey << " can't be found."; + return std::nullopt; + } + + auto it = destGroup->find(valueKey); + if (it == destGroup->cend()) { + qWarning() << "value " << valueKey << " can't be found."; + return std::nullopt; + } + return *it; } QString DesktopEntry::Value::unescape(const QString &str) const noexcept diff --git a/src/desktopicons.cpp b/src/desktopicons.cpp new file mode 100644 index 0000000..e69de29 diff --git a/src/include/cgroupsidentifier.h b/src/include/cgroupsidentifier.h new file mode 100644 index 0000000..0e5b8fa --- /dev/null +++ b/src/include/cgroupsidentifier.h @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef CGROUPSIDENTIFIER_H +#define CGROUPSIDENTIFIER_H + +#include "identifier.h" + +class CGroupsIdentifier : public Identifier +{ +public: + IdentifyRet Identify(pid_t pid) override; + +private: + [[nodiscard]] QString parseCGroupsPath(QString path) const noexcept; +}; + +#endif diff --git a/src/include/desktopentry.h b/src/include/desktopentry.h index 011afcc..f74b306 100644 --- a/src/include/desktopentry.h +++ b/src/include/desktopentry.h @@ -13,6 +13,29 @@ constexpr static auto defaultKeyStr = "default"; enum class ParseError { NoError, NotFound, MismatchedFile, InvalidLocation, OpenFailed, GroupHeaderInvalid, EntryKeyInvalid }; +struct DesktopFile +{ + DesktopFile(const DesktopFile &) = default; + DesktopFile(DesktopFile &&) = default; + DesktopFile &operator=(const DesktopFile &) = default; + DesktopFile &operator=(DesktopFile &&) = default; + ~DesktopFile() = default; + + const QString &filePath() const { return m_filePath; } + const QString &desktopId() const { return m_desktopId; } + + static std::optional searchDesktopFile(const QString &desktopFilePath, ParseError &err) noexcept; + +private: + DesktopFile(QString &&path, QString &&fileId) + : m_filePath(std::move(path)) + , m_desktopId(std::move(fileId)) + { + } + QString m_filePath{""}; + QString m_desktopId{""}; +}; + class DesktopEntry { public: @@ -33,39 +56,17 @@ public: DesktopEntry() = default; ~DesktopEntry() = default; + [[nodiscard]] ParseError parse(const DesktopFile &file) noexcept; [[nodiscard]] ParseError parse(QTextStream &stream) noexcept; - [[nodiscard]] QMap group(const QString &key) const noexcept; + [[nodiscard]] std::optional> group(const QString &key) const noexcept; + [[nodiscard]] std::optional value(const QString &key, const QString &valueKey) const noexcept; private: QMap> m_entryMap; - auto parserGroupHeader(const QString &str) noexcept; ParseError parseEntry(const QString &str, decltype(m_entryMap)::iterator ¤tGroup) noexcept; }; -struct DesktopFile -{ - DesktopFile(const DesktopFile &) = default; - DesktopFile(DesktopFile &&) = default; - DesktopFile &operator=(const DesktopFile &) = default; - DesktopFile &operator=(DesktopFile &&) = default; - ~DesktopFile() = default; - - const QString &filePath() const { return m_filePath; } - const QString &desktopId() const { return m_desktopId; } - - static std::optional searchDesktopFile(const QString &desktopFilePath, ParseError &err) noexcept; - -private: - DesktopFile(QString &&path, QString &&fileId) - : m_filePath(std::move(path)) - , m_desktopId(std::move(fileId)) - { - } - QString m_filePath; - QString m_desktopId; -}; - QDebug operator<<(QDebug debug, const DesktopEntry::Value &v); QDebug operator<<(QDebug debug, const ParseError &v); diff --git a/src/include/desktopicons.h b/src/include/desktopicons.h new file mode 100644 index 0000000..c8ec13f --- /dev/null +++ b/src/include/desktopicons.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef DESKTOPICONS_H +#define DESKTOPICONS_H + +#include "global.h" + +class DesktopIcons +{ +public: + DesktopIcons() = default; + const IconMap &icons() const noexcept { return m_icons; } + IconMap &iconsRef() noexcept { return m_icons; } + +private: + IconMap m_icons; +}; + +#endif diff --git a/src/include/global.h b/src/include/global.h index 41804bb..a678e77 100644 --- a/src/include/global.h +++ b/src/include/global.h @@ -13,14 +13,27 @@ #include #include #include +#include +#include +#include +#include +#include using IconMap = QMap>>; constexpr auto DDEApplicationManager1ServiceName = u8"org.deepin.dde.ApplicationManager1"; constexpr auto DDEApplicationManager1ObjectPath = u8"/org/deepin/dde/ApplicationManager1"; constexpr auto DDEApplicationManager1ApplicationObjectPath = u8"/org/deepin/dde/ApplicationManager1/Application/"; +constexpr auto DDEApplicationManager1InstanceObjectPath = u8"/org/deepin/dde/ApplicationManager1/Instance/"; constexpr auto DDEApplicationManager1JobManagerObjectPath = u8"/org/deepin/dde/ApplicationManager1/JobManager1"; constexpr auto DDEApplicationManager1JobObjectPath = u8"/org/deepin/dde/ApplicationManager1/JobManager1/Job/"; +constexpr auto DesktopFileEntryKey = u8"Desktop Entry"; +constexpr auto DesktopFileActionKey = u8"Desktop Action "; +constexpr auto ApplicationLauncherBinary = + u8"/home/heyuming/workspace/dde-application-manager/build/plugin/appLauncher/am-launcher"; +constexpr auto ApplicationManagerDBusName = u8"deepin_application_manager_bus"; + +enum class DBusType { Session = QDBusConnection::SessionBus, System = QDBusConnection::SystemBus, Custom }; class ApplicationManager1DBus { @@ -30,15 +43,52 @@ public: ApplicationManager1DBus &operator=(const ApplicationManager1DBus &) = delete; ApplicationManager1DBus &operator=(ApplicationManager1DBus &&) = delete; const QString &BusAddress() { return m_busAddress; } - void setBusAddress(const QString &address) { m_busAddress = address; } - QDBusConnection &CustomBus() + void init(DBusType type, const QString &busAddress = "") { - static auto con = QDBusConnection::connectToBus(m_busAddress, "org.deepin.dde.ApplicationManager1"); - if (!con.isConnected()) { - qWarning() << con.lastError(); - std::terminate(); + if (m_initFlag) { + return; } - return con; + + m_busAddress = busAddress; + m_type = type; + m_initFlag = true; + return; + } + + QDBusConnection &globalBus() + { + if (m_connection.has_value()) { + return m_connection.value(); + } + + if (!m_initFlag) { + qFatal() << "invoke init at first."; + } + + switch (m_type) { + case DBusType::Session: + [[fallthrough]]; + case DBusType::System: { + m_connection.emplace( + QDBusConnection::connectToBus(static_cast(m_type), ApplicationManagerDBusName)); + if (!m_connection->isConnected()) { + qFatal() << m_connection->lastError(); + } + return m_connection.value(); + } + case DBusType::Custom: { + if (m_busAddress.isEmpty()) { + qFatal() << "connect to custom dbus must init this object by custom dbus address"; + } + m_connection.emplace( + QDBusConnection::connectToBus(static_cast(m_type), ApplicationManagerDBusName)); + if (!m_connection->isConnected()) { + qFatal() << m_connection->lastError(); + } + return m_connection.value(); + } + } + Q_UNREACHABLE(); } static ApplicationManager1DBus &instance() { @@ -49,10 +99,14 @@ public: private: ApplicationManager1DBus() = default; ~ApplicationManager1DBus() = default; + bool m_initFlag; + DBusType m_type; QString m_busAddress; + std::optional m_connection{std::nullopt}; }; -bool registerObjectToDbus(QObject *o, const QString &path, const QString &interface); +bool registerObjectToDBus(QObject *o, const QString &path, const QString &interface); +void unregisterObjectFromDBus(const QString &path); template QString getDBusInterface() @@ -62,7 +116,71 @@ QString getDBusInterface() if (auto infoIndex = infoObject->indexOfClassInfo("D-Bus Interface"); infoIndex != -1) { return infoObject->classInfo(infoIndex).value(); } + qWarning() << "no interface found."; return {}; } +inline uid_t getCurrentUID() +{ + return getuid(); // current use linux getuid +} + +inline QLocale getUserLocale() +{ + return QLocale::system(); // current use env +} + +inline QString unescapeString(const QString &input) +{ + QRegularExpression regex("\\\\x([0-9A-Fa-f]{2})"); + QRegularExpressionMatchIterator it = regex.globalMatch(input); + QString output{input}; + + while (it.hasNext()) { + QRegularExpressionMatch match = it.next(); + bool ok; + // Get the hexadecimal value from the match and convert it to a number. + int asciiValue = match.captured(1).toInt(&ok, 16); + if (ok) { + // Convert the ASCII value to a QChar and perform the replacement. + output.replace(match.capturedStart(), match.capturedLength(), QChar(asciiValue)); + } + } + + return output; +} + +inline QString escapeToObjectPath(const QString &str) +{ + if (str.isEmpty()) { + return "_"; + } + + auto ret = str; + QRegularExpression re{R"([^a-zA-Z0-9])"}; + auto matcher = re.globalMatch(ret); + while (matcher.hasNext()) { + auto replaceList = matcher.next().capturedTexts(); + replaceList.removeDuplicates(); + for (const auto &c : replaceList) { + auto hexStr = QString::number(static_cast(c.front().toLatin1()), 16); + ret.replace(c, QString{R"(_%1)"}.arg(hexStr)); + } + } + return ret; +} + +inline QString unescapeFromObjectPath(const QString &str) +{ + auto ret = str; + for (qsizetype i = 0; i < str.size(); ++i) { + if (str[i] == '_' and i + 2 < str.size()) { + auto hexStr = str.sliced(i + 1, 2); + ret.replace(hexStr, QChar::fromLatin1(hexStr.toUInt(nullptr, 16))); + i += 2; + } + } + return ret; +} + #endif diff --git a/src/include/identifier.h b/src/include/identifier.h new file mode 100644 index 0000000..5d26e65 --- /dev/null +++ b/src/include/identifier.h @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef IDENTIFIER_H +#define IDENTIFIER_H + +#include +#include + +struct IdentifyRet +{ + QString ApplicationId; + QString InstanceId; +}; + +class Identifier +{ +public: + virtual ~Identifier() = default; + virtual IdentifyRet Identify(pid_t pid) = 0; +}; + +#endif diff --git a/src/main.cpp b/src/main.cpp index 905869d..1ac8e8a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,59 @@ -int main() +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "global.h" +#include +#include +#include +#include "applicationmanager1service.h" +#include "cgroupsidentifier.h" +#include "global.h" + +static void registerComplexDbusType() { - return 0; + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>>(); + qDBusRegisterMetaType(); +} + +int main(int argc, char *argv[]) +{ + QCoreApplication app{argc, argv}; + auto &bus = ApplicationManager1DBus::instance(); + bus.init(DBusType::Session); + auto &AMBus = bus.globalBus(); + + registerComplexDbusType(); + ApplicationManager1Service AMService{std::make_unique(), AMBus}; + QList fileList{}; + auto pathEnv = qgetenv("XDG_DATA_DIRS"); + if (pathEnv.isEmpty()) { + qFatal() << "environment variable $XDG_DATA_DIRS is empty."; + } + auto desktopFileDirs = pathEnv.split(':'); + + for (const auto &dir : desktopFileDirs) { + auto dirPath = QDir{QDir::cleanPath(dir) + "/applications"}; + if (!dirPath.exists()) { + continue; + } + QDirIterator it{dirPath.absolutePath(), + {"*.desktop"}, + QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Readable, + QDirIterator::Subdirectories}; + while (it.hasNext()) { + auto file = it.next(); + ParseError err; + auto ret = DesktopFile::searchDesktopFile(file, err); + if (!ret.has_value()) { + continue; + } + if (!AMService.addApplication(std::move(ret).value())) { + break; + } + } + } + + return app.exec(); } diff --git a/src/utils.cpp b/src/utils.cpp index b954c28..a208258 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,11 +1,22 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + #include "global.h" -bool registerObjectToDbus(QObject *o, const QString &path, const QString &interface) +bool registerObjectToDBus(QObject *o, const QString &path, const QString &interface) { - auto &con = ApplicationManager1DBus::instance().CustomBus(); - if (!con.registerObject(path, interface, o)) { - qCritical() << "register object failed:" << path << interface << con.lastError(); - return false; + auto &con = ApplicationManager1DBus::instance().globalBus(); + if (!con.registerObject(path, interface, o, QDBusConnection::RegisterOption::ExportAllContents)) { + qFatal() << "register object failed:" << path << interface << con.lastError(); + } else { + qInfo() << "register object:" << path << interface; } return true; } + +void unregisterObjectFromDBus(const QString &path) +{ + auto &con = ApplicationManager1DBus::instance().globalBus(); + con.unregisterObject(path); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0e76aac..39db387 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,6 +23,7 @@ target_link_libraries(${BIN_NAME} PRIVATE ) target_compile_options(${BIN_NAME} PRIVATE + -fno-access-control -fsanitize=undefined -fsanitize=address ) diff --git a/tests/ut_desktopentry.cpp b/tests/ut_desktopentry.cpp index b942f34..ff3f697 100644 --- a/tests/ut_desktopentry.cpp +++ b/tests/ut_desktopentry.cpp @@ -57,10 +57,10 @@ TEST_F(TestDesktopEntry, prase) ASSERT_EQ(err, ParseError::NoError); auto group = entry.group("Desktop Entry"); - ASSERT_FALSE(group.isEmpty()); + ASSERT_TRUE(group); - auto name = group.constFind("Name"); - ASSERT_NE(name, group.cend()); + auto name = group->constFind("Name"); + ASSERT_NE(name, group->cend()); bool ok; name->toBoolean(ok); diff --git a/tests/ut_jobmanager.cpp b/tests/ut_jobmanager.cpp index 47cc91c..d12cf77 100644 --- a/tests/ut_jobmanager.cpp +++ b/tests/ut_jobmanager.cpp @@ -3,16 +3,18 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #include "jobmanager1service.h" -#include "jobservice.h" #include class TestJobManager : public testing::Test { public: - JobManager1Service &service() { return m_jobManager; } + static void SetUpTestCase() { m_jobManager = new JobManager1Service(nullptr); } + + static void TearDownTestCase() { delete m_jobManager; } + JobManager1Service &service() { return *m_jobManager; } private: - JobManager1Service m_jobManager; + static inline JobManager1Service *m_jobManager{nullptr}; }; TEST_F(TestJobManager, addJob) @@ -35,7 +37,7 @@ TEST_F(TestJobManager, addJob) }); manager.addJob( - sourcePath, + sourcePath.path(), [](auto value) -> QVariant { EXPECT_TRUE(value.toString() == "Application"); return QVariant::fromValue(true); diff --git a/tests/utils.cpp b/tests/utils.cpp index 3b7a5ec..1356037 100644 --- a/tests/utils.cpp +++ b/tests/utils.cpp @@ -1,6 +1,15 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + #include "global.h" -bool registerObjectToDbus(QObject *, const QString &, const QString &) +bool registerObjectToDBus(QObject *, const QString &, const QString &) { return true; } + +void unregisterObjectFromDBus(const QString &) +{ + return; +}