已合并
fix(driver): 修复注册失败清理并补齐插件测试部署 #416
liuxingsen创建于 9月24日
fix(driver): 修复注册失败清理并补齐插件测试部署 #416
已合并
共 23 个文件变更+443-18
| @@ -1,3 +1,13 @@ | |||
| 1 | +# Copyright (c) 2025 Huawei Technologies Co., Ltd. | ||
| 2 | +# openUBMC is licensed under Mulan PSL v2. | ||
| 3 | +# You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 4 | +# You may obtain a copy of Mulan PSL v2 at: | ||
| 5 | +# http://license.coscl.org.cn/MulanPSL2 | ||
| 6 | +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 7 | +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 8 | +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 9 | +# See the Mulan PSL v2 for more details. | ||
| 10 | + | ||
| 1 | import os | 11 | import os |
| 2 | from conanbase import ConanBase | 12 | from conanbase import ConanBase |
| 3 | from conan.tools.meson import Meson, MesonToolchain | 13 | from conan.tools.meson import Meson, MesonToolchain |
| @@ -53,6 +53,9 @@ using device_drivers = std::vector<std::shared_ptr<device_driver_t>>; | |||
| 53 | 53 | ||
| 54 | mc::string dump_device_driver(const device_driver_t& driver, driver_handle_t handle); | 54 | mc::string dump_device_driver(const device_driver_t& driver, driver_handle_t handle); |
| 55 | 55 | ||
| 56 | +// 同一实例的加载、卸载和设备访问由调用者串行化;本类不提供实例级并发保护。 | ||
| 57 | +// 工厂加载候选库时使用独立实例,加载成功后才在缓存锁内发布。 | ||
| 58 | +// get_device/get_devices 返回的引用及设备中的插件指针不得跨越 unload 的生命周期。 | ||
| 56 | class device_driver { | 59 | class device_driver { |
| 57 | public: | 60 | public: |
| 58 | device_driver(const std::string& driver_name); | 61 | device_driver(const std::string& driver_name); |
| @@ -1,6 +1,13 @@ | |||
| 1 | /* | 1 | /* |
| 2 | * Copyright (c) 2026 Huawei Technologies Co., Ltd. | 2 | * Copyright (c) 2026 Huawei Technologies Co., Ltd. |
| 3 | * openUBMC is licensed under Mulan PSL v2. | 3 | * openUBMC is licensed under Mulan PSL v2. |
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 4 | */ | 11 | */ |
| 5 | 12 | ||
| 6 | #ifndef DEVMON_COMMON_DRIVER_SOURCE_H | 13 | #ifndef DEVMON_COMMON_DRIVER_SOURCE_H |
| @@ -1,3 +1,15 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * openUBMC is licensed under Mulan PSL v2. | ||
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 11 | + */ | ||
| 12 | + | ||
| 1 | #include <devmon-common/csr_types.h> | 13 | #include <devmon-common/csr_types.h> |
| 2 | #include <mc/reflect.h> | 14 | #include <mc/reflect.h> |
| 3 | 15 | ||
| @@ -66,6 +66,9 @@ void device_driver::load_file(const std::string& library_file, const driver_load | |||
| 66 | MC_THROW(mc::system_exception, "Dynamic driver loading is not supported on Zephyr for driver: ${name}", | 66 | MC_THROW(mc::system_exception, "Dynamic driver loading is not supported on Zephyr for driver: ${name}", |
| 67 | ("name", m_driver_name)); | 67 | ("name", m_driver_name)); |
| 68 | #else | 68 | #else |
| 69 | + MC_ASSERT_THROW(m_driver_handle == nullptr, mc::invalid_arg_exception, "Driver already loaded: ${name}", | ||
O | |||
| 70 | + ("name", m_driver_name)); | ||
| 71 | + | ||
| 69 | auto start = std::chrono::steady_clock::now(); | 72 | auto start = std::chrono::steady_clock::now(); |
| 70 | 73 | ||
| 71 | driver_load_info info; | 74 | driver_load_info info; |
| @@ -100,17 +103,30 @@ void device_driver::load_file(const std::string& library_file, const driver_load | |||
| 100 | 103 | ||
| 101 | m_driver_handle = driver_handle; | 104 | m_driver_handle = driver_handle; |
| 102 | 105 | ||
| 106 | + bool registration_failed = false; | ||
| 107 | + std::string registration_error; | ||
| 103 | try { | 108 | try { |
| 104 | register_driver(); | 109 | register_driver(); |
| 105 | info.success = true; | 110 | info.success = true; |
| 106 | info.device_count = m_devices.size(); | 111 | info.device_count = m_devices.size(); |
| 107 | } catch (const std::exception& e) { | 112 | } catch (const std::exception& e) { |
| 108 | - info.error_msg = e.what(); | 113 | + registration_error = e.what(); |
| 114 | + registration_failed = true; | ||
| 115 | + } catch (...) { | ||
| 116 | + registration_error = "unknown driver registration error"; | ||
| 117 | + registration_failed = true; | ||
| 109 | } | 118 | } |
| 110 | 119 | ||
| 111 | info.load_time_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count(); | 120 | info.load_time_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count(); |
| 112 | 121 | ||
| 122 | + if (registration_failed) { | ||
| 123 | + info.error_msg = registration_error; | ||
| 124 | + unload(); | ||
| 125 | + } | ||
| 113 | record_load_info(std::move(info)); | 126 | record_load_info(std::move(info)); |
| 127 | + if (registration_failed) { | ||
| 128 | + MC_THROW(mc::bad_function_call_exception, "Failed to register driver: ${error}", ("error", registration_error)); | ||
| 129 | + } | ||
| 114 | 130 | ||
| 115 | dlog("Driver ${name} loaded successfully from ${file}", ("name", m_driver_name)("file", library_file)); | 131 | dlog("Driver ${name} loaded successfully from ${file}", ("name", m_driver_name)("file", library_file)); |
| 116 | #endif | 132 | #endif |
| @@ -119,8 +135,8 @@ void device_driver::load_file(const std::string& library_file, const driver_load | |||
| 119 | void device_driver::unload() | 135 | void device_driver::unload() |
| 120 | { | 136 | { |
| 121 | #ifndef __ZEPHYR__ | 137 | #ifndef __ZEPHYR__ |
| 122 | - // 先将 handle 保存到局部变量并清空成员,形成 "exchange-then-close" 模式, | 138 | + // 调用者保证同一实例串行访问。先清空成员,使后续串行 unload 不会再次关闭句柄; |
| 123 | - // 防止多线程场景下 TOCTOU 竞态导致同一 handle 被 dlclose 两次。 | 139 | + // 普通指针的读取和赋值并非原子交换,不能据此保证并发 load/unload 安全。 |
| 124 | void* handle = m_driver_handle; | 140 | void* handle = m_driver_handle; |
| 125 | m_driver_handle = nullptr; | 141 | m_driver_handle = nullptr; |
| 126 | 142 | ||
| @@ -1,6 +1,13 @@ | |||
| 1 | /* | 1 | /* |
| 2 | * Copyright (c) 2026 Huawei Technologies Co., Ltd. | 2 | * Copyright (c) 2026 Huawei Technologies Co., Ltd. |
| 3 | * openUBMC is licensed under Mulan PSL v2. | 3 | * openUBMC is licensed under Mulan PSL v2. |
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 4 | */ | 11 | */ |
| 5 | 12 | ||
| 6 | #include <devmon-common/driver_source.h> | 13 | #include <devmon-common/driver_source.h> |
| @@ -1,3 +1,15 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * openUBMC is licensed under Mulan PSL v2. | ||
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 11 | + */ | ||
| 12 | + | ||
| 1 | /* | 13 | /* |
| 2 | * Mock implementation of dlsym and dlclose for testing | 14 | * Mock implementation of dlsym and dlclose for testing |
| 3 | */ | 15 | */ |
| @@ -0,0 +1,18 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * openUBMC is licensed under Mulan PSL v2. | ||
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 11 | + */ | ||
| 12 | + | ||
| 13 | +#include <devmon-common/driver_abi.h> | ||
| 14 | + | ||
| 15 | +extern "C" __attribute__((visibility("default"))) status_t register_device_driver(device_driver_t**, uint8_t*) | ||
| 16 | +{ | ||
| 17 | + throw 42; | ||
| 18 | +} | ||
| @@ -0,0 +1,30 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * openUBMC is licensed under Mulan PSL v2. | ||
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 11 | + */ | ||
| 12 | + | ||
| 13 | +#include <devmon-common/driver_abi.h> | ||
| 14 | + | ||
| 15 | +#include <exception> | ||
| 16 | + | ||
| 17 | +namespace { | ||
| 18 | +class plugin_exception : public std::exception { | ||
| 19 | +public: | ||
| 20 | + const char* what() const noexcept override | ||
| 21 | + { | ||
| 22 | + return "plugin registration failure"; | ||
| 23 | + } | ||
| 24 | +}; | ||
| 25 | +} // namespace | ||
| 26 | + | ||
| 27 | +extern "C" __attribute__((visibility("default"))) status_t register_device_driver(device_driver_t**, uint8_t*) | ||
| 28 | +{ | ||
| 29 | + throw plugin_exception{}; | ||
| 30 | +} | ||
| @@ -48,7 +48,27 @@ common_test_exe = executable( | |||
| 48 | install_dir: get_option('bindir'), | 48 | install_dir: get_option('bindir'), |
| 49 | ) | 49 | ) |
| 50 | 50 | ||
| 51 | +memory_throwing_driver = shared_library( | ||
| 52 | + 'memory_throwing_driver', | ||
| 53 | + 'memory_throwing_driver.cpp', | ||
| 54 | + include_directories: [common_inc], | ||
| 55 | + install: true, | ||
| 56 | + install_dir: get_option('bindir'), | ||
| 57 | +) | ||
| 58 | + | ||
| 59 | +memory_nonstandard_throwing_driver = shared_library( | ||
| 60 | + 'memory_nonstandard_throwing_driver', | ||
| 61 | + 'memory_nonstandard_throwing_driver.cpp', | ||
| 62 | + include_directories: [common_inc], | ||
| 63 | + install: true, | ||
| 64 | + install_dir: get_option('bindir'), | ||
| 65 | +) | ||
| 66 | + | ||
| 67 | +memory_test_drivers = [memory_throwing_driver, memory_nonstandard_throwing_driver] | ||
| 68 | +common_test_driver_dir = meson.current_build_dir() | ||
| 69 | + | ||
| 51 | # Register test | 70 | # Register test |
| 52 | common_test_env = environment() | 71 | common_test_env = environment() |
| 53 | common_test_env.set('MC_SHM_REGION', 'devmon.test.common') | 72 | common_test_env.set('MC_SHM_REGION', 'devmon.test.common') |
| 54 | -test('common_test', common_test_exe, env: common_test_env, depends: test_data_deps) | 73 | +common_test_env.set('DEVMON_TEST_DRIVER_DIR', common_test_driver_dir) |
| 74 | +test('common_test', common_test_exe, env: common_test_env, depends: test_data_deps + memory_test_drivers) | ||
| @@ -30,8 +30,10 @@ | |||
| 30 | #include <atomic> | 30 | #include <atomic> |
| 31 | #include <chrono> | 31 | #include <chrono> |
| 32 | #include <condition_variable> | 32 | #include <condition_variable> |
| 33 | +#include <cstdlib> | ||
| 33 | #include <cstring> | 34 | #include <cstring> |
| 34 | #include <dlfcn.h> | 35 | #include <dlfcn.h> |
| 36 | +#include <filesystem> | ||
| 35 | #include <memory> | 37 | #include <memory> |
| 36 | #include <mutex> | 38 | #include <mutex> |
| 37 | #include <thread> | 39 | #include <thread> |
| @@ -105,6 +107,17 @@ void* open_libdl_handle() | |||
| 105 | return handle; | 107 | return handle; |
| 106 | } | 108 | } |
| 107 | 109 | ||
| 110 | +#ifndef __APPLE__ | ||
| 111 | +std::filesystem::path throwing_driver_path(const char* library_name) | ||
| 112 | +{ | ||
| 113 | + const char* driver_dir = std::getenv("DEVMON_TEST_DRIVER_DIR"); | ||
| 114 | + if (driver_dir != nullptr && driver_dir[0] != '\0') { | ||
| 115 | + return std::filesystem::path(driver_dir) / library_name; | ||
| 116 | + } | ||
| 117 | + return std::filesystem::read_symlink("/proc/self/exe").parent_path() / library_name; | ||
| 118 | +} | ||
| 119 | +#endif | ||
| 120 | + | ||
| 108 | bool g_register_driver_force_error = true; | 121 | bool g_register_driver_force_error = true; |
| 109 | bool g_register_driver_return_empty = false; | 122 | bool g_register_driver_return_empty = false; |
| 110 | device_driver_t g_stub_device_entry = {"StubDevice", stub_ctor, stub_init, stub_start, stub_stop}; | 123 | device_driver_t g_stub_device_entry = {"StubDevice", stub_ctor, stub_init, stub_start, stub_stop}; |
| @@ -394,6 +407,106 @@ TEST(DeviceDriverTest, LoadMissingLibraryThrowsSystemException) | |||
| 394 | EXPECT_THROW(driver.load("/tmp/devmon_missing_path"), mc::system_exception); | 407 | EXPECT_THROW(driver.load("/tmp/devmon_missing_path"), mc::system_exception); |
| 395 | } | 408 | } |
| 396 | 409 | ||
| 410 | +#ifndef __APPLE__ | ||
| 411 | +TEST(DeviceDriverTest, LoadLibraryWithoutRegistrationRejectsAndClosesHandle) | ||
| 412 | +{ | ||
| 413 | + devmon::device_driver driver("missing_registration"); | ||
| 414 | + devmon::device_driver::remove_load_info("libdl.so.2"); | ||
| 415 | + | ||
| 416 | + EXPECT_THROW(driver.load_file("libdl.so.2"), mc::bad_function_call_exception); | ||
| 417 | + EXPECT_EQ(driver.m_driver_handle, nullptr); | ||
| 418 | + EXPECT_TRUE(driver.get_devices().empty()); | ||
| 419 | + | ||
| 420 | + const auto load_info = devmon::device_driver::get_load_info_snapshot(); | ||
| 421 | + const auto info = std::find_if(load_info.begin(), load_info.end(), [](const auto& entry) { | ||
| 422 | + return entry.driver_name == "libdl.so.2"; | ||
| 423 | + }); | ||
| 424 | + ASSERT_NE(info, load_info.end()); | ||
| 425 | + EXPECT_FALSE(info->success); | ||
| 426 | + EXPECT_FALSE(info->error_msg.empty()); | ||
| 427 | + devmon::device_driver::remove_load_info("libdl.so.2"); | ||
| 428 | +} | ||
| 429 | + | ||
| 430 | +TEST(DeviceDriverTest, RepeatedLoadKeepsOriginalHandle) | ||
| 431 | +{ | ||
| 432 | + devmon::device_driver driver("already_loaded"); | ||
| 433 | + void* original_handle = dlopen(nullptr, RTLD_LAZY); | ||
| 434 | + ASSERT_NE(original_handle, nullptr); | ||
| 435 | + driver.m_driver_handle = original_handle; | ||
| 436 | + | ||
| 437 | + EXPECT_THROW(driver.load_file("libdl.so.2"), mc::invalid_arg_exception); | ||
| 438 | + EXPECT_EQ(driver.m_driver_handle, original_handle); | ||
| 439 | + | ||
| 440 | + if (driver.m_driver_handle != original_handle) { | ||
| 441 | + dlclose(original_handle); | ||
| 442 | + } | ||
| 443 | +} | ||
| 444 | + | ||
| 445 | +TEST(DeviceDriverTest, NonstandardRegistrationExceptionClosesHandle) | ||
| 446 | +{ | ||
| 447 | + devmon::device_driver driver("throwing_registration"); | ||
| 448 | + const auto library_path = throwing_driver_path("libmemory_nonstandard_throwing_driver.so"); | ||
| 449 | + ASSERT_TRUE(std::filesystem::is_regular_file(library_path)) << "Missing test driver: " << library_path; | ||
| 450 | + devmon::device_driver::remove_load_info(library_path.filename().string()); | ||
| 451 | + | ||
| 452 | + try { | ||
| 453 | + driver.load_file(library_path.string()); | ||
| 454 | + FAIL() << "Expected a driver registration exception"; | ||
| 455 | + } catch (const mc::bad_function_call_exception& error) { | ||
| 456 | + EXPECT_STREQ(error.what(), "Failed to register driver: unknown driver registration error"); | ||
| 457 | + } | ||
| 458 | + | ||
| 459 | + EXPECT_EQ(driver.m_driver_handle, nullptr); | ||
| 460 | + EXPECT_TRUE(driver.get_devices().empty()); | ||
| 461 | + void* loaded_handle = dlopen(library_path.c_str(), RTLD_NOLOAD | RTLD_LAZY); | ||
| 462 | + EXPECT_EQ(loaded_handle, nullptr); | ||
| 463 | + if (loaded_handle != nullptr) { | ||
| 464 | + dlclose(loaded_handle); | ||
| 465 | + } | ||
| 466 | + | ||
| 467 | + const auto load_info = devmon::device_driver::get_load_info_snapshot(); | ||
| 468 | + const auto info = std::find_if(load_info.begin(), load_info.end(), [&library_path](const auto& entry) { | ||
| 469 | + return entry.driver_name == library_path.filename().string(); | ||
| 470 | + }); | ||
| 471 | + ASSERT_NE(info, load_info.end()); | ||
| 472 | + EXPECT_FALSE(info->success); | ||
| 473 | + EXPECT_EQ(info->error_msg, "unknown driver registration error"); | ||
| 474 | + devmon::device_driver::remove_load_info(library_path.filename().string()); | ||
| 475 | +} | ||
| 476 | + | ||
| 477 | +TEST(DeviceDriverTest, PluginDefinedRegistrationExceptionClosesHandle) | ||
| 478 | +{ | ||
| 479 | + devmon::device_driver driver("throwing_registration"); | ||
| 480 | + const auto library_path = throwing_driver_path("libmemory_throwing_driver.so"); | ||
| 481 | + ASSERT_TRUE(std::filesystem::is_regular_file(library_path)) << "Missing test driver: " << library_path; | ||
| 482 | + devmon::device_driver::remove_load_info(library_path.filename().string()); | ||
| 483 | + | ||
| 484 | + try { | ||
| 485 | + driver.load_file(library_path.string()); | ||
| 486 | + FAIL() << "Expected a driver registration exception"; | ||
| 487 | + } catch (const mc::bad_function_call_exception& error) { | ||
| 488 | + EXPECT_STREQ(error.what(), "Failed to register driver: plugin registration failure"); | ||
| 489 | + } | ||
| 490 | + | ||
| 491 | + EXPECT_EQ(driver.m_driver_handle, nullptr); | ||
| 492 | + EXPECT_TRUE(driver.get_devices().empty()); | ||
| 493 | + void* loaded_handle = dlopen(library_path.c_str(), RTLD_NOLOAD | RTLD_LAZY); | ||
| 494 | + EXPECT_EQ(loaded_handle, nullptr); | ||
| 495 | + if (loaded_handle != nullptr) { | ||
| 496 | + dlclose(loaded_handle); | ||
| 497 | + } | ||
| 498 | + | ||
| 499 | + const auto load_info = devmon::device_driver::get_load_info_snapshot(); | ||
| 500 | + const auto info = std::find_if(load_info.begin(), load_info.end(), [&library_path](const auto& entry) { | ||
| 501 | + return entry.driver_name == library_path.filename().string(); | ||
| 502 | + }); | ||
| 503 | + ASSERT_NE(info, load_info.end()); | ||
| 504 | + EXPECT_FALSE(info->success); | ||
| 505 | + EXPECT_EQ(info->error_msg, "plugin registration failure"); | ||
| 506 | + devmon::device_driver::remove_load_info(library_path.filename().string()); | ||
| 507 | +} | ||
| 508 | +#endif | ||
| 509 | + | ||
| 397 | TEST(DeviceDriverTest, RegisterDriverThrowsWhenSymbolMissing) | 510 | TEST(DeviceDriverTest, RegisterDriverThrowsWhenSymbolMissing) |
| 398 | { | 511 | { |
| 399 | devmon::device_driver driver("missing_symbol_driver"); | 512 | devmon::device_driver driver("missing_symbol_driver"); |
| @@ -1,6 +1,13 @@ | |||
| 1 | /* | 1 | /* |
| 2 | * Copyright (c) 2026 Huawei Technologies Co., Ltd. | 2 | * Copyright (c) 2026 Huawei Technologies Co., Ltd. |
| 3 | * openUBMC is licensed under Mulan PSL v2. | 3 | * openUBMC is licensed under Mulan PSL v2. |
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 4 | */ | 11 | */ |
| 5 | 12 | ||
| 6 | #include <devmon-common/driver_source.h> | 13 | #include <devmon-common/driver_source.h> |
| @@ -3,10 +3,10 @@ | |||
| 3 | * openUBMC is licensed under Mulan PSL v2. | 3 | * openUBMC is licensed under Mulan PSL v2. |
| 4 | * You can use this software according to the terms and conditions of the Mulan PSL v2. | 4 | * You can use this software according to the terms and conditions of the Mulan PSL v2. |
| 5 | * You may obtain a copy of Mulan PSL v2 at: | 5 | * You may obtain a copy of Mulan PSL v2 at: |
| 6 | - * http://license.coscl.org.cn/Mulan PSL v2 | 6 | + * http://license.coscl.org.cn/MulanPSL2 |
| 7 | * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | 7 | * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, |
| 8 | * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | 8 | * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, |
| 9 | - * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. | 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. |
| 10 | * See the Mulan PSL v2 for more details. | 10 | * See the Mulan PSL v2 for more details. |
| 11 | */ | 11 | */ |
| 12 | 12 | ||
| @@ -2,7 +2,7 @@ | |||
| 2 | * Copyright (c) 2025 Huawei Technologies Co., Ltd. | 2 | * Copyright (c) 2025 Huawei Technologies Co., Ltd. |
| 3 | * openUBMC is licensed under Mulan PSL v2. | 3 | * openUBMC is licensed under Mulan PSL v2. |
| 4 | * You can use this software according to the terms and conditions of the Mulan PSL v2. | 4 | * You can use this software according to the terms and conditions of the Mulan PSL v2. |
| 5 | - * You may obtain a copy of the Mulan PSL v2 at: | 5 | + * You may obtain a copy of Mulan PSL v2 at: |
| 6 | * http://license.coscl.org.cn/MulanPSL2 | 6 | * http://license.coscl.org.cn/MulanPSL2 |
| 7 | * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | 7 | * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, |
| 8 | * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | 8 | * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, |
| @@ -1,3 +1,15 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * openUBMC is licensed under Mulan PSL v2. | ||
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 11 | + */ | ||
| 12 | + | ||
| 1 | #include <devmon/devmon_root_object.h> | 13 | #include <devmon/devmon_root_object.h> |
| 2 | #include <mc/log.h> | 14 | #include <mc/log.h> |
| 3 | 15 | ||
| @@ -66,11 +66,12 @@ if devmon_test_sources.length() > 0 | |||
| 66 | # Register test | 66 | # Register test |
| 67 | devmon_test_env = environment() | 67 | devmon_test_env = environment() |
| 68 | devmon_test_env.set('MC_SHM_REGION', 'devmon.test.service') | 68 | devmon_test_env.set('MC_SHM_REGION', 'devmon.test.service') |
| 69 | + devmon_test_env.set('DEVMON_TEST_DRIVER_DIR', common_test_driver_dir) | ||
| 69 | test( | 70 | test( |
| 70 | 'devmon_test', | 71 | 'devmon_test', |
| 71 | devmon_test_exe, | 72 | devmon_test_exe, |
| 72 | env: devmon_test_env, | 73 | env: devmon_test_env, |
| 73 | - depends: test_data_deps, | 74 | + depends: test_data_deps + memory_test_drivers, |
| 74 | timeout: 120, | 75 | timeout: 120, |
| 75 | ) | 76 | ) |
| 76 | endif | 77 | endif |
| @@ -1,3 +1,15 @@ | |||
| 1 | +/* | ||
| 2 | + * Copyright (c) 2026 Huawei Technologies Co., Ltd. | ||
| 3 | + * openUBMC is licensed under Mulan PSL v2. | ||
| 4 | + * You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | + * You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | + * http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | + * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | + * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | + * See the Mulan PSL v2 for more details. | ||
| 11 | + */ | ||
| 12 | + | ||
| 1 | #include <hwdiscovery/discovery/hwdiscovery_root_object.h> | 13 | #include <hwdiscovery/discovery/hwdiscovery_root_object.h> |
| 2 | 14 | ||
| 3 | MC_REFLECT(devmon::hwdiscovery_root_object, ((m_root_interface, "root"))) | 15 | MC_REFLECT(devmon::hwdiscovery_root_object, ((m_root_interface, "root"))) |
| @@ -75,4 +75,5 @@ hwdiscovery_test_exe = executable( | |||
| 75 | 75 | ||
| 76 | hwdiscovery_test_env = environment() | 76 | hwdiscovery_test_env = environment() |
| 77 | hwdiscovery_test_env.set('MC_SHM_REGION', 'devmon.test.hwdiscovery') | 77 | hwdiscovery_test_env.set('MC_SHM_REGION', 'devmon.test.hwdiscovery') |
| 78 | -test('hwdiscovery_test', hwdiscovery_test_exe, env: hwdiscovery_test_env, depends: test_data_deps) | 78 | +hwdiscovery_test_env.set('DEVMON_TEST_DRIVER_DIR', common_test_driver_dir) |
| 79 | +test('hwdiscovery_test', hwdiscovery_test_exe, env: hwdiscovery_test_env, depends: test_data_deps + memory_test_drivers) | ||
| @@ -1,7 +1,7 @@ | |||
| 1 | -- Copyright (c) 2026 Huawei Technologies Co., Ltd. | 1 | -- Copyright (c) 2026 Huawei Technologies Co., Ltd. |
| 2 | -- openUBMC is licensed under Mulan PSL v2. | 2 | -- openUBMC is licensed under Mulan PSL v2. |
| 3 | --- You can use this software according to the terms and conditions of Mulan PSL v2. | 3 | +-- You can use this software according to the terms and conditions of the Mulan PSL v2. |
| 4 | --- You may obtain a copy of the License at: | 4 | +-- You may obtain a copy of Mulan PSL v2 at: |
| 5 | -- http://license.coscl.org.cn/MulanPSL2 | 5 | -- http://license.coscl.org.cn/MulanPSL2 |
| 6 | -- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | 6 | -- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, |
| 7 | -- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | 7 | -- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, |
| @@ -230,25 +230,30 @@ restore_loop_ld_library_path() { | |||
| 230 | fi | 230 | fi |
| 231 | } | 231 | } |
| 232 | 232 | ||
| 233 | -# 从 meson 测试定义解析 devmon_test 路径与运行时库路径 | 233 | +# 从 meson 测试定义解析 devmon_test 路径、运行时库路径与插件夹具目录 |
| 234 | resolve_devmon_test_runtime() { | 234 | resolve_devmon_test_runtime() { |
| 235 | TEST_EXE="" | 235 | TEST_EXE="" |
| 236 | TEST_LD_PATH="" | 236 | TEST_LD_PATH="" |
| 237 | + local test_driver_dir="${BUILD_DIR}/libs/common/tests" | ||
| 237 | 238 | ||
| 238 | if command -v meson >/dev/null && [ -f "${BUILD_DIR}/build.ninja" ]; then | 239 | if command -v meson >/dev/null && [ -f "${BUILD_DIR}/build.ninja" ]; then |
| 239 | - read -r TEST_EXE TEST_LD_PATH < <( | 240 | + local -a test_runtime=() |
| 241 | + mapfile -t test_runtime < <( | ||
| 240 | meson introspect "${BUILD_DIR}" --tests 2>/dev/null | python3 -c " | 242 | meson introspect "${BUILD_DIR}" --tests 2>/dev/null | python3 -c " |
| 241 | import json, sys | 243 | import json, sys |
| 242 | for test in json.load(sys.stdin): | 244 | for test in json.load(sys.stdin): |
| 243 | if test.get('name') == 'devmon_test': | 245 | if test.get('name') == 'devmon_test': |
| 244 | cmd = test.get('cmd') or [''] | 246 | cmd = test.get('cmd') or [''] |
| 245 | env = test.get('env') or {} | 247 | env = test.get('env') or {} |
| 246 | - print(cmd[0], env.get('LD_LIBRARY_PATH', '')) | 248 | + print(cmd[0]) |
| 249 | + print(env.get('LD_LIBRARY_PATH', '')) | ||
| 250 | + print(env.get('DEVMON_TEST_DRIVER_DIR', '')) | ||
| 247 | break | 251 | break |
| 248 | -else: | 252 | +" 2>/dev/null || true |
| 249 | - print('', '') | ||
| 250 | -" 2>/dev/null || echo " " | ||
| 251 | ) | 253 | ) |
| 254 | + TEST_EXE="${test_runtime[0]:-}" | ||
| 255 | + TEST_LD_PATH="${test_runtime[1]:-}" | ||
| 256 | + test_driver_dir="${test_runtime[2]:-${test_driver_dir}}" | ||
| 252 | fi | 257 | fi |
| 253 | 258 | ||
| 254 | if [ -z "${TEST_EXE}" ] || [ ! -f "${TEST_EXE}" ]; then | 259 | if [ -z "${TEST_EXE}" ] || [ ! -f "${TEST_EXE}" ]; then |
| @@ -263,6 +268,7 @@ else: | |||
| 263 | done | 268 | done |
| 264 | fi | 269 | fi |
| 265 | 270 | ||
| 271 | + export DEVMON_TEST_DRIVER_DIR="${test_driver_dir}" | ||
| 266 | restore_loop_ld_library_path | 272 | restore_loop_ld_library_path |
| 267 | if [ -n "${TEST_LD_PATH}" ]; then | 273 | if [ -n "${TEST_LD_PATH}" ]; then |
| 268 | if [ -n "${LD_LIBRARY_PATH:-}" ]; then | 274 | if [ -n "${LD_LIBRARY_PATH:-}" ]; then |
| @@ -1,4 +1,13 @@ | |||
| 1 | #!/bin/bash | 1 | #!/bin/bash |
| 2 | +# Copyright (c) 2025 Huawei Technologies Co., Ltd. | ||
| 3 | +# openUBMC is licensed under Mulan PSL v2. | ||
| 4 | +# You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 5 | +# You may obtain a copy of Mulan PSL v2 at: | ||
| 6 | +# http://license.coscl.org.cn/MulanPSL2 | ||
| 7 | +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 8 | +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 9 | +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 10 | +# See the Mulan PSL v2 for more details. | ||
| 2 | 11 | ||
| 3 | # 智能编译脚本 - 根据系统资源动态调整编译并发数 | 12 | # 智能编译脚本 - 根据系统资源动态调整编译并发数 |
| 4 | # 版本:2.2 | 13 | # 版本:2.2 |
| @@ -1,3 +1,13 @@ | |||
| 1 | +-- Copyright (c) 2025 Huawei Technologies Co., Ltd. | ||
| 2 | +-- openUBMC is licensed under Mulan PSL v2. | ||
| 3 | +-- You can use this software according to the terms and conditions of the Mulan PSL v2. | ||
| 4 | +-- You may obtain a copy of Mulan PSL v2 at: | ||
| 5 | +-- http://license.coscl.org.cn/MulanPSL2 | ||
| 6 | +-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, | ||
| 7 | +-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, | ||
| 8 | +-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. | ||
| 9 | +-- See the Mulan PSL v2 for more details. | ||
| 10 | + | ||
| 1 | package.path = './temp/opt/bmc/lualib/?.lua;' .. package.path | 11 | package.path = './temp/opt/bmc/lualib/?.lua;' .. package.path |
| 2 | 12 | ||
| 3 | -- 动态查找测试可执行文件 | 13 | -- 动态查找测试可执行文件 |
| @@ -130,8 +130,127 @@ C++ 宿主加载生产 `hwproxy.plugins.*` 快照(power_mgmt / general_hardwar | |||
| 130 | - 连接器位置解析(多种整数类型、字符串、边界情况、类型容错) | 130 | - 连接器位置解析(多种整数类型、字符串、边界情况、类型容错) |
| 131 | - Unit 信息提取(反射解析、类型验证、异常处理、字段容错) | 131 | - Unit 信息提取(反射解析、类型验证、异常处理、字段容错) |
| 132 | 132 | ||
| 133 | +## 动态驱动加载资源回归 | ||
| 134 | + | ||
| 135 | +本节跟踪 [PR !416](https://gitcode.com/openUBMC/devmon/pull/416) / [任务 #144](https://gitcode.com/openUBMC/devmon/issues/144) 的动态加载问题。生产代码位于 [device_driver.cpp](../libs/common/src/device_driver.cpp),回归用例位于 [test_driver.cpp](../libs/common/tests/test_driver.cpp),真实插件夹具为 [memory_throwing_driver.cpp](../libs/common/tests/memory_throwing_driver.cpp) 和 [memory_nonstandard_throwing_driver.cpp](../libs/common/tests/memory_nonstandard_throwing_driver.cpp)。范围限于注册失败清理和重复加载保护,不代表全库内存安全验收。 | ||
| 136 | + | ||
| 137 | +### 问题与清理顺序 | ||
| 138 | + | ||
| 139 | +原 `device_driver::load_file()` 在 `dlopen` 成功、注册失败后仍正常返回,工厂随后可能缓存失败驱动;再次加载还可能覆盖旧句柄,丢失对应的 `dlclose` 入口。当前实现先拒绝已有句柄的重复加载,并在注册失败时清理后向调用者报告失败。 | ||
| 140 | + | ||
| 141 | +以下是同一函数的关键片段;省略日志和耗时统计,不是完整函数。 | ||
| 142 | + | ||
| 143 | +```cpp | ||
| 144 | +// 修复前:捕获注册异常后继续正常返回。 | ||
| 145 | +try { | ||
| 146 | + register_driver(); | ||
| 147 | + info.success = true; | ||
| 148 | + info.device_count = m_devices.size(); | ||
| 149 | +} catch (const std::exception& e) { | ||
| 150 | + info.error_msg = e.what(); | ||
| 151 | +} | ||
| 152 | +``` | ||
| 153 | + | ||
| 154 | +```cpp | ||
| 155 | +// 当前:先复制插件错误,离开 catch 后再卸载动态库。 | ||
| 156 | +bool registration_failed = false; | ||
| 157 | +std::string registration_error; | ||
| 158 | +try { | ||
| 159 | + register_driver(); | ||
| 160 | + info.success = true; | ||
| 161 | + info.device_count = m_devices.size(); | ||
| 162 | +} catch (const std::exception& e) { | ||
| 163 | + registration_error = e.what(); | ||
| 164 | + registration_failed = true; | ||
| 165 | +} catch (...) { | ||
| 166 | + registration_error = "unknown driver registration error"; | ||
| 167 | + registration_failed = true; | ||
| 168 | +} | ||
| 169 | +``` | ||
| 170 | + | ||
| 171 | +`try` 内的函数抛出异常后,控制流进入匹配的 `catch`,不会继续执行成功赋值。`const std::exception& e` 是对异常对象的只读引用,可类比 C 中借用对象的指针;它不会复制异常对象。`e.what()` 返回的错误文本也不应跨越异常对象的寿命使用,赋给 `std::string` 才得到独立拥有的文本副本,类似 C 中另行分配并复制错误字符串。`catch (...)` 还接住 `throw 42` 这类不继承 `std::exception` 的异常。 | ||
| 172 | + | ||
| 173 | +当前失败路径的执行顺序为:复制错误文本 → 离开 `catch` 并销毁本路径的插件异常对象 → `unload()` 清空设备项并 `dlclose` → 记录失败信息 → 抛出 `mc::bad_function_call_exception`。C++ 异常对象销毁时会执行其析构函数;若异常类型由插件定义,这段代码可能位于 `.so`,所以必须先结束异常处理再卸载库。复制后的错误文本仍保留到日志记录和框架异常构造完成。`mc::bad_function_call_exception` 是项目的错误类型,并非 C++ 标准库提供的资源清理机制。 | ||
| 174 | + | ||
| 175 | +[device_driver.h](../libs/common/include/devmon-common/device_driver.h) 明确约定:同一实例的加载、卸载和设备访问由调用者串行化。工厂为每次候选加载创建独立实例,在缓存锁内发布成功结果;竞争失败的独立实例会卸载自身句柄。`unload()` 的普通指针赋值与 C 的 `void *handle = obj->handle; obj->handle = NULL;` 一样,并非原子交换,也没有互斥作用。`get_device()` / `get_devices()` 返回借用引用,设备中的函数指针仍指向插件;调用方必须在卸载前结束使用,不能把 `std::shared_ptr` 持有的设备结构体误认为动态库也被自动保活。 | ||
| 176 | + | ||
| 177 | +### 回归用例与运行入口 | ||
| 178 | + | ||
| 179 | +以下用例均属于 `DeviceDriverTest`: | ||
| 180 | + | ||
| 181 | +| 用例 | 场景及关键断言 | | ||
| 182 | +| --- | --- | | ||
| 183 | +| `LoadLibraryWithoutRegistrationRejectsAndClosesHandle` | 库缺注册入口:抛出错误、句柄置空、设备集合为空,并记录失败 | | ||
| 184 | +| `RepeatedLoadKeepsOriginalHandle` | 已有句柄时再次加载:拒绝操作,保留原句柄 | | ||
| 185 | +| `NonstandardRegistrationExceptionClosesHandle` | 真实插件抛出非标准异常:清理后报错,`RTLD_NOLOAD` 检查插件不再驻留 | | ||
| 186 | +| `PluginDefinedRegistrationExceptionClosesHandle` | 真实插件抛出自定义 `std::exception`:异常对象先于库卸载销毁,检查句柄及驻留状态 | | ||
| 187 | + | ||
| 188 | +从仓库根目录使用正式测试入口: | ||
| 189 | + | ||
| 190 | +```bash | ||
| 191 | +bingo test -ut -jit | ||
| 192 | +bingo test -ut -jit -as | ||
| 193 | +``` | ||
| 194 | + | ||
| 195 | +这是 CI 环境的完整复现入口。本轮本机 Bingo 缺少 `test` 子命令,因此本地使用复用依赖的原生 Meson 构建,不能将其表述为 Bingo 全流程通过。当前 Lua 包装入口不会将 Bingo 的 `-f` 参数转交给 GTest,不能用 `-f 'DeviceDriverTest.*'` 证明这些 C++ 用例已运行;应检查日志中的实际 `RUN` / `OK` / `FAILED` 记录。执行前核对实际构建源码、测试程序和依赖版本,同时保留实际可执行文件路径。新增用例依赖 `libdl.so.2`、`/proc/self/exe` 和 `RTLD_NOLOAD`,目前验证范围为 Linux/glibc。 | ||
| 196 | + | ||
| 197 | +### 插件构建、安装与用例隔离 | ||
| 198 | + | ||
| 199 | +[common 测试构建配置](../libs/common/tests/meson.build) 将 `test_driver.cpp` 同时编入 `common_test` 可执行文件及 `common_test_lib` 静态库;[devmon 测试构建配置](../libs/devmon/tests/meson.build) 链接该静态库。`test_csr.cpp` 引用同一对象文件中的 `test_driver::create()`,因此插件回归用例也会进入 `devmon_test`。这与 C 链接静态库时为满足一个函数引用而取出整个 `.o` 的情形相同。 | ||
| 200 | + | ||
| 201 | +原配置仅构建插件,未安装;测试又只在自身可执行文件目录找库,导致 CI 的 `temp/bin/devmon_test` 缺少夹具。现将两个插件安装到与测试程序相同的 `bindir`,并在所有引用这些用例的 Meson 测试入口声明插件构建依赖: | ||
| 202 | + | ||
| 203 | +| 入口 | 插件定位方式 | | ||
| 204 | +| --- | --- | | ||
| 205 | +| Meson `common_test` / `devmon_test` / unidev 的 `hwdiscovery_test` | Meson 设置 `DEVMON_TEST_DRIVER_DIR`,指向构建目录的 common 测试夹具目录 | | ||
| 206 | +| 安装或移动后的测试程序(包括 CI `temp/bin/devmon_test`) | 未设置该变量时,在 `/proc/self/exe` 所在目录找插件 | | ||
| 207 | + | ||
| 208 | +`scripts/run_tests_loop.sh` 从 Meson 测试定义读取并传递插件目录;缺少定义时使用构建目录的 `libs/common/tests`。直接运行构建目录内的 `devmon_test` 时也应设置 `DEVMON_TEST_DRIVER_DIR`,或使用 Meson 测试入口。部署验收必须取消该变量,防止意外从构建树借到插件而掩盖漏打包。测试先检查夹具存在,再断言注册失败和插件卸载;不能把缺库导致的 `mc::system_exception` 当作注册失败的 `mc::bad_function_call_exception`。 | ||
| 209 | + | ||
| 210 | +两个插件分别固定抛出整数异常和插件定义的 `std::exception`,不再通过 `DEVMON_TEST_STD_EXCEPTION` 切换行为。这里抛整数用于覆盖 `catch (...)`;另一个插件的异常对象包含定义在 `.so` 中的虚函数实现,测试确保捕获结束后才卸载库。路径变量只配置测试资产所在目录,不改变插件行为。夹具只在启用测试的构建中安装,不进入关闭测试的产品构建。 | ||
| 211 | + | ||
| 212 | +### 路径修复与 C++ 写法 | ||
| 213 | + | ||
| 214 | +原测试直接使用可执行文件目录,无法覆盖插件位于 common 目录、程序位于 devmon 目录的构建布局: | ||
| 215 | + | ||
| 216 | +```cpp | ||
| 217 | +// 修改前,来自原测试。 | ||
| 218 | +auto library_path = std::filesystem::read_symlink("/proc/self/exe").parent_path() / "libmemory_throwing_driver.so"; | ||
| 219 | +``` | ||
| 220 | + | ||
| 221 | +现在两个用例各自选固定插件,并提前诊断缺失文件。以下摘自整数异常用例: | ||
| 222 | + | ||
| 223 | +```cpp | ||
| 224 | +const auto library_path = throwing_driver_path("libmemory_nonstandard_throwing_driver.so"); | ||
| 225 | +ASSERT_TRUE(std::filesystem::is_regular_file(library_path)) << "Missing test driver: " << library_path; | ||
| 226 | +``` | ||
| 227 | + | ||
| 228 | +`auto` 让编译器从右侧推导类型,此处得到 `std::filesystem::path`;`const` 表示这个局部路径不再修改。`path` 自己保存路径内容,可类比一个管理字符串的 C 结构体;路径之间的 `/` 是 C++ 重载的拼接操作,不是整数除法。`throwing_driver_path()` 先通过与 C 相同的 `getenv()` 借用目录字符串,再构造自己的路径对象;未设置目录时回到可执行文件目录。`ASSERT_TRUE` 是 GTest 的断言宏,失败即结束当前用例;后面的 `<<` 为诊断输出追加实际路径。它先排除“测试资产缺失”,后面的异常类型、错误文本、加载记录和 `RTLD_NOLOAD` 断言才检验“插件已执行但注册失败并卸载”。 | ||
| 229 | + | ||
| 230 | +```mermaid | ||
| 231 | +flowchart LR | ||
| 232 | + A[原安装配置:只安装测试程序] --> B[temp/bin 缺少插件] | ||
| 233 | + B --> C[dlopen 失败,未执行注册函数] | ||
| 234 | + D[修复:两个固定插件随测试程序安装] --> E[同目录找到插件] | ||
| 235 | + E --> F[调用注册函数并捕获异常] | ||
| 236 | + F --> G[复制错误文本,异常对象销毁] | ||
| 237 | + G --> H[清空设备并 dlclose] | ||
| 238 | + H --> I[断言错误文本及插件不再驻留] | ||
| 239 | +``` | ||
| 240 | + | ||
| 241 | +### 验证记录与限制 | ||
| 242 | + | ||
| 243 | +- 本轮合入主线 `9eea32c` 后,以 GCC 9、原生 Meson、本机既有依赖完成构建;三个已配置测试入口全部通过:调度测试 59 项、`common_test` 415 项、`devmon_test` 1024 项通过,后者另有 14 项 CSR 引用对象用例按现有条件跳过。不是完整 Bingo/产品依赖构建结果。 | ||
| 244 | +- 构建目录中两个主要入口的 `DeviceDriverTest.*` 各 17 项通过。执行实际 `meson install` 后,取消插件目录变量,以安装目录中的库运行两个测试程序,各 17 项随机顺序重复 10 轮通过;临时移走两个已安装夹具时,目标两用例明确因缺文件失败,恢复夹具后通过。 | ||
| 245 | +- `unidev=true` 单独完成 Meson 配置,核对 `common_test`、`devmon_test`、`hwdiscovery_test` 均具有插件目录和两个插件依赖;本轮没有编译或运行完整 unidev 套件。循环脚本的实际 Meson 参数解析、空库路径字段及解析失败回退已验证。 | ||
| 246 | +- 历史本地 `57e786db` 的 ASAN 驱动测试 17/17、`common_test` 408/408 属于旧提交与旧构建,不能计入本轮验证。本轮未重新执行 ASAN 或目标板测试。 | ||
| 247 | +- 用户提供的 2026-10-13 CI 日志对应源提交 `5aa8a6a` 的合成提交 `42a06e874afcb094d0e5cb2dfce139ffa0b7d6b7`:编译了插件,包内 `.so` 列表却不含插件,实际运行 `temp/bin/devmon_test`,最终列出两条插件异常用例失败。附件仅包含失败摘要,没有原始断言全文。 | ||
| 248 | +- [tests/main.cpp](main.cpp) 使用 `std::quick_exit`;仅设置 `detect_leaks=1` 不能证明 LSAN 正常退出扫描已完成。远端 CI 必须按推送后的实际提交单独核对,不能复用上述历史失败或本地结果替代。 | ||
| 249 | + | ||
| 133 | ## 测试统计 | 250 | ## 测试统计 |
| 134 | 251 | ||
| 252 | +以下总数、覆盖率和连续运行统计为既有文档记录,未在本轮重新测量;不能替代上节按提交和入口区分的验证结果。 | ||
| 253 | + | ||
| 135 | ### 总体统计 | 254 | ### 总体统计 |
| 136 | 255 | ||
| 137 | 该测试套件共包含 **646 个测试用例**: | 256 | 该测试套件共包含 **646 个测试用例**: |
| @@ -242,7 +361,7 @@ C++ 宿主加载生产 `hwproxy.plugins.*` 快照(power_mgmt / general_hardwar | |||
| 242 | - ✅ 代码行覆盖率良好(93.1%) | 361 | - ✅ 代码行覆盖率良好(93.1%) |
| 243 | - ✅ 函数覆盖率良好(72.0%) | 362 | - ✅ 函数覆盖率良好(72.0%) |
| 244 | - ⚠️ 分支覆盖率较低(47.6%,仍需改进) | 363 | - ⚠️ 分支覆盖率较低(47.6%,仍需改进) |
| 245 | -- ✅ 测试稳定性良好(100 次连续运行全部通过) | 364 | +- 历史稳定性记录为 100 次连续运行通过;动态加载回归仍有上文列出的 CI 失败,不能据此判断当前完整测试通过。 |
| 246 | 365 | ||
| 247 | ### 改进方向 | 366 | ### 改进方向 |
| 248 | 1. **提高分支覆盖率**:增加条件分支和边界情况的测试 | 367 | 1. **提高分支覆盖率**:增加条件分支和边界情况的测试 |
⚠️ 警告:重复加载守卫存在 TOCTOU 竞态
新增的重复加载守卫
MC_ASSERT_THROW(m_driver_handle == nullptr, ...)是一个先检查后执行的序列(check-then-act),且没有加锁。若同一device_driver实例被两个线程并发调用load_file(),两个线程都可能先读到nullptr,随后各自dlopen出不同句柄并先后写入m_driver_handle,后写入者会覆盖先前句柄,丢失释放入口——正是本 PR 想消除的句柄覆盖/泄漏问题。本文件
unload()里已用 "exchange-then-close" 模式显式规避多线程dlclose竞态,说明该代码库关注并发访问。修复建议:用与
unload()相同的互斥量将「检查 + dlopen + 赋值」串行化;或在文档与头文件注释中明确:同一实例的并发load_file不受支持(当工厂路径总是使用全新实例并通过缓存去重时,这主要是防御性约束)。