mirror of
https://github.com/FairRootGroup/FairMQ.git
synced 2025-10-13 16:46:47 +00:00
Fix race in plugin manager/services
This commit is contained in:
parent
a53ef79552
commit
ee8afd7d2b
|
@ -223,6 +223,7 @@ target_link_libraries(FairMQ
|
||||||
INTERFACE # only consumers link against interface dependencies
|
INTERFACE # only consumers link against interface dependencies
|
||||||
|
|
||||||
PUBLIC # libFairMQ AND consumers of libFairMQ link aginst public dependencies
|
PUBLIC # libFairMQ AND consumers of libFairMQ link aginst public dependencies
|
||||||
|
pthread
|
||||||
dl
|
dl
|
||||||
Boost::boost
|
Boost::boost
|
||||||
Boost::program_options
|
Boost::program_options
|
||||||
|
|
|
@ -13,10 +13,10 @@
|
||||||
using namespace fair::mq;
|
using namespace fair::mq;
|
||||||
|
|
||||||
DeviceRunner::DeviceRunner(int argc, char* const argv[])
|
DeviceRunner::DeviceRunner(int argc, char* const argv[])
|
||||||
: fRawCmdLineArgs(tools::ToStrVector(argc, argv, false))
|
: fDevice(nullptr)
|
||||||
, fPluginManager(PluginManager::MakeFromCommandLineOptions(fRawCmdLineArgs))
|
, fRawCmdLineArgs(tools::ToStrVector(argc, argv, false))
|
||||||
, fConfig()
|
, fConfig()
|
||||||
, fDevice(nullptr)
|
, fPluginManager(fRawCmdLineArgs)
|
||||||
, fEvents()
|
, fEvents()
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
@ -27,16 +27,16 @@ auto DeviceRunner::Run() -> int
|
||||||
////////////////////////
|
////////////////////////
|
||||||
|
|
||||||
// Load builtin plugins last
|
// Load builtin plugins last
|
||||||
fPluginManager->LoadPlugin("s:control");
|
fPluginManager.LoadPlugin("s:control");
|
||||||
|
|
||||||
////// CALL HOOK ///////
|
////// CALL HOOK ///////
|
||||||
fEvents.Emit<hooks::SetCustomCmdLineOptions>(*this);
|
fEvents.Emit<hooks::SetCustomCmdLineOptions>(*this);
|
||||||
////////////////////////
|
////////////////////////
|
||||||
|
|
||||||
fPluginManager->ForEachPluginProgOptions([&](boost::program_options::options_description options){
|
fPluginManager.ForEachPluginProgOptions([&](boost::program_options::options_description options){
|
||||||
fConfig.AddToCmdLineOptions(options);
|
fConfig.AddToCmdLineOptions(options);
|
||||||
});
|
});
|
||||||
fConfig.AddToCmdLineOptions(fPluginManager->ProgramOptions());
|
fConfig.AddToCmdLineOptions(fPluginManager.ProgramOptions());
|
||||||
|
|
||||||
////// CALL HOOK ///////
|
////// CALL HOOK ///////
|
||||||
fEvents.Emit<hooks::ModifyRawCmdLineArgs>(*this);
|
fEvents.Emit<hooks::ModifyRawCmdLineArgs>(*this);
|
||||||
|
@ -83,16 +83,16 @@ auto DeviceRunner::Run() -> int
|
||||||
fDevice->SetConfig(fConfig);
|
fDevice->SetConfig(fConfig);
|
||||||
|
|
||||||
// Initialize plugin services
|
// Initialize plugin services
|
||||||
fPluginManager->EmplacePluginServices(&fConfig, fDevice);
|
fPluginManager.EmplacePluginServices(&fConfig, *fDevice);
|
||||||
|
|
||||||
// Instantiate and run plugins
|
// Instantiate and run plugins
|
||||||
fPluginManager->InstantiatePlugins();
|
fPluginManager.InstantiatePlugins();
|
||||||
|
|
||||||
// Run the device
|
// Run the device
|
||||||
fDevice->RunStateMachine();
|
fDevice->RunStateMachine();
|
||||||
|
|
||||||
// Wait for control plugin to release device control
|
// Wait for control plugin to release device control
|
||||||
fPluginManager->WaitForPluginsToReleaseDeviceControl();
|
fPluginManager.WaitForPluginsToReleaseDeviceControl();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,10 +61,10 @@ class DeviceRunner
|
||||||
template<typename H>
|
template<typename H>
|
||||||
auto RemoveHook() -> void { fEvents.Unsubscribe<H>("runner"); }
|
auto RemoveHook() -> void { fEvents.Unsubscribe<H>("runner"); }
|
||||||
|
|
||||||
|
std::unique_ptr<FairMQDevice> fDevice;
|
||||||
std::vector<std::string> fRawCmdLineArgs;
|
std::vector<std::string> fRawCmdLineArgs;
|
||||||
std::shared_ptr<PluginManager> fPluginManager;
|
|
||||||
FairMQProgOptions fConfig;
|
FairMQProgOptions fConfig;
|
||||||
std::shared_ptr<FairMQDevice> fDevice;
|
PluginManager fPluginManager;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EventManager fEvents;
|
EventManager fEvents;
|
||||||
|
|
|
@ -154,7 +154,9 @@ struct Machine_ : public state_machine_def<Machine_>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Machine_()
|
Machine_()
|
||||||
: fWork()
|
: fUnblockHandler()
|
||||||
|
, fStateHandlers()
|
||||||
|
, fWork()
|
||||||
, fWorkAvailableCondition()
|
, fWorkAvailableCondition()
|
||||||
, fWorkDoneCondition()
|
, fWorkDoneCondition()
|
||||||
, fWorkMutex()
|
, fWorkMutex()
|
||||||
|
@ -198,10 +200,10 @@ struct Machine_ : public state_machine_def<Machine_>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InitDeviceFct
|
struct DefaultFct
|
||||||
{
|
{
|
||||||
template<typename EVT, typename FSM, typename SourceState, typename TargetState>
|
template<typename EVT, typename FSM, typename SourceState, typename TargetState>
|
||||||
void operator()(EVT const&, FSM& fsm, SourceState& /* ss */, TargetState& ts)
|
void operator()(EVT const& e, FSM& fsm, SourceState& /* ss */, TargetState& ts)
|
||||||
{
|
{
|
||||||
fsm.fState = ts.Type();
|
fsm.fState = ts.Type();
|
||||||
|
|
||||||
|
@ -212,45 +214,7 @@ struct Machine_ : public state_machine_def<Machine_>
|
||||||
}
|
}
|
||||||
fsm.fWorkAvailable = true;
|
fsm.fWorkAvailable = true;
|
||||||
LOG(state) << "Entering " << ts.Name() << " state";
|
LOG(state) << "Entering " << ts.Name() << " state";
|
||||||
fsm.fWork = fsm.fInitWrapperHandler;
|
fsm.fWork = fsm.fStateHandlers.at(e.Type());
|
||||||
fsm.fWorkAvailableCondition.notify_one();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct InitTaskFct
|
|
||||||
{
|
|
||||||
template<typename EVT, typename FSM, typename SourceState, typename TargetState>
|
|
||||||
void operator()(EVT const&, FSM& fsm, SourceState& /* ss */, TargetState& ts)
|
|
||||||
{
|
|
||||||
fsm.fState = ts.Type();
|
|
||||||
|
|
||||||
unique_lock<mutex> lock(fsm.fWorkMutex);
|
|
||||||
while (fsm.fWorkActive)
|
|
||||||
{
|
|
||||||
fsm.fWorkDoneCondition.wait(lock);
|
|
||||||
}
|
|
||||||
fsm.fWorkAvailable = true;
|
|
||||||
LOG(state) << "Entering " << ts.Name() << " state";
|
|
||||||
fsm.fWork = fsm.fInitTaskWrapperHandler;
|
|
||||||
fsm.fWorkAvailableCondition.notify_one();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RunFct
|
|
||||||
{
|
|
||||||
template<typename EVT, typename FSM, typename SourceState, typename TargetState>
|
|
||||||
void operator()(EVT const&, FSM& fsm, SourceState& /* ss */, TargetState& ts)
|
|
||||||
{
|
|
||||||
fsm.fState = ts.Type();
|
|
||||||
|
|
||||||
unique_lock<mutex> lock(fsm.fWorkMutex);
|
|
||||||
while (fsm.fWorkActive)
|
|
||||||
{
|
|
||||||
fsm.fWorkDoneCondition.wait(lock);
|
|
||||||
}
|
|
||||||
fsm.fWorkAvailable = true;
|
|
||||||
LOG(state) << "Entering " << ts.Name() << " state";
|
|
||||||
fsm.fWork = fsm.fRunWrapperHandler;
|
|
||||||
fsm.fWorkAvailableCondition.notify_one();
|
fsm.fWorkAvailableCondition.notify_one();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -303,48 +267,10 @@ struct Machine_ : public state_machine_def<Machine_>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ResetTaskFct
|
|
||||||
{
|
|
||||||
template<typename EVT, typename FSM, typename SourceState, typename TargetState>
|
|
||||||
void operator()(EVT const&, FSM& fsm, SourceState& /* ss */, TargetState& ts)
|
|
||||||
{
|
|
||||||
fsm.fState = ts.Type();
|
|
||||||
|
|
||||||
unique_lock<mutex> lock(fsm.fWorkMutex);
|
|
||||||
while (fsm.fWorkActive)
|
|
||||||
{
|
|
||||||
fsm.fWorkDoneCondition.wait(lock);
|
|
||||||
}
|
|
||||||
fsm.fWorkAvailable = true;
|
|
||||||
LOG(state) << "Entering " << ts.Name() << " state";
|
|
||||||
fsm.fWork = fsm.fResetTaskWrapperHandler;
|
|
||||||
fsm.fWorkAvailableCondition.notify_one();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ResetDeviceFct
|
|
||||||
{
|
|
||||||
template<typename EVT, typename FSM, typename SourceState, typename TargetState>
|
|
||||||
void operator()(EVT const&, FSM& fsm, SourceState& /* ss */, TargetState& ts)
|
|
||||||
{
|
|
||||||
fsm.fState = ts.Type();
|
|
||||||
|
|
||||||
unique_lock<mutex> lock(fsm.fWorkMutex);
|
|
||||||
while (fsm.fWorkActive)
|
|
||||||
{
|
|
||||||
fsm.fWorkDoneCondition.wait(lock);
|
|
||||||
}
|
|
||||||
fsm.fWorkAvailable = true;
|
|
||||||
LOG(state) << "Entering " << ts.Name() << " state";
|
|
||||||
fsm.fWork = fsm.fResetWrapperHandler;
|
|
||||||
fsm.fWorkAvailableCondition.notify_one();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ExitingFct
|
struct ExitingFct
|
||||||
{
|
{
|
||||||
template<typename EVT, typename FSM, typename SourceState, typename TargetState>
|
template<typename EVT, typename FSM, typename SourceState, typename TargetState>
|
||||||
void operator()(EVT const&, FSM& fsm, SourceState& /* ss */, TargetState& ts)
|
void operator()(EVT const& e, FSM& fsm, SourceState& /* ss */, TargetState& ts)
|
||||||
{
|
{
|
||||||
LOG(state) << "Entering " << ts.Name() << " state";
|
LOG(state) << "Entering " << ts.Name() << " state";
|
||||||
fsm.fState = ts.Type();
|
fsm.fState = ts.Type();
|
||||||
|
@ -357,7 +283,7 @@ struct Machine_ : public state_machine_def<Machine_>
|
||||||
fsm.fWorkAvailableCondition.notify_one();
|
fsm.fWorkAvailableCondition.notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
fsm.fExitHandler();
|
fsm.fStateHandlers.at(e.Type())();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -375,18 +301,18 @@ struct Machine_ : public state_machine_def<Machine_>
|
||||||
// Transition table for Machine_
|
// Transition table for Machine_
|
||||||
struct transition_table : boost::mpl::vector<
|
struct transition_table : boost::mpl::vector<
|
||||||
// Start Event Next Action Guard
|
// Start Event Next Action Guard
|
||||||
Row<IDLE_FSM_STATE, INIT_DEVICE_FSM_EVENT, INITIALIZING_DEVICE_FSM_STATE, InitDeviceFct, none>,
|
Row<IDLE_FSM_STATE, INIT_DEVICE_FSM_EVENT, INITIALIZING_DEVICE_FSM_STATE, DefaultFct, none>,
|
||||||
Row<IDLE_FSM_STATE, END_FSM_EVENT, EXITING_FSM_STATE, ExitingFct, none>,
|
Row<IDLE_FSM_STATE, END_FSM_EVENT, EXITING_FSM_STATE, ExitingFct, none>,
|
||||||
Row<INITIALIZING_DEVICE_FSM_STATE, internal_DEVICE_READY_FSM_EVENT, DEVICE_READY_FSM_STATE, AutomaticFct, none>,
|
Row<INITIALIZING_DEVICE_FSM_STATE, internal_DEVICE_READY_FSM_EVENT, DEVICE_READY_FSM_STATE, AutomaticFct, none>,
|
||||||
Row<DEVICE_READY_FSM_STATE, INIT_TASK_FSM_EVENT, INITIALIZING_TASK_FSM_STATE, InitTaskFct, none>,
|
Row<DEVICE_READY_FSM_STATE, INIT_TASK_FSM_EVENT, INITIALIZING_TASK_FSM_STATE, DefaultFct, none>,
|
||||||
Row<DEVICE_READY_FSM_STATE, RESET_DEVICE_FSM_EVENT, RESETTING_DEVICE_FSM_STATE, ResetDeviceFct, none>,
|
Row<DEVICE_READY_FSM_STATE, RESET_DEVICE_FSM_EVENT, RESETTING_DEVICE_FSM_STATE, DefaultFct, none>,
|
||||||
Row<INITIALIZING_TASK_FSM_STATE, internal_READY_FSM_EVENT, READY_FSM_STATE, AutomaticFct, none>,
|
Row<INITIALIZING_TASK_FSM_STATE, internal_READY_FSM_EVENT, READY_FSM_STATE, AutomaticFct, none>,
|
||||||
Row<READY_FSM_STATE, RUN_FSM_EVENT, RUNNING_FSM_STATE, RunFct, none>,
|
Row<READY_FSM_STATE, RUN_FSM_EVENT, RUNNING_FSM_STATE, DefaultFct, none>,
|
||||||
Row<READY_FSM_STATE, RESET_TASK_FSM_EVENT, RESETTING_TASK_FSM_STATE, ResetTaskFct, none>,
|
Row<READY_FSM_STATE, RESET_TASK_FSM_EVENT, RESETTING_TASK_FSM_STATE, DefaultFct, none>,
|
||||||
Row<RUNNING_FSM_STATE, PAUSE_FSM_EVENT, PAUSED_FSM_STATE, PauseFct, none>,
|
Row<RUNNING_FSM_STATE, PAUSE_FSM_EVENT, PAUSED_FSM_STATE, DefaultFct, none>,
|
||||||
Row<RUNNING_FSM_STATE, STOP_FSM_EVENT, READY_FSM_STATE, StopFct, none>,
|
Row<RUNNING_FSM_STATE, STOP_FSM_EVENT, READY_FSM_STATE, StopFct, none>,
|
||||||
Row<RUNNING_FSM_STATE, internal_READY_FSM_EVENT, READY_FSM_STATE, InternalStopFct, none>,
|
Row<RUNNING_FSM_STATE, internal_READY_FSM_EVENT, READY_FSM_STATE, InternalStopFct, none>,
|
||||||
Row<PAUSED_FSM_STATE, RUN_FSM_EVENT, RUNNING_FSM_STATE, RunFct, none>,
|
Row<PAUSED_FSM_STATE, RUN_FSM_EVENT, RUNNING_FSM_STATE, DefaultFct, none>,
|
||||||
Row<RESETTING_TASK_FSM_STATE, internal_DEVICE_READY_FSM_EVENT, DEVICE_READY_FSM_STATE, AutomaticFct, none>,
|
Row<RESETTING_TASK_FSM_STATE, internal_DEVICE_READY_FSM_EVENT, DEVICE_READY_FSM_STATE, AutomaticFct, none>,
|
||||||
Row<RESETTING_DEVICE_FSM_STATE, internal_IDLE_FSM_EVENT, IDLE_FSM_STATE, AutomaticFct, none>,
|
Row<RESETTING_DEVICE_FSM_STATE, internal_IDLE_FSM_EVENT, IDLE_FSM_STATE, AutomaticFct, none>,
|
||||||
Row<OK_FSM_STATE, ERROR_FOUND_FSM_EVENT, ERROR_FSM_STATE, ErrorFoundFct, none>>
|
Row<OK_FSM_STATE, ERROR_FOUND_FSM_EVENT, ERROR_FSM_STATE, ErrorFoundFct, none>>
|
||||||
|
@ -425,14 +351,8 @@ struct Machine_ : public state_machine_def<Machine_>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function<void(void)> fInitWrapperHandler;
|
|
||||||
function<void(void)> fInitTaskWrapperHandler;
|
|
||||||
function<void(void)> fRunWrapperHandler;
|
|
||||||
function<void(void)> fPauseWrapperHandler;
|
|
||||||
function<void(void)> fResetWrapperHandler;
|
|
||||||
function<void(void)> fResetTaskWrapperHandler;
|
|
||||||
function<void(void)> fExitHandler;
|
|
||||||
function<void(void)> fUnblockHandler;
|
function<void(void)> fUnblockHandler;
|
||||||
|
unordered_map<FairMQStateMachine::Event, function<void(void)>> fStateHandlers;
|
||||||
|
|
||||||
// function to execute user states in a worker thread
|
// function to execute user states in a worker thread
|
||||||
function<void(void)> fWork;
|
function<void(void)> fWork;
|
||||||
|
@ -457,7 +377,7 @@ struct Machine_ : public state_machine_def<Machine_>
|
||||||
// Wait for work to be done.
|
// Wait for work to be done.
|
||||||
while (!fWorkAvailable && !fWorkerTerminated)
|
while (!fWorkAvailable && !fWorkerTerminated)
|
||||||
{
|
{
|
||||||
fWorkAvailableCondition.wait_for(lock, chrono::milliseconds(300));
|
fWorkAvailableCondition.wait_for(lock, chrono::milliseconds(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fWorkerTerminated)
|
if (fWorkerTerminated)
|
||||||
|
@ -493,13 +413,13 @@ FairMQStateMachine::FairMQStateMachine()
|
||||||
: fChangeStateMutex()
|
: fChangeStateMutex()
|
||||||
, fFsm(new FairMQFSM)
|
, fFsm(new FairMQFSM)
|
||||||
{
|
{
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->fInitWrapperHandler = bind(&FairMQStateMachine::InitWrapper, this);
|
static_pointer_cast<FairMQFSM>(fFsm)->fStateHandlers.emplace(INIT_DEVICE, bind(&FairMQStateMachine::InitWrapper, this));
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->fInitTaskWrapperHandler = bind(&FairMQStateMachine::InitTaskWrapper, this);
|
static_pointer_cast<FairMQFSM>(fFsm)->fStateHandlers.emplace(INIT_TASK, bind(&FairMQStateMachine::InitTaskWrapper, this));
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->fRunWrapperHandler = bind(&FairMQStateMachine::RunWrapper, this);
|
static_pointer_cast<FairMQFSM>(fFsm)->fStateHandlers.emplace(RUN, bind(&FairMQStateMachine::RunWrapper, this));
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->fPauseWrapperHandler = bind(&FairMQStateMachine::PauseWrapper, this);
|
static_pointer_cast<FairMQFSM>(fFsm)->fStateHandlers.emplace(PAUSE, bind(&FairMQStateMachine::PauseWrapper, this));
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->fResetWrapperHandler = bind(&FairMQStateMachine::ResetWrapper, this);
|
static_pointer_cast<FairMQFSM>(fFsm)->fStateHandlers.emplace(RESET_TASK, bind(&FairMQStateMachine::ResetTaskWrapper, this));
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->fResetTaskWrapperHandler = bind(&FairMQStateMachine::ResetTaskWrapper, this);
|
static_pointer_cast<FairMQFSM>(fFsm)->fStateHandlers.emplace(RESET_DEVICE, bind(&FairMQStateMachine::ResetWrapper, this));
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->fExitHandler = bind(&FairMQStateMachine::Exit, this);
|
static_pointer_cast<FairMQFSM>(fFsm)->fStateHandlers.emplace(END, bind(&FairMQStateMachine::Exit, this));
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->fUnblockHandler = bind(&FairMQStateMachine::Unblock, this);
|
static_pointer_cast<FairMQFSM>(fFsm)->fUnblockHandler = bind(&FairMQStateMachine::Unblock, this);
|
||||||
|
|
||||||
static_pointer_cast<FairMQFSM>(fFsm)->start();
|
static_pointer_cast<FairMQFSM>(fFsm)->start();
|
||||||
|
|
|
@ -31,13 +31,55 @@ const std::string fair::mq::PluginManager::fgkLibPrefix = "FairMQPlugin_";
|
||||||
fair::mq::PluginManager::PluginManager()
|
fair::mq::PluginManager::PluginManager()
|
||||||
: fSearchPaths{{"."}}
|
: fSearchPaths{{"."}}
|
||||||
, fPluginFactories()
|
, fPluginFactories()
|
||||||
|
, fPluginServices()
|
||||||
, fPlugins()
|
, fPlugins()
|
||||||
, fPluginOrder()
|
, fPluginOrder()
|
||||||
, fPluginProgOptions()
|
, fPluginProgOptions()
|
||||||
, fPluginServices()
|
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fair::mq::PluginManager::PluginManager(const vector<string> args)
|
||||||
|
: fSearchPaths{{"."}}
|
||||||
|
, fPluginFactories()
|
||||||
|
, fPluginServices()
|
||||||
|
, fPlugins()
|
||||||
|
, fPluginOrder()
|
||||||
|
, fPluginProgOptions()
|
||||||
|
{
|
||||||
|
// Parse command line options
|
||||||
|
auto options = ProgramOptions();
|
||||||
|
auto vm = po::variables_map{};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
auto parsed = po::command_line_parser(args).options(options).allow_unregistered().run();
|
||||||
|
po::store(parsed, vm);
|
||||||
|
po::notify(vm);
|
||||||
|
} catch (const po::error& e)
|
||||||
|
{
|
||||||
|
throw ProgramOptionsParseError{ToString("Error occured while parsing the 'Plugin Manager' program options: ", e.what())};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process plugin search paths
|
||||||
|
auto append = vector<fs::path>{};
|
||||||
|
auto prepend = vector<fs::path>{};
|
||||||
|
auto searchPaths = vector<fs::path>{};
|
||||||
|
if (vm.count("plugin-search-path"))
|
||||||
|
{
|
||||||
|
for (const auto& path : vm["plugin-search-path"].as<vector<string>>())
|
||||||
|
{
|
||||||
|
if (path.substr(0, 1) == "<") { prepend.emplace_back(path.substr(1)); }
|
||||||
|
else if (path.substr(0, 1) == ">") { append.emplace_back(path.substr(1)); }
|
||||||
|
else { searchPaths.emplace_back(path); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set supplied options
|
||||||
|
SetSearchPaths(searchPaths);
|
||||||
|
for(const auto& path : prepend) { PrependSearchPath(path); }
|
||||||
|
for(const auto& path : append) { AppendSearchPath(path); }
|
||||||
|
if (vm.count("plugin")) { LoadPlugins(vm["plugin"].as<vector<string>>()); }
|
||||||
|
}
|
||||||
|
|
||||||
auto fair::mq::PluginManager::ValidateSearchPath(const fs::path& path) -> void
|
auto fair::mq::PluginManager::ValidateSearchPath(const fs::path& path) -> void
|
||||||
{
|
{
|
||||||
if (path.empty()) throw BadSearchPath{"Specified path is empty."};
|
if (path.empty()) throw BadSearchPath{"Specified path is empty."};
|
||||||
|
@ -81,46 +123,6 @@ auto fair::mq::PluginManager::ProgramOptions() -> po::options_description
|
||||||
return plugin_options;
|
return plugin_options;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto fair::mq::PluginManager::MakeFromCommandLineOptions(const vector<string> args) -> shared_ptr<PluginManager>
|
|
||||||
{
|
|
||||||
// Parse command line options
|
|
||||||
auto options = ProgramOptions();
|
|
||||||
auto vm = po::variables_map{};
|
|
||||||
try
|
|
||||||
{
|
|
||||||
auto parsed = po::command_line_parser(args).options(options).allow_unregistered().run();
|
|
||||||
po::store(parsed, vm);
|
|
||||||
po::notify(vm);
|
|
||||||
} catch (const po::error& e)
|
|
||||||
{
|
|
||||||
throw ProgramOptionsParseError{ToString("Error occured while parsing the 'Plugin Manager' program options: ", e.what())};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process plugin search paths
|
|
||||||
auto append = vector<fs::path>{};
|
|
||||||
auto prepend = vector<fs::path>{};
|
|
||||||
auto searchPaths = vector<fs::path>{};
|
|
||||||
if (vm.count("plugin-search-path"))
|
|
||||||
{
|
|
||||||
for (const auto& path : vm["plugin-search-path"].as<vector<string>>())
|
|
||||||
{
|
|
||||||
if (path.substr(0, 1) == "<") { prepend.emplace_back(path.substr(1)); }
|
|
||||||
else if (path.substr(0, 1) == ">") { append.emplace_back(path.substr(1)); }
|
|
||||||
else { searchPaths.emplace_back(path); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create PluginManager with supplied options
|
|
||||||
auto mgr = make_shared<PluginManager>();
|
|
||||||
mgr->SetSearchPaths(searchPaths);
|
|
||||||
for(const auto& path : prepend) { mgr->PrependSearchPath(path); }
|
|
||||||
for(const auto& path : append) { mgr->AppendSearchPath(path); }
|
|
||||||
if (vm.count("plugin")) { mgr->LoadPlugins(vm["plugin"].as<vector<string>>()); }
|
|
||||||
|
|
||||||
// Return the plugin manager and command line options, that have not been recognized.
|
|
||||||
return mgr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto fair::mq::PluginManager::LoadPlugin(const string& pluginName) -> void
|
auto fair::mq::PluginManager::LoadPlugin(const string& pluginName) -> void
|
||||||
{
|
{
|
||||||
if (pluginName.substr(0,2) == "p:")
|
if (pluginName.substr(0,2) == "p:")
|
||||||
|
|
|
@ -47,9 +47,10 @@ namespace mq
|
||||||
class PluginManager
|
class PluginManager
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using PluginFactory = std::shared_ptr<fair::mq::Plugin>(PluginServices&);
|
using PluginFactory = std::unique_ptr<fair::mq::Plugin>(PluginServices&);
|
||||||
|
|
||||||
PluginManager();
|
PluginManager();
|
||||||
|
PluginManager(const std::vector<std::string> args);
|
||||||
|
|
||||||
~PluginManager()
|
~PluginManager()
|
||||||
{
|
{
|
||||||
|
@ -69,7 +70,7 @@ class PluginManager
|
||||||
struct PluginInstantiationError : std::runtime_error { using std::runtime_error::runtime_error; };
|
struct PluginInstantiationError : std::runtime_error { using std::runtime_error::runtime_error; };
|
||||||
|
|
||||||
static auto ProgramOptions() -> boost::program_options::options_description;
|
static auto ProgramOptions() -> boost::program_options::options_description;
|
||||||
static auto MakeFromCommandLineOptions(const std::vector<std::string>) -> std::shared_ptr<PluginManager>;
|
static auto MakeFromCommandLineOptions(const std::vector<std::string>) -> PluginManager;
|
||||||
struct ProgramOptionsParseError : std::runtime_error { using std::runtime_error::runtime_error; };
|
struct ProgramOptionsParseError : std::runtime_error { using std::runtime_error::runtime_error; };
|
||||||
|
|
||||||
static auto LibPrefix() -> const std::string& { return fgkLibPrefix; }
|
static auto LibPrefix() -> const std::string& { return fgkLibPrefix; }
|
||||||
|
@ -116,10 +117,10 @@ class PluginManager
|
||||||
static const std::string fgkLibPrefix;
|
static const std::string fgkLibPrefix;
|
||||||
std::vector<boost::filesystem::path> fSearchPaths;
|
std::vector<boost::filesystem::path> fSearchPaths;
|
||||||
std::map<std::string, std::function<PluginFactory>> fPluginFactories;
|
std::map<std::string, std::function<PluginFactory>> fPluginFactories;
|
||||||
std::map<std::string, std::shared_ptr<Plugin>> fPlugins;
|
std::unique_ptr<PluginServices> fPluginServices;
|
||||||
|
std::map<std::string, std::unique_ptr<Plugin>> fPlugins;
|
||||||
std::vector<std::string> fPluginOrder;
|
std::vector<std::string> fPluginOrder;
|
||||||
std::map<std::string, boost::program_options::options_description> fPluginProgOptions;
|
std::map<std::string, boost::program_options::options_description> fPluginProgOptions;
|
||||||
std::unique_ptr<PluginServices> fPluginServices;
|
|
||||||
}; /* class PluginManager */
|
}; /* class PluginManager */
|
||||||
|
|
||||||
} /* namespace mq */
|
} /* namespace mq */
|
||||||
|
|
|
@ -98,7 +98,7 @@ auto PluginServices::ChangeDeviceState(const std::string& controller, const Devi
|
||||||
|
|
||||||
if (fDeviceController == controller)
|
if (fDeviceController == controller)
|
||||||
{
|
{
|
||||||
fDevice->ChangeState(fkDeviceStateTransitionMap.at(next));
|
fDevice.ChangeState(fkDeviceStateTransitionMap.at(next));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -38,9 +38,9 @@ class PluginServices
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
PluginServices() = delete;
|
PluginServices() = delete;
|
||||||
PluginServices(FairMQProgOptions* config, std::shared_ptr<FairMQDevice> device)
|
PluginServices(FairMQProgOptions* config, FairMQDevice& device)
|
||||||
: fConfig{config}
|
: fConfig(config)
|
||||||
, fDevice{device}
|
, fDevice(device)
|
||||||
, fDeviceController()
|
, fDeviceController()
|
||||||
, fDeviceControllerMutex()
|
, fDeviceControllerMutex()
|
||||||
, fReleaseDeviceControlCondition()
|
, fReleaseDeviceControlCondition()
|
||||||
|
@ -114,7 +114,7 @@ class PluginServices
|
||||||
friend auto operator<<(std::ostream& os, const DeviceStateTransition& transition) -> std::ostream& { return os << ToStr(transition); }
|
friend auto operator<<(std::ostream& os, const DeviceStateTransition& transition) -> std::ostream& { return os << ToStr(transition); }
|
||||||
|
|
||||||
/// @return current device state
|
/// @return current device state
|
||||||
auto GetCurrentDeviceState() const -> DeviceState { return fkDeviceStateMap.at(static_cast<FairMQDevice::State>(fDevice->GetCurrentState())); }
|
auto GetCurrentDeviceState() const -> DeviceState { return fkDeviceStateMap.at(static_cast<FairMQDevice::State>(fDevice.GetCurrentState())); }
|
||||||
|
|
||||||
/// @brief Become device controller
|
/// @brief Become device controller
|
||||||
/// @param controller id
|
/// @param controller id
|
||||||
|
@ -160,14 +160,14 @@ class PluginServices
|
||||||
/// the state is running in.
|
/// the state is running in.
|
||||||
auto SubscribeToDeviceStateChange(const std::string& subscriber, std::function<void(DeviceState /*newState*/)> callback) -> void
|
auto SubscribeToDeviceStateChange(const std::string& subscriber, std::function<void(DeviceState /*newState*/)> callback) -> void
|
||||||
{
|
{
|
||||||
fDevice->SubscribeToStateChange(subscriber, [&,callback](FairMQDevice::State newState){
|
fDevice.SubscribeToStateChange(subscriber, [&,callback](FairMQDevice::State newState){
|
||||||
callback(fkDeviceStateMap.at(newState));
|
callback(fkDeviceStateMap.at(newState));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @brief Unsubscribe from device state changes
|
/// @brief Unsubscribe from device state changes
|
||||||
/// @param subscriber id
|
/// @param subscriber id
|
||||||
auto UnsubscribeFromDeviceStateChange(const std::string& subscriber) -> void { fDevice->UnsubscribeFromStateChange(subscriber); }
|
auto UnsubscribeFromDeviceStateChange(const std::string& subscriber) -> void { fDevice.UnsubscribeFromStateChange(subscriber); }
|
||||||
|
|
||||||
// Config API
|
// Config API
|
||||||
struct PropertyNotFoundError : std::runtime_error { using std::runtime_error::runtime_error; };
|
struct PropertyNotFoundError : std::runtime_error { using std::runtime_error::runtime_error; };
|
||||||
|
@ -272,7 +272,7 @@ class PluginServices
|
||||||
|
|
||||||
private:
|
private:
|
||||||
FairMQProgOptions* fConfig; // TODO make it a shared pointer, once old AliceO2 code is cleaned up
|
FairMQProgOptions* fConfig; // TODO make it a shared pointer, once old AliceO2 code is cleaned up
|
||||||
std::shared_ptr<FairMQDevice> fDevice;
|
FairMQDevice& fDevice;
|
||||||
boost::optional<std::string> fDeviceController;
|
boost::optional<std::string> fDeviceController;
|
||||||
mutable std::mutex fDeviceControllerMutex;
|
mutable std::mutex fDeviceControllerMutex;
|
||||||
std::condition_variable fReleaseDeviceControlCondition;
|
std::condition_variable fReleaseDeviceControlCondition;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#include "FairMQDevice.h"
|
#include "FairMQDevice.h"
|
||||||
|
|
||||||
|
@ -38,7 +39,7 @@ class FairMQBenchmarkSampler : public FairMQDevice
|
||||||
protected:
|
protected:
|
||||||
bool fSameMessage;
|
bool fSameMessage;
|
||||||
int fMsgSize;
|
int fMsgSize;
|
||||||
int fMsgCounter;
|
std::atomic<int> fMsgCounter;
|
||||||
int fMsgRate;
|
int fMsgRate;
|
||||||
uint64_t fNumIterations;
|
uint64_t fNumIterations;
|
||||||
uint64_t fMaxIterations;
|
uint64_t fMaxIterations;
|
||||||
|
|
|
@ -17,12 +17,11 @@ using namespace std;
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// ugly global state, but std::signal gives us no other choice
|
volatile sig_atomic_t gSignalStatus = 0;
|
||||||
std::function<void(int)> gSignalHandlerClosure;
|
|
||||||
|
|
||||||
extern "C" auto signal_handler(int signal) -> void
|
extern "C" auto signal_handler(int signal) -> void
|
||||||
{
|
{
|
||||||
gSignalHandlerClosure(signal);
|
gSignalStatus = signal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,10 +36,13 @@ Control::Control(const string name, const Plugin::Version version, const string
|
||||||
: Plugin(name, version, maintainer, homepage, pluginServices)
|
: Plugin(name, version, maintainer, homepage, pluginServices)
|
||||||
, fControllerThread()
|
, fControllerThread()
|
||||||
, fSignalHandlerThread()
|
, fSignalHandlerThread()
|
||||||
|
, fShutdownThread()
|
||||||
, fEvents()
|
, fEvents()
|
||||||
, fEventsMutex()
|
, fEventsMutex()
|
||||||
|
, fShutdownMutex()
|
||||||
, fNewEvent()
|
, fNewEvent()
|
||||||
, fDeviceTerminationRequested{false}
|
, fDeviceTerminationRequested(false)
|
||||||
|
, fHasShutdown(false)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -73,7 +75,7 @@ Control::Control(const string name, const Plugin::Version version, const string
|
||||||
LOG(debug) << "catch-signals: " << GetProperty<int>("catch-signals");
|
LOG(debug) << "catch-signals: " << GetProperty<int>("catch-signals");
|
||||||
if (GetProperty<int>("catch-signals") > 0)
|
if (GetProperty<int>("catch-signals") > 0)
|
||||||
{
|
{
|
||||||
gSignalHandlerClosure = bind(&Control::SignalHandler, this, placeholders::_1);
|
fSignalHandlerThread = thread(&Control::SignalHandler, this);
|
||||||
signal(SIGINT, signal_handler);
|
signal(SIGINT, signal_handler);
|
||||||
signal(SIGTERM, signal_handler);
|
signal(SIGTERM, signal_handler);
|
||||||
}
|
}
|
||||||
|
@ -263,17 +265,41 @@ auto Control::StaticMode() -> void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Control::SignalHandler(int signal) -> void
|
auto Control::SignalHandler() -> void
|
||||||
{
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (gSignalStatus != 0 && !fHasShutdown)
|
||||||
|
{
|
||||||
|
LOG(info) << "Received device shutdown request (signal " << gSignalStatus << ").";
|
||||||
|
LOG(info) << "Waiting for graceful device shutdown. Hit Ctrl-C again to abort immediately.";
|
||||||
|
|
||||||
if (!fDeviceTerminationRequested)
|
if (!fDeviceTerminationRequested)
|
||||||
{
|
{
|
||||||
fDeviceTerminationRequested = true;
|
fDeviceTerminationRequested = true;
|
||||||
|
gSignalStatus = 0;
|
||||||
|
fShutdownThread = thread(&Control::HandleShutdownSignal, this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LOG(warn) << "Received 2nd device shutdown request (signal " << gSignalStatus << ").";
|
||||||
|
LOG(warn) << "Aborting immediately!";
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (fHasShutdown)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this_thread::sleep_for(chrono::milliseconds(100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Control::HandleShutdownSignal() -> void
|
||||||
|
{
|
||||||
StealDeviceControl();
|
StealDeviceControl();
|
||||||
|
|
||||||
LOG(info) << "Received device shutdown request (signal " << signal << ").";
|
|
||||||
LOG(info) << "Waiting for graceful device shutdown. Hit Ctrl-C again to abort immediately.";
|
|
||||||
|
|
||||||
UnsubscribeFromDeviceStateChange(); // In case, static or interactive mode have subscribed already
|
UnsubscribeFromDeviceStateChange(); // In case, static or interactive mode have subscribed already
|
||||||
SubscribeToDeviceStateChange([&](DeviceState newState)
|
SubscribeToDeviceStateChange([&](DeviceState newState)
|
||||||
{
|
{
|
||||||
|
@ -284,17 +310,13 @@ auto Control::SignalHandler(int signal) -> void
|
||||||
fNewEvent.notify_one();
|
fNewEvent.notify_one();
|
||||||
});
|
});
|
||||||
|
|
||||||
fSignalHandlerThread = thread(&Control::RunShutdownSequence, this);
|
RunShutdownSequence();
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LOG(warn) << "Received 2nd device shutdown request (signal " << signal << ").";
|
|
||||||
LOG(warn) << "Aborting immediately !";
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Control::RunShutdownSequence() -> void
|
auto Control::RunShutdownSequence() -> void
|
||||||
|
{
|
||||||
|
lock_guard<mutex> lock(fShutdownMutex);
|
||||||
|
if (!fHasShutdown)
|
||||||
{
|
{
|
||||||
auto nextState = GetCurrentDeviceState();
|
auto nextState = GetCurrentDeviceState();
|
||||||
EmptyEventQueue();
|
EmptyEventQueue();
|
||||||
|
@ -318,15 +340,18 @@ auto Control::RunShutdownSequence() -> void
|
||||||
ChangeDeviceState(DeviceStateTransition::Resume);
|
ChangeDeviceState(DeviceStateTransition::Resume);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// ignore other states
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextState = WaitForNextState();
|
nextState = WaitForNextState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fHasShutdown = true;
|
||||||
UnsubscribeFromDeviceStateChange();
|
UnsubscribeFromDeviceStateChange();
|
||||||
ReleaseDeviceControl();
|
ReleaseDeviceControl();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
auto Control::RunStartupSequence() -> void
|
auto Control::RunStartupSequence() -> void
|
||||||
{
|
{
|
||||||
|
@ -357,6 +382,7 @@ Control::~Control()
|
||||||
{
|
{
|
||||||
if (fControllerThread.joinable()) fControllerThread.join();
|
if (fControllerThread.joinable()) fControllerThread.join();
|
||||||
if (fSignalHandlerThread.joinable()) fSignalHandlerThread.join();
|
if (fSignalHandlerThread.joinable()) fSignalHandlerThread.join();
|
||||||
|
if (fShutdownThread.joinable()) fShutdownThread.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
} /* namespace plugins */
|
} /* namespace plugins */
|
||||||
|
|
|
@ -37,17 +37,21 @@ class Control : public Plugin
|
||||||
auto PrintInteractiveHelp() -> void;
|
auto PrintInteractiveHelp() -> void;
|
||||||
auto StaticMode() -> void;
|
auto StaticMode() -> void;
|
||||||
auto WaitForNextState() -> DeviceState;
|
auto WaitForNextState() -> DeviceState;
|
||||||
auto SignalHandler(int signal) -> void;
|
auto SignalHandler() -> void;
|
||||||
|
auto HandleShutdownSignal() -> void;
|
||||||
auto RunShutdownSequence() -> void;
|
auto RunShutdownSequence() -> void;
|
||||||
auto RunStartupSequence() -> void;
|
auto RunStartupSequence() -> void;
|
||||||
auto EmptyEventQueue() -> void;
|
auto EmptyEventQueue() -> void;
|
||||||
|
|
||||||
std::thread fControllerThread;
|
std::thread fControllerThread;
|
||||||
std::thread fSignalHandlerThread;
|
std::thread fSignalHandlerThread;
|
||||||
|
std::thread fShutdownThread;
|
||||||
std::queue<DeviceState> fEvents;
|
std::queue<DeviceState> fEvents;
|
||||||
std::mutex fEventsMutex;
|
std::mutex fEventsMutex;
|
||||||
|
std::mutex fShutdownMutex;
|
||||||
std::condition_variable fNewEvent;
|
std::condition_variable fNewEvent;
|
||||||
std::atomic<bool> fDeviceTerminationRequested;
|
std::atomic<bool> fDeviceTerminationRequested;
|
||||||
|
std::atomic<bool> fHasShutdown;
|
||||||
}; /* class Control */
|
}; /* class Control */
|
||||||
|
|
||||||
auto ControlPluginProgramOptions() -> Plugin::ProgOptions;
|
auto ControlPluginProgramOptions() -> Plugin::ProgOptions;
|
||||||
|
|
|
@ -46,7 +46,7 @@ int main(int argc, char* argv[])
|
||||||
// });
|
// });
|
||||||
|
|
||||||
runner.AddHook<InstantiateDevice>([](DeviceRunner& r){
|
runner.AddHook<InstantiateDevice>([](DeviceRunner& r){
|
||||||
r.fDevice = std::shared_ptr<FairMQDevice>{getDevice(r.fConfig)};
|
r.fDevice = std::unique_ptr<FairMQDevice>{getDevice(r.fConfig)};
|
||||||
});
|
});
|
||||||
|
|
||||||
return runner.Run();
|
return runner.Run();
|
||||||
|
|
|
@ -44,7 +44,6 @@ struct PluginServices : ::testing::Test {
|
||||||
{
|
{
|
||||||
fRunStateMachineThread = std::thread(&FairMQDevice::RunStateMachine, mDevice.get());
|
fRunStateMachineThread = std::thread(&FairMQDevice::RunStateMachine, mDevice.get());
|
||||||
mDevice->SetTransport("zeromq");
|
mDevice->SetTransport("zeromq");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~PluginServices()
|
~PluginServices()
|
||||||
|
|
|
@ -104,14 +104,14 @@ TEST(PluginManager, LoadPluginStatic)
|
||||||
TEST(PluginManager, Factory)
|
TEST(PluginManager, Factory)
|
||||||
{
|
{
|
||||||
const auto args = vector<string>{"-l", "debug", "--help", "-S", ">/lib", "</home/user/lib", "/usr/local/lib", "/usr/lib"};
|
const auto args = vector<string>{"-l", "debug", "--help", "-S", ">/lib", "</home/user/lib", "/usr/local/lib", "/usr/lib"};
|
||||||
auto mgr = PluginManager::MakeFromCommandLineOptions(args);
|
PluginManager mgr(args);
|
||||||
const auto path1 = path{"/home/user/lib"};
|
const auto path1 = path{"/home/user/lib"};
|
||||||
const auto path2 = path{"/usr/local/lib"};
|
const auto path2 = path{"/usr/local/lib"};
|
||||||
const auto path3 = path{"/usr/lib"};
|
const auto path3 = path{"/usr/lib"};
|
||||||
const auto path4 = path{"/lib"};
|
const auto path4 = path{"/lib"};
|
||||||
const auto expected = vector<path>{path1, path2, path3, path4};
|
const auto expected = vector<path>{path1, path2, path3, path4};
|
||||||
ASSERT_TRUE(static_cast<bool>(mgr));
|
// ASSERT_TRUE(static_cast<bool>(mgr));
|
||||||
ASSERT_TRUE(mgr->SearchPaths() == expected);
|
ASSERT_TRUE(mgr.SearchPaths() == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(PluginManager, SearchPathValidation)
|
TEST(PluginManager, SearchPathValidation)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user