ChimeraTK-DeviceAccess-DoocsBackend 01.12.01
Loading...
Searching...
No Matches
DoocsBackend.cc
Go to the documentation of this file.
1/*
2 * DoocsBackend.cc
3 *
4 * Created on: Apr 26, 2016
5 * Author: Martin Hierholzer
6 */
7
8#include "DoocsBackend.h"
9
10#include "CatalogueCache.h"
11#include "CatalogueFetcher.h"
19#include "RegisterInfo.h"
20#include "StringUtility.h"
22
23#include <ChimeraTK/async/DataConsistencyRealmStore.h>
24#include <ChimeraTK/BackendFactory.h>
25#include <ChimeraTK/DeviceAccessVersion.h>
26#include <ChimeraTK/TypeChangingDecorator.h>
27
28#include <boost/algorithm/string.hpp>
29
30#include <algorithm>
31#include <fstream>
32
33// this is required since we link against the DOOCS libEqServer.so
34const char* object_name = "DoocsBackend";
35
36namespace ctk = ChimeraTK;
37
38extern "C" {
39boost::shared_ptr<ChimeraTK::DeviceBackend> ChimeraTK_DeviceAccess_createBackend(
40 std::string address, std::map<std::string, std::string> parameters) {
41 return ChimeraTK::DoocsBackend::createInstance(address, parameters);
42}
43
44static std::vector<std::string> ChimeraTK_DeviceAccess_sdmParameterNames{"facility", "device", "location"};
45
46static std::string ChimeraTK_DeviceAccess_version{CHIMERATK_DEVICEACCESS_VERSION};
47
48static std::string backend_name = "doocs";
49}
50
51static DoocsBackendRegisterCatalogue fetchCatalogue(
52 std::string serverAddress, std::string cacheFile, std::future<void> cancelFlag);
53
54/********************************************************************************************************************/
55
56static DoocsBackendRegisterCatalogue fetchCatalogue(
57 std::string serverAddress, std::string cacheFile, std::future<void> cancelFlag) {
58 auto result = CatalogueFetcher(serverAddress, std::move(cancelFlag)).fetch();
59 auto catalogue = std::move(result.first);
60 auto isCatalogueComplete = result.second;
61 bool isCacheFileNameSpecified = not cacheFile.empty();
62
63 if(isCatalogueComplete && isCacheFileNameSpecified) {
64 Cache::saveCatalogue(catalogue, cacheFile);
65 }
66 return catalogue;
67}
68
69namespace ChimeraTK {
70
71 /********************************************************************************************************************/
72
73 DoocsBackend::BackendRegisterer DoocsBackend::backendRegisterer;
74
76 std::cout << "DoocsBackend::BackendRegisterer: registering backend type doocs" << std::endl;
77 ChimeraTK::BackendFactory::getInstance().registerBackendType(
78 "doocs", &DoocsBackend::createInstance, {"facility", "device", "location"});
79 }
80
81 /********************************************************************************************************************/
82
83 DoocsBackend::DoocsBackend(const std::string& serverAddress, const std::string& cacheFile,
84 const std::string& updateCache, const std::string& dataConsistencyRealmName)
85 : _serverAddress(serverAddress), _cacheFile(cacheFile) {
86 if(cacheFileExists() && isCachingEnabled()) {
87 // provide catalogue immediately from cache
88 catalogue = Cache::readCatalogue(_cacheFile);
89 _catalogueFromCache = true;
90
91 // update cache file in the background
92 if(updateCache == "1") {
93 std::thread(fetchCatalogue, serverAddress, cacheFile, _cancelFlag.get_future()).detach();
94 }
95 }
96 else {
97 // fill catalogue in the background (and save to cache if enabled)
98 _catalogueFuture =
99 std::async(std::launch::async, fetchCatalogue, serverAddress, cacheFile, _cancelFlag.get_future());
100 }
101
102 // Reduce ZeroMQ timeout so inconsistencies get corrected more quickly. The downside is that DOOCS will do more
103 // frequent RPC polls on rarely changing ZeroMQ variables (every 10 seconds instead of every 4 minutes), but this
104 // is acceptable as it is still slow enough.
105 doocs::zmq_set_subscription_timeout(10);
106
107 _dataConsistencyRealm = async::DataConsistencyRealmStore::getInstance().getRealm(dataConsistencyRealmName);
108
109 FILL_VIRTUAL_FUNCTION_TEMPLATE_VTABLE(getRegisterAccessor_impl);
110 }
111
112 /********************************************************************************************************************/
113
115 if(_catalogueFuture.valid()) {
116 try {
117 _cancelFlag.set_value(); // cancel fill catalogue async task
118 _catalogueFuture.get();
119 }
120 catch(...) {
121 // prevent throwing in destructor (ub if it does);
122 }
123 }
124 }
125
126 /********************************************************************************************************************/
127
128 bool DoocsBackend::cacheFileExists() {
129 if(_cacheFile.empty()) {
130 return false;
131 }
132 std::ifstream f(_cacheFile.c_str());
133 return f.good();
134 }
135
136 /********************************************************************************************************************/
137
138 bool DoocsBackend::isCachingEnabled() const {
139 return !_cacheFile.empty();
140 }
141
142 /********************************************************************************************************************/
143
144 boost::shared_ptr<DeviceBackend> DoocsBackend::createInstance(
145 std::string address, std::map<std::string, std::string> parameters) {
146 // if address is empty, build it from parameters (for compatibility with SDM)
147 if(address.empty()) {
148 RegisterPath serverAddress;
149 serverAddress /= parameters["facility"];
150 serverAddress /= parameters["device"];
151 serverAddress /= parameters["location"];
152 address = std::string(serverAddress).substr(1);
153 }
154 std::string cacheFile{};
155 std::string updateCache{"0"};
156 try {
157 cacheFile = parameters.at("cacheFile");
158 updateCache = parameters.at("updateCache");
159 }
160 catch(std::out_of_range&) {
161 // empty cacheFile string => no caching
162 // empty updateCache string => no cache update
163 }
164
165 std::string dataConsistencyRealmName{"doocsEventId"};
166 if(parameters.find("dataConsistencyRealmName") != parameters.end()) {
167 dataConsistencyRealmName = parameters.at("dataConsistencyRealmName");
168 }
169
170 // create and return the backend
171 return boost::shared_ptr<DeviceBackend>(
172 new DoocsBackend(address, cacheFile, updateCache, dataConsistencyRealmName));
173 }
174
175 /********************************************************************************************************************/
176
178 std::unique_lock<std::mutex> lk(_mxRecovery);
179 if(lastFailedAddress != "") {
180 // Check if the backend is already in the exception state. If so, obtain the stored exception
181 // message to use as a stable error message in case the recovery check fails again.
182 std::string storedExceptionMessage;
183 if(!isFunctional()) {
184 storedExceptionMessage = getActiveExceptionMessage();
185 }
186 // open() is called after a runtime_error: check if device is recovered.
187 doocs::EqAdr ea;
188 ea.adr(lastFailedAddress);
189 EqCall eq;
190 doocs::EqData src, dst;
191 int rc = eq.get(&ea, &src, &dst);
192 // if again error received, throw exception
193 if(rc == doocs::TransactionResult::transaction_error || rc == doocs::TransactionResult::transport_error) {
194 lk.unlock();
195 // Use the stored exception message if available to ensure stable error messages
196 // during recovery attempts, avoiding oscillation between different DOOCS error texts.
197 auto message = storedExceptionMessage.empty() || storedExceptionMessage == "(exception cleared)" ?
198 std::format("Cannot read from DOOCS property '{}': {}", lastFailedAddress, dst.get_string()) :
199 storedExceptionMessage;
200 setException(message);
201 throw ChimeraTK::runtime_error(message);
202 }
203 lastFailedAddress = "";
204 }
205 _startVersion = {};
206 setOpenedAndClearException();
207
208 // re-trigger catalogue filling? Only done if catalogue is not taken from cache, is not currently begin fetched, and
209 // the catalogue is incomplete.
210 if(!_catalogueFromCache && !_catalogueFuture.valid() && !catalogue.isComplete()) {
211 _cancelFlag = std::promise<void>{};
212 _catalogueFuture =
213 std::async(std::launch::async, fetchCatalogue, _serverAddress, _cacheFile, _cancelFlag.get_future());
214 }
215 }
216
217 /********************************************************************************************************************/
218
219 RegisterCatalogue DoocsBackend::getRegisterCatalogue() const {
220 return RegisterCatalogue(getBackendRegisterCatalogue().clone());
221 }
222
223 /********************************************************************************************************************/
224
226 if(_catalogueFuture.valid()) {
227 catalogue = _catalogueFuture.get();
228 }
229 return catalogue;
230 }
231
232 /********************************************************************************************************************/
233
236 _opened = false;
237 _asyncReadActivated = false;
238 {
239 std::unique_lock<std::mutex> lk(_mxRecovery);
240
241 lastFailedAddress = "";
242 }
243 }
244
245 /********************************************************************************************************************/
246
247 void DoocsBackend::informRuntimeError(const std::string& address) {
248 std::lock_guard<std::mutex> lk(_mxRecovery);
249 if(lastFailedAddress == "") {
250 lastFailedAddress = address;
251 }
252 }
253
254 /********************************************************************************************************************/
255
257 _asyncReadActivated = false;
258 std::string message{"Unknown exception reported by another accessor"};
259 try {
260 checkActiveException();
261 }
262 catch(ChimeraTK::runtime_error& e) {
263 message = e.what();
264 }
266 }
267
268 /********************************************************************************************************************/
269
271 if(!isFunctional()) { // Spec TransferElement 8.5.4.1 and 8.5.4.2 : No effect if closed or has error
272 return;
273 }
274 auto wasActive = _asyncReadActivated.exchange(
275 true); // Spec TransferElement 8.5.7 : Thread safe against other activateAsyncRead() calls
276 if(wasActive) { // Spec TransferElement 8.5.4.3 : No effect if already active
277 return;
278 }
279
281 }
282
283 /********************************************************************************************************************/
284
285 template<typename UserType>
286 boost::shared_ptr<NDRegisterAccessor<UserType>> DoocsBackend::getRegisterAccessor_impl(
287 const RegisterPath& registerPathName, size_t numberOfWords, size_t wordOffsetInRegister, AccessModeFlags flags) {
288 boost::shared_ptr<NDRegisterAccessor<UserType>> p;
289 std::string path = _serverAddress + registerPathName;
290
291 // check for additional hierarchy level, which indicates an access to a field of a complex property data type
292 bool hasExtraLevel = false;
293 if(!boost::starts_with(path, "doocs://") && !boost::starts_with(path, "epics://")) {
294 size_t nSlashes = std::count(path.begin(), path.end(), '/');
295 if(nSlashes == 4) {
296 hasExtraLevel = true;
297 }
298 else if(nSlashes < 3 || nSlashes > 4) {
299 throw ChimeraTK::logic_error(std::string("DOOCS address has an illegal format: ") + path);
300 }
301 }
302 else if(boost::starts_with(path, "doocs://")) {
303 size_t nSlashes = std::count(path.begin(), path.end(), '/');
304 // we have 3 extra slashes compared to the standard syntax without "doocs:://"
305 if(nSlashes == 4 + 3) {
306 hasExtraLevel = true;
307 }
308 else if(nSlashes < 3 + 3 || nSlashes > 4 + 3) {
309 throw ChimeraTK::logic_error(std::string("DOOCS address has an illegal format: ") + path);
310 }
311 }
312
313 // split the path into property name and field name
314 std::string field;
315 if(hasExtraLevel) {
316 field = path.substr(path.find_last_of('/') + 1);
317 path = path.substr(0, path.find_last_of('/'));
318 }
319
320 // if backend is open, read property once to obtain type
321 int doocsTypeId = DATA_NULL;
322 if(isOpen()) {
323 doocs::EqAdr ea;
324 EqCall eq;
325 doocs::EqData src, dst;
326 ea.adr(path);
327 int rc = eq.get(&ea, &src, &dst);
328 if(!rc) {
329 doocsTypeId = dst.type();
330 }
331 }
332
333 // if backend is closed, or if property could not be read, use the (potentially cached) catalogue
334 if(doocsTypeId == DATA_NULL) {
335 auto reg = getBackendRegisterCatalogue().getBackendRegister(registerPathName);
336 doocsTypeId = reg.doocsTypeId;
337 }
338
339 // check type and create matching accessor
340 bool extraLevelUsed = false;
341 auto sharedThis = boost::static_pointer_cast<DoocsBackend>(shared_from_this());
342
343 if(field == "eventId") {
344 extraLevelUsed = true;
345 p.reset(new DoocsBackendEventIdRegisterAccessor<UserType>(sharedThis, path, registerPathName, flags));
346 }
347 else if(field == "timeStamp") {
348 extraLevelUsed = true;
349 p.reset(new DoocsBackendTimeStampRegisterAccessor<UserType>(sharedThis, path, registerPathName, flags));
350 }
351 else {
352 switch(doocsTypeId) {
353 case DATA_BOOL:
354 case DATA_A_BOOL:
355 case DATA_SHORT:
356 case DATA_A_SHORT:
357 case DATA_USHORT:
358 case DATA_A_USHORT:
359 case DATA_INT:
360 case DATA_A_INT:
361 case DATA_UINT:
362 case DATA_A_UINT:
363 case DATA_LONG:
364 case DATA_A_LONG:
365 case DATA_ULONG:
366 case DATA_A_ULONG:
367 case DATA_FLOAT:
368 case DATA_SPECTRUM:
369 case DATA_GSPECTRUM:
370 case DATA_A_FLOAT:
371 case DATA_DOUBLE:
372 case DATA_A_DOUBLE:
374 sharedThis, path, registerPathName, numberOfWords, wordOffsetInRegister, flags));
375 break;
376
377 case DATA_IIII:
379 sharedThis, path, registerPathName, numberOfWords, wordOffsetInRegister, flags));
380 break;
381
382 case DATA_IFFF:
383 if(!hasExtraLevel) {
384 throw ChimeraTK::logic_error("DOOCS property of IFFF type '" + _serverAddress + registerPathName +
385 "' cannot be accessed as a whole.");
386 }
387 extraLevelUsed = true;
389 sharedThis, path, field, registerPathName, numberOfWords, wordOffsetInRegister, flags));
390 break;
391
392 case DATA_TEXT:
393 case DATA_STRING:
395 sharedThis, path, registerPathName, numberOfWords, wordOffsetInRegister, flags));
396 break;
397
398 case DATA_IMAGE: {
399 auto accImpl = new DoocsBackendImageRegisterAccessor(
400 sharedThis, path, registerPathName, numberOfWords, wordOffsetInRegister, flags);
401 if constexpr(std::is_same_v<UserType, std::uint8_t>) {
402 p.reset(accImpl);
403 }
404 else {
405 boost::shared_ptr<NDRegisterAccessor<std::uint8_t>> pImpl(accImpl);
406 // any UserType can hold uint8
407 boost::shared_ptr<NDRegisterAccessor<UserType>> accDecorated(
408 new TypeChangingRangeCheckingDecorator<UserType, std::uint8_t>(
409 boost::dynamic_pointer_cast<ChimeraTK::NDRegisterAccessor<std::uint8_t>>(pImpl)));
410 p = accDecorated;
411 }
412 break;
413 }
414
415 default:
416 throw ChimeraTK::logic_error("Unsupported DOOCS data type " +
417 std::string(doocs::EqData().type_string(doocsTypeId)) + " of property '" + _serverAddress +
418 registerPathName + "'");
419 }
420 }
421
422 // if the field name has been specified but the data type does not use it, throw an exception
423 if(hasExtraLevel && !extraLevelUsed) {
424 throw ChimeraTK::logic_error("Specifiaction of field name is not supported for the DOOCS data type " +
425 std::string(doocs::EqData().type_string(doocsTypeId)) + ": " + _serverAddress + registerPathName);
426 }
427
428 p->setExceptionBackend(shared_from_this());
429 return p;
430 }
431
432 /********************************************************************************************************************/
433
434} /* namespace ChimeraTK */
const char * object_name
boost::shared_ptr< ChimeraTK::DeviceBackend > ChimeraTK_DeviceAccess_createBackend(std::string address, std::map< std::string, std::string > parameters)
std::pair< DoocsBackendRegisterCatalogue, bool > fetch()
Backend to access DOOCS control system servers.
std::atomic< bool > _asyncReadActivated
DoocsBackend(const std::string &serverAddress, const std::string &cacheFile, const std::string &updateCache, const std::string &dataConsistencyRealmName)
std::string _serverAddress
DOOCS address component for the server (FACILITY/DEVICE)
void activateAsyncRead() noexcept override
static boost::shared_ptr< DeviceBackend > createInstance(std::string address, std::map< std::string, std::string > parameters)
const DoocsBackendRegisterCatalogue & getBackendRegisterCatalogue() const
void informRuntimeError(const std::string &address)
Called by accessors to inform about addess causing a runtime_error.
static BackendRegisterer backendRegisterer
RegisterCatalogue getRegisterCatalogue() const override
boost::shared_ptr< NDRegisterAccessor< UserType > > getRegisterAccessor_impl(const RegisterPath &registerPathName, size_t numberOfWords, size_t wordOffsetInRegister, AccessModeFlags flags)
void setExceptionImpl() noexcept override
void activateAllListeners(DoocsBackend *backend)
Activate all listeners for the given backend. Should be called from DoocsBackend::activateAsyncRead()...
void deactivateAllListeners(DoocsBackend *backend)
Deactivate all listeners the given backend. Should be called from DoocsBackend::close().
void deactivateAllListenersAndPushException(DoocsBackend *backend, const std::string &message)
Deactivate all listeners for the given backend and push exceptions into the queues.
DoocsBackendRegisterCatalogue readCatalogue(const std::string &xmlfile)
void saveCatalogue(const DoocsBackendRegisterCatalogue &c, const std::string &xmlfile)