ChimeraTK-ApplicationCore 04.06.00
Loading...
Searching...
No Matches
DeviceManager.cc
Go to the documentation of this file.
1// SPDX-FileCopyrightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#include "DeviceManager.h"
5
6#include "RecoveryHelper.h"
7#include "Utilities.h"
8
9#include <ChimeraTK/cppext/finally.hpp>
10
11#include <boost/thread/exceptions.hpp>
12
13#include <string>
14
15namespace ChimeraTK {
16
18
19 /********************************************************************************************************************/
20
21 DeviceManager::DeviceManager(Application* application, const std::string& deviceAliasOrCDD)
22 : ApplicationModule(application, "/Devices/" + Utilities::escapeName(deviceAliasOrCDD, false), ""),
23 _device(deviceAliasOrCDD), _deviceAliasOrCDD(deviceAliasOrCDD), _owner{application} {
24 auto involvedBackends = _device.getInvolvedBackendIDs();
25 // Create a recovery group with barrier size 1.
26 _recoveryGroup = std::make_shared<RecoveryGroup>(involvedBackends, _owner);
27
28 // loop all already existing DeviceManagers and look for shared backends
29 int64_t recoveryGroupSize{1};
30 for(const auto& [alias, existingDeviceManager] : Application::getInstance().getDeviceManagerMap()) {
31 for(auto backendID : involvedBackends) {
32 if(existingDeviceManager->_recoveryGroup->recoveryBackendIDs.contains(backendID)) {
33 // Note: The next line modifies involvedBackends while iterating over it.
34 // This is only allowed because the iteration is terminated with a break below!
35 involvedBackends.merge(existingDeviceManager->_recoveryGroup->recoveryBackendIDs);
36 existingDeviceManager->_recoveryGroup = _recoveryGroup;
37 ++recoveryGroupSize;
38 break;
39 }
40 }
41 }
42
43 if(recoveryGroupSize > 1) {
44 // update the recovery group
45 _recoveryGroup->recoveryBackendIDs = involvedBackends;
46 // The barrier does not allow modification of the number of participants.
47 // We put a placement new to replace it. Before this, we have to call the constructor of the old instance.
48 _recoveryGroup->recoveryBarrier.~barrier();
49 new(&_recoveryGroup->recoveryBarrier) std::barrier(recoveryGroupSize);
50 }
51 }
52
53 /********************************************************************************************************************/
54
55 std::vector<VariableNetworkNode> DeviceManager::getNodesList() const {
56 std::vector<VariableNetworkNode> rv;
57
58 // obtain register catalogue
59 auto catalog = _device.getRegisterCatalogue();
60
61 // iterate catalogue, create VariableNetworkNode for all registers
62 for(const auto& reg : catalog) {
63 // ignore 2D registers
64 if(reg.getNumberOfDimensions() > 1) {
65 continue;
66 }
67
68 // guess direction and determine update mode
69 VariableDirection direction{};
70 UpdateMode updateMode;
71 if(reg.isWriteable()) {
72 direction = {VariableDirection::consuming, false};
73 updateMode = UpdateMode::push;
74 }
75 else {
76 direction = {VariableDirection::feeding, false};
77 if(reg.getSupportedAccessModes().has(AccessMode::wait_for_new_data)) {
78 updateMode = UpdateMode::push;
79 }
80 else {
81 updateMode = UpdateMode::poll;
82 }
83 }
84
85 // find minimum type required to represent data
86 const auto* valTyp = &(reg.getDataDescriptor().minimumDataType().getAsTypeInfo());
87
88 if(reg.getTags().contains(SystemTags::reverseRecovery) && reg.isReadable()) {
89 direction.withReturn = true;
90 }
91
92 // create node and add to list
93 rv.emplace_back(reg.getRegisterName(), _deviceAliasOrCDD, reg.getRegisterName(), updateMode, direction,
94 reg.isReadable(), *valTyp, reg.getNumberOfElements());
95 for(const auto& tag : reg.getTags()) {
96 rv.back().addTag(tag);
97 }
98 }
99
100 return rv;
101 }
102
103 /********************************************************************************************************************/
104
105 void DeviceManager::reportException(const std::string& errMsg) {
106 if(_owner->getTestableMode().isEnabled()) {
107 assert(_owner->getTestableMode().testLock());
108 }
109
110 // The error queue must only be modified when holding both mutexes (error mutex and testable mode mutex), because
111 // the testable mode counter must always be consistent with the content of the queue.
112 // To avoid deadlocks you must always first acquire the testable mode mutex if you need both.
113 // You can hold the error mutex without holding the testable mode mutex (for instance for checking the error
114 // predicate), but then you must not try to acquire the testable mode mutex!
115 boost::unique_lock<boost::shared_mutex> errorLock(_errorMutex);
116
117 if(!_deviceHasError) { // only report new errors if the device does not have reported errors already
118 if(_errorQueue.push(errMsg)) {
119 if(_owner->getTestableMode().isEnabled()) {
120 ++_owner->getTestableMode()._counter;
121 }
122 } // else do nothing. There are plenty of errors reported already: The queue is full.
123 // set the error flag and notify the other threads
124 _device.setException(errMsg);
125 _deviceHasError = true;
126 _exceptionVersionNumber = {}; // generate a new exception version number
127
128 // Release the error lock before notifying the other device managers in the recovery group.
129 // They will re-inform us, and holding the lock would deadlock trying to re-acquire this error mutex.
130 errorLock.unlock();
131
132 // Inform the other DeviceManagers in the recovery group.
133 for(auto const& [cdd, deviceManager] : _owner->getDeviceManagerMap()) {
134 if(deviceManager.get() == this) {
135 continue;
136 }
137 if(deviceManager->_recoveryGroup == _recoveryGroup) {
138 // Only append a device info to the recovery message if there is none.
139 auto deviceInfo =
140 (errMsg.find("[in device ") == std::string::npos) ? "[in device " + _deviceAliasOrCDD + "]" : "";
141 deviceManager->reportException(errMsg + deviceInfo);
142 }
143 }
144 }
145 }
146
147 /********************************************************************************************************************/
148
150 boost::shared_lock<boost::shared_mutex> errorLock(_errorMutex);
152 }
153
154 /********************************************************************************************************************/
155
157 // Whenever the loop is left, we have to drop the barrier so the other DeviceManagers are not blocked indefinitely.
158 // They could never terminate in this situation, and hence the whole application could never terminate.
159 try {
160 mainLoopImpl();
161 }
162 catch(...) {
163 _recoveryGroup->shutdown = true;
164 _recoveryGroup->recoveryBarrier.arrive_and_drop();
165 throw;
166 }
167 }
168
169 /********************************************************************************************************************/
170
172 Application::registerThread("DM_" + (--RegisterPath(getName()))); // strip leading "/Device" from name...
173 std::string error;
174
175 // flag whether the devices was opened+initialised for the first time
176 bool firstSuccess = true;
177
178 while(true) {
179 std::unique_ptr<RegisterCatalogue> catalogue;
180
181 // Sync point DETECTION:
182 // The manager has seen an error and (re)starts recovery. Wait until all
183 // involved DeviceManagers have seen it.
185 // Reset error stage to NO_ERROR. Contains a barrier to make sure all threads have seen it.
186 _recoveryGroup->resetErrorAtStage();
187
188 // Starting stage OPEN
189 // [Spec: 2.3.1] (Re)-open the device.
190 do {
191 _owner->getTestableMode().unlock("Wait before open/recover device");
192 usleep(500000);
193 boost::this_thread::interruption_point();
194 _owner->getTestableMode().lock("Attempt open/recover device", true);
195
196 try {
197 std::lock_guard<std::mutex> deviceOpenLock(RecoveryGroup::globalDeviceOpenMutex);
198 // std::lock_guard<std::mutex> deviceOpenLock(_recoveryGroup->deviceOpenCloseMutex);
199 _device.open();
200 catalogue = std::make_unique<RegisterCatalogue>(_device.getRegisterCatalogue());
201 }
202 catch(ChimeraTK::runtime_error& e) {
203 assert(_deviceError._status != StatusOutput::Status::OK); // any error must already be reported...
204 if(std::string(_deviceError._message) != e.what()) {
205 ChimeraTK::logger(Logger::Severity::error, "Device " + _deviceAliasOrCDD) << e.what() << std::endl;
206 // set proper error message in very first attempt to open the device
208 _deviceError.write(StatusOutput::Status::FAULT, e.what());
209 }
210
211 continue; // should not be necessary because isFunctional() should return false. But no harm in leaving it in.
212 }
213 } while(!_device.isFunctional());
214
215 boost::unique_lock<boost::shared_mutex> errorLock(_errorMutex);
216
217 // [Spec: 2.3.3] Empty exception reporting queue.
218 while(_errorQueue.pop()) {
219 if(_owner->getTestableMode()._enabled) {
220 assert(_owner->getTestableMode()._counter > 0);
221 --_owner->getTestableMode()._counter;
222 }
223 }
224 errorLock.unlock(); // we don't need to hold the lock for now, but we will need it later
225
226 for(auto& writeMe : _writeRegisterPaths) {
227 if(!catalogue->getRegister(writeMe).isWriteable()) { // reg.isWriteable()) {
228 _owner->getTestableMode().unlock("throwing" + std::string(writeMe) + " is not writeable!");
229 throw ChimeraTK::logic_error(std::string(writeMe) + " is not writeable!");
230 }
231 }
232
233 for(auto& readMe : _readRegisterPaths) {
234 if(!catalogue->getRegister(readMe).isReadable()) { // reg.isReadable()) {
235 _owner->getTestableMode().unlock("throwing" + std::string(readMe) + " is not readable!");
236 throw ChimeraTK::logic_error(std::string(readMe) + " is not readable!");
237 }
238 }
239 catalogue.reset();
240
241 // Sync point (stage OPEN complete): Device opened. Synchronise before starting init scripts.
242 assert(_recoveryGroup->errorAtStage ==
243 RecoveryGroup::RecoveryStage::NO_ERROR); // no other thread must have modified the flag until here.
244
245 // no need to check the return value. No error reported in the OPEN stage.
247
248 // Starting stage INIT_HANDLERS
249 // [Spec: 2.3.2] Run initialisation handlers
250 try {
251 for(auto& initHandler : _initialisationHandlers) {
252 {
253 // Hold the open/close lock while executing the init handler, so no other
254 // DeviceManager closes the device while the init handler is running.
255 std::lock_guard<std::mutex> openCloseLock(_recoveryGroup->_initHandlerOpenCloseMutex);
256 _device.close();
257 initHandler(_device);
258
259 std::lock_guard<std::mutex> deviceOpenLock(RecoveryGroup::globalDeviceOpenMutex);
260 _device.open();
261 }
262 }
263 }
264 catch(ChimeraTK::runtime_error& e) {
265 assert(_deviceError._status != StatusOutput::Status::OK); // any error must already be reported...
266 // update error message, since it might have been changed...
267 if(std::string(_deviceError._message) != e.what()) {
268 ChimeraTK::logger(Logger::Severity::error, "Device " + _deviceAliasOrCDD) << e.what() << std::endl;
270 _deviceError.write(StatusOutput::Status::FAULT, e.what());
271 }
272 // Mark recovery as failed. All DeviceManagers will return to the beginning of the recovery after the next
273 // synchronisation point
275 }
276 catch(...) {
277 // This will terminate this DeviceManager main loop
278 // Drop the testable mode lock before we bail out.
279 // The other DeviceManagers in this recovery group will be informed in the mainLoop's catch all block.
280 _owner->getTestableMode().unlock("ERROR in INIT_HANDLERS");
281 throw;
282 }
283
284 // Sync point (stage INIT_HANDLERS complete): Wait until all init scripts are done before writing recovery
285 // accessors.
287 // If another thread has already continued and set an error for recovery stage RECOVERY_ACCESSORS,
288 // waitForRecoveryStage(INIT_HANDLERS) will still return 'true', so all threads arrive at the
289 // barrier for stage RECOVERY_ACCESSORS.
290 // If there was error in stage INIT_HANDLERS, all threads will see it here and continue.
291 continue;
292 }
293
294 // Starting stage RECOVERY_ACCESSORS
295 // Write all recovery accessors
296 // We are now entering the critical recovery section. It is protected by the recovery mutex until the
297 // deviceHasError flag has been cleared.
298 boost::unique_lock<boost::shared_mutex> recoveryLock(_recoveryMutex);
299 try {
300 // sort recovery helpers according to write order
301 _recoveryHelpers.sort([](boost::shared_ptr<RecoveryHelper>& a, boost::shared_ptr<RecoveryHelper>& b) {
302 return a->writeOrder < b->writeOrder;
303 });
304 for(auto& recoveryHelper : _recoveryHelpers) {
305 if(recoveryHelper->recoveryDirection == RecoveryHelper::Direction::toDevice) {
306 if(recoveryHelper->versionNumber != VersionNumber{nullptr}) {
307 recoveryHelper->accessor->write();
308 recoveryHelper->wasWritten = true;
309 }
310 }
311 else if(recoveryHelper->recoveryDirection == RecoveryHelper::Direction::fromDevice) {
312 if(recoveryHelper->accessor->isReadable()) {
313 recoveryHelper->notificationQueue.push();
314 }
315 }
316 }
317 }
318 catch(ChimeraTK::runtime_error& e) {
319 // update error message, since it might have been changed...
320 if(std::string(_deviceError._message) != e.what()) {
321 ChimeraTK::logger(Logger::Severity::error, "Device " + _deviceAliasOrCDD) << e.what() << std::endl;
323 _deviceError.write(StatusOutput::Status::FAULT, e.what());
324 }
325 // Mark recovery as failed. All DeviceManagers will return to the beginning of the recovery after the next
326 // synchronisation point
328 }
329 catch(...) {
330 // This will terminate this DeviceManager main loop
331 // Drop the testable mode lock before we bail out.
332 // The other DeviceManagers in this recovery group will be informed in the mainLoop's catch all block.
333 _owner->getTestableMode().unlock("ERROR in RECOVERY_ACCESSORS");
334 throw;
335 }
336
337 // Sync point (stage RECOVERY_ACCESSORS complete): All recovery accessors have been written.
339 // In case of error, jump back to the beginning of the recovery/open procedure
340 continue;
341 }
342
343 // complete the recovery, then wait for the exception
344 errorLock.lock();
345 _deviceHasError = false;
346 errorLock.unlock();
347
348 // Sync point (stage CLEAR_ERROR complete): All DeviceManagers have cleared the error flag.
349 // This barrier protects against exceptions being reported. From now on read/write operations can
350 // report exceptions again, and they will not be suppressed by reportException due to an existing error condition.
352
353 recoveryLock.unlock();
354
355 // send the trigger that the device is available again
356 _device.activateAsyncRead();
359 _initialValueLatch.count_down();
360 }
361
362 // [Spec: 2.3.5] Reset exception state and wait for the next error to be reported.
365
366 if(!firstSuccess) {
367 ChimeraTK::logger(Logger::Severity::info, "Device " + _deviceAliasOrCDD) << "Error cleared." << std::endl;
368 }
369 firstSuccess = false;
370
371 // decrement special testable mode counter, was incremented manually above to make sure initialisation completes
372 // within one "application step"
373 if(Application::getInstance().getTestableMode()._enabled) {
374 --_owner->getTestableMode()._deviceInitialisationCounter;
375 }
376
377 // [Spec: 2.3.8] Wait for an exception being reported by the ExceptionHandlingDecorators
378 // release the testable mode mutex for waiting for the exception.
379 _owner->getTestableMode().unlock("Wait for exception");
380
381 // Do not modify the queue without holding the testable mode lock, because we also consistently have to modify
382 // the counter protected by that mutex.
383 // Just call wait(), not pop_wait().
384 boost::this_thread::interruption_point();
385 _errorQueue.wait();
386 boost::this_thread::interruption_point();
387
388 _owner->getTestableMode().lock("Process exception", true);
389 // increment special testable mode counter to make sure the initialisation completes within one
390 // "application step"
391 if(Application::getInstance().getTestableMode()._enabled) {
392 ++_owner->getTestableMode()._deviceInitialisationCounter; // matched above with a decrement
393 }
394
395 errorLock.lock(); // we need both locks to modify the queue
396
397 auto popResult = _errorQueue.pop(error);
398 assert(popResult); // this if should always be true, otherwise the waiting did not work.
399 (void)popResult; // avoid warning in production build. g++5.4 does not support [[maybe_unused]] yet.
400 if(_owner->getTestableMode()._enabled) {
401 assert(_owner->getTestableMode()._counter > 0);
402 --_owner->getTestableMode()._counter;
403 }
404
405 // [ExceptionHandling Spec: C.3.3.14] report exception to the control system
406 ChimeraTK::logger(Logger::Severity::error, "Device " + _deviceAliasOrCDD) << error << std::endl;
408 _deviceError.write(StatusOutput::Status::FAULT, error);
409
410 // We must not hold the lock while waiting for the synchronousTransferCounter to go back to 0. Only release it
411 // after deviceError has been written, so the CircularDependencyDetector can read the error message from its
412 // thread for printing.
413 errorLock.unlock();
414
415 // [ExceptionHandling Spec: C.3.3.15] Wait for all synchronous transfers to finish before starting recovery.
416 while(_synchronousTransferCounter > 0) {
417 usleep(1000);
418 }
419
420 } // while(true)
421 }
422
423 /********************************************************************************************************************/
424
425 void DeviceManager::prepare() {
426 // Set initial status to error
427 setCurrentVersionNumber({});
428 _deviceError.write(StatusOutput::Status::FAULT, "Attempting to open device...");
429
430 // Increment special testable mode counter to make sure the initialisation completes within one
431 // "application step". Start with counter increased (device not initialised yet, wait).
432 // We can to this here without testable mode lock because the application is still single threaded.
433 if(Application::getInstance().getTestableMode()._enabled) {
434 ++_owner->getTestableMode()._deviceInitialisationCounter; // released and increased in handeException loop
435 }
436 }
437
438 /********************************************************************************************************************/
439
440 void DeviceManager::addInitialisationHandler(std::function<void(ChimeraTK::Device&)> initialisationHandler) {
441 _initialisationHandlers.push_back(std::move(initialisationHandler));
442 }
443
444 /********************************************************************************************************************/
445
446 void DeviceManager::addRecoveryAccessor(boost::shared_ptr<RecoveryHelper> recoveryAccessor) {
447 _recoveryHelpers.push_back(std::move(recoveryAccessor));
448 }
449
450 /********************************************************************************************************************/
451
452 uint64_t DeviceManager::writeOrder() {
453 return ++_writeOrderCounter;
454 }
455
456 /********************************************************************************************************************/
457
458 boost::shared_lock<boost::shared_mutex> DeviceManager::getRecoverySharedLock() {
459 return boost::shared_lock<boost::shared_mutex>(_recoveryMutex);
460 }
461
462 /********************************************************************************************************************/
463
464 void DeviceManager::waitForInitialValues() {
465 _initialValueLatch.wait();
466 }
467
468 /********************************************************************************************************************/
469
470 std::list<EntityOwner*> DeviceManager::getInputModulesRecursively(std::list<EntityOwner*> startList) {
471 // The DeviceManager does not process the device registers, and hence circular networks involving the
472 // DeviceManager are not truly circular. Hence no real circular network checking is done here.
473
474 // If the startList is empty, the recursion scan might be about the status/control variables of the DeviceManager.
475 // Hence we add the DeviceManager to the empty list.
476 if(startList.empty()) {
477 startList.push_back(this);
478 }
479 return startList;
480 }
481
482 /********************************************************************************************************************/
483
484 size_t DeviceManager::getCircularNetworkHash() const {
485 return 0; // The device module is never part of a circular network
486 }
487
488 /********************************************************************************************************************/
489
490 void DeviceManager::incrementDataFaultCounter() {
491 throw ChimeraTK::logic_error("incrementDataFaultCounter() called on a DeviceManager. This is probably "
492 "caused by incorrect ownership of variables/accessors or VariableGroups.");
493 }
494
495 /********************************************************************************************************************/
496
497 void DeviceManager::decrementDataFaultCounter() {
498 throw ChimeraTK::logic_error("decrementDataFaultCounter() called on a DeviceManager. This is probably "
499 "caused by incorrect ownership of variables/accessors or VariableGroups.");
500 }
501
502 /********************************************************************************************************************/
503
504 void DeviceManager::terminate() {
505 if(_moduleThread.joinable()) {
506 _moduleThread.interrupt();
507 // try joining the thread
508 while(!_moduleThread.try_join_for(boost::chrono::milliseconds(10))) {
509 // send boost interrupted exception through the _errorQueue
510 _errorQueue.push_exception(std::make_exception_ptr(boost::thread_interrupted()));
511
512 // it may not suffice to send the exception once, as the exception might get overwritten in the queue, thus we
513 // repeat this until the thread was joined.
514 }
515 }
516 assert(!_moduleThread.joinable());
517 }
518
519 /********************************************************************************************************************/
520
521 bool DeviceManager::RecoveryGroup::waitForRecoveryStage(RecoveryStage stage) {
522 if(shutdown) {
523 throw boost::thread_interrupted(); // NOLINT hicpp-exception-baseclass
524 }
525 boost::this_thread::interruption_point();
526
527 recoveryBarrier.arrive_and_wait();
528
529 if(shutdown) {
530 throw boost::thread_interrupted(); // NOLINT hicpp-exception-baseclass
531 }
532 boost::this_thread::interruption_point();
533
546 return !(errorAtStage == stage);
547 }
548
549 /********************************************************************************************************************/
550
551 void DeviceManager::RecoveryGroup::setErrorAtStage(RecoveryStage stage) {
552 assert((errorAtStage == RecoveryStage::NO_ERROR) || (errorAtStage == stage));
553 errorAtStage = stage;
554 }
555
556 /********************************************************************************************************************/
557
558 void DeviceManager::RecoveryGroup::resetErrorAtStage() {
559 errorAtStage = RecoveryStage::NO_ERROR;
560
561 recoveryBarrier.arrive_and_wait();
562 boost::this_thread::interruption_point();
563 }
564
565} // namespace ChimeraTK
detail::TestableMode & getTestableMode()
Get the TestableMode control object of this application.
static void registerThread(const std::string &name)
Register the thread in the application system and give it a name.
std::map< std::string, boost::shared_ptr< DeviceManager > > const & getDeviceManagerMap()
Access the device manager map.
static Application & getInstance()
Obtain instance of the application.
void setCurrentVersionNumber(VersionNumber versionNumber) override
Set the current version number.
std::atomic< int64_t > _synchronousTransferCounter
bool _deviceHasError
The error flag whether the device is functional.
boost::latch _initialValueLatch
std::list< RegisterPath > _readRegisterPaths
std::vector< VariableNetworkNode > getNodesList() const
Create and return list of VariableNetworkNodes for all device registers.
bool _isHoldingInitialValueLatch
Latch to halt accessors until initial values can be received.
VersionNumber getExceptionVersionNumber()
Use this function to read the exception version number.
VersionNumber _exceptionVersionNumber
Version number of the last exception.
std::list< RegisterPath > _writeRegisterPaths
boost::shared_mutex _errorMutex
Mutex to protect deviceHasError.
VoidOutput deviceBecameFunctional
A trigger that indicated that the device just became available again an error (in contrast to the err...
void reportException(const std::string &errMsg)
Use this function to report an exception.
std::shared_ptr< RecoveryGroup > _recoveryGroup
cppext::future_queue< std::string > _errorQueue
Queue used for communication between reportException() and the moduleThread.
void mainLoopImpl()
This functions tries to open the device and set the deviceError.
std::list< std::function< void(ChimeraTK::Device &)> > _initialisationHandlers
DeviceManager(Application *application, const std::string &deviceAliasOrCDD)
Create DeviceManager which handles device exceptions and performs the recovery.
boost::shared_mutex _recoveryMutex
Mutex for writing the DeviceModule::writeRecoveryOpen.
void mainLoop() override
Wrapper around the actual main loop implementation to add unsubscribing from the barrier to allow a c...
std::list< boost::shared_ptr< RecoveryHelper > > _recoveryHelpers
List of TransferElements to be written after the device has been recovered.
StatusWithMessage _deviceError
A VariableGroup for exception status and message.
const std::string & getName() const
Get the name of the module instance.
Definition EntityOwner.h:59
bool write(ChimeraTK::VersionNumber versionNumber)=delete
InvalidityTracer application module.
@ shutdown
The application is in the process of shutting down.
UpdateMode
Enum to define the update mode of variables.
Definition Flags.h:31
Logger::StreamProxy logger(Logger::Severity severity, std::string context)
Convenience function to obtain the logger stream.
Definition Logger.h:124
static std::mutex globalDeviceOpenMutex
Protect the device open actions for all DeviceManagers and groups.
void writeOk()
Set status to OK, clear the message and write the outputs.
ScalarOutput< std::string > _message
void write(StatusOutput::Status status, std::string message)
Set the status and the message and write the outputs.
Struct to define the direction of variables.
Definition Flags.h:13
bool withReturn
Presence of return channel.
Definition Flags.h:21
constexpr std::string_view cdd