Compare commits

...

8 Commits

Author SHA1 Message Date
guanzi008 3f1e1fce25
Merge bf24e8397e into 41a2650979 2025-11-17 10:44:17 +08:00
ComixHe 41a2650979 feat: Adding process-aware fileLock implementation
build / ubuntu_24.04 (push) Has been cancelled Details
build / ubuntu_22.04 (push) Has been cancelled Details
coverage / codecov (push) Has been cancelled Details
Signed-off-by: ComixHe <ComixHe1895@outlook.com>
2025-11-13 18:09:32 +08:00
reddevillg c31b6d74cf feat: Disable backtrace by default
The LINYAPS_BACKTRACE environment variable now controls the verbosity of
error messages, including file paths, line numbers, and backtrace details.

Signed-off-by: reddevillg <reddevillg@gmail.com>
2025-11-13 14:14:56 +08:00
dengbo 45f13e1813 fix: the application was abnormally removed due to an error in executing hooks
1. Hook scripts should be executed before exporting and generating caches; otherwise, execution failures may result in the application being removed.
2. Enhances logging for better visibility into hook execution
failures and aligns hook handling across installation, upgrade,
and dependency management workflows.
2025-11-13 14:14:11 +08:00
guanzi008 bf24e8397e feat: Implement loading of extensions from config files....
调整了一下
2025-10-30 17:35:35 +08:00
guanzi008 ef0a7539ca feat: Implement loading of extensions from config files....
添加了严格沙箱模式
2025-10-21 20:06:24 +08:00
guanzi008 c40805bf34 feat: Implement loading of extensions from config files.... 2025-10-21 18:16:17 +08:00
guanzi008 3213d097af feat: Implement loading of extensions from config files 2025-10-21 08:43:47 +08:00
31 changed files with 3125 additions and 125 deletions

View File

@ -105,6 +105,118 @@
}
}
},
"LlCliFilesystemEntry": {
"title": "LlCliFilesystemEntry",
"description": "filesystem entry used by ll-cli configuration",
"type": "object",
"required": [
"host",
"target"
],
"properties": {
"host": {
"type": "string",
"description": "source path on host"
},
"target": {
"type": "string",
"description": "mount target path inside container"
},
"mode": {
"type": "string",
"description": "mount mode, either ro or rw"
},
"persist": {
"type": "boolean",
"description": "whether this mount should persist in sandbox storage"
}
}
},
"LlCliCommandConfig": {
"title": "LlCliCommandConfig",
"description": "per-command overrides for ll-cli configuration",
"type": "object",
"properties": {
"env": {
"type": "object",
"description": "environment variables applied to command",
"additionalProperties": {
"type": "string"
}
},
"filesystem": {
"type": "array",
"description": "filesystem entries applied to command",
"items": {
"$ref": "#/$defs/LlCliFilesystemEntry"
}
},
"args_prefix": {
"type": "array",
"description": "arguments prepended before command",
"items": {
"type": "string"
}
},
"args_suffix": {
"type": "array",
"description": "arguments appended after command",
"items": {
"type": "string"
}
},
"entrypoint": {
"type": "string",
"description": "override entrypoint for command"
},
"cwd": {
"type": "string",
"description": "working directory for command"
}
}
},
"LlCliConfig": {
"title": "LlCliConfig",
"description": "default ll-cli configuration provided by package",
"type": "object",
"properties": {
"filesystem_allow_only": {
"type": "array",
"description": "explicit mount allowlist; when present only these mounts are permitted",
"items": {
"$ref": "#/$defs/LlCliFilesystemEntry"
}
},
"filesystem": {
"type": "array",
"description": "additional filesystem mounts appended to defaults",
"items": {
"$ref": "#/$defs/LlCliFilesystemEntry"
}
},
"env": {
"type": "object",
"description": "default environment variables",
"additionalProperties": {
"type": "string"
}
},
"commands": {
"type": "object",
"description": "command specific overrides",
"additionalProperties": {
"$ref": "#/$defs/LlCliCommandConfig"
}
},
"extensions": {
"type": "array",
"description": "default extensions applied when launching app",
"items": {
"type": "string"
}
}
}
},
"ContainerProcessStateInfo": {
"description": "information about process which in container that start by ll-cli, all content\nwill write to /run/linglong/UID/PID\n",
"type": "object",
@ -379,6 +491,10 @@
"permissions": {
"$ref": "#/$defs/ApplicationConfigurationPermissions"
},
"cli_config": {
"$ref": "#/$defs/LlCliConfig",
"description": "default ll-cli configuration delivered with package"
},
"runtime": {
"type": "string",
"description": "used runtime of package"
@ -773,6 +889,10 @@
"$ref": "#/$defs/ExtensionDefine"
}
},
"cli_config": {
"$ref": "#/$defs/LlCliConfig",
"description": "default ll-cli configuration provided by package"
},
"ext_impl": {
"$ref": "#/$defs/ExtensionImpl"
}
@ -1418,6 +1538,15 @@
"ApplicationConfigurationPermissions": {
"$ref": "#/$defs/ApplicationConfigurationPermissions"
},
"LlCliFilesystemEntry": {
"$ref": "#/$defs/LlCliFilesystemEntry"
},
"LlCliCommandConfig": {
"$ref": "#/$defs/LlCliCommandConfig"
},
"LlCliConfig": {
"$ref": "#/$defs/LlCliConfig"
},
"ContainerProcessStateInfo": {
"$ref": "#/$defs/ContainerProcessStateInfo"
},

View File

@ -95,6 +95,87 @@ $defs:
destination:
type: string
description: mount source file to the another position of container
LlCliFilesystemEntry:
title: LlCliFilesystemEntry
description: filesystem entry used by ll-cli configuration
type: object
required:
- host
- target
properties:
host:
type: string
description: source path on host
target:
type: string
description: mount target path inside container
mode:
type: string
description: mount mode, either ro or rw
persist:
type: boolean
description: whether this mount should persist in sandbox storage
LlCliCommandConfig:
title: LlCliCommandConfig
description: per-command overrides for ll-cli configuration
type: object
properties:
env:
type: object
description: environment variables applied to command
additionalProperties:
type: string
filesystem:
type: array
description: filesystem entries applied to command
items:
$ref: '#/$defs/LlCliFilesystemEntry'
args_prefix:
type: array
description: arguments prepended before command
items:
type: string
args_suffix:
type: array
description: arguments appended after command
items:
type: string
entrypoint:
type: string
description: override entrypoint for command
cwd:
type: string
description: working directory for command
LlCliConfig:
title: LlCliConfig
description: default ll-cli configuration provided by package
type: object
properties:
filesystem_allow_only:
type: array
description: explicit mount allowlist; when present only these mounts are permitted
items:
$ref: '#/$defs/LlCliFilesystemEntry'
filesystem:
type: array
description: additional filesystem mounts appended to defaults
items:
$ref: '#/$defs/LlCliFilesystemEntry'
env:
type: object
description: default environment variables
additionalProperties:
type: string
commands:
type: object
description: command specific overrides
additionalProperties:
$ref: '#/$defs/LlCliCommandConfig'
extensions:
type: array
description: default extensions applied when launching app
items:
type: string
ContainerProcessStateInfo:
description: |
information about process which in container that start by ll-cli, all content
@ -312,6 +393,9 @@ $defs:
type: string
permissions:
$ref: '#/$defs/ApplicationConfigurationPermissions'
cli_config:
$ref: '#/$defs/LlCliConfig'
description: default ll-cli configuration delivered with package
runtime:
type: string
description: used runtime of package
@ -604,6 +688,9 @@ $defs:
description: description of extension
items:
$ref: '#/$defs/ExtensionDefine'
cli_config:
$ref: '#/$defs/LlCliConfig'
description: default ll-cli configuration provided by package
ext_impl:
$ref: '#/$defs/ExtensionImpl'
PackageInfo:

View File

@ -5,6 +5,8 @@
*/
#include "configure.h"
#include "linglong/api/dbus/v1/dbus_peer.h"
#include "linglong/api/types/v1/Generators.hpp"
#include "linglong/api/types/v1/PackageInfoV2.hpp"
#include "linglong/cli/cli.h"
#include "linglong/cli/cli_printer.h"
#include "linglong/cli/dbus_notifier.h"
@ -33,10 +35,20 @@
#include <memory>
#include <thread>
#include <cstdio>
#include <string>
#include <fcntl.h>
#include <unistd.h>
#include <wordexp.h>
#include <nlohmann/json.hpp>
#include <filesystem>
#include <fstream>
#include <optional>
#include <sstream>
#include <tuple>
#include <unordered_set>
using namespace linglong::utils::error;
using namespace linglong::package;
using namespace linglong::cli;
@ -631,22 +643,887 @@ linglong::utils::error::Result<linglong::repo::OSTreeRepo *> initOSTreeRepo()
return repo;
}
// ===== begin: ll-cli config helpers =====
using json = nlohmann::json;
static std::string configUsageLines()
{
return std::string{
_(" ll-cli config set-extensions [--global | <appid> | --base <baseid>] ext1,ext2\n"
" ll-cli config add-extensions [--global | <appid> | --base <baseid>] ext1,ext2\n"
" ll-cli config set-env [--global | <appid> | --base <baseid>] KEY=VAL [KEY=VAL ...]\n"
" ll-cli config unset-env [--global | <appid> | --base <baseid>] KEY [KEY ...]\n"
" ll-cli config add-fs [--global | <appid> | --base <baseid>] --host PATH --target PATH [--mode "
"ro|rw] [--persist]\n"
" ll-cli config rm-fs [--global | <appid> | --base <baseid>] (--target PATH | --index N)\n"
" ll-cli config add-fs-allow [--global | <appid> | --base <baseid>] --host PATH --target PATH [--mode "
"ro|rw] [--persist]\n"
" ll-cli config rm-fs-allow [--global | <appid> | --base <baseid>] (--target PATH | --index N)\n"
" ll-cli config clear-fs-allow [--global | <appid> | --base <baseid>]\n"
" ll-cli config set-command [--global | <appid> | --base <baseid>] <cmd> [--entrypoint P] [--cwd D] "
"[--args-prefix \"...\"] [--args-suffix \"...\"] [KEY=VAL ...]\n"
" ll-cli config unset-command [--global | <appid> | --base <baseid>] <cmd>\n") };
}
static std::string configShortHelp()
{
return std::string{ _("Configuration commands:\n"
" config Manage ll-cli configuration (see `ll-cli config --help`)\n") };
}
static std::string configFooterMessage()
{
return std::string{ _("If you found any problems during use,\n"
"You can report bugs to the linyaps team under this project: "
"https://github.com/OpenAtom-Linyaps/linyaps/issues") };
}
static void printConfigUsage(FILE *stream = stderr)
{
auto usageLines = configUsageLines();
std::fprintf(stream, "%s\n%s", _("Usage:"), usageLines.c_str());
}
enum class Scope { Global, App, Base };
static std::filesystem::path getBaseConfigDir()
{
const char *xdg = ::getenv("XDG_CONFIG_HOME");
if (xdg && xdg[0]) {
return std::filesystem::path(xdg) / "linglong";
}
const char *home = ::getenv("HOME");
if (home && home[0]) {
return std::filesystem::path(home) / ".config" / "linglong";
}
return {};
}
static std::filesystem::path getSystemConfigDir()
{
return std::filesystem::path(LINGLONG_DATA_DIR) / "config";
}
static std::filesystem::path buildConfigPath(const std::filesystem::path &base,
Scope scope,
const std::string &appId,
const std::string &baseId)
{
if (base.empty()) {
return {};
}
switch (scope) {
case Scope::Global:
return base / "config.json";
case Scope::App:
return base / "apps" / appId / "config.json";
case Scope::Base:
return base / "base" / baseId / "config.json";
}
return {};
}
static std::filesystem::path getConfigPath(Scope scope,
const std::string &appId,
const std::string &baseId)
{
return buildConfigPath(getBaseConfigDir(), scope, appId, baseId);
}
static std::vector<std::filesystem::path> getConfigSearchPaths(Scope scope,
const std::string &appId,
const std::string &baseId)
{
std::vector<std::filesystem::path> paths;
std::unordered_set<std::string> seen;
auto addPath = [&](const std::filesystem::path &candidate) {
if (candidate.empty()) {
return;
}
auto normalized = candidate.lexically_normal();
auto key = normalized.string();
if (!key.empty() && seen.insert(key).second) {
paths.emplace_back(std::move(normalized));
}
};
addPath(buildConfigPath(getBaseConfigDir(), scope, appId, baseId));
addPath(buildConfigPath(getSystemConfigDir(), scope, appId, baseId));
return paths;
}
static bool ensureParentDir(const std::filesystem::path &p)
{
std::error_code ec;
auto parent = p.parent_path();
if (parent.empty()) {
return true;
}
return std::filesystem::create_directories(parent, ec) || std::filesystem::exists(parent);
}
static std::optional<json> readJsonIfExists(const std::filesystem::path &p, bool *existed = nullptr)
{
try {
std::error_code ec;
if (!std::filesystem::exists(p, ec)) {
if (existed) {
*existed = false;
}
return json::object();
}
std::ifstream in(p);
if (!in.is_open()) {
return std::nullopt;
}
if (existed) {
*existed = true;
}
json j;
in >> j;
return j.is_null() ? json::object() : j;
} catch (...) {
return std::nullopt;
}
}
static bool writeJsonAtomic(const std::filesystem::path &p, const json &j)
{
try {
if (!ensureParentDir(p)) {
return false;
}
auto tmp = p;
tmp += ".tmp";
{
std::ofstream out(tmp);
if (!out.is_open()) {
return false;
}
out << j.dump(2) << "\n";
}
std::error_code ec;
std::filesystem::rename(tmp, p, ec);
if (ec) {
std::filesystem::remove(p, ec);
std::filesystem::rename(tmp, p, ec);
}
return !ec;
} catch (...) {
return false;
}
}
static std::vector<std::string> splitCsv(const std::string &s)
{
std::vector<std::string> out;
std::stringstream ss(s);
std::string tok;
while (std::getline(ss, tok, ',')) {
if (!tok.empty()) {
out.push_back(tok);
}
}
return out;
}
static std::string trim(const std::string &s)
{
const char *ws = " \t\r\n";
size_t b = s.find_first_not_of(ws);
if (b == std::string::npos) {
return "";
}
size_t e = s.find_last_not_of(ws);
return s.substr(b, e - b + 1);
}
static void jsonSetExtensions(json &root, const std::vector<std::string> &exts, bool overwrite)
{
auto &arr = root["extensions"];
if (!arr.is_array() || overwrite) {
arr = json::array();
}
std::unordered_set<std::string> exist;
if (arr.is_array()) {
for (auto &e : arr) {
if (e.is_string()) {
exist.insert(e.get<std::string>());
}
}
}
for (auto &s : exts) {
if (!s.empty() && !exist.count(s)) {
arr.push_back(s);
exist.insert(s);
}
}
}
static void jsonSetEnv(json &root, const std::vector<std::string> &kvs)
{
auto &obj = root["env"];
if (!obj.is_object()) {
obj = json::object();
}
for (auto &kv : kvs) {
auto pos = kv.find('=');
if (pos == std::string::npos) {
continue;
}
auto k = trim(kv.substr(0, pos));
auto v = kv.substr(pos + 1);
if (k.empty()) {
continue;
}
obj[k] = v;
}
}
static void jsonUnsetEnv(json &root, const std::vector<std::string> &keys)
{
if (!root.contains("env") || !root["env"].is_object()) {
return;
}
for (auto &k : keys) {
root["env"].erase(k);
}
}
struct FsArg {
std::string host, target, mode;
bool persist = false;
};
static void jsonAddFsTo(json &root, const FsArg &fs, const char *field)
{
auto &arr = root[field];
if (!arr.is_array()) {
arr = json::array();
}
for (auto &e : arr) {
if (!e.is_object()) {
continue;
}
if (e.value("host", "") == fs.host && e.value("target", "") == fs.target) {
return;
}
}
json o = json::object();
o["host"] = fs.host;
o["target"] = fs.target;
o["mode"] = (fs.mode == "rw" ? "rw" : "ro");
if (fs.persist) {
o["persist"] = true;
}
arr.push_back(std::move(o));
}
static void jsonAddFs(json &root, const FsArg &fs)
{
jsonAddFsTo(root, fs, "filesystem");
}
static void jsonAddFsAllow(json &root, const FsArg &fs)
{
jsonAddFsTo(root, fs, "filesystem_allow_only");
}
static bool jsonRmFsByTargetFrom(json &root, const std::string &target, const char *field)
{
if (!root.contains(field) || !root[field].is_array()) {
return false;
}
auto &arr = root[field];
auto old = arr.size();
arr.erase(std::remove_if(arr.begin(), arr.end(), [&](const json &e) {
return e.is_object() && e.value("target", "") == target;
}),
arr.end());
return arr.size() != old;
}
static bool jsonRmFsByTarget(json &root, const std::string &target)
{
return jsonRmFsByTargetFrom(root, target, "filesystem");
}
static bool jsonRmFsAllowByTarget(json &root, const std::string &target)
{
return jsonRmFsByTargetFrom(root, target, "filesystem_allow_only");
}
static bool jsonRmFsByIndexFrom(json &root, size_t idx, const char *field)
{
if (!root.contains(field) || !root[field].is_array()) {
return false;
}
auto &arr = root[field];
if (idx >= arr.size()) {
return false;
}
arr.erase(arr.begin() + idx);
return true;
}
static bool jsonRmFsByIndex(json &root, size_t idx)
{
return jsonRmFsByIndexFrom(root, idx, "filesystem");
}
static bool jsonRmFsAllowByIndex(json &root, size_t idx)
{
return jsonRmFsByIndexFrom(root, idx, "filesystem_allow_only");
}
static void jsonClearFsAllow(json &root)
{
root["filesystem_allow_only"] = json::array();
}
struct CmdSetArg {
std::string cmd;
std::optional<std::string> entrypoint;
std::optional<std::string> cwd;
std::vector<std::string> argsPrefix;
std::vector<std::string> argsSuffix;
std::vector<std::string> envKVs;
};
static void jsonSetCommand(json &root, const CmdSetArg &a)
{
if (a.cmd.empty()) {
return;
}
auto &cmds = root["commands"];
if (!cmds.is_object()) {
cmds = json::object();
}
auto &node = cmds[a.cmd];
if (!node.is_object()) {
node = json::object();
}
if (a.entrypoint) {
node["entrypoint"] = *a.entrypoint;
}
if (a.cwd) {
node["cwd"] = *a.cwd;
}
if (!a.argsPrefix.empty()) {
auto &arr = node["args_prefix"];
arr = json::array();
for (auto &s : a.argsPrefix) {
arr.push_back(s);
}
}
if (!a.argsSuffix.empty()) {
auto &arr = node["args_suffix"];
arr = json::array();
for (auto &s : a.argsSuffix) {
arr.push_back(s);
}
}
if (!a.envKVs.empty()) {
auto &env = node["env"];
if (!env.is_object()) {
env = json::object();
}
for (auto &kv : a.envKVs) {
auto pos = kv.find('=');
if (pos == std::string::npos) {
continue;
}
auto k = trim(kv.substr(0, pos));
auto v = kv.substr(pos + 1);
if (!k.empty()) {
env[k] = v;
}
}
}
}
static void jsonUnsetCommand(json &root, const std::string &cmd)
{
if (!root.contains("commands") || !root["commands"].is_object()) {
return;
}
root["commands"].erase(cmd);
}
// ===== end: ll-cli config helpers =====
class ConfigAwareFormatter : public CLI::Formatter {
public:
ConfigAwareFormatter(std::string shortSection, std::string fullSection, std::string footerMessage)
: shortSection_(std::move(shortSection))
, fullSection_(std::move(fullSection))
, footerMessage_(std::move(footerMessage))
{}
std::string make_help(const CLI::App *app, std::string name, CLI::AppFormatMode mode) const override
{
std::string result = Formatter::make_help(app, std::move(name), mode);
const std::string &section = (mode == CLI::AppFormatMode::All) ? fullSection_ : shortSection_;
if (!section.empty()) {
if (!result.empty() && result.back() != '\n') {
result.push_back('\n');
}
result += section;
if (!section.empty() && result.back() != '\n') {
result.push_back('\n');
}
}
if (!footerMessage_.empty()) {
if (!result.empty() && result.back() != '\n') {
result.push_back('\n');
}
result += footerMessage_;
if (result.back() != '\n') {
result.push_back('\n');
}
}
return result;
}
private:
std::string shortSection_;
std::string fullSection_;
std::string footerMessage_;
};
int runCliApplication(int argc, char **mainArgv)
{
// ===== begin: "ll-cli config ..." dispatcher =====
{
if (argc >= 2 && std::string(mainArgv[1]) == "config") {
if (argc < 3) {
printConfigUsage();
return 1;
}
std::string sub = mainArgv[2];
auto parseScope = [&](int start) -> std::tuple<Scope, std::string, std::string, int> {
if (start >= argc) {
return { Scope::Global, "", "", start };
}
std::string t = mainArgv[start];
if (t == "--global") {
return { Scope::Global, "", "", start + 1 };
}
if (t == "--base") {
if (start + 1 >= argc) {
fprintf(stderr, "--base requires <baseid>\n");
return { Scope::Global, "", "", argc };
}
return { Scope::Base, "", mainArgv[start + 1], start + 2 };
}
return { Scope::App, t, "", start + 1 };
};
auto loadCliConfigFromPackage = [&](Scope scope,
const std::string &appId,
const std::string &baseId)
-> std::optional<json> {
if (scope == Scope::Global) {
return std::nullopt;
}
auto repoResult = initOSTreeRepo();
if (!repoResult) {
qWarning() << "load cli config from package failed:" << repoResult.error();
return std::nullopt;
}
auto *repoPtr = *repoResult;
auto list = repoPtr->listLocal();
if (!list) {
qWarning() << "list local packages failed:" << list.error();
return std::nullopt;
}
const auto matcher = [&](const linglong::api::types::v1::PackageInfoV2 &info) -> bool {
if (scope == Scope::App) {
return info.id == appId && info.kind == "app";
}
if (scope == Scope::Base) {
return info.id == baseId && info.kind == "base";
}
return false;
};
auto it = std::find_if(list->begin(), list->end(), matcher);
if (it == list->end() || !it->cliConfig) {
return std::nullopt;
}
json packaged = *(it->cliConfig);
return packaged;
};
auto openConfig = [&](Scope scope,
const std::string &appId,
const std::string &baseId) -> std::optional<json> {
auto userPath = getConfigPath(scope, appId, baseId);
if (userPath.empty()) {
fprintf(stderr, "invalid config path\n");
return std::nullopt;
}
bool userExists = false;
auto userJson = readJsonIfExists(userPath, &userExists);
if (!userJson) {
fprintf(stderr, "failed to read %s\n", userPath.string().c_str());
return std::nullopt;
}
if (!userExists) {
auto searchPaths = getConfigSearchPaths(scope, appId, baseId);
for (size_t idx = 1; idx < searchPaths.size(); ++idx) {
bool existed = false;
auto fallback = readJsonIfExists(searchPaths[idx], &existed);
if (!fallback || !existed) {
continue;
}
return fallback;
}
if (auto packaged = loadCliConfigFromPackage(scope, appId, baseId)) {
return packaged;
}
}
return userJson;
};
auto saveConfig = [&](Scope scope,
const std::string &appId,
const std::string &baseId,
const json &j) -> bool {
auto path = getConfigPath(scope, appId, baseId);
if (path.empty()) {
return false;
}
if (!writeJsonAtomic(path, j)) {
fprintf(stderr, "failed to write %s\n", path.string().c_str());
return false;
}
printf("Written %s\n", path.string().c_str());
return true;
};
if (sub == "set-extensions" || sub == "add-extensions") {
auto [scope, appId, baseId, i] = parseScope(3);
if (i >= argc) {
printConfigUsage();
return 1;
}
std::vector<std::string> exts = splitCsv(mainArgv[i]);
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
jsonSetExtensions(*j, exts, sub == "set-extensions");
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "set-env") {
auto [scope, appId, baseId, i] = parseScope(3);
if (i >= argc) {
printConfigUsage();
return 1;
}
std::vector<std::string> kvs;
for (; i < argc; ++i) {
kvs.push_back(mainArgv[i]);
}
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
jsonSetEnv(*j, kvs);
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "unset-env") {
auto [scope, appId, baseId, i] = parseScope(3);
if (i >= argc) {
printConfigUsage();
return 1;
}
std::vector<std::string> keys;
for (; i < argc; ++i) {
keys.push_back(mainArgv[i]);
}
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
jsonUnsetEnv(*j, keys);
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "add-fs") {
auto [scope, appId, baseId, i] = parseScope(3);
FsArg fs;
fs.mode = "ro";
fs.persist = false;
for (; i < argc; ++i) {
std::string a = mainArgv[i];
if (a == "--persist") {
fs.persist = true;
} else if (a == "--host" && i + 1 < argc) {
fs.host = mainArgv[++i];
} else if (a == "--target" && i + 1 < argc) {
fs.target = mainArgv[++i];
} else if (a == "--mode" && i + 1 < argc) {
fs.mode = mainArgv[++i];
} else {
fprintf(stderr, "unknown arg: %s\n", a.c_str());
return 1;
}
}
if (fs.host.empty() || fs.target.empty()) {
printConfigUsage();
return 1;
}
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
jsonAddFs(*j, fs);
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "add-fs-allow") {
auto [scope, appId, baseId, i] = parseScope(3);
FsArg fs;
fs.mode = "ro";
fs.persist = false;
for (; i < argc; ++i) {
std::string a = mainArgv[i];
if (a == "--persist") {
fs.persist = true;
} else if (a == "--host" && i + 1 < argc) {
fs.host = mainArgv[++i];
} else if (a == "--target" && i + 1 < argc) {
fs.target = mainArgv[++i];
} else if (a == "--mode" && i + 1 < argc) {
fs.mode = mainArgv[++i];
} else {
fprintf(stderr, "unknown arg: %s\n", a.c_str());
return 1;
}
}
if (fs.host.empty() || fs.target.empty()) {
printConfigUsage();
return 1;
}
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
jsonAddFsAllow(*j, fs);
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "rm-fs") {
auto [scope, appId, baseId, i] = parseScope(3);
std::optional<std::string> target;
std::optional<size_t> index;
for (; i < argc; ++i) {
std::string a = mainArgv[i];
if (a == "--target" && i + 1 < argc) {
target = mainArgv[++i];
} else if (a == "--index" && i + 1 < argc) {
index = static_cast<size_t>(std::stoul(mainArgv[++i]));
} else {
fprintf(stderr, "unknown arg: %s\n", a.c_str());
return 1;
}
}
if (!target && !index) {
printConfigUsage();
return 1;
}
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
bool ok = false;
if (target) {
ok = jsonRmFsByTarget(*j, *target);
}
if (!ok && index) {
ok = jsonRmFsByIndex(*j, *index);
}
if (!ok) {
fprintf(stderr, "no filesystem entry removed\n");
return 1;
}
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "rm-fs-allow") {
auto [scope, appId, baseId, i] = parseScope(3);
std::optional<std::string> target;
std::optional<size_t> index;
for (; i < argc; ++i) {
std::string a = mainArgv[i];
if (a == "--target" && i + 1 < argc) {
target = mainArgv[++i];
} else if (a == "--index" && i + 1 < argc) {
index = static_cast<size_t>(std::stoul(mainArgv[++i]));
} else {
fprintf(stderr, "unknown arg: %s\n", a.c_str());
return 1;
}
}
if (!target && !index) {
printConfigUsage();
return 1;
}
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
bool ok = false;
if (target) {
ok = jsonRmFsAllowByTarget(*j, *target);
}
if (!ok && index) {
ok = jsonRmFsAllowByIndex(*j, *index);
}
if (!ok) {
fprintf(stderr, "no filesystem_allow entry removed\n");
return 1;
}
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "clear-fs-allow") {
auto [scope, appId, baseId, i] = parseScope(3);
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
jsonClearFsAllow(*j);
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "set-command") {
auto [scope, appId, baseId, i] = parseScope(3);
if (i >= argc) {
printConfigUsage();
return 1;
}
CmdSetArg a;
a.cmd = mainArgv[i++];
for (; i < argc; ++i) {
std::string t = mainArgv[i];
if (t == "--entrypoint" && i + 1 < argc) {
a.entrypoint = mainArgv[++i];
} else if (t == "--cwd" && i + 1 < argc) {
a.cwd = mainArgv[++i];
} else if (t == "--args-prefix" && i + 1 < argc) {
std::stringstream ss(mainArgv[++i]);
std::string tok;
while (ss >> tok) {
a.argsPrefix.push_back(tok);
}
} else if (t == "--args-suffix" && i + 1 < argc) {
std::stringstream ss(mainArgv[++i]);
std::string tok;
while (ss >> tok) {
a.argsSuffix.push_back(tok);
}
} else if (t.find('=') != std::string::npos) {
a.envKVs.push_back(t);
} else {
fprintf(stderr, "unknown arg: %s\n", t.c_str());
return 1;
}
}
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
jsonSetCommand(*j, a);
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
if (sub == "unset-command") {
auto [scope, appId, baseId, i] = parseScope(3);
if (i >= argc) {
printConfigUsage();
return 1;
}
std::string cmd = mainArgv[i];
auto j = openConfig(scope, appId, baseId);
if (!j) {
return 1;
}
jsonUnsetCommand(*j, cmd);
if (!saveConfig(scope, appId, baseId, *j)) {
return 1;
}
return 0;
}
printConfigUsage();
return 1;
}
}
// ===== end: "ll-cli config ..." dispatcher =====
CLI::App commandParser{ _(
"linyaps CLI\n"
"A CLI program to run application and manage application and runtime\n") };
auto shortConfigHelp = configShortHelp();
auto fullConfigHelp = shortConfigHelp + configUsageLines();
commandParser.formatter(std::make_shared<ConfigAwareFormatter>(shortConfigHelp,
fullConfigHelp,
configFooterMessage()));
commandParser.option_defaults()->group(_("Options"));
if (auto formatter = commandParser.get_formatter()) {
formatter->label("OPTIONS", _("OPTIONS"));
formatter->label("SUBCOMMAND", _("SUBCOMMAND"));
formatter->label("SUBCOMMANDS", _("SUBCOMMANDS"));
formatter->label("POSITIONALS", _("POSITIONALS"));
formatter->label("Usage", _("Usage"));
formatter->label("REQUIRED", _("REQUIRED"));
}
auto argv = commandParser.ensure_utf8(mainArgv);
if (argc == 1) {
std::cout << commandParser.help() << std::endl;
return 0;
}
commandParser.get_help_ptr()->description(_("Print this help message and exit"));
commandParser.set_help_all_flag("--help-all", _("Expand all help"));
if (auto *helpOption = commandParser.get_help_ptr()) {
helpOption->description(_("Print this help message and exit"));
helpOption->group(_("Options"));
}
if (auto *helpAllOption = commandParser.set_help_all_flag("--help-all", _("Expand all help"))) {
helpAllOption->group(_("Options"));
}
commandParser.usage(_("Usage: ll-cli [OPTIONS] [SUBCOMMAND]"));
commandParser.footer(_(R"(If you found any problems during use,
You can report bugs to the linyaps team under this project: https://github.com/OpenAtom-Linyaps/linyaps/issues)"));
commandParser.footer("");
// group empty will hide command
constexpr auto CliHiddenGroup = "";

View File

@ -18,6 +18,7 @@
#include "linglong/api/types/v1/helper.hpp"
#include "linglong/api/types/v1/BuilderProjectBuildEXT.hpp"
#include "linglong/api/types/v1/LlCLIConfig.hpp"
#include "linglong/api/types/v1/BuilderProjectModules.hpp"
#include "linglong/api/types/v1/BuilderProjectPackage.hpp"
#include "linglong/api/types/v1/ApplicationConfigurationPermissions.hpp"
@ -50,6 +51,10 @@ std::string build;
*/
std::optional<BuilderProjectBuildEXT> buildext;
/**
* default ll-cli configuration delivered with package
*/
std::optional<LlCLIConfig> cliConfig;
/**
* command of builder project
*/
std::optional<std::vector<std::string>> command;

View File

@ -67,6 +67,9 @@
#include "linglong/api/types/v1/BuilderProjectSource.hpp"
#include "linglong/api/types/v1/BuilderProjectPackage.hpp"
#include "linglong/api/types/v1/BuilderProjectModules.hpp"
#include "linglong/api/types/v1/LlCLIConfig.hpp"
#include "linglong/api/types/v1/LlCLICommandConfig.hpp"
#include "linglong/api/types/v1/LlCLIFilesystemEntry.hpp"
#include "linglong/api/types/v1/BuilderProjectBuildEXT.hpp"
#include "linglong/api/types/v1/Apt.hpp"
#include "linglong/api/types/v1/BuilderConfig.hpp"
@ -108,6 +111,15 @@ void to_json(json & j, const Apt & x);
void from_json(const json & j, BuilderProjectBuildEXT & x);
void to_json(json & j, const BuilderProjectBuildEXT & x);
void from_json(const json & j, LlCLIFilesystemEntry & x);
void to_json(json & j, const LlCLIFilesystemEntry & x);
void from_json(const json & j, LlCLICommandConfig & x);
void to_json(json & j, const LlCLICommandConfig & x);
void from_json(const json & j, LlCLIConfig & x);
void to_json(json & j, const LlCLIConfig & x);
void from_json(const json & j, BuilderProjectModules & x);
void to_json(json & j, const BuilderProjectModules & x);
@ -383,6 +395,83 @@ j["apt"] = x.apt;
}
}
inline void from_json(const json & j, LlCLIFilesystemEntry& x) {
x.host = j.at("host").get<std::string>();
x.mode = get_stack_optional<std::string>(j, "mode");
x.persist = get_stack_optional<bool>(j, "persist");
x.target = j.at("target").get<std::string>();
}
inline void to_json(json & j, const LlCLIFilesystemEntry & x) {
j = json::object();
j["host"] = x.host;
if (x.mode) {
j["mode"] = x.mode;
}
if (x.persist) {
j["persist"] = x.persist;
}
j["target"] = x.target;
}
inline void from_json(const json & j, LlCLICommandConfig& x) {
x.argsPrefix = get_stack_optional<std::vector<std::string>>(j, "args_prefix");
x.argsSuffix = get_stack_optional<std::vector<std::string>>(j, "args_suffix");
x.cwd = get_stack_optional<std::string>(j, "cwd");
x.entrypoint = get_stack_optional<std::string>(j, "entrypoint");
x.env = get_stack_optional<std::map<std::string, std::string>>(j, "env");
x.filesystem = get_stack_optional<std::vector<LlCLIFilesystemEntry>>(j, "filesystem");
}
inline void to_json(json & j, const LlCLICommandConfig & x) {
j = json::object();
if (x.argsPrefix) {
j["args_prefix"] = x.argsPrefix;
}
if (x.argsSuffix) {
j["args_suffix"] = x.argsSuffix;
}
if (x.cwd) {
j["cwd"] = x.cwd;
}
if (x.entrypoint) {
j["entrypoint"] = x.entrypoint;
}
if (x.env) {
j["env"] = x.env;
}
if (x.filesystem) {
j["filesystem"] = x.filesystem;
}
}
inline void from_json(const json & j, LlCLIConfig& x) {
x.commands = get_stack_optional<std::map<std::string, LlCLICommandConfig>>(j, "commands");
x.env = get_stack_optional<std::map<std::string, std::string>>(j, "env");
x.extensions = get_stack_optional<std::vector<std::string>>(j, "extensions");
x.filesystem = get_stack_optional<std::vector<LlCLIFilesystemEntry>>(j, "filesystem");
x.filesystemAllowOnly = get_stack_optional<std::vector<LlCLIFilesystemEntry>>(j, "filesystem_allow_only");
}
inline void to_json(json & j, const LlCLIConfig & x) {
j = json::object();
if (x.commands) {
j["commands"] = x.commands;
}
if (x.env) {
j["env"] = x.env;
}
if (x.extensions) {
j["extensions"] = x.extensions;
}
if (x.filesystem) {
j["filesystem"] = x.filesystem;
}
if (x.filesystemAllowOnly) {
j["filesystem_allow_only"] = x.filesystemAllowOnly;
}
}
inline void from_json(const json & j, BuilderProjectModules& x) {
x.files = j.at("files").get<std::vector<std::string>>();
x.name = j.at("name").get<std::string>();
@ -468,6 +557,7 @@ inline void from_json(const json & j, BuilderProject& x) {
x.base = j.at("base").get<std::string>();
x.build = j.at("build").get<std::string>();
x.buildext = get_stack_optional<BuilderProjectBuildEXT>(j, "buildext");
x.cliConfig = get_stack_optional<LlCLIConfig>(j, "cli_config");
x.command = get_stack_optional<std::vector<std::string>>(j, "command");
x.exclude = get_stack_optional<std::vector<std::string>>(j, "exclude");
x.include = get_stack_optional<std::vector<std::string>>(j, "include");
@ -487,6 +577,9 @@ j["build"] = x.build;
if (x.buildext) {
j["buildext"] = x.buildext;
}
if (x.cliConfig) {
j["cli_config"] = x.cliConfig;
}
if (x.command) {
j["command"] = x.command;
}
@ -746,6 +839,7 @@ inline void from_json(const json & j, PackageInfoDisplay& x) {
x.arch = j.at("arch").get<std::vector<std::string>>();
x.base = j.at("base").get<std::string>();
x.channel = j.at("channel").get<std::string>();
x.cliConfig = get_stack_optional<LlCLIConfig>(j, "cli_config");
x.command = get_stack_optional<std::vector<std::string>>(j, "command");
x.compatibleVersion = get_stack_optional<std::string>(j, "compatible_version");
x.description = get_stack_optional<std::string>(j, "description");
@ -769,6 +863,9 @@ j = json::object();
j["arch"] = x.arch;
j["base"] = x.base;
j["channel"] = x.channel;
if (x.cliConfig) {
j["cli_config"] = x.cliConfig;
}
if (x.command) {
j["command"] = x.command;
}
@ -809,6 +906,7 @@ inline void from_json(const json & j, PackageInfoV2& x) {
x.arch = j.at("arch").get<std::vector<std::string>>();
x.base = j.at("base").get<std::string>();
x.channel = j.at("channel").get<std::string>();
x.cliConfig = get_stack_optional<LlCLIConfig>(j, "cli_config");
x.command = get_stack_optional<std::vector<std::string>>(j, "command");
x.compatibleVersion = get_stack_optional<std::string>(j, "compatible_version");
x.description = get_stack_optional<std::string>(j, "description");
@ -831,6 +929,9 @@ j = json::object();
j["arch"] = x.arch;
j["base"] = x.base;
j["channel"] = x.channel;
if (x.cliConfig) {
j["cli_config"] = x.cliConfig;
}
if (x.command) {
j["command"] = x.command;
}
@ -1248,6 +1349,9 @@ x.interactionMessageType = get_stack_optional<InteractionMessageType>(j, "Intera
x.interactionReply = get_stack_optional<InteractionReply>(j, "InteractionReply");
x.interactionRequest = get_stack_optional<InteractionRequest>(j, "InteractionRequest");
x.layerInfo = get_stack_optional<LayerInfo>(j, "LayerInfo");
x.llCLICommandConfig = get_stack_optional<LlCLICommandConfig>(j, "LlCliCommandConfig");
x.llCLIConfig = get_stack_optional<LlCLIConfig>(j, "LlCliConfig");
x.llCLIFilesystemEntry = get_stack_optional<LlCLIFilesystemEntry>(j, "LlCliFilesystemEntry");
x.ociConfigurationPatch = get_stack_optional<OciConfigurationPatch>(j, "OCIConfigurationPatch");
x.packageInfo = get_stack_optional<PackageInfo>(j, "PackageInfo");
x.packageInfoDisplay = get_stack_optional<PackageInfoDisplay>(j, "PackageInfoDisplay");
@ -1336,6 +1440,15 @@ j["InteractionRequest"] = x.interactionRequest;
if (x.layerInfo) {
j["LayerInfo"] = x.layerInfo;
}
if (x.llCLICommandConfig) {
j["LlCliCommandConfig"] = x.llCLICommandConfig;
}
if (x.llCLIConfig) {
j["LlCliConfig"] = x.llCLIConfig;
}
if (x.llCLIFilesystemEntry) {
j["LlCliFilesystemEntry"] = x.llCLIFilesystemEntry;
}
if (x.ociConfigurationPatch) {
j["OCIConfigurationPatch"] = x.ociConfigurationPatch;
}
@ -1435,7 +1548,7 @@ case InteractionMessageType::Install: j = "Install"; break;
case InteractionMessageType::Uninstall: j = "Uninstall"; break;
case InteractionMessageType::Unknown: j = "Unknown"; break;
case InteractionMessageType::Upgrade: j = "Upgrade"; break;
default: throw std::runtime_error("Unexpected value in enumeration \"[object Object]\": " + std::to_string(static_cast<int>(x)));
default: throw std::runtime_error("Unexpected value in enumeration \"InteractionMessageType\": " + std::to_string(static_cast<int>(x)));
}
}
@ -1461,7 +1574,7 @@ case State::Processing: j = "Processing"; break;
case State::Queued: j = "Queued"; break;
case State::Succeed: j = "Succeed"; break;
case State::Unknown: j = "Unknown"; break;
default: throw std::runtime_error("Unexpected value in enumeration \"[object Object]\": " + std::to_string(static_cast<int>(x)));
default: throw std::runtime_error("Unexpected value in enumeration \"State\": " + std::to_string(static_cast<int>(x)));
}
}
@ -1489,7 +1602,7 @@ case SubState::PostAction: j = "PostAction"; break;
case SubState::PreAction: j = "PreAction"; break;
case SubState::Uninstall: j = "Uninstall"; break;
case SubState::Unknown: j = "Unknown"; break;
default: throw std::runtime_error("Unexpected value in enumeration \"[object Object]\": " + std::to_string(static_cast<int>(x)));
default: throw std::runtime_error("Unexpected value in enumeration \"SubState\": " + std::to_string(static_cast<int>(x)));
}
}
@ -1501,7 +1614,7 @@ else { throw std::runtime_error("Input JSON does not conform to schema!"); }
inline void to_json(json & j, const Version & x) {
switch (x) {
case Version::The1: j = "1"; break;
default: throw std::runtime_error("Unexpected value in enumeration \"[object Object]\": " + std::to_string(static_cast<int>(x)));
default: throw std::runtime_error("Unexpected value in enumeration \"Version\": " + std::to_string(static_cast<int>(x)));
}
}
}

View File

@ -35,6 +35,9 @@
#include "linglong/api/types/v1/InteractionReply.hpp"
#include "linglong/api/types/v1/InteractionRequest.hpp"
#include "linglong/api/types/v1/LayerInfo.hpp"
#include "linglong/api/types/v1/LlCLICommandConfig.hpp"
#include "linglong/api/types/v1/LlCLIConfig.hpp"
#include "linglong/api/types/v1/LlCLIFilesystemEntry.hpp"
#include "linglong/api/types/v1/OciConfigurationPatch.hpp"
#include "linglong/api/types/v1/PackageInfo.hpp"
#include "linglong/api/types/v1/PackageInfoDisplay.hpp"
@ -106,6 +109,9 @@ std::optional<InteractionMessageType> interactionMessageType;
std::optional<InteractionReply> interactionReply;
std::optional<InteractionRequest> interactionRequest;
std::optional<LayerInfo> layerInfo;
std::optional<LlCLICommandConfig> llCLICommandConfig;
std::optional<LlCLIConfig> llCLIConfig;
std::optional<LlCLIFilesystemEntry> llCLIFilesystemEntry;
std::optional<OciConfigurationPatch> ociConfigurationPatch;
std::optional<PackageInfo> packageInfo;
std::optional<PackageInfoDisplay> packageInfoDisplay;

View File

@ -0,0 +1,66 @@
// This file is generated by tools/codegen.sh
// DO NOT EDIT IT.
// clang-format off
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// LlCLICommandConfig.hpp data = nlohmann::json::parse(jsonString);
#pragma once
#include <optional>
#include <nlohmann/json.hpp>
#include "linglong/api/types/v1/helper.hpp"
#include "linglong/api/types/v1/LlCLIFilesystemEntry.hpp"
namespace linglong {
namespace api {
namespace types {
namespace v1 {
/**
* per-command overrides for ll-cli configuration
*/
using nlohmann::json;
/**
* per-command overrides for ll-cli configuration
*/
struct LlCLICommandConfig {
/**
* arguments prepended before command
*/
std::optional<std::vector<std::string>> argsPrefix;
/**
* arguments appended after command
*/
std::optional<std::vector<std::string>> argsSuffix;
/**
* working directory for command
*/
std::optional<std::string> cwd;
/**
* override entrypoint for command
*/
std::optional<std::string> entrypoint;
/**
* environment variables applied to command
*/
std::optional<std::map<std::string, std::string>> env;
/**
* filesystem entries applied to command
*/
std::optional<std::vector<LlCLIFilesystemEntry>> filesystem;
};
}
}
}
}
// clang-format on

View File

@ -0,0 +1,67 @@
// This file is generated by tools/codegen.sh
// DO NOT EDIT IT.
// clang-format off
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// LlCLIConfig.hpp data = nlohmann::json::parse(jsonString);
#pragma once
#include <optional>
#include <nlohmann/json.hpp>
#include "linglong/api/types/v1/helper.hpp"
#include "linglong/api/types/v1/LlCLICommandConfig.hpp"
#include "linglong/api/types/v1/LlCLIFilesystemEntry.hpp"
namespace linglong {
namespace api {
namespace types {
namespace v1 {
/**
* default ll-cli configuration delivered with package
*
* default ll-cli configuration provided by package
*/
using nlohmann::json;
/**
* default ll-cli configuration delivered with package
*
* default ll-cli configuration provided by package
*/
struct LlCLIConfig {
/**
* command specific overrides
*/
std::optional<std::map<std::string, LlCLICommandConfig>> commands;
/**
* default environment variables
*/
std::optional<std::map<std::string, std::string>> env;
/**
* default extensions applied when launching app
*/
std::optional<std::vector<std::string>> extensions;
/**
* additional filesystem mounts appended to defaults
*/
std::optional<std::vector<LlCLIFilesystemEntry>> filesystem;
/**
* explicit mount allowlist; when present only these mounts are permitted
*/
std::optional<std::vector<LlCLIFilesystemEntry>> filesystemAllowOnly;
};
}
}
}
}
// clang-format on

View File

@ -0,0 +1,56 @@
// This file is generated by tools/codegen.sh
// DO NOT EDIT IT.
// clang-format off
// To parse this JSON data, first install
//
// json.hpp https://github.com/nlohmann/json
//
// Then include this file, and then do
//
// LlCLIFilesystemEntry.hpp data = nlohmann::json::parse(jsonString);
#pragma once
#include <optional>
#include <nlohmann/json.hpp>
#include "linglong/api/types/v1/helper.hpp"
namespace linglong {
namespace api {
namespace types {
namespace v1 {
/**
* filesystem entry used by ll-cli configuration
*/
using nlohmann::json;
/**
* filesystem entry used by ll-cli configuration
*/
struct LlCLIFilesystemEntry {
/**
* source path on host
*/
std::string host;
/**
* mount mode, either ro or rw
*/
std::optional<std::string> mode;
/**
* whether this mount should persist in sandbox storage
*/
std::optional<bool> persist;
/**
* mount target path inside container
*/
std::string target;
};
}
}
}
}
// clang-format on

View File

@ -17,6 +17,7 @@
#include <nlohmann/json.hpp>
#include "linglong/api/types/v1/helper.hpp"
#include "linglong/api/types/v1/LlCLIConfig.hpp"
#include "linglong/api/types/v1/ExtensionImpl.hpp"
#include "linglong/api/types/v1/ExtensionDefine.hpp"
#include "linglong/api/types/v1/ApplicationConfigurationPermissions.hpp"
@ -52,6 +53,10 @@ std::string base;
*/
std::string channel;
/**
* default ll-cli configuration provided by package
*/
std::optional<LlCLIConfig> cliConfig;
/**
* command of package info
*/
std::optional<std::vector<std::string>> command;

View File

@ -17,6 +17,7 @@
#include <nlohmann/json.hpp>
#include "linglong/api/types/v1/helper.hpp"
#include "linglong/api/types/v1/LlCLIConfig.hpp"
#include "linglong/api/types/v1/ExtensionImpl.hpp"
#include "linglong/api/types/v1/ExtensionDefine.hpp"
#include "linglong/api/types/v1/ApplicationConfigurationPermissions.hpp"
@ -52,6 +53,10 @@ std::string base;
*/
std::string channel;
/**
* default ll-cli configuration provided by package
*/
std::optional<LlCLIConfig> cliConfig;
/**
* command of package info
*/
std::optional<std::vector<std::string>> command;

View File

@ -9,6 +9,7 @@ pfl_add_library(
STATIC
SOURCES
# find -regex '\.\/*.+\.[ch]\(pp\)?\(.in\)?' -type f -printf '%P\n'| sort
src/linglong/common/constants.h
src/linglong/common/dir.cpp
src/linglong/common/dir.h
src/linglong/common/display.cpp

View File

@ -0,0 +1,7 @@
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#pragma once
constexpr auto default_file_mode = 0644;

View File

@ -1164,7 +1164,9 @@ utils::error::Result<void> Builder::commitToLocalRepo() noexcept
auto info = api::types::v1::PackageInfoV2{
.arch = { projectRef->arch.toStdString() },
.base = project.base,
.channel = projectRef->channel,
.cliConfig = project.cliConfig,
.command = project.command,
.description = project.package.description,
.id = project.package.id,
@ -1176,7 +1178,6 @@ utils::error::Result<void> Builder::commitToLocalRepo() noexcept
.version = project.package.version,
};
info.base = project.base;
if (project.runtime) {
info.runtime = project.runtime;
}

View File

@ -664,6 +664,9 @@ int Cli::run(const RunOptions &options)
runContext.enableSecurityContext(runtime::getDefaultSecurityContexts());
const auto &fsPolicy = runContext.filesystemPolicy();
const bool restrictFilesystem = fsPolicy.allowListConfigured;
linglong::generator::ContainerCfgBuilder cfgBuilder;
cfgBuilder.setAppId(curAppRef->id)
.setAnnotation(generator::ANNOTATION::LAST_PID, std::to_string(pid))
@ -673,15 +676,28 @@ int Cli::run(const RunOptions &options)
.bindDevNode()
.bindCgroup()
.bindXDGRuntime()
.bindUserGroup()
.bindRemovableStorageMounts()
.bindHostRoot()
.bindHostStatics()
.bindHome(homeEnv)
.enablePrivateDir()
.mapPrivate(std::string{ homeEnv } + "/.ssh", true)
.mapPrivate(std::string{ homeEnv } + "/.gnupg", true)
.bindIPC()
.bindUserGroup();
if (!restrictFilesystem) {
cfgBuilder.bindRemovableStorageMounts()
.bindHostRoot()
.bindHostStatics();
}
cfgBuilder.bindHome(homeEnv).enablePrivateDir();
if (restrictFilesystem) {
cfgBuilder.disableHostHomeBind();
}
if (restrictFilesystem) {
cfgBuilder.mapPrivate(std::string{ homeEnv }, true);
} else {
cfgBuilder.mapPrivate(std::string{ homeEnv } + "/.ssh", true)
.mapPrivate(std::string{ homeEnv } + "/.gnupg", true);
}
cfgBuilder.bindIPC()
.forwardDefaultEnv()
.enableSelfAdjustingMount();

View File

@ -688,13 +688,19 @@ QVariantMap PackageManager::installFromLayer(const QDBusUnixFileDescriptor &fd,
return;
}
if (!localRef) {
auto newRef = package::Reference::fromPackageInfo(*info);
if (!newRef) {
taskRef.reportError(std::move(newRef).error());
return;
}
auto newRef = package::Reference::fromPackageInfo(*info);
if (!newRef) {
taskRef.reportError(std::move(newRef).error());
return;
}
auto ret = executePostInstallHooks(*newRef);
if (!ret) {
taskRef.reportError(std::move(ret).error());
return;
}
if (!localRef) {
auto generateCacheRet = this->tryGenerateCache(*newRef);
if (!generateCacheRet) {
taskRef.reportError(std::move(generateCacheRet).error());
@ -710,31 +716,19 @@ QVariantMap PackageManager::installFromLayer(const QDBusUnixFileDescriptor &fd,
return;
}
auto newRef = package::Reference::fromPackageInfo(*info);
if (!newRef) {
taskRef.reportError(std::move(newRef).error());
return;
}
auto generateCacheRet = this->tryGenerateCache(*newRef);
if (!generateCacheRet) {
taskRef.reportError(std::move(generateCacheRet).error());
return;
}
auto ret = removeAfterInstall(*localRef, *newRef, std::vector{ module });
ret = removeAfterInstall(*localRef, *newRef, std::vector{ module });
if (!ret) {
LogE("failed to remove old reference {} after install {}: {}",
localRef->toString(),
packageRef.toString(),
ret.error().message());
}
ret = executePostInstallHooks(*newRef);
if (!ret) {
taskRef.reportError(std::move(ret).error());
return;
}
};
auto refSpec = fmt::format("{}:{}/{}/{}/{}",
@ -1342,8 +1336,21 @@ void PackageManager::Update(PackageTask &taskContext,
if (tmp.state() != linglong::api::types::v1::State::Succeed) {
LogE("failed to rollback install {}", newRef.toString());
}
auto ret = executePostUninstallHooks(newRef);
if (!ret) {
LogE("failed to rollback execute uninstall hooks: {}", ret.error());
}
});
auto result = executePostInstallHooks(newRef);
if (!result) {
taskContext.updateState(linglong::api::types::v1::State::Failed,
"Failed to execute postInstall hooks.\n"
+ result.error().message());
return;
}
auto oldRefLayerItem = this->repo.getLayerItem(oldRef);
auto msg = fmt::format("Upgrade {} (from repo : {}) to {} (from repo : {}) success",
@ -1377,12 +1384,6 @@ void PackageManager::Update(PackageTask &taskContext,
return;
}
ret = executePostInstallHooks(newRef);
if (!ret) {
qCritical() << "failed to execute post install hooks" << ret.error().message();
return;
}
auto result = this->tryGenerateCache(newRef);
if (!result) {
taskContext.reportError(
@ -1504,14 +1505,14 @@ void PackageManager::pullDependency(PackageTask &taskContext,
transaction.addRollBack([this, remoteRef = *remote, module]() noexcept {
auto result = this->repo.remove(remoteRef.reference, module);
if (!result) {
qCritical() << result.error();
Q_ASSERT(false);
LogE("failed to remove remote reference: {} : {}",
remoteRef.reference.toString(),
result.error().message());
}
result = executePostUninstallHooks(remoteRef.reference);
if (!result) {
qCritical() << result.error();
Q_ASSERT(false);
LogE("failed to rollback execute uninstall hooks: {}", result.error());
}
});
}

View File

@ -114,6 +114,7 @@ public
const std::vector<std::string> &modules) noexcept;
utils::error::Result<void> tryGenerateCache(const package::Reference &ref) noexcept;
utils::error::Result<void> executePostInstallHooks(const package::Reference &ref) noexcept;
utils::error::Result<void> executePostUninstallHooks(const package::Reference &ref) noexcept;
void pullDependency(PackageTask &taskContext,
const api::types::v1::PackageInfoV2 &info,
const std::string &module) noexcept;
@ -169,7 +170,6 @@ private:
Prune(std::vector<api::types::v1::PackageInfoV2> &removedInfo) noexcept;
utils::error::Result<void> generateCache(const package::Reference &ref) noexcept;
utils::error::Result<void> removeCache(const package::Reference &ref) noexcept;
utils::error::Result<void> executePostUninstallHooks(const package::Reference &ref) noexcept;
linglong::repo::OSTreeRepo &repo; // NOLINT
PackageTaskQueue tasks;

View File

@ -206,6 +206,11 @@ utils::error::Result<void> RefInstallationAction::install(PackageTask &task)
if (tmp.state() != linglong::api::types::v1::State::Succeed) {
LogE("failed to rollback install {}", newRef.toString());
}
auto result = pm.executePostUninstallHooks(newRef);
if (!result) {
LogE("failed to rollback execute uninstall hooks: {}", result.error());
}
});
return LINGLONG_OK;
@ -224,6 +229,14 @@ utils::error::Result<void> RefInstallationAction::postInstall(PackageTask &task)
const auto &newRef = operation.newRef->reference;
const auto &oldRef = operation.oldRef;
auto ret = pm.executePostInstallHooks(newRef);
if (!ret) {
task.updateState(linglong::api::types::v1::State::Failed,
"Failed to execute postInstall hooks.\n" + ret.error().message());
return LINGLONG_ERR(ret);
}
auto layer = this->repo.getLayerItem(newRef);
if (!layer) {
task.reportError(
@ -256,13 +269,6 @@ utils::error::Result<void> RefInstallationAction::postInstall(PackageTask &task)
}
}
auto ret = pm.executePostInstallHooks(newRef);
if (!ret) {
task.updateState(linglong::api::types::v1::State::Failed,
"Failed to execute postInstall hooks.\n" + ret.error().message());
return LINGLONG_ERR(ret);
}
transaction.commit();
task.updateState(linglong::api::types::v1::State::Succeed,
fmt::format("Install {} (from repo: {}) success",

View File

@ -329,6 +329,12 @@ utils::error::Result<void> UabInstallationAction::postInstall(PackageTask &task)
const auto &newRef = operation.newRef->reference;
const auto &oldRef = operation.oldRef;
auto ret = pm.executePostInstallHooks(newRef);
if (!ret) {
task.reportError(std::move(ret).error());
return LINGLONG_ERR("failed to execute post install hooks");
}
if (!oldRef) {
auto mergeRet = repo.mergeModules();
if (!mergeRet) {
@ -360,12 +366,6 @@ utils::error::Result<void> UabInstallationAction::postInstall(PackageTask &task)
}
}
auto ret = pm.executePostInstallHooks(newRef);
if (!ret) {
task.reportError(std::move(ret).error());
return LINGLONG_ERR("failed to execute post install hooks");
}
transaction.commit();
task.updateState(linglong::api::types::v1::State::Succeed, "install uab successfully");
@ -426,6 +426,11 @@ utils::error::Result<void> UabInstallationAction::installUabLayer(
if (!ret) {
LogE("rollback importLayerDir failed: {}", ret.error());
}
ret = pm.executePostUninstallHooks(ref);
if (!ret) {
LogE("failed to rollback execute uninstall hooks: {}", ret.error());
}
});
}
@ -508,4 +513,4 @@ utils::error::Result<void> UabInstallationAction::installDistributionModeUAB()
return LINGLONG_OK;
}
} // namespace linglong::service
} // namespace linglong::service

View File

@ -3,6 +3,9 @@
*/
#include "run_context.h"
#include "configure.h"
#include "linglong/api/types/v1/Generators.hpp"
#include "linglong/common/display.h"
#include "linglong/extension/extension.h"
@ -11,8 +14,359 @@
#include <utility>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <map>
#include <nlohmann/json.hpp>
#include <optional>
#include <sstream>
#include <unordered_set>
#include <vector>
namespace linglong::runtime {
static std::vector<std::filesystem::path> configBasesUserFirst()
{
namespace fs = std::filesystem;
std::vector<fs::path> bases;
std::unordered_set<std::string> seen;
auto addBase = [&](const fs::path &candidate) {
if (candidate.empty()) {
return;
}
auto normalized = candidate.lexically_normal();
auto key = normalized.string();
if (key.empty()) {
return;
}
if (seen.insert(key).second) {
bases.emplace_back(std::move(normalized));
}
};
if (const char *xdgConfigHome = ::getenv("XDG_CONFIG_HOME");
xdgConfigHome && xdgConfigHome[0] != '\0') {
addBase(fs::path(xdgConfigHome) / "linglong");
} else if (const char *homeEnv = ::getenv("HOME"); homeEnv && homeEnv[0] != '\0') {
addBase(fs::path(homeEnv) / ".config" / "linglong");
}
addBase(fs::path(LINGLONG_DATA_DIR) / "config");
return bases;
}
static std::vector<std::filesystem::path> configBasesFallbackFirst()
{
auto bases = configBasesUserFirst();
if (bases.size() <= 1) {
return bases;
}
std::rotate(bases.begin(), std::prev(bases.end()), bases.end());
return bases;
}
static std::vector<std::string> loadExtensionsFromConfig(const std::string &appId)
{
namespace fs = std::filesystem;
std::vector<std::string> result;
// 2. 定义解析函数:从指定 JSON 文件提取 "extensions" 数组,并加入 result
auto parseExtensions = [&](const fs::path &p) {
try {
if (!fs::exists(p)) return;
std::ifstream in(p);
if (!in.is_open()) return;
nlohmann::json j;
in >> j;
if (!j.contains("extensions") || !j.at("extensions").is_array()) return;
for (const auto &elem : j.at("extensions")) {
if (elem.is_string()) {
std::string ext = elem.get<std::string>();
if (std::find(result.begin(), result.end(), ext) == result.end()) {
result.emplace_back(std::move(ext));
}
}
}
} catch (...) {
// 文件不存在或解析失败时忽略
}
};
for (const auto &base : configBasesUserFirst()) {
parseExtensions(base / "config.json");
if (!appId.empty()) {
parseExtensions(base / "apps" / appId / "config.json");
}
}
return result;
}
static std::vector<std::string> loadExtensionsFromBase(const std::string &baseId)
{
namespace fs = std::filesystem;
std::vector<std::string> result;
if (baseId.empty()) {
return result;
}
for (const auto &root : configBasesUserFirst()) {
fs::path cfgPath = root / "base" / baseId / "config.json";
try {
if (!fs::exists(cfgPath)) {
continue;
}
std::ifstream in(cfgPath);
if (!in.is_open()) {
continue;
}
nlohmann::json j;
in >> j;
if (!j.contains("extensions") || !j.at("extensions").is_array()) {
continue;
}
for (const auto &elem : j.at("extensions")) {
if (elem.is_string()) {
std::string ext = elem.get<std::string>();
if (std::find(result.begin(), result.end(), ext) == result.end()) {
result.emplace_back(std::move(ext));
}
}
}
} catch (...) {
// ignore parse errors
}
}
return result;
}
// ===== begin: config helpers for env/mount/commands (Global->Base->App merge) =====
using json = nlohmann::json;
static json loadMergedJsonWithBase(const std::string &appId, const std::string &baseId)
{
namespace fs = std::filesystem;
json merged = json::object();
auto readIfExists = [](const fs::path &p) -> std::optional<json> {
try {
if (!fs::exists(p)) {
return std::nullopt;
}
std::ifstream in(p);
if (!in.is_open()) {
return std::nullopt;
}
json j;
in >> j;
return j;
} catch (...) {
return std::nullopt;
}
};
for (const auto &root : configBasesFallbackFirst()) {
if (auto g = readIfExists(root / "config.json")) {
merged.merge_patch(*g);
}
if (!baseId.empty()) {
if (auto b = readIfExists(root / "base" / baseId / "config.json")) {
merged.merge_patch(*b);
}
}
if (!appId.empty()) {
if (auto a = readIfExists(root / "apps" / appId / "config.json")) {
merged.merge_patch(*a);
}
}
}
return merged;
}
static std::string expandUserHome(const std::string &path)
{
if (path == "~" || path.rfind("~/", 0) == 0) {
const char *home = ::getenv("HOME");
if (home && home[0]) {
return path == "~" ? std::string(home) : (std::string(home) + path.substr(1));
}
}
return path;
}
static void collectEnvFromJson(const json &j, std::vector<std::string> &out)
{
if (!j.contains("env") || !j.at("env").is_object()) {
return;
}
for (auto it = j.at("env").begin(); it != j.at("env").end(); ++it) {
const std::string key = it.key();
std::string val = it.value().is_string() ? it.value().get<std::string>() : std::string();
if (val.find('$') != std::string::npos) {
qWarning() << "ignore env with variable expansion:" << QString::fromStdString(key);
continue;
}
if (!key.empty() && key.back() == '+') {
out.emplace_back(key.substr(0, key.size() - 1) + "+=" + val);
} else {
out.emplace_back(key + "=" + val);
}
}
}
static std::vector<ocppi::runtime::config::types::Mount>
parseFilesystemMounts(const std::string &appId, const json &arr)
{
using Mount = ocppi::runtime::config::types::Mount;
std::vector<Mount> mounts;
if (!arr.is_array()) {
return mounts;
}
for (const auto &e : arr) {
if (!e.is_object()) {
continue;
}
std::string host = e.value("host", "");
std::string target = e.value("target", "");
std::string mode = e.value("mode", "ro");
bool persist = e.value("persist", false);
if (host.empty() || target.empty()) {
continue;
}
if (host.find('$') != std::string::npos || target.find('$') != std::string::npos) {
qWarning() << "ignore mount with variable expansion:" << QString::fromStdString(host)
<< "->" << QString::fromStdString(target);
continue;
}
host = expandUserHome(host);
if (persist) {
const char *home = ::getenv("HOME");
if (home && home[0] && !appId.empty()) {
std::filesystem::path p(home);
p /= ".var/app";
p /= appId;
p /= std::filesystem::path(host).filename();
host = p.string();
}
std::error_code ec;
std::filesystem::path hostPath(host);
if (!std::filesystem::exists(hostPath, ec)) {
ec.clear();
if (!std::filesystem::create_directories(hostPath, ec) && ec) {
ec.clear();
auto parent = hostPath.parent_path();
if (!parent.empty()) {
std::filesystem::create_directories(parent, ec);
}
}
}
if (ec || !std::filesystem::exists(hostPath, ec)) {
qWarning() << "failed to prepare persist directory for"
<< QString::fromStdString(host) << ":" << ec.message().c_str();
continue;
}
}
Mount m;
m.type = "bind";
m.source = host;
m.destination = target;
m.options = { { (mode == "rw" ? "rw" : "ro"), "rbind" } };
mounts.emplace_back(std::move(m));
}
return mounts;
}
static void collectMountsFromJson(const std::string &appId,
const json &j,
std::vector<ocppi::runtime::config::types::Mount> &out)
{
if (!j.contains("filesystem") || !j.at("filesystem").is_array()) {
return;
}
auto mounts = parseFilesystemMounts(appId, j.at("filesystem"));
std::move(mounts.begin(), mounts.end(), std::back_inserter(out));
}
struct CommandSettings {
std::vector<std::string> envKVs;
std::vector<ocppi::runtime::config::types::Mount> mounts;
std::vector<std::string> argsPrefix;
std::vector<std::string> argsSuffix;
std::optional<std::string> entrypoint;
std::optional<std::string> cwd;
};
static const json *pickCommandNode(const json &merged, const std::string &execName)
{
if (!merged.contains("commands") || !merged.at("commands").is_object()) {
return nullptr;
}
const auto &cmds = merged.at("commands");
if (auto it = cmds.find(execName); it != cmds.end() && it->is_object()) {
return &(*it);
}
if (auto it = cmds.find("*"); it != cmds.end() && it->is_object()) {
return &(*it);
}
return nullptr;
}
static void loadStrVec(const json &node, const char *key, std::vector<std::string> &out)
{
if (!node.contains(key) || !node.at(key).is_array()) {
return;
}
for (const auto &v : node.at(key)) {
if (v.is_string()) {
out.emplace_back(v.get<std::string>());
}
}
}
static CommandSettings parseCommandSettings(const std::string &appId, const json &node)
{
CommandSettings cs;
if (node.contains("env") && node.at("env").is_object()) {
for (auto it = node.at("env").begin(); it != node.at("env").end(); ++it) {
const std::string key = it.key();
std::string val = it.value().is_string() ? it.value().get<std::string>() : std::string();
if (val.find('$') != std::string::npos) {
qWarning() << "ignore env with variable expansion in command settings:"
<< QString::fromStdString(key);
continue;
}
if (!key.empty() && key.back() == '+') {
cs.envKVs.emplace_back(key.substr(0, key.size() - 1) + "+=" + val);
} else {
cs.envKVs.emplace_back(key + "=" + val);
}
}
}
if (node.contains("filesystem") && node.at("filesystem").is_array()) {
collectMountsFromJson(appId, node, cs.mounts);
}
loadStrVec(node, "args_prefix", cs.argsPrefix);
loadStrVec(node, "args_suffix", cs.argsSuffix);
if (node.contains("entrypoint") && node.at("entrypoint").is_string()) {
cs.entrypoint = node.at("entrypoint").get<std::string>();
}
if (node.contains("cwd") && node.at("cwd").is_string()) {
cs.cwd = node.at("cwd").get<std::string>();
}
return cs;
}
// ===== end: config helpers =====
RuntimeLayer::RuntimeLayer(package::Reference ref, RunContext &context)
: reference(std::move(ref))
, runContext(context)
@ -82,6 +436,8 @@ utils::error::Result<void> RunContext::resolve(const linglong::package::Referenc
{
LINGLONG_TRACE("resolve RunContext from runnable " + runnable.toString());
filesystemPolicyCache.reset();
containerID = runtime::genContainerID(runnable);
auto item = repo.getLayerItem(runnable);
@ -155,8 +511,50 @@ utils::error::Result<void> RunContext::resolve(const linglong::package::Referenc
}
// 手动解析多个扩展
// 先从命令行选项或配置文件获取扩展列表
// 先从命令行选项或应用/全局配置获取扩展列表
std::vector<std::string> extRefs;
if (options.extensionRefs && !options.extensionRefs->empty()) {
auto manualExtensionDef = makeManualExtensionDefine(*options.extensionRefs);
extRefs = *options.extensionRefs;
} else {
extRefs = loadExtensionsFromConfig(runnable.id);
}
if (extRefs.empty() && info.cliConfig && info.cliConfig->extensions) {
extRefs = *info.cliConfig->extensions;
}
// 如果未获取到扩展列表,则尝试根据 base 层加载
if (extRefs.empty()) {
// 获取 baseId
std::string baseId;
// 1. 优先使用 ResolveOptions::baseRef如果提供
if (options.baseRef && !options.baseRef->empty()) {
// 假设存在 FuzzyReference::parse可解析出 id 部分
auto baseRef = linglong::package::FuzzyReference::parse(*options.baseRef);
if (baseRef) {
baseId = baseRef->id;
}
}
// 2. 否则从当前运行包信息中获取
if (baseId.empty()) {
auto item = repo.getLayerItem(runnable);
if (item && !item->info.base.empty()) {
baseId = item->info.base;
}
}
// 3. 若 baseId 非空,则读取 base 配置
if (!baseId.empty()) {
extRefs = loadExtensionsFromBase(baseId);
}
}
// 若 extRefs 非空,继续使用原有的手动解析逻辑
if (!extRefs.empty()) {
auto manualExtensionDef = makeManualExtensionDefine(extRefs);
if (!manualExtensionDef) {
return LINGLONG_ERR(manualExtensionDef);
}
@ -177,6 +575,8 @@ utils::error::Result<void> RunContext::resolve(const api::types::v1::BuilderProj
{
LINGLONG_TRACE("resolve RunContext from builder project " + target.package.id);
filesystemPolicyCache.reset();
auto targetRef = package::Reference::fromBuilderProject(target);
if (!targetRef) {
return LINGLONG_ERR(targetRef);
@ -572,10 +972,176 @@ RunContext::fillContextCfg(linglong::generator::ContainerCfgBuilder &builder)
return res;
}
// === begin: merge Global->Base->App config ===
std::string currentAppId;
if (appLayer) currentAppId = appLayer->getReference().id;
else if (!targetId.empty()) currentAppId = targetId;
std::string currentBaseId;
if (baseLayer) currentBaseId = baseLayer->getReference().id;
json mergedCfg = json::object();
auto mergeCliConfig = [&](RuntimeLayer *layer) {
if (layer == nullptr) {
return;
}
auto item = layer->getCachedItem();
if (!item) {
return;
}
if (item->info.cliConfig) {
json cfg = *(item->info.cliConfig);
if (mergedCfg.empty()) {
mergedCfg = std::move(cfg);
} else {
mergedCfg.merge_patch(cfg);
}
}
};
mergeCliConfig(baseLayer ? &*baseLayer : nullptr);
mergeCliConfig(appLayer ? &*appLayer : nullptr);
auto configFromDirs = loadMergedJsonWithBase(currentAppId, currentBaseId);
if (mergedCfg.empty()) {
mergedCfg = configFromDirs;
} else {
mergedCfg.merge_patch(configFromDirs);
}
std::optional<std::string> mergedPath;
// 1) common env
{
std::vector<std::string> envKVs;
collectEnvFromJson(mergedCfg, envKVs);
if (!envKVs.empty()) {
std::map<std::string, std::string> genEnv;
std::string basePath;
if (auto sysPath = ::getenv("PATH")) {
basePath = sysPath;
}
auto extPathIt = environment.find("PATH");
for (const auto &kv : envKVs) {
auto pos = kv.find("+=");
if (pos != std::string::npos) {
auto key = kv.substr(0, pos);
auto add = kv.substr(pos + 2);
if (key == "PATH") {
if (genEnv.count("PATH")) {
genEnv["PATH"] += ":" + add;
} else if (extPathIt != environment.end()) {
genEnv["PATH"] =
extPathIt->second.empty() ? add : extPathIt->second + ":" + add;
} else if (!basePath.empty()) {
genEnv["PATH"] = basePath + ":" + add;
} else {
genEnv["PATH"] = add;
}
} else {
qWarning() << "ignore '+=' env for key:" << QString::fromStdString(key);
}
continue;
}
auto eq = kv.find('=');
if (eq == std::string::npos) {
continue;
}
genEnv[kv.substr(0, eq)] = kv.substr(eq + 1);
}
if (!genEnv.empty()) {
if (auto it = genEnv.find("PATH"); it != genEnv.end()) {
mergedPath = it->second;
}
builder.appendEnv(genEnv);
}
}
}
// 2) common filesystem
{
const auto &fsPolicy = filesystemPolicy();
if (fsPolicy.allowListConfigured) {
if (!fsPolicy.allowList.empty()) {
auto allowList = fsPolicy.allowList;
builder.addExtraMounts(std::move(allowList));
}
} else if (!fsPolicy.extra.empty()) {
auto extraMounts = fsPolicy.extra;
builder.addExtraMounts(std::move(extraMounts));
}
}
// === end: merge Global->Base->App config ===
if (!environment.empty()) {
if (auto it = environment.find("PATH"); it != environment.end()) {
mergedPath = it->second;
}
builder.appendEnv(environment);
}
// === begin: command-level settings (highest priority) ===
{
std::string execName = currentAppId;
if (!execName.empty()) {
if (const json *node = pickCommandNode(mergedCfg, execName)) {
CommandSettings cs = parseCommandSettings(currentAppId, *node);
if (!cs.envKVs.empty()) {
std::map<std::string, std::string> cmdEnv;
std::string basePath;
if (auto sysPath = ::getenv("PATH")) {
basePath = sysPath;
}
auto extPathIt = environment.find("PATH");
for (const auto &kv : cs.envKVs) {
auto posp = kv.find("+=");
if (posp != std::string::npos) {
auto key = kv.substr(0, posp);
auto add = kv.substr(posp + 2);
if (key == "PATH") {
if (cmdEnv.count("PATH")) {
cmdEnv["PATH"] += ":" + add;
} else if (mergedPath) {
cmdEnv["PATH"] =
mergedPath->empty() ? add : *mergedPath + ":" + add;
} else if (extPathIt != environment.end()) {
cmdEnv["PATH"] =
extPathIt->second.empty()
? add
: extPathIt->second + ":" + add;
} else if (!basePath.empty()) {
cmdEnv["PATH"] = basePath + ":" + add;
} else {
cmdEnv["PATH"] = add;
}
} else {
qWarning() << "ignore '+=' env for key in command settings:"
<< QString::fromStdString(key);
}
continue;
}
auto eq = kv.find('=');
if (eq == std::string::npos) {
continue;
}
cmdEnv[kv.substr(0, eq)] = kv.substr(eq + 1);
}
if (!cmdEnv.empty()) {
if (auto it = cmdEnv.find("PATH"); it != cmdEnv.end()) {
mergedPath = it->second;
}
builder.appendEnv(cmdEnv);
}
}
if (!cs.mounts.empty()) builder.addExtraMounts(cs.mounts);
// TODO: when builder exposes API for entrypoint/cwd/args, apply here as well.
}
}
}
// === end: command-level settings ===
detectDisplaySystem(builder);
for (auto ctx = securityContexts.begin(); ctx != securityContexts.end(); ++ctx) {
@ -726,6 +1292,45 @@ utils::error::Result<std::filesystem::path> RunContext::getBaseLayerPath() const
return std::filesystem::path{ layerDir->absolutePath().toStdString() };
}
std::string RunContext::currentAppId() const
{
if (appLayer) {
return appLayer->getReference().id;
}
return targetId;
}
const RunContext::FilesystemPolicy &RunContext::filesystemPolicy() const
{
if (!filesystemPolicyCache) {
FilesystemPolicy policy;
auto appId = currentAppId();
std::string baseId;
if (baseLayer) {
baseId = baseLayer->getReference().id;
}
auto mergedCfg = loadMergedJsonWithBase(appId, baseId);
if (auto it = mergedCfg.find("filesystem_allow_only"); it != mergedCfg.end()) {
policy.allowListConfigured = true;
if (it->is_array()) {
policy.allowList = parseFilesystemMounts(appId, *it);
}
}
if (!policy.allowListConfigured) {
if (auto it = mergedCfg.find("filesystem"); it != mergedCfg.end()) {
policy.extra = parseFilesystemMounts(appId, *it);
}
}
filesystemPolicyCache = std::move(policy);
}
return *filesystemPolicyCache;
}
utils::error::Result<std::filesystem::path> RunContext::getRuntimeLayerPath() const
{
LINGLONG_TRACE("get runtime layer path");

View File

@ -16,6 +16,9 @@
#include <filesystem>
#include <list>
#include <optional>
#include <string>
#include <vector>
namespace linglong::runtime {
@ -65,6 +68,13 @@ struct ResolveOptions
class RunContext
{
public:
struct FilesystemPolicy
{
bool allowListConfigured{ false };
std::vector<ocppi::runtime::config::types::Mount> allowList;
std::vector<ocppi::runtime::config::types::Mount> extra;
};
RunContext(repo::OSTreeRepo &r)
: repo(r)
{
@ -83,6 +93,9 @@ public:
repo::OSTreeRepo &getRepo() const { return repo; }
std::string currentAppId() const;
const FilesystemPolicy &filesystemPolicy() const;
const std::string &getContainerId() const { return containerID; }
const std::optional<RuntimeLayer> &getBaseLayer() const { return baseLayer; }
@ -128,6 +141,7 @@ private:
std::string containerID;
std::filesystem::path bundle;
std::map<std::string, std::string> environment;
mutable std::optional<FilesystemPolicy> filesystemPolicyCache;
};
} // namespace linglong::runtime

View File

@ -12,6 +12,7 @@ pfl_add_executable(
DISABLE_INSTALL
SOURCES
# find -regex '\./src/.+\.[ch]\(pp\)?' -type f -printf '%P\n'| sort
src/common/tempdir.h
src/linglong/builder/linglong_builder_test.cpp
src/linglong/builder/source_fetcher_test.cpp
src/linglong/common/strings_test.cpp
@ -24,6 +25,8 @@ pfl_add_executable(
src/linglong/package/architecture_test.cpp
src/linglong/package/fallback_version_test.cpp
src/linglong/package/layer_packager_test.cpp
src/linglong/package_manager/action_test.cpp
src/linglong/package_manager/uab_installation_test.cpp
src/linglong/package/reference_test.cpp
src/linglong/package/semver_compare_test.cpp
src/linglong/package/semver_increment_test.cpp
@ -33,8 +36,6 @@ pfl_add_executable(
src/linglong/package/uab_file_test.cpp
src/linglong/package/version_test.cpp
src/linglong/package/versionv2_test.cpp
src/linglong/package_manager/action_test.cpp
src/linglong/package_manager/uab_installation_test.cpp
src/linglong/repo/client_factory_test.cpp
src/linglong/repo/config_test.cpp
src/linglong/repo/ostree_repo_test.cpp
@ -42,6 +43,7 @@ pfl_add_executable(
src/linglong/utils/command_test.cpp
src/linglong/utils/error/error_test.cpp
src/linglong/utils/file.cpp
src/linglong/utils/filelock_test.cpp
src/linglong/utils/gkeyfile_wrapper_test.cpp
src/linglong/utils/log.cpp
src/linglong/utils/namespce.cpp

View File

@ -7,6 +7,7 @@
#include <gtest/gtest.h>
#include "linglong/common/strings.h"
#include "linglong/utils/command/env.h"
#include "linglong/utils/error/error.h"
#include <filesystem>
@ -14,9 +15,11 @@
using namespace linglong::utils::error;
using namespace linglong::common;
using linglong::utils::command::EnvironmentVariableGuard;
TEST(Error, New)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR");
return LINGLONG_ERR("message", -1);
@ -31,6 +34,7 @@ TEST(Error, New)
TEST(Error, WrapResult)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR");
auto res = []() -> Result<void> {
@ -59,6 +63,7 @@ TEST(Error, WrapResult)
TEST(Error, WrapError)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR");
auto res = []() -> Result<void> {
@ -82,6 +87,7 @@ TEST(Error, WrapError)
TEST(Error, WarpErrorCode)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR");
return LINGLONG_ERR("app install failed", ErrorCode::AppInstallFailed);
@ -95,6 +101,7 @@ TEST(Error, WarpErrorCode)
TEST(Error, WarpStdErrorCode)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR");
std::error_code ec;
@ -112,6 +119,7 @@ TEST(Error, WarpStdErrorCode)
TEST(Error, WarpQtFile)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR");
QFile file("/not_exists_file");
@ -127,10 +135,9 @@ TEST(Error, WarpQtFile)
ASSERT_TRUE(strings::contains(msg, "No such file or directory")) << msg;
}
// 在现有的 error_test.cpp 文件末尾添加以下测试
TEST(Error, WarpGError)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR with GError");
g_autoptr(GError) gErr =
@ -147,6 +154,7 @@ TEST(Error, WarpGError)
TEST(Error, WarpSystemError)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR with std::system_error");
try {
@ -166,6 +174,7 @@ TEST(Error, WarpSystemError)
TEST(Error, WarpExceptionPtr)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR with std::exception_ptr");
std::exception_ptr eptr;
@ -202,6 +211,7 @@ TEST(Error, WarpExceptionPtr)
TEST(Error, WarpStdException)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR with std::exception");
std::invalid_argument e("Invalid argument provided");
@ -217,6 +227,7 @@ TEST(Error, WarpStdException)
TEST(Error, WarpNestedError)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
auto innerFn = []() -> Result<void> {
LINGLONG_TRACE("inner function");
@ -242,6 +253,7 @@ TEST(Error, WarpNestedError)
TEST(Error, WarpErrorWithCustomCode)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR with custom error code");
return LINGLONG_ERR("Custom error occurred", 12345);
@ -256,6 +268,7 @@ TEST(Error, WarpErrorWithCustomCode)
TEST(Error, WarpQStringMessage)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR with QString message");
QString errorMsg = QString("Error occurred at line %1").arg(__LINE__);
@ -271,6 +284,7 @@ TEST(Error, WarpQStringMessage)
TEST(Error, WarpStdStringMessage)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR with std::string message");
std::string errorMsg = "Standard string error message";
@ -286,6 +300,7 @@ TEST(Error, WarpStdStringMessage)
TEST(Error, WarpEmptyFile)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test LINGLONG_ERR with empty QFile");
QFile emptyFile;
@ -301,6 +316,7 @@ TEST(Error, WarpEmptyFile)
TEST(Error, SuccessCase)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
auto res = []() -> Result<void> {
LINGLONG_TRACE("test successful operation");
// 模拟成功操作
@ -312,6 +328,7 @@ TEST(Error, SuccessCase)
TEST(Error, ErrorCodeEnumValues)
{
EnvironmentVariableGuard guard("LINYAPS_BACKTRACE", "1");
// 测试各种错误码枚举值
auto testErrorCode = [](ErrorCode code, const std::string &description) -> Result<void> {
LINGLONG_TRACE("test error code : " + description);
@ -331,3 +348,45 @@ TEST(Error, ErrorCodeEnumValues)
ASSERT_FALSE(res3.has_value());
ASSERT_EQ(res3.error().code(), 0); // Success 错误码为0
}
TEST(Error, NoBacktrace)
{
auto res = []() -> Result<void> {
LINGLONG_TRACE("trace");
return LINGLONG_ERR("error");
}();
ASSERT_FALSE(res.has_value());
auto msg = res.error().message();
ASSERT_FALSE(strings::contains(msg, "trace")) << msg;
ASSERT_TRUE(strings::contains(msg, "error")) << msg;
}
TEST(Error, DefaultError)
{
auto res1 = []() -> Result<void> {
LINGLONG_TRACE("trace 1");
return LINGLONG_ERR("error 1", 1);
};
auto res2 = [&res1]() -> Result<void> {
LINGLONG_TRACE("trace 2");
return LINGLONG_ERR(res1());
}();
ASSERT_FALSE(res2.has_value());
auto msg = res2.error().message();
ASSERT_FALSE(strings::contains(msg, "trace")) << msg;
ASSERT_TRUE(strings::contains(msg, "error 1")) << msg;
ASSERT_EQ(res2.error().code(), 1);
auto res3 = [&res1]() -> Result<void> {
LINGLONG_TRACE("trace 3");
return LINGLONG_ERR("error 3", res1());
}();
ASSERT_FALSE(res3.has_value());
msg = res3.error().message();
ASSERT_FALSE(strings::contains(msg, "trace")) << msg;
ASSERT_TRUE(strings::contains(msg, "error 3")) << msg;
ASSERT_EQ(res3.error().code(), 1);
}

View File

@ -0,0 +1,428 @@
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "linglong/utils/error/error.h"
#include "linglong/utils/filelock.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <sys/wait.h>
#include <unistd.h>
using namespace linglong::utils::filelock;
using namespace linglong::utils::error;
namespace {
// Helper function to create a temporary file path
std::filesystem::path GetTempFilePath()
{
return std::filesystem::temp_directory_path() / ("test_filelock.lock");
}
} // namespace
// Test fixture for FileLock tests
class FileLockTest : public ::testing::Test
{
protected:
std::filesystem::path temp_path;
void SetUp() override
{
temp_path = GetTempFilePath();
// Ensure the file does not exist initially
std::filesystem::remove(temp_path);
}
void TearDown() override { std::filesystem::remove(temp_path); }
};
// Test creating FileLock with create_if_missing = true
TEST_F(FileLockTest, CreateWithMissingFile)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
EXPECT_TRUE(std::filesystem::exists(temp_path));
const auto &lock = std::move(result).value();
EXPECT_EQ(lock.type(), LockType::Read); // Default type
EXPECT_FALSE(lock.isLocked());
}
// Test creating FileLock without create_if_missing when file missing
TEST_F(FileLockTest, CreateWithoutMissingFileFails)
{
auto result = FileLock::create(temp_path, false);
ASSERT_FALSE(result);
EXPECT_THAT(result.error().message(),
::testing::HasSubstr("open file failed: No such file or directory"));
}
// Test creating FileLock on existing file
TEST_F(FileLockTest, CreateOnExistingFile)
{
std::ofstream file(temp_path);
file.close();
auto result = FileLock::create(temp_path, false);
ASSERT_TRUE(result);
const auto &lock = std::move(result).value();
EXPECT_FALSE(lock.isLocked());
}
// Test lock and unlock for Read lock
TEST_F(FileLockTest, LockUnlockRead)
{
auto result = FileLock::create(temp_path);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
auto lock_result = lock.lock(LockType::Read);
ASSERT_TRUE(lock_result) << lock_result.error().message();
EXPECT_TRUE(lock.isLocked());
EXPECT_EQ(lock.type(), LockType::Read);
auto unlock_result = lock.unlock();
ASSERT_TRUE(unlock_result);
EXPECT_FALSE(lock.isLocked());
}
// Test lock and unlock for Write lock
TEST_F(FileLockTest, LockUnlockWrite)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
auto lock_result = lock.lock(LockType::Write);
ASSERT_TRUE(lock_result);
EXPECT_TRUE(lock.isLocked());
EXPECT_EQ(lock.type(), LockType::Write);
auto unlock_result = lock.unlock();
ASSERT_TRUE(unlock_result);
EXPECT_FALSE(lock.isLocked());
}
// Test tryLock success
TEST_F(FileLockTest, TryLockSuccess)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
auto try_result = lock.tryLock(LockType::Write);
ASSERT_TRUE(try_result);
EXPECT_TRUE(*try_result);
EXPECT_TRUE(lock.isLocked());
EXPECT_TRUE(lock.unlock());
}
// Test tryLock failure when already locked by another process
TEST_F(FileLockTest, TryLockFailureInterProcess)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock1 = std::move(result).value();
EXPECT_TRUE(lock1.lock(LockType::Write));
EXPECT_TRUE(lock1.isLocked());
const pid_t pid = fork();
if (pid == 0) { // Child process
auto result_child = FileLock::create(temp_path, false);
if (!result_child) {
_exit(1);
}
auto lock_child = std::move(result_child).value();
auto try_result = lock_child.tryLock(LockType::Write);
if (!try_result || *try_result) {
_exit(1);
}
_exit(0);
} else if (pid > 0) { // Parent
int status{ 0 };
EXPECT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_EQ(WEXITSTATUS(status), 0);
} else {
FAIL() << "Fork failed";
}
}
// Test tryLockFor with timeout success
TEST_F(FileLockTest, TryLockForSuccess)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
auto try_for_result = lock.tryLockFor(LockType::Write, std::chrono::milliseconds(100));
ASSERT_TRUE(try_for_result);
EXPECT_TRUE(*try_for_result);
EXPECT_TRUE(lock.isLocked());
}
// Test tryLockFor with timeout failure
TEST_F(FileLockTest, TryLockForFailure)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock1 = std::move(result).value();
EXPECT_TRUE(lock1.lock(LockType::Write));
EXPECT_TRUE(lock1.isLocked());
const pid_t pid = fork();
if (pid == 0) { // Child
auto result_child = FileLock::create(temp_path, false);
if (!result_child) {
_exit(1);
}
auto lock_child = std::move(result_child).value();
auto try_for_result =
lock_child.tryLockFor(LockType::Write, std::chrono::milliseconds(100));
if (!try_for_result || *try_for_result) {
_exit(1);
}
_exit(0);
} else if (pid > 0) {
int status{ 0 };
EXPECT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_EQ(WEXITSTATUS(status), 0);
} else {
FAIL() << "Fork failed";
}
}
// Test relock from Read to Write
TEST_F(FileLockTest, RelockReadToWrite)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
EXPECT_TRUE(lock.lock(LockType::Read));
EXPECT_EQ(lock.type(), LockType::Read);
auto relock_result = lock.relock(LockType::Write);
ASSERT_TRUE(relock_result);
EXPECT_EQ(lock.type(), LockType::Write);
EXPECT_TRUE(lock.isLocked());
}
// Test relock from Write to Read
TEST_F(FileLockTest, RelockWriteToRead)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
EXPECT_TRUE(lock.lock(LockType::Write));
EXPECT_EQ(lock.type(), LockType::Write);
auto relock_result = lock.relock(LockType::Read);
ASSERT_TRUE(relock_result);
EXPECT_EQ(lock.type(), LockType::Read);
EXPECT_TRUE(lock.isLocked());
}
// Test relock when not locked fails
TEST_F(FileLockTest, RelockNotLocked)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
auto relock_result = lock.relock(LockType::Write);
// Since not locked, relock should just return OK if same type, but test behavior
// Actually, relock assumes locked, but code checks if type == new_type, returns OK
// But to test properly:
EXPECT_TRUE(
relock_result); // Since type_ is Read by default, changing to Write would attempt to lock
// Wait, relock is for changing type while locked.
// If not locked, it shouldn't be called, but let's see code: relock doesn't check if locked.
// It just sets the new flock.
// So if not locked, it would effectively lock it.
auto lock_result = lock.lock(LockType::Read);
ASSERT_TRUE(lock_result);
auto relock = lock.relock(LockType::Read);
ASSERT_TRUE(relock);
}
// Test multiple read locks possible (shared)
TEST_F(FileLockTest, MultipleReadLocks)
{
auto result1 = FileLock::create(temp_path);
ASSERT_TRUE(result1);
auto lock1 = std::move(result1).value();
EXPECT_TRUE(lock1.lock(LockType::Read));
const auto pid = ::fork();
if (pid == 0) {
auto result2 = FileLock::create(temp_path);
if (!result2) {
_exit(1);
}
auto lock2 = std::move(result2).value();
if (auto ret = lock2.lock(LockType::Read); !ret) {
_exit(2);
}
_exit(0);
} else if (pid > 0) {
int status{ 0 };
EXPECT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_EQ(WEXITSTATUS(status), 0);
} else {
FAIL() << "Fork failed";
}
}
// Test write lock exclusive
TEST_F(FileLockTest, WriteLockExclusive)
{
auto result1 = FileLock::create(temp_path);
ASSERT_TRUE(result1);
auto lock1 = std::move(result1).value();
EXPECT_TRUE(lock1.lock(LockType::Write));
const auto pid = ::fork();
if (pid == 0) {
auto result2 = FileLock::create(temp_path, false);
if (!result2) {
_exit(1);
}
auto lock2 = std::move(result2).value();
auto try_read = lock2.tryLock(LockType::Read);
if (!try_read || *try_read) {
_exit(2);
}
auto try_write = lock2.tryLock(LockType::Write);
if (!try_write || *try_write) {
_exit(3);
}
_exit(0);
} else if (pid > 0) {
int status{ 0 };
EXPECT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_EQ(WEXITSTATUS(status), 0);
} else {
FAIL() << "Fork failed";
}
}
// Test behavior after fork
TEST_F(FileLockTest, LockAfterFork)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
EXPECT_TRUE(lock.lock(LockType::Write));
const pid_t pid = fork();
if (pid == 0) { // Child
// In child, lock should not be considered locked because pid changed
if (lock.isLocked()) {
_exit(1);
}
auto lock_result = lock.lock(LockType::Write);
if (lock_result) { // Should fail because "cannot operate on lock from forked process"
_exit(2);
}
_exit(0);
} else if (pid > 0) {
int status{ 0 };
EXPECT_EQ(waitpid(pid, &status, 0), pid);
EXPECT_EQ(WEXITSTATUS(status), 0);
EXPECT_TRUE(lock.isLocked()); // Parent still locked
} else {
FAIL() << "Fork failed";
}
}
// Test cannot create duplicate lock in same process
TEST_F(FileLockTest, NoDuplicateLockSameProcess)
{
auto result1 = FileLock::create(temp_path, true);
ASSERT_TRUE(result1);
auto result2 = FileLock::create(temp_path, false);
ASSERT_FALSE(result2);
EXPECT_THAT(result2.error().message(),
::testing::HasSubstr("process already holds a lock on file"));
}
// Test move constructor
TEST_F(FileLockTest, MoveConstructor)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock1 = std::move(result).value();
EXPECT_TRUE(lock1.lock(LockType::Write));
auto lock2(std::move(lock1));
EXPECT_TRUE(lock2.isLocked());
EXPECT_FALSE(lock1.isLocked()); // Moved from
}
// Test move assignment
TEST_F(FileLockTest, MoveAssignment)
{
auto result1 = FileLock::create(temp_path, true);
ASSERT_TRUE(result1);
auto lock1 = std::move(result1).value();
EXPECT_TRUE(lock1.lock(LockType::Write));
const std::filesystem::path other_path =
std::filesystem::temp_directory_path() / ("other_" + std::to_string(::getpid()) + ".lock");
auto result2 = FileLock::create(other_path, true);
ASSERT_TRUE(result2);
auto lock2 = std::move(result2).value();
lock2 = std::move(lock1);
EXPECT_TRUE(lock2.isLocked());
EXPECT_EQ(lock2.type(), LockType::Write);
EXPECT_FALSE(lock1.isLocked());
}
// Test tryLock when already locked same type
TEST_F(FileLockTest, TryLockAlreadyLockedSameType)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
EXPECT_TRUE(lock.lock(LockType::Read));
auto try_result = lock.tryLock(LockType::Read);
ASSERT_TRUE(try_result);
}
// Test lock when already locked different type
TEST_F(FileLockTest, LockAlreadyLockedDiffType)
{
auto result = FileLock::create(temp_path, true);
ASSERT_TRUE(result);
auto lock = std::move(result).value();
EXPECT_TRUE(lock.lock(LockType::Read));
auto lock_result = lock.lock(LockType::Write);
ASSERT_FALSE(lock_result);
EXPECT_THAT(lock_result.error().message(),
::testing::HasSubstr("use relock to change lock type"));
}

View File

@ -769,11 +769,12 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
.type = "tmpfs" });
auto containerHome = homePath->string();
homeMount->emplace_back(Mount{ .destination = containerHome,
.options = string_list{ "rbind" },
.source = *homePath,
.type = "bind" });
if (!skipHostHome) {
homeMount->emplace_back(Mount{ .destination = containerHome,
.options = string_list{ "rbind" },
.source = *homePath,
.type = "bind" });
}
environment["HOME"] = containerHome;
auto mountDir = [this](const std::filesystem::path &hostDir, const std::string &containerDir) {
@ -803,7 +804,7 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
value ? std::filesystem::path{ value } : *homePath / ".local" / "share";
std::string containerDataHome = containerHome + "/.local/share";
if (XDG_DATA_HOME != containerDataHome) {
if (!mountDir(XDG_DATA_HOME, containerDataHome)) {
if (!skipHostHome && !mountDir(XDG_DATA_HOME, containerDataHome)) {
error_.reason = XDG_DATA_HOME.string() + " can't be mount";
error_.code = BUILD_MOUNT_HOME_ERROR;
return false;
@ -834,7 +835,7 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
}
std::string containerConfigHome = containerHome + "/.config";
if (XDGConfigHome != containerConfigHome) {
if (!mountDir(XDGConfigHome, containerConfigHome)) {
if (!skipHostHome && !mountDir(XDGConfigHome, containerConfigHome)) {
error_.reason = XDGConfigHome.string() + " can't be mount";
error_.code = BUILD_MOUNT_HOME_ERROR;
return false;
@ -852,7 +853,7 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
}
std::string containerCacheHome = containerHome + "/.cache";
if (XDGCacheHome != containerCacheHome) {
if (!mountDir(XDGCacheHome, containerCacheHome)) {
if (!skipHostHome && !mountDir(XDGCacheHome, containerCacheHome)) {
error_.reason = XDGCacheHome.string() + " can't be mount";
error_.code = BUILD_MOUNT_HOME_ERROR;
return false;
@ -869,7 +870,7 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
}
std::string containerStateHome = containerHome + "/.local/state";
if (XDG_STATE_HOME != containerStateHome) {
if (!mountDir(XDG_STATE_HOME, containerStateHome)) {
if (!skipHostHome && !mountDir(XDG_STATE_HOME, containerStateHome)) {
error_.reason = XDG_STATE_HOME.string() + " can't be mount";
error_.code = BUILD_MOUNT_HOME_ERROR;
return false;
@ -881,10 +882,12 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
auto hostSystemdUserDir = XDG_CONFIG_HOME / "systemd" / "user";
if ((XDG_CONFIG_HOME != containerConfigHome)
&& std::filesystem::exists(hostSystemdUserDir, ec)) {
homeMount->emplace_back(Mount{ .destination = containerConfigHome + "/systemd/user",
if (!skipHostHome) {
homeMount->emplace_back(Mount{ .destination = containerConfigHome + "/systemd/user",
.options = string_list{ "rbind" },
.source = hostSystemdUserDir,
.type = "bind" });
}
}
// FIXME: Many applications get configurations from dconf, so we expose dconf to all
@ -893,15 +896,18 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
auto hostUserDconfPath = XDG_CONFIG_HOME / "dconf";
if ((XDG_CONFIG_HOME != containerConfigHome)
&& std::filesystem::exists(hostUserDconfPath, ec)) {
homeMount->emplace_back(Mount{ .destination = containerConfigHome + "/dconf",
if (!skipHostHome) {
homeMount->emplace_back(Mount{ .destination = containerConfigHome + "/dconf",
.options = string_list{ "rbind" },
.source = hostUserDconfPath,
.type = "bind" });
}
}
// for dde application theme
auto hostDDEApiPath = XDG_CACHE_HOME / "deepin" / "dde-api";
if ((XDG_CACHE_HOME != containerCacheHome) && std::filesystem::exists(hostDDEApiPath, ec)) {
if (!skipHostHome && (XDG_CACHE_HOME != containerCacheHome)
&& std::filesystem::exists(hostDDEApiPath, ec)) {
homeMount->emplace_back(Mount{ .destination = containerCacheHome + "/deepin/dde-api",
.options = string_list{ "rbind" },
.source = hostDDEApiPath,
@ -910,7 +916,8 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
// for xdg-user-dirs
auto XDGUserDirs = XDG_CONFIG_HOME / "user-dirs.dirs";
if ((XDG_CONFIG_HOME != containerConfigHome) && std::filesystem::exists(XDGUserDirs, ec)) {
if (!skipHostHome && (XDG_CONFIG_HOME != containerConfigHome)
&& std::filesystem::exists(XDGUserDirs, ec)) {
homeMount->push_back(Mount{ .destination = containerConfigHome + "/user-dirs.dirs",
.options = string_list{ "rbind" },
.source = XDGUserDirs,
@ -918,7 +925,8 @@ bool ContainerCfgBuilder::buildMountHome() noexcept
}
auto XDGUserLocale = XDG_CONFIG_HOME / "user-dirs.locale";
if ((XDG_CONFIG_HOME != containerConfigHome) && std::filesystem::exists(XDGUserLocale, ec)) {
if (!skipHostHome && (XDG_CONFIG_HOME != containerConfigHome)
&& std::filesystem::exists(XDGUserLocale, ec)) {
homeMount->push_back(Mount{ .destination = containerConfigHome + "/user-dirs.locale",
.options = string_list{ "rbind" },
.source = XDGUserLocale,

View File

@ -143,6 +143,11 @@ public:
ContainerCfgBuilder &bindHostRoot() noexcept;
ContainerCfgBuilder &bindHostStatics() noexcept;
ContainerCfgBuilder &bindHome(std::filesystem::path hostHome) noexcept;
ContainerCfgBuilder &disableHostHomeBind() noexcept
{
skipHostHome = true;
return *this;
}
ContainerCfgBuilder &bindXOrgSocket(const std::filesystem::path &socket) noexcept;
ContainerCfgBuilder &bindXAuthFile(const std::filesystem::path &authFile) noexcept;
@ -304,6 +309,7 @@ private:
// home dir
std::optional<std::filesystem::path> homePath;
std::optional<std::vector<ocppi::runtime::config::types::Mount>> homeMount;
bool skipHostHome{ false };
// private dir
std::filesystem::path privatePath;

View File

@ -9,10 +9,11 @@ pfl_add_library(
STATIC
SOURCES
# find -regex '\.\/*.+\.[ch]\(pp\)?\(.in\)?' -type f -printf '%P\n'| sort
src/linglong/utils/command/env.cpp
src/linglong/utils/command/env.h
src/linglong/utils/bash_command_helper.h
src/linglong/utils/command/cmd.cpp
src/linglong/utils/command/cmd.h
src/linglong/utils/command/env.cpp
src/linglong/utils/command/env.h
src/linglong/utils/command/ocppi-helper.cpp
src/linglong/utils/command/ocppi-helper.h
src/linglong/utils/dbus/log.cpp
@ -25,11 +26,18 @@ pfl_add_library(
src/linglong/utils/error/details/error_impl.h
src/linglong/utils/error/error.cpp
src/linglong/utils/error/error.h
src/linglong/utils/file.cpp
src/linglong/utils/file.h
src/linglong/utils/filelock.cpp
src/linglong/utils/filelock.h
src/linglong/utils/finally/finally.cpp
src/linglong/utils/finally/finally.h
src/linglong/utils/gettext.h
src/linglong/utils/gkeyfile_wrapper.h
src/linglong/utils/global/initialize.cpp
src/linglong/utils/global/initialize.h
src/linglong/utils/hooks.cpp
src/linglong/utils/hooks.h
src/linglong/utils/log/formatter.cpp
src/linglong/utils/log/formatter.h
src/linglong/utils/log/log.cpp
@ -46,14 +54,8 @@ pfl_add_library(
src/linglong/utils/serialize/yaml.h
src/linglong/utils/transaction.cpp
src/linglong/utils/transaction.h
src/linglong/utils/file.cpp
src/linglong/utils/file.h
src/linglong/utils/xdg/directory.h
src/linglong/utils/xdg/directory.cpp
src/linglong/utils/gkeyfile_wrapper.h
src/linglong/utils/hooks.cpp
src/linglong/utils/hooks.h
src/linglong/utils/bash_command_helper.h
src/linglong/utils/xdg/directory.h
COMPILE_FEATURES
PUBLIC
cxx_std_17

View File

@ -6,7 +6,10 @@
#pragma once
#include <fmt/format.h>
#include <memory>
#include <optional>
#include <string>
#include <utility>
@ -18,14 +21,17 @@ public:
ErrorImpl(const char *file,
int line,
int code,
std::string msg,
std::string trace_msg,
std::optional<std::string> msg,
std::unique_ptr<ErrorImpl> cause = nullptr)
: _code(code)
, _msg(std::move(msg))
, _file(file)
, _line(line)
, cause(std::move(cause))
, _trace_msg(std::move(trace_msg))
{
backtrace = (getenv("LINYAPS_BACKTRACE") != nullptr);
}
[[nodiscard]] auto code() const -> int { return _code; };
@ -37,17 +43,31 @@ public:
if (!msg.empty()) {
msg += "\n";
}
msg += err->_file + ":" + std::to_string(err->_line) + " " + err->_msg;
if (backtrace) {
auto trace_msg = err->_trace_msg;
if (err->_msg) {
trace_msg += ": " + *err->_msg;
}
msg += fmt::format("{}:{} {}", err->_file, err->_line, trace_msg);
} else {
if (err->_msg) {
msg = *err->_msg;
break;
}
}
}
return msg;
}
private:
int _code;
std::string _msg;
std::optional<std::string> _msg;
std::string _file;
int _line;
std::unique_ptr<ErrorImpl> cause;
std::string _trace_msg;
bool backtrace = false;
};
} // namespace linglong::utils::error::details

View File

@ -83,7 +83,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
static_cast<int>(code),
trace_msg + ": " + msg.toStdString(),
trace_msg,
msg.toStdString(),
nullptr));
}
@ -96,7 +97,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
static_cast<int>(code),
trace_msg + ": " + msg,
trace_msg,
msg,
nullptr));
}
@ -109,7 +111,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
static_cast<int>(code),
trace_msg + ": " + msg,
trace_msg,
msg,
nullptr));
}
@ -120,7 +123,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
code,
trace_msg + ": " + msg.toStdString(),
trace_msg,
msg.toStdString(),
nullptr));
}
@ -131,7 +135,7 @@ public:
int code = -1) -> Error
{
return Error(
std::make_unique<details::ErrorImpl>(file, line, code, trace_msg + ": " + msg, nullptr));
std::make_unique<details::ErrorImpl>(file, line, code, trace_msg, msg, nullptr));
}
static auto
@ -139,7 +143,7 @@ public:
-> Error
{
return Error(
std::make_unique<details::ErrorImpl>(file, line, code, trace_msg + ": " + msg, nullptr));
std::make_unique<details::ErrorImpl>(file, line, code, trace_msg, msg, nullptr));
}
static auto Err(const char *file,
@ -148,12 +152,13 @@ public:
const QString &msg,
const QFile &qfile) -> Error
{
return Error(std::make_unique<details::ErrorImpl>(
file,
line,
qfile.error(),
trace_msg + ": " + msg.toStdString() + ": " + qfile.errorString().toStdString(),
nullptr));
return Error(std::make_unique<details::ErrorImpl>(file,
line,
qfile.error(),
trace_msg,
msg.toStdString() + ": "
+ qfile.errorString().toStdString(),
nullptr));
}
static auto Err(const char *file, int line, const std::string &trace_msg, const QFile &qfile)
@ -162,8 +167,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
qfile.error(),
trace_msg + ": "
+ qfile.fileName().toStdString() + ": "
trace_msg,
qfile.fileName().toStdString() + ": "
+ qfile.errorString().toStdString(),
nullptr));
}
@ -174,16 +179,17 @@ public:
std::exception_ptr err,
int code = -1) -> Error
{
std::string what = trace_msg + ": ";
std::string what;
try {
std::rethrow_exception(std::move(err));
} catch (const std::exception &e) {
what += e.what();
what = e.what();
} catch (...) {
what += "unknown";
what = "unknown";
}
return Error(std::make_unique<details::ErrorImpl>(file, line, code, what, nullptr));
return Error(
std::make_unique<details::ErrorImpl>(file, line, code, trace_msg, what, nullptr));
}
static auto Err(const char *file,
@ -193,7 +199,7 @@ public:
std::exception_ptr err,
int code = -1) -> Error
{
std::string what = trace_msg + ": " + msg.toStdString() + ": ";
std::string what = msg.toStdString() + ": ";
try {
std::rethrow_exception(std::move(err));
} catch (const std::exception &e) {
@ -202,18 +208,15 @@ public:
what += "unknown";
}
return Error(std::make_unique<details::ErrorImpl>(file, line, code, what, nullptr));
return Error(
std::make_unique<details::ErrorImpl>(file, line, code, trace_msg, what, nullptr));
}
static auto
Err(const char *file, int line, const std::string &trace_msg, const std::exception &e) -> Error
{
return Error(std::make_unique<details::ErrorImpl>(file,
line,
-1,
trace_msg + ": " + e.what(),
nullptr));
return Error(
std::make_unique<details::ErrorImpl>(file, line, -1, trace_msg, e.what(), nullptr));
}
static auto Err(const char *file,
@ -223,9 +226,10 @@ public:
const std::exception &e,
int code = -1) -> Error
{
std::string what = trace_msg + ": " + msg.toStdString() + ": " + e.what();
std::string what = msg.toStdString() + ": " + e.what();
return Error(std::make_unique<details::ErrorImpl>(file, line, code, what, nullptr));
return Error(
std::make_unique<details::ErrorImpl>(file, line, code, trace_msg, what, nullptr));
}
static auto Err(const char *file,
@ -281,7 +285,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
cause.error().code(),
trace_msg + ": " + msg.toStdString(),
trace_msg,
msg.toStdString(),
std::move(cause.error().pImpl)));
}
@ -297,7 +302,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
cause.error().code(),
trace_msg + ": " + msg,
trace_msg,
msg,
std::move(cause.error().pImpl)));
}
@ -313,7 +319,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
cause.error().code(),
trace_msg + ": " + msg,
trace_msg,
msg,
std::move(cause.error().pImpl)));
}
@ -329,6 +336,7 @@ public:
line,
cause.error().code(),
trace_msg,
std::nullopt,
std::move(cause.error().pImpl)));
}
@ -341,7 +349,8 @@ public:
return Error(std::make_unique<details::ErrorImpl>(file,
line,
cause.code(),
trace_msg + ": " + msg,
trace_msg,
msg,
std::move(cause.pImpl)));
}
@ -353,6 +362,7 @@ public:
line,
cause.code(),
trace_msg,
std::nullopt,
std::move(cause.pImpl)));
}

View File

@ -0,0 +1,287 @@
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#include "linglong/utils/filelock.h"
#include "linglong/common/constants.h"
#include "linglong/utils/finally/finally.h"
#include "linglong/utils/log/log.h"
#include <fmt/format.h>
#include <filesystem>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
namespace linglong::utils::filelock {
pid_t FileLock::pid() const noexcept
{
return pid_;
}
utils::error::Result<FileLock> FileLock::create(std::filesystem::path path,
bool create_if_missing) noexcept
{
LINGLONG_TRACE("create file lock");
auto cur_pid = ::getpid();
auto &[global_pid, locked_paths] = process_locked_paths;
if (global_pid != cur_pid) {
global_pid = cur_pid;
locked_paths.clear();
}
std::error_code ec;
auto abs_path = std::filesystem::absolute(path, ec);
if (ec) {
return LINGLONG_ERR(fmt::format("canonicalize path {} error: {}", path, ec.message()));
}
if (locked_paths.find(abs_path) != locked_paths.end()) {
return LINGLONG_ERR(fmt::format("process already holds a lock on file {}", abs_path));
}
unsigned int flags = O_RDWR | O_CLOEXEC | O_NOFOLLOW;
if (create_if_missing) {
flags |= O_CREAT;
}
auto fd = ::open(abs_path.c_str(), flags, default_file_mode);
if (fd < 0) {
return LINGLONG_ERR(fmt::format("open file failed: {}", ::strerror(errno)));
}
locked_paths[abs_path] = true;
FileLock lock(fd, std::move(abs_path));
return lock;
}
FileLock::~FileLock() noexcept
{
if (isLocked()) {
auto ret = unlock();
if (!ret) {
LogW("unlock file failed: {}", ret.error());
}
}
if (fd > 0 && ::close(fd) < 0) {
LogW("close file failed: {}", ::strerror(errno));
}
fd = -1;
auto it = process_locked_paths.second.find(path);
if (it != process_locked_paths.second.end()) {
process_locked_paths.second.erase(it);
}
}
FileLock::FileLock(int fd, std::filesystem::path path) noexcept
: fd(fd)
, path(std::move(path))
{
}
FileLock::FileLock(FileLock &&other) noexcept
: type_(other.type_)
, fd(other.fd)
, path(std::move(other.path))
{
if (other.pid_ != pid()) {
LogF("move lock to different process");
}
locked.store(other.locked.load(std::memory_order_relaxed), std::memory_order_relaxed);
other.locked.store(false, std::memory_order_relaxed);
other.fd = -1;
}
FileLock &FileLock::operator=(FileLock &&other) noexcept
{
if (this == &other) {
return *this;
}
if (other.pid_ != pid()) {
LogF("move lock to different process");
}
fd = other.fd;
path = std::move(other.path);
type_ = other.type_;
locked.store(other.locked.load(std::memory_order_relaxed), std::memory_order_relaxed);
other.locked.store(false, std::memory_order_relaxed);
other.fd = -1;
return *this;
}
utils::error::Result<void> FileLock::lockCheck() const noexcept
{
LINGLONG_TRACE("validating file lock");
if (pid_ != ::getpid()) {
return LINGLONG_ERR("cannot operate on lock from forked process");
}
return LINGLONG_OK;
}
utils::error::Result<void> FileLock::lock(LockType type) noexcept
{
LINGLONG_TRACE("lock file");
auto ret = lockCheck();
if (!ret) {
return LINGLONG_ERR(ret);
}
if (isLocked()) {
if (type == type_) {
return LINGLONG_OK;
}
return LINGLONG_ERR(
fmt::format("use relock to change lock type from {} to {}", type_, type));
}
struct flock fl{};
fl.l_type = (type == LockType::Write) ? F_WRLCK : F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
while (true) {
if (::fcntl(fd, F_SETLKW, &fl) == 0) {
type_ = type;
locked.store(true, std::memory_order_relaxed);
return LINGLONG_OK;
}
if (errno == EINTR) {
continue;
}
return LINGLONG_ERR(fmt::format("failed to lock file {}: {}", path, ::strerror(errno)));
}
}
utils::error::Result<bool> FileLock::tryLock(LockType type) noexcept
{
LINGLONG_TRACE("try lock file");
auto ret = lockCheck();
if (!ret) {
return LINGLONG_ERR(ret);
}
if (isLocked()) {
if (type == type_) {
return LINGLONG_OK;
}
return LINGLONG_ERR(
fmt::format("use relock to change lock type from {} to {}", type_, type));
}
struct flock fl{};
fl.l_type = (type == LockType::Write) ? F_WRLCK : F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
while (true) {
if (::fcntl(fd, F_SETLK, &fl) == 0) {
type_ = type;
locked.store(true, std::memory_order_relaxed);
return true;
}
if (errno == EINTR) {
continue;
}
if (errno == EACCES || errno == EAGAIN) {
return false;
}
return LINGLONG_ERR(fmt::format("failed to lock file {}: {}", path, ::strerror(errno)));
}
}
utils::error::Result<void> FileLock::unlock() noexcept
{
LINGLONG_TRACE("unlock file");
auto ret = lockCheck();
if (!ret) {
return LINGLONG_ERR(ret);
}
if (!isLocked()) {
return LINGLONG_OK;
}
struct flock fl{};
fl.l_type = F_UNLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
while (true) {
if (::fcntl(fd, F_SETLK, &fl) == 0) {
locked.store(false, std::memory_order_relaxed);
return LINGLONG_OK;
}
if (errno == EINTR) {
continue;
}
return LINGLONG_ERR(fmt::format("failed to unlock file {}: {}", path, ::strerror(errno)));
}
}
utils::error::Result<void> FileLock::relock(LockType new_type) noexcept
{
LINGLONG_TRACE("upgrade lock type");
if (type_ == new_type) {
return LINGLONG_OK;
}
auto ret = lockCheck();
if (!ret) {
return LINGLONG_ERR(ret);
}
struct flock fl{};
fl.l_type = (new_type == LockType::Write) ? F_WRLCK : F_RDLCK;
fl.l_whence = SEEK_SET;
fl.l_start = 0;
fl.l_len = 0;
while (true) {
if (::fcntl(fd, F_SETLKW, &fl) == 0) {
type_ = new_type;
return LINGLONG_OK;
}
if (errno == EINTR) {
continue;
}
return LINGLONG_ERR(fmt::format("failed to relock file {}: {}", path, ::strerror(errno)));
}
}
bool FileLock::isLocked() const noexcept
{
return locked.load(std::memory_order_relaxed) && pid() == ::getpid();
}
} // namespace linglong::utils::filelock

View File

@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later
#pragma once
#include "linglong/utils/error/error.h"
#include <fmt/format.h>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <thread>
#include <unordered_map>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
namespace linglong::utils::filelock {
enum class LockType : uint8_t {
Read,
Write,
};
class FileLock
{
public:
static utils::error::Result<FileLock> create(std::filesystem::path path,
bool create_if_missing = true) noexcept;
~FileLock() noexcept;
FileLock(const FileLock &) = delete;
FileLock &operator=(const FileLock &) = delete;
FileLock(FileLock &&) noexcept;
FileLock &operator=(FileLock &&) noexcept;
utils::error::Result<void> lock(LockType type) noexcept;
utils::error::Result<bool> tryLock(LockType type) noexcept;
template <typename Rep, typename Period>
utils::error::Result<bool>
tryLockFor(LockType type,
const std::chrono::duration<Rep, Period> &timeout = std::chrono::milliseconds(100),
const std::chrono::milliseconds &poll_interval = std::chrono::milliseconds(10))
{
LINGLONG_TRACE("try lock with timeout");
auto deadline = std::chrono::steady_clock::now() + timeout;
while (std::chrono::steady_clock::now() < deadline) {
auto ret = tryLock(type);
if (!ret) {
return LINGLONG_ERR(ret);
}
if (*ret) {
return true;
}
std::this_thread::sleep_for(poll_interval);
}
return false;
}
utils::error::Result<void> relock(LockType new_type) noexcept;
utils::error::Result<void> unlock() noexcept;
[[nodiscard]] LockType type() const noexcept { return type_; }
[[nodiscard]] bool isLocked() const noexcept;
[[nodiscard]] pid_t pid() const noexcept;
private:
[[nodiscard]] utils::error::Result<void> lockCheck() const noexcept;
FileLock(int fd, std::filesystem::path path) noexcept;
pid_t pid_{ ::getpid() };
LockType type_{ LockType::Read };
std::atomic_bool locked{ false };
int fd{ -1 };
std::filesystem::path path;
static inline std::pair<pid_t, std::unordered_map<std::string, bool>> process_locked_paths{
::getpid(), {}
};
};
} // namespace linglong::utils::filelock
template <>
struct fmt::formatter<linglong::utils::filelock::LockType>
{
constexpr auto parse(fmt::format_parse_context &ctx) { return ctx.begin(); }
template <typename FormatContext>
auto format(const linglong::utils::filelock::LockType &p, FormatContext &ctx) const
{
const auto *msg = p == linglong::utils::filelock::LockType::Read ? "read" : "write";
return fmt::format_to(ctx.out(), "{}", msg);
}
};