From b54611f619f8a187df47ed433c57955c44ed0f28 Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 13:26:58 +0800 Subject: [PATCH 1/5] Add validated TOML application manifests --- 3rdparty/README.md | 28 +++ CMakeLists.txt | 12 ++ build/conf/sample1.app.toml | 10 + docs/service-manifests.md | 80 ++++++++ docs/service-manifests_CN.md | 80 ++++++++ include/ark/application_manifest.hpp | 37 ++++ include/ark/plugin_manager.hpp | 2 +- src/app/CMakeLists.txt | 1 + src/app/main.cpp | 42 +---- src/config/CMakeLists.txt | 16 ++ src/config/application_manifest.cpp | 264 +++++++++++++++++++++++++++ tests/core/CMakeLists.txt | 12 ++ tests/core/README.md | 5 + tests/core/README_CN.md | 4 + tests/core/application_manifest.cpp | 263 ++++++++++++++++++++++++++ tests/core/templates.cpp | 8 + 16 files changed, 830 insertions(+), 34 deletions(-) create mode 100644 build/conf/sample1.app.toml create mode 100644 docs/service-manifests.md create mode 100644 docs/service-manifests_CN.md create mode 100644 include/ark/application_manifest.hpp create mode 100644 src/config/CMakeLists.txt create mode 100644 src/config/application_manifest.cpp create mode 100644 tests/core/application_manifest.cpp diff --git a/3rdparty/README.md b/3rdparty/README.md index 4c4411c61..63215edf6 100755 --- a/3rdparty/README.md +++ b/3rdparty/README.md @@ -5,6 +5,7 @@ | [Protobuf](https://github.com/protocolbuffers/protobuf) | | vcpkg baseline | | [arknet](https://github.com/OpenArkStudio/arknet) | | v0.1.0 | | [RapidXML](https://rapidxml.sourceforge.net/) | | 1.13 | +| [toml++](https://github.com/marzer/tomlplusplus) (application manifests, MIT) | | v3.4.0 fallback | | [args](https://github.com/Taywee/args/releases/tag/6.2.2) | | 6.2.2 | | [spdlog](https://github.com/gabime/spdlog/releases/tag/v1.17.0) | v1.x | v1.17.0 | | [json](https://github.com/nlohmann/json/releases/download/v3.9.1/json.hpp) | v3.9.1 | | @@ -20,3 +21,30 @@ arknet is maintained in its own repository and acquired by CMake at the release tag. RapidXML is supplied by the package manager; the unavailable ArkNX fork is no longer a submodule. The vcpkg manifest pins the package baseline. See [network builds and regression tests](../docs/networking.md). + +The optional `ark::config` target privately uses toml++ under its +[MIT license](https://github.com/marzer/tomlplusplus/blob/v3.4.0/LICENSE). +CMake prefers an installed `tomlplusplus` 3.4+ package and otherwise fetches commit +`30172438cee64926dc41fdd9c11fb3ba5b2ba9de` (v3.4.0); no parser source is vendored. +See [application manifests](../docs/service-manifests.md) for the configuration API. + +### toml++ license notice + +```text +MIT License + +Copyright (c) Mark Gillard + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 7419bb0eb..0e1f9889f 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,6 +75,14 @@ endif() option(ARK_BUILD_TESTS "Enable build tests" ON) option(ARK_BUILD_CORE_TESTS "Build core C++20 regressions" ${ARK_BUILD_TESTS}) option(ARK_BUILD_EXAMPLES "Enable build examples" ON) +set(ark_config_default OFF) +if(ARK_BUILD_APPLICATIONS OR ARK_BUILD_CORE_TESTS) + set(ark_config_default ON) +endif() +option(ARK_BUILD_CONFIG "Build the application manifest reader" ${ark_config_default}) +if(ARK_BUILD_APPLICATIONS AND NOT ARK_BUILD_CONFIG) + message(FATAL_ERROR "ARK_BUILD_APPLICATIONS requires ARK_BUILD_CONFIG") +endif() #--------------------------------------------------------------------------------------- # project switches @@ -201,6 +209,10 @@ endif() find_path(RAPIDXML_INCLUDE_DIR rapidxml.hpp PATH_SUFFIXES rapidxml REQUIRED) +if(ARK_BUILD_CONFIG) + add_subdirectory(src/config) +endif() + add_library(ark_core INTERFACE) add_library(ark::core ALIAS ark_core) target_compile_features(ark_core INTERFACE cxx_std_20) diff --git a/build/conf/sample1.app.toml b/build/conf/sample1.app.toml new file mode 100644 index 000000000..6457bfcf3 --- /dev/null +++ b/build/conf/sample1.app.toml @@ -0,0 +1,10 @@ +[application] +plugin_library_dir = "bin/" +plugin_config_dir = "conf/" + +[[plugins]] +name = "AFUtilityPlugin" +config = "" + +[[plugins]] +name = "Sample1Plugin" diff --git a/docs/service-manifests.md b/docs/service-manifests.md new file mode 100644 index 000000000..812f5c92c --- /dev/null +++ b/docs/service-manifests.md @@ -0,0 +1,80 @@ +# Application plugin manifests + +An application manifest selects plugin libraries and their configuration files. Pass its path through the existing `-a` / `--app_conf` argument. A `.toml` suffix selects TOML; other paths retain legacy XML parsing. A TOML error never falls back to XML. + +## TOML format + +The shipped [sample1.app.toml](../build/conf/sample1.app.toml) is equivalent to [sample1.app.conf](../build/conf/sample1.app.conf): + +```toml +[application] +plugin_library_dir = "bin/" +plugin_config_dir = "conf/" + +[[plugins]] +name = "AFUtilityPlugin" +config = "" + +[[plugins]] +name = "Sample1Plugin" +``` + +| Field | Type and default | Meaning | +| --- | --- | --- | +| `application` | Required table | Application manifest settings. | +| `application.plugin_library_dir` | String, `""` | Prefix for plugin library names. | +| `application.plugin_config_dir` | String, `""` | Prefix for plugin configuration names. | +| `plugins` | Array of tables, empty | Omission and a top-level `plugins = []` both select no plugins. | +| `plugins[].name` | Required nonempty string | Unique library stem, such as `Sample1Plugin`; platform extensions and the Debug `_d` suffix remain automatic. | +| `plugins[].config` | String, `""` | Configuration filename; omission and `""` retain empty-configuration behavior. | + +Unknown fields, incorrect types, duplicate or empty plugin names, invalid TOML/UTF-8 and embedded NUL characters are rejected before plugin startup. Diagnostics contain the path and available line/column, without printing configuration values. + +Directory strings are preserved exactly. Relative paths use the process working directory. The manager appends filenames by string concatenation, so directory prefixes normally need a trailing `/` or platform-appropriate separator. The loader does not add separators or require configuration files for empty `config` values. + +The order of `[[plugins]]` entries does **not** define module initialization order: the manager still consumes an unordered map. This format introduces no dependency ordering or live reload mechanism. + +## Run sample1 + +Follow the [application build instructions](networking.md) to build `app`, `AFUtilityPlugin` and `Sample1Plugin`. For a build directory named `out`, run these commands from the repository root: + +```sh +cp -R build/conf out/conf +cd out +./bin/app --busid=1.0.0.1 --name=sample1 -a conf/sample1.app.toml +``` + +Debug builds use `app_d`. Logging still reads `conf/log.conf`; bus, logging, database and business configuration formats are unchanged. + +## XML compatibility + +The existing wrapper and attributes remain valid: + +```xml + + + + + + +``` + +The application loader requires a root containing a `plugins` child; a direct `` root is not an application manifest. Missing directory attributes and `conf` remain empty strings, and unknown XML attributes remain ignored. XML duplicate/empty names and embedded NUL are now rejected during manifest loading instead of reaching plugin loading with ambiguous or truncated data. A manifest error exits the application with a nonzero status before `plugin_manager::start()`. + +## C++ and build integration + +`ark::config` is a static CMake target with a standard-library-only public header: + +```cpp +#include + +auto manifest = ark::load_application_manifest("conf/sample1.app.toml"); +``` + +```cmake +target_link_libraries(my_loader PRIVATE ark::config) +``` + +`ARK_BUILD_CONFIG` defaults to `ON` for application or core-test builds, otherwise `OFF`. Applications require it. The target privately uses [toml++](https://github.com/marzer/tomlplusplus) (MIT): CMake prefers an installed `tomlplusplus` 3.4+ package, otherwise fetches v3.4.0 at `30172438cee64926dc41fdd9c11fb3ba5b2ba9de`. The target does not propagate parser headers or macros to `ark::core` / `ark::ark` consumers. RapidXML remains the XML dependency. + +Run `cmake --build out --target check-application-manifest` for native regressions. `check-core` includes them when `ARK_BUILD_CONFIG=ON`. diff --git a/docs/service-manifests_CN.md b/docs/service-manifests_CN.md new file mode 100644 index 000000000..17f515eee --- /dev/null +++ b/docs/service-manifests_CN.md @@ -0,0 +1,80 @@ +# 应用插件清单 + +应用插件清单指定要加载的插件库及配置文件,继续通过 `-a` / `--app_conf` 传入路径。以 `.toml` 结尾的路径使用 TOML,其他路径保留原有 XML 解析方式。TOML 解析失败时直接报错,不会回退到 XML。 + +## TOML 格式 + +仓库中的 [sample1.app.toml](../build/conf/sample1.app.toml) 与 [sample1.app.conf](../build/conf/sample1.app.conf) 等价: + +```toml +[application] +plugin_library_dir = "bin/" +plugin_config_dir = "conf/" + +[[plugins]] +name = "AFUtilityPlugin" +config = "" + +[[plugins]] +name = "Sample1Plugin" +``` + +| 字段 | 类型及默认值 | 含义 | +| --- | --- | --- | +| `application` | 表,必填 | 应用清单设置。 | +| `application.plugin_library_dir` | 字符串,`""` | 插件库名称前的路径前缀。 | +| `application.plugin_config_dir` | 字符串,`""` | 插件配置文件名的路径前缀。 | +| `plugins` | 表数组,空 | 省略或在顶层设置 `plugins = []` 均表示不加载插件。 | +| `plugins[].name` | 非空字符串,必填 | 唯一的动态库名称,例如 `Sample1Plugin`,不含扩展名;平台后缀及 Debug 的 `_d` 后缀仍由加载器补充。 | +| `plugins[].config` | 字符串,`""` | 配置文件名;省略或设置 `""` 均保留原有的空配置语义。 | + +类型错误、未知字段、重复或空插件名、无效 TOML / UTF-8 和嵌入的 NUL 字符都会在插件启动前被拒绝。错误信息包含清单路径及可用的行列位置,不输出配置值。 + +加载器原样保留目录字符串,相对路径以进程工作目录为基准。库名和配置文件名通过字符串拼接追加到前缀后,因此目录前缀通常需要以 `/` 或平台适用的分隔符结尾,加载器不会自动补充分隔符。`config` 为空时,不要求存在对应的配置文件。 + +`[[plugins]]` 的书写顺序**不定义模块初始化顺序**,插件管理器仍接收无序映射。本次格式支持不引入依赖排序或运行时重新加载机制。 + +## 运行 sample1 + +按[应用构建说明](networking_CN.md)构建 `app`、`AFUtilityPlugin` 和 `Sample1Plugin`。假设构建目录为 `out`,从仓库根目录执行: + +```sh +cp -R build/conf out/conf +cd out +./bin/app --busid=1.0.0.1 --name=sample1 -a conf/sample1.app.toml +``` + +Debug 构建使用 `app_d`。日志仍读取 `conf/log.conf`;总线、日志、数据库和业务配置的格式保持原样。 + +## XML 兼容性 + +原有包装节点和属性名继续有效: + +```xml + + + + + + +``` + +应用加载器要求根节点下包含 `plugins` 子节点,直接以 `` 为根节点的文件不属于应用清单。目录属性和 `conf` 省略时仍为空字符串,未知 XML 属性仍被忽略。重复或空插件名和嵌入的 NUL 字符现在会在清单读取阶段被拒绝,避免将歧义或截断数据传入插件加载阶段。清单读取失败时,应用在调用 `plugin_manager::start()` 前以非零状态退出。 + +## C++ 和构建集成 + +`ark::config` 是静态 CMake 目标,公开头文件仅依赖 C++ 标准库: + +```cpp +#include + +auto manifest = ark::load_application_manifest("conf/sample1.app.toml"); +``` + +```cmake +target_link_libraries(my_loader PRIVATE ark::config) +``` + +构建应用或核心测试时,`ARK_BUILD_CONFIG` 默认为 `ON`,其他情况下默认为 `OFF`;应用构建要求启用此选项。目标内部使用 MIT 许可的 [toml++](https://github.com/marzer/tomlplusplus):CMake 优先查找已安装的 3.4 或更新版本的 `tomlplusplus` 包,否则获取固定提交 `30172438cee64926dc41fdd9c11fb3ba5b2ba9de` 对应的 v3.4.0。此目标不会向 `ark::core` / `ark::ark` 的使用者传播解析器头文件和宏。XML 继续使用已有的 RapidXML 依赖。 + +运行 `cmake --build out --target check-application-manifest` 可执行原生回归;`ARK_BUILD_CONFIG=ON` 时,`check-core` 也会运行这些测试。 diff --git a/include/ark/application_manifest.hpp b/include/ark/application_manifest.hpp new file mode 100644 index 000000000..13f2370e3 --- /dev/null +++ b/include/ark/application_manifest.hpp @@ -0,0 +1,37 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#pragma once + +#include +#include +#include +#include + +namespace ark { + +struct application_manifest +{ + std::string plugin_library_dir; + std::string plugin_config_dir; + std::unordered_map plugins; +}; + +class application_manifest_error : public std::runtime_error +{ +public: + application_manifest_error( + const std::string& path, const std::string& reason, std::size_t line = 0, std::size_t column = 0); + + const std::string& path() const noexcept; + std::size_t line() const noexcept; + std::size_t column() const noexcept; + +private: + std::string path_; + std::size_t line_; + std::size_t column_; +}; + +// A .toml suffix selects TOML; other paths retain the legacy XML format. +application_manifest load_application_manifest(const std::string& path); + +} // namespace ark diff --git a/include/ark/plugin_manager.hpp b/include/ark/plugin_manager.hpp index 862473df6..9e1eb4d09 100644 --- a/include/ark/plugin_manager.hpp +++ b/include/ark/plugin_manager.hpp @@ -155,7 +155,7 @@ class plugin_manager final : private noncopyable void set_app_conf(const std::string& value) { ARK_ASSERT_RET_NONE(!value.empty()); - ARK_ASSERT_RET_NONE(value.find(".app.conf") != std::string::npos); + ARK_ASSERT_RET_NONE(value.find(".app.conf") != std::string::npos || value.ends_with(".toml")); app_conf_path_ = value; } diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 751af4838..ffbf2a007 100755 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -8,6 +8,7 @@ endif(UNIX AND (NOT APPLE)) file(GLOB_RECURSE APP_SRC main.cpp) add_executable(${project_name} ${APP_SRC}) +target_link_libraries(${project_name} ark::config) if(WIN32) target_link_libraries(${project_name}) diff --git a/src/app/main.cpp b/src/app/main.cpp index 565ccea73..9bd0dfd61 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -22,6 +22,7 @@ #include +#include "ark/application_manifest.hpp" #include "ark/bus.hpp" #include "ark/dep.hpp" #include "ark/macros.hpp" @@ -29,7 +30,6 @@ #include "ark/platform.hpp" #include "ark/plugin_manager.hpp" #include "ark/time/date_time.hpp" -#include "ark/xml.hpp" #include "shutdown_request.hpp" using namespace ark; @@ -279,31 +279,6 @@ void main_loop() { #endif } -// load app configuration -bool load_plugin_conf(std::string const& app_conf_path, OUT std::string& plugin_library_dir, - OUT std::string& plugin_conf_dir, OUT std::unordered_map& plugins) { - xml xml_doc(app_conf_path); - - auto root_node = xml_doc.get_root_node(); - ARK_ASSERT_RET_VAL(root_node.is_valid(), false); - - auto plugins_node = root_node.find_node("plugins"); - ARK_ASSERT_RET_VAL(plugins_node.is_valid(), false); - - plugin_library_dir = plugins_node.get_string("plugin_lib_dir"); - plugin_conf_dir = plugins_node.get_string("plugin_conf_dir"); - - for (auto plugin_node = plugins_node.find_node("plugin"); plugin_node.is_valid(); plugin_node.next_node()) - { - std::string plugin_name = plugin_node.get_string("name"); - std::string plugin_conf = plugin_node.get_string("conf"); - auto ret = plugins.insert(std::make_pair(plugin_name, plugin_conf)).second; - ARK_ASSERT_CONTINUE(ret); - } - - return true; -} - int main(int argc, char* argv[]) { if (!parse_args(argc, argv)) { @@ -314,16 +289,17 @@ int main(int argc, char* argv[]) { print_logo(); const auto& app_conf_path = plugin_manager::instance()->get_app_conf(); - std::string plugin_library_dir; - std::string plugin_conf_dir; - std::unordered_map plugins; - // load app conf - if (!load_plugin_conf(app_conf_path, plugin_library_dir, plugin_conf_dir, plugins)) + application_manifest manifest; + try + { manifest = load_application_manifest(app_conf_path); } + catch (const application_manifest_error& error) { - return 0; + std::cerr << "Application manifest loading failed: " << error.what() << std::endl; + return 1; } - plugin_manager::instance()->start(date_time::get_now_time(), plugin_library_dir, plugin_conf_dir, plugins); + plugin_manager::instance()->start( + date_time::get_now_time(), manifest.plugin_library_dir, manifest.plugin_config_dir, manifest.plugins); for (;; std::this_thread::sleep_for(std::chrono::milliseconds(1))) { diff --git a/src/config/CMakeLists.txt b/src/config/CMakeLists.txt new file mode 100644 index 000000000..046e95cf0 --- /dev/null +++ b/src/config/CMakeLists.txt @@ -0,0 +1,16 @@ +find_package(tomlplusplus 3.4 CONFIG QUIET) +if(NOT TARGET tomlplusplus::tomlplusplus) + include(FetchContent) + FetchContent_Declare(tomlplusplus + GIT_REPOSITORY https://github.com/marzer/tomlplusplus.git + GIT_TAG 30172438cee64926dc41fdd9c11fb3ba5b2ba9de + GIT_SHALLOW FALSE) + FetchContent_MakeAvailable(tomlplusplus) +endif() + +add_library(ark_config STATIC application_manifest.cpp) +add_library(ark::config ALIAS ark_config) +target_compile_features(ark_config PUBLIC cxx_std_20) +target_include_directories(ark_config PUBLIC "$") +target_include_directories(ark_config PRIVATE "${RAPIDXML_INCLUDE_DIR}") +target_link_libraries(ark_config PRIVATE tomlplusplus::tomlplusplus) diff --git a/src/config/application_manifest.cpp b/src/config/application_manifest.cpp new file mode 100644 index 000000000..5ca39423f --- /dev/null +++ b/src/config/application_manifest.cpp @@ -0,0 +1,264 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +namespace ark { +namespace { + +std::string diagnostic_path(std::string_view path) { + std::string result; + for (const auto character : path) + { + switch (character) + { + case '\0': + result += "\\0"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + default: + result += character; + break; + } + } + return result; +} + +std::string error_message(const std::string& path, const std::string& reason, std::size_t line, std::size_t column) { + auto message = diagnostic_path(path); + if (line != 0) + { + message += ":" + std::to_string(line) + ":" + std::to_string(column); + } + return message + ": " + reason; +} + +[[noreturn]] void fail_at(const std::string& path, const char* reason, std::string_view contents, std::size_t offset) { + std::size_t line = 1; + std::size_t column = 1; + for (std::size_t index = 0; index < offset && index < contents.size(); ++index) + { + if (contents[index] == '\n') + { + ++line; + column = 1; + } + else + { + ++column; + } + } + throw application_manifest_error(path, reason, line, column); +} + +[[noreturn]] void fail_at(const std::string& path, const char* reason, const toml::source_region& source) { + throw application_manifest_error(path, reason, source.begin.line, source.begin.column); +} + +void validate_keys(const toml::table& table, std::initializer_list allowed, const std::string& path) { + for (const auto& [key, value] : table) + { + bool known = false; + for (const auto candidate : allowed) + { + known = known || key.str() == candidate; + } + if (!known) + { + fail_at(path, "unknown manifest field", key.source()); + } + } +} + +std::string read_string( + const toml::table& table, std::string_view key, const std::string& path, bool required = false) { + const auto* node = table.get(key); + if (node == nullptr) + { + if (required) + { + fail_at(path, "missing plugin name", table.source()); + } + return {}; + } + const auto* value = node->as_string(); + if (value == nullptr) + { + fail_at(path, "manifest field must be a string", node->source()); + } + if (value->get().find('\0') != std::string::npos) + { + fail_at(path, "manifest string contains NUL", node->source()); + } + if (required && value->get().empty()) + { + fail_at(path, "plugin name must not be empty", node->source()); + } + return value->get(); +} + +application_manifest parse_toml(const std::string& path, std::string_view contents) { + toml::table document; + try + { document = toml::parse(contents); } + catch (const toml::parse_error& error) + { fail_at(path, "invalid TOML syntax or encoding", error.source()); } + validate_keys(document, {"application", "plugins"}, path); + const auto* application_node = document.get("application"); + if (application_node == nullptr) + { + throw application_manifest_error(path, "missing application table", 1, 1); + } + const auto* application = application_node->as_table(); + if (application == nullptr) + { + fail_at(path, "application must be a table", application_node->source()); + } + validate_keys(*application, {"plugin_library_dir", "plugin_config_dir"}, path); + application_manifest result; + result.plugin_library_dir = read_string(*application, "plugin_library_dir", path); + result.plugin_config_dir = read_string(*application, "plugin_config_dir", path); + const auto* plugins_node = document.get("plugins"); + if (plugins_node == nullptr) + { + return result; + } + const auto* plugins = plugins_node->as_array(); + if (plugins == nullptr) + { + fail_at(path, "plugins must be an array of tables", plugins_node->source()); + } + for (const auto& node : *plugins) + { + const auto* plugin = node.as_table(); + if (plugin == nullptr) + { + fail_at(path, "plugin entry must be a table", node.source()); + } + validate_keys(*plugin, {"name", "config"}, path); + auto name = read_string(*plugin, "name", path, true); + auto config = read_string(*plugin, "config", path); + if (!result.plugins.emplace(std::move(name), std::move(config)).second) + { + fail_at(path, "duplicate plugin name", plugin->source()); + } + } + return result; +} + +application_manifest parse_xml(const std::string& path, const std::string& contents) { + auto buffer = contents; + buffer.push_back('\0'); + rapidxml::xml_document<> document; + try + { document.parse(buffer.data()); } + catch (const rapidxml::parse_error& error) + { fail_at(path, "invalid XML syntax", contents, static_cast(error.where() - buffer.data())); } + const auto* root = document.first_node(); + const auto* plugins = root == nullptr ? nullptr : root->first_node("plugins"); + if (plugins == nullptr) + { + throw application_manifest_error(path, "missing XML plugins element", 1, 1); + } + const auto read_attribute = [&](const rapidxml::xml_node<>* node, const char* key) { + const auto* attribute = node->first_attribute(key); + if (attribute == nullptr) + { + return std::string{}; + } + std::string value(attribute->value(), attribute->value_size()); + if (value.find('\0') != std::string::npos) + { + fail_at(path, "manifest string contains NUL", contents, + static_cast(attribute->value() - buffer.data())); + } + return value; + }; + application_manifest result; + result.plugin_library_dir = read_attribute(plugins, "plugin_lib_dir"); + result.plugin_config_dir = read_attribute(plugins, "plugin_conf_dir"); + for (auto* plugin = plugins->first_node("plugin"); plugin != nullptr; plugin = plugin->next_sibling()) + { + auto name = read_attribute(plugin, "name"); + if (name.empty()) + { + fail_at(path, "plugin name must not be empty", contents, + static_cast(plugin->name() - buffer.data())); + } + if (!result.plugins.emplace(std::move(name), read_attribute(plugin, "conf")).second) + { + fail_at(path, "duplicate plugin name", contents, static_cast(plugin->name() - buffer.data())); + } + } + return result; +} + +} // namespace + +application_manifest_error::application_manifest_error( + const std::string& path, const std::string& reason, std::size_t line, std::size_t column) + : std::runtime_error(error_message(path, reason, line, column)) + , path_(path) + , line_(line) + , column_(column) { +} + +const std::string& application_manifest_error::path() const noexcept { + return path_; +} + +std::size_t application_manifest_error::line() const noexcept { + return line_; +} + +std::size_t application_manifest_error::column() const noexcept { + return column_; +} + +application_manifest load_application_manifest(const std::string& path) { + if (path.find('\0') != std::string::npos) + { + throw application_manifest_error(path, "manifest path contains NUL"); + } + std::string contents; + try + { + const std::filesystem::path native_path(std::u8string(path.begin(), path.end())); + if (std::filesystem::is_directory(native_path)) + { + throw application_manifest_error(path, "manifest path is a directory"); + } + std::ifstream input(native_path, std::ios::binary); + input.exceptions(std::ios::badbit); + if (!input) + { + throw application_manifest_error(path, "could not open manifest"); + } + contents.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + } + catch (const std::filesystem::filesystem_error&) + { throw application_manifest_error(path, "could not open manifest path"); } + catch (const std::ios_base::failure&) + { throw application_manifest_error(path, "could not read manifest"); } + const auto nul = contents.find('\0'); + if (nul != std::string::npos) + { + fail_at(path, "manifest contains NUL", contents, nul); + } + return path.ends_with(".toml") ? parse_toml(path, contents) : parse_xml(path, contents); +} + +} // namespace ark diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt index 2e75a07a0..6ddd418db 100644 --- a/tests/core/CMakeLists.txt +++ b/tests/core/CMakeLists.txt @@ -36,6 +36,18 @@ add_custom_target(check-xml USES_TERMINAL) add_dependencies(check-core check-xml) +if(ARK_BUILD_CONFIG) + add_executable(ark_application_manifest_tests main.cpp application_manifest.cpp) + # This configuration-only consumer deliberately excludes the inherited network SDK. + set_property(TARGET ark_application_manifest_tests PROPERTY LINK_LIBRARIES ark::config) + target_compile_definitions(ark_application_manifest_tests PRIVATE ARK_TEST_SOURCE_DIR="${PROJECT_SOURCE_DIR}") + add_custom_target(check-application-manifest + COMMAND $ + DEPENDS ark_application_manifest_tests + USES_TERMINAL) + add_dependencies(check-core check-application-manifest) +endif() + if(ARK_BUILD_PROTO) add_executable(ark_kernel_initialization_tests main.cpp module_initialization.cpp entity_config_copy.cpp ../../plugins/kernel/src/class_call_back_manager.cpp diff --git a/tests/core/README.md b/tests/core/README.md index 2712a2aa9..731b0101f 100644 --- a/tests/core/README.md +++ b/tests/core/README.md @@ -21,6 +21,11 @@ identity, binary messages, legacy wire payloads and external JSON field names. They also exercise the new `ark::protobuf` descriptor and enum names. Initialize Protobuf as described for application builds. +With `ARK_BUILD_CONFIG=ON` (the default for core tests), +`check-application-manifest` checks TOML/XML equivalence, defaults, strict fields, +invalid input, diagnostics and owning results. It is also part of `check-core`. +See [application manifests](../../docs/service-manifests.md). + `check-kernel-initialization` loads real metadata and static data from `resource/meta/`. It checks four kernel/config initialization orders with both resource-relative paths and legacy paths beginning with `/`, for eight combinations. diff --git a/tests/core/README_CN.md b/tests/core/README_CN.md index e5f560a38..61a2eadac 100644 --- a/tests/core/README_CN.md +++ b/tests/core/README_CN.md @@ -19,5 +19,9 @@ cmake --build core-build --target check-core 旧版报文字节和对外 JSON 字段名,以及新的 `ark::protobuf` 描述符和枚举名称; 需要按应用构建说明初始化 Protobuf 依赖。 +`ARK_BUILD_CONFIG=ON`(核心测试默认启用)时,`check-application-manifest` +验证 TOML / XML 等价性、默认值、严格字段、无效输入、诊断和拥有型结果,并纳入 +`check-core`。参见[应用插件清单](../../docs/service-manifests_CN.md)。 + `check-kernel-initialization` 从 `resource/meta/` 加载真实元数据和静态数据, 分别使用资源相对路径及以 `/` 开头的旧路径,验证内核与配置模块的四种初始化顺序,共八种组合。 diff --git a/tests/core/application_manifest.cpp b/tests/core/application_manifest.cpp new file mode 100644 index 000000000..0d2dcbeec --- /dev/null +++ b/tests/core/application_manifest.cpp @@ -0,0 +1,263 @@ +// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE. +#include +#include +#include +#include +#include +#include + +#include "../network/doctest.h" + +#include + +namespace { + +std::string utf8_path(const std::filesystem::path& path) { + const auto encoded = path.u8string(); + return {encoded.begin(), encoded.end()}; +} + +std::filesystem::path native_path(std::string_view path) { + return std::filesystem::path(std::u8string(path.begin(), path.end())); +} + +class manifest_files +{ +public: + manifest_files() + : directory_(std::filesystem::temp_directory_path() / + ("ark-manifest-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()))) { + if (!std::filesystem::create_directory(directory_)) + { + throw std::runtime_error("could not create temporary manifest directory"); + } + } + + ~manifest_files() { + std::error_code ignored; + std::filesystem::remove_all(directory_, ignored); + } + + std::string path(std::string_view name) const { + return utf8_path(directory_ / native_path(name)); + } + + std::string write(std::string_view contents, std::string_view name = "sample.app.toml") const { + const auto filename = path(name); + std::ofstream output(native_path(filename), std::ios::binary); + output.exceptions(std::ios::badbit | std::ios::failbit); + output.write(contents.data(), static_cast(contents.size())); + output.close(); + return filename; + } + +private: + std::filesystem::path directory_; +}; + +void check_error(const std::string& path, bool has_position = true) { + try + { + static_cast(ark::load_application_manifest(path)); + DOCTEST_FAIL("invalid manifest was accepted"); + } + catch (const ark::application_manifest_error& error) + { + DOCTEST_CHECK(error.path() == path); + DOCTEST_CHECK(std::string(error.what()).find(path) != std::string::npos); + DOCTEST_CHECK((error.line() > 0) == has_position); + DOCTEST_CHECK((error.column() > 0) == has_position); + } +} + +} // namespace + +DOCTEST_TEST_CASE("TOML and XML samples produce equivalent owning application manifests") { + const auto base = std::filesystem::path(ARK_TEST_SOURCE_DIR) / "build" / "conf"; + const auto toml = ark::load_application_manifest(utf8_path(base / "sample1.app.toml")); + const auto xml = ark::load_application_manifest(utf8_path(base / "sample1.app.conf")); + DOCTEST_CHECK(toml.plugin_library_dir == "bin/"); + DOCTEST_CHECK(toml.plugin_config_dir == "conf/"); + DOCTEST_REQUIRE(toml.plugins.size() == 2); + DOCTEST_CHECK(toml.plugins.at("AFUtilityPlugin").empty()); + DOCTEST_CHECK(toml.plugins.at("Sample1Plugin").empty()); + DOCTEST_CHECK(toml.plugin_library_dir == xml.plugin_library_dir); + DOCTEST_CHECK(toml.plugin_config_dir == xml.plugin_config_dir); + DOCTEST_CHECK(toml.plugins == xml.plugins); +} + +DOCTEST_TEST_CASE("TOML preserves directory prefixes and optional empty plugin configurations") { + ark::application_manifest manifest; + { + manifest_files files; + manifest = ark::load_application_manifest(files.write(R"([application] +plugin_library_dir = 'relative.with.dots/bin' +plugin_config_dir = '' +[[plugins]] +name = 'FirstPlugin' +[[plugins]] +name = 'SecondPlugin' +config = '' +[[plugins]] +name = 'ThirdPlugin' +config = 'nested/service.xml' +)")); + } + DOCTEST_CHECK(manifest.plugin_library_dir == "relative.with.dots/bin"); + DOCTEST_CHECK(manifest.plugin_config_dir.empty()); + DOCTEST_REQUIRE(manifest.plugins.size() == 3); + DOCTEST_CHECK(manifest.plugins.at("FirstPlugin").empty()); + DOCTEST_CHECK(manifest.plugins.at("SecondPlugin").empty()); + DOCTEST_CHECK(manifest.plugins.at("ThirdPlugin") == "nested/service.xml"); +} + +DOCTEST_TEST_CASE("Missing directories and omitted or empty plugin arrays retain empty manifest semantics") { + manifest_files files; + const std::array inputs = { + "[application]\n", "plugins = []\n[application]\n", ""}; + for (std::size_t index = 0; index < inputs.size(); ++index) + { + const auto manifest = ark::load_application_manifest( + files.write(inputs[index], index == 2 ? "empty.app.conf" : "empty.app.toml")); + DOCTEST_CHECK(manifest.plugin_library_dir.empty()); + DOCTEST_CHECK(manifest.plugin_config_dir.empty()); + DOCTEST_CHECK(manifest.plugins.empty()); + } +} + +DOCTEST_TEST_CASE("XML retains legacy attribute names optional conf and unknown attributes") { + manifest_files files; + const auto manifest = ark::load_application_manifest(files.write( + R"( + +)", + "legacy.app.conf")); + DOCTEST_CHECK(manifest.plugin_library_dir == "bin/"); + DOCTEST_CHECK(manifest.plugin_config_dir == "../conf/"); + DOCTEST_REQUIRE(manifest.plugins.size() == 2); + DOCTEST_CHECK(manifest.plugins.at("FirstPlugin").empty()); + DOCTEST_CHECK(manifest.plugins.at("SecondPlugin").empty()); +} + +DOCTEST_TEST_CASE("TOML validates required tables exact field types and plugin names") { + manifest_files files; + const std::array invalid = {"", "plugins = []\n", "application = 7\n", "application = []\n", + "[application]\nplugin_library_dir = 1\n", "[application]\nplugin_config_dir = false\n", + "plugins = 'bad'\n[application]\n", "plugins = [1]\n[application]\n", "[plugins]\n[application]\n", + "[application]\n[[plugins]]\n", "[application]\n[[plugins]]\nname = ''\n", + "[application]\n[[plugins]]\nname = 42\n", "[application]\n[[plugins]]\nname = 'Plugin'\nconfig = []\n", + "[application]\n[[plugins]]\nname = 'Plugin'\nconfig = {}\n"}; + for (const auto input : invalid) + { + DOCTEST_CAPTURE(input); + check_error(files.write(input)); + } +} + +DOCTEST_TEST_CASE("TOML rejects unknown fields at every schema level") { + manifest_files files; + const std::array invalid = {"typo = 1\n[application]\n", + "[application]\nplugin_lib_dir = 'bin/'\n", "[application]\n[[plugins]]\nname = 'Plugin'\nconf = ''\n", + "[application]\n[extra]\n"}; + for (const auto input : invalid) + { + check_error(files.write(input)); + } +} + +DOCTEST_TEST_CASE("Both manifest formats reject duplicate and empty plugin names before returning a result") { + manifest_files files; + const std::array invalid = { + "[application]\n[[plugins]]\nname = 'Plugin'\n[[plugins]]\nname = 'Plugin'\n", + "", + "", ""}; + for (std::size_t index = 0; index < invalid.size(); ++index) + { + check_error(files.write(invalid[index], index == 0 ? "duplicate.toml" : "invalid.app.conf")); + } + auto manifest = ark::load_application_manifest(files.write("[application]\n[[plugins]]\nname = 'Original'\n")); + DOCTEST_CHECK_THROWS_AS( + manifest = ark::load_application_manifest(files.write(invalid[0])), ark::application_manifest_error); + DOCTEST_REQUIRE(manifest.plugins.size() == 1); + DOCTEST_CHECK(manifest.plugins.contains("Original")); +} + +DOCTEST_TEST_CASE("TOML and XML syntax errors have source positions without disclosing configuration values") { + manifest_files files; + const std::array invalid = {"[application]\nplugin_library_dir = SECRET_SENTINEL\n", + "[application]\n[[plugins]]\nname = 'Plugin'\nname = 'SECRET_SENTINEL'\n", + "\n", ""}; + for (std::size_t index = 0; index < invalid.size(); ++index) + { + const auto path = files.write(invalid[index], index < 2 ? "syntax.toml" : "syntax.app.conf"); + check_error(path); + try + { static_cast(ark::load_application_manifest(path)); } + catch (const ark::application_manifest_error& error) + { DOCTEST_CHECK(std::string(error.what()).find("SECRET_SENTINEL") == std::string::npos); } + } + const auto path = files.write("[application]\nplugin_library_dir = 7\n"); + try + { + static_cast(ark::load_application_manifest(path)); + DOCTEST_FAIL("numeric directory accepted"); + } + catch (const ark::application_manifest_error& error) + { + DOCTEST_CHECK(error.line() == 2); + DOCTEST_CHECK(error.column() > 0); + } +} + +DOCTEST_TEST_CASE("Manifest suffix dispatch never falls back from TOML to XML") { + manifest_files files; + check_error(files.write("")); + check_error(files.write("[application]\n", "wrong.app.conf")); + check_error(files.write("[application]\n", "wrong.app.toml.extra")); + check_error(files.write(" unmatched", "empty.app.conf")); + check_error(files.write("", "empty.app.conf")); + check_error(files.write("", "direct-root.app.conf")); +} + +DOCTEST_TEST_CASE("Manifest strings and input reject raw or decoded embedded NUL") { + manifest_files files; + const std::array decoded = {"[application]\nplugin_library_dir = \"x\\u0000y\"\n", + "[application]\nplugin_config_dir = \"x\\u0000y\"\n", "[application]\n[[plugins]]\nname = \"x\\u0000y\"\n", + "[application]\n[[plugins]]\nname = 'Plugin'\nconfig = \"x\\u0000y\"\n", + ""}; + for (std::size_t index = 0; index < decoded.size(); ++index) + { + check_error(files.write(decoded[index], index == 4 ? "nul.app.conf" : "nul.toml")); + } + for (const auto name : {"raw.toml", "raw.app.conf"}) + { + std::string input = name == std::string_view("raw.toml") ? "[application]\n" : ""; + input += '\0'; + input += "hidden data"; + check_error(files.write(input, name)); + } + const auto nul_path = files.path("missing") + std::string("\0.toml", 6); + DOCTEST_CHECK_THROWS_AS(ark::load_application_manifest(nul_path), ark::application_manifest_error); +} + +DOCTEST_TEST_CASE("TOML supports UTF8 values and native Unicode filenames and rejects invalid UTF8") { + manifest_files files; + const auto path = + files.write("[application]\nplugin_config_dir = '配置/'\n[[plugins]]\nname = '插件'\n", "配置.app.toml"); + const auto manifest = ark::load_application_manifest(path); + DOCTEST_CHECK(manifest.plugin_config_dir == "配置/"); + DOCTEST_CHECK(manifest.plugins.contains("插件")); + std::string invalid = "[application]\nplugin_config_dir = '"; + invalid += static_cast(0xff); + invalid += "'\n"; + check_error(files.write(invalid)); +} + +DOCTEST_TEST_CASE("Missing files and unreadable directory inputs fail without partial results") { + manifest_files files; + check_error(files.path("missing.toml"), false); + check_error(files.path("missing.app.conf"), false); + const auto directory = files.path("directory.toml"); + DOCTEST_REQUIRE(std::filesystem::create_directory(native_path(directory))); + check_error(directory, false); +} diff --git a/tests/core/templates.cpp b/tests/core/templates.cpp index f01863ef4..f94393a25 100644 --- a/tests/core/templates.cpp +++ b/tests/core/templates.cpp @@ -9,6 +9,14 @@ #include #include +DOCTEST_TEST_CASE("application manifest paths accept TOML and retain legacy XML suffixes") { + ark::plugin_manager manager; + manager.set_app_conf("conf/sample1.app.toml"); + DOCTEST_CHECK(manager.get_app_conf() == "conf/sample1.app.toml"); + manager.set_app_conf("conf/sample1.app.conf"); + DOCTEST_CHECK(manager.get_app_conf() == "conf/sample1.app.conf"); +} + namespace { template From d90fd8cdb5d97e59f7a76b3aee54ea3b89536a1b Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 13:28:24 +0800 Subject: [PATCH 2/5] Clarify application manifest path restrictions --- docs/service-manifests.md | 6 +++--- docs/service-manifests_CN.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/service-manifests.md b/docs/service-manifests.md index 812f5c92c..8f2560b97 100644 --- a/docs/service-manifests.md +++ b/docs/service-manifests.md @@ -1,10 +1,10 @@ # Application plugin manifests -An application manifest selects plugin libraries and their configuration files. Pass its path through the existing `-a` / `--app_conf` argument. A `.toml` suffix selects TOML; other paths retain legacy XML parsing. A TOML error never falls back to XML. +An application manifest selects plugin libraries and their configuration files. Pass a `.toml` manifest or an existing `.app.conf` XML manifest through the `-a` / `--app_conf` argument. A TOML error never falls back to XML. ## TOML format -The shipped [sample1.app.toml](../build/conf/sample1.app.toml) is equivalent to [sample1.app.conf](../build/conf/sample1.app.conf): +The shipped [sample1.app.toml](../build/conf/sample1.app.toml) is equivalent to [sample1.app.conf](../build/conf/sample1.app.conf). The example uses TOML 1.0 syntax: ```toml [application] @@ -63,7 +63,7 @@ The application loader requires a root containing a `plugins` child; a direct `< ## C++ and build integration -`ark::config` is a static CMake target with a standard-library-only public header: +`ark::config` is a static CMake target with a standard-library-only public header. The public `load_application_manifest()` reader selects TOML for a `.toml` suffix and XML for other paths; the application's `-a` argument retains the manifest path restrictions described above. ```cpp #include diff --git a/docs/service-manifests_CN.md b/docs/service-manifests_CN.md index 17f515eee..e2239ad30 100644 --- a/docs/service-manifests_CN.md +++ b/docs/service-manifests_CN.md @@ -1,10 +1,10 @@ # 应用插件清单 -应用插件清单指定要加载的插件库及配置文件,继续通过 `-a` / `--app_conf` 传入路径。以 `.toml` 结尾的路径使用 TOML,其他路径保留原有 XML 解析方式。TOML 解析失败时直接报错,不会回退到 XML。 +应用插件清单指定要加载的插件库及配置文件,通过 `-a` / `--app_conf` 传入 `.toml` 清单或原有 `.app.conf` XML 清单。TOML 解析失败时直接报错,不会回退到 XML。 ## TOML 格式 -仓库中的 [sample1.app.toml](../build/conf/sample1.app.toml) 与 [sample1.app.conf](../build/conf/sample1.app.conf) 等价: +仓库中的 [sample1.app.toml](../build/conf/sample1.app.toml) 与 [sample1.app.conf](../build/conf/sample1.app.conf) 等价,示例采用 TOML 1.0 语法: ```toml [application] @@ -63,7 +63,7 @@ Debug 构建使用 `app_d`。日志仍读取 `conf/log.conf`;总线、日志 ## C++ 和构建集成 -`ark::config` 是静态 CMake 目标,公开头文件仅依赖 C++ 标准库: +`ark::config` 是静态 CMake 目标,公开头文件仅依赖 C++ 标准库。公开读取函数 `load_application_manifest()` 对 `.toml` 后缀使用 TOML,对其他路径使用 XML;应用的 `-a` 参数仍保留前述清单路径限制。 ```cpp #include From 030a8f5c3cb384543a3e1f8a964866b54c988dac Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 16:07:07 +0800 Subject: [PATCH 3/5] docs(config): publish bilingual application manifest guides --- docs/site/scripts/check-site.mjs | 23 +++++++++++++++++++++++ docs/site/scripts/content.mjs | 8 ++++++-- docs/site/scripts/content.test.mjs | 20 +++++++++++++++++++- docs/site/scripts/migration-source.mjs | 7 +++++++ docs/site/scripts/prepare-content.mjs | 4 ++-- docs/site/src/legacy-routes.json | 8 ++++++++ docs/site/src/navigation.ts | 11 +++++++++++ 7 files changed, 76 insertions(+), 5 deletions(-) diff --git a/docs/site/scripts/check-site.mjs b/docs/site/scripts/check-site.mjs index a84889b4c..5562940cc 100644 --- a/docs/site/scripts/check-site.mjs +++ b/docs/site/scripts/check-site.mjs @@ -83,5 +83,28 @@ for (const prefix of ["", "zh/"]) { const plugin = await readFile(join(root, prefix, "guides/plugins/index.html"), "utf8"); assert.match(plugin, /role="note"/); assert.match(plugin, /class="astro-code/); + const manifestSlug = "guides/service-configuration/application-manifests"; + const manifestRoute = "/ark/" + prefix + manifestSlug; + const manifest = parse(await readFile(join(root, prefix, manifestSlug, "index.html"), "utf8")); + const groups = new Set(); + const labels = new Set(); + const languages = new Set(); + const text = node => node.value ?? (node.childNodes ?? []).map(text).join(""); + function inspect(node) { + const attrs = new Map((node.attrs ?? []).map(attr => [attr.name, attr.value])); + if (attrs.has("data-nb-sidebar-group-label")) groups.add(text(node).trim()); + if (attrs.has("data-nb-sidebar-link") && attrs.get("href")?.replace(/\/$/, "") === manifestRoute) labels.add(text(node).trim()); + if (node.tagName === "option") languages.add(attrs.get("value")?.replace(/\/$/, "")); + for (const child of node.childNodes ?? []) inspect(child); + } + inspect(manifest); + assert.ok(groups.has(prefix ? "服务配置" : "Service configuration"), "missing localized service configuration group"); + assert.ok(labels.has(prefix ? "应用插件清单" : "Application plugin manifests"), "missing localized manifest navigation"); + assert.ok(languages.has("/ark/" + manifestSlug), "missing English manifest language target"); + assert.ok(languages.has("/ark/zh/" + manifestSlug), "missing Chinese manifest language target"); + const manifestMarkdown = await readFile(join(root, prefix, manifestSlug, "index.md"), "utf8"); + assert.match(manifestMarkdown, /```toml\n\[application\]/); + assert.match(manifestMarkdown, /load_application_manifest/); + assert.ok(manifestMarkdown.includes(`](/ark/${prefix}networking)`), "manifest Markdown lost its build-guide link"); } console.log(`Checked ${pages.length} static HTML pages and ${references.size} internal references, language switching, cards, callouts, highlighting and AI Markdown.`); diff --git a/docs/site/scripts/content.mjs b/docs/site/scripts/content.mjs index ea8731e6f..278774736 100644 --- a/docs/site/scripts/content.mjs +++ b/docs/site/scripts/content.mjs @@ -1,11 +1,15 @@ import { unified } from "unified"; import remarkParse from "remark-parse"; -import { sourceAliases } from "./migration-source.mjs"; +import { pageLocations, sourceAliases } from "./migration-source.mjs"; const parser = unified().use(remarkParse); +export function canonicalFile(file) { + return pageLocations[file] ?? sourceAliases[file] ?? file; +} + export function pageRoute(file) { - file = sourceAliases[file] ?? file; + file = canonicalFile(file); const chinese = file.endsWith("_CN.md"); let slug = file.replace(/(?:_CN)?\.md$/, ""); if (slug === "README") slug = ""; diff --git a/docs/site/scripts/content.test.mjs b/docs/site/scripts/content.test.mjs index b0b72b609..5341efbd5 100644 --- a/docs/site/scripts/content.test.mjs +++ b/docs/site/scripts/content.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { test } from "node:test"; -import { convertPage, pageExtension, pageRoute } from "./content.mjs"; +import { canonicalFile, convertPage, pageExtension, pageRoute } from "./content.mjs"; test("bilingual routes and home aliases", () => { assert.equal(pageRoute("README_CN.md"), "/zh/"); @@ -10,6 +10,24 @@ test("bilingual routes and home aliases", () => { assert.equal(pageRoute("features/index.md"), "/features"); assert.equal(pageRoute("code-style.md"), "/project/code-style"); assert.equal(pageRoute("project-layout_CN.md"), "/zh/reference/directory-structure"); + assert.equal(canonicalFile("service-manifests.md"), "guides/service-configuration/application-manifests.md"); + assert.equal(canonicalFile("service-manifests_CN.md"), "guides/service-configuration/application-manifests_CN.md"); + assert.equal(pageRoute("service-manifests.md"), "/guides/service-configuration/application-manifests"); + assert.equal(pageRoute("service-manifests_CN.md"), "/zh/guides/service-configuration/application-manifests"); +}); + +test("relocated manifest sources resolve links from their original Markdown location", () => { + const files = new Set(["service-manifests.md", "service-manifests_CN.md", "networking.md", "networking_CN.md"]); + for (const chinese of [false, true]) { + const suffix = chinese ? "_CN" : ""; + const prefix = chinese ? "/zh" : ""; + const source = `# Manifest\n\n[build](networking${suffix}.md)\n\n[manifest](service-manifests${suffix}.md)\n\n[sample](../build/conf/sample1.app.toml)\n\n\`\`\`toml\n[application]\nplugin_config_dir = "conf/"\n\`\`\`\n`; + const result = convertPage(source, `service-manifests${suffix}.md`, files); + assert.ok(result.includes(`[build](${prefix}/networking)`)); + assert.ok(result.includes(`[manifest](${prefix}/guides/service-configuration/application-manifests)`)); + assert.ok(result.includes("[sample](https://github.com/OpenArkStudio/ark/blob/main/build/conf/sample1.app.toml)")); + assert.ok(result.includes('```toml\n[application]\nplugin_config_dir = "conf/"\n```')); + } }); test("convert parsed links while preserving code and labels", () => { diff --git a/docs/site/scripts/migration-source.mjs b/docs/site/scripts/migration-source.mjs index 9dc79676b..5382c19aa 100644 --- a/docs/site/scripts/migration-source.mjs +++ b/docs/site/scripts/migration-source.mjs @@ -10,6 +10,13 @@ export const sourceAliases = { "project-layout_CN.md": "reference/directory-structure_CN.md", }; +// Keep standalone Markdown sources in place while giving them a canonical +// location in the documentation hierarchy. +export const pageLocations = { + "service-manifests.md": "guides/service-configuration/application-manifests.md", + "service-manifests_CN.md": "guides/service-configuration/application-manifests_CN.md", +}; + export const articles = { "en-us/app-architecture.md": "docs/concepts/application-architecture.md", "en-us/app.md": "docs/guides/applications.md", diff --git a/docs/site/scripts/prepare-content.mjs b/docs/site/scripts/prepare-content.mjs index a54d051e7..8c10f31b1 100644 --- a/docs/site/scripts/prepare-content.mjs +++ b/docs/site/scripts/prepare-content.mjs @@ -2,7 +2,7 @@ import { existsSync as fsExists } from "node:fs"; import { readdir, readFile, mkdir, writeFile, cp, rm } from "node:fs/promises"; import { fileURLToPath } from "node:url"; import { posix, join } from "node:path"; -import { convertPage, pageExtension } from "./content.mjs"; +import { canonicalFile, convertPage, pageExtension } from "./content.mjs"; import { sourceAliases } from "./migration-source.mjs"; const site = fileURLToPath(new URL("../", import.meta.url)); @@ -29,7 +29,7 @@ for (const file of files) { const chinese = file.endsWith("_CN.md"); const sibling = chinese ? file.replace(/_CN\.md$/, ".md") : file.replace(/\.md$/, "_CN.md"); if (!files.has(sibling)) throw new Error(`${file}: missing translation`); - const slug = file.replace(/(?:_CN)?\.md$/, "").replace(/^README$/, "index"); + const slug = canonicalFile(file).replace(/(?:_CN)?\.md$/, "").replace(/^README$/, "index"); const source = await readFile(join(docs, file), "utf8"); const target = join(content, chinese ? "zh" : "docs", slug + pageExtension(source)); await mkdir(join(target, ".."), { recursive: true }); diff --git a/docs/site/src/legacy-routes.json b/docs/site/src/legacy-routes.json index e534a98c3..e07266881 100644 --- a/docs/site/src/legacy-routes.json +++ b/docs/site/src/legacy-routes.json @@ -1,4 +1,12 @@ { + "/service-manifests": { + "to": "/guides/service-configuration/application-manifests", + "anchors": {} + }, + "/zh/service-manifests": { + "to": "/zh/guides/service-configuration/application-manifests", + "anchors": {} + }, "/code-style": { "to": "/project/code-style", "anchors": {} diff --git a/docs/site/src/navigation.ts b/docs/site/src/navigation.ts index 3ca78cd07..6489d493c 100644 --- a/docs/site/src/navigation.ts +++ b/docs/site/src/navigation.ts @@ -93,6 +93,17 @@ const tree: Entry[] = [ "Application launcher", "应用启动器" ], + { + "en": "Service configuration", + "zh": "服务配置", + "children": [ + [ + "guides/service-configuration/application-manifests", + "Application plugin manifests", + "应用插件清单" + ] + ] + }, [ "guides/tools", "Configuration tools", From 0b3f0ccce04e77880fad2fd36fdabd9cfc4a770c Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 18:18:27 +0800 Subject: [PATCH 4/5] fix(app): validate manifests before starting console thread --- src/app/main.cpp | 9 ++++----- tests/app/manifest_failures.cmake | 31 +++++++++++++++++++++++++++++++ tests/core/CMakeLists.txt | 11 +++++++++++ tests/core/README.md | 5 +++++ tests/core/README_CN.md | 4 ++++ 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 tests/app/manifest_failures.cmake diff --git a/src/app/main.cpp b/src/app/main.cpp index 9bd0dfd61..779e8d3d4 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -258,11 +258,6 @@ bool parse_args(int argc, char* argv[]) { misc::bus2_str(plugin_manager::instance()->get_bus_id()), ".log"); logger::instance()->init_logger(log_conf.Get(), log_file_name); -#ifdef ARK_PLATFORM_WIN - // Create back thread, for some command - create_back_thread(); -#endif - return true; } @@ -298,6 +293,10 @@ int main(int argc, char* argv[]) { return 1; } +#ifdef ARK_PLATFORM_WIN + create_back_thread(); +#endif + plugin_manager::instance()->start( date_time::get_now_time(), manifest.plugin_library_dir, manifest.plugin_config_dir, manifest.plugins); diff --git a/tests/app/manifest_failures.cmake b/tests/app/manifest_failures.cmake new file mode 100644 index 000000000..ea8e34beb --- /dev/null +++ b/tests/app/manifest_failures.cmake @@ -0,0 +1,31 @@ +# Copyright (c) 2026 OpenArkStudio. SPDX-License-Identifier: Apache-2.0 + +foreach(argument PROGRAM SOURCE_ROOT RUNTIME_DIR) + if(NOT DEFINED ${argument}) + message(FATAL_ERROR "${argument} is required") + endif() +endforeach() + +file(MAKE_DIRECTORY "${RUNTIME_DIR}") +file(WRITE "${RUNTIME_DIR}/invalid.toml" "[application\n") +file(WRITE "${RUNTIME_DIR}/invalid.app.conf" "") +file(WRITE "${RUNTIME_DIR}/wrong-type.toml" "[application]\nplugin_library_dir = 42\n") + +foreach(manifest missing.toml invalid.toml invalid.app.conf wrong-type.toml) + execute_process( + COMMAND "${PROGRAM}" --busid=1.0.1.1 --name=manifest-error-test + "--app_conf=${RUNTIME_DIR}/${manifest}" + "--log_conf=${SOURCE_ROOT}/build/conf/log.conf" + WORKING_DIRECTORY "${RUNTIME_DIR}" + RESULT_VARIABLE result + OUTPUT_VARIABLE output + ERROR_VARIABLE errors + TIMEOUT 15) + if(NOT result STREQUAL "1") + message(FATAL_ERROR "${manifest}: expected normal exit 1, got ${result}\n${output}\n${errors}") + endif() + if(NOT errors MATCHES "Application manifest loading failed:") + message(FATAL_ERROR "${manifest}: missing manifest diagnostic\n${errors}") + endif() + message(STATUS "${manifest}: normal exit 1 with manifest diagnostic") +endforeach() diff --git a/tests/core/CMakeLists.txt b/tests/core/CMakeLists.txt index 6ddd418db..421377d3d 100644 --- a/tests/core/CMakeLists.txt +++ b/tests/core/CMakeLists.txt @@ -46,6 +46,17 @@ if(ARK_BUILD_CONFIG) DEPENDS ark_application_manifest_tests USES_TERMINAL) add_dependencies(check-core check-application-manifest) + + if(TARGET app) + add_custom_target(check-app-manifest-failures + COMMAND ${CMAKE_COMMAND} "-DPROGRAM=$" + "-DSOURCE_ROOT=${PROJECT_SOURCE_DIR}" + "-DRUNTIME_DIR=${CMAKE_CURRENT_BINARY_DIR}/manifest failure runtime" + -P "${PROJECT_SOURCE_DIR}/tests/app/manifest_failures.cmake" + DEPENDS app + USES_TERMINAL) + add_dependencies(check-core check-app-manifest-failures) + endif() endif() if(ARK_BUILD_PROTO) diff --git a/tests/core/README.md b/tests/core/README.md index 731b0101f..ca442b0e2 100644 --- a/tests/core/README.md +++ b/tests/core/README.md @@ -26,6 +26,11 @@ With `ARK_BUILD_CONFIG=ON` (the default for core tests), invalid input, diagnostics and owning results. It is also part of `check-core`. See [application manifests](../../docs/service-manifests.md). +When the application is also built, `check-app-manifest-failures` runs the real +launcher against missing, malformed and mistyped manifests. It requires normal +exit code 1 with a manifest diagnostic, including on Windows, and is included in +`check-core`. + `check-kernel-initialization` loads real metadata and static data from `resource/meta/`. It checks four kernel/config initialization orders with both resource-relative paths and legacy paths beginning with `/`, for eight combinations. diff --git a/tests/core/README_CN.md b/tests/core/README_CN.md index 61a2eadac..3cfef93a7 100644 --- a/tests/core/README_CN.md +++ b/tests/core/README_CN.md @@ -23,5 +23,9 @@ cmake --build core-build --target check-core 验证 TOML / XML 等价性、默认值、严格字段、无效输入、诊断和拥有型结果,并纳入 `check-core`。参见[应用插件清单](../../docs/service-manifests_CN.md)。 +同时构建应用时,`check-app-manifest-failures` 使用真实启动器验证文件缺失、语法错误和 +字段类型错误的清单,要求正常返回退出码 1 并输出清单诊断。该入口覆盖 Windows, +并纳入 `check-core`。 + `check-kernel-initialization` 从 `resource/meta/` 加载真实元数据和静态数据, 分别使用资源相对路径及以 `/` 开头的旧路径,验证内核与配置模块的四种初始化顺序,共八种组合。 From 0e8c12cf0e71f5f78da6a8fd82f268d818075a9e Mon Sep 17 00:00:00 2001 From: nick Date: Fri, 16 Oct 2026 18:39:09 +0800 Subject: [PATCH 5/5] docs(config): align launcher guides with TOML manifests --- docs/guides/applications.md | 39 +++++++++++++++++++++++++--------- docs/guides/applications_CN.md | 39 +++++++++++++++++++++++++--------- 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/docs/guides/applications.md b/docs/guides/applications.md index 5b2fc47b2..3d949f39d 100644 --- a/docs/guides/applications.md +++ b/docs/guides/applications.md @@ -1,6 +1,6 @@ # Application launcher -The `app` executable selects a process identity, reads an XML application manifest and loads its plugin libraries. Start with the runnable [Sample1 quick start](../get-started/quick-start.md); this page explains configuration paths, library names and startup checks. +The `app` executable selects a process identity, reads a TOML or XML application manifest and loads its plugin libraries. Start with the runnable [Sample1 quick start](../get-started/quick-start.md); this page explains configuration paths, library names and startup checks. ## Command-line arguments @@ -8,7 +8,7 @@ The `app` executable selects a process identity, reads an XML application manife | --- | --- | --- | | `--busid` / `-b` | Yes | Logical process Bus address, such as `1.0.1.1`; this is not a socket bind address | | `--name` / `-n` | Yes | Application name, also used in the log filename | -| `--app_conf` / `-a` | Yes | XML application manifest; use a `*.app.conf` filename | +| `--app_conf` / `-a` | Yes | TOML application manifest with the lowercase `.toml` extension, or a legacy XML `*.app.conf` file | | `--log_conf` / `-l` | No | Logger configuration; defaults to `./conf/log.conf` | | `--help` / `-h` | No | Print the launcher's argument reference | @@ -29,13 +29,31 @@ Relative paths resolve from the process working directory. Moving the executable | --- | --- | --- | | Application manifest | `--app_conf` | `conf/sample1.app.conf` | | Logger settings | `--log_conf`, or its default | `conf/log.conf` | -| Plugin libraries | `plugin_lib_dir` plus the library filename | `bin/Sample1Plugin.dylib` on macOS Release | -| Per-plugin settings | `plugin_conf_dir` plus that plugin's `conf` value | `conf/my-plugin.conf` when `conf="my-plugin.conf"` | +| Plugin libraries | TOML `plugin_library_dir` or XML `plugin_lib_dir`, plus the library filename | `bin/Sample1Plugin.dylib` on macOS Release | +| Per-plugin settings | TOML `plugin_config_dir` + `config`, or XML `plugin_conf_dir` + `conf` | `conf/my-plugin.conf` when the filename is `my-plugin.conf` | | Kernel metadata and configuration | The Kernel's `resource/` directory | `resource/meta/entity_class.config`, `resource/server/item.config` | The launcher passes the per-plugin path to the plugin manager; each module decides how to read its own configuration. Kernel resource paths are separate from the application manifest and are needed only by applications that use those Kernel facilities. See [Data-driven configuration](../concepts/data-driven.md) and [Configuration tools](tools.md). -## Select plugins with XML +## Select plugins + +For a new manifest, use TOML. The equivalent of Sample1's XML configuration is checked in as [sample1.app.toml](https://github.com/OpenArkStudio/ark/blob/main/build/conf/sample1.app.toml): + +```toml +[application] +plugin_library_dir = "bin/" +plugin_config_dir = "conf/" + +[[plugins]] +name = "AFUtilityPlugin" + +[[plugins]] +name = "Sample1Plugin" +``` + +Pass `--app_conf=conf/sample1.app.toml` to select it. The application table is required; directory fields are optional strings. Each plugin requires a unique, nonempty `name`; its optional `config` string selects a per-plugin file. See [Application manifests](../service-manifests.md) for validation rules and format boundaries. Bus, logger and business-resource configuration formats are unchanged. + +### Existing XML manifests The checked-in [Sample1 manifest](https://github.com/OpenArkStudio/ark/blob/main/build/conf/sample1.app.conf) loads a utility plugin and one sample plugin: @@ -53,17 +71,17 @@ The checked-in [Sample1 manifest](https://github.com/OpenArkStudio/ark/blob/main - Use the library stem in `name`, without a platform suffix or a `lib` prefix. The loader appends `.dll`, `.so` or `.dylib`; Debug builds also append `_d` before the suffix. - An empty `conf` value supplies no per-plugin filename. Sample1 does not need a separate `sample1.conf` file. -Copy the executable and selected plugin libraries from the same build. Keep Debug and Release artifacts in separate runtime directories. The XML list is not a dependency-order contract: modules should resolve their dependencies through the [module API](modules.md), rather than rely on entry order. +Copy the executable and selected plugin libraries from the same build. Keep Debug and Release artifacts in separate runtime directories. The plugin list is not a dependency-order contract: modules should resolve their dependencies through the [module API](modules.md), rather than rely on entry order. ## Check startup and stop the process -For Sample1, look for a log entry containing `sample1_module` and `PostInit`. This identifies entry into the sample's post-initialization callback, which registers its timer. A `LoadPlugin` message alone only identifies a library the loader is attempting to use. +For Sample1, look for a log entry containing `sample1_module` and `PostInit`. This identifies entry into the sample's post-initialization callback, which registers its timer. On macOS/Linux, Ctrl+C or `SIGTERM` requests shutdown through the application's main loop. On Windows, enter `exit` in the application console. Sample1 logs `PreShut` and then `Shut` during orderly shutdown. This sample check is not a readiness or shutdown guarantee for every plugin combination. @@ -72,8 +90,9 @@ The current launcher can return exit code `0` for argument or manifest failures, | Symptom | Check | | --- | --- | | Usage text or `Application parameter is invalid` | Supply all three required arguments; check spelling with `--help` | -| Manifest or logger configuration cannot be read | Check the working directory and both explicit paths; keep the `*.app.conf` naming convention | -| `Load shared library[...] failed` | Check `plugin_lib_dir`, platform suffix, Debug suffix and the library's dependent shared libraries | +| Manifest loading fails | Check the working directory, explicit path and format; a lowercase `.toml` suffix selects TOML, while legacy `*.app.conf` files use XML | +| Logger configuration cannot be read | Check the working directory and `--log_conf` path | +| `Load shared library[...] failed` | Check TOML `plugin_library_dir` or XML `plugin_lib_dir`, platform suffix, Debug suffix and the library's dependent shared libraries | | Plugin loads but the expected module is unavailable | Check registration, enabled targets and required plugins; see [Create a plugin](plugins.md) | | Kernel metadata or configuration is missing | Stage the generated `resource/meta/` and `resource/server/` files in the process working directory | diff --git a/docs/guides/applications_CN.md b/docs/guides/applications_CN.md index af4861958..d6d72759e 100644 --- a/docs/guides/applications_CN.md +++ b/docs/guides/applications_CN.md @@ -1,6 +1,6 @@ # 应用启动器 -`app` 程序设置进程标识、读取 XML 应用配置,并加载所选插件的动态库。首次运行可先按 [Sample1 快速开始](../get-started/quick-start_CN.md)操作;本文说明配置路径、动态库命名和启动检查。 +`app` 程序设置进程标识、读取 TOML 或 XML 应用配置,并加载所选插件的动态库。首次运行可先按 [Sample1 快速开始](../get-started/quick-start_CN.md)操作;本文说明配置路径、动态库命名和启动检查。 ## 命令行参数 @@ -8,7 +8,7 @@ | --- | --- | --- | | `--busid` / `-b` | 是 | 进程的逻辑 Bus 地址,例如 `1.0.1.1`;不是 socket 绑定地址 | | `--name` / `-n` | 是 | 应用名称,也用于日志文件名 | -| `--app_conf` / `-a` | 是 | XML 应用配置,文件名使用 `*.app.conf` | +| `--app_conf` / `-a` | 是 | 扩展名为小写 `.toml` 的 TOML 应用配置,或旧版 XML `*.app.conf` 文件 | | `--log_conf` / `-l` | 否 | 日志配置,默认为 `./conf/log.conf` | | `--help` / `-h` | 否 | 输出启动器的参数说明 | @@ -29,13 +29,31 @@ | --- | --- | --- | | 应用配置 | `--app_conf` | `conf/sample1.app.conf` | | 日志配置 | `--log_conf`,或使用默认值 | `conf/log.conf` | -| 插件动态库 | `plugin_lib_dir` 加动态库文件名 | macOS Release 使用 `bin/Sample1Plugin.dylib` | -| 插件配置 | `plugin_conf_dir` 加对应插件的 `conf` 值 | `conf="my-plugin.conf"` 时为 `conf/my-plugin.conf` | +| 插件动态库 | TOML `plugin_library_dir` 或 XML `plugin_lib_dir` 加动态库文件名 | macOS Release 使用 `bin/Sample1Plugin.dylib` | +| 插件配置 | TOML `plugin_config_dir` + `config`,或 XML `plugin_conf_dir` + `conf` | 配置文件名为 `my-plugin.conf` 时,对应 `conf/my-plugin.conf` | | Kernel 元数据和配置 | Kernel 使用的 `resource/` 目录 | `resource/meta/entity_class.config`、`resource/server/item.config` | 启动器把插件配置路径交给插件管理器,具体模块自行读取配置。Kernel 资源路径与应用配置路径相互独立,只有使用相关 Kernel 能力的应用才需要这些资源。详见[数据驱动配置](../concepts/data-driven_CN.md)和[配置工具](tools_CN.md)。 -## 使用 XML 选择插件 +## 选择插件 + +新建应用配置可使用 TOML。仓库中的 [sample1.app.toml](https://github.com/OpenArkStudio/ark/blob/main/build/conf/sample1.app.toml) 与 Sample1 的 XML 配置等价: + +```toml +[application] +plugin_library_dir = "bin/" +plugin_config_dir = "conf/" + +[[plugins]] +name = "AFUtilityPlugin" + +[[plugins]] +name = "Sample1Plugin" +``` + +通过 `--app_conf=conf/sample1.app.toml` 选择该文件。`application` 表必须存在,目录字段为可省略的字符串。每个插件的 `name` 必须非空且唯一;可选的 `config` 字符串指定插件配置文件。字段校验和格式边界见[应用配置清单](../service-manifests_CN.md)。Bus、日志和业务资源配置格式保持现有约定。 + +### 兼容已有 XML 配置 仓库中的 [Sample1 应用配置](https://github.com/OpenArkStudio/ark/blob/main/build/conf/sample1.app.conf)加载工具插件和一个示例插件: @@ -53,17 +71,17 @@ - `name` 使用动态库名称主体,不加平台后缀,也不加 `lib` 前缀。加载器追加 `.dll`、`.so` 或 `.dylib`,Debug 构建还会在后缀前追加 `_d`。 - `conf` 为空表示没有提供插件配置文件名。Sample1 不需要单独的 `sample1.conf`。 -程序和所选插件动态库应来自同一次构建,Debug 与 Release 文件分别放置。XML 条目顺序不代表依赖初始化顺序;模块应通过[模块 API](modules_CN.md)查找依赖。 +程序和所选插件动态库应来自同一次构建,Debug 与 Release 文件分别放置。插件条目顺序不代表依赖初始化顺序;模块应通过[模块 API](modules_CN.md)查找依赖。 ## 确认启动并停止进程 -Sample1 的日志出现 `sample1_module` 和 `PostInit`,表示示例已经进入初始化后回调,该回调负责注册定时器。单独一条 `LoadPlugin` 日志只表示加载器正在尝试使用该动态库。 +Sample1 的日志出现 `sample1_module` 和 `PostInit`,表示示例已经进入初始化后回调,该回调负责注册定时器。 macOS/Linux 可使用 Ctrl+C 或 `SIGTERM`,由应用主循环处理停止请求。Windows 在应用控制台输入 `exit`。正常停止时,Sample1 依次输出 `PreShut` 和 `Shut`。这个示例检查不代表所有插件组合都具备相同的就绪条件或停止保证。 @@ -72,8 +90,9 @@ macOS/Linux 可使用 Ctrl+C 或 `SIGTERM`,由应用主循环处理停止请 | 现象 | 检查项 | | --- | --- | | 输出帮助或 `Application parameter is invalid` | 补齐三个必填参数,用 `--help` 核对拼写 | -| 无法读取应用配置或日志配置 | 核对工作目录和两个配置路径,应用配置文件名使用 `*.app.conf` | -| `Load shared library[...] failed` | 核对 `plugin_lib_dir`、平台后缀、Debug 后缀,以及动态库自身的共享库依赖 | +| 应用配置加载失败 | 检查工作目录、显式路径和文件格式;小写 `.toml` 后缀选择 TOML,旧版 `*.app.conf` 使用 XML | +| 日志配置无法读取 | 检查工作目录和 `--log_conf` 路径 | +| `Load shared library[...] failed` | 核对 TOML `plugin_library_dir` 或 XML `plugin_lib_dir`、平台后缀、Debug 后缀,以及动态库自身的共享库依赖 | | 插件已加载,但目标模块不存在 | 核对模块注册、启用的构建目标和依赖插件,详见[创建插件](plugins_CN.md) | | Kernel 元数据或配置缺失 | 在进程工作目录准备生成的 `resource/meta/` 和 `resource/server/` 文件 |