diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d4709585d4c7217fb0e34bea3d6c7cdc656b156..ee83cb68528c3e75fcb25703087069140dcb1c7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,12 +84,12 @@ ENDMACRO( COPY_MAPPING_FILES ) # Create the executables for automated unit testing. if(TESTING_IS_ENABLED) - + include_directories(${CMAKE_SOURCE_DIR}/tests/include) aux_source_directory(${CMAKE_SOURCE_DIR}/tests/executables_src testExecutables) foreach( testExecutableSrcFile ${testExecutables}) #NAME_WE means the base name without path and (longest) extension get_filename_component(executableName ${testExecutableSrcFile} NAME_WE) - add_executable(${executableName} ${testExecutableSrcFile}) + add_executable(${executableName} ${testExecutableSrcFile} ) # do not link against the boost unit test library, the tests are not written for it! target_link_libraries(${executableName} ${PROJECT_NAME} ${ChimeraTK-ControlSystemAdapter_LIBRARIES} ${HDF5_LIBRARIES}) set_target_properties(${executableName} PROPERTIES LINK_FLAGS "${Boost_LINK_FLAGS} ${ChimeraTK-ControlSystemAdapter_LINK_FLAGS}") @@ -190,7 +190,22 @@ if(DEMO_IS_ENABLED) # copy config files for 2nd example with automation FILE( COPY ${CMAKE_SOURCE_DIR}/example3/demoApp3.conf DESTINATION ${PROJECT_BINARY_DIR}) - + + + #add_executable(exceptionTestApp exception_test/exceptionTestApp.cc exception_test/ExceptionDevice.cc example/TimerDummyDevice.cc) + #set_target_properties(exceptionTestApp PROPERTIES COMPILE_FLAGS "${ChimeraTK-ControlSystemAdapter_CXX_FLAGS}") + #set_target_properties(exceptionTestApp PROPERTIES LINK_FLAGS "${ChimeraTK-ControlSystemAdapter_LINK_FLAGS} ${ChimeraTK-ControlSystemAdapter-DoocsAdapter_LINK_FLAGS}") + #target_link_libraries(exceptionTestApp ${PROJECT_NAME} + # ${Boost_LIBRARIES} + # ${ChimeraTK-ControlSystemAdapter_LIBRARIES} + # ${ChimeraTK-ControlSystemAdapter-DoocsAdapter_LIBRARIES} + # ${LibXML++_LIBRARIES} + # ${glib_LIBRARIES} + # ${HDF5_LIBRARIES}) + + #FILE( COPY ${CMAKE_SOURCE_DIR}/exception_test/exceptionTestApp.conf DESTINATION ${PROJECT_BINARY_DIR}) + + endif(DEMO_IS_ENABLED) # C++ library diff --git a/example/TimerDummyDevice.cc b/example/TimerDummyDevice.cc index 7d0f6384bc99d2b6e89c084d7e5e500d0df398cb..6a3933d45e249488e52e9076f03b9a453962f5ae 100644 --- a/example/TimerDummyDevice.cc +++ b/example/TimerDummyDevice.cc @@ -19,7 +19,10 @@ class TimerDummy : public ChimeraTK::DeviceBackendImpl { template<typename UserType> boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> getRegisterAccessor_impl( - const ChimeraTK::RegisterPath& registerPathName, size_t, size_t, ChimeraTK::AccessModeFlags flags); + const ChimeraTK::RegisterPath& registerPathName, + size_t, + size_t, + ChimeraTK::AccessModeFlags flags); DEFINE_VIRTUAL_FUNCTION_TEMPLATE_VTABLE_FILLER(TimerDummy, getRegisterAccessor_impl, 4); void open() override {} @@ -95,7 +98,10 @@ void TimerDummyRegisterAccessor<std::string>::doPostRead() {} template<typename UserType> boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>> TimerDummy::getRegisterAccessor_impl( - const ChimeraTK::RegisterPath& registerPathName, size_t, size_t, ChimeraTK::AccessModeFlags flags) { + const ChimeraTK::RegisterPath& registerPathName, + size_t, + size_t, + ChimeraTK::AccessModeFlags flags) { assert(registerPathName == "/macropulseNr"); assert(flags.has(ChimeraTK::AccessMode::wait_for_new_data)); flags.checkForUnknownFlags({ChimeraTK::AccessMode::wait_for_new_data}); diff --git a/include/Application.h b/include/Application.h index 778c45faa038764a8f5cc6cb0de8e6e68e17d73c..8d5d631b84feea5eca16eacb90e71f3bda31d9b3 100644 --- a/include/Application.h +++ b/include/Application.h @@ -19,6 +19,7 @@ #include "InternalModule.h" #include "Profiler.h" #include "VariableNetwork.h" +//#include "DeviceModule.h" namespace ChimeraTK { @@ -27,6 +28,7 @@ namespace ChimeraTK { class VariableNetwork; class TriggerFanOut; class TestFacility; + class DeviceModule; template<typename UserType> class Accessor; @@ -192,6 +194,7 @@ namespace ChimeraTK { return VariableNetworkNode::makeConstant(makeFeeder, value, length); } + void registerDeviceModule(DeviceModule* deviceModule); protected: friend class Module; friend class VariableNetwork; @@ -393,6 +396,7 @@ namespace ChimeraTK { "caused by " "incorrect ownership of variables/accessors or VariableGroups."); } + std::list<DeviceModule*> deviceModuleList; }; } /* namespace ChimeraTK */ diff --git a/include/DeviceModule.h b/include/DeviceModule.h index d3aa1353fdab8f5659f25cf9d471a2759d6d945e..87cf7972aeb95c40a16eef904e5b7cc5ccc257a9 100644 --- a/include/DeviceModule.h +++ b/include/DeviceModule.h @@ -1,3 +1,34 @@ +/*! + * \author Nadeem Shehzad (DESY) + * \date 21.02.2019 + * \page excpetiondoc Exception Handling + * \section Introduction + * + * To handle expection, the current simple implementation includes two error + * state variables: + * - "state" (boolean flag if error occurred) + * - "message" (string with error message) + * + * These variables are automatically connected to the control systen in this + * format: + * - /Devices/{AliasName}/message + * - /Devices/{AliasName}/status + * + * In this implementation a user/application can report an exception + * by calling reportException of DeviceModule with an exception string. + * The reportException packs the exception in a queue and the blocks the thread. + * This queue is processed by an internal function handleException which + * updates the DeviceError variables (status=1 and message= YourExceptionString) + * and tries to open the device. Once device can be opened the DeviceError + * variables are updated (status=0 and message="") and blocking threads + * are notified to continue. It must be noted that whatever operation which + * lead to exception e.g., read or write, should be repeated after the exception + * is handled. + * + * Checkout testExceptionTest.cc under tests/executables_src to see how it + * works. + */ + /* * DeviceModule.h * @@ -8,26 +39,35 @@ #ifndef CHIMERATK_DEVICE_MODULE_H #define CHIMERATK_DEVICE_MODULE_H -#include <ChimeraTK/ForwardDeclarations.h> -#include <ChimeraTK/RegisterPath.h> - +#include "ControlSystemModule.h" #include "Module.h" +#include "ScalarAccessor.h" +#include "VariableGroup.h" #include "VariableNetworkNode.h" #include "VirtualModule.h" +#include <ChimeraTK/ForwardDeclarations.h> +#include <ChimeraTK/RegisterPath.h> namespace ChimeraTK { - + class Application; class DeviceModule : public Module { public: /** Constructor: The device represented by this DeviceModule is identified by * either the device alias found in the DMAP file or directly an URI. The * given optional prefix will be prepended to all register names * (separated by a slash). */ + + DeviceModule( + Application* application, const std::string& deviceAliasOrURI, const std::string& registerNamePrefix = ""); + DeviceModule(const std::string& deviceAliasOrURI, const std::string& registerNamePrefix = ""); /** Default constructor: create dysfunctional device module */ DeviceModule() {} + /** Destructor */ + virtual ~DeviceModule(); + /** Move operation with the move constructor */ DeviceModule(DeviceModule&& other) { operator=(std::move(other)); } @@ -37,6 +77,7 @@ namespace ChimeraTK { deviceAliasOrURI = std::move(other.deviceAliasOrURI); registerNamePrefix = std::move(other.registerNamePrefix); subModules = std::move(other.subModules); + deviceError = std::move(other.deviceError); return *this; } @@ -63,6 +104,25 @@ namespace ChimeraTK { ModuleType getModuleType() const override { return ModuleType::Device; } + /** Use this function to report an exception. It should be called whenever a + * ChimeraTK::runtime_error has been caught when trying to interact with this + * device. This function shall not be called by the user, all exception + * handling is done internally by ApplicationCore. */ + void reportException(std::string errMsg); + void run() override; + + void terminate() override; + + VersionNumber getCurrentVersionNumber() const override { return currentVersionNumber; } + + void setCurrentVersionNumber(VersionNumber versionNumber) override { + if(versionNumber > currentVersionNumber) currentVersionNumber = versionNumber; + } + + VersionNumber currentVersionNumber; + /** This function connects DeviceError VariableGroup to ContolSystem*/ + void defineConnections() override; + protected: // populate virtualisedModuleFromCatalog based on the information in the // device's catalogue @@ -77,6 +137,35 @@ namespace ChimeraTK { // it is little more than a cache and thus does not change the logical state // of this module mutable std::map<std::string, DeviceModule> subModules; + /** A VariableGroup for exception status and message. It can be protected, as + * it is automatically connected to the control system in + * DeviceModule::defineConnections() */ + struct DeviceError : public VariableGroup { + using VariableGroup::VariableGroup; + ScalarOutput<int> status{this, "status", "", ""}; + ScalarOutput<std::string> message{this, "message", "", ""}; + }; + DeviceError deviceError{this, "DeviceError", "Error status of the device"}; + + /** The thread waiting for reportException(). It runs handleException() */ + boost::thread moduleThread; + + /** Queue used for communication between reportException() and the + * moduleThread. */ + cppext::future_queue<std::string> errorQueue{5}; + + /** Mutex for errorCondVar */ + std::mutex errorMutex; + + /** This condition variable is used to block reportException() until the error + * state has been resolved by the moduleThread. */ + std::condition_variable errorCondVar; + + /** This functions tries to open the device and set the deviceError. Once done + * it notifies the waiting thread(s). + * The function is running an endless loop inside its own thread + * (moduleThread). */ + void handleException(); }; } /* namespace ChimeraTK */ diff --git a/include/Module.h b/include/Module.h index 2dfd9c17b04379b5bb9cdab5ed88e0e2868a121d..5034be353ac85f06a1a59139a33b0c1ddc072660 100644 --- a/include/Module.h +++ b/include/Module.h @@ -91,6 +91,7 @@ namespace ChimeraTK { * eliminate hierarchies where requested and apply other dynamic model * changes. */ virtual const Module& virtualise() const = 0; + virtual void defineConnections(){}; /** * Connect the entire module into another module. All variables inside this diff --git a/src/Application.cc b/src/Application.cc index bc66e581cbddef6127711ada35a880ac47529987..0ef1cac8a208a2a4cf2d12508d1e251b14ff7aeb 100644 --- a/src/Application.cc +++ b/src/Application.cc @@ -20,6 +20,7 @@ #include "ConstantAccessor.h" #include "ConsumingFanOut.h" #include "DebugPrintAccessorDecorator.h" +#include "DeviceModule.h" #include "FeedingFanOut.h" #include "ScalarAccessor.h" #include "TestableModeAccessorDecorator.h" @@ -169,8 +170,12 @@ void Application::run() { // start the threads for the modules for(auto& module : getSubmoduleListRecursive()) { + std::cout << module->getFullDescription() << std::endl; module->run(); } + for(auto& deviceModule : deviceModuleList) { + deviceModule->run(); + } } /*********************************************************************************************************************/ @@ -194,6 +199,9 @@ void Application::shutdown() { module->terminate(); } + for(auto& deviceModule : deviceModuleList) { + deviceModule->terminate(); + } ApplicationBase::shutdown(); } /*********************************************************************************************************************/ @@ -477,6 +485,9 @@ std::pair<boost::shared_ptr<ChimeraTK::NDRegisterAccessor<UserType>>, /*********************************************************************************************************************/ void Application::makeConnections() { + for(auto& devModule : deviceModuleList) { + devModule->defineConnections(); + } // finalise connections: decide still-undecided details, in particular for // control-system and device varibales, which get created "on the fly". finaliseNetworks(); @@ -1084,3 +1095,8 @@ std::unique_lock<std::mutex>& Application::getTestableModeLockObject() { thread_local std::unique_lock<std::mutex> myLock(Application::testableMode_mutex, std::defer_lock); return myLock; } + +/*********************************************************************************************************************/ +void Application::registerDeviceModule(DeviceModule* deviceModule) { + deviceModuleList.push_back(deviceModule); +} diff --git a/src/ApplicationModule.cc b/src/ApplicationModule.cc index e4e2e84d12fb550efd28f0732319bf7a442063dc..438bb5519e370af644f85acc906c28e666e3b0bf 100644 --- a/src/ApplicationModule.cc +++ b/src/ApplicationModule.cc @@ -62,6 +62,7 @@ namespace ChimeraTK { Application::registerThread("AM_" + getName()); Application::testableModeLock("start"); // enter the main loop + std::cout << "mainLoopWrapper" << std::endl; mainLoop(); Application::testableModeUnlock("terminate"); } diff --git a/src/DeviceModule.cc b/src/DeviceModule.cc index 90dc0128ca4b577176e30b231e04f2411e34c673..d86d127dc1b2386d36ba9a45f255ac71698fb03c 100644 --- a/src/DeviceModule.cc +++ b/src/DeviceModule.cc @@ -10,6 +10,7 @@ #include "Application.h" #include "DeviceModule.h" +//#include "ControlSystemModule.h" namespace ChimeraTK { @@ -22,6 +23,23 @@ namespace ChimeraTK { /*********************************************************************************************************************/ + DeviceModule::DeviceModule(Application* application, + const std::string& _deviceAliasOrURI, + const std::string& _registerNamePrefix) + : Module(nullptr, + _registerNamePrefix.empty() ? "<Device:" + _deviceAliasOrURI + ">" : + _registerNamePrefix.substr(_registerNamePrefix.find_last_of("/") + 1), + ""), + deviceAliasOrURI(_deviceAliasOrURI), registerNamePrefix(_registerNamePrefix) { + application->registerDeviceModule(this); + } + + /*********************************************************************************************************************/ + + DeviceModule::~DeviceModule() { assert(!moduleThread.joinable()); } + + /*********************************************************************************************************************/ + VariableNetworkNode DeviceModule::operator()(const std::string& registerName, UpdateMode mode, const std::type_info& valueType, @@ -147,4 +165,85 @@ namespace ChimeraTK { return virtualisedModuleFromCatalog; } + /*********************************************************************************************************************/ + + void DeviceModule::reportException(std::string errMsg) { + std::unique_lock<std::mutex> lk(errorMutex); + errorQueue.push(errMsg); + errorCondVar.wait(lk); + lk.unlock(); + } + /*********************************************************************************************************************/ + + void DeviceModule::handleException() { + Application::registerThread("DM_" + getName()); + Device d; + std::string error; + + try { + while(true) { + errorQueue.pop_wait(error); + boost::this_thread::interruption_point(); + std::lock_guard<std::mutex> lk(errorMutex); + deviceError.status = 1; + deviceError.message = error; + deviceError.setCurrentVersionNumber({}); + deviceError.writeAll(); + while(true) { + boost::this_thread::interruption_point(); + try { + d.open(deviceAliasOrURI); + if(d.isOpened()) { + break; + } + } + catch(std::exception& ex) { + deviceError.status = 1; + deviceError.message = ex.what(); + deviceError.setCurrentVersionNumber({}); + deviceError.writeAll(); + } + usleep(500000); + } + deviceError.status = 0; + deviceError.message = ""; + deviceError.setCurrentVersionNumber({}); + deviceError.writeAll(); + errorCondVar.notify_all(); + } + } + catch(...) { + // before we leave this thread, we might need to notify other waiting + // threads. boost::this_thread::interruption_point() throws an exception + // when the thread should be interrupted, so we will end up here + errorCondVar.notify_all(); + throw; + } + } + + /*********************************************************************************************************************/ + + void DeviceModule::run() { + // start the module thread + assert(!moduleThread.joinable()); + moduleThread = boost::thread(&DeviceModule::handleException, this); + } + + /*********************************************************************************************************************/ + + void DeviceModule::terminate() { + if(moduleThread.joinable()) { + moduleThread.interrupt(); + reportException("ExitOnNone"); + moduleThread.join(); + } + assert(!moduleThread.joinable()); + } + + void DeviceModule::defineConnections() { + std::string prefix = "Devices/" + deviceAliasOrURI + "/"; + ControlSystemModule cs(prefix); + deviceError.connectTo(cs); + } + } // namespace ChimeraTK diff --git a/src/VariableGroup.cc b/src/VariableGroup.cc index dcb35d65603e5a5c38311ad74c3cb1eeb7be773b..c18d220b50666ea7fd84e8565e2955f7aca1edf9 100644 --- a/src/VariableGroup.cc +++ b/src/VariableGroup.cc @@ -12,9 +12,10 @@ namespace ChimeraTK { VariableGroup::VariableGroup(EntityOwner* owner, const std::string& name, const std::string& description, bool eliminateHierarchy, const std::unordered_set<std::string>& tags) : ModuleImpl(owner, name, description, eliminateHierarchy, tags) { - if(!dynamic_cast<ApplicationModule*>(owner) && !dynamic_cast<VariableGroup*>(owner)) { - throw ChimeraTK::logic_error("VariableGroups must be owned either by " - "ApplicationModule or other VariableGroups!"); + if(!dynamic_cast<ApplicationModule*>(owner) && !dynamic_cast<DeviceModule*>(owner) && + !dynamic_cast<VariableGroup*>(owner)) { + throw ChimeraTK::logic_error("VariableGroups must be owned by ApplicationModule, DeviceModule or " + "other VariableGroups!"); } } diff --git a/tests/executables_src/testExceptionTest.cc b/tests/executables_src/testExceptionTest.cc new file mode 100644 index 0000000000000000000000000000000000000000..0b7547785122d8b7596f9582a020813cf1c5eeed --- /dev/null +++ b/tests/executables_src/testExceptionTest.cc @@ -0,0 +1,102 @@ +#include <future> + +#define BOOST_TEST_MODULE testExceptionTest + +#include <boost/mpl/list.hpp> +#include <boost/test/included/unit_test.hpp> +#include <boost/test/test_case_template.hpp> + +#include <ChimeraTK/BackendFactory.h> +#include <ChimeraTK/Device.h> +#include <ChimeraTK/NDRegisterAccessor.h> + +#include "Application.h" +#include "ApplicationModule.h" +#include "ControlSystemModule.h" +#include "DeviceModule.h" +#include "ExceptionDevice.h" +#include "ScalarAccessor.h" +#include "TestFacility.h" + +using namespace boost::unit_test_framework; +namespace ctk = ChimeraTK; + +/* dummy application */ + +struct TestApplication : public ctk::Application { + TestApplication() : Application("testSuite") {} + ~TestApplication() { shutdown(); } + + using Application::makeConnections; // we call makeConnections() manually in + // the tests to catch exceptions etc. + + void defineConnections() {} // the setup is done in the tests + + ctk::DeviceModule dev{this, "(ExceptionDummy?map=DemoDummy.map)"}; + ctk::ControlSystemModule cs; +}; + +/*********************************************************************************************************************/ + +BOOST_AUTO_TEST_CASE(testThinkOfAName) { + TestApplication app; + boost::shared_ptr<ExceptionDummy> backend = boost::dynamic_pointer_cast<ExceptionDummy>( + ChimeraTK::BackendFactory::getInstance().createBackend("(ExceptionDummy?map=DemoDummy.map)")); + + app.dev.connectTo(app.cs); + ctk::TestFacility test; + app.initialise(); + app.run(); + auto message = test.getScalar<std::string>("/Devices/(ExceptionDummy?map=DemoDummy.map)/message"); + auto status = test.getScalar<int>("/Devices/(ExceptionDummy?map=DemoDummy.map)/status"); + + // initially there should be no error set + message.readLatest(); + status.readLatest(); + BOOST_CHECK(static_cast<std::string>(message) == ""); + BOOST_CHECK(status.readLatest() == 0); + + // close the device, reopening it will throw an exception + backend->close(); + backend->throwException = true; + + // test the error injection capability of our ExceptionDummy + try { + backend->open(); + BOOST_FAIL("Exception expected."); + } + catch(ChimeraTK::runtime_error&) { + } + + // report exception to the DeviceModule: it should try reopening the device + // but fail + std::atomic<bool> reportExceptionFinished; + reportExceptionFinished = false; + std::thread reportThread([&] { // need to launch in background, reportException() blocks + app.dev.reportException("Some fancy exception text"); + reportExceptionFinished = true; + }); + + // check the error status and that reportException() is still blocking + sleep(2); + message.readLatest(); + status.readLatest(); + BOOST_CHECK_EQUAL(static_cast<std::string>(message), + "DummyException: This is a test"); // from the ExceptionDummy + BOOST_CHECK(status == 1); + BOOST_CHECK(reportExceptionFinished == false); + BOOST_CHECK(!backend->isOpen()); + + // allow to reopen the device successfully, wait until this has hapopened + backend->throwException = false; + reportThread.join(); + + // the device should now be open again + BOOST_CHECK(backend->isOpen()); + + // check the error status has been cleared + message.readLatest(); + status.readLatest(); + BOOST_CHECK(static_cast<std::string>(message) == ""); + BOOST_CHECK(status.readLatest() == 0); +} diff --git a/tests/include/ExceptionDevice.h b/tests/include/ExceptionDevice.h new file mode 100644 index 0000000000000000000000000000000000000000..e4b795841b692752416cb96f210d0370f9012912 --- /dev/null +++ b/tests/include/ExceptionDevice.h @@ -0,0 +1,34 @@ +#include <ChimeraTK/BackendFactory.h> +#include <ChimeraTK/DeviceAccessVersion.h> +#include <ChimeraTK/DummyBackend.h> + +class ExceptionDummy : public ChimeraTK::DummyBackend { + public: + ExceptionDummy(std::string mapFileName) : DummyBackend(mapFileName) { throwException = false; } + bool throwException; + static boost::shared_ptr<DeviceBackend> createInstance(std::string, std::map<std::string, std::string> parameters) { + return boost::shared_ptr<DeviceBackend>(new ExceptionDummy(parameters["map"])); + } + void open() override { + if(throwException) { + throw(ChimeraTK::runtime_error("DummyException: This is a test")); + } + else + ChimeraTK::DummyBackend::open(); + } + + class BackendRegisterer { + public: + BackendRegisterer(); + }; + static BackendRegisterer backendRegisterer; +}; + +ExceptionDummy::BackendRegisterer ExceptionDummy::backendRegisterer; + +ExceptionDummy::BackendRegisterer::BackendRegisterer() { + std::cout << "ExceptionDummy::BackendRegisterer: registering backend type " + "ExceptionDummy" + << std::endl; + ChimeraTK::BackendFactory::getInstance().registerBackendType("ExceptionDummy", &ExceptionDummy::createInstance); +}