Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/guides/applications.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ If a shutdown hook returns `false` or throws, the launcher reports its phase, mo

<Aside type="caution" title="Do not use process survival as the readiness check">

Manifest loading or validation failures return exit code `1` before plugin startup. If `plugin_manager::start()` returns `false`, the launcher reports a startup failure and returns `1` before entering the update loop or starting the Windows command thread. Completed startup work is not automatically rolled back. Argument parsing failures, including help handling, still return `0`. Check the expected module's initialization log and application-specific readiness before routing traffic to a process. A running PID or a zero exit status alone is insufficient.
Manifest loading or validation failures return exit code `1` before plugin startup. If `plugin_manager::start()` returns `false` or a module/plugin startup exception propagates, the launcher reports the failing phase, module or plugin and available exception details, then returns `1` before entering the update loop or starting the Windows command thread. The launcher does not roll back partial startup, run normal shutdown hooks or retry in the same process. Deployment tools should start a fresh process after this failure exit. Missing libraries or `DllEntryPlugin` entrypoints also report `load_plugin: <plugin>` and return `1`. An already loaded library missing its entrypoint is retained until process exit; it is not automatically unloaded. Argument parsing failures, including help handling, still return `0`. Check the expected module's initialization log and application-specific readiness before routing traffic to a process. A running PID or a zero exit status alone is insufficient.

</Aside>

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/applications_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ macOS/Linux 可使用 Ctrl+C 或 `SIGTERM`,由应用主循环处理停止请

<Aside type="caution" title="进程存在不等于应用就绪">

应用配置读取或校验失败时,启动器会在启动插件前返回退出码 `1`。`plugin_manager::start()` 返回 `false` 时,启动器会报告启动失败并返回 `1`,不进入更新循环,也不启动 Windows 命令线程;已经完成的启动工作不会自动回滚。参数解析失败(包括帮助处理)仍返回 `0`。接入流量前,应检查目标模块的初始化日志和应用自身的就绪条件,不能只判断 PID 存在或退出码为零。
应用配置读取或校验失败时,启动器会在启动插件前返回退出码 `1`。`plugin_manager::start()` 返回 `false`,或模块、插件启动异常向外传播时,启动器输出失败阶段、模块或插件名及可用异常信息,随后返回 `1`,不进入更新循环,也不启动 Windows 命令线程。启动器不会回滚部分启动工作、执行正常关闭钩子或在同一进程中重试;部署工具应在失败退出后启动新进程。缺少库文件或 `DllEntryPlugin` 入口时,也会报告 `load_plugin: <plugin>` 并返回 `1`。已加载但缺少入口的库保留至进程退出,不会自动卸载。参数解析失败(包括帮助处理)仍返回 `0`。接入流量前,应检查目标模块的初始化日志和应用自身的就绪条件,不能只判断 PID 存在或退出码为零。

</Aside>

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ Normal shutdown first stops managed top-level task admission and keeps pumping a

<Aside type="caution" title="Handle the manager's startup result">

The first `false` from `init()`, `post_init()`, `check_config()` or `pre_update()` stops that stage and all later startup stages, and `plugin_manager::start()` returns `false`. Each callback runs at most once in its stage. Modules that already completed a stage are not automatically rolled back.
The first `false` from `init()`, `post_init()`, `check_config()` or `pre_update()` stops that stage and all later startup stages, and `plugin_manager::start()` returns `false`. Each callback runs at most once in its stage. Exceptions propagate unchanged and also stop the remaining startup calls. `get_startup_failure()` retains the current `phase: module` context on failure, or `load_plugin: plugin` while loading a library and invoking its entrypoint; it is empty after successful startup and must be read on the manager's owner thread. Modules that already completed a stage are not automatically rolled back.

The application launcher checks this result and returns exit code `1` before its update loop or Windows command thread starts. This failure path does not call `pre_shut()` or `shut()` for partial-start cleanup. `update()` return values remain ignored. Keep the runtime plugin selection complete and check startup results in callers that manage their own loop.
The application launcher reports the retained context and any exception details, then returns exit code `1` before its update loop or Windows command thread starts. This failure path does not call `pre_shut()` or `shut()` for partial-start cleanup and does not retry in the same process; deployment tools should start a fresh process. Missing libraries and `DllEntryPlugin` entrypoints return false with `load_plugin: <plugin>` context. A loaded library missing its entrypoint remains retained until process exit because its static initializers may have started work; there is no automatic unload or rollback. Assertions for other program invariants remain unchanged. `update()` return values remain ignored. Keep the runtime plugin selection complete and check startup results in callers that manage their own loop.

</Aside>

Expand Down
4 changes: 2 additions & 2 deletions docs/guides/modules_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ private:

<Aside type="caution" title="检查管理器的启动结果">

`init()`、`post_init()`、`check_config()` 或 `pre_update()` 首次返回 `false` 时,管理器停止该阶段的剩余模块及后续启动阶段,`plugin_manager::start()` 返回 `false`。同一阶段内每个回调至多执行一次;已经完成的阶段不会自动回滚。
`init()`、`post_init()`、`check_config()` 或 `pre_update()` 首次返回 `false` 时,管理器停止该阶段的剩余模块及后续启动阶段,`plugin_manager::start()` 返回 `false`。同一阶段内每个回调至多执行一次。异常保持原样向外传播,也会停止剩余启动调用。失败时,`get_startup_failure()` 保留当前的 `phase: module` 上下文;库加载及其入口调用使用 `load_plugin: plugin`。成功启动后上下文为空,读取时必须位于管理器所属线程。已经完成的阶段不会自动回滚。

应用启动器会检查这个结果,并在进入更新循环或启动 Windows 命令线程前返回退出码 `1`。这个失败路径不会调用 `pre_shut()` 或 `shut()` 清理部分启动资源。`update()` 的返回值仍被忽略。部署时应完整配置依赖插件;自行管理循环的调用方需要检查启动结果。
应用启动器输出保留的上下文及可用异常信息,并在进入更新循环或启动 Windows 命令线程前返回退出码 `1`。这个失败路径不会调用 `pre_shut()` 或 `shut()` 清理部分启动资源,也不会在同一进程中重试;部署工具应启动新进程。缺少库文件或 `DllEntryPlugin` 入口时,管理器返回 false,并保留 `load_plugin: <plugin>` 上下文。已加载但缺少入口的库保留至进程退出,因为其静态初始化可能已启动工作;管理器不会自动卸载或回滚。其他程序不变量的断言保持不变。`update()` 的返回值仍被忽略。部署时应完整配置依赖插件;自行管理循环的调用方需要检查启动结果。

</Aside>

Expand Down
36 changes: 30 additions & 6 deletions include/ark/plugin_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class plugin_manager final : private noncopyable
throw std::logic_error("Plugin manager cannot start during a lifecycle call or after shutdown begins");
}
phase_guard active(*this, call_phase::starting);
startup_failure_.clear();
timestamp_ = timestamp;

plugin_library_dir_ = lib_dir;
Expand Down Expand Up @@ -241,6 +242,11 @@ class plugin_manager final : private noncopyable
return pending_tasks_unchecked();
}

const std::string& get_startup_failure() const {
check_owner();
return startup_failure_;
}

shutdown_state get_shutdown_state() const {
check_owner();
return shutdown_state_;
Expand Down Expand Up @@ -445,8 +451,11 @@ class plugin_manager final : private noncopyable
for (auto const& iter : plugins_)
{
phase_guard installing(*this, call_phase::installing);
bool ret = load_plugin_library(iter.first);
ARK_ASSERT_RET_VAL(ret, false);
startup_failure_ = "load_plugin: " + iter.first;
if (!load_plugin_library(iter.first))
{
return false;
}
}

// initialize all modules
Expand All @@ -455,12 +464,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);

startup_failure_ = "init: " + iter.first;
if (!module_ptr->init())
{
return false;
}
}

startup_failure_.clear();
return true;
}

Expand All @@ -470,12 +481,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);

startup_failure_ = "post_init: " + iter.first;
if (!module_ptr->post_init())
{
return false;
}
}

startup_failure_.clear();
return true;
}

Expand All @@ -485,12 +498,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);

startup_failure_ = "check_config: " + iter.first;
if (!module_ptr->check_config())
{
return false;
}
}

startup_failure_.clear();
return true;
}

Expand All @@ -500,12 +515,14 @@ class plugin_manager final : private noncopyable
module* module_ptr = iter.second;
ARK_ASSERT_CONTINUE(module_ptr != nullptr);

startup_failure_ = "pre_update: " + iter.first;
if (!module_ptr->pre_update())
{
return false;
}
}

startup_failure_.clear();
return true;
}

Expand Down Expand Up @@ -590,7 +607,13 @@ class plugin_manager final : private noncopyable
{
plugin_libs_.insert(std::make_pair(plugin_name, library));
auto func = reinterpret_cast<dll_entry_plugin_func>(library->get_symbol("DllEntryPlugin"));
ARK_ASSERT_RET_VAL(func != nullptr, false);
if (func == nullptr)
{
ARK_LOG_ERROR("Shared library[{}] has no DllEntryPlugin entrypoint", library->get_name());
// Keep an already loaded handle until process exit. Static
// initializers may have started work before symbol validation.
return false;
}
func(this, plugin_name, logger::instance());

return true;
Expand All @@ -603,10 +626,10 @@ class plugin_manager final : private noncopyable
auto error = dlerror();
#endif
ARK_LOG_ERROR("Load shared library[{}] failed, ErrorNo[{}]", library->get_name(), error);
ARK_ASSERT_RET_VAL(0, false);
// Native loading failed, so this object owns no loaded handle.
ARK_DELETE(library);
return false;
}

return true;
}

auto unload_plugin_library(const std::string& plugin_name) -> bool {
Expand Down Expand Up @@ -681,6 +704,7 @@ class plugin_manager final : private noncopyable
shutdown_state shutdown_state_{shutdown_state::running};
std::exception_ptr shutdown_exception_;
std::string shutdown_failure_;
std::string startup_failure_;

// Bus id
int bus_id_{0};
Expand Down
29 changes: 26 additions & 3 deletions src/app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,16 @@ bool main_loop(std::exception_ptr* error = nullptr) {
#endif
}

void report_startup_failure(const char* detail) {
std::cerr << "Application startup failed";
const auto& failure = plugin_manager::instance()->get_startup_failure();
if (!failure.empty())
{
std::cerr << " at " << failure;
}
std::cerr << ": " << detail << std::endl;
}

int main(int argc, char* argv[]) {
std::chrono::milliseconds shutdown_timeout{};
if (!parse_args(argc, argv, shutdown_timeout))
Expand All @@ -324,10 +334,23 @@ int main(int argc, char* argv[]) {
return 1;
}

if (!plugin_manager::instance()->start(
date_time::get_now_time(), manifest.plugin_library_dir, manifest.plugin_config_dir, manifest.plugins))
try
{
if (!plugin_manager::instance()->start(
date_time::get_now_time(), manifest.plugin_library_dir, manifest.plugin_config_dir, manifest.plugins))
{
report_startup_failure("returned false");
return 1;
}
}
catch (const std::exception& error)
{
report_startup_failure(error.what());
return 1;
}
catch (...)
{
std::cerr << "Application startup failed" << std::endl;
report_startup_failure("unknown exception");
return 1;
}

Expand Down
26 changes: 26 additions & 0 deletions tests/app/missing_entry_plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE.
#include <iostream>

#include <ark/macros.hpp>

namespace ark {
class plugin_manager;
}

namespace {

struct library_probe
{
library_probe() {
std::cerr << "app-startup-test:library_loaded\n";
}
};

library_probe loaded_library;

} // namespace

// A loadable library deliberately omitting DllEntryPlugin.
ARK_EXPORT_FUNC void DllExitPlugin(ark::plugin_manager*) {
std::cerr << "app-startup-test:uninstall\n";
}
29 changes: 26 additions & 3 deletions tests/app/startup_failure_plugin.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2026 OpenArkStudio. Licensed under Apache-2.0. See LICENSE.
#include <cstdlib>
#include <iostream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
Expand All @@ -13,7 +14,9 @@ class startup_failure_module final : public ark::module
{
public:
void fail_at(std::string phase) {
failed_phase_ = std::move(phase);
const auto separator = phase.find(':');
behavior_ = separator == std::string::npos ? "false" : phase.substr(0, separator);
failed_phase_ = separator == std::string::npos ? std::move(phase) : phase.substr(separator + 1);
}

const std::string& get_name() const override {
Expand Down Expand Up @@ -58,10 +61,23 @@ class startup_failure_module final : public ark::module
private:
bool record(std::string_view phase) {
std::cerr << "app-startup-test:" << phase << '\n';
return phase != failed_phase_;
if (phase == failed_phase_)
{
if (behavior_ == "throw")
{
throw std::runtime_error("test startup exception in " + std::string(phase));
}
if (behavior_ == "unknown")
{
throw 7;
}
return false;
}
return true;
}

std::string failed_phase_;
std::string behavior_;
bool updated_{};
};

Expand All @@ -73,15 +89,22 @@ startup_failure_module fixture;
ARK_EXPORT_FUNC void DllEntryPlugin(
ark::plugin_manager* manager, const std::string& plugin_name, ark::logger* shared_logger) {
ark::logger::init(shared_logger);
fixture.fail_at(manager->get_plugin_conf(plugin_name));
const auto& mode = manager->get_plugin_conf(plugin_name);
fixture.fail_at(mode);
if (!manager->add_module(fixture.get_name(), &fixture) || !manager->add_update_module(&fixture))
{
std::cerr << "Startup test module registration failed\n";
std::abort();
}
if (mode == "throw:load_plugin")
{
std::cerr << "app-startup-test:load_plugin\n";
throw std::runtime_error("test plugin entry exception");
}
}

ARK_EXPORT_FUNC void DllExitPlugin(ark::plugin_manager* manager) {
std::cerr << "app-startup-test:uninstall\n";
manager->remove_update_module(fixture.get_name());
manager->remove_module(fixture.get_name());
}
Loading