From afafa09547eeadf55f56af22ee1b36dd68039425 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 21 Feb 2023 12:10:31 -0800 Subject: [PATCH] ioc: revise qsrv 2 prototype ioc: check for mis-matched onStartSubscription()/onDisableSubscription() ioc: fix subscription lifetime ioc: catch exceptions in dbEvent callbacks ioc: avoid unnecessary virtual ioc: minor ioc: fix qsrv -S ioc: qsrvGroupSourceInit() catch+log ioc: runOnServer avoid std::function ioc: cleanup and simplifications. Avoid some redundant std::map lookups. Make Group partially const to prevent implicit ctor. ioc: avoid typedefs only used once ioc: overhaul Group::show(). shows triggers ioc: MappingType ioc: pvxsgl -> pvxgl ioc: separate group config singleton from server singleton ioc: remove unnecessary forward declarations ioc: restructure pvxsInitHook ioc: qsrv runtime disable by default ioc: compat w/ older Base ioc: link pvxsIoc w/ DB libs ioc: Channel proper detection of invalid PV ioc: no need to keep vector around ioc: fix initial group update for mappings w/o dbChannel ioc: redo testing split out group tests, only run with Base >= 7.0 ioc: minor ioc: loc_bad_alloc ioc: avoid symbol/DTYP clash with pva2pva ioc: test record alias in group json ioc: test put failure when SPC_NOMOD and DISP=1 ioc: test channel filters ioc: unnecessary capture ioc: avoid sharing Value between multiple subscriptions It is possible to create two subscriptions through the same channel. ioc: group subscription include queueSize ioc: eliminate unused atomicMonitor ioc: consolidate GroupSource::get() avoid some indirection ioc: pvRequest override of atomicPutGet ioc: fix group non-atomic put ioc: test asTrap hooks ioc: test putOrder also sets field order ioc: simplify GroupConfigProcessor::loadConfigFiles() Also ensure that groupMapMutex is held ioc: testqgroup cover JSON def. ioc: dbLoadGroup() use macros ioc: pvxsl() take integer argument ioc: display.form and info(Q:form ioc: "NO_ALARM" -> "" ioc: use dbServer at least for informational callbacks. ioc: consolidate createRequestAndSubscriptionHandlers() ioc: eliminate ChannelAndLock properties dbChannel doesn't need a separate DBManyLock ioc: test that putOrder also controls field order ioc: MappingType -> MappingInfo Handle info(Q:time:tag Add +type:"const" ioc: cleanup includes ioc: test dbNotifyCancel() ioc: inline checkForTrailingCommentsAtEnd() --- Makefile | 2 + ioc/Makefile | 42 +- ioc/channel.cpp | 27 +- ioc/channel.h | 66 +- ioc/dbentry.h | 19 +- ioc/dberrormessage.cpp | 1 + ioc/dblocker.h | 2 + ioc/demo.cpp | 8 +- ioc/dummygroup.cpp | 23 + ioc/dummysingle.cpp | 8 + ioc/field.cpp | 32 +- ioc/field.h | 37 +- ioc/fieldconfig.h | 31 +- ioc/fielddefinition.cpp | 12 +- ioc/fielddefinition.h | 9 +- ioc/fieldname.cpp | 47 +- ioc/fieldname.h | 6 +- ioc/fieldsubscriptionctx.cpp | 20 +- ioc/fieldsubscriptionctx.h | 13 +- ioc/group.cpp | 90 ++- ioc/group.h | 57 +- ioc/groupconfig.h | 5 +- ioc/groupconfigprocessor.cpp | 662 ++++++++++--------- ioc/groupconfigprocessor.h | 42 +- ioc/groupdefinition.h | 15 +- ioc/groupprocessorcontext.cpp | 25 +- ioc/groupprocessorcontext.h | 1 - ioc/groupsource.cpp | 441 ++++++------- ioc/groupsource.h | 21 +- ioc/groupsourcehooks.cpp | 215 +++--- ioc/groupsrcsubscriptionctx.h | 4 + ioc/imagedemo.c | 4 +- ioc/iochooks.cpp | 102 ++- ioc/iocserver.h | 41 -- ioc/iocshcommand.h | 36 +- ioc/iocsource.cpp | 1056 +++++++++++++++--------------- ioc/iocsource.h | 131 ++-- ioc/localfieldlog.cpp | 2 +- ioc/metadata.h | 115 ---- ioc/pvxs/iochooks.h | 19 +- ioc/pvxsIoc.dbd | 6 +- ioc/singlesource.cpp | 648 +++++++++--------- ioc/singlesource.h | 72 -- ioc/singlesourcehooks.cpp | 119 +++- ioc/singlesrcsubscriptionctx.cpp | 15 +- ioc/singlesrcsubscriptionctx.h | 26 +- ioc/subscriptionctx.h | 68 +- ioc/typeutils.cpp | 80 +-- ioc/typeutils.h | 35 +- qsrv/qsrvMain.cpp | 8 +- src/utilpvt.h | 4 + test/Makefile | 31 +- test/const.db | 9 + test/image.db | 4 +- test/image.json | 20 + test/iq.db | 6 +- test/table.db | 6 +- test/testioc.cpp | 535 --------------- test/testioc.db | 110 ---- test/testioc.h | 121 ++++ test/testioc.json | 73 --- test/testiocg.db | 120 ---- test/testqgroup.cpp | 712 ++++++++++++++++++++ test/testqsingle.cpp | 869 ++++++++++++++++++++++++ test/testqsingle.db | 61 ++ test/testqsingle64.db | 7 + 66 files changed, 4109 insertions(+), 3145 deletions(-) create mode 100644 ioc/dummygroup.cpp create mode 100644 ioc/dummysingle.cpp delete mode 100644 ioc/iocserver.h delete mode 100644 ioc/metadata.h create mode 100644 test/const.db create mode 100644 test/image.json delete mode 100644 test/testioc.cpp delete mode 100644 test/testioc.db create mode 100644 test/testioc.h delete mode 100644 test/testioc.json delete mode 100644 test/testiocg.db create mode 100644 test/testqgroup.cpp create mode 100644 test/testqsingle.cpp create mode 100644 test/testqsingle.db create mode 100644 test/testqsingle64.db diff --git a/Makefile b/Makefile index d32169b..5def74d 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,10 @@ tools_DEPEND_DIRS = src DIRS += ioc ioc_DEPEND_DIRS = src +ifdef BASE_3_15 DIRS += qsrv qsrv_DEPEND_DIRS = src ioc +endif DIRS += test test_DEPEND_DIRS = src ioc diff --git a/ioc/Makefile b/ioc/Makefile index 43f9a3f..4c0cea8 100644 --- a/ioc/Makefile +++ b/ioc/Makefile @@ -26,14 +26,28 @@ DBD += pvxsIoc.dbd INC += pvxs/iochooks.h LIBRARY += pvxsIoc -PROD_LIBS = Com SHRLIB_VERSION = $(PVXS_MAJOR_VERSION).$(PVXS_MINOR_VERSION) +pvxsIoc_SRCS += iochooks.cpp + +ifdef BASE_3_15 + pvxsIoc_SRCS += credentials.cpp pvxsIoc_SRCS += channel.cpp pvxsIoc_SRCS += demo.cpp pvxsIoc_SRCS += dberrormessage.cpp +pvxsIoc_SRCS += imagedemo.c +pvxsIoc_SRCS += iocsource.cpp +pvxsIoc_SRCS += localfieldlog.cpp +pvxsIoc_SRCS += securityclient.cpp +pvxsIoc_SRCS += singlesource.cpp +pvxsIoc_SRCS += singlesourcehooks.cpp +pvxsIoc_SRCS += singlesrcsubscriptionctx.cpp +pvxsIoc_SRCS += typeutils.cpp + +ifdef BASE_7_0 + pvxsIoc_SRCS += field.cpp pvxsIoc_SRCS += fielddefinition.cpp pvxsIoc_SRCS += fieldname.cpp @@ -43,15 +57,23 @@ pvxsIoc_SRCS += groupconfigprocessor.cpp pvxsIoc_SRCS += groupprocessorcontext.cpp pvxsIoc_SRCS += groupsource.cpp pvxsIoc_SRCS += groupsourcehooks.cpp -pvxsIoc_SRCS += imagedemo.c -pvxsIoc_SRCS += iochooks.cpp -pvxsIoc_SRCS += iocsource.cpp -pvxsIoc_SRCS += localfieldlog.cpp -pvxsIoc_SRCS += securityclient.cpp -pvxsIoc_SRCS += singlesource.cpp -pvxsIoc_SRCS += singlesourcehooks.cpp -pvxsIoc_SRCS += singlesrcsubscriptionctx.cpp -pvxsIoc_SRCS += typeutils.cpp + +else # BASE_7_0 + +pvxsIoc_SRCS += dummygroup.cpp + +endif # BASE_7_0 + +pvxsIoc_LIBS += $(EPICS_BASE_IOC_LIBS) + +else # BASE_3_15 + +pvxsIoc_SRCS += dummysingle.cpp +pvxsIoc_SRCS += dummygroup.cpp + +pvxsIoc_LIBS += Com + +endif # BASE_3_15 LIB_LIBS += pvxs LIB_LIBS += $(EPICS_BASE_IOC_LIBS) diff --git a/ioc/channel.cpp b/ioc/channel.cpp index 0278b65..012841a 100644 --- a/ioc/channel.cpp +++ b/ioc/channel.cpp @@ -19,30 +19,19 @@ namespace ioc { * * @param name the db channel name */ -Channel::Channel(const std::string& name) - :pDbChannel(std::shared_ptr(dbChannelCreate(name.c_str()), +Channel::Channel(const char* name) + :std::shared_ptr(std::shared_ptr(dbChannelCreate(name), [](dbChannel* ch) { if (ch) { dbChannelDelete(ch); } - })) { - if (pDbChannel) { - prepare(); - } + })) +{ + if(!*this) + throw std::runtime_error(SB()<<"Invalid PV: "< pDbChannel; - void prepare(); - +class Channel : public std::shared_ptr { public: + Channel() = default; + Channel(const Channel&) = default; + Channel(Channel&&) = default; // This constructor calls dbChannelOpen() - explicit Channel(const std::string& name); + explicit Channel(const char* name); + inline + explicit Channel(const std::string& name) + :Channel(name.c_str()) + {} -/** - * Destructor is default because pDbChannel cleans up after itself. - */ - ~Channel() = default; + Channel& operator=(const Channel&) = default; + Channel& operator=(Channel&&) = default; - /** - * Cast as a shared pointer to a dbChannel. This returns the pDbChannel member - * - * @return the pDbChannel member - */ operator dbChannel*() const { - return pDbChannel.get(); + return get(); } -/** - * Const pointer indirection operator - * @return pointer to the dbChannel associated with this group channel - */ - const dbChannel* operator->() const { - return pDbChannel.get(); - } - - explicit operator bool() const { - return pDbChannel.operator bool(); - } - -/** - * Move constructor - * - * @param other other Channel - */ - Channel(Channel&& other) noexcept - :pDbChannel(std::move(other.pDbChannel)) { - } - -/** - * Move assignment operator - * - * @param other the other channel - * @return the moved channel - */ - Channel& operator=(Channel&& other) noexcept { - pDbChannel = std::move(other.pDbChannel); - other.pDbChannel = nullptr; - return *this; - } - - // Disallowed methods. Copy and move constructors - Channel(const Channel&) = delete; - const std::shared_ptr& shared_ptr() const { - return pDbChannel; - }; }; } // pvxs diff --git a/ioc/dbentry.h b/ioc/dbentry.h index 852e262..5ba8924 100644 --- a/ioc/dbentry.h +++ b/ioc/dbentry.h @@ -13,7 +13,7 @@ #include #include -#include "dbentry.h" +#include namespace pvxs { namespace ioc { @@ -27,6 +27,16 @@ public: DBEntry() { dbInitEntry(pdbbase, &ent); } + explicit DBEntry(dbCommon *prec) { +#if EPICS_VERSION_INT >= VERSION_INT(3, 16, 1, 0) + dbInitEntryFromRecord(prec, &ent); +#else + dbInitEntry(pdbbase, &ent); + (void)dbFindRecord(&ent, prec->name); +#endif + } + DBEntry(const DBEntry&) = delete; + DBEntry(DBEntry&&) = delete; ~DBEntry() { dbFinishEntry(&ent); @@ -40,6 +50,13 @@ public: return &ent; } + const char* info(const char *key) { + const char *ret = nullptr; + if(!dbFindInfo(&ent, key)) { + ret = ent.pinfonode->string; + } + return ret; + } }; } // ioc diff --git a/ioc/dberrormessage.cpp b/ioc/dberrormessage.cpp index 94967d4..06a28ad 100644 --- a/ioc/dberrormessage.cpp +++ b/ioc/dberrormessage.cpp @@ -8,6 +8,7 @@ */ #include +#include #include #include "dberrormessage.h" diff --git a/ioc/dblocker.h b/ioc/dblocker.h index a450e6b..42dea29 100644 --- a/ioc/dblocker.h +++ b/ioc/dblocker.h @@ -34,6 +34,8 @@ public: :lock(L) { dbScanLock(lock); } + DBLocker(const DBLocker&) = delete; + DBLocker(DBLocker&&) = delete; ~DBLocker() { dbScanUnlock(lock); diff --git a/ioc/demo.cpp b/ioc/demo.cpp index 865d560..c5bb7b0 100644 --- a/ioc/demo.cpp +++ b/ioc/demo.cpp @@ -75,12 +75,12 @@ struct dset5 { long (* process)(REC*); }; -dset5 devWfPDBDemo = { 5, 0, 0, &init_spin, 0, &process_spin }; -dset5 devLoPDBUTag = { 5, 0, 0, 0, 0, &process_utag }; +dset5 devWfPDBQ2Demo = { 5, 0, 0, &init_spin, 0, &process_spin }; +dset5 devLoPDBQ2UTag = { 5, 0, 0, 0, 0, &process_utag }; } // namespace extern "C" { -epicsExportAddress(dset, devWfPDBDemo); -epicsExportAddress(dset, devLoPDBUTag); +epicsExportAddress(dset, devWfPDBQ2Demo); +epicsExportAddress(dset, devLoPDBQ2UTag); } diff --git a/ioc/dummygroup.cpp b/ioc/dummygroup.cpp new file mode 100644 index 0000000..7d1fc78 --- /dev/null +++ b/ioc/dummygroup.cpp @@ -0,0 +1,23 @@ +#include +#include "utilpvt.h" + +#include +#include + +namespace pvxs { +namespace ioc { +long dbLoadGroup(const char* jsonFilename, const char* macros) { + fprintf(stderr, "QSRV2 groups not supported with Base <3.16\n"); + return -1; +} + +void IOCGroupConfigCleanup() {} + +}} + +static +void pvxsGroupSourceRegistrar() {} + +extern "C" { +epicsExportRegistrar(pvxsGroupSourceRegistrar); +} diff --git a/ioc/dummysingle.cpp b/ioc/dummysingle.cpp new file mode 100644 index 0000000..845a02b --- /dev/null +++ b/ioc/dummysingle.cpp @@ -0,0 +1,8 @@ +#include + +static +void pvxsSingleSourceRegistrar() {} + +extern "C" { +epicsExportRegistrar(pvxsSingleSourceRegistrar); +} diff --git a/ioc/field.cpp b/ioc/field.cpp index 3a5c3b0..dbdde48 100644 --- a/ioc/field.cpp +++ b/ioc/field.cpp @@ -11,30 +11,38 @@ #include #include "field.h" +#include "typeutils.h" namespace pvxs { namespace ioc { -/** - * Construct a Field from a field name and channel name - * - * @param stringFieldName the field name - * @param stringChannelName the channel name - */ -Field::Field(const std::string& stringFieldName, const std::string& stringChannelName, std::string id) - :id(std::move(id)), fieldName(stringFieldName), isMeta(false), allowProc(false), isArray(false), - value(stringChannelName), - properties(stringChannelName) { - +Field::Field(const FieldDefinition &def) + :id(def.structureId) + ,fieldName(def.name) + ,info(def.info) +{ + if(!def.channel.empty()) { + value = Channel(def.channel); + properties = Channel(def.channel); + info.updateNsecMask(dbChannelRecord(value)); + } if (!fieldName.fieldNameComponents.empty()) { name = fieldName.fieldNameComponents[0].name; - fullName = std::string(fieldName.to_string()); + fullName = fieldName.to_string(); if (fieldName.fieldNameComponents[fieldName.fieldNameComponents.size() - 1].isArray()) { isArray = true; } } + if(info.type == MappingInfo::Any) { + // pre-compute the type which will be stored in the Any field + auto type = fromDbrType(dbChannelFinalFieldType(value)); + if (dbChannelFinalElements(value) != 1) { + type = type.arrayOf(); + } + anyType = TypeDef(type).create(); + } } /** diff --git a/ioc/field.h b/ioc/field.h index 3fdc036..b57cac7 100644 --- a/ioc/field.h +++ b/ioc/field.h @@ -20,25 +20,12 @@ #include "dblocker.h" #include "channel.h" #include "dbmanylocker.h" +#include "fielddefinition.h" #include "fieldname.h" namespace pvxs { namespace ioc { -class Field; -typedef std::vector Triggers; - -class ChannelAndLock { -public: - Channel channel; - std::vector references; - DBManyLock lock; - - explicit ChannelAndLock(const std::string& stringChannelName) - :channel(stringChannelName) { - } -}; - class Field { private: public: @@ -46,18 +33,24 @@ public: std::string name; FieldName fieldName; std::string fullName; - bool isMeta, allowProc; - bool isArray; - ChannelAndLock value; - ChannelAndLock properties; - Triggers triggers; // reference to the fields that are triggered by this field during subscriptions + MappingInfo info; + bool isArray = false; + Channel value; + Channel properties; + DBManyLock lock; + // reference to the fields that are triggered by this field during subscriptions + // points to storage in containing Group::fields + std::vector triggers; - Field(const std::string& stringFieldName, const std::string& stringChannelName, std::string id); + // only for Meta mapping. type infered from dbChannelFinalFieldType() + Value anyType; + + Field(const FieldDefinition& def); + Field(const Field&) = delete; + Field(Field&&) = default; Value findIn(Value valueTarget) const; }; -typedef std::vector Fields; - } // pvxs } // ioc diff --git a/ioc/fieldconfig.h b/ioc/fieldconfig.h index ea37cb0..b949f79 100644 --- a/ioc/fieldconfig.h +++ b/ioc/fieldconfig.h @@ -13,21 +13,44 @@ #include #include +#include + +#include + namespace pvxs { namespace ioc { +struct MappingInfo { + enum type_t { + Scalar, // implied default + Plain, + Any, + Meta, + Proc, + Structure, + Const, + } type = Scalar; + static + const char *name(type_t t); + + uint32_t nsecMask = 0u; + + Value cval; + + void updateNsecMask(dbCommon *prec); +}; + /** * Class to read the group field configuration into during initialization. * It is subsequently read into GroupChannelField for runtime use */ class FieldConfig { public: - std::string type, channel, trigger, structureId; - int64_t putOrder; + std::string channel, trigger, structureId; + MappingInfo info; + int64_t putOrder = 0; }; -typedef std::map FieldConfigMap; - } // pvxs } // ioc diff --git a/ioc/fielddefinition.cpp b/ioc/fielddefinition.cpp index 7de484a..c02d280 100644 --- a/ioc/fielddefinition.cpp +++ b/ioc/fielddefinition.cpp @@ -9,6 +9,8 @@ #include +#include + #include "fielddefinition.h" namespace pvxs { @@ -22,13 +24,9 @@ namespace ioc { * @param fieldName the name of the field */ FieldDefinition::FieldDefinition(const FieldConfig& fieldConfig, const std::string& fieldName) - :putOrder(0) { - channel = fieldConfig.channel; - name = fieldName; - structureId = fieldConfig.structureId; - putOrder = fieldConfig.putOrder; - type = fieldConfig.type; -} + :FieldConfig(fieldConfig) + ,name(fieldName) +{} } } diff --git a/ioc/fielddefinition.h b/ioc/fielddefinition.h index b74e0c7..e6d627d 100644 --- a/ioc/fielddefinition.h +++ b/ioc/fielddefinition.h @@ -26,14 +26,10 @@ typedef std::set TriggerNames; /** * Class to store group fields definitions while they are being processed after being read from files into FieldConfig. */ -class FieldDefinition { +class FieldDefinition : public FieldConfig { public: std::string name; // Field's name - std::string channel; // Database record name aka channel - std::string structureId; // Field's Normative Type structure ID or any other arbitrary string if not a normative type - std::string type; // Database field type TriggerNames triggerNames; // Fields in this group which are posted on events from channel - int64_t putOrder; // Order to serialise the field for put operations FieldDefinition(const FieldConfig& fieldConfig, const std::string& fieldName); @@ -42,9 +38,6 @@ public: } }; -typedef std::vector FieldDefinitions; -typedef std::map FieldDefinitionMap; - } // pvxs } // ioc #endif //PVXS_FIELDDEFINITION_H diff --git a/ioc/fieldname.cpp b/ioc/fieldname.cpp index b7637f1..c107538 100644 --- a/ioc/fieldname.cpp +++ b/ioc/fieldname.cpp @@ -13,12 +13,11 @@ #include #include "fieldname.h" +#include "utilpvt.h" namespace pvxs { namespace ioc { -static void pad(std::string& stringToPad, size_t padLength); - /** * Construct a Group field name from a field name string. The string is a sequence of components separated by * periods each of which may be optionally followed by an array specifier. e.g. a.b[1].c. @@ -30,7 +29,7 @@ static void pad(std::string& stringToPad, size_t padLength); FieldName::FieldName(const std::string& fieldName) { if (!fieldName.empty()) { // Split field name on periods - std::stringstream splitter(fieldName); + std::istringstream splitter(fieldName); std::string fieldNamePart; while (std::getline(splitter, fieldNamePart, '.')) { if (fieldNamePart.empty()) { @@ -73,37 +72,37 @@ FieldName::FieldName(const std::string& fieldName) { * @param padLength the amount of padding to add, defaults to none */ std::string FieldName::to_string(size_t padLength) const { - std::string fieldName; - if (fieldNameComponents.empty()) { - fieldName = "/"; + std::ostringstream strm; + strm<<(*this); + auto sofar(strm.tellp()); + if(sofar >=0 && size_t(sofar) < padLength) { + for(auto i : range(padLength - size_t(sofar))) { + (void)i; + strm.put(PADDING_CHARACTER); + } + } + return strm.str(); +} + +std::ostream& operator<<(std::ostream& strm, const FieldName& name) +{ + if (name.fieldNameComponents.empty()) { + strm<<"/"; } else { bool first = true; - for (const auto& fieldNameComponent: fieldNameComponents) { + for (const auto& fieldNameComponent: name.fieldNameComponents) { if (!first) { - fieldName += "."; + strm.put('.'); } else { first = false; } - fieldName += fieldNameComponent.name; + strm< stringToPad.size()) { - stringToPad.insert(stringToPad.size(), padLength - stringToPad.size(), PADDING_CHARACTER); - } + return strm; } } // pvxs diff --git a/ioc/fieldname.h b/ioc/fieldname.h index 8e499bf..4cf0d32 100644 --- a/ioc/fieldname.h +++ b/ioc/fieldname.h @@ -10,6 +10,7 @@ #ifndef PVXS_FIELDNAME_H #define PVXS_FIELDNAME_H +#include #include #include "fieldnamecomponent.h" @@ -40,7 +41,7 @@ public: * * @param suffix the suffix to add to the field name, defaults to none */ - void show(const std::string& suffix = {}) const { + void show(const std::string& suffix) const { printf("%s%s", to_string(PADDING_WIDTH - suffix.size()).c_str(), suffix.c_str()); } @@ -99,8 +100,11 @@ public: return fieldNameComponents[fieldNameComponents.size() - 1].name; } + friend std::ostream& operator<<(std::ostream&, const FieldName&); }; +std::ostream& operator<<(std::ostream&, const FieldName&); + } // pvxs } // ioc diff --git a/ioc/fieldsubscriptionctx.cpp b/ioc/fieldsubscriptionctx.cpp index 9403177..d3eb900 100644 --- a/ioc/fieldsubscriptionctx.cpp +++ b/ioc/fieldsubscriptionctx.cpp @@ -24,23 +24,11 @@ namespace ioc { */ void FieldSubscriptionCtx::subscribeField(dbEventCtx pEventCtx, EVENTFUNC (* subscriptionCallback), unsigned int selectOptions, bool forValues) { - auto& pDbChannel = (forValues ? field->value.channel : field->properties.channel).shared_ptr(); + auto& pDbChannel = (forValues ? field->value : field->properties); auto& pEventSubscription = forValues ? pValueEventSubscription : pPropertiesEventSubscription; - pEventSubscription.reset( - db_add_event( - pEventCtx, - pDbChannel.get(), - subscriptionCallback, - this, selectOptions), - [](dbEventSubscription pEventSub) { - if (pEventSub) { - db_cancel_event(pEventSub); - } - }); - - if (!pEventSubscription) { - throw std::runtime_error("Failed to create db subscription"); - } + pEventSubscription.subscribe(pEventCtx, pDbChannel, + subscriptionCallback, + this, selectOptions); } } // pvcs diff --git a/ioc/fieldsubscriptionctx.h b/ioc/fieldsubscriptionctx.h index 4f2dc76..43e2241 100644 --- a/ioc/fieldsubscriptionctx.h +++ b/ioc/fieldsubscriptionctx.h @@ -32,8 +32,8 @@ class GroupSourceSubscriptionCtx; */ class FieldSubscriptionCtx : public SubscriptionCtx { public: - GroupSourceSubscriptionCtx* pGroupCtx; - Field* field; + GroupSourceSubscriptionCtx* const pGroupCtx; + Field* const field; // Map channel to field index in group.fields void subscribeField(dbEventCtx pEventCtx, EVENTFUNC (* subscriptionCallback), @@ -46,12 +46,15 @@ public: * @param groupSourceSubscriptionCtx the group subscription context this is a part of */ explicit FieldSubscriptionCtx(Field& field, GroupSourceSubscriptionCtx* groupSourceSubscriptionCtx) - :pGroupCtx(groupSourceSubscriptionCtx), field(&field) { + :pGroupCtx(groupSourceSubscriptionCtx), field(&field) + { + if(!field.value) { + // no associated dbChannel, so nothing to wait for + hadValueEvent = hadPropertyEvent = true; + } }; FieldSubscriptionCtx(FieldSubscriptionCtx&&) = default; - - FieldSubscriptionCtx(const FieldSubscriptionCtx&) = delete; }; } // pvcs diff --git a/ioc/group.cpp b/ioc/group.cpp index 3e4262d..9321f7e 100644 --- a/ioc/group.cpp +++ b/ioc/group.cpp @@ -10,11 +10,44 @@ #include #include +#include +#include + #include "group.h" +#include "utilpvt.h" namespace pvxs { namespace ioc { +static +IOCGroupConfig* configInstance; + +static +void onceConfigInstance(void*) +{ + try { + configInstance = new IOCGroupConfig; + } catch (std::exception& e) { + cantProceed("ERROR %s : %s\n", __func__, e.what()); + } +} + +static +epicsThreadOnceId onceConfig = EPICS_THREAD_ONCE_INIT; + +IOCGroupConfig& IOCGroupConfig::instance() +{ + epicsThreadOnce(&onceConfig, &onceConfigInstance, nullptr); + return *configInstance; +} + +void IOCGroupConfigCleanup() +{ + epicsGuard G(configInstance->groupMapMutex); + configInstance->groupMap.clear(); + configInstance->groupConfigFiles.clear(); +} + /** * Show details for this group. * This displays information to the terminal and is to be used by the IOC command shell @@ -28,35 +61,32 @@ void Group::show(int level) const { // no locking as we only print things which are const after initialization // Group field information - printf(" Atomic Get/Put:%s Atomic Monitor:%s Members:%ld\n", + printf(" Atomic Get/Put:%s Atomic Members:%ld\n", (atomicPutGet ? "yes" : "no"), - (atomicMonitor ? "yes" : "no"), fields.size()); // If we need to show detailed information then iterate through all fields showing details if (level > 1) { - if (!fields.empty()) { - for (auto& field: fields) { - if (!field.id.empty()) { - std::string suffix; - printf(" "); - suffix = ""; - field.fieldName.show(suffix); - printf(" <-> \"%s\"\n", field.id.c_str()); - } + for (auto& field: fields) { + // " grp.fld id=foo chan=pv:name.VAL\n" + printf(" %s\t<%s>%s%s%s%s%s\n", + field.fieldName.to_string().c_str(), + MappingInfo::name(field.info.type), + field.id.empty() ? "" : " id=", + field.id.empty() ? "" : field.id.c_str(), + field.value ? " chan=" : "", + field.value ? dbChannelName(field.value) : "", + field.triggers.empty() ? "" : " has triggers"); - if (field.value.channel) { - printf(" "); - std::string suffix; - if (field.isMeta) { - suffix = ""; - } else if (field.allowProc) { - suffix = ""; - } - field.fieldName.show(suffix); - if (field.value.channel) { - printf(" <-> %s\n", dbChannelName(field.value.channel)); + if(level > 2) { + for(auto& trig : field.triggers) { + bool found = false; + for(auto& field2 : fields) { + found |= &field2 == trig; // cross-check pointer validity } + if(!found) + printf("ERROR inconsistent field triggers!!!\n"); + printf(" %s\n", trig->fieldName.to_string().c_str()); } } } @@ -70,17 +100,11 @@ void Group::show(int level) const { * @return the de-referenced field from the set of fields */ Field& Group::operator[](const std::string& fieldName) { - auto foundField = std::find_if(fields.begin(), fields.end(), [fieldName](Field& field) { - return fieldName == field.fullName; - }); - - if (foundField == fields.end()) { - std::ostringstream fileNameStream; - fileNameStream << "field not found in group: \"" << fieldName << "\""; - throw std::logic_error(fileNameStream.str()); - }; - - return *foundField; + for(auto& field : fields) { + if(field.fullName == fieldName) + return field; + } + throw std::logic_error(SB()<<"field not found in group: \"" << fieldName << "\""); } } // pvxs diff --git a/ioc/group.h b/ioc/group.h index e6e4ee5..469f538 100644 --- a/ioc/group.h +++ b/ioc/group.h @@ -10,10 +10,16 @@ #ifndef PVXS_GROUP_H #define PVXS_GROUP_H +#include #include +#include +#include +#include #include +#include + #include "dbmanylocker.h" #include "field.h" @@ -30,34 +36,47 @@ public: class Group { private: public: - std::string name; - Fields fields; - bool atomicPutGet, atomicMonitor; + const std::string name; + const bool atomicPutGet; + std::vector fields; Value valueTemplate; ChannelLocks value; ChannelLocks properties; -/** - * Constructor for IOC group. - * Set the atomic and monitor atomic flags - */ - Group() - :atomicPutGet(false), atomicMonitor(false) { - } - -/** - * Destructor for IOC group - */ - virtual ~Group() = default; - - virtual void show(int level) const; + void show(int level) const; Field& operator[](const std::string& fieldName); + Group(const std::string& name, bool atomicPutGet) + :name(name) + ,atomicPutGet(atomicPutGet) + {} Group(const Group&) = delete; }; -// A map of group name to Group -typedef std::map GroupMap; +struct IOCGroupConfig { + static + IOCGroupConfig& instance(); + + std::map groupMap; + struct JFile { + struct DeleteMac { + void operator()(MAC_HANDLE *handle) { (void)macDeleteHandle(handle); } + }; + + std::unique_ptr jf; + std::string fname; + std::string macros; + std::unique_ptr handle; + JFile(decltype(jf)&& jf, const std::string& fname, const std::string& macros, + decltype(handle)&& handle) + :jf(std::move(jf)), fname(fname), macros(macros), handle(std::move(handle)) + {} + }; + std::list groupConfigFiles; + + // For locking access to groupMap + epicsMutex groupMapMutex{}; +}; } // pvxs } // ioc diff --git a/ioc/groupconfig.h b/ioc/groupconfig.h index 10f3e5e..91b777d 100644 --- a/ioc/groupconfig.h +++ b/ioc/groupconfig.h @@ -25,15 +25,12 @@ class GroupConfig { public: bool atomic, atomicIsSet; std::string structureId; - FieldConfigMap fieldConfigMap; + std::map fieldConfigMap; GroupConfig() :atomic(true), atomicIsSet(false) { } }; -// A map of group name to GroupConfig -typedef std::map GroupConfigMap; - } // pvxs } // ioc diff --git a/ioc/groupconfigprocessor.cpp b/ioc/groupconfigprocessor.cpp index 213ca6a..20321eb 100644 --- a/ioc/groupconfigprocessor.cpp +++ b/ioc/groupconfigprocessor.cpp @@ -28,11 +28,18 @@ #include "utilpvt.h" #include "yajlcallbackhandler.h" +// include last to avoid clash of #define printf with other headers +#include + namespace pvxs { namespace ioc { DEFINE_LOGGER(_logname, "pvxs.ioc.group.processor"); +GroupConfigProcessor::GroupConfigProcessor() + :config(IOCGroupConfig::instance()) +{} + /** * Parse group configuration that has been defined in db configuration files. * This involves extracting info fields named "Q:Group" from the database configuration @@ -68,41 +75,92 @@ void GroupConfigProcessor::loadConfigFromDb() { * Get the list of group files configured on the iocServer and convert them to Group Configuration objects. */ void GroupConfigProcessor::loadConfigFiles() { - runOnPvxsServer([this](IOCServer* pPvxsServer) { - // get list of group files to load - auto& groupConfigFiles = pPvxsServer->groupConfigFiles; + // take the list of group files to load + auto groupConfigFiles(std::move(config.groupConfigFiles)); - // For each file load the configuration file - auto it = groupConfigFiles.begin(); - while (it != groupConfigFiles.end()) { - std::string groupConfigFileName(*it); - groupConfigFiles.erase(it++); + // For each file load the configuration file + for(auto& jfile : groupConfigFiles) { + auto& groupConfigFileName = jfile.fname; - // Get contents of group definition file - std::ifstream jsonGroupConfigStream(groupConfigFileName, std::ifstream::in); - if (!jsonGroupConfigStream.is_open()) { - fprintf(stderr, "Error opening \"%s\"\n", groupConfigFileName.c_str()); - continue; - } - - std::stringstream buffer; - buffer << jsonGroupConfigStream.rdbuf(); - auto jsonGroupConfig = buffer.str(); - - log_debug_printf(_logname, "Process dbGroup file \"%s\"\n", groupConfigFileName.c_str()); - - try { - parseConfigString(jsonGroupConfig.c_str()); - if (!groupProcessingWarnings.empty()) { - fprintf(stderr, "warning(s) from group definition file \"%s\"\n%s\n", - groupConfigFileName.c_str(), groupProcessingWarnings.c_str()); + std::ostringstream buffer; + std::string line; + size_t lineno = 0u; + while(std::getline(*jfile.jf, line)) { + lineno++; + if(jfile.handle) { + if(auto exp = macDefExpand(line.c_str(), jfile.handle.get())) { + try{ + line = exp; + }catch(...){ + free(exp); + throw; + } + free(exp); + } else { + fprintf(stderr, "Error reading \"%s\" line %zu too long\n", + groupConfigFileName.c_str(), lineno); + continue; } - } catch (std::exception& e) { - throw std::runtime_error( - SB() << "Error reading group definition file \"" << groupConfigFileName << "\"\n" << e.what()); } + buffer<eof() || jfile.jf->bad()) { + fprintf(stderr, "Error reading \"%s\"\n", groupConfigFileName.c_str()); + continue; + } + + log_debug_printf(_logname, "Process dbGroup file \"%s\"\n", groupConfigFileName.c_str()); + + try { + parseConfigString(buffer.str().c_str()); + if (!groupProcessingWarnings.empty()) { + fprintf(stderr, "warning(s) from group definition file \"%s\"\n%s\n", + groupConfigFileName.c_str(), groupProcessingWarnings.c_str()); + } + } catch (std::exception& e) { + throw std::runtime_error( + SB() << "Error reading group definition file \"" << groupConfigFileName << "\"\n" << e.what()); + } + } +} + +void GroupConfigProcessor::validateGroups() { + assert(groupDefinitionMap.empty()); // not yet populated + auto groups(std::move(groupConfigMap)); + + for(auto& git : groups) { + try { + auto& group = git.second; + + for(auto& fit : group.fieldConfigMap) { + auto& field = fit.second; + switch(field.info.type) { + case MappingInfo::Scalar: + case MappingInfo::Plain: + case MappingInfo::Any: + case MappingInfo::Meta: + case MappingInfo::Proc: + if(field.channel.empty()) { + throw std::runtime_error(SB()<<"field "<second; + auto& fieldDefinition = groupDefinition.fields.at(index); log_debug_printf(_logname, " pvxs trigger '%s.%s' -> ", groupName.c_str(), fieldName.c_str()); @@ -329,13 +394,14 @@ void GroupConfigProcessor::defineGroupTriggers(FieldDefinition& fieldDefinition, } } else { // otherwise map to the specific target if it exists - if (groupDefinition.fieldMap.count(triggerName) == 0) { + auto it(groupDefinition.fieldMap.find(triggerName)); + if (it == groupDefinition.fieldMap.end()) { fprintf(stderr, "Error: Group \"%s\" defines triggers to nonexistent field \"%s\" \n", groupName.c_str(), triggerName.c_str()); continue; } - auto& index = ((FieldDefinitionMap&)groupDefinition.fieldMap)[triggerName]; - auto& targetedField = groupDefinition.fields[index]; + auto index = it->second; + auto& targetedField = groupDefinition.fields.at(index); assert(targetedField.name == triggerName); // And if it references a PV @@ -358,50 +424,49 @@ void GroupConfigProcessor::defineGroupTriggers(FieldDefinition& fieldDefinition, * 3. Build the lockers for each group and field based on their triggers */ void GroupConfigProcessor::createGroups() { - runOnPvxsServer([this](IOCServer* pPvxsServer) { - auto& groupMap = pPvxsServer->groupMap; + auto& groupMap = config.groupMap; - // First pass: Create groups and get array capacities - for (auto& groupDefinitionMapEntry: groupDefinitionMap) { - auto& groupName = groupDefinitionMapEntry.first; - auto& groupDefinition = groupDefinitionMapEntry.second; - try { - if (groupMap.count(groupName) != 0) { - throw std::runtime_error("Group name already in use"); - } - // Create group - auto& group = groupMap[groupName]; - - // Set basic group information - group.name = groupName; - group.atomicPutGet = groupDefinition.atomic != False; - group.atomicMonitor = groupDefinition.hasTriggers; - - // Initialise the given group's fields from the given group definition - initialiseGroupFields(group, groupDefinition); - } catch (std::exception& e) { - fprintf(stderr, "%s: Error Group not created: %s\n", groupName.c_str(), e.what()); + // First pass: Create groups and get array capacities + for (auto& groupDefinitionMapEntry: groupDefinitionMap) { + auto& groupName = groupDefinitionMapEntry.first; + auto& groupDefinition = groupDefinitionMapEntry.second; + try { + // Create group + auto pair = groupMap.emplace(std::piecewise_construct, + std::forward_as_tuple(groupName), + std::forward_as_tuple(groupName, + groupDefinition.atomic != False)); + if (!pair.second) { + throw std::runtime_error("Group name already in use"); } - } + auto& group = pair.first->second; - // Second Pass: assemble group's PV structure definitions and db locker - for (auto& groupDefinitionMapEntry: groupDefinitionMap) { - auto& groupName = groupDefinitionMapEntry.first; - auto& groupDefinition = groupDefinitionMapEntry.second; - try { - auto& group = groupMap[groupName]; - // Initialise the given group's db locks - initialiseDbLocker(group); - // Initialize the given group's triggers and associated db locks - initialiseTriggers(group, groupDefinition); - // Initialise the given group's value type - initialiseValueTemplate(group, groupDefinition); - } catch (std::exception& e) { - fprintf(stderr, "%s: Error Group not created: %s\n", groupName.c_str(), e.what()); - } + // Initialise the given group's fields from the given group definition + initialiseGroupFields(group, groupDefinition); + } catch (std::exception& e) { + fprintf(stderr, "%s: Error Group not created: %s\n", groupName.c_str(), e.what()); } - }); + } + + // Second Pass: assemble group's PV structure definitions and db locker + for (auto& groupDefinitionMapEntry: groupDefinitionMap) { + auto& groupName = groupDefinitionMapEntry.first; + auto& groupDefinition = groupDefinitionMapEntry.second; + try { + auto it(groupMap.find(groupName)); + assert(it!=groupMap.end()); + auto& group =it->second; + // Initialise the given group's db locks + initialiseDbLocker(group); + // Initialize the given group's triggers and associated db locks + initialiseTriggers(group, groupDefinition); + // Initialise the given group's value type + initialiseValueTemplate(group, groupDefinition); + } catch (std::exception& e) { + fprintf(stderr, "%s: Error Group not created: %s\n", groupName.c_str(), e.what()); + } + } } /** @@ -423,7 +488,7 @@ void GroupConfigProcessor::initialiseGroupFields(Group& group, const GroupDefini // for each field for (auto& fieldDefinition: groupDefinition.fields) { - group.fields.emplace_back(fieldDefinition.name, fieldDefinition.channel, fieldDefinition.structureId); + group.fields.emplace_back(fieldDefinition); } } @@ -473,11 +538,13 @@ void GroupConfigProcessor::initialiseValueTemplate(Group& group, const GroupDefi * @param groupDefinition the group definition */ void GroupConfigProcessor::initialiseTriggers(Group& group, const GroupDefinition& groupDefinition) { + std::vector references; // For all fields in the group for (auto& fieldDefinition: groupDefinition.fields) { // As long as it has a channel specified if (!fieldDefinition.channel.empty()) { auto& field = group[fieldDefinition.name]; + references.clear(); // Look at the fields that it triggers for (auto& referencedFieldName: fieldDefinition.triggerNames) { auto referencedFieldIt = groupDefinition.fieldMap.find(referencedFieldName); @@ -487,18 +554,14 @@ void GroupConfigProcessor::initialiseTriggers(Group& group, const GroupDefinitio // Add new trigger reference field.triggers.emplace_back(&referencedField); // Add new lock record - if (referencedField.value.channel) { - field.value.references.emplace_back(referencedField.value.channel->addr.precord); - } - if (referencedField.properties.channel) { - field.properties.references.emplace_back(referencedField.properties.channel->addr.precord); + if (referencedField.value) { + references.emplace_back(referencedField.value->addr.precord); } } } // Make the locks - field.value.lock = DBManyLock(field.value.references); - field.properties.lock = DBManyLock(field.properties.references); + field.lock = DBManyLock(references); } } } @@ -514,32 +577,221 @@ void GroupConfigProcessor::addTemplatesForDefinedFields(std::vector& gro const GroupDefinition& groupDefinition) { for (auto& fieldDefinition: groupDefinition.fields) { auto& field = group[fieldDefinition.name]; - if (fieldDefinition.channel.empty()) { - addMembersForId(groupMembers, field); - } else { - auto& type = fieldDefinition.type; - - dbChannel* pDbChannel = field.value.channel; - if (type == "meta") { - field.isMeta = true; - addMembersForMetaData(groupMembers, field); - } else if (type == "proc") { - field.allowProc = true; - } else if (type.empty() || type == "scalar") { - addMembersForScalarType(groupMembers, field, pDbChannel); - } else if (type == "plain") { - addMembersForPlainType(groupMembers, field, pDbChannel); - } else if (type == "any") { - addMembersForAnyType(groupMembers, field); - } else if (type == "structure") { - addMembersForStructureType(groupMembers, field); + dbChannel* pDbChannel = field.value; + switch(fieldDefinition.info.type) { + case MappingInfo::Scalar: + addMembersForScalarType(groupMembers, field, pDbChannel); + break; + case MappingInfo::Plain: + addMembersForPlainType(groupMembers, field, pDbChannel); + break; + case MappingInfo::Any: + addMembersForAnyType(groupMembers, field); + break; + case MappingInfo::Meta: + addMembersForMetaData(groupMembers, field); + break; + case MappingInfo::Proc: // nothing to do here + break; + case MappingInfo::Structure: + addMembersForStructureType(groupMembers, field); + break; + case MappingInfo::Const: + { + TypeDef def(field.info.cval); + if(field.fieldName.empty()) { // placing in root + throw std::logic_error("TODO: \"\":{+type:\"const\" ...} not currently supported"); } else { - throw std::runtime_error(std::string("Unknown +type=") + type); + std::vector newMem({def.as(field.fieldName.leafFieldName())}); + setFieldTypeDefinition(groupMembers, field.fieldName, newMem); } } + break; + } } } +/** + * To process key part of json nodes. This will be followed by a boolean, integer, block, or null + * + * @param parserContext the parser context + * @param key the key + * @param keyLength the length of the key + * @return non-zero if successful + */ +static +int parserCallbackKey(void* parserContext, const unsigned char* key, const size_t keyLength) { + return GroupConfigProcessor::yajlProcess(parserContext, [&key, &keyLength](GroupProcessorContext* self) { + if (keyLength == 0 && self->depth != 2) { + throw std::runtime_error("empty group or key name not allowed"); + } + + std::string name((const char*)key, keyLength); + + if (self->depth == 1) { + self->groupName.swap(name); + } else if (self->depth == 2) { + self->field.swap(name); + } else if (self->depth == 3) { + self->key.swap(name); + } else { + throw std::logic_error("Malformed json group definition: too many nesting levels"); + } + + return 1; + }); +} + +/** + * To process null json nodes + * + * @param parserContext the parser context + * @return non-zero if successful + */ +static +int parserCallbackNull(void* parserContext) { + return GroupConfigProcessor::yajlProcess(parserContext, [](GroupProcessorContext* self) { + self->assign(Value()); + return 1; + }); +} + +/** + * To process boolean json nodes + * + * @param parserContext the parser context + * @param booleanValue the boolean value + * @return non-zero if successful + */ +static +int parserCallbackBoolean(void* parserContext, int booleanValue) { + return GroupConfigProcessor::yajlProcess(parserContext, [&booleanValue](GroupProcessorContext* self) { + auto value = pvxs::TypeDef(TypeCode::Bool).create(); + value = booleanValue; + self->assign(value); + return 1; + }); +} + +/** + * To process integer json nodes + * + * @param parserContext the parser context + * @param integerVal the integer value + * @return non-zero if successful + */ +static +int parserCallbackInteger(void* parserContext, long long integerVal) { + return GroupConfigProcessor::yajlProcess(parserContext, [&integerVal](GroupProcessorContext* self) { + auto value = pvxs::TypeDef(TypeCode::Int64).create(); + value = (int64_t)integerVal; + self->assign(value); + return 1; + }); +} + +/** + * To process double json nodes + * + * @param parserContext the parser context + * @param doubleVal the double value + * @return non-zero if successful + */ +static +int parserCallbackDouble(void* parserContext, double doubleVal) { + return GroupConfigProcessor::yajlProcess(parserContext, [&doubleVal](GroupProcessorContext* self) { + auto value = pvxs::TypeDef(TypeCode::Float64).create(); + value = doubleVal; + self->assign(value); + return 1; + }); +} + +/** + * To process string json nodes + * + * @param parserContext the parser context + * @param stringVal the string value + * @param stringLen the string length + * @return non-zero if successful + */ +static +int parserCallbackString(void* parserContext, const unsigned char* stringVal, + const size_t stringLen) { + return GroupConfigProcessor::yajlProcess(parserContext, [&stringVal, &stringLen](GroupProcessorContext* self) { + std::string val((const char*)stringVal, stringLen); + auto value = pvxs::TypeDef(TypeCode::String).create(); + value = val; + self->assign(value); + return 1; + }); +} + +/** + * To start processing new json blocks + * + * @param parserContext the parser context + * @return non-zero if successful + */ +static +int parserCallbackStartBlock(void* parserContext) { + return GroupConfigProcessor::yajlProcess(parserContext, [](GroupProcessorContext* self) { + self->depth++; + if (self->depth > 3) { + throw std::runtime_error("Group field def. can't contain Object (too deep)"); + } + return 1; + }); +} + +/** + * To end processing the current json block + * + * @param parserContext the parser context + * @return non-zero if successful + */ +static +int parserCallbackEndBlock(void* parserContext) { + return GroupConfigProcessor::yajlProcess(parserContext, [](GroupProcessorContext* self) { + assert(self->key.empty()); // cleared in assign() + + if (self->depth == 3) { + self->key.clear(); + } else if (self->depth == 2) { + self->field.clear(); + } else if (self->depth == 1) { + self->groupName.clear(); + } else { + throw std::logic_error("Internal error in json parser: invalid depth"); + } + self->depth--; + + return 1; + }); +} + + + +/** + * These are the callbacks designated by yajl for its parser functions + * They must be defined in this order. + * Note that we don't use number, or arrays + */ +static const +yajl_callbacks yajlParserCallbacks{ + &parserCallbackNull, + &parserCallbackBoolean, + &parserCallbackInteger, + &parserCallbackDouble, + nullptr, // number + &parserCallbackString, + &parserCallbackStartBlock, + &parserCallbackKey, + &parserCallbackEndBlock, + nullptr, // start_array, + nullptr, // end_array, +}; + /** * Parse the given json string as a group configuration part for the given dbRecord * name and extract group definition into our groupDefinitionMap @@ -585,157 +837,6 @@ void GroupConfigProcessor::parseConfigString(const char* jsonGroupDefinition, co } } -/** - * To process key part of json nodes. This will be followed by a boolean, integer, block, or null - * - * @param parserContext the parser context - * @param key the key - * @param keyLength the length of the key - * @return non-zero if successful - */ -int GroupConfigProcessor::parserCallbackKey(void* parserContext, const unsigned char* key, const size_t keyLength) { - return GroupConfigProcessor::yajlProcess(parserContext, [&key, &keyLength](GroupProcessorContext* self) { - if (keyLength == 0 && self->depth != 2) { - throw std::runtime_error("empty group or key name not allowed"); - } - - std::string name((const char*)key, keyLength); - - if (self->depth == 1) { - self->groupName.swap(name); - } else if (self->depth == 2) { - self->field.swap(name); - } else if (self->depth == 3) { - self->key.swap(name); - } else { - throw std::logic_error("Malformed json group definition: too many nesting levels"); - } - - return 1; - }); -} - -/** - * To process null json nodes - * - * @param parserContext the parser context - * @return non-zero if successful - */ -int GroupConfigProcessor::parserCallbackNull(void* parserContext) { - return GroupConfigProcessor::yajlProcess(parserContext, [](GroupProcessorContext* self) { - self->assign(Value()); - return 1; - }); -} - -/** - * To process boolean json nodes - * - * @param parserContext the parser context - * @param booleanValue the boolean value - * @return non-zero if successful - */ -int GroupConfigProcessor::parserCallbackBoolean(void* parserContext, int booleanValue) { - return GroupConfigProcessor::yajlProcess(parserContext, [&booleanValue](GroupProcessorContext* self) { - auto value = pvxs::TypeDef(TypeCode::Bool).create(); - value = booleanValue; - self->assign(value); - return 1; - }); -} - -/** - * To process integer json nodes - * - * @param parserContext the parser context - * @param integerVal the integer value - * @return non-zero if successful - */ -int GroupConfigProcessor::parserCallbackInteger(void* parserContext, long long integerVal) { - return GroupConfigProcessor::yajlProcess(parserContext, [&integerVal](GroupProcessorContext* self) { - auto value = pvxs::TypeDef(TypeCode::Int64).create(); - value = (int64_t)integerVal; - self->assign(value); - return 1; - }); -} - -/** - * To process double json nodes - * - * @param parserContext the parser context - * @param doubleVal the double value - * @return non-zero if successful - */ -int GroupConfigProcessor::parserCallbackDouble(void* parserContext, double doubleVal) { - return GroupConfigProcessor::yajlProcess(parserContext, [&doubleVal](GroupProcessorContext* self) { - auto value = pvxs::TypeDef(TypeCode::Float64).create(); - value = doubleVal; - self->assign(value); - return 1; - }); -} - -/** - * To process string json nodes - * - * @param parserContext the parser context - * @param stringVal the string value - * @param stringLen the string length - * @return non-zero if successful - */ -int GroupConfigProcessor::parserCallbackString(void* parserContext, const unsigned char* stringVal, - const size_t stringLen) { - return GroupConfigProcessor::yajlProcess(parserContext, [&stringVal, &stringLen](GroupProcessorContext* self) { - std::string val((const char*)stringVal, stringLen); - auto value = pvxs::TypeDef(TypeCode::String).create(); - value = val; - self->assign(value); - return 1; - }); -} - -/** - * To start processing new json blocks - * - * @param parserContext the parser context - * @return non-zero if successful - */ -int GroupConfigProcessor::parserCallbackStartBlock(void* parserContext) { - return GroupConfigProcessor::yajlProcess(parserContext, [](GroupProcessorContext* self) { - self->depth++; - if (self->depth > 3) { - throw std::runtime_error("Group field def. can't contain Object (too deep)"); - } - return 1; - }); -} - -/** - * To end processing the current json block - * - * @param parserContext the parser context - * @return non-zero if successful - */ -int GroupConfigProcessor::parserCallbackEndBlock(void* parserContext) { - return GroupConfigProcessor::yajlProcess(parserContext, [](GroupProcessorContext* self) { - assert(self->key.empty()); // cleared in assign() - - if (self->depth == 3) { - self->key.clear(); - } else if (self->depth == 2) { - self->field.clear(); - } else if (self->depth == 1) { - self->groupName.clear(); - } else { - throw std::logic_error("Internal error in json parser: invalid depth"); - } - self->depth--; - - return 1; - }); -} - /** * Get the info field string from the given dbEntry for the given key. * If the key is not found then return the given default value. @@ -755,20 +856,6 @@ const char* GroupConfigProcessor::infoField(DBEntry& dbEntry, const char* key, c return dbGetInfoString(dbEntry); } -/** - * Checks to see if there are trailing comments at the end of the line. - * Throws an exception if there are - * - * @param line the line to check - */ -void GroupConfigProcessor::checkForTrailingCommentsAtEnd(const std::string& line) { - size_t idx = line.find_first_not_of(" \t\n\r"); - if (idx != std::string::npos) { - // trailing comments not allowed - throw std::runtime_error("Trailing comments are not allowed"); - } -} - /** * Add a scalar field as the prescribed subfield by adding the appropriate members to the given members list * @@ -840,29 +927,12 @@ void GroupConfigProcessor::addMembersForStructureType(std::vector& group using namespace pvxs::members; std::vector newIdMembers( - { groupField.isArray ? StructA("", groupField.id, {}) : Struct("", groupField.id, {}) }); + { groupField.isArray ? StructA(groupField.name, groupField.id, {}) : Struct(groupField.name, groupField.id, {}) }); // Add ID to the group members at the position determined by group field name setFieldTypeDefinition(groupMembers, groupField.fieldName, newIdMembers); } -/** - * Add metadata fields to the prescribed subfield (or top level) by adding the appropriate members to the - * given members list. - * - * @param groupMembers the given group members to update - * @param groupField the group field used to determine the members to add and how to create them - */ -void GroupConfigProcessor::addMembersForId(std::vector& groupMembers, const Field& groupField) { - using namespace pvxs::members; - std::vector newMetaMembers({ - Struct(groupField.name, groupField.id, {}), - }); - - // Add metadata to the group members at the position determined by group field name - setFieldTypeDefinition(groupMembers, groupField.fieldName, newMetaMembers); -} - /** * Add metadata fields to the prescribed subfield (or top level) by adding the appropriate members to the * given members list. @@ -903,7 +973,7 @@ TypeDef GroupConfigProcessor::getTypeDefForChannel(const dbChannel* pDbChannel) bool display = true; bool control = true; bool valueAlarm = (dbfType != DBF_STRING); - leaf = nt::NTScalar{ leafCode, display, control, valueAlarm }.build(); + leaf = nt::NTScalar{ leafCode, display, control, valueAlarm, true }.build(); } return leaf; } @@ -932,6 +1002,8 @@ TypeDef GroupConfigProcessor::getTypeDefForChannel(const dbChannel* pDbChannel) * @param groupMembers the group members to add new members to * @param fieldName The field name to use to determine how to create the members * @param leafMembers the leaf member or members to place at the leaf of the members tree + * @param isLeaf If true, leafMembers are added to the parent of the leaf. + * If false, leafMembers are added as members of a Struct which is the leaf */ void GroupConfigProcessor::setFieldTypeDefinition(std::vector& groupMembers, const FieldName& fieldName, const std::vector& leafMembers, bool isLeaf) { @@ -948,6 +1020,7 @@ void GroupConfigProcessor::setFieldTypeDefinition(std::vector& groupMemb childrenToAdd = leafMembers; } + // iterate name in reverse order. rightmost -> leftmost, leaf -> root for (auto componentNumber = fieldName.size(); componentNumber > 0; componentNumber--) { const auto& component = fieldName[componentNumber - 1]; @@ -1020,7 +1093,12 @@ bool GroupConfigProcessor::yajlParseHelper(std::istream& jsonGroupDefinitionStre size_t consumed = yajl_get_bytes_consumed(handle); if (consumed < line.size()) { - checkForTrailingCommentsAtEnd(line.substr(consumed)); + size_t idx = line.find_first_not_of(" \t\n\r", consumed); + if (idx != std::string::npos) { + // TODO: detect the end of potentially multi-line comments... + // for now trailing comments not allowed + throw std::runtime_error("Trailing content after } are not allowed"); + } } #ifndef EPICS_YAJL_VERSION @@ -1089,8 +1167,8 @@ bool GroupConfigProcessor::yajlParseHelper(std::istream& jsonGroupDefinitionStre */ void GroupConfigProcessor::initialiseDbLocker(Group& group) { for (auto& field: group.fields) { - dbChannel* pValueChannel = field.value.channel; - dbChannel* pPropertiesChannel = field.properties.channel; + dbChannel* pValueChannel = field.value; + dbChannel* pPropertiesChannel = field.properties; if (pValueChannel) { group.value.channels.emplace_back(pValueChannel->addr.precord); } diff --git a/ioc/groupconfigprocessor.h b/ioc/groupconfigprocessor.h index 037e225..f0ffc61 100644 --- a/ioc/groupconfigprocessor.h +++ b/ioc/groupconfigprocessor.h @@ -11,13 +11,14 @@ #define PVXS_GROUPCONFIGPROCESSOR_H #include +#include #include #include "dbentry.h" +#include "group.h" #include "groupconfig.h" #include "groupdefinition.h" -#include "iocserver.h" namespace pvxs { namespace ioc { @@ -31,36 +32,20 @@ class GroupProcessorContext; * and converting them to Groups. */ class GroupConfigProcessor { - GroupDefinitionMap groupDefinitionMap; - - /** - * These are the callbacks designated by yajl for its parser functions - * They must be defined in this order. - * Note that we don't use number, or arrays - */ - yajl_callbacks yajlParserCallbacks{ - &parserCallbackNull, - &parserCallbackBoolean, - &parserCallbackInteger, - &parserCallbackDouble, - nullptr, // number - &parserCallbackString, - &parserCallbackStartBlock, - &parserCallbackKey, - &parserCallbackEndBlock, - nullptr, // start_array, - nullptr, // end_array, - }; + // populated by defineGroups() + std::map groupDefinitionMap; public: - GroupConfigMap groupConfigMap; + std::map groupConfigMap; // Group processing warning messages if not empty std::string groupProcessingWarnings; - GroupConfigProcessor() = default; + IOCGroupConfig& config; - static void checkForTrailingCommentsAtEnd(const std::string& line); + GroupConfigProcessor(); + + void validateGroups(); void defineGroups(); void createGroups(); static const char* infoField(DBEntry& dbEntry, const char* key, const char* defaultValue = nullptr); @@ -78,7 +63,6 @@ private: addTemplatesForDefinedFields(std::vector& groupMembers, Group& group, const GroupDefinition& groupDefinition); static void addMembersForAnyType(std::vector& groupMembers, const Field& groupField); - static void addMembersForId(std::vector& groupMembers, const Field& groupField); static void addMembersForMetaData(std::vector& groupMembers, const Field& groupField); static void addMembersForPlainType(std::vector& groupMembers, const Field& groupField, const dbChannel* pDbChannel); @@ -94,14 +78,6 @@ private: const std::string& groupName); void defineFieldSortOrder(); static void resolveSelfTriggerReferences(GroupDefinition& groupDefinition); - static int parserCallbackBoolean(void* parserContext, int booleanValue); - static int parserCallbackDouble(void* parserContext, double doubleVal); - static int parserCallbackEndBlock(void* parserContext); - static int parserCallbackInteger(void* parserContext, long long int integerVal); - static int parserCallbackKey(void* parserContext, const unsigned char* key, size_t keyLength); - static int parserCallbackNull(void* parserContext); - static int parserCallbackStartBlock(void* parserContext); - static int parserCallbackString(void* parserContext, const unsigned char* stringVal, size_t stringLen); void parseConfigString(const char* jsonGroupDefinition, const char* dbRecordName = nullptr); static void defineTriggers(GroupDefinition& groupDefinition, const FieldConfig& fieldConfig, const std::string& fieldName); diff --git a/ioc/groupdefinition.h b/ioc/groupdefinition.h index 5b615fe..6914fca 100644 --- a/ioc/groupdefinition.h +++ b/ioc/groupdefinition.h @@ -21,11 +21,6 @@ namespace pvxs { namespace ioc { -/** - * channel trigger map, maps field name to set of related field it is triggered by - */ -typedef std::map FieldTriggerMap; - /** * A Group PV * This class represents a group PV. It contains a set of channels @@ -37,17 +32,13 @@ public: std::string structureId; // The Normative Type structure ID or any other arbitrary string if not a normative type bool hasTriggers{ false }; TriState atomic{ Unset }; - FieldDefinitions fields; // The group's fields - FieldDefinitionMap fieldMap; // The field map, mapping field order - FieldTriggerMap fieldTriggerMap; // The trigger map, mapping fields to related triggering fields + std::vector fields; // The group's fields + std::map fieldMap; // The field map, mapping field order + std::map fieldTriggerMap; // The trigger map, mapping fields to related triggering fields GroupDefinition() = default; - virtual ~GroupDefinition() = default; }; -// A map of group name to GroupPv -typedef std::map GroupDefinitionMap; - } // pvxs } // ioc diff --git a/ioc/groupprocessorcontext.cpp b/ioc/groupprocessorcontext.cpp index 1e34bf7..74da6ff 100644 --- a/ioc/groupprocessorcontext.cpp +++ b/ioc/groupprocessorcontext.cpp @@ -9,6 +9,7 @@ #include #include "groupprocessorcontext.h" +#include "utilpvt.h" namespace pvxs { namespace ioc { @@ -41,7 +42,26 @@ void GroupProcessorContext::assign(const Value& value) { auto& groupField = groupPvConfig.fieldConfigMap[field]; if (key == "+type") { - groupField.type = value.as(); + auto tname = value.as(); + MappingInfo::type_t type = groupField.info.type; + if(tname == "scalar") { + type = MappingInfo::Scalar; + } else if(tname == "plain") { + type = MappingInfo::Plain; + } else if(tname == "any") { + type = MappingInfo::Any; + } else if(tname == "meta") { + type = MappingInfo::Meta; + } else if(tname == "proc") { + type = MappingInfo::Proc; + } else if(tname == "structure") { + type = MappingInfo::Structure; + } else if(tname == "const") { + type = MappingInfo::Const; + } else { + groupConfigProcessor->groupProcessingWarnings += SB()<<"Unknown mapping +type:\""<(); @@ -55,6 +75,9 @@ void GroupProcessorContext::assign(const Value& value) { } else if (key == "+putorder") { groupField.putOrder = value.as(); + } else if (key == "+const") { + groupField.info.cval = value; + } else { groupConfigProcessor->groupProcessingWarnings += "Unknown group field option "; groupConfigProcessor->groupProcessingWarnings += field + ":" + key; diff --git a/ioc/groupprocessorcontext.h b/ioc/groupprocessorcontext.h index baa3d55..0e2d792 100644 --- a/ioc/groupprocessorcontext.h +++ b/ioc/groupprocessorcontext.h @@ -14,7 +14,6 @@ #include #include "groupconfigprocessor.h" -#include "iocserver.h" namespace pvxs { namespace ioc { diff --git a/ioc/groupsource.cpp b/ioc/groupsource.cpp index 7f22298..87b515c 100644 --- a/ioc/groupsource.cpp +++ b/ioc/groupsource.cpp @@ -39,33 +39,32 @@ DEFINE_LOGGER(_logname, "pvxs.ioc.group.source"); */ GroupSource::GroupSource() :eventContext(db_init_events()) // Initialise event context + ,config(IOCGroupConfig::instance()) { // Get GroupPv configuration and register each pv name in the server - runOnPvxsServer([this](IOCServer* pPvxsServer) { - auto names(std::make_shared>()); + auto names(std::make_shared>()); - // Lock map and get names - { - epicsGuard G(pPvxsServer->groupMapMutex); + // Lock map and get names + { + epicsGuard G(config.groupMapMutex); - // For each defined group, add group name to the list of all records - for (auto& groupMapEntry: pPvxsServer->groupMap) { - auto& groupName = groupMapEntry.first; - names->insert(groupName); - } + // For each defined group, add group name to the list of all records + for (auto& groupMapEntry: config.groupMap) { + auto& groupName = groupMapEntry.first; + names->insert(groupName); } + } - allRecords.names = names; + allRecords.names = names; - // Start event pump - if (!eventContext) { - throw std::runtime_error("Group Source: Event Context failed to initialise: db_init_events()"); - } + // Start event pump + if (!eventContext) { + throw std::runtime_error("Group Source: Event Context failed to initialise: db_init_events()"); + } - if (db_start_events(eventContext.get(), "qsrvGroup", nullptr, nullptr, epicsThreadPriorityCAServerLow - 1)) { - throw std::runtime_error("Could not start event thread: db_start_events()"); - } - }); + if (db_start_events(eventContext.get(), "qsrvGroup", nullptr, nullptr, epicsThreadPriorityCAServerLow - 1)) { + throw std::runtime_error("Could not start event thread: db_start_events()"); + } } /** @@ -79,12 +78,22 @@ void GroupSource::onCreate(std::unique_ptr&& channelCont auto& sourceName = channelControl->name(); log_debug_printf(_logname, "Accepting channel for '%s'\n", sourceName.c_str()); - runOnPvxsServer([&](IOCServer* pPvxsServer) { - // Create callbacks for handling requests and group subscriptions - auto& group = pPvxsServer->groupMap[sourceName]; - createRequestAndSubscriptionHandlers(channelControl, group); - }); + // Create callbacks for handling requests and group subscriptions + auto it(config.groupMap.find(sourceName)); + if(it != config.groupMap.end()) { + auto& group(it->second); + channelControl->onOp([&](std::unique_ptr&& channelConnectOperation) { + onOp(group, std::move(channelConnectOperation)); + }); + channelControl + ->onSubscribe([this, &group](std::unique_ptr&& subscriptionOperation) { + // The group subscription must be kept alive + // We accomplish this further on during the binding of the onStart() + auto subscriptionContext(std::make_shared(group)); + onSubscribe(subscriptionContext, std::move(subscriptionOperation)); + }); + } } /** @@ -93,14 +102,12 @@ void GroupSource::onCreate(std::unique_ptr&& channelCont * @param searchOperation the search operation */ void GroupSource::onSearch(Search& searchOperation) { - runOnPvxsServer([&](IOCServer* pPvxsServer) { - for (auto& pv: searchOperation) { - if (allRecords.names->count(pv.name()) == 1) { - pv.claim(); - log_debug_printf(_logname, "Claiming '%s'\n", pv.name()); - } + for (auto& pv: searchOperation) { + if (allRecords.names->count(pv.name()) == 1) { + pv.claim(); + log_debug_printf(_logname, "Claiming '%s'\n", pv.name()); } - }); + } } /** @@ -115,28 +122,6 @@ void GroupSource::show(std::ostream& outputStream) { } } -/** - * Create request and subscription handlers for group record sources - * - * @param channelControl the control channel pointer that we got from onCreate - * @param group the group that we're creating the request and subscription handlers for - */ -void GroupSource::createRequestAndSubscriptionHandlers(std::unique_ptr& channelControl, - Group& group) { - // Get and Put requests - channelControl->onOp([&](std::unique_ptr&& channelConnectOperation) { - onOp(group, std::move(channelConnectOperation)); - }); - - channelControl - ->onSubscribe([this, &group](std::unique_ptr&& subscriptionOperation) { - // The group subscription must be kept alive - // We accomplish this further on during the binding of the onStart() - auto subscriptionContext(std::make_shared(group)); - onSubscribe(subscriptionContext, std::move(subscriptionOperation)); - }); -} - /** * Called when a client pauses / stops a subscription it has been subscribed to. * This function loops over all fields event subscriptions the group subscription context and disables each of them. @@ -145,11 +130,10 @@ void GroupSource::createRequestAndSubscriptionHandlers(std::unique_ptr& groupSubscriptionCtx) { for (auto& fieldSubscriptionCtx: groupSubscriptionCtx->fieldSubscriptionContexts) { - auto pValueEventSubscription = fieldSubscriptionCtx.pValueEventSubscription.get(); - auto pPropertiesEventSubscription = fieldSubscriptionCtx.pPropertiesEventSubscription.get(); - db_event_disable(pValueEventSubscription); - db_event_disable(pPropertiesEventSubscription); + fieldSubscriptionCtx.pValueEventSubscription.disable(); + fieldSubscriptionCtx.pPropertiesEventSubscription.disable(); } + groupSubscriptionCtx->eventsEnabled = false; } /** @@ -185,9 +169,9 @@ void GroupSource::onOp(Group& group, securityCache->credentials.reset(new Credentials(*putOperation->credentials())); auto fieldIndex = 0u; for (auto& field: group.fields) { - if (field.value.channel) { + if (field.value) { securityCache->securityClients[fieldIndex] - .update(field.value.channel, *securityCache->credentials); + .update(field.value, *securityCache->credentials); } fieldIndex++; } @@ -216,6 +200,29 @@ void GroupSource::onStart(const std::shared_ptr& gro } } +static +void subscriptionPost(GroupSourceSubscriptionCtx *pGroupCtx) +{ + // Make sure that the initial subscription update has occurred on all channels before replying + // As we make two initial updates when opening a new subscription, for each field, + // we need all updates for all fields to have completed before continuing + if (!pGroupCtx->eventsPrimed) { + for (auto& fieldCtx: pGroupCtx->fieldSubscriptionContexts) { + if (!fieldCtx.hadValueEvent || !fieldCtx.hadPropertyEvent) { + return; + } + } + pGroupCtx->eventsPrimed = true; + } + + auto& currentValue = pGroupCtx->currentValue; + + // If events have been primed then return the value to the subscriber, + // and unmark all accumulated changes + pGroupCtx->subscriptionControl->post(currentValue.clone()); + currentValue.unmark(); +} + /** * Called when a client starts a subscription it has subscribed to. For each field in the subscription, * enable events and post a single event to both the values and properties event channels to kick things off. @@ -223,13 +230,82 @@ void GroupSource::onStart(const std::shared_ptr& gro * @param groupSubscriptionCtx the group subscription context containing the field event subscriptions to start */ void GroupSource::onStartSubscription(const std::shared_ptr& groupSubscriptionCtx) { + groupSubscriptionCtx->eventsEnabled = true; for (auto& fieldSubscriptionCtx: groupSubscriptionCtx->fieldSubscriptionContexts) { - auto pValueEventSubscription = fieldSubscriptionCtx.pValueEventSubscription.get(); - auto pPropertiesEventSubscription = fieldSubscriptionCtx.pPropertiesEventSubscription.get(); - db_event_enable(pValueEventSubscription); - db_event_enable(pPropertiesEventSubscription); - db_post_single_event(pValueEventSubscription); - db_post_single_event(pPropertiesEventSubscription); + fieldSubscriptionCtx.pValueEventSubscription.enable(); + fieldSubscriptionCtx.pPropertiesEventSubscription.enable(); + } + // maybe post initial here in pathological case with no +channel. (eg. all const) + subscriptionPost(groupSubscriptionCtx.get()); +} + +/** + * This callback handles notifying of updates to subscribed-to pv values. + * + * @param userArg the user argument passed to the callback function from the framework: a FieldSubscriptionCtx + * @param pDbFieldLog the database field log containing the changes being notified + */ +static +void subscriptionValueCallback(void* userArg, dbChannel* pChannel, int, struct db_field_log* pDbFieldLog) { + try { + auto fieldSubscriptionCtx = (FieldSubscriptionCtx*)userArg; + fieldSubscriptionCtx->hadValueEvent = true; + + // Find the group subscription context from the field subscription context + auto& pGroupCtx = fieldSubscriptionCtx->pGroupCtx; + // Also find the field + auto& field = *fieldSubscriptionCtx->field; + auto currentValue = pGroupCtx->currentValue; + + // lock all records to be triggered + DBManyLocker G(field.lock); + + for (auto& pTriggeredField: field.triggers) { + auto leafNode = pTriggeredField->findIn(currentValue); + dbChannel *channelToUse = pTriggeredField->value; + bool isSelfTrig = channelToUse==pChannel; + auto change = UpdateType::type(UpdateType::Value | UpdateType::Alarm); +#if EPICS_VERSION_INT >= VERSION_INT(7, 0, 6, 0) + if(isSelfTrig && pDbFieldLog) { + // when available, use DBE mask from db_field_log + change = UpdateType::type(pDbFieldLog->mask & UpdateType::Everything); + } +#endif + LocalFieldLog localFieldLog(channelToUse, isSelfTrig ? pDbFieldLog : nullptr); + IOCSource::get(leafNode, pTriggeredField->info, pTriggeredField->anyType, + change, channelToUse, localFieldLog.pFieldLog); + } + + subscriptionPost(pGroupCtx); + + } catch(std::exception& e) { + log_exc_printf(_logname, "Unhandled exception in %s\n", __func__); + } +} + +static +void subscriptionPropertiesCallback(void* userArg, dbChannel* pChannel, int, struct db_field_log* pDbFieldLog) { + try { + auto subscriptionContext = (FieldSubscriptionCtx*)userArg; + subscriptionContext->hadPropertyEvent = true; + + auto& field(*subscriptionContext->field); + + auto fieldValue(field.findIn(subscriptionContext->pGroupCtx->currentValue)); + + /* For a property update, we (may) only post changes to the field mapping + * in question. But never the triggered fields. + */ + + DBLocker L(dbChannelRecord(pChannel)); + LocalFieldLog localFieldLog(pChannel, pDbFieldLog); + IOCSource::get(fieldValue, field.info, field.anyType, + UpdateType::Property, pChannel, pDbFieldLog); + + subscriptionPost(subscriptionContext->pGroupCtx); + + } catch(std::exception& e) { + log_exc_printf(_logname, "Unhandled exception in %s\n", __func__); } } @@ -247,6 +323,12 @@ void GroupSource::onSubscribe(const std::shared_ptr& groupSubscriptionCtx->subscriptionControl = subscriptionOperation ->connect(groupSubscriptionCtx->group.valueTemplate); + server::MonitorStat stats; + groupSubscriptionCtx->subscriptionControl->stats(stats); + // include actual negotiated queue size with initial update + groupSubscriptionCtx->currentValue["record._options.queueSize"] = stats.limitQueue; + groupSubscriptionCtx->currentValue["record._options.atomic"] = true; + // Initialise the field subscription contexts. One for each group field. // This is stored in the group context groupSubscriptionCtx->fieldSubscriptionContexts.reserve(groupSubscriptionCtx->group.fields.size()); @@ -254,16 +336,36 @@ void GroupSource::onSubscribe(const std::shared_ptr& groupSubscriptionCtx->fieldSubscriptionContexts.emplace_back(field, groupSubscriptionCtx.get()); auto& fieldSubscriptionContext = groupSubscriptionCtx->fieldSubscriptionContexts.back(); + if(field.info.type == MappingInfo::Const) { + auto fld(field.findIn(groupSubscriptionCtx->currentValue)); + // only populate const values once + fld.assign(field.info.cval); + continue; // nothing to subscribe + + } else if(!field.value) { + continue; // no associated dbChannel + } + + auto leafNode = field.findIn(groupSubscriptionCtx->currentValue); + IOCSource::initialize(leafNode, field.info, field.value); + // Two subscription are made for each group channel for pvxs - if (field.isMeta) { + // one for value|alarm changes + if (field.info.type == MappingInfo::Meta) { fieldSubscriptionContext .subscribeField(eventContext.get(), subscriptionValueCallback, DBE_ALARM); } else { fieldSubscriptionContext .subscribeField(eventContext.get(), subscriptionValueCallback, DBE_VALUE | DBE_ALARM | DBE_ARCHIVE); } - fieldSubscriptionContext - .subscribeField(eventContext.get(), subscriptionPropertiesCallback, DBE_PROPERTY, false); + // one for property changes + if (field.info.type == MappingInfo::Meta || field.info.type == MappingInfo::Scalar) { + // only scalar and meta mappings include property metadata (display, control, ...) + fieldSubscriptionContext + .subscribeField(eventContext.get(), subscriptionPropertiesCallback, DBE_PROPERTY, false); + } else { + fieldSubscriptionContext.hadPropertyEvent = true; + } } // If all goes well, set up handlers for start and stop monitoring events @@ -273,53 +375,54 @@ void GroupSource::onSubscribe(const std::shared_ptr& }); } +static +bool getGroupField(const Field& field, Value valueTarget, const std::string& groupName, + const std::unique_ptr& getOperation) { + try { + IOCSource::initialize(valueTarget, field.info, field.value); + LocalFieldLog localFieldLog(field.value); + IOCSource::get(valueTarget, field.info, field.anyType, + UpdateType::Everything, field.value, localFieldLog.pFieldLog); + } catch (std::exception& e) { + std::stringstream errorString; + errorString << "Error retrieving value for pvName: " << groupName << (field.name.empty() ? "/" : ".") + << field.fullName << " : " + << e.what(); + getOperation->error(errorString.str().c_str()); + return false; + } + return true; +} + /** * Handle the get operation * * @param group the group to get * @param getOperation the current executing operation */ -void GroupSource::get(Group& group, std::unique_ptr& getOperation) { - groupGet(group, [&getOperation](Value& value) { - getOperation->reply(value); - }, [&getOperation](const char* errorMessage) { - getOperation->error(errorMessage); - }); -} - -/** - * Get each field and make up the whole group structure - * - * @param group group to base result on - * @param returnFn function to call with the result - * @param errorFn function to call on errors - */ -void GroupSource::groupGet(Group& group, const std::function& returnFn, - const std::function& errorFn) { +void GroupSource::get(Group& group, const std::unique_ptr& getOperation) { + bool atomic = group.atomicPutGet; + getOperation->pvRequest()["record._options.atomic"].as(atomic); // Make an empty value to return auto returnValue(group.valueTemplate.cloneEmpty()); + returnValue["record._options.atomic"] = atomic; // If the group is configured for an atomic get operation, // then we need to get all the fields at once, so we lock them all together // and do the operation in one go - if (group.atomicPutGet) { + if (atomic) { // Lock all the fields DBManyLocker G(group.value.lock); // Loop through all fields for (auto& field: group.fields) { - // ignore all zero length named fields that are not meta - if (field.name.empty() && !field.isMeta) { + if(field.info.type == MappingInfo::Proc || field.info.type==MappingInfo::Structure) continue; - } - // find the leaf node in which to set the value auto leafNode = field.findIn(returnValue); - if (leafNode) { - if (!getGroupField(field, leafNode, group.name, errorFn)) { - return; - } + if (!getGroupField(field, leafNode, group.name, getOperation)) { + return; } } @@ -331,19 +434,15 @@ void GroupSource::groupGet(Group& group, const std::function& retu // Loop through all fields for (auto& field: group.fields) { - // ignore all zero length fields that are not meta - if (field.name.empty() && !field.isMeta) { - continue; - } + dbChannel* pDbChannel = field.value; // find the leaf node in which to set the value auto leafNode = field.findIn(returnValue); - if (leafNode) { + if (pDbChannel && leafNode) { // Lock this field - dbChannel* pDbChannel = field.value.channel; DBLocker F(pDbChannel->addr.precord); - if (!getGroupField(field, leafNode, group.name, errorFn)) { + if (!getGroupField(field, leafNode, group.name, getOperation)) { return; } } @@ -351,34 +450,7 @@ void GroupSource::groupGet(Group& group, const std::function& retu } // Send reply - returnFn(returnValue); -} - -/** - * Get a group field into the specified Value target object. The group name is provided in case there are errors - * to better identify the location of the error when the error function is called with the error text - * - * @param field the field to get - * @param valueTarget the place to store the value retrieved - * @param groupName the name of the group that the field is a part of - * @param errorFn the function to call if errors occur - * @return true if retrieved successfully, false otherwise - */ -bool GroupSource::getGroupField(const Field& field, Value valueTarget, const std::string& groupName, - const std::function& errorFn) { - try { - LocalFieldLog localFieldLog(field.value.channel); - IOCSource::get(field.value.channel, nullptr, valueTarget, - field.isMeta ? FOR_METADATA : FOR_VALUE_AND_PROPERTIES, localFieldLog.pFieldLog); - } catch (std::exception& e) { - std::stringstream errorString; - errorString << "Error retrieving value for pvName: " << groupName << (field.name.empty() ? "/" : ".") - << field.fullName << " : " - << e.what(); - errorFn(errorString.str().c_str()); - return false; - } - return true; + getOperation->reply(returnValue); } /** @@ -392,13 +464,17 @@ bool GroupSource::getGroupField(const Field& field, Value valueTarget, const std void GroupSource::putGroup(Group& group, std::unique_ptr& putOperation, const Value& value, const GroupSecurityCache& groupSecurityCache) { try { + CurrentOp op(putOperation.get()); + + bool atomic = group.atomicPutGet; + putOperation->pvRequest()["record._options.atomic"].as(atomic); + std::vector securityLoggers(group.fields.size()); // Prepare group put operation auto fieldIndex = 0; for (auto& field: group.fields) { - dbChannel* pDbChannel = field.value.channel; - if (pDbChannel) { + if (dbChannel* pDbChannel = field.value) { IOCSource::doPreProcessing(pDbChannel, securityLoggers[fieldIndex], *groupSecurityCache.credentials, groupSecurityCache.securityClients[fieldIndex]); @@ -416,7 +492,7 @@ void GroupSource::putGroup(Group& group, std::unique_ptr& putOpe // If the group is configured for an atomic put operation, // then we need to put all the fields at once, so we lock them all together // and do the operation in one go - if (group.atomicPutGet) { + if (atomic) { // Lock all the fields DBManyLocker G(group.value.lock); // Loop through all fields @@ -424,7 +500,7 @@ void GroupSource::putGroup(Group& group, std::unique_ptr& putOpe // Put the field putGroupField(value, field, groupSecurityCache.securityClients[fieldIndex]); // Do processing if required - IOCSource::doPostProcessing(field.value.channel, groupSecurityCache.forceProcessing); + IOCSource::doPostProcessing(field.value, groupSecurityCache.forceProcessing); fieldIndex++; } @@ -436,13 +512,15 @@ void GroupSource::putGroup(Group& group, std::unique_ptr& putOpe // Loop through all fields for (auto& field: group.fields) { - dbChannel* pDbChannel = field.value.channel; + dbChannel* pDbChannel = field.value; + if(!pDbChannel) + continue; // Lock this field DBLocker F(pDbChannel->addr.precord); // Put the field putGroupField(value, field, groupSecurityCache.securityClients[fieldIndex]); // Do processing if required - IOCSource::doPostProcessing(field.value.channel, groupSecurityCache.forceProcessing); + IOCSource::doPostProcessing(field.value, groupSecurityCache.forceProcessing); // Unlock this field when locker goes out of scope fieldIndex++; } @@ -476,99 +554,8 @@ void GroupSource::putGroupField(const Value& value, const Field& field, const Se // If the field references a valid part of the given value then we can send it to the database if (leafNode && leafNode.isMarked()) { - SecurityLogger securityLogger; IOCSource::doFieldPreProcessing(securityClient); // pre-process field - IOCSource::put(field.value.channel, leafNode); - } -} - -/** - * Used by both value and property subscriptions, this function will get the database value and return it - * to the monitor. It is called whenever a field subscription event is received. - * - * @param fieldSubscriptionCtx the field subscription context - * @param getOperationType the operation this callback serves - * @param pDbFieldLog the database field log - */ -void GroupSource::subscriptionCallback(FieldSubscriptionCtx* fieldSubscriptionCtx, - const GetOperationType getOperationType, struct db_field_log* pDbFieldLog) { - - // Find the group subscription context from the field subscription context - auto& pGroupCtx = fieldSubscriptionCtx->pGroupCtx; - // Also find the field - auto field = fieldSubscriptionCtx->field; - - // Get the current value of this group subscription - // We simply merge new field changes onto this value as events occur - auto currentValue = pGroupCtx->currentValue; - - // Lock only fields triggered by this field - DBManyLocker G(getOperationType <= FOR_METADATA ? field->value.lock : field->properties.lock); - - // for all triggered fields get the values. Assumes that self has been added to triggered list - for (auto& pTriggeredField: field->triggers) { - // Find leaf node within the current value. This will be a reference into the currentValue. - // So that if we assign the leafNode with the value we `get()` back, then currentValue will be updated - auto leafNode = pTriggeredField->findIn(currentValue); - if (leafNode) { - dbChannel* channelToUse = (getOperationType == FOR_PROPERTIES) ? pTriggeredField->properties.channel - : pTriggeredField->value.channel; - LocalFieldLog localFieldLog(channelToUse, (pTriggeredField == field) ? pDbFieldLog : nullptr); - IOCSource::get(pTriggeredField->value.channel, pTriggeredField->properties.channel, - leafNode, getOperationType, localFieldLog.pFieldLog); - } - } - - // Make sure that the initial subscription update has occurred on all channels before replying - // As we make two initial updates when opening a new subscription, for each field, - // we need all updates for all fields to have completed before continuing - if (!pGroupCtx->eventsPrimed) { - for (auto& fieldCtx: pGroupCtx->fieldSubscriptionContexts) { - if (!fieldCtx.hadValueEvent || !fieldCtx.hadPropertyEvent) { - return; - } - } - pGroupCtx->eventsPrimed = true; - } - - // If events have been primed then return the value to the subscriber, - // and unmark all accumulated changes - pGroupCtx->subscriptionControl->post(currentValue.clone()); - currentValue.unmark(); - - // Unlock fields in group when locker goes out of scope -} - -/** - * This callback handles notifying of updates to subscribed-to pv values. - * - * @param userArg the user argument passed to the callback function from the framework: a FieldSubscriptionCtx - * @param pDbFieldLog the database field log containing the changes being notified - */ -void GroupSource::subscriptionValueCallback(void* userArg, dbChannel*, int, struct db_field_log* pDbFieldLog) { - auto subscriptionContext = (FieldSubscriptionCtx*)userArg; - { - epicsGuard G((subscriptionContext->pGroupCtx)->eventLock); - subscriptionContext->hadValueEvent = true; - } - subscriptionCallback(subscriptionContext, - subscriptionContext->field->isMeta ? FOR_METADATA : FOR_VALUE, pDbFieldLog); -} - -/** - * This callback handles notifying of updates to subscribed-to pv properties. - * - * @param userArg the user argument passed to the callback function from the framework: a FieldSubscriptionCtx - * @param pDbFieldLog the database field log containing the changes being notified - */ -void GroupSource::subscriptionPropertiesCallback(void* userArg, dbChannel*, int, struct db_field_log* pDbFieldLog) { - auto subscriptionContext = (FieldSubscriptionCtx*)userArg; - { - epicsGuard G((subscriptionContext->pGroupCtx)->eventLock); - subscriptionContext->hadPropertyEvent = true; - } - if (!subscriptionContext->field->isMeta) { - subscriptionCallback(subscriptionContext, FOR_PROPERTIES, pDbFieldLog); + IOCSource::put(field.value, leafNode, field.info); } } diff --git a/ioc/groupsource.h b/ioc/groupsource.h index 91f2f82..fcd9fb9 100644 --- a/ioc/groupsource.h +++ b/ioc/groupsource.h @@ -12,7 +12,6 @@ #include "dbeventcontextdeleter.h" #include "groupsrcsubscriptionctx.h" -#include "iocserver.h" #include "iocsource.h" #include "securityclient.h" @@ -43,20 +42,15 @@ private: // The event context for all subscriptions DBEventContext eventContext; - // Create request and subscription handlers for single record sources - void createRequestAndSubscriptionHandlers(std::unique_ptr& channelControl, - Group& group); + IOCGroupConfig& config; + // Handles all get, put and subscribe requests static void onOp(Group& group, std::unique_ptr&& channelConnectOperation); ////////////////////////////// // Get ////////////////////////////// - static void get(Group& group, std::unique_ptr& getOperation); - static void groupGet(Group& group, const std::function& returnFn, - const std::function& errorFn); - static bool getGroupField(const Field& field, Value valueTarget, const std::string& groupName, - const std::function& errorFn); + static void get(Group& group, const std::unique_ptr& getOperation); ////////////////////////////// // Put @@ -67,15 +61,6 @@ private: ////////////////////////////// // Subscriptions ////////////////////////////// - // Called when values are requested by a subscription - static void - subscriptionValueCallback(void* userArg, dbChannel* pDbChannel, int eventsRemaining, db_field_log* pDbFieldLog); - static void - subscriptionPropertiesCallback(void* userArg, dbChannel* pDbChannel, int eventsRemaining, - db_field_log* pDbFieldLog); - static void - subscriptionCallback(FieldSubscriptionCtx* fieldSubscriptionCtx, - GetOperationType getOperationType, struct db_field_log* pDbFieldLog); static void onDisableSubscription(const std::shared_ptr& groupSubscriptionCtx); static void onStartSubscription(const std::shared_ptr& groupSubscriptionCtx); void onSubscribe(const std::shared_ptr& groupSubscriptionCtx, diff --git a/ioc/groupsourcehooks.cpp b/ioc/groupsourcehooks.cpp index 44ad022..1941be5 100644 --- a/ioc/groupsourcehooks.cpp +++ b/ioc/groupsourcehooks.cpp @@ -9,8 +9,11 @@ #include +#include + #include #include +#include #include @@ -21,7 +24,11 @@ #include "groupconfigprocessor.h" #include "iocshcommand.h" -// must include after log.h has been included to avoid clash with printf macro +#if EPICS_VERSION_INT < VERSION_INT(7, 0, 3, 1) +# define iocshSetError(ret) do { (void)ret; }while(0) +#endif + +// include last to avoid clash of #define printf with other headers #include namespace pvxs { namespace ioc { @@ -31,10 +38,10 @@ namespace ioc { * * @param jsonFileName */ -void dbLoadGroupCmd(const char* jsonFileName) { - (void)dbLoadGroup(jsonFileName); - auto gp = GroupConfigProcessor(); - gp.loadConfigFiles(); +static +void dbLoadGroupCmd(const char* jsonFileName, const char *macros) { + iocshSetError(dbLoadGroup(jsonFileName, macros)); + GroupConfigProcessor().loadConfigFiles(); } /** @@ -44,70 +51,113 @@ void dbLoadGroupCmd(const char* jsonFileName) { * @param level optional depth to show details for * @param pattern optionally only show records matching the regex pattern */ +static void pvxsgl(int level, const char* pattern) { - runOnPvxsServer(([level, &pattern](IOCServer* pPvxsServer) { - try { - // Default pattern to match everything - if (!pattern) { - pattern = ""; - } + try { + // Default pattern to match everything + if (!pattern) { + pattern = ""; + } - { - epicsGuard G(pPvxsServer->groupMapMutex); + { + auto& config(IOCGroupConfig::instance()); + epicsGuard G(config.groupMapMutex); - // For each group - for (auto& mapEntry: pPvxsServer->groupMap) { - auto& groupName = mapEntry.first; - auto& group = mapEntry.second; - // if no pattern specified or the pattern matches - if (!pattern[0] || !!epicsStrGlobMatch(groupName.c_str(), pattern)) { - // Print the group name - printf("%s\n", groupName.c_str()); - // print sub-levels if required - if (level > 0) { - group.show(level); - } + // For each group + for (auto& mapEntry: config.groupMap) { + auto& groupName = mapEntry.first; + auto& group = mapEntry.second; + // if no pattern specified or the pattern matches + if (!pattern[0] || !!epicsStrGlobMatch(groupName.c_str(), pattern)) { + // Print the group name + printf("%s\n", groupName.c_str()); + // print sub-levels if required + if (level > 0) { + group.show(level); } } } - } catch (std::exception& e) { - fprintf(stderr, "%s\n", e.what()); } - })); + } catch (std::exception& e) { + fprintf(stderr, "%s\n", e.what()); + } } -/** - * Load JSON group definition file. - * This function does not actually parse the given file, but adds it to the list of files to be loaded, - * at the appropriate time in the startup process. - * -* @param jsonFilename the json file containing the group definitions. If filename is a dash or a dash then star, the list of - * files is cleared. If it starts with a dash followed by a filename then file is removed from the list. Otherwise - * the filename is added to the list of files to be loaded. - * @return 0 for success, 1 for failure - */ -long dbLoadGroup(const char* jsonFilename) { +static +const auto dbLoadGroupMsg = + "dbLoadGroup(\"file.json\")\n" + "dbLoadGroup(\"file.json\", \"MAC=value,...\")\n" + "\n" + "Load additional DB group definitions from file.\n" + "\n" + "dbLoadGroup(\"-*\")\n" + "dbLoadGroup(\"-file.json\")\n" + "dbLoadGroup(\"-file.json\", \"MAC=value,...\")\n" + "\n" + "Remove all, or one, previously added group definitions.\n" + ; + +long dbLoadGroup(const char* jsonFilename, const char* macros) { try { if (!jsonFilename || !jsonFilename[0]) { - printf("dbLoadGroup(\"file.json\")\n" - "Load additional DB group definitions from file.\n"); - fprintf(stderr, "Missing filename\n"); + fprintf(stderr, "%s\n" + "Error: Missing required JSON filename\n", dbLoadGroupMsg); return 1; } + if(!macros) + macros = ""; - runOnPvxsServer([&jsonFilename](IOCServer* pPvxsServer) { - if (jsonFilename[0] == '-') { - jsonFilename++; - if (jsonFilename[0] == '*' && jsonFilename[1] == '\0') { - pPvxsServer->groupConfigFiles.clear(); - } else { - pPvxsServer->groupConfigFiles.remove(jsonFilename); - } - } else { - pPvxsServer->groupConfigFiles.remove(jsonFilename); - pPvxsServer->groupConfigFiles.emplace_back(jsonFilename); + bool remove = jsonFilename[0] == '-'; + if(remove) + jsonFilename++; + + auto& config(IOCGroupConfig::instance()); + auto& gCF = config.groupConfigFiles; + + if(strcmp(jsonFilename, "*")==0) { + gCF.clear(); + return 0; + } + + decltype(IOCGroupConfig::JFile::jf) jfile; + decltype(IOCGroupConfig::JFile::handle) macs; + if(!remove) { + jfile.reset(new std::ifstream(jsonFilename)); + if (!jfile->is_open()) { + fprintf(stderr, "Error opening \"%s\"\n", jsonFilename); + return 1; } - }); + + if(macros[0]!='\0') { + MAC_HANDLE* mac; + const char * env_pair[] = {"", "environ", NULL, NULL}; + if(macCreateHandle(&mac, env_pair)) + throw std::bad_alloc(); + macs.reset(mac); + + char **pairs = nullptr; + + auto noinstall = macParseDefns(mac, macros, &pairs)<0 || macInstallMacros(mac, pairs)<0; + free(pairs); + if(noinstall) { + fprintf(stderr, "Error Invalid macros for \"%s\", \"%s\"\n", + jsonFilename, macros); + return 1; + } + } + + } + + for(auto next(gCF.begin()); next != gCF.end();) + { + auto it(next++); + if(it->fname==jsonFilename && it->macros==macros) + config.groupConfigFiles.erase(it); + } + + if(!remove) { + gCF.emplace_back(std::move(jfile), jsonFilename, macros, std::move(macs)); + } return 0; } catch (std::exception& e) { fprintf(stderr, "Error: %s\n", e.what()); @@ -129,25 +179,37 @@ using namespace pvxs; * @param theInitHookState the initHook state - we only want to trigger on the initHookAfterIocBuilt state - ignore all others */ void qsrvGroupSourceInit(initHookState theInitHookState) { - if (theInitHookState == initHookAfterInitDatabase) { - GroupConfigProcessor processor; - // Parse all info(Q:Group... records to configure groups - processor.loadConfigFromDb(); + try { + if(!IOCSource::enabled()) + return; + if (theInitHookState == initHookAfterInitDatabase) { + GroupConfigProcessor processor; + epicsGuard G(processor.config.groupMapMutex); - // Load group configuration files - processor.loadConfigFiles(); + // Parse all info(Q:Group... records to configure groups + processor.loadConfigFromDb(); - // Configure groups - processor.defineGroups(); + // Load group configuration files + processor.loadConfigFiles(); - // Resolve triggers - processor.resolveTriggerReferences(); + // checks on groupConfigMap + processor.validateGroups(); - // Create Server Groups - processor.createGroups(); - } else if (theInitHookState == initHookAfterIocBuilt) { - // Load group configuration from parsed groups in iocServer - pvxs::ioc::iocServer().addSource("qsrvGroup", std::make_shared(), 1); + // Configure groups + processor.defineGroups(); + + // Resolve triggers + processor.resolveTriggerReferences(); + + // Create Server Groups + processor.createGroups(); + } else if (theInitHookState == initHookAfterIocBuilt) { + // Load group configuration from parsed groups in iocServer + pvxs::ioc::server().addSource("qsrvGroup", std::make_shared(), 1); + } + } catch(std::exception& e) { + fprintf(stderr, "ERROR: Unhandled exception in %s(%d): %s\n", + __func__, theInitHookState, e.what()); } } @@ -164,16 +226,15 @@ void qsrvGroupSourceInit(initHookState theInitHookState) { */ void pvxsGroupSourceRegistrar() { // Register commands to be available in the IOC shell - IOCShCommand("pvxsgl", "[level, [pattern]]", "Group Sources list.\n" - "List record/field names.\n" - "If `level` is set then show only down to that level.\n" - "If `pattern` is set then show records that match the pattern.") + IOCShCommand("pvxgl", "[level, [pattern]]", + "Group Sources list.\n" + "List record/field names.\n" + "If `level` is set then show only down to that level.\n" + "If `pattern` is set then show records that match the pattern.") .implementation<&pvxsgl>(); - IOCShCommand("dbLoadGroup", "jsonDefinitionFile", "Load Group Record Definition from given file.\n" - "'-' or '-*' to remove previous files.\n" - "'-' to remove the file from the list.\n" - "otherwise add the file to the list of files to load.\n") + IOCShCommand("dbLoadGroup", + "JSON file", "macros", dbLoadGroupMsg) .implementation<&dbLoadGroupCmd>(); initHookRegister(&qsrvGroupSourceInit); diff --git a/ioc/groupsrcsubscriptionctx.h b/ioc/groupsrcsubscriptionctx.h index 6404662..f51ad1a 100644 --- a/ioc/groupsrcsubscriptionctx.h +++ b/ioc/groupsrcsubscriptionctx.h @@ -28,6 +28,7 @@ public: Group& group; epicsMutex eventLock{}; bool eventsPrimed = false, firstEvent = true; + bool eventsEnabled = false; std::unique_ptr subscriptionControl{}; // This is as a special case for storing the initial value prior to both initial subscription events returning @@ -38,6 +39,9 @@ public: explicit GroupSourceSubscriptionCtx(Group& subscribedGroup) :group(subscribedGroup), currentValue(subscribedGroup.valueTemplate.cloneEmpty()) { } + ~GroupSourceSubscriptionCtx() { + assert(!eventsEnabled); // check for mis-matched onStartSubscription()/onDisableSubscription() + } }; diff --git a/ioc/imagedemo.c b/ioc/imagedemo.c index fda70f7..ef662a2 100644 --- a/ioc/imagedemo.c +++ b/ioc/imagedemo.c @@ -19,7 +19,7 @@ * VALA - pixel array (USHORT) */ static -long QSRV_image_demo(aSubRecord* prec) { +long QSRV2_image_demo(aSubRecord* prec) { epicsUInt32 H = *(epicsUInt32*)prec->a, W = *(epicsUInt32*)prec->b; epicsUInt16* I = (epicsUInt16*)prec->vala; @@ -43,4 +43,4 @@ long QSRV_image_demo(aSubRecord* prec) { return 0; } -epicsRegisterFunction(QSRV_image_demo); +epicsRegisterFunction(QSRV2_image_demo); diff --git a/ioc/iochooks.cpp b/ioc/iochooks.cpp index cfd6c01..d40ffd6 100644 --- a/ioc/iochooks.cpp +++ b/ioc/iochooks.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -25,11 +26,10 @@ #include #include -#include "iocserver.h" #include "iocshcommand.h" #include "utilpvt.h" -// must include after log.h has been included to avoid clash with printf macro +// include last to avoid clash of #define printf with other headers #include #if EPICS_VERSION_INT >= VERSION_INT(7, 0, 4, 0) @@ -42,23 +42,14 @@ namespace ioc { DEFINE_LOGGER(_logname, "pvxs.ioc"); // The pvxs server singleton -std::atomic pvxsServer{}; +std::atomic pvxsServer{}; /** * Get the plain pvxs server instance * * @return the pvxs server instance */ -server::Server& server() { - return iocServer(); -} - -/** - * Get the pvxs server instance - * - * @return the pvxs server instance - */ -IOCServer& iocServer() { +server::Server server() { if (auto pPvxsServer = pvxsServer.load()) { return *pPvxsServer; } else { @@ -66,31 +57,6 @@ IOCServer& iocServer() { } } -/** - * Get the pvxs server and execute the given function against it - * - * @param function the function to call - * @param method the string method from which this is called. Use the __func__ macro by default - * @param context the activity being attempted when the error occurred - */ -void -runOnServer(const std::function& function, const char* method, const char* context) { - try { - if (auto pPvxsServer = pvxsServer.load()) { - function(pPvxsServer); - } - } catch (std::exception& e) { - if (context) { - fprintf(stderr, "%s: ", context); - } - if (method) { - fprintf(stderr, "Error in %s: ", method); - } - fprintf(stderr, "%s\n", e.what()); - throw e; - } -} - /** * The function to call when we exit the IOC process. This is only installed as the callback function * after the database has been initialized. This function will stop the pvxs server instance and destroy the @@ -98,17 +64,26 @@ runOnServer(const std::function& function, const char* method, * * @param pep - The pointer to the exit parameter list - unused */ -void pvxsAtExit(void* pep) { - runOnPvxsServerWhile_("In IOC exit event handler", [](IOCServer* pPvxsServer) { +static +void pvxsAtExit(void*) { + runOnPvxsServerWhile_("In IOC exit event handler", [](server::Server* pPvxsServer) { if (pvxsServer.compare_exchange_strong(pPvxsServer, nullptr)) { // take ownership - std::unique_ptr serverInstance(pPvxsServer); + std::unique_ptr serverInstance(pPvxsServer); serverInstance->stop(); + IOCGroupConfigCleanup(); log_debug_printf(_logname, "Stopped Server%s", "\n"); } }); } +void testShutdown() +{ +#ifndef USE_DEINIT_HOOKS + pvxsAtExit(nullptr); +#endif +} + //////////////////////////////////// // Two ioc shell commands for pvxs //////////////////////////////////// @@ -120,8 +95,9 @@ void pvxsAtExit(void* pep) { * * @param detail */ +static void pvxsr(int detail) { - runOnPvxsServer([&detail](IOCServer* pPvxsServer) { + runOnPvxsServer([&detail](server::Server* pPvxsServer) { std::ostringstream strm; Detailed D(strm, detail); strm << *pPvxsServer; @@ -141,6 +117,7 @@ void pvxsr(int detail) { * - thread count, and * - EPICS PVA environment variable settings */ +static void pvxsi() { try { std::ostringstream capture; @@ -160,35 +137,38 @@ void pvxsi() { * * @param theInitHookState the initHook state to respond to */ +static void pvxsInitHook(initHookState theInitHookState) { - // iocBuild() - if (theInitHookState == initHookAfterInitDatabase) { + switch(theInitHookState) { + case initHookAfterInitDatabase: + // when de-init hooks not available, register for later cleanup via atexit() // function to run before exitDatabase #ifndef USE_DEINIT_HOOKS epicsAtExit(&pvxsAtExit, nullptr); #endif - } else - // iocRun() - if (theInitHookState == initHookAfterCaServerRunning || theInitHookState == initHookAfterIocRunning) { - runOnPvxsServer([](IOCServer* pPvxsServer) { + break; + case initHookAfterCaServerRunning: + case initHookAfterIocRunning: + runOnPvxsServer([](server::Server* pPvxsServer) { pPvxsServer->start(); log_debug_printf(_logname, "Started Server %p", pPvxsServer); }); - } else - // iocPause() - if (theInitHookState == initHookAfterCaServerPaused) { - runOnPvxsServer([](IOCServer* pPvxsServer) { + break; + case initHookAfterCaServerPaused: + runOnPvxsServer([](server::Server* pPvxsServer) { pPvxsServer->stop(); log_debug_printf(_logname, "Stopped Server %p", pPvxsServer); }); - } else - + break; #ifdef USE_DEINIT_HOOKS - // iocShutdown() (called from exitDatabase() at exit, and testIocShutdownOk() ) - if (theInitHookState == initHookAtShutdown) { + // use de-init hook when available + case initHookAtShutdown: pvxsAtExit(nullptr); - } + break; #endif + default: + break; + } } } @@ -198,12 +178,6 @@ using namespace pvxs::ioc; namespace { -// Initialise the pvxs server instance -void initialisePvxsServer(); - -// Register callback functions to be used in the IOC shell and also during initialization. -void pvxsBaseRegistrar(); - /** * Create the pvxs server instance. We use the global pvxsServer atomic */ @@ -212,7 +186,7 @@ void initialisePvxsServer() { auto serv = pvxsServer.load(); if (!serv) { Config conf = ::pvxs::impl::inUnitTest() ? Config::isolated() : Config::from_env(); - std::unique_ptr temp(new IOCServer(conf)); + std::unique_ptr temp(new Server(conf)); if (pvxsServer.compare_exchange_strong(serv, temp.get())) { log_debug_printf(_logname, "Installing Server %p\n", temp.get()); diff --git a/ioc/iocserver.h b/ioc/iocserver.h deleted file mode 100644 index 81d159b..0000000 --- a/ioc/iocserver.h +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright - See the COPYRIGHT that is included with this distribution. - * pvxs is distributed subject to a Software License Agreement found - * in file LICENSE that is included with this distribution. - * - * Author George S. McIntyre , 2023 - * - */ - -#ifndef PVXS_IOCSERVER_H -#define PVXS_IOCSERVER_H - -#include - -#include - -#include "group.h" - -namespace pvxs { -namespace ioc { - -class IOCServer : public server::Server { - -public: - explicit IOCServer(const server::Config& config) - :pvxs::server::Server(config) { - } - - GroupMap groupMap; - std::list groupConfigFiles; - - // For locking access to groupMap - epicsMutex groupMapMutex{}; -}; - -IOCServer& iocServer(); - -} // pvxs -} // ioc - -#endif //PVXS_IOCSERVER_H diff --git a/ioc/iocshcommand.h b/ioc/iocshcommand.h index 4e2cac6..99d8720 100644 --- a/ioc/iocshcommand.h +++ b/ioc/iocshcommand.h @@ -19,7 +19,6 @@ #include "iocshargument.h" #include "iocshindex.h" -#include "iocserver.h" namespace pvxs { namespace ioc { @@ -72,8 +71,11 @@ public: static const iocshArg argstack[1 + sizeof...(IOCShFunctionArgumentTypes)] = { { argumentNames[Idxs], IOCShFunctionArgument::code }... }; static const iocshArg* const arguments[] = { &argstack[Idxs]..., 0 }; - static const iocshFuncDef functionDefinition = { name, sizeof...(IOCShFunctionArgumentTypes), arguments, - usage }; + static const iocshFuncDef functionDefinition = { name, sizeof...(IOCShFunctionArgumentTypes), arguments +#ifdef IOCSHFUNCDEF_HAS_USAGE + ,usage +#endif + }; iocshRegister(&functionDefinition, &call < function, Idxs... >); } @@ -88,8 +90,32 @@ public: } }; -void runOnServer(const std::function& function, const char* method = nullptr, - const char* context = nullptr); +extern std::atomic pvxsServer; + +/** + * Get the pvxs server and execute the given function against it + * + * @param function the function to call + * @param method the string method from which this is called. Use the __func__ macro by default + * @param context the activity being attempted when the error occurred + */ +template +void +runOnServer(FN&& function, const char* method, const char* context = nullptr) { + try { + if (auto pPvxsServer = pvxsServer.load()) { + function(pPvxsServer); + } + } catch (std::exception& e) { + fprintf(stderr, "%s%sError in %s: %s\n", + context ? context : "", + context ? ": " : "", + method, + e.what()); + throw e; + } +} + } // pvxs } // ioc diff --git a/ioc/iocsource.cpp b/ioc/iocsource.cpp index 81891d8..8cbff68 100644 --- a/ioc/iocsource.cpp +++ b/ioc/iocsource.cpp @@ -9,10 +9,14 @@ #include #include +#include #include #include +#include +#include +#include "alarm.h" #include "iocsource.h" #include "dbentry.h" #include "dberrormessage.h" @@ -20,258 +24,369 @@ #include "credentials.h" #include "securityclient.h" #include "securitylogger.h" +#include "utilpvt.h" + +// include last to avoid clash of #define printf with other headers +#include namespace pvxs { namespace ioc { -/** - * IOC function that will get data from the database. This will use the provided value prototype to determine the shape - * of the data to be returned (if it has a `value` subfield, it is a structure). The provided channel - * is used to retrieve the data and the flags forValues and forProperties are used to determine whether to fetch - * values, properties or both from the database. - * - * When the data has been retrieved the provided returnFn is called with the value, otherwise the an - * exception is thrown - * - * @param pDbValueChannel the channel to get the value from - * @param pDbPropertiesChannel the channel to get the properties from - * @param valuePrototype the value prototype to use to determine the shape of the data - * @param getOperationType for values, for properties or for metadata - * @param pDbFieldLog the field log of changes if this comes from a subscription - */ -void IOCSource::get(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& valuePrototype, - const GetOperationType getOperationType, - db_field_log* pDbFieldLog) { - // Assumes this is the leaf node in a group, or is a simple db record.field reference - Value value = valuePrototype; // The value that will be returned, if compound then metadata is set here - Value valueTarget = valuePrototype; // The part of value that will be retrieved from the database value field - bool isCompound = false; - if (getOperationType <= FOR_VALUE && value.type() == TypeCode::Any) { - auto type = fromDbrType(dbChannelFinalFieldType(pDbValueChannel)); - if (dbChannelFinalElements(pDbValueChannel) != 1) { - type = type.arrayOf(); + +bool IOCSource::enabled() +{ + /* -1 - disabled + * 0 - lazy init, check environment + * 1 - enabled + */ + static std::atomic ena{}; + + auto e = ena.load(); + if(e==0) { + e = inUnitTest() ? 1 : -1; // default to disabled normally (not unittest) + + auto env_dis = getenv("EPICS_IOC_IGNORE_SERVERS"); + auto env_ena = getenv("PVXS_QSRV_ENABLE"); + + if(env_dis && strstr(env_dis, "qsrv2")) { + e = -1; + + } else if(env_ena && epicsStrCaseCmp(env_ena, "YES")==0) { + e = 1; + + } else if(env_ena && epicsStrCaseCmp(env_ena, "NO")==0) { + e = -1; + + } else if(env_ena) { + // will be seen during initialization, print synchronously + fprintf(stderr, "ERROR: PVXS_QSRV_ENABLE=%s not YES/NO. Defaulting to %s.\n", + env_ena, + e==1 ? "YES" : "NO"); } - value = valueTarget = TypeDef(type).create(); - valuePrototype.from(value); - } else if (auto targetCandidate = value["value"]) { - isCompound = true; - valueTarget = targetCandidate; - } - - // options bit mask LSB to MSB - uint32_t options = 0; - if (isCompound && getOperationType <= FOR_METADATA) { - options = IOC_VALUE_OPTIONS; - } - if (isCompound && (getOperationType == FOR_VALUE_AND_PROPERTIES || - getOperationType == FOR_PROPERTIES)) { - options |= IOC_PROPERTIES_OPTIONS; - } - - if (dbChannelFinalElements(pDbValueChannel) == 1) { - getScalar(pDbValueChannel, pDbPropertiesChannel, value, valueTarget, options, getOperationType, pDbFieldLog); - } else { - getArray(pDbValueChannel, pDbPropertiesChannel, value, valueTarget, options, getOperationType, pDbFieldLog); + printf("INFO: PVXS QSRV2 is loaded and %s\n", + e==1 ? "ENABLED." : "disabled.\n" + " To enable set: epicsEnvSet(\"PVXS_QSRV_ENABLE\",\"YES\")\n" + " and ensure that $EPICS_IOC_IGNORE_SERVERS does not contain \"qsrv2\"."); + ena = e; } + return e==1; } -/** - * Get a scalar value from the database - * @param pDbValueChannel the database channel to get the value from - * @param pDbPropertiesChannel the database channel to get the properties from - * @param value the value to set including metadata if this is a compound value - * @param valueTarget where to store the "value" part of the scalar within value - * @param requestedOptions the options defining what metadata to get - * @param getOperationType for values, for properties or for metadata - * @param pDbFieldLog the field log of changes if this comes from a subscription - */ -void IOCSource::getScalar(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& value, Value& valueTarget, - uint32_t& requestedOptions, const GetOperationType getOperationType, db_field_log* pDbFieldLog) { - ValueBuffer valueBuffer{}; // Enough for metadata and 1 scalar - void* pValueBuffer = &valueBuffer; - long nElements = (getOperationType <= FOR_VALUE) ? 1 : 0; - long actualOptions = requestedOptions; +void IOCSource::initialize(Value& value, const MappingInfo &info, dbChannel *chan) +{ + if(info.type==MappingInfo::Scalar) { + if(auto fld = value["display.form.choices"]) { + shared_array choices({ + "Default", + "String", + "Binary", + "Decimal", + "Hex", + "Exponential", + "Engineering", + }); + fld = choices; - // Get metadata/properties and field value - // Note that metadata will precede the value in the buffer and will be laid out in the options order - DBErrorMessage dbErrorMessage; - if (getOperationType <= FOR_METADATA) { - dbErrorMessage = - dbChannelGet(pDbValueChannel, dbChannelFinalFieldType(pDbValueChannel), pValueBuffer, &actualOptions, - &nElements, - pDbFieldLog); - } else { - dbErrorMessage = - dbChannelGet(pDbPropertiesChannel, dbChannelFinalFieldType(pDbPropertiesChannel), pValueBuffer, - &actualOptions, - &nElements, - pDbFieldLog); - } + if(dbIsValueField(dbChannelFldDes(chan))) { // only apply Q:form to VAL + DBEntry ent(dbChannelRecord(chan)); - if (dbErrorMessage) { - throw std::runtime_error(dbErrorMessage.c_str()); - } - - // Get metadata/properties from buffer if any have been requested - getMetadata(value, pValueBuffer, requestedOptions, actualOptions); - - // Get the value if it has been requested - if (getOperationType <= FOR_VALUE) { - if (dbChannelFinalFieldType(pDbValueChannel) == DBR_ENUM && valueTarget.type() == TypeCode::Struct) { - valueTarget["index"] = *(uint16_t*)(pValueBuffer); - } else { - getValueFromBuffer(valueTarget, pValueBuffer); - } - } -} - -/** - * Get an array value from the database - * - * @param pDbValueChannel the database channel to get the value from - * @param pDbPropertiesChannel the database channel to get the properties from - * @param value the value to set including metadata if this is a compound value - * @param valueTarget where to store the "value" part of the array within value - * @param requestedOptions the options defining what metadata to get - * @param getOperationType for values, for properties or for metadata - * @param pDbFieldLog the field log of changes if this comes from a subscription - */ -void IOCSource::getArray(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& value, Value& valueTarget, - uint32_t& requestedOptions, const GetOperationType getOperationType, db_field_log* pDbFieldLog) { - // value buffer to store the field we will get from the database including metadata. - std::vector valueBuffer; - auto nElements = (getOperationType <= FOR_VALUE) ? (long)dbChannelFinalElements(pDbValueChannel) - : 0; // maximal number of elements - // Initialize the buffer to the maximal size including metadata and zero it out - valueBuffer.resize(nElements * pDbValueChannel->addr.field_size + MAX_METADATA_SIZE, '\0'); - void* pValueBuffer = &valueBuffer[0]; - long actualOptions = requestedOptions; - - // Get the metadata/properties and value into this buffer - // Note that metadata will precede the array value in the buffer and will be laid out in the options order - DBErrorMessage dbErrorMessage; - if (getOperationType <= FOR_METADATA) { - dbErrorMessage = - dbChannelGet(pDbValueChannel, dbChannelFinalFieldType(pDbValueChannel), pValueBuffer, &actualOptions, - &nElements, - pDbFieldLog); - } else { - dbErrorMessage = - dbChannelGet(pDbPropertiesChannel, pDbPropertiesChannel->final_type, pValueBuffer, &actualOptions, - &nElements, - pDbFieldLog); - } - - if (dbErrorMessage) { - throw std::runtime_error(dbErrorMessage.c_str()); - } - - // Get metadata/properties from buffer if any have been requested - getMetadata(value, pValueBuffer, requestedOptions, actualOptions); - - // Get the value array if it has been requested - if (getOperationType <= FOR_VALUE) { - // Get the array value from the updated buffer pointer - // Note: nElements will have been updated with the number of actual elements in the array - if (dbChannelFinalFieldType(pDbValueChannel) == DBR_ENUM && valueTarget.type() == TypeCode::Struct) { - shared_array values(nElements); - for (auto i = 0; i < nElements; i++) { - values[i] = ((uint16_t*)pValueBuffer)[i]; + if(auto tag = ent.info("Q:form")) { + for(auto i : range(choices.size())) { + if(choices[i] == tag) { + value["display.form.index"] = i; + break; + } + } + } } - valueTarget["index"] = values.freeze(); + } + } +} + +static +void getScalarValue(dbChannel* pChannel, + db_field_log *pfl, + Value& value) +{ + long nReq = 1; + union { + double _align; + char _size[MAX_STRING_SIZE]; + char str[MAX_STRING_SIZE]; + } buf; + + DBErrorMessage dbErrorMessage(dbChannelGet(pChannel, dbChannelFinalFieldType(pChannel), + &buf, nullptr, &nReq, pfl)); + if (dbErrorMessage) { + throw std::runtime_error(SB()<>(dbChannelFinalElements(pChannel) * dbChannelFinalFieldSize(pChannel))); + long nReq = dbChannelFinalElements(pChannel); + + DBErrorMessage dbErrorMessage(dbChannelGet(pChannel, final_type, + buf->data(), nullptr, &nReq, pfl)); + if (dbErrorMessage) { + throw std::runtime_error(SB()<size() >= 1u) { + // long string + (*buf)[buf->size()-1u] = '\0'; // paranoia? + value = std::string(buf->data()); + + } else if(final_type == DBR_STRING) { + shared_array arr(nReq); + + for(long n = 0; n < nReq; n++) { + auto sval = &(*buf)[n*MAX_STRING_SIZE]; + auto nlen = strnlen(sval, MAX_STRING_SIZE); + arr[n] = std::string(sval, nlen); + } + + value.from(arr.freeze()); + } else { + std::shared_ptr cbuf(buf, buf->data()); + buf.reset(); // TODO: c++14 adds aliasing ctor by move() + shared_array arr(cbuf, nReq, value.type().arrayType()); + cbuf.reset(); + + value.from(arr.freeze()); + } +} + +// update timeStamp.* and maybe alarm.* +static +void getTimeAlarm(dbChannel* pChannel, + db_field_log *pfl, + Value& node, + const MappingInfo& info, + UpdateType::type change) +{ + long nReq = 0; + long options = DBR_STATUS | DBR_AMSG | DBR_TIME | DBR_UTAG; + struct ValueTimeAlarm { + DBRstatus + DBRamsg + DBRtime + DBRutag + } meta; + + DBErrorMessage dbErrorMessage(dbChannelGet(pChannel, dbChannelFinalFieldType(pChannel), + &meta, &options, &nReq, pfl)); + if (dbErrorMessage) { + throw std::runtime_error(SB()< arr(meta.no_str); + for (epicsUInt32 i = 0; i < meta.no_str; i++) { + arr[i] = meta.strs[i]; + } + choices.from(arr.freeze()); + } + } + if(auto dlL = node["display.limitLow"]) { // if numeric + if(options & DBR_GR_DOUBLE) { + dlL = meta.lower_disp_limit; + node["display.limitHigh"] = meta.upper_disp_limit; + } + if(options & DBR_CTRL_DOUBLE) { + node["control.limitLow"] = meta.lower_ctrl_limit; + node["control.limitHigh"] = meta.upper_ctrl_limit; + } + if(options & DBR_AL_DOUBLE) { + node["valueAlarm.lowAlarmLimit"] = meta.lower_alarm_limit; + node["valueAlarm.lowWarningLimit"] = meta.lower_warning_limit; + node["valueAlarm.highWarningLimit"] = meta.upper_warning_limit; + node["valueAlarm.highAlarmLimit"] = meta.upper_alarm_limit; + } + } + if(true) { // cheating at the moment. DESC is not marked DBE_PROPERTY + if(auto desc = node["display.description"]) + desc = dbChannelRecord(pChannel)->desc; + } +} + +void IOCSource::get(Value& node, // node within top level structure addressed by Field::fieldName + const MappingInfo &info, + const Value& anyType, + UpdateType::type change, + dbChannel *pChannel, // which type of event + db_field_log* pDbFieldLog) +{ + if(info.type==MappingInfo::Proc || info.type==MappingInfo::Structure) + return; + + if(info.type==MappingInfo::Const) { + node.assign(info.cval); + return; + } + + if((change & UpdateType::Property) && info.type==MappingInfo::Scalar) { + getProperties(pChannel, pDbFieldLog, node); + } + + if((info.type==MappingInfo::Scalar || info.type==MappingInfo::Meta) && (change & (UpdateType::Value | UpdateType::Alarm))) { + getTimeAlarm(pChannel, pDbFieldLog, node, info, change); + } + + if((change & UpdateType::Value) && info.type!=MappingInfo::Meta) { + Value value; + if(info.type==MappingInfo::Scalar) { + value = node["value"]; + } else if(info.type==MappingInfo::Any) { + value = anyType.cloneEmpty(); + node.from(value); } else { - getValueFromBuffer(valueTarget, pValueBuffer, nElements); + value = node; } - } -} -/** - * Put a given value to the specified channel. Throw an exception if there are any errors. - * - * @param pDbChannel the channel to put the value into - * @param value the value to put - */ -void IOCSource::put(dbChannel* pDbChannel, const Value& value) { - Value valueSource = value; - // TODO may need to handle Array of and Union as special cases as well - if (value.type() == TypeCode::Any) { - valueSource = valueSource["->"]; - } else if (auto sourceCandidate = value["value"]) { - valueSource = sourceCandidate; - } - - if (dbChannelFinalElements(pDbChannel) == 1) { - putScalar(pDbChannel, valueSource); - } else { - putArray(pDbChannel, valueSource); - } -} - -/** - * Put a given scalar value to the specified channel. Throw an exception if there are any errors. - * - * @param pDbChannel the channel to put the value into - * @param value the scalar value to put - */ -void IOCSource::putScalar(dbChannel* pDbChannel, const Value& value) { - ScalarValueBuffer valueBuffer{}; - auto pValueBuffer = (char*)&valueBuffer; - - if (dbChannelFinalFieldType(pDbChannel) == DBR_ENUM) { - *(uint16_t*)(pValueBuffer) = (value)["index"].as(); - } else { - setValueInBuffer(value, pValueBuffer, pDbChannel); - } - - long status; - if (dbChannelFieldType(pDbChannel) >= DBF_INLINK && dbChannelFieldType(pDbChannel) <= DBF_FWDLINK) { - status = dbChannelPutField(pDbChannel, dbChannelFinalFieldType(pDbChannel), pValueBuffer, 1); - } else { - status = dbChannelPut(pDbChannel, dbChannelFinalFieldType(pDbChannel), pValueBuffer, 1); - } - DBErrorMessage dbErrorMessage(status); - if (dbErrorMessage) { - throw std::runtime_error(dbErrorMessage.c_str()); - } -} - -/** - * Put a given array value to the specified channel. Throw an exception if there are any errors. - * - * @param pDbChannel the channel to put the value into - * @param value the array value to put - */ -void IOCSource::putArray(dbChannel* pDbChannel, const Value& value) { - auto valueArray = value.as>(); - - void* pValueBuffer; - long nElements = (long)valueArray.size(); - std::vector stringValueBuffer; - - if (dbChannelFinalFieldType(pDbChannel) == DBR_STRING) { - stringValueBuffer.resize(MAX_STRING_SIZE * valueArray.size(), '\0'); - char* pCurrent = stringValueBuffer.data(); - auto stringArray = valueArray.castTo(); - for (auto& element: stringArray) { - element.copy(pCurrent, MAX_STRING_SIZE - 1); - pCurrent += MAX_STRING_SIZE; + if(dbChannelFinalElements(pChannel)==1) { + getScalarValue(pChannel, pDbFieldLog, value); + } else { + getArrayValue(pChannel, pDbFieldLog, value); } - pValueBuffer = stringValueBuffer.data(); - } else { - // Set the buffer to the internal value buffer as it's the same for the db records for non-string fields - pValueBuffer = (void*)valueArray.data(); - setValueInBuffer(value, (char*)pValueBuffer, nElements); - } - - long status; - if (dbChannelFieldType(pDbChannel) >= DBF_INLINK && dbChannelFieldType(pDbChannel) <= DBF_FWDLINK) { - status = dbChannelPutField(pDbChannel, dbChannelFinalFieldType(pDbChannel), pValueBuffer, nElements); - } else { - status = dbChannelPut(pDbChannel, dbChannelFinalFieldType(pDbChannel), pValueBuffer, nElements); - } - DBErrorMessage dbErrorMessage(status); - if (dbErrorMessage) { - throw std::runtime_error(dbErrorMessage.c_str()); } } @@ -335,10 +450,12 @@ void IOCSource::doPostProcessing(dbChannel* pDbChannel, TriState forceProcessing dbChannelFinalFieldSize(pDbChannel) < DBR_PUT_ACKT && forceProcessing == Unset)) { if (pDbChannel->addr.precord->pact) { +#if EPICS_VERSION_INT >= VERSION_INT(3, 16, 2, 0) if (dbAccessDebugPUTF && pDbChannel->addr.precord->tpro) { printf("%s: single source onPut to Active '%s', setting RPRO=1\n", epicsThreadGetNameSelf(), pDbChannel->addr.precord->name); } +#endif pDbChannel->addr.precord->rpro = TRUE; } else { pDbChannel->addr.precord->putf = TRUE; @@ -368,133 +485,163 @@ void IOCSource::setForceProcessingFlag(const Value& pvRequest, }); } -/** - * Set a return value from the given database value buffer - * - * @param valueTarget the value to set - * @param pValueBuffer pointer to the database value buffer - */ -void IOCSource::getValueFromBuffer(Value& valueTarget, const void* pValueBuffer) { - auto valueType(valueTarget.type()); - - if (valueType == TypeCode::String) { - valueTarget = ((const char*)pValueBuffer); +static +void doDbPut(dbChannel* pDbChannel, short dbr, const void *pValueBuffer, size_t nElements) +{ + long status; + if (dbChannelFieldType(pDbChannel) >= DBF_INLINK && dbChannelFieldType(pDbChannel) <= DBF_FWDLINK) { + status = dbChannelPutField(pDbChannel, dbr, pValueBuffer, nElements); } else { - SwitchTypeCodeForTemplatedCall(valueType, getValueFromBuffer, (valueTarget, pValueBuffer)); + status = dbChannelPut(pDbChannel, dbr, pValueBuffer, nElements); + } + DBErrorMessage dbErrorMessage(status); + if (dbErrorMessage) { + throw std::runtime_error(dbErrorMessage.c_str()); } } +static +void putScalar(dbChannel* pDbChannel, const Value& value) +{ + union { + epicsUInt8 UCHAR; + epicsUInt16 USHORT; + epicsUInt32 ULONG; + epicsUInt64 UINT64; + epicsInt8 CHAR; + epicsInt16 SHORT; + epicsInt32 LONG; + epicsInt64 INT64; + float FLOAT; + double DOUBLE; + char STRING[MAX_STRING_SIZE]; + } buf; + + switch(dbChannelFinalFieldType(pDbChannel)) { +#define CASE(DBR, CTYPE) case DBR_ ## DBR: buf.DBR = value.as(); break + CASE(CHAR, int8_t); + case DBR_ENUM: + CASE(SHORT, int16_t); + CASE(LONG, int32_t); + CASE(UCHAR, uint8_t); + CASE(USHORT, uint16_t); + CASE(ULONG, uint32_t); +#ifdef DBR_INT64 + CASE(INT64, int64_t); + CASE(UINT64, uint64_t); +#endif + CASE(FLOAT, float); + CASE(DOUBLE, double); +#undef CASE + case DBR_STRING: + { + auto s(value.as()); + strncpy(buf.STRING, s.c_str(), MAX_STRING_SIZE-1u); + buf.STRING[MAX_STRING_SIZE-1u] = '\0'; + break; + } + default: + throw std::logic_error(SB()<<__func__<<" unhandled case "<()); + auto N = str.size()+1u; // include NIL + + doDbPut(pDbChannel, DBR_CHAR, str.c_str(), N); +} + +static +void putStringArray(dbChannel* pDbChannel, const Value& value) +{ + auto arr = value.as>(); + + std::vector buf(MAX_STRING_SIZE * arr.size()); + + char* pCurrent = buf.data(); + for (auto& element: arr) { + element.copy(pCurrent, MAX_STRING_SIZE - 1); + pCurrent += MAX_STRING_SIZE; + } + + doDbPut(pDbChannel, DBR_STRING, buf.data(), arr.size()); +} + +static +void putArray(dbChannel* pDbChannel, const Value& value) +{ + auto arr = value.as>(); + + short dbr; + switch(arr.original_type()) { + case ArrayType::Null: + return; + case ArrayType::Bool: + case ArrayType::Value: + default: + throw std::runtime_error(SB()<<"Unsupported "<<__func__<<" from "< values(nElements); - char stringBuffer[MAX_STRING_SIZE + 1]{ 0 }; // Need to do this because some strings may not be null terminated - for (auto i = 0; i < nElements; i++) { - auto pStringValue = (char*)&((const char*)pValueBuffer)[i * MAX_STRING_SIZE]; - strncpy(stringBuffer, pStringValue, MAX_STRING_SIZE); - values[i] = (char*)&stringBuffer[0]; - } - valueTarget = values.freeze().template castTo(); +void IOCSource::put(dbChannel* pDbChannel, const Value& node, MappingInfo info) { + Value value; + switch(info.type) { + case MappingInfo::Meta: + case MappingInfo::Proc: + case MappingInfo::Structure: + return; // can't write + case MappingInfo::Any: + value = node["->"]; // de-ref into Any + break; + case MappingInfo::Plain: + value = node; + break; + case MappingInfo::Scalar: + value = node["value"]; + if(value.type()==TypeCode::Struct) + value = value["index"]; // NTEnum + break; + case MappingInfo::Const: + value = info.cval; + break; + } + + if (dbChannelFinalElements(pDbChannel) == 1) { + putScalar(pDbChannel, value); + } else if(dbChannelFinalFieldType(pDbChannel) == DBR_CHAR && value.type()==TypeCode::String) { + putLongString(pDbChannel, value); + } else if(dbChannelFinalFieldType(pDbChannel) == DBR_STRING) { + putStringArray(pDbChannel, value); } else { - SwitchTypeCodeForTemplatedCall(valueType, getValueFromBuffer, (valueTarget, pValueBuffer, nElements)); - } -} - -/** - * Set scalar value into given database buffer - * - * @param valueSource the value to put into the buffer - * @param pValueBuffer the database buffer to put it in - * @param pDbChannel the db channel - */ -void IOCSource::setValueInBuffer(const Value& valueSource, char* pValueBuffer, dbChannel* pDbChannel) { - auto valueType(valueSource.type()); - if (valueType == TypeCode::String) { - setStringValueInBuffer(valueSource, pValueBuffer); - } else if (valueType == TypeCode::Any || valueType == TypeCode::Union) { - SwitchTypeCodeForTemplatedCall(fromDbrType(dbChannelFinalFieldType(pDbChannel)), setValueInBuffer, - (valueSource, pValueBuffer)); - } else { - SwitchTypeCodeForTemplatedCall(valueType, setValueInBuffer, (valueSource, pValueBuffer)); - } -} - -/** - * Set an array value in the given buffer - * - * @param valueSource the value to put into the buffer - * @param pValueBuffer the database buffer to put it in - * @param nElements the number of elements to put into the buffer - */ -void IOCSource::setValueInBuffer(const Value& valueSource, char* pValueBuffer, long nElements) { - auto valueType(valueSource.type()); - if (valueType == TypeCode::StringA) { - auto sharedValueArray = valueSource.as>(); - for (auto i = 0u; i < sharedValueArray.size(); i++, pValueBuffer += MAX_STRING_SIZE) { - setStringValueInBuffer(sharedValueArray[i], pValueBuffer); - } - } else { - SwitchTypeCodeForTemplatedCall(valueType, setValueInBuffer, (valueSource, pValueBuffer, nElements)); - } -} - -/** - * Given a string value source, and a buffer, copy the string contents into the buffer up to the MAX_STRING_SIZE. - * Null terminate the string before exiting. - * - * @param valueSource the string value source - * @param pValueBuffer the buffer to copy the string contents to - */ -void IOCSource::setStringValueInBuffer(const Value& valueSource, char* pValueBuffer) { - auto stringValue = valueSource.as(); - auto len = std::min(stringValue.length(), (size_t)MAX_STRING_SIZE - 1); - stringValue.copy(pValueBuffer, len); - pValueBuffer[len] = '\0'; -} - -/** - * Set the value field of the given return value to an array of scalars pointed to by pValueBuffer - * Supported types are: - * TypeCode::Int8 TypeCode::UInt8 - * TypeCode::Int16 TypeCode::UInt16 - * TypeCode::Int32 TypeCode::UInt32 - * TypeCode::Int64 TypeCode::UInt64 - * TypeCode::Float32 TypeCode::Float64 - * - * @tparam valueType the type of the scalars stored in this array. One of the supported types - * @param valueTarget the return value - * @param pValueBuffer the pointer to the data containing the database data to store in the return value - * @param nElements the number of elements in the array - */ -template -void IOCSource::getValueFromBuffer(Value& valueTarget, const void* pValueBuffer, const long& nElements) { - shared_array values(nElements); - for (auto i = 0; i < nElements; i++) { - values[i] = ((valueType*)pValueBuffer)[i]; - } - valueTarget = values.freeze().template castTo(); -} - -/** - * Get the value into the given database value buffer (templated) - * - * @tparam valueType the type of the scalars stored in this array. One of the supported types - * @param valueSource the value to put into the buffer - * @param pValueBuffer the database buffer to put it in - * @param nElements the number of elements to put into the buffer - */ -template -void IOCSource::setValueInBuffer(const Value& valueSource, void* pValueBuffer, long nElements) { - auto valueArray = valueSource.as>(); - for (auto i = 0; i < nElements; i++) { - ((valueType*)pValueBuffer)[i] = valueArray[i]; + putArray(pDbChannel, value); } } @@ -505,177 +652,52 @@ void IOCSource::setValueInBuffer(const Value& valueSource, void* pValueBuffer, l * @param errOnLinks determines whether to throw an error on finding links, default no * @return the TypeCode that the channel is configured for */ -TypeCode IOCSource::getChannelValueType(const dbChannel* pDbChannel, const bool errOnLinks) { - auto dbChannel(pDbChannel); - short dbrType(dbChannelFinalFieldType(dbChannel)); - auto nFinalElements(dbChannelFinalElements(dbChannel)); - auto nElements(dbChannelElements(dbChannel)); +TypeCode IOCSource::getChannelValueType(const dbChannel* chan, const bool errOnLinks) { + /* for links, could check dbChannelFieldType(). + * for long strings, dbChannelCreate() '$' handling overwrites dbAddr::field_type + * (for some reason...) + */ + if(!chan) + throw std::runtime_error("Missing required +channel"); + auto field_type(dbChannelFldDes(chan)->field_type); + auto final_field_type(dbChannelFinalFieldType(chan)); - TypeCode valueType; + if(errOnLinks && field_type >= DBF_INLINK && field_type <= DBF_OUTLINK) + throw std::runtime_error("Link fields not allowed in this context"); - if (dbChannelFieldType(dbChannel) == DBF_STRING && nElements == 1 && dbrType && nFinalElements > 1) { - // single character long DBF_STRING being cast to DBF_CHAR array. - valueType = TypeCode::String; + bool isArray = dbChannelFinalElements(chan)!=1; + bool stringlike = (field_type >= DBF_INLINK && field_type <= DBF_OUTLINK) || field_type == DBF_STRING; - } else { - if (dbrType == DBF_INLINK || dbrType == DBF_OUTLINK || dbrType == DBF_FWDLINK) { - if (errOnLinks) { - throw std::runtime_error("Link fields not allowed in this context"); - } else { - // Handle as chars and fail later - dbrType = DBF_CHAR; - } - } + // string-like field being treated as single char[]. aka. long string + if(stringlike && final_field_type==DBR_CHAR && isArray) + return TypeCode::String; + + TypeCode valueType(fromDbrType(final_field_type)); + + if(isArray) + valueType = valueType.arrayOf(); - valueType = fromDbfType(dbfType(dbrType)); - if (valueType != TypeCode::Null && nFinalElements != 1) { - valueType = valueType.arrayOf(); - } - } return valueType; } -/** - * Get Metadata from the given buffer into the provided value object. The options parameter is used - * to select the metadata to retrieve. It must always be retrieved in the specified order - * as it is laid out that way by the db subsystems on retrieval. - * - * @param value the value object to retrieve the metadata into - * @param pValueBuffer the db value buffer retrieved from the db subsystem - * @param options the options parameter used to select the metadata. - */ -void IOCSource::getMetadata(Value& value, void*& pValueBuffer, const uint32_t& requestedOptions, - const uint32_t& actualOptions) { - if (requestedOptions) { - // Temporary variable to store metadata while retrieving it - Metadata metadata; +static +thread_local server::ExecOp* currentOp; - // Alarm - if (requestedOptions & DBR_STATUS) { - get4MetadataFields(pValueBuffer, uint16_t, - metadata.metadata.status, metadata.metadata.severity, - metadata.metadata.acks, metadata.metadata.ackt); - if (actualOptions & DBR_STATUS) { - checkedSetField(metadata.metadata.status, alarm.status); - checkedSetField(metadata.metadata.severity, alarm.severity); - checkedSetField(metadata.metadata.acks, alarm.acks); - checkedSetField(metadata.metadata.ackt, alarm.ackt); - } - } +CurrentOp::CurrentOp(server::ExecOp *op) + :prev(currentOp) +{ + currentOp = op; +} - // Alarm message - if (requestedOptions & DBR_AMSG) { - getMetadataString(pValueBuffer, metadata.metadata.amsg); - if (actualOptions & DBR_AMSG) { - checkedSetStringField(metadata.metadata.amsg, alarm.message); - } - } +CurrentOp::~CurrentOp() +{ + currentOp = prev; +} - // Units - if (requestedOptions & DBR_UNITS) { - getMetadataBuffer(pValueBuffer, const char, metadata.pUnits, DB_UNITS_SIZE); - if (actualOptions & DBR_UNITS && value["display"]) { - checkedSetStringField(metadata.pUnits, display.units); - } - } - - // Precision - if (requestedOptions & DBR_PRECISION) { - getMetadataBuffer(pValueBuffer, const dbr_precision, metadata.pPrecision, dbr_precision_size); - if (actualOptions & DBR_PRECISION && value["display"]) { - checkedSetField(metadata.pPrecision->precision.dp, display.precision); - } - } - - // Time - if (requestedOptions & DBR_TIME) { - get2MetadataFields(pValueBuffer, uint32_t, metadata.metadata.time.secPastEpoch, - metadata.metadata.time.nsec); - if (actualOptions & DBR_TIME) { - checkedSetField(metadata.metadata.time.secPastEpoch + POSIX_TIME_AT_EPICS_EPOCH, - timeStamp.secondsPastEpoch); - checkedSetField(metadata.metadata.time.nsec, timeStamp.nanoseconds); - } - } - - // User tag - if (requestedOptions & DBR_UTAG) { - getMetadataField(pValueBuffer, uint64_t, metadata.metadata.utag); - if (actualOptions & DBR_UTAG) { - checkedSetField(metadata.metadata.utag, timeStamp.userTag); - } - } - - // Enum strings - if (requestedOptions & DBR_ENUM_STRS) { - getMetadataBuffer(pValueBuffer, const dbr_enumStrs, metadata.enumStrings, dbr_enumStrs_size); - if (actualOptions & DBR_ENUM_STRS && value["value.choices"] && metadata.enumStrings) { - shared_array choices(metadata.enumStrings->no_str); - for (epicsUInt32 i = 0; i < metadata.enumStrings->no_str; i++) { - choices[i] = metadata.enumStrings->strs[i]; - } - value["value.choices"] = choices.freeze().castTo(); - } - } - - // Display long - if (requestedOptions & DBR_GR_LONG) { - getMetadataBuffer(pValueBuffer, const struct dbr_grLong, metadata.graphicsLong, dbr_grLong_size); - if (actualOptions & DBR_GR_LONG && value["display"]) { - checkedSetLongField(metadata.graphicsLong->lower_disp_limit, display.limitLow); - checkedSetLongField(metadata.graphicsLong->upper_disp_limit, display.limitHigh); - } - } - - // Display double - if (requestedOptions & DBR_GR_DOUBLE) { - getMetadataBuffer(pValueBuffer, const struct dbr_grDouble, metadata.graphicsDouble, dbr_grDouble_size); - if (actualOptions & DBR_GR_DOUBLE && value["display"]) { - checkedSetDoubleField(metadata.graphicsDouble->lower_disp_limit, display.limitLow); - checkedSetDoubleField(metadata.graphicsDouble->upper_disp_limit, display.limitHigh); - } - } - - // Control long - if (requestedOptions & DBR_CTRL_LONG) { - getMetadataBuffer(pValueBuffer, const struct dbr_ctrlLong, metadata.controlLong, dbr_ctrlLong_size); - if (actualOptions & DBR_CTRL_LONG && value["control"]) { - checkedSetLongField(metadata.controlLong->lower_ctrl_limit, control.limitLow); - checkedSetLongField(metadata.controlLong->upper_ctrl_limit, control.limitHigh); - } - } - - // Control double - if (requestedOptions & DBR_CTRL_DOUBLE) { - getMetadataBuffer(pValueBuffer, const struct dbr_ctrlDouble, metadata.controlDouble, dbr_ctrlDouble_size); - if (actualOptions & DBR_CTRL_DOUBLE && value["control"]) { - checkedSetDoubleField(metadata.controlDouble->lower_ctrl_limit, control.limitLow); - checkedSetDoubleField(metadata.controlDouble->upper_ctrl_limit, control.limitHigh); - } - } - - // Alarm long - if (requestedOptions & DBR_AL_LONG) { - getMetadataBuffer(pValueBuffer, const struct dbr_alLong, metadata.alarmLong, dbr_alLong_size); - if (actualOptions & DBR_AL_LONG && value["valueAlarm"]) { - checkedSetLongField(metadata.alarmLong->lower_alarm_limit, valueAlarm.lowAlarmLimit); - checkedSetLongField(metadata.alarmLong->lower_warning_limit, valueAlarm.lowWarningLimit); - checkedSetLongField(metadata.alarmLong->upper_warning_limit, valueAlarm.highWarningLimit); - checkedSetLongField(metadata.alarmLong->upper_alarm_limit, valueAlarm.highAlarmLimit); - } - } - - // Alarm double - if (requestedOptions & DBR_AL_DOUBLE) { - getMetadataBuffer(pValueBuffer, const struct dbr_alDouble, metadata.alarmDouble, dbr_alDouble_size); - if (actualOptions & DBR_AL_DOUBLE && value["valueAlarm"]) { - checkedSetDoubleField(metadata.alarmDouble->lower_alarm_limit, valueAlarm.lowAlarmLimit); - checkedSetDoubleField(metadata.alarmDouble->lower_warning_limit, valueAlarm.lowWarningLimit); - checkedSetDoubleField(metadata.alarmDouble->upper_warning_limit, valueAlarm.highWarningLimit); - checkedSetDoubleField(metadata.alarmDouble->upper_alarm_limit, valueAlarm.highAlarmLimit); - } - } - } +server::ExecOp* +CurrentOp::current() +{ + return currentOp; } } // pvxs diff --git a/ioc/iocsource.h b/ioc/iocsource.h index e4a3cc0..1710909 100644 --- a/ioc/iocsource.h +++ b/ioc/iocsource.h @@ -12,127 +12,70 @@ #include +#include + #include "dbeventcontextdeleter.h" -#include "field.h" -#include "metadata.h" +#include "fieldconfig.h" #include "singlesrcsubscriptionctx.h" #include "credentials.h" #include "securitylogger.h" #include "securityclient.h" -struct AllMetadataSize { - DBRstatus - DBRamsg - DBRunits - DBRprecision - DBRtime - DBRutag - DBRenumStrs - DBRgrDouble - DBRctrlDouble - DBRalDouble -}; - -// Maximum amount of space that metadata can use in the buffer returned by dbGetField() -#define MAX_METADATA_SIZE sizeof(AllMetadataSize) +// added in Base 7.0.6 +#ifndef DBRamsg +# define DBRamsg +# define DBRutag +# define DBR_AMSG 0 +# define DBR_UTAG 0 +#endif namespace pvxs { namespace ioc { -// To specify the get desired operation -typedef enum { - FOR_VALUE_AND_PROPERTIES, - FOR_VALUE, - FOR_METADATA, - FOR_PROPERTIES, -} GetOperationType; - -// For alignment, we need this kind of union as the basis for our buffer -typedef union { - double _scalar; - char _string[MAX_STRING_SIZE]; -} ScalarValueBuffer; - -// For this is a structure that is the maximum length for a scalar value with metadata -// DON'T use _max_scalar to reference the value -typedef struct { - double _max_meta[MAX_METADATA_SIZE]; - ScalarValueBuffer _max_scalar; -} ValueBuffer; +namespace UpdateType { +enum type { + Value = DBE_VALUE, + Alarm = DBE_ALARM, + Property = DBE_PROPERTY, + Everything = DBE_VALUE | DBE_ALARM | DBE_PROPERTY, // GET +}; +} class IOCSource { public: - static void get(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& valuePrototype, - GetOperationType getOperationType, - db_field_log* pDbFieldLog); - static void put(dbChannel* pDbChannel, const Value& value); - static void putScalar(dbChannel* pDbChannel, const Value& value); - static void putArray(dbChannel* pDbChannel, const Value& value); + static bool enabled(); + + static void initialize(Value& value, const MappingInfo &info, dbChannel *chan); + + static void get(Value& valuePrototype, + const MappingInfo& info, const Value &anyType, + UpdateType::type change, + dbChannel *pChannel, + db_field_log* pDbFieldLog); + static void put(dbChannel* pDbChannel, const Value& value, MappingInfo info); static void doPostProcessing(dbChannel* pDbChannel, TriState forceProcessing); static void doPreProcessing(dbChannel* pDbChannel, SecurityLogger& securityLogger, const Credentials& credentials, const SecurityClient& securityClient); static void doFieldPreProcessing(const SecurityClient& securityClient); - ////////////////////////////// - // Get & Subscription - ////////////////////////////// - // Get a return value from the given database value buffer - static void getValueFromBuffer(Value& valueTarget, const void* pValueBuffer); - // Get a return value from the given database value buffer - static void getValueFromBuffer(Value& valueTarget, const void* pValueBuffer, const long& nElements); -/** - * Set the value field of the given return value to a scalar pointed to by pValueBuffer - * Supported types are: - * TypeCode::Int8 TypeCode::UInt8 - * TypeCode::Int16 TypeCode::UInt16 - * TypeCode::Int32 TypeCode::UInt32 - * TypeCode::Int64 TypeCode::UInt64 - * TypeCode::Float32 TypeCode::Float64 - * - * @tparam valueType the type of the scalar stored in the buffer. One of the supported types - * @param valueTarget the return value - * @param pValueBuffer the pointer to the data containing the database data to store in the return value - */ - template static void getValueFromBuffer(Value& valueTarget, const void* pValueBuffer) { - valueTarget = ((valueType*)pValueBuffer)[0]; - } - - // Get a return value from the given database value buffer (templated) - template - static void getValueFromBuffer(Value& valueTarget, const void* pValueBuffer, const long& nElements); - - ////////////////////////////// - // Set - ////////////////////////////// - // Set the value into the given database value buffer - static void setValueInBuffer(const Value& valueSource, char* pValueBuffer, dbChannel* pDbChannel); - // Set the value into the given database value buffer - static void setValueInBuffer(const Value& valueSource, char* pValueBuffer, long nElements); - // Set string value in the given buffer - static void setStringValueInBuffer(const Value& valueSource, char* pValueBuffer); -// Set the value into the given database value buffer (templated) - template static void setValueInBuffer(const Value& valueSource, void* pValueBuffer) { - ((valueType*)pValueBuffer)[0] = valueSource.as(); - } - // Set the value into the given database value buffer (templated) - template - static void setValueInBuffer(const Value& valueSource, void* pValueBuffer, long nElements); - ////////////////////////////// // Common Utils ////////////////////////////// // Utility function to get the TypeCode that the given database channel is configured for static TypeCode getChannelValueType(const dbChannel* pDbChannel, bool errOnLinks = false); - static void getScalar(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& value, Value& valueTarget, - uint32_t& requestedOptions, GetOperationType getOperationType, db_field_log* pDbFieldLog); - static void getArray(dbChannel* pDbValueChannel, dbChannel* pDbPropertiesChannel, Value& value, Value& valueTarget, - uint32_t& requestedOptions, GetOperationType getOperationType, db_field_log* pDbFieldLog); - static void getMetadata(Value& valuePrototype, void*& pValueBuffer, const uint32_t& requestedOptions, - const uint32_t& actualOptions); static void setForceProcessingFlag(const Value& pvRequest, const std::shared_ptr& securityControlObject); }; +struct CurrentOp { + explicit CurrentOp(server::ExecOp *op); + ~CurrentOp(); + static + server::ExecOp *current(); +private: + server::ExecOp *prev; +}; + } // pvxs } // ioc diff --git a/ioc/localfieldlog.cpp b/ioc/localfieldlog.cpp index 41e6ae2..7b2280c 100644 --- a/ioc/localfieldlog.cpp +++ b/ioc/localfieldlog.cpp @@ -14,7 +14,7 @@ namespace ioc { LocalFieldLog::LocalFieldLog(dbChannel* pDbChannel, db_field_log* existingFieldLog) :pFieldLog(existingFieldLog) { - if (!pFieldLog && (ellCount(&pDbChannel->pre_chain) != 0 || ellCount(&pDbChannel->pre_chain) == 0)) { + if (pDbChannel && !pFieldLog && (ellCount(&pDbChannel->pre_chain) != 0 || ellCount(&pDbChannel->pre_chain) == 0)) { pFieldLog = db_create_read_log(pDbChannel); if (pFieldLog) { pFieldLog = dbChannelRunPreChain(pDbChannel, pFieldLog); diff --git a/ioc/metadata.h b/ioc/metadata.h deleted file mode 100644 index 6d182fe..0000000 --- a/ioc/metadata.h +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright - See the COPYRIGHT that is included with this distribution. - * pvxs is distributed subject to a Software License Agreement found - * in file LICENSE that is included with this distribution. - * - * Author George S. McIntyre , 2023 - * - */ - -#ifndef PVXS_METADATA_H -#define PVXS_METADATA_H - -#include -#include - -#include -#include - -// Options when retrieving values: value, time and alarm only -#define IOC_VALUE_OPTIONS ( \ - DBR_TIME | \ - DBR_STATUS | \ - DBR_AMSG | \ - DBR_UTAG ) - -// Options when retrieving properties: -#define IOC_PROPERTIES_OPTIONS ( \ - DBR_UNITS | \ - DBR_PRECISION | \ - DBR_TIME | \ - DBR_UTAG | \ - DBR_ENUM_STRS| \ - DBR_GR_DOUBLE | \ - DBR_CTRL_DOUBLE | \ - DBR_AL_DOUBLE) - -#define getMetadataField(_buffer, _type, _field1) getMetadataFieldsEnclosure(_buffer, _type, metadataFieldGetter(_field1) ) -#define get2MetadataFields(_buffer, _type, _field1, _field2) getMetadataFieldsEnclosure(_buffer, _type, metadataFieldGetter(_field1) metadataFieldGetter(_field2) ) -#define get4MetadataFields(_buffer, _type, _field1, _field2, _field3, _field4) getMetadataFieldsEnclosure(_buffer, _type, metadataFieldGetter(_field1) metadataFieldGetter(_field2) metadataFieldGetter(_field3) metadataFieldGetter(_field4)) -#define getMetadataFieldsEnclosure(_buffer, _type, _getters) { \ - auto* __pBuffer = (_type*)pValueBuffer; \ - _getters \ - (_buffer) = (void*)__pBuffer; \ -} - -#define metadataFieldGetter(_field) (_field) = *__pBuffer++; - -#define getMetadataBuffer(_buffer, _type, _field, _size) { \ - (_field) = (_type*)(_buffer); \ - (_buffer) = ((void*)&((const char*)(_buffer))[_size]); \ -} - -#define getMetadataString(_buffer, _field) { \ - strcpy(_field, (const char*)(_buffer)); \ - (_buffer) = (void*)((const char*)(_buffer) + sizeof(_field)); \ -} - -#define checkedSetField(_lvalue, _rvalue) \ -if (auto&& __field = value[#_rvalue] ) { \ - __field = _lvalue; \ -} - -#define checkedSetDoubleField(_lvalue, _rvalue) \ -if (auto&& __field = value[#_rvalue] ) { \ - if ( !std::isnan(_lvalue)) { \ - __field = _lvalue; \ - } \ -} - -#define checkedSetLongField(_lvalue, _rvalue) \ -if (auto&& __field = value[#_rvalue] ) { \ - __field = _lvalue; \ -} - -#define checkedSetStringField(_lvalue, _rvalue) \ -if (auto&& __field = value[#_rvalue] ) { \ - if ( strlen(_lvalue)) { \ - __field = _lvalue; \ - } \ -} - -namespace pvxs { -namespace ioc { - -struct CommonMetadata { - DBRstatus - DBRamsg - DBRtime - DBRutag - - enum { - mask = DBR_STATUS | DBR_AMSG | DBR_TIME | DBR_UTAG - }; -}; - -/** - * structure to store metadata - */ -struct Metadata { - CommonMetadata metadata{}; - const char* pUnits{}; - const dbr_precision* pPrecision{}; - const dbr_enumStrs* enumStrings{}; - const struct dbr_grDouble* graphicsDouble{}; - const struct dbr_grLong* graphicsLong{}; - const struct dbr_ctrlDouble* controlDouble{}; - const struct dbr_ctrlLong* controlLong{}; - const struct dbr_alDouble* alarmDouble{}; - const struct dbr_alLong* alarmLong{}; -}; - -} // ioc -} // pvxs - -#endif //PVXS_METADATA_H diff --git a/ioc/pvxs/iochooks.h b/ioc/pvxs/iochooks.h index f2dc25d..01d8db3 100644 --- a/ioc/pvxs/iochooks.h +++ b/ioc/pvxs/iochooks.h @@ -54,7 +54,7 @@ namespace ioc { * return; * * server::SharedPV mypv(...); - * ioc::iocServer() + * ioc::server() * .addPV("my:pv:name", mypv); * } * static void myregistrar() { @@ -66,7 +66,7 @@ namespace ioc { * @endcode */ PVXS_IOC_API -server::Server& server(); +server::Server server(); /** * Load JSON group definition file. @@ -74,12 +74,21 @@ server::Server& server(); * at the appropriate time in the startup process. * * @param jsonFilename the json file containing the group definitions + * @param macros NULL, or a comma separated list of macro definitions. eg. "KEY=VAL,OTHER=SECOND" * @return 0 for success, 1 for failure * @since UNRELEASED */ PVXS_IOC_API -long dbLoadGroup(const char* jsonFilename); +long dbLoadGroup(const char* jsonFilename, const char* macros=nullptr); -} -} // namespace pvxs::ioc +/** Call just before testIocShutdownOk() + * + * Shutdown QSRV. Only needed with Base <= 7.0.4 . + * Since 7.0.4, QSRV shutdown occurs during testIocShutdownOk() . + * @since UNRELEASED + */ +PVXS_IOC_API +void testShutdown(); + +}} // namespace pvxs::ioc #endif // PVXS_IOCHOOKS_H diff --git a/ioc/pvxsIoc.dbd b/ioc/pvxsIoc.dbd index 4705874..6330d4a 100644 --- a/ioc/pvxsIoc.dbd +++ b/ioc/pvxsIoc.dbd @@ -3,7 +3,7 @@ registrar(pvxsSingleSourceRegistrar) registrar(pvxsGroupSourceRegistrar) # from demo.cpp -device(waveform, CONSTANT, devWfPDBDemo, "QSRV Demo") -device(longin, CONSTANT, devLoPDBUTag, "QSRV Set UTag") +device(waveform, CONSTANT, devWfPDBQ2Demo, "QSRV2 Demo") +device(longin, CONSTANT, devLoPDBQ2UTag, "QSRV2 Set UTag") # from imagedemo.c -function(QSRV_image_demo) +function(QSRV2_image_demo) diff --git a/ioc/singlesource.cpp b/ioc/singlesource.cpp index fcf255c..ef66177 100644 --- a/ioc/singlesource.cpp +++ b/ioc/singlesource.cpp @@ -39,6 +39,309 @@ namespace ioc { DEFINE_LOGGER(_logname, "pvxs.ioc.single.source"); +namespace { + +void subscriptionCallback(SingleSourceSubscriptionCtx* subscriptionContext, + UpdateType::type change, + dbChannel* pChannel, + struct db_field_log* pDbFieldLog) { + try { + // Get the current value of this subscription + // We simply merge new field changes onto this value as events occur + auto currentValue = subscriptionContext->currentValue; + + { + DBLocker F(dbChannelRecord(subscriptionContext->info->chan)); + // TODO MappingInfo::nsecMask + IOCSource::get(currentValue, MappingInfo(), Value(), change, pChannel, pDbFieldLog); + } + + // Make sure that the initial subscription update has occurred on both channels before continuing + // As we make two initial updates when opening a new subscription, we need both to have completed before continuing + if (subscriptionContext->hadValueEvent && subscriptionContext->hadPropertyEvent) { + // Return value + subscriptionContext->subscriptionControl->post(currentValue.clone()); + currentValue.unmark(); + } + } catch(std::exception& e) { + log_exc_printf(_logname, "Unhandled exception in %s\n", __func__); + } +} + +static +void subscriptionValueCallback(void* userArg, struct dbChannel* pChannel, int, struct db_field_log* pDbFieldLog) { + auto subscriptionContext = (SingleSourceSubscriptionCtx*)userArg; + subscriptionContext->hadValueEvent = true; + UpdateType::type change = UpdateType::type(UpdateType::Value | UpdateType::Alarm); +#if EPICS_VERSION_INT >= VERSION_INT(7, 0, 6, 0) + if(pDbFieldLog) { + // when available, use DBE mask from db_field_log + change = UpdateType::type(pDbFieldLog->mask & UpdateType::Everything); + } +#endif + subscriptionCallback(subscriptionContext, change, pChannel, pDbFieldLog); +} + +static +void subscriptionPropertiesCallback(void* userArg, struct dbChannel* pChannel, int, + struct db_field_log* pDbFieldLog) { + auto subscriptionContext = (SingleSourceSubscriptionCtx*)userArg; + subscriptionContext->hadPropertyEvent = true; + subscriptionCallback(subscriptionContext, UpdateType::Property, pChannel, pDbFieldLog); +} + +/** + * Called by the framework when a client subscribes to a channel. We intercept the call before this function is called + * to add a new subscription context with a value prototype matching the channel definition. + * + * @param subscriptionContext a new subscription context with a value prototype matching the channel + * @param subscriptionOperation the channel subscription operation + */ +void onSubscribe(const std::shared_ptr& subscriptionContext, + const DBEventContext& eventContext, + std::unique_ptr&& subscriptionOperation) +{ + // inform peer of data type and acquire control of the subscription queue + subscriptionContext->subscriptionControl = subscriptionOperation->connect(subscriptionContext->currentValue); + + IOCSource::initialize(subscriptionContext->currentValue, + *subscriptionContext->info, + subscriptionContext->info->chan); + + // Two subscription are made for pvxs + // first subscription is for Value changes + subscriptionContext->pValueEventSubscription.subscribe(eventContext.get(), + subscriptionContext->info->chan, + subscriptionValueCallback, + subscriptionContext.get(), + DBE_VALUE | DBE_ALARM | DBE_ARCHIVE + ); + // second subscription is for Property changes + subscriptionContext->pPropertiesEventSubscription.subscribe(eventContext.get(), + subscriptionContext->pPropertiesChannel, + subscriptionPropertiesCallback, + subscriptionContext.get(), + DBE_PROPERTY + ); + + // If all goes well, Set up handlers for start and stop monitoring events + // The subscription context is being kept alive because it is being bound into some internal storage by onStart + subscriptionContext->subscriptionControl->onStart([subscriptionContext](bool isStarting) { + if (isStarting) { + subscriptionContext->eventsEnabled = true; + subscriptionContext->pValueEventSubscription.enable(); + subscriptionContext->pPropertiesEventSubscription.enable(); + } else { + subscriptionContext->pValueEventSubscription.disable(); + subscriptionContext->pPropertiesEventSubscription.disable(); + subscriptionContext->eventsEnabled = false; + } + }); +} +/** + * Create a Value Prototype for storing values returned by the given channel. + * + * @param dbChannelSharedPtr pointer to the channel + * @return a value prototype for the given channel + */ +Value getValuePrototype(const std::shared_ptr& sinfo) { + dbChannel* chan(sinfo->chan); + short dbrType(dbChannelFinalFieldType(chan)); + auto valueType(IOCSource::getChannelValueType(chan)); + + Value valuePrototype; + // To control optional metadata set to true to include in the output + bool display = true; + bool control = true; + bool valueAlarm = true; + + if (dbrType == DBR_ENUM) { + valuePrototype = nt::NTEnum{}.create(); + } else { + valuePrototype = nt::NTScalar{ valueType, display, control, valueAlarm, true }.create(); + } + return valuePrototype; +} + +/** + * Callback for asynchronous put operations to handle the actual put value operation + * + * @param notify the process notify object to use + * @param type the put notification type + * @return 1 for success and 0 for errors + */ +int putCallback(struct processNotify* notify, notifyPutType type) { + if (notify->status != notifyOK) { + return 0; + } + + auto pPutOperationCache = (PutOperationCache*)notify->usrPvt; + auto valueToSet = std::move(pPutOperationCache->valueToSet); + + switch (type) { + case putDisabledType: + // Request has been made but the record has been disabled, so noop and only call done callback + return 0; + case putFieldType: + // As this type will be only called for Links the IOCSource::put() will handle the locking as a special case + case putType: + // For this type, the caller has already locked the record, so we'll not lock either + IOCSource::put(pPutOperationCache->notify.chan, valueToSet, MappingInfo()); // put + break; + } + return 1; +} + +/** + * Callback when asynchronous put's are complete + * + * @param notify the process notify object to use + */ +void doneCallback(struct processNotify* notify) { + // Get our put operation cache object from the user pointer + auto pPutOperationCache = (PutOperationCache*)notify->usrPvt; + + // Get the cached putOperation controller + auto putOperation = std::move(pPutOperationCache->putOperation); + + // TODO handle cancelled requests +// int expected = 1; +// if (std::atomic_compare_exchange_weak(&pPutOperationCache->notifyBusy, &expected, 0) == 0) { +// std::cerr << "SinglePut dbNotify state error?\n"; +// } + + switch (notify->status) { + case notifyOK: + // If everything is ok then notify the caller + putOperation->reply(); + break; + case notifyCanceled: + return; // skip notification + case notifyError: + putOperation->error("Error in dbNotify"); + break; + case notifyPutDisabled: + putOperation->error("Put disabled"); + break; + } +} + +/** + * Handle the get operation + * + * @param pDbChannel the channel that the request comes in on + * @param getOperation the current executing operation + * @param valuePrototype a value prototype that is made based on the expected type to be returned + */ +void singleGet(const SingleInfo& info, + std::unique_ptr& getOperation, + const Value& valuePrototype) { + dbChannel* pDbChannel(info.chan); + try { + auto returnValue = valuePrototype.cloneEmpty(); + // TODO: MappingInfo::nsecMask + IOCSource::initialize(returnValue, info, pDbChannel); + { + DBLocker F(pDbChannel->addr.precord); // lock + LocalFieldLog localFieldLog(pDbChannel); + IOCSource::get(returnValue, info, + Value(), UpdateType::Everything, + pDbChannel, localFieldLog.pFieldLog); + } + getOperation->reply(returnValue); + } catch (const std::exception& getException) { + getOperation->error(getException.what()); + } +} + +/** + * Handler for the onOp event raised by pvxs Sources when they are started, in order to define the get and put handlers + * on a per source basis. + * This is called after the event has been intercepted and we add the channel and value prototype to the call. + * + * @param dbChannelSharedPtr the channel to which the get/put operation pertains + * @param valuePrototype the value prototype that is appropriate for the given channel + * @param channelConnectOperation the channel connect operation object + */ +void onOp(const std::shared_ptr& sInfo, const Value& valuePrototype, + std::unique_ptr&& channelConnectOperation) { + // Announce the channel type with a `connect()` call. This happens only once + channelConnectOperation->connect(valuePrototype); + + // Set up handler for get requests + channelConnectOperation + ->onGet([sInfo, valuePrototype](std::unique_ptr&& getOperation) { + singleGet(*sInfo, getOperation, valuePrototype); + }); + + // Make a security cache for this client's connection to this pv + // Each time the same client calls put we will re-use the cached security client + // The security cache will be deleted when the client disconnects from this pv + auto putOperationCache = std::make_shared(); + + // Set up handler for put requests + channelConnectOperation + ->onPut([sInfo, putOperationCache]( + std::unique_ptr&& putOperation, + Value&& value) { + try { + dbChannel* pDbChannel = sInfo->chan; + if (!putOperationCache->done) { + putOperationCache->credentials.reset(new Credentials(*putOperation->credentials())); + putOperationCache->securityClient.update(pDbChannel, *putOperationCache->credentials); + putOperationCache->notify.usrPvt = putOperationCache.get(); + putOperationCache->notify.chan = pDbChannel; + putOperationCache->notify.putCallback = putCallback; + putOperationCache->notify.doneCallback = doneCallback; + + auto& pvRequest = putOperation->pvRequest(); + pvRequest["record._options.block"].as(putOperationCache->doWait); + IOCSource::setForceProcessingFlag(pvRequest, putOperationCache); + if (putOperationCache->forceProcessing) { + putOperationCache->doWait = false; // no point in waiting + } + putOperationCache->done = true; + } + + SecurityLogger securityLogger; + + IOCSource::doPreProcessing(pDbChannel, + securityLogger, + *putOperationCache->credentials, + putOperationCache->securityClient); // pre-process + IOCSource::doFieldPreProcessing(putOperationCache->securityClient); // pre-process field + if (putOperationCache->doWait) { + putOperationCache->valueToSet = value; + // TODO prevent concurrent put with callbacks (notifyBusy) + + putOperationCache->notify.requestType = value["value"].isMarked() ? putProcessRequest + : processRequest; + putOperationCache->putOperation = std::move(putOperation); + dbProcessNotify(&putOperationCache->notify); + return; + } + + CurrentOp op(putOperation.get()); + + if (dbChannelFieldType(pDbChannel) >= DBF_INLINK + && dbChannelFieldType(pDbChannel) <= DBF_FWDLINK) { + // Locking is handled by dbPutField() called as a special case in IOCSource::put() for links + IOCSource::put(pDbChannel, value, MappingInfo()); // put + } else { + // All other field types call dbChannelPut() directly, so we have to perform locking here + DBLocker F(pDbChannel->addr.precord); // lock + IOCSource::put(pDbChannel, value, MappingInfo()); // put + IOCSource::doPostProcessing(pDbChannel, putOperationCache->forceProcessing); // post-process + } + putOperation->reply(); + } catch (std::exception& e) { + putOperation->error(e.what()); + } + }); +} + +} // namespace + /** * Constructor for SingleSource registrar. */ @@ -75,25 +378,37 @@ SingleSource::SingleSource() */ void SingleSource::onCreate(std::unique_ptr&& channelControl) { auto sourceName(channelControl->name().c_str()); - dbChannel* pDbChannel = dbChannelCreate(sourceName); - if (!pDbChannel) { - log_debug_printf(_logname, "Ignore requested source '%s'\n", sourceName); + Channel pDbChannel; + try { + pDbChannel = Channel(sourceName); + } catch (std::exception& e) { + log_debug_printf(_logname, "Ignore requested channel '%s' : %s\n", sourceName, e.what()); return; } + log_debug_printf(_logname, "Accepting channel for '%s'\n", sourceName); - // Set up a shared pointer to the database channel and provide a deleter lambda for when it will eventually be deleted - std::shared_ptr dbChannelSharedPtr(pDbChannel, [](dbChannel* ch) { dbChannelDelete(ch); }); - - DBErrorMessage dbErrorMessage(dbChannelOpen(dbChannelSharedPtr.get())); - if (dbErrorMessage) { - log_debug_printf(_logname, "Error opening database channel for '%s: %s'\n", sourceName, - dbErrorMessage.c_str()); - throw std::runtime_error(dbErrorMessage.c_str()); - } + auto sInfo(std::make_shared(std::move(pDbChannel))); // Create callbacks for handling requests and channel subscriptions - createRequestAndSubscriptionHandlers(std::move(channelControl), dbChannelSharedPtr); + Value valuePrototype = getValuePrototype(sInfo); + + // Get and Put requests + channelControl + ->onOp([sInfo, valuePrototype](std::unique_ptr&& channelConnectOperation) { + onOp(sInfo, valuePrototype, std::move(channelConnectOperation)); + }); + + // binding 'this' safe as Server shutdown will close connections before dropping Source + channelControl + ->onSubscribe([this, valuePrototype, sInfo]( + std::unique_ptr&& subscriptionOperation) { + // The subscription must be kept alive + // We accomplish this further on during the binding of the onStart() + auto subscriptionContext(std::make_shared(sInfo)); + subscriptionContext->currentValue = valuePrototype.cloneEmpty(); + onSubscribe(subscriptionContext, eventContext, std::move(subscriptionOperation)); + }); } /** @@ -122,312 +437,5 @@ void SingleSource::show(std::ostream& outputStream) { } } -/** - * Create request and subscription handlers for single record sources - * - * @param channelControl the control channel pointer that we got from onCreate - * @param dbChannelSharedPtr the pointer to the database channel to set up the handlers for - */ -void SingleSource::createRequestAndSubscriptionHandlers(std::unique_ptr&& channelControl, - const std::shared_ptr& dbChannelSharedPtr) { - - Value valuePrototype = getValuePrototype(dbChannelSharedPtr); - - // Get and Put requests - channelControl - ->onOp([dbChannelSharedPtr, valuePrototype](std::unique_ptr&& channelConnectOperation) { - onOp(dbChannelSharedPtr, valuePrototype, std::move(channelConnectOperation)); - }); - - // Subscription requests - channelControl - ->onSubscribe([this, valuePrototype, dbChannelSharedPtr]( - std::unique_ptr&& subscriptionOperation) { - // The subscription must be kept alive - // We accomplish this further on during the binding of the onStart() - auto subscriptionContext(std::make_shared(dbChannelSharedPtr)); - subscriptionContext->currentValue = valuePrototype; - onSubscribe(subscriptionContext, std::move(subscriptionOperation)); - }); -} - -/** - * Create a Value Prototype for storing values returned by the given channel. - * - * @param dbChannelSharedPtr pointer to the channel - * @return a value prototype for the given channel - */ -Value SingleSource::getValuePrototype(const std::shared_ptr& dbChannelSharedPtr) { - auto dbChannel(dbChannelSharedPtr.get()); - short dbrType(dbChannelFinalFieldType(dbChannel)); - auto valueType(IOCSource::getChannelValueType(dbChannelSharedPtr.get())); - - Value valuePrototype; - // To control optional metadata set to true to include in the output - bool display = true; - bool control = true; - bool valueAlarm = true; - - if (dbrType == DBR_ENUM) { - valuePrototype = nt::NTEnum{}.create(); - } else { - valuePrototype = nt::NTScalar{ valueType, display, control, valueAlarm }.create(); - } - return valuePrototype; -} - -/** - * Handle the get operation - * - * @param pDbChannel the channel that the request comes in on - * @param getOperation the current executing operation - * @param valuePrototype a value prototype that is made based on the expected type to be returned - */ -void SingleSource::get(dbChannel* pDbChannel, std::unique_ptr& getOperation, - const Value& valuePrototype) { - try { - auto returnValue = valuePrototype.cloneEmpty(); - { - DBLocker F(pDbChannel->addr.precord); // lock - LocalFieldLog localFieldLog(pDbChannel); - IOCSource::get(pDbChannel, nullptr, returnValue, FOR_VALUE_AND_PROPERTIES, localFieldLog.pFieldLog); - } - getOperation->reply(returnValue); - } catch (const std::exception& getException) { - getOperation->error(getException.what()); - } -} - -/** - * Handler for the onOp event raised by pvxs Sources when they are started, in order to define the get and put handlers - * on a per source basis. - * This is called after the event has been intercepted and we add the channel and value prototype to the call. - * - * @param dbChannelSharedPtr the channel to which the get/put operation pertains - * @param valuePrototype the value prototype that is appropriate for the given channel - * @param channelConnectOperation the channel connect operation object - */ -void SingleSource::onOp(const std::shared_ptr& dbChannelSharedPtr, const Value& valuePrototype, - std::unique_ptr&& channelConnectOperation) { - // Announce the channel type with a `connect()` call. This happens only once - channelConnectOperation->connect(valuePrototype); - - // Set up handler for get requests - channelConnectOperation - ->onGet([dbChannelSharedPtr, valuePrototype](std::unique_ptr&& getOperation) { - get(dbChannelSharedPtr.get(), getOperation, valuePrototype); - }); - - // Make a security cache for this client's connection to this pv - // Each time the same client calls put we will re-use the cached security client - // The security cache will be deleted when the client disconnects from this pv - auto putOperationCache = std::make_shared(); - - // Set up handler for put requests - channelConnectOperation - ->onPut([dbChannelSharedPtr, valuePrototype, putOperationCache]( - std::unique_ptr&& putOperation, - Value&& value) { - try { - auto pDbChannel = dbChannelSharedPtr.get(); - if (!putOperationCache->done) { - putOperationCache->credentials.reset(new Credentials(*putOperation->credentials())); - putOperationCache->securityClient.update(pDbChannel, *putOperationCache->credentials); - putOperationCache->notify.usrPvt = putOperationCache.get(); - putOperationCache->notify.chan = pDbChannel; - putOperationCache->notify.putCallback = putCallback; - putOperationCache->notify.doneCallback = doneCallback; - - auto& pvRequest = putOperation->pvRequest(); - pvRequest["record._options.block"].as(putOperationCache->doWait); - IOCSource::setForceProcessingFlag(pvRequest, putOperationCache); - if (putOperationCache->forceProcessing) { - putOperationCache->doWait = false; // no point in waiting - } - putOperationCache->done = true; - } - - SecurityLogger securityLogger; - - IOCSource::doPreProcessing(pDbChannel, - securityLogger, - *putOperationCache->credentials, - putOperationCache->securityClient); // pre-process - IOCSource::doFieldPreProcessing(putOperationCache->securityClient); // pre-process field - if (putOperationCache->doWait) { - putOperationCache->valueToSet = value; - // TODO prevent concurrent put with callbacks (notifyBusy) - - putOperationCache->notify.requestType = value["value"].isMarked() ? putProcessRequest - : processRequest; - putOperationCache->putOperation = std::move(putOperation); - dbProcessNotify(&putOperationCache->notify); - return; - } else if (dbChannelFieldType(pDbChannel) >= DBF_INLINK - && dbChannelFieldType(pDbChannel) <= DBF_FWDLINK) { - // Locking is handled by dbPutField() called as a special case in IOCSource::put() for links - IOCSource::put(pDbChannel, value); // put - } else { - // All other field types call dbChannelPut() directly, so we have to perform locking here - DBLocker F(pDbChannel->addr.precord); // lock - IOCSource::put(pDbChannel, value); // put - IOCSource::doPostProcessing(pDbChannel, putOperationCache->forceProcessing); // post-process - } - putOperation->reply(); - } catch (std::exception& e) { - putOperation->error(e.what()); - } - }); -} -/** - * Callback for asynchronous put operations to handle the actual put value operation - * - * @param notify the process notify object to use - * @param type the put notification type - * @return 1 for success and 0 for errors - */ -int SingleSource::putCallback(struct processNotify* notify, notifyPutType type) { - if (notify->status != notifyOK) { - return 0; - } - - auto pPutOperationCache = (PutOperationCache*)notify->usrPvt; - auto valueToSet = std::move(pPutOperationCache->valueToSet); - - switch (type) { - case putDisabledType: - // Request has been made but the record has been disabled, so noop and only call done callback - return 0; - case putFieldType: - // As this type will be only called for Links the IOCSource::put() will handle the locking as a special case - case putType: - // For this type, the caller has already locked the record, so we'll not lock either - IOCSource::put(pPutOperationCache->notify.chan, valueToSet); // put - break; - } - return 1; -} - -/** - * Callback when asynchronous put's are complete - * - * @param notify the process notify object to use - */ -void SingleSource::doneCallback(struct processNotify* notify) { - // Get our put operation cache object from the user pointer - auto pPutOperationCache = (PutOperationCache*)notify->usrPvt; - - // Get the cached putOperation controller - auto putOperation = std::move(pPutOperationCache->putOperation); - - // TODO handle cancelled requests -// int expected = 1; -// if (std::atomic_compare_exchange_weak(&pPutOperationCache->notifyBusy, &expected, 0) == 0) { -// std::cerr << "SinglePut dbNotify state error?\n"; -// } - - switch (notify->status) { - case notifyOK: - // If everything is ok then notify the caller - putOperation->reply(); - break; - case notifyCanceled: - return; // skip notification - case notifyError: - putOperation->error("Error in dbNotify"); - break; - case notifyPutDisabled: - putOperation->error("Put disabled"); - break; - } -} - -/** - * Called by the framework when the monitoring client issues a start or stop subscription - * - * @param subscriptionContext the subscription context - * @param isStarting true if the client issued a start subscription request, false otherwise - */ -void SingleSource::onStart(const std::shared_ptr& subscriptionContext, bool isStarting) { - if (isStarting) { - onStartSubscription(subscriptionContext); - } else { - onDisableSubscription(subscriptionContext); - } -} - -/** - * Called when a client starts a subscription it has subscribed to - * - * @param subscriptionContext the subscription context - */ -void SingleSource::onStartSubscription(const std::shared_ptr& subscriptionContext) { - db_event_enable(subscriptionContext->pValueEventSubscription.get()); - db_event_enable(subscriptionContext->pPropertiesEventSubscription.get()); - db_post_single_event(subscriptionContext->pValueEventSubscription.get()); - db_post_single_event(subscriptionContext->pPropertiesEventSubscription.get()); -} - -/** - * Called by the framework when a client subscribes to a channel. We intercept the call before this function is called - * to add a new subscription context with a value prototype matching the channel definition. - * - * @param subscriptionContext a new subscription context with a value prototype matching the channel - * @param subscriptionOperation the channel subscription operation - */ -void SingleSource::onSubscribe(const std::shared_ptr& subscriptionContext, - std::unique_ptr&& subscriptionOperation) const { - // inform peer of data type and acquire control of the subscription queue - subscriptionContext->subscriptionControl = subscriptionOperation->connect(subscriptionContext->currentValue); - - // Two subscription are made for pvxs - // first subscription is for Value changes - addSubscriptionEvent(Value, eventContext, subscriptionContext, DBE_VALUE | DBE_ALARM | DBE_ARCHIVE); - // second subscription is for Property changes - addSubscriptionEvent(Properties, eventContext, subscriptionContext, DBE_PROPERTY); - - // If either fail to complete then raise an error (removes last ref to shared_ptr subscriptionContext) - if (!subscriptionContext->pValueEventSubscription - || !subscriptionContext->pPropertiesEventSubscription) { - throw std::runtime_error("Failed to create db subscription"); - } - - // If all goes well, Set up handlers for start and stop monitoring events - // The subscription context is being kept alive because it is being bound into some internal storage by onStart - subscriptionContext->subscriptionControl->onStart([subscriptionContext](bool isStarting) { - onStart(subscriptionContext, isStarting); - }); -} - -/** - * Used by both value and property subscriptions, this function will get and return the database value to the monitor. - * - * @param subscriptionContext the subscription context - * @param getOperationType the operation this callback serves - * @param pDbFieldLog the database field log - */ -void SingleSource::subscriptionCallback(SingleSourceSubscriptionCtx* subscriptionContext, - const GetOperationType getOperationType, struct db_field_log* pDbFieldLog) { - - // Get the current value of this subscription - // We simply merge new field changes onto this value as events occur - auto currentValue = subscriptionContext->currentValue; - - { - DBLocker F(dbChannelRecord(subscriptionContext->pValueChannel.get())); - IOCSource::get(subscriptionContext->pValueChannel.get(), - ((getOperationType == FOR_PROPERTIES) ? subscriptionContext->pPropertiesChannel.get() : nullptr), - currentValue, getOperationType, pDbFieldLog); - } - - // Make sure that the initial subscription update has occurred on both channels before continuing - // As we make two initial updates when opening a new subscription, we need both to have completed before continuing - if (subscriptionContext->hadValueEvent && subscriptionContext->hadPropertyEvent) { - // Return value - subscriptionContext->subscriptionControl->post(currentValue.clone()); - currentValue.unmark(); - } -} - } // ioc } // pvxs diff --git a/ioc/singlesource.h b/ioc/singlesource.h index 1933f10..770e296 100644 --- a/ioc/singlesource.h +++ b/ioc/singlesource.h @@ -15,7 +15,6 @@ #include "dbeventcontextdeleter.h" #include "iocsource.h" -#include "metadata.h" #include "singlesrcsubscriptionctx.h" namespace pvxs { @@ -42,77 +41,6 @@ private: List allRecords; // The event context for all subscriptions DBEventContext eventContext; - - // Create request and subscription handlers for single record sources - void createRequestAndSubscriptionHandlers(std::unique_ptr&& channelControl, - const std::shared_ptr& dbChannelSharedPtr); - // Handles all get, put and subscribe requests - static void onOp(const std::shared_ptr& forceProcessingOption, const Value& valuePrototype, - std::unique_ptr&& channelConnectOperation); - // Helper function to create a value prototype for the given channel - static Value getValuePrototype(const std::shared_ptr& dbChannelSharedPtr); - - ////////////////////////////// - // Get - ////////////////////////////// - // Handle the get operation - static void get(dbChannel* pDbChannel, std::unique_ptr& getOperation, - const Value& valuePrototype); - - ////////////////////////////// - // Subscriptions - ////////////////////////////// -/** - * This callback handles notifying of updates to subscribed-to pv values. The macro addSubscriptionEvent(...) - * creates the call to this function, so your IDE may mark it as unused (don't believe it :) ) - * - * @param userArg the user argument passed to the callback function from the framework: the subscriptionContext - * @param pDbFieldLog the database field log containing the changes to notify - */ - static void subscriptionValueCallback(void* userArg, struct dbChannel*, int, struct db_field_log* pDbFieldLog) { - auto subscriptionContext = (SingleSourceSubscriptionCtx*)userArg; - subscriptionContext->hadValueEvent = true; - subscriptionCallback(subscriptionContext, FOR_VALUE, pDbFieldLog); - } - -/** - * This callback handles notifying of updates to subscribed-to pv properties. The macro addSubscriptionEvent(...) - * creates the call to this function, so your IDE may mark it as unused (don't believe it :) ) - * - * @param userArg the user argument passed to the callback function from the framework: the subscriptionContext - * @param pDbFieldLog the database field log containing the changes to notify - */ - static void subscriptionPropertiesCallback(void* userArg, struct dbChannel*, int, - struct db_field_log* pDbFieldLog) { - auto subscriptionContext = (SingleSourceSubscriptionCtx*)userArg; - subscriptionContext->hadPropertyEvent = true; - subscriptionCallback(subscriptionContext, FOR_PROPERTIES, pDbFieldLog); - } - - // General subscriptions callback - static void - subscriptionCallback(SingleSourceSubscriptionCtx* subscriptionCtx, GetOperationType getOperationType, - struct db_field_log* pDbFieldLog); -/** - * Called when a client pauses / stops a subscription it has been subscribed to - * - * @param subscriptionContext the subscription context - */ - static void onDisableSubscription(const std::shared_ptr& subscriptionContext) { - db_event_disable(subscriptionContext->pValueEventSubscription.get()); - db_event_disable(subscriptionContext->pPropertiesEventSubscription.get()); - } - - // Called by onStart() when a client starts a subscription it has subscribed to - static void onStartSubscription(const std::shared_ptr& subscriptionContext); - // Called when a subscription is being set up - void onSubscribe(const std::shared_ptr& subscriptionContext, - std::unique_ptr&& subscriptionOperation) const; - // Called when a client starts or stops a subscription. isStarting flag determines which - static void onStart(const std::shared_ptr& subscriptionContext, bool isStarting); - - static int putCallback(processNotify* notify, notifyPutType type); - static void doneCallback(processNotify* notify); }; } // ioc diff --git a/ioc/singlesourcehooks.cpp b/ioc/singlesourcehooks.cpp index 65b2af4..0395cdb 100644 --- a/ioc/singlesourcehooks.cpp +++ b/ioc/singlesourcehooks.cpp @@ -11,38 +11,26 @@ #include #include +#include #include +#define PVXS_ENABLE_EXPERT_API + +#include #include +#include +#include #include "iocshcommand.h" #include "singlesource.h" -// must include after log.h has been included to avoid clash with printf macro +// include last to avoid clash of #define printf with other headers #include namespace pvxs { namespace ioc { -/** - * List single db record/field names that are registered with the pvxs IOC server - * With no arguments this will list all the single record names. - * With the optional showDetails arguments it will additionally display detailed information. - * - * @param pShowDetails if "yes", "YES", "true","TRUE", "1" then show details, otherwise don't show details - */ -void pvxsl(const char* pShowDetails) { - runOnPvxsServer([&pShowDetails](IOCServer* pPvxsServer) { - auto showDetails = false; - - if (pShowDetails) { - std::string showDetailsValue(pShowDetails); - std::transform(showDetailsValue.begin(), showDetailsValue.end(), showDetailsValue.begin(), - [](unsigned char c) { return std::tolower(c); }); - if (showDetailsValue == "yes" || showDetailsValue == "true" || showDetailsValue == "1") { - showDetails = true; - } - } - +void pvxsl(int detail) { + runOnPvxsServer([detail](server::Server* pPvxsServer) { // For each registered source/IOID pair print a line of either detailed or regular information for (auto& pair: pPvxsServer->listSource()) { auto& record = pair.first; @@ -58,14 +46,14 @@ void pvxsl(const char* pShowDetails) { auto list = source->onList(); if (list.names && !list.names->empty()) { - if (showDetails) { + if (detail) { printf("------------------\n"); printf("SOURCE: %s@%d%s\n", record.c_str(), pair.second, (list.dynamic ? " [dynamic]" : "")); printf("------------------\n"); printf("RECORDS: \n"); } for (auto& name: *list.names) { - if (showDetails) { + if (detail) { printf(" "); } printf("%s\n", name.c_str()); @@ -78,18 +66,96 @@ void pvxsl(const char* pShowDetails) { } } // namespace pvxs::ioc +using namespace pvxs; using namespace pvxs::ioc; namespace { +void qReport(unsigned level) { + try{ + runOnPvxsServer([level](server::Server* pPvxsServer) { + std::ostringstream strm; + Detailed D(strm, level); + strm << *pPvxsServer; + printf("%s", strm.str().c_str()); + }); + }catch(...){ + // runOnPvxsServer has already logged + } +} + +void qStats(unsigned *channels, unsigned *clients) { + try{ + auto action([channels, clients](server::Server* pPvxsServer) { + auto report(pPvxsServer->report(false)); + if(clients) { + *clients = report.connections.size(); + } + if(channels) { + size_t nchan = 0u; + for(auto& conn : report.connections) { + nchan += conn.channels.size(); + } + *channels = nchan; + } + }); + runOnPvxsServer(action); + }catch(...){ + // runOnPvxsServer has already logged + } +} + +int qClient(char *pBuf, size_t bufSize) { + try { + if(auto op = CurrentOp::current()) { + auto& peer(op->peerName()); + const auto& cred(op->credentials()); + + if(cred->method=="ca") { + (void)epicsSnprintf(pBuf, bufSize, "q2:%s@%s", + cred->account.c_str(), + peer.c_str()); + } else { + (void)epicsSnprintf(pBuf, bufSize, "q2:%s/%s@%s", + cred->method.c_str(), + cred->account.c_str(), + peer.c_str()); + } + return 0; + } + }catch(std::exception& e){ + // shouldn't really happen, but if it does once then it will probably + // happen a lot. So limit noise. + static bool shown = false; + if(!shown) { + shown = true; + errlogPrintf("Unexpected exception in %s: %s\n", __func__, e.what()); + } + } + return -1; +} + +dbServer qsrv2Server = { + ELLNODE_INIT, + "qsrv2", + qReport, + qStats, + qClient, +}; + /** * Initialise qsrv database single records by adding them as sources in our running pvxs server instance * * @param theInitHookState the initHook state - we only want to trigger on the initHookAfterIocBuilt state - ignore all others */ void qsrvSingleSourceInit(initHookState theInitHookState) { + if(!IOCSource::enabled()) + return; + if (theInitHookState == initHookAtBeginning) { + (void)dbRegisterServer(&qsrv2Server); + } else if (theInitHookState == initHookAfterIocBuilt) { - pvxs::ioc::iocServer().addSource("qsrvSingle", std::make_shared(), 0); + pvxs::ioc::server().addSource("qsrvSingle", std::make_shared(), 0); } } @@ -107,9 +173,8 @@ void qsrvSingleSourceInit(initHookState theInitHookState) { */ void pvxsSingleSourceRegistrar() { // Register commands to be available in the IOC shell - IOCShCommand("pvxsl", "[show_detailed_information?]", "Single Sources list.\n" - "List record/field names.\n" - "If `show_detailed_information?` flag is `yes`, `true` or `1` then show detailed information.\n") + IOCShCommand("pvxsl", "details", + "List PV names.\n") .implementation<&pvxsl>(); initHookRegister(&qsrvSingleSourceInit); diff --git a/ioc/singlesrcsubscriptionctx.cpp b/ioc/singlesrcsubscriptionctx.cpp index c6b171b..adc4064 100644 --- a/ioc/singlesrcsubscriptionctx.cpp +++ b/ioc/singlesrcsubscriptionctx.cpp @@ -8,6 +8,7 @@ */ #include "singlesrcsubscriptionctx.h" +#include "utilpvt.h" namespace pvxs { namespace ioc { @@ -17,15 +18,9 @@ namespace ioc { * * @param dbChannelSharedPtr pointer to the db channel to use to construct the single source subscription context */ -SingleSourceSubscriptionCtx::SingleSourceSubscriptionCtx(const std::shared_ptr& dbChannelSharedPtr) { - pValueChannel = dbChannelSharedPtr; - pPropertiesChannel.reset(dbChannelCreate(dbChannelName(dbChannelSharedPtr)), [](dbChannel* ch) { - if (ch) dbChannelDelete(ch); - }); - if (pPropertiesChannel && dbChannelOpen(pPropertiesChannel.get())) { - throw std::bad_alloc(); - } - -} +SingleSourceSubscriptionCtx::SingleSourceSubscriptionCtx(const std::shared_ptr &sInfo) + :pPropertiesChannel(dbChannelName(sInfo->chan)) + ,info(sInfo) +{} } // iocs } // pvxs diff --git a/ioc/singlesrcsubscriptionctx.h b/ioc/singlesrcsubscriptionctx.h index ee89708..64002ae 100644 --- a/ioc/singlesrcsubscriptionctx.h +++ b/ioc/singlesrcsubscriptionctx.h @@ -12,29 +12,43 @@ #include -#include - +#include "channel.h" +#include "fieldconfig.h" #include "subscriptionctx.h" namespace pvxs { namespace ioc { +struct SingleInfo : public MappingInfo { + Channel chan; + + explicit SingleInfo(Channel&& chan) :chan(std::move(chan)) { + updateNsecMask(dbChannelRecord(this->chan)); + } +}; + /** * A subscription context */ class SingleSourceSubscriptionCtx : public SubscriptionCtx { public: - explicit SingleSourceSubscriptionCtx(const std::shared_ptr& dbChannelSharedPtr); -// For locking access to subscription context - std::shared_ptr pValueChannel; - std::shared_ptr pPropertiesChannel; + explicit SingleSourceSubscriptionCtx(const std::shared_ptr& sInfo); + + // extra dbChannel* to have a distinct state for any server side filters. (eg. decimate) + const Channel pPropertiesChannel; // This is used to store the current value. Each subscription event simply merges // new fields into this value Value currentValue{}; + std::shared_ptr info; epicsMutex eventLock{}; std::unique_ptr subscriptionControl{}; + bool eventsEnabled = false; + + ~SingleSourceSubscriptionCtx() { + assert(!eventsEnabled); + } }; } // ioc diff --git a/ioc/subscriptionctx.h b/ioc/subscriptionctx.h index 94e13f3..1ba4c59 100644 --- a/ioc/subscriptionctx.h +++ b/ioc/subscriptionctx.h @@ -11,43 +11,59 @@ #define PVXS_SUBSCRIPTIONCTX_H #include +#include -/** - * Add a subscription event by calling db_add_event using the given subscriptionCtx - * and selecting the correct elements based on the given type of event being added. - * You need to specify the correct options that correspond to the event type. - * Adds a deleter to clean up the subscription by calling db_cancel_event. - * - * @param _type the type of event being added. `Value` or `Properties` - * @param _eventContext the name of the dbEventCtx to use to add events - * @param _subscriptionContext - The subscriptionCtx to use - * @param _options the options. DBE_VALUE, DBE_ALARM, or DBE_PROPERTY or some combination - */ -#define addSubscriptionEvent(_type, _eventContext, _subscriptionContext, _options) \ - _subscriptionContext->p ## _type ## EventSubscription \ - .reset(db_add_event((_eventContext) .get(), \ - (_subscriptionContext) ->p ## _type ## Channel.get(), \ - subscription ## _type ## Callback, \ - (void*) (_subscriptionContext).get(), \ - _options \ - ), \ - [](dbEventSubscription pEventSubscription) { \ - if (pEventSubscription) { \ - db_cancel_event(pEventSubscription); \ - } \ - }) +#include + +#include "channel.h" +#include "dbeventcontextdeleter.h" namespace pvxs { namespace ioc { +class Subscription { + std::shared_ptr sub; // holds void* returned by db_add_event() +public: + /* Add a subscription event by calling db_add_event using the given subscriptionCtx + * and selecting the correct elements based on the given type of event being added. + * You need to specify the correct options that correspond to the event type. + * Adds a deleter to clean up the subscription by calling db_cancel_event. + */ + void subscribe(void* context, + const Channel& pChan, + EVENTFUNC *user_sub, void *user_arg, unsigned select) + { + auto chan(pChan); // bind by value + sub.reset(db_add_event(context, chan, + user_sub, user_arg, select), + [chan](void* sub) mutable + { + db_cancel_event(sub); + chan = Channel(); // dbChannel* must outlive subscription + }); + if(!sub) + throw std::runtime_error("Failed to create db subscription"); + } + void enable() { + if(sub) { + db_event_enable(sub.get()); + db_post_single_event(sub.get()); + } + } + void disable() { + if(sub) + db_event_disable(sub.get()); + } +}; + /** * A subscription context */ class SubscriptionCtx { public: // For locking access to subscription context - std::shared_ptr pValueEventSubscription{}; - std::shared_ptr pPropertiesEventSubscription{}; + Subscription pValueEventSubscription; + Subscription pPropertiesEventSubscription; bool hadValueEvent = false; bool hadPropertyEvent = false; }; diff --git a/ioc/typeutils.cpp b/ioc/typeutils.cpp index 3f23b9a..b863d4e 100644 --- a/ioc/typeutils.cpp +++ b/ioc/typeutils.cpp @@ -7,58 +7,19 @@ * */ +#include + #include #include +#include +#include "dbentry.h" +#include "fielddefinition.h" #include "typeutils.h" namespace pvxs { -/** - * Convert the given database field type code into a pvxs type code - * - * @param dbfType the database field type code - * @return a pvxs type code - * - */ -TypeCode fromDbfType(dbfType dbfType) { - switch (dbfType) { - case DBF_CHAR: - return TypeCode::Int8; - case DBF_UCHAR: - return TypeCode::UInt8; - case DBF_SHORT: - return TypeCode::Int16; - case DBF_USHORT: - return TypeCode::UInt16; - case DBF_LONG: - return TypeCode::Int32; - case DBF_ULONG: - return TypeCode::UInt32; - case DBF_INT64: - return TypeCode::Int64; - case DBF_UINT64: - return TypeCode::UInt64; - case DBF_FLOAT: - return TypeCode::Float32; - case DBF_DOUBLE: - return TypeCode::Float64; - case DBF_ENUM: - case DBF_MENU: - return TypeCode::Struct; - case DBF_STRING: - case DBF_INLINK: - case DBF_OUTLINK: - case DBF_FWDLINK: - return TypeCode::String; - case DBF_DEVICE: - case DBF_NOACCESS: - default: - return TypeCode::Null; - } -} - /** * Convert the given database record type code into a pvxs type code * @@ -81,10 +42,12 @@ TypeCode fromDbrType(short dbrType) { return TypeCode::Int32; case DBR_ULONG: return TypeCode::UInt32; +#ifdef DBR_INT64 case DBR_INT64: return TypeCode::Int64; case DBR_UINT64: return TypeCode::UInt64; +#endif case DBR_FLOAT: return TypeCode::Float32; case DBR_DOUBLE: @@ -97,4 +60,33 @@ TypeCode fromDbrType(short dbrType) { } } + +namespace ioc { +const char *MappingInfo::name(type_t t) +{ + switch(t) { + case Scalar: return "scalar"; + case Plain: return "plain"; + case Any: return "any"; + case Meta: return "meta"; + case Proc: return "proc"; + case Structure: return "structure"; + case Const: return "const"; + } + return ""; } + +void MappingInfo::updateNsecMask(dbCommon *prec) +{ + assert(prec); + DBEntry ent(prec); + if(auto val = ent.info("Q:time:tag")) { + epicsInt32 dig = 0; + if(strncmp(val, "nsec:lsb:", 9)==0 && !epicsParseInt32(&val[9], &dig, 10, nullptr)) { + nsecMask = (uint64_t(1u)< #include -#include -#include - -/** - * To switch the given `TypeCode` for a statically typed call to the given function with the appropriate template type - * e.g. - * `SwitchTypeCodeForTemplate(typeCode, getValue,(value, pBuffer))` - * will convert a typeCode of TypeCode::Int8 into a call to - * `getValue(value, pBuffer)` - * - * @param _typeCode the typecode to be used in the switch statement - should be of type TypeCode or short - * @param _function the templated function to call - * @param _arguments the list of arguments to be passed to the templated function. include the parentheses - */ -#define SwitchTypeCodeForTemplatedCall(_typeCode, _function, _arguments) \ -switch ((_typeCode.code)) { \ - case TypeCode::Int8: case TypeCode::Int8A: return _function_arguments ; \ - case TypeCode::UInt8: case TypeCode::UInt8A: return _function_arguments ; \ - case TypeCode::Int16: case TypeCode::Int16A: return _function_arguments ; \ - case TypeCode::UInt16: case TypeCode::UInt16A: return _function_arguments ; \ - case TypeCode::Int32: case TypeCode::Int32A: return _function_arguments ; \ - case TypeCode::UInt32: case TypeCode::UInt32A: return _function_arguments ; \ - case TypeCode::Int64: case TypeCode::Int64A: return _function_arguments ; \ - case TypeCode::UInt64: case TypeCode::UInt64A: return _function_arguments ; \ - case TypeCode::Float32: case TypeCode::Float32A: return _function_arguments ; \ - case TypeCode::Float64: case TypeCode::Float64A: return _function_arguments ; \ - case TypeCode::String: case TypeCode::StringA: \ - case TypeCode::Struct: case TypeCode::StructA: \ - case TypeCode::Union: case TypeCode::UnionA: \ - case TypeCode::Any: case TypeCode::AnyA: \ - default: \ - throw std::logic_error(SB() << "Unsupported Type: " << TypeCode(_typeCode) ); \ -} +#include namespace pvxs { -TypeCode fromDbfType(dbfType dbfType); TypeCode fromDbrType(short dbrType); namespace ioc { diff --git a/qsrv/qsrvMain.cpp b/qsrv/qsrvMain.cpp index c4f3070..e0d51cb 100644 --- a/qsrv/qsrvMain.cpp +++ b/qsrv/qsrvMain.cpp @@ -338,11 +338,11 @@ int main(int argc, char* argv[]) { return 1; } } else { - // If non-interactive then exit + // If non-interactive then spin forever if (dbIsLoaded || userScriptHasBeenExecuted) { - epicsExitCallAtExits(); - epicsThreadSleep(0.1); - epicsThreadSuspendSelf(); + while(true) { + epicsThreadSleep(1000.0); + } } else { // Indicate that there was probably an error if nothing was loaded or executed std::cerr << "Nothing to do!\n"; diff --git a/src/utilpvt.h b/src/utilpvt.h index da379ec..7c84a77 100644 --- a/src/utilpvt.h +++ b/src/utilpvt.h @@ -244,6 +244,10 @@ timeval totv(double t) return ret; } +namespace ioc { +void IOCGroupConfigCleanup(); +} + //! Scoped restore of std::ostream state (format flags, fill char, and field width) struct Restore { std::ostream& strm; diff --git a/test/Makefile b/test/Makefile index 641ef41..b728add 100644 --- a/test/Makefile +++ b/test/Makefile @@ -115,13 +115,30 @@ DBDDEPENDS_FILES += testioc.dbd$(DEP) testioc_DBD = base.dbd pvxsIoc.dbd TESTFILES += $(COMMON_DIR)/testioc.dbd -TESTPROD_HOST += testioc -testioc_SRCS += testioc.cpp -testioc_SRCS += testioc_registerRecordDeviceDriver.cpp -testioc_LIBS = pvxsIoc pvxs $(EPICS_BASE_IOC_LIBS) -TESTFILES += ../testioc.db ../testiocg.db ../image.db ../ntenum.db -TESTFILES += ../testioc.json -TESTS += testioc +TESTPROD_HOST += testqsingle +testqsingle_SRCS += testqsingle +testqsingle_SRCS += testioc_registerRecordDeviceDriver.cpp +testqsingle_LIBS = pvxsIoc pvxs $(EPICS_BASE_IOC_LIBS) +TESTFILES += ../testqsingle.db +TESTFILES += ../testqsingle64.db +TESTFILES += ../testioc.acf +TESTS += testqsingle + +endif + +ifdef BASE_7_0 + +TESTPROD_HOST += testqgroup +testqgroup_SRCS += testqgroup +testqgroup_SRCS += testioc_registerRecordDeviceDriver.cpp +testqgroup_LIBS = pvxsIoc pvxs $(EPICS_BASE_IOC_LIBS) +TESTFILES += ../table.db +TESTFILES += ../image.db +TESTFILES += ../image.json +TESTFILES += ../iq.db +TESTFILES += ../ntenum.db +TESTFILES += ../const.db +TESTS += testqgroup PROD_SRCS_RTEMS += rtemsTestData.c diff --git a/test/const.db b/test/const.db new file mode 100644 index 0000000..549512e --- /dev/null +++ b/test/const.db @@ -0,0 +1,9 @@ +record(ai, "$(P)dummy") { + info(Q:group, { + "$(P)const": { + "s.i": { +type:"const", +const:14 }, + "s.d": { +type:"const", +const:1.5 }, + "s.s": { +type:"const", +const:"hello" } + } + }) +} diff --git a/test/image.db b/test/image.db index b912c64..950d858 100644 --- a/test/image.db +++ b/test/image.db @@ -4,6 +4,7 @@ record(longout, "$(N):ArraySize0_RBV") { field(VAL, "100") info(Q:group, { "$(N):Array":{ + "+atomic":true, "dimension[0].size":{+channel:"VAL", +type:"plain", +putorder:0} } }) @@ -21,7 +22,7 @@ record(longout, "$(N):ArraySize1_RBV") { } record(aSub, "$(N):ArrayData_") { - field(SNAM, "QSRV_image_demo") + field(SNAM, "QSRV2_image_demo") field(PINI, "YES") field(FTA, "ULONG") field(FTB, "ULONG") @@ -88,4 +89,5 @@ record(bo, "$(N):extra") { "attribute[1]":{+type:"meta", +channel:"SEVR"} } }) + alias("$(N):extra:alias") } diff --git a/test/image.json b/test/image.json new file mode 100644 index 0000000..2ca2990 --- /dev/null +++ b/test/image.json @@ -0,0 +1,20 @@ +/* demonstrate a definition equivalent to the info(Q:group, ...) tags in image.dbd */ +{ + "$(N):Array2":{ + "+id":"epics:nt/NTNDArray:1.0", + "value":{"+type":"any", + "+channel":"$(N):ArrayData.VAL", + "+trigger":"*"}, + "":{"+type":"meta", "+channel":"$(N):ArrayData.SEVR"}, + "x":{"+type":"meta", "+channel":"$(N):ArrayData.SEVR"}, + "dimension[0].size":{"+channel":"$(N):ArraySize0_RBV.VAL", "+type":"plain", "+putorder":0}, + "dimension[1].size":{"+channel":"$(N):ArraySize1_RBV.VAL", "+type":"plain", "+putorder":0}, + "attribute[0].name":{"+type":"plain", "+channel":"$(N):ColorMode_.VAL"}, + "attribute[0].value":{"+type":"any", "+channel":"$(N):ColorMode.VAL"}, + "attribute[1].value":{"+type":"any", + "+channel":"$(N):extra.VAL", + "+putorder":0, + "+trigger":"attribute[1].value"}, + "attribute[1]":{"+type":"meta", "+channel":"$(N):extra:alias.SEVR"}, + } +} diff --git a/test/iq.db b/test/iq.db index 73895f0..f8a6912 100644 --- a/test/iq.db +++ b/test/iq.db @@ -38,7 +38,7 @@ record(calc, "$(N)Phase:Q") { } record(waveform, "$(N)I") { - field(DTYP, "QSRV Demo") + field(DTYP, "QSRV2 Demo") field( INP, "$(N)Phase:I") field(FTVL, "DOUBLE") field(NELM, "500") @@ -46,10 +46,11 @@ record(waveform, "$(N)I") { info(Q:group, { "$(N)iq":{"I": {+channel:"VAL"}} }) + info(Q:form, "Engineering") } record(waveform, "$(N)Q") { - field(DTYP, "QSRV Demo") + field(DTYP, "QSRV2 Demo") field( INP, "$(N)Phase:Q") field(FTVL, "DOUBLE") field(NELM, "500") @@ -57,6 +58,7 @@ record(waveform, "$(N)Q") { info(Q:group, { "$(N)iq":{"Q": {+channel:"VAL", +trigger:"*"}} }) + info(Q:form, "Engineering") } record(calcout, "$(N)dly_") { diff --git a/test/table.db b/test/table.db index 86a1480..d38a936 100644 --- a/test/table.db +++ b/test/table.db @@ -2,7 +2,7 @@ record(aai, "$(N)Labels_") { field(FTVL, "STRING") field(NELM, "2") - field(INP , {const:["Column A", "Column B"]}) + field(INP , {const:["$(LBL1)", "$(LBL2)"]}) info(Q:group, { "$(N)Tbl":{ +id:"epics:nt/NTTable:1.0", @@ -17,7 +17,7 @@ record(aao, "$(N)A") { field(NELM, "10") info(Q:group, { "$(N)Tbl":{ - "value.A":{+type:"plain", +channel:"VAL", +putorder:1} + "value.A":{+type:"plain", +channel:"VAL", +putorder:$(PO1)} } }) field(TPRO, "1") @@ -29,7 +29,7 @@ record(aao, "$(N)B") { info(Q:group, { "$(N)Tbl":{ "":{+type:"meta", +channel:"VAL"}, - "value.B":{+type:"plain", +channel:"VAL", +putorder:1} + "value.B":{+type:"plain", +channel:"VAL", +putorder:$(PO2)} } }) field(TPRO, "1") diff --git a/test/testioc.cpp b/test/testioc.cpp deleted file mode 100644 index c1d9823..0000000 --- a/test/testioc.cpp +++ /dev/null @@ -1,535 +0,0 @@ -/** - * Copyright - See the COPYRIGHT that is included with this distribution. - * pvxs is distributed subject to a Software License Agreement found - * in file LICENSE that is included with this distribution. - */ - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -extern "C" { -extern int testioc_registerRecordDeviceDriver(struct dbBase*); -} - -using namespace pvxs; - -namespace { - -} // namespace - -#define testOkB(__pass, __fmt, ...) boxLeft(); testOk(__pass, __fmt, ##__VA_ARGS__) -#define testEqB(__lhs, __rhs) boxLeft(); testEq(__lhs, __rhs) -#define testStrEqB(__lhs, __rhs) boxLeft(); testStrEq(__lhs, __rhs) -#define testArrEqB(__lhs, __rhs) boxLeft(); testArrEq(__lhs, __rhs) -#define testdbPutArrFieldOkB(__pv, __dbrType, __count, __pbuf) boxLeft(); testdbPutArrFieldOk(__pv, __dbrType, __count, __pbuf) -#define testdbGetFieldEqualB(__pv, __dbrType, ...) boxLeft(); testdbGetFieldEqual(__pv, __dbrType, ##__VA_ARGS__) -#define testdbGetArrFieldEqualB(__pv, __dbfType, __nRequest, __pbufcnt, __pbuf) boxLeft(); testdbGetArrFieldEqual(__pv, __dbfType, __nRequest, __pbufcnt, __pbuf) -#define testThrowsB(__lambda) boxLeft(); testThrows(__lambda) - -static void boxLeft(); -static std::shared_ptr subscribe(epicsEvent& event, const char* pvName); -static Value waitForUpdate(const std::shared_ptr& subscription, epicsEvent& event); -static pvxs::client::Context clientContext; - -// List of all tests to be run in order -static std::initializer_list tests = { - []() { - testThrowsB([] { ioc::server(); }); - }, - []() { - testdbReadDatabase("testioc.dbd", nullptr, nullptr); - testOkB(true, R"("testioc.dbd" loaded)"); - }, - []() { - testOkB(!testioc_registerRecordDeviceDriver(pdbbase), "testioc_registerRecordDeviceDriver(pdbbase)"); - }, - []() { testOkB((bool)ioc::server(), "ioc::server()"); }, - []() { - testdbReadDatabase("testioc.db", nullptr, "user=test"); - testOkB(true, R"(testdbReadDatabase("testioc.db", nullptr, "user=test"))"); - }, - []() { - testdbReadDatabase("testiocg.db", nullptr, "user=test"); - testOkB(true, R"(testdbReadDatabase("testiocg.db", nullptr, "user=test"))"); - }, - []() { - testdbReadDatabase("image.db", nullptr, "N=tst"); - testOkB(true, R"(testdbReadDatabase("testiocg.db", nullptr, "user=test"))"); - }, - []() { testOkB(!pvxs::ioc::dbLoadGroup("../testioc.json"), R"(dbLoadGroup("testioc.json"))"); }, - []() { - testIocInitOk(); - testPass("testIocInitOk()"); - }, - []() { testdbGetFieldEqualB("test:aiExample", DBR_DOUBLE, 42.2); }, - []() { testdbGetFieldEqualB("test:stringExample", DBR_STRING, "Some random value"); }, - []() { - shared_array expected({ 1.0, 2.0, 3.0 }); - testdbGetArrFieldEqualB("test:arrayExample", DBR_DOUBLE, 3, expected.size(), expected.data()); - }, - []() { testdbGetFieldEqualB("test:longExample", DBR_LONG, 102042); }, - []() { testdbGetFieldEqualB("test:longlongExample", DBR_INT64, 0x123456789); }, - []() { testdbGetFieldEqualB("test:enumExample", DBR_ENUM, 2); }, - []() { - char expected[MAX_STRING_SIZE * 2]{ 0 }; - std::string first("Column A"); - std::string second("Column B"); - first.copy(&expected[0], MAX_STRING_SIZE - 1); - second.copy(&expected[MAX_STRING_SIZE], MAX_STRING_SIZE - 1); - - testdbGetArrFieldEqualB("test:groupExampleAS", DBR_STRING, 2, 2, &expected); - }, - []() { - shared_array expected({ 10, 20, 30, 40, 50 }); - testdbGetArrFieldEqualB("test:vectorExampleD1", DBR_DOUBLE, 5, expected.size(), expected.data()); - }, - []() { - shared_array expected({ 1.1, 2.2, 3.3, 4.4, 5.5 }); - testdbGetArrFieldEqualB("test:vectorExampleD2", DBR_DOUBLE, 5, expected.size(), expected.data()); - }, - []() { - clientContext = ioc::server().clientConfig().build(); - testShow() << clientContext.config(); - testOkB(true, "cli = ioc::server().clientConfig().build()"); - }, - []() { - auto val = clientContext.get("test:aiExample").exec()->wait(5.0); - auto aiExample = val["value"].as(); - auto expected = 42.2; - testEqB(aiExample, expected); - }, - []() { - auto val = clientContext.get("test:stringExample").exec()->wait(5.0); - auto stringExample = val["value"].as(); - auto expected = "Some random value"; - testStrEqB(stringExample, expected); - }, - []() { - auto val = clientContext.get("test:arrayExample").exec()->wait(5.0); - shared_array expected({ 1.0, 2.0, 3.0 }); - auto arrayExample = val["value"].as>(); - testArrEqB(arrayExample, expected); - }, - []() { - auto val = clientContext.get("test:arrayExample.[1:2]").exec()->wait(5.0); - shared_array expected({ 2.0, 3.0 }); - auto arrayExample = val["value"].as>(); - testArrEqB(arrayExample, expected); - }, - []() { - shared_array array({}); - testdbPutArrFieldOkB("test:arrayExample", DBR_DOUBLE, array.size(), array.data()); - }, - []() { - auto val = clientContext.get("test:arrayExample").exec()->wait(5.0); - shared_array expected({}); - auto arrayExample = val["value"].as>(); - testArrEqB(arrayExample, expected); - }, - []() { - shared_array array({ 1.0, 2.0, 3.0, 4.0, 5.0 }); - testdbPutArrFieldOkB("test:arrayExample", DBR_DOUBLE, array.size(), array.data()); - }, - []() { - auto val = clientContext.get("test:arrayExample").exec()->wait(5.0); - shared_array expected({ 1.0, 2.0, 3.0, 4.0, 5.0 }); - auto arrayExample = val["value"].as>(); - testArrEqB(arrayExample, expected); - }, - []() { - auto val = clientContext.get("test:longExample").exec()->wait(5.0); - auto longValue = val["value"].as(); - auto expected = 102042; - testEqB(longValue, expected); - }, - []() { - auto val = clientContext.get("test:longlongExample").exec()->wait(5.0); - auto longValue = val["value"].as(); - auto expected = 0x123456789; - testEqB(longValue, expected); - }, - []() { - auto val = clientContext.get("test:enumExample").exec()->wait(5.0); - auto enumExample = val["value.index"].as(); - auto expected = 2; - testEqB(enumExample, expected); - }, - []() { - auto val = clientContext.get("test:enumExample").exec()->wait(5.0); - shared_array expected({ "zero", "one", "two" }); - auto enumExampleChoices = val["value.choices"].as>(); - testArrEqB(enumExampleChoices, expected); - }, - []() { - auto val = clientContext.get("test:tableExample").exec()->wait(5.0); - shared_array expected({ "Column A", "Column B" }); - auto tableExampleLabels = val["labels"].as>(); - testArrEqB(tableExampleLabels, expected); - }, - []() { - auto val = clientContext.get("test:tableExample").exec()->wait(5.0); - shared_array expected({ 10, 20, 30, 40, 50 }); - auto tableExampleValueA = val["value.A"].as>(); - testArrEqB(tableExampleValueA, expected); - }, - []() { - auto val = clientContext.get("test:structExample").exec()->wait(5.0); - auto structExampleStringValue = val["string.value"].as(); - auto expected = "Some random value"; - testStrEqB(structExampleStringValue, expected); - }, - []() { - auto val = clientContext.get("test:structExample").exec()->wait(5.0); - auto structExampleAiValue = val["ai.value"].as(); - auto expected = 42.2; - testEqB(structExampleAiValue, expected); - }, - []() { - auto val = clientContext.get("test:structExample").exec()->wait(5.0); - shared_array expected({ 1, 2, 3, 4, 5 }); - auto structExampleArrayValue = val["array.value"].as>(); - testArrEqB(structExampleArrayValue, expected); - }, - []() { - auto val = clientContext.get("test:structExample").exec()->wait(5.0); - auto structExampleSa_0_LongValue = val["sa[0].long.value"].as(); - auto expected = 102042; - testEqB(structExampleSa_0_LongValue, expected); - }, - []() { - auto val = clientContext.get("test:structExample").exec()->wait(5.0); - auto structExampleSa_0_EnumValueIndex = val["sa[0].enum.value.index"].as(); - auto expected = 2; - testEqB(structExampleSa_0_EnumValueIndex, expected); - }, - []() { - auto val = clientContext.get("test:structExample").exec()->wait(5.0); - auto structExampleSa_0_EnumValueChoices = val["sa[0].enum.value.choices"] - .as>(); - shared_array expected({ "zero", "one", "two" }); - testArrEqB(structExampleSa_0_EnumValueChoices, expected); - }, - []() { - auto val = clientContext.get("test:structExample2").exec()->wait(5.0); - auto structExample2Sa_0_AnyValue = val["sa[0].any"].as(); - auto expected = 102042; - testEqB(structExample2Sa_0_AnyValue, expected); - }, - []() { - clientContext.put("test:calcExample.FLNK").set("value", "").exec()->wait(5.0); - testdbGetFieldEqualB("test:calcExample.FLNK", DBR_STRING, ""); - }, - []() { - clientContext.put("test:calcExample.FLNK").set("value", "test:stringExample").exec()->wait(5.0); - testdbGetFieldEqualB("test:calcExample.FLNK", DBR_STRING, "test:stringExample"); - }, - []() { - // TODO check whether long strings need to be null terminated - shared_array arrayLinkVal( - { 't', 'e', 's', 't', ':', 'a', 'i', 'E', 'x', 'a', 'm', 'p', 'l', 'e', '\0' }); - clientContext.put("test:calcExample.FLNK$").set("value", arrayLinkVal).exec()->wait(5.0); - testdbGetFieldEqualB("test:calcExample.FLNK", DBR_STRING, "test:aiExample"); - }, - []() { - shared_array expected({}); - clientContext.put("tst:Array").build([&expected](Value&& prototype) -> Value { - auto putval = prototype.cloneEmpty(); - putval["value"] = expected; - return putval; - }) - .exec()->wait(5.0); - testdbGetArrFieldEqualB("tst:ArrayData", DBR_USHORT, 0, expected.size(), expected.data()); - }, - []() { - shared_array expected({ 1, 2, 3, 4, 5 }); - clientContext.put("tst:Array").build([&expected](Value&& prototype) -> Value { - auto putval = prototype.cloneEmpty(); - putval["value"] = expected; - return putval; - }) - .exec()->wait(5.0); - testdbGetArrFieldEqualB("tst:ArrayData", DBR_USHORT, 5, expected.size(), expected.data()); - }, - []() { - clientContext.put("test:slowmo.PROC").set("value", 0).pvRequest("record[block=true]").exec()->wait(5.0); - testdbGetFieldEqualB("test:slowmo", DBR_DOUBLE, 1.0); - }, - []() { - clientContext.put("test:longlongExample").set("value", 0x987654321ull).exec()->wait(5.0); - testdbGetFieldEqualB("test:longlongExample", DBR_UINT64, 0x987654321ull); - }, - []() { - clientContext.put("test:procCounter.HIGH").set("value", 0).pvRequest("record[process=true]").exec() - ->wait(5.0); - testdbGetFieldEqualB("test:procCounter", DBR_DOUBLE, 1.0); - }, - []() { - clientContext.put("test:procCounter.HIGH").set("value", 0).pvRequest("record[process=false]").exec() - ->wait(5.0); - testdbGetFieldEqualB("test:procCounter", DBR_DOUBLE, 1.0); - }, - []() { - clientContext.put("test:procCounter.HIGH").set("value", 0).pvRequest("record[process=passive]").exec() - ->wait(5.0); - testdbGetFieldEqualB("test:procCounter", DBR_DOUBLE, 2.0); - }, - []() { - epicsEvent event; - - // Subscribe for changes to PV - auto pvName = "test:longExample"; - auto subscription = subscribe(event, pvName); - - clientContext.hurryUp(); - - // Wait for initial update - waitForUpdate(subscription, event); - - uint64_t expectedValue = 10L; - clientContext.put(pvName) - .set("value", expectedValue) - .exec() - ->wait(5.0); - - auto updatedValue = waitForUpdate(subscription, event); - if (updatedValue) { - auto actualValue = updatedValue["value"].as(); - testEqB(expectedValue, actualValue); - } - subscription->cancel(); - }, - []() { - epicsEvent event; - - // Subscribe for changes to Group PV - auto pvGroupName = "test:structExample"; - auto subscription = subscribe(event, pvGroupName); - - clientContext.hurryUp(); - waitForUpdate(subscription, event); - testDiag("Got Initial Update!"); - - auto pvName = "test:longExample"; - uint64_t expectedValue = 12L; - clientContext.put(pvName) - .set("value", expectedValue) - .exec() - ->wait(5.0); - testDiag("Issued Put!"); - - auto subFieldName = "sa[0].long.value"; - auto updatedValue = waitForUpdate(subscription, event); - testShow()<<"Event!!\n"<(); - testEqB(expectedValue, actualValue); - } - subscription->cancel(); - }, - []() { - epicsEvent event; - - // Subscribe for changes to Group PV - auto pvGroupName = "test:tableExample"; - auto subscription = subscribe(event, pvGroupName); - - clientContext.hurryUp(); - waitForUpdate(subscription, event); - testDiag("Got Initial Update!"); - - auto pvName = "test:vectorExampleD2"; - shared_array expectedValue({ 3.1, 3.2, 3.3, 3.4, 3.5 }); - clientContext.put(pvName).build([&expectedValue](Value&& prototype) -> Value { - auto putVal = prototype.cloneEmpty(); - putVal["value"] = expectedValue; - return putVal; - }) - .exec()->wait(5.0); - - testDiag("Issued Put!"); - auto subFieldName = "value.B"; - auto updatedValue = waitForUpdate(subscription, event); - if (updatedValue) { - auto actualValue = updatedValue[subFieldName].as>(); - testArrEq(expectedValue, actualValue); - } - subscription->cancel(); - }, - []() { - epicsEvent event; - - // Subscribe for changes to Group PV - auto pvGroupName = "test:structExample2"; - auto subscription = subscribe(event, pvGroupName); - - clientContext.hurryUp(); - waitForUpdate(subscription, event); - testDiag("Got Initial Update!"); - - auto pvName = "test:longExample"; - uint64_t expectedValue = 13L; - clientContext.put(pvName) - .set("value", expectedValue) - .exec() - ->wait(5.0); - testDiag("Issued Put!"); - - auto subFieldName = "sa[0].any"; - auto updatedValue = waitForUpdate(subscription, event); - if (updatedValue) { - auto actualValue = updatedValue[subFieldName].as(); - testEqB(expectedValue, actualValue); - } - subscription->cancel(); - }, - []() { - epicsEvent event; - - // Subscribe for changes to Group PV - auto pvGroupName = "test:tableExample2"; - auto subscription = subscribe(event, pvGroupName); - - clientContext.hurryUp(); - waitForUpdate(subscription, event); - testDiag("Got Initial Update!"); - - auto pvName = "test:vectorExampleD2"; - shared_array expectedValue({ 3.1, 3.2, 3.3, 3.4, 3.5 }); - clientContext.put(pvName).build([&expectedValue](Value&& prototype) -> Value { - auto putVal = prototype.cloneEmpty(); - putVal["value"] = expectedValue; - return putVal; - }) - .exec()->wait(5.0); - testDiag("Issued Put!"); - - auto subFieldName = "value.jB"; - auto updatedValue = waitForUpdate(subscription, event); - if (updatedValue) { - auto actualValue = updatedValue[subFieldName].as>(); - testArrEq(expectedValue, actualValue); - } - subscription->cancel(); - }, - []() { - // Reset the link field to avoid double updates - testdbPutFieldOk("test:vectorExampleD2.FLNK", DBR_STRING, ""); - - epicsEvent event; - - // Subscribe for changes to Group PV - auto pvGroupName = "test:tableExample"; - auto subscription = subscribe(event, pvGroupName); - - clientContext.hurryUp(); - waitForUpdate(subscription, event); - testDiag("Got Initial Update!"); - - auto pvName = "test:tableExample"; - shared_array expectedA({ 4.1, 4.2, 4.3, 4.4, 4.5 }); - shared_array expectedB({ 5.1, 5.2, 5.3, 5.4, 5.5 }); - - clientContext.put(pvName).build([&expectedA, &expectedB](Value&& prototype) -> Value { - auto putVal = prototype.cloneEmpty(); - putVal["value.A"] = expectedA; - putVal["value.B"] = expectedB; - return putVal; - }) - .exec()->wait(5.0); - - testDiag("Issued Put!"); - auto updatedValue = waitForUpdate(subscription, event); - if (updatedValue) { - auto actualAValue = updatedValue["value.A"].as>(); - auto actualBValue = updatedValue["value.B"].as>(); - testArrEq(expectedA, actualAValue); - testArrEq(expectedB, actualBValue); - } - subscription->cancel(); - }, -}; - -/** - * Test runner - * - * @return overall test status - */ -MAIN(testioc) { - auto testNum = 0; - testPlan((int)tests.size() + 2); - testSetup(); - testdbPrepare(); - - // Run tests - for (auto& test: tests) { - if (testNum++) { - printf("#├──────────────────────────────────────────────────────────────────────┤\n"); - } else { - printf("#┌──────────────────────────────────────────────────────────────────────┐\n"); - } - try { - test(); - } catch (const std::exception& e) { - testFail("Test failed with unexpected exception: %s\n", e.what()); - } - } - printf("#└──────────────────────────────────────────────────────────────────────┘"); - - testIocShutdownOk(); - testdbCleanup(); - - return testDone(); -} - -//static void testDbLoadGroupOk(const char* file) { -// testOk(!pvxs::ioc::dbLoadGroup(file), "%s scheduled to be loaded during IocInit()", file); -//} - -static void boxLeft() { -} - -/** - * Subscribe to a given pv using the specified epics event for synchronization - * - * @param event epics event - * @param pvName the pvName to subscribe to - * @return the subscription - */ -static std::shared_ptr subscribe(epicsEvent& event, const char* pvName) { - return clientContext.monitor(pvName) - .maskConnected(true) - .maskDisconnected(true) - .event([&event, pvName](client::Subscription& subscription) { - testDiag("%s update event occurred", pvName); - event.signal(); - }) - .exec(); -} - -/** - * Wait for up to 5 seconds for an update to the specified subscription on the epics event - * - * @param subscription the subscription - * @param event the epics event - * @return the updated value or an empty value if the subscription timed out - */ -static Value waitForUpdate(const std::shared_ptr& subscription, epicsEvent& event) { - while (true) { - if (auto value = subscription->pop()) { - return value; - } else if (!event.wait(5.0)) { - testFail("timeout waiting for event for %s", subscription->name().c_str()); - return {}; - } - } -} - diff --git a/test/testioc.db b/test/testioc.db deleted file mode 100644 index 22b6e43..0000000 --- a/test/testioc.db +++ /dev/null @@ -1,110 +0,0 @@ -record(ai, "$(user):aiExample") -{ - field(DESC, "Analog input") - field(EGU, "Counts") - field(EGUF, "10") - field(HHSV, "MAJOR") - field(HIGH, "6") - field(HIHI, "8") - field(HOPR, "10") - field(HSV, "MINOR") - field(INP, "$(user):calcExample.VAL NPP NMS") - field(LLSV, "MAJOR") - field(LOLO, "2") - field(LOPR, "0") - field(LOW, "4") - field(LSV, "MINOR") - field(PREC, "2") - field(RVAL, "1234") - field(SEVR, "2") - field(STAT, "1") - field(VAL, "42.2") - field(FLNK,"$(user):structExampleSave") -} -record(calc, "$(user):calcExample") -{ - field(DESC, "Counter") -# field(SCAN,"$(SCAN=)") - field(FLNK, "$(user):aiExample") - field(CALC, "(A +#include +#include +#include +#include + +#include +#include +#include + +class TestIOC { + bool running = false; +public: + TestIOC() { + testdbPrepare(); + } + void init() { + if(!running) { + testIocInitOk(); + running = true; + } + } + void shutdown() { + if(running) { + pvxs::ioc::testShutdown(); + testIocShutdownOk(); + } + } + ~TestIOC() { + this->shutdown(); + testdbCleanup(); + } +}; + +struct TestClient : pvxs::client::Context +{ + TestClient() : pvxs::client::Context(pvxs::ioc::server().clientConfig().build()) {} +}; + +struct TestSubscription +{ + epicsEvent evt; + const std::shared_ptr sub; + TestSubscription(pvxs::client::MonitorBuilder b) + :sub(b.event([this](pvxs::client::Subscription& subscription) { + testDiag("%s update event occurred", subscription.name().c_str()); + evt.signal(); + }).exec()) + {} + pvxs::Value waitForUpdate() { + while (true) { + if (auto value = sub->pop()) { + testDiag("Update %s", sub->name().c_str()); + return value; + } else if (!evt.wait(5.0)) { + testFail("timeout waiting for event for %s", sub->name().c_str()); + return {}; + } + } + } + void testEmpty() { + while (true) { + if (auto value = sub->pop()) { + testTrue(false)<<" Unexpected update for "<name()<<"\n" + <name().c_str()); + return; + } + } + } +}; + +template +void testFldEq(const pvxs::Value& top, const char* fldname, const T& expect) +{ + if(auto fld = top[fldname]) { + T actual; + if(fld.as(actual)) { + testEq(expect, actual); + } else { + testFalse(false)<<" unable to convert "< %ld", pv, dbrType, count, status); + +done: + if(chan) + dbChannelDelete(chan); +} +#endif + +#endif // TESTIOC_H diff --git a/test/testioc.json b/test/testioc.json deleted file mode 100644 index 0a7fd56..0000000 --- a/test/testioc.json +++ /dev/null @@ -1,73 +0,0 @@ -/* - * This file defines two groups equivalent to the groups defined in testiocg.db - * Dependencies: testioc.db - */ - -/* - * Group test:tableExample2 - * Use +id mapping: to present as a epics NTTable - * with two columns of doubles taken from $(user):vectorExampleD1 and $(user):vectorExampleD2 - * Get metadata (alarms, etc) from $(user):vectorExampleD1 - */ -{ - "test:tableExample2": { - "+id": "epics:nt/NTTable:1.0", - "": { - "+type": "meta", - "+channel": "test:vectorExampleD1.VAL" - }, - "labels": { - "+channel": "test:groupExampleAS.VAL", - "+type": "plain" - }, - "value.jA": { - "+channel": "test:vectorExampleD1.VAL", - "+type": "plain", - "+putorder": 1 - }, - "value.jB": { - "+channel": "test:vectorExampleD2.VAL", - "+type": "plain", - "+putorder": 1 - }, - "_save": { - "+channel": "test:groupExampleSave.VAL", - "+type": "proc", - "+putorder": 2, - "+trigger": "*" - } - }, - /* - * Group test:structExample2 - * This is an example of a structure that does not correspond to an EPICS normative type - */ - "test:structExample2": { - "+atomic": false, - "ai": { - "+channel": "test:aiExample.VAL", - "+putorder": 0 - }, - "calc": { - "+channel": "test:calcExample.VAL", - "+type": "plain", - "+putorder": 1 - }, - "string": { - "+channel": "test:stringExample.VAL", - "+putorder": 0 - }, - "array": { - "+channel": "test:arrayExample.VAL", - "+putorder": 0 - }, - "sa[0].any": { - "+channel": "test:longExample.VAL", - "+type": "any", - "+putorder": 1 - }, - "sa[1].enum": { - "+channel": "test:enumExample.VAL", - "+putorder": 0 - } - } -} diff --git a/test/testiocg.db b/test/testiocg.db deleted file mode 100644 index b3e6b75..0000000 --- a/test/testiocg.db +++ /dev/null @@ -1,120 +0,0 @@ -# This file defines two groups equivalent to the groups defined in testioc.json -# Dependencies: testioc.db - -# Group $(user):tableExample -# Use +id mapping: to present as a epics NTTable -# with two columns of doubles taken from $(user):vectorExampleD1 and $(user):vectorExampleD2 -# Get metadata (alarms, etc) from $(user):vectorExampleD1 -record(aai, "$(user):groupExampleAS") { - field(FTVL, "STRING") - field(NELM, "2") - field(INP , {const:["Column A", "Column B"]}) - info(Q:group, { - "$(user):tableExample":{ - +id:"epics:nt/NTTable:1.0", - +atomic:false, - "labels":{+type:"plain", +channel:"VAL"} - } - }) - field(TPRO, "1") -} - -record(waveform, "$(user):vectorExampleD1") { - field(FTVL, "DOUBLE") - field(NELM, "10") - info(Q:group, { - "$(user):tableExample":{ - "":{+type:"meta", +channel:"VAL"}, - "value.A":{+type:"plain", +channel:"VAL", +putorder:1} - } - }) - field(INP, {const:[10,20,30,40,50]}) - field(TPRO, "1") -} - -record(waveform, "$(user):vectorExampleD2") { - field(FTVL, "DOUBLE") - field(NELM, "10") - info(Q:group, { - "$(user):tableExample":{ - "value.B":{+type:"plain", +channel:"VAL", +putorder:1} - } - }) - field(INP, {const:[1.1,2.2,3.3,4.4,5.5]}) - field(TPRO, "1") - field(FLNK, "$(user):groupExampleSave") -} - -record(longout, "$(user):groupExampleSave") { - field(MDEL, "-1") # ensure we always trigger group monitor - info(Q:group, { - "$(user):tableExample":{ - "_save":{+type:"proc", - +channel:"VAL", - +putorder:2, - +trigger:"*"} - } - }) - field(TPRO, "1") -} - -# Group $(user):structExample -# This is an example of a structure that does not correspond to an EPICS normative type -record("*", "$(user):aiExample") { - info(Q:group, { - "$(user):structExample":{ - "ai":{+channel:"VAL", +putorder:0} - } - }) -} -record("*", "$(user):calcExample") { - info(Q:group, { - "$(user):structExample":{ - "calc":{+channel:"VAL", +putorder:1} - } - }) -} - -record("*", "$(user):stringExample") { - info(Q:group, { - "$(user):structExample":{ - "string":{+channel:"VAL", +putorder:0} - } - }) -} - -record("*", "$(user):arrayExample") { - info(Q:group, { - "$(user):structExample":{ - "array":{+channel:"VAL", +putorder:0} - } - }) -} - -record("*", "$(user):longExample") { - info(Q:group, { - "$(user):structExample":{ - "sa[0].long":{+channel:"VAL", +putorder:0} - } - }) - field(FLNK, "$(user):structExampleSave") -} - -record("*", "$(user):enumExample") { - info(Q:group, { - "$(user):structExample":{ - "sa[0].enum":{+channel:"VAL", +putorder:0} - } - }) -} - -record(longout, "$(user):structExampleSave") { - info(Q:group, { - "$(user):structExample":{ - "_save":{+type:"proc", - +channel:"VAL", - +putorder:2, - +trigger:"*"} - } - }) -} diff --git a/test/testqgroup.cpp b/test/testqgroup.cpp new file mode 100644 index 0000000..4bbdc06 --- /dev/null +++ b/test/testqgroup.cpp @@ -0,0 +1,712 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "testioc.h" +#include "utilpvt.h" + +extern "C" { +extern int testioc_registerRecordDeviceDriver(struct dbBase*); +} + +using namespace pvxs; + +namespace { + +std::atomic testTimeSec{12345678}; + +int testTimeCurrent(epicsTimeStamp *pDest) +{ + pDest->secPastEpoch = testTimeSec; + pDest->nsec = 102030; + return 0; +} + +void checkUTAG(Value& v, int32_t expect, const char *fld="timeStamp.userTag") +{ +#ifdef DBR_UTAG + auto utag = v[fld]; + int32_t tag = -1; + if(!utag.isMarked() || (tag = utag.as())!=expect) + testFail("userTag not set (%d != %d)", int(expect), int(tag)); + utag.tryFrom(0); + utag.unmark(); +#endif +} + +void testTable() +{ + testDiag("%s", __func__); + TestClient ctxt; + + auto val(ctxt.get("tbl:Tbl").exec()->wait(5.0)); + checkUTAG(val, 0); + testStrEq(std::string(SB()<wait(5.0); + checkUTAG(val, 0); + testStrEq(std::string(SB()< colA({1.0, 2.0, 3.0}); + shared_array colB({4.0, 5.0, 6.0}); + testTimeSec++; + ctxt.put("tbl:Tbl").set("value.A", colA).set("value.B", colB).exec()->wait(5.0); + + val = sub.waitForUpdate(); + checkUTAG(val, 0); + testStrEq(std::string(SB()<wait(5.0); + checkUTAG(val, 0); + testStrEq(std::string(SB()<wait(5.0)); + checkUTAG(val, 0); + testStrEq(std::string(SB()<wait(5.0); + testdbGetFieldEqual("enm:ENUM:INDEX", DBR_LONG, 0); + + // ntenum.db defines no +trigger so only implied self-trigger. + // aka. not timeStamp expected + val = sub.waitForUpdate(); + testStrEq(std::string(SB()<wait(5.0)); + checkUTAG(val, 0); + checkUTAG(val, 0, "x.timeStamp.userTag"); + testStrEq(std::string(SB()< uint16_t = 0\n" + "attribute[0].alarm.severity int32_t = 0\n" + "attribute[0].alarm.status int32_t = 0\n" + "attribute[0].alarm.message string = \"\"\n" + "attribute[0].timeStamp.secondsPastEpoch int64_t = 0\n" + "attribute[0].timeStamp.nanoseconds int32_t = 0\n" + "attribute[0].timeStamp.userTag int32_t = 0\n" + "attribute[1] struct\n" + "attribute[1].name string = \"\"\n" + "attribute[1].value any\n" + "attribute[1].value-> uint16_t = 0\n" + "attribute[1].alarm.severity int32_t = 3\n" + "attribute[1].alarm.status int32_t = 2\n" + "attribute[1].alarm.message string = \"UDF\"\n" + "attribute[1].timeStamp.secondsPastEpoch int64_t = 631152000\n" + "attribute[1].timeStamp.nanoseconds int32_t = 0\n" + "attribute[1].timeStamp.userTag int32_t = 0\n" + "dimension struct[]\n" + "dimension[0] struct\n" + "dimension[0].size int32_t = 100\n" + "dimension[1] struct\n" + "dimension[1].size int32_t = 100\n" + "value any\n" + "value-> uint16_t[] = {10000}[0, 655, 1310, 1966, 2621, 3276, ...]\n" + "x.alarm.severity int32_t = 0\n" + "x.alarm.status int32_t = 0\n" + "x.alarm.message string = \"\"\n" + "x.timeStamp.secondsPastEpoch int64_t = 643497678\n" + "x.timeStamp.nanoseconds int32_t = 102030\n"); + + TestSubscription sub(ctxt.monitor("img:Array2")); + val = sub.waitForUpdate(); + checkUTAG(val, 0); + checkUTAG(val, 0, "x.timeStamp.userTag"); + testStrEq(std::string(SB()< uint16_t = 0\n" + "attribute[0].alarm.severity int32_t = 0\n" + "attribute[0].alarm.status int32_t = 0\n" + "attribute[0].alarm.message string = \"\"\n" + "attribute[0].timeStamp.secondsPastEpoch int64_t = 0\n" + "attribute[0].timeStamp.nanoseconds int32_t = 0\n" + "attribute[0].timeStamp.userTag int32_t = 0\n" + "attribute[1] struct\n" + "attribute[1].name string = \"\"\n" + "attribute[1].value any\n" + "attribute[1].value-> uint16_t = 0\n" + "attribute[1].alarm.severity int32_t = 3\n" + "attribute[1].alarm.status int32_t = 2\n" + "attribute[1].alarm.message string = \"UDF\"\n" + "attribute[1].timeStamp.secondsPastEpoch int64_t = 631152000\n" + "attribute[1].timeStamp.nanoseconds int32_t = 0\n" + "attribute[1].timeStamp.userTag int32_t = 0\n" + "dimension struct[]\n" + "dimension[0] struct\n" + "dimension[0].size int32_t = 100\n" + "dimension[1] struct\n" + "dimension[1].size int32_t = 100\n" + "value any\n" + "value-> uint16_t[] = {10000}[0, 655, 1310, 1966, 2621, 3276, ...]\n" + "x.alarm.severity int32_t = 0\n" + "x.alarm.status int32_t = 0\n" + "x.alarm.message string = \"\"\n" + "x.timeStamp.secondsPastEpoch int64_t = 643497678\n" + "x.timeStamp.nanoseconds int32_t = 102030\n"); + + testTimeSec++; + testdbPutFieldOk("img:ArrayData_.PROC", DBR_LONG, 0); + + val = sub.waitForUpdate(); + checkUTAG(val, 0); + checkUTAG(val, 0, "x.timeStamp.userTag"); + testStrEq(std::string(SB()< uint16_t = 0\n" + "attribute[0].alarm.severity int32_t = 0\n" + "attribute[0].alarm.status int32_t = 0\n" + "attribute[0].alarm.message string = \"\"\n" + "attribute[0].timeStamp.secondsPastEpoch int64_t = 0\n" + "attribute[0].timeStamp.nanoseconds int32_t = 0\n" + "attribute[0].timeStamp.userTag int32_t = 0\n" + "attribute[1] struct\n" + "attribute[1].name string = \"\"\n" + "attribute[1].value any\n" + "attribute[1].value-> uint16_t = 0\n" + "attribute[1].alarm.severity int32_t = 3\n" + "attribute[1].alarm.status int32_t = 2\n" + "attribute[1].alarm.message string = \"UDF\"\n" + "attribute[1].timeStamp.secondsPastEpoch int64_t = 631152000\n" + "attribute[1].timeStamp.nanoseconds int32_t = 0\n" + "attribute[1].timeStamp.userTag int32_t = 0\n" + "dimension struct[]\n" + "dimension[0] struct\n" + "dimension[0].size int32_t = 100\n" + "dimension[1] struct\n" + "dimension[1].size int32_t = 100\n" + "value any\n" + "value-> uint16_t[] = {10000}[0, 655, 1310, 1966, 2621, 3276, ...]\n" + "x.alarm.severity int32_t = 0\n" + "x.alarm.status int32_t = 0\n" + "x.alarm.message string = \"\"\n" + "x.timeStamp.secondsPastEpoch int64_t = 643497681\n" + "x.timeStamp.nanoseconds int32_t = 102030\n"); + + sub.testEmpty(); + + { + shared_array arr({1, 2, 3}); + ctxt.put("img:Array").set("value", arr).pvRequest("record[process=false]").exec()->wait(1111115.0); + testdbGetArrFieldEqual("img:ArrayData", DBR_LONG, arr.size(), arr.size(), arr.data()); + } +} + +void testIQ() +{ + testDiag("%s", __func__); + TestClient ctxt; + + auto val(ctxt.get("iq:iq").exec()->wait(5.0)); + checkUTAG(val, 4, "I.timeStamp.userTag"); + checkUTAG(val, 4, "Q.timeStamp.userTag"); + testStrEq(std::string(SB()<wait(5.0)); + testStrEq(std::string(SB()< + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "testioc.h" +#include "utilpvt.h" + +extern "C" { +extern int testioc_registerRecordDeviceDriver(struct dbBase*); +} + +using namespace pvxs; + +namespace { + +std::atomic timeSim{true}; +std::atomic testTimeSec{12345678}; + +int testTimeCurrent(epicsTimeStamp *pDest) +{ + if(timeSim) { + pDest->secPastEpoch = testTimeSec; + pDest->nsec = 102030; + return 0; + } else { + return 1; + } +} + +void forceUTAG(const char *rec) +{ + dbCommon *prec = testdbRecordPtr(rec); + dbScanLock(prec); +#ifdef DBR_UTAG + prec->utag = 42; +#endif + dbScanUnlock(prec); +} + +void checkUTAG(Value& v, int32_t expect=42) +{ +#ifdef DBR_UTAG + auto utag = v["timeStamp.userTag"]; + if(!utag.isMarked() || utag.as()!=expect) + testFail("userTag not set"); + utag = 0; + utag.unmark(); +#endif +} + +void testGetScalar() +{ + testDiag("%s", __func__); + TestClient ctxt; + + testdbPutFieldOk("test:ai.PROC", DBF_LONG, 0); + forceUTAG("test:ai"); + + auto val(ctxt.get("test:ai").exec()->wait(5.0)); + checkUTAG(val); + testFldEq(val, "timeStamp.secondsPastEpoch", testTimeSec + POSIX_TIME_AT_EPICS_EPOCH); + testStrEq(std::string(SB()<wait(5.0); + checkUTAG(val); + testStrEq(std::string(SB()<wait(5.0); + checkUTAG(val); + testStrEq(std::string(SB()<wait(5.0); + checkUTAG(val); + testStrEq(std::string(SB()<wait(5.0); + checkUTAG(val); +#if EPICS_VERSION_INT < VERSION_INT(3, 16, 0, 0) + if(val["value"].as()=="0") + val["value"] = ""; +#endif + testStrEq(std::string(SB()<(ctxt.get("test:this:is:a:really:really:long:record:name.NAME").exec()->wait(5.0), + "value", "test:this:is:a:really:really:long:recor"); + + testFldEq(ctxt.get("test:this:is:a:really:really:long:record:name.NAME$").exec()->wait(5.0), + "value", "test:this:is:a:really:really:long:record:name"); + + testFldEq(ctxt.get("test:src.INP").exec()->wait(5.0), + "value", "test:this:is:a:really:really:long:recor"); + + testFldEq(ctxt.get("test:src.INP$").exec()->wait(5.0), + "value", "test:this:is:a:really:really:long:record:name NPP NMS"); + + testdbPutFieldOk("test:nsec.PROC", DBF_LONG, 0); + forceUTAG("test:nsec"); // forced value should be ignored + + val = ctxt.get("test:nsec").exec()->wait(5.0); +#ifdef DBR_UTAG + testFldEq(val, "timeStamp.userTag", 142); + val["timeStamp.userTag"].unmark(); +#else + testSkip(1, "not UTAG"); +#endif + testStrEq(std::string(SB()<wait(5.0)); + checkUTAG(val); + testStrEq(std::string(SB()<wait(5.0); + checkUTAG(val); + testStrEq(std::string(SB()<wait(5.0); + checkUTAG(val); + testStrEq(std::string(SB()<wait(5.0); + testStrEq(std::string(SB()<wait(5.0); + testdbGetFieldEqual("test:ai", DBF_DOUBLE, 53.2); + + ctxt.put("test:ai.DESC").set("value", "testing").exec()->wait(5.0); + testdbGetFieldEqual("test:ai.DESC", DBF_STRING, "testing"); + + ctxt.put("test:bo").set("value.index", 1).exec()->wait(5.0); + testdbGetFieldEqual("test:bo", DBF_STRING, "One"); + + { + shared_array arr({1, -3, 5, -7}); + ctxt.put("test:wf:i32").set("value", arr).exec()->wait(5.0); + testdbGetArrFieldEqual("test:wf:i32", DBR_LONG, arr.size(), arr.size(), arr.data()); + } + { + shared_array arr({2.0, 3.2, 5.6}); + ctxt.put("test:wf:f64").set("value", arr).exec()->wait(5.0); + testdbGetArrFieldEqual("test:wf:f64", DBR_DOUBLE, arr.size(), arr.size(), arr.data()); + } + { + shared_array arr({"x", "why", "that last one"}); + ctxt.put("test:wf:s").set("value", arr).exec()->wait(5.0); + + char str[MAX_STRING_SIZE*3u] = {}; + strcpy(&str[MAX_STRING_SIZE*0u], "x"); + strcpy(&str[MAX_STRING_SIZE*1u], "why"); + strcpy(&str[MAX_STRING_SIZE*2u], "that last one"); + testdbGetArrFieldEqual("test:wf:s", DBR_STRING, 3, 3, str); + } + + testdbGetFieldEqual("test:ai2.INP", DBF_STRING, "test:ai NPP NMS"); + + ctxt.put("test:ai2.INP").set("value", "").exec()->wait(5.0); + testdbGetFieldEqual("test:ai2.INP", DBF_STRING, ""); + + ctxt.put("test:ai2.INP").set("value", "test:ai").exec()->wait(5.0); + testdbGetFieldEqual("test:ai2.INP", DBF_STRING, "test:ai NPP NMS"); + + ctxt.put("test:ai2.INP$").set("value", "test:this:is:a:really:really:long:record:name").exec()->wait(5.0); + testdbGetFieldEqual("test:ai2.INP", DBF_STRING, "test:this:is:a:really:really:long:recor"); + { + const char expect[] = "test:this:is:a:really:really:long:record:name NPP NMS"; + testdbGetArrFieldEqual("test:ai2.INP$", DBR_CHAR, NELEMENTS(expect), NELEMENTS(expect), expect); + } + + ctxt.put("test:ai2.INP$").set("value", "").exec()->wait(5.0); + testdbGetFieldEqual("test:ai2.INP", DBF_STRING, ""); + + try{ + ctxt.put("test:ai.STAT").set("value.index", 1).exec()->wait(5.0); + testFail("test:ai.STAT was writable"); + }catch(pvxs::client::RemoteError& e){ + std::string msg(e.what()); + testTrue(msg.find("noMod")!=msg.npos || msg.find("511")!=msg.npos) + <<" expected RemoteError: "<wait(5.0); + testFail("test:ro was writable"); + }catch(pvxs::client::RemoteError& e){ + testStrEq(e.what(), "Put not permitted"); + } + + try{ + ctxt.put("test:disp").set("value", 42).exec()->wait(5.0); + testFail("test:disp was writable"); + }catch(pvxs::client::RemoteError& e){ + testStrMatch(".*Field Disabled.*", e.what()); + } +} + +void testGetPut64() +{ +#ifdef DBR_UINT64 + testDiag("%s", __func__); + TestClient ctxt; + + { + const epicsUInt64 dbl[] = {11111111111111111, 222222222222222, 3333333333333}; + testdbPutArrFieldOk("test:wf:u64", DBF_UINT64, NELEMENTS(dbl), dbl); + } + + testdbPutFieldOk("test:i64.PROC", DBF_LONG, 0); + forceUTAG("test:i64"); + forceUTAG("test:wf:u64"); + + auto val(ctxt.get("test:i64").exec()->wait(5.0)); + checkUTAG(val); + testFldEq(val, "value", 12345678901234); + + val = ctxt.get("test:wf:u64").exec()->wait(5.0); + checkUTAG(val); + testStrEq(std::string(SB()<("value", 12345678901230).exec()->wait(5.0); + testdbGetFieldEqual("test:i64", DBF_INT64, 12345678901230ll); + + { + shared_array arr({5555555555555555, 66666666666666}); + ctxt.put("test:wf:u64").set("value", arr).exec()->wait(5.0); + testdbGetArrFieldEqual("test:wf:u64", DBR_UINT64, arr.size(), arr.size(), arr.data()); + } + +#else // DBR_UINT64 + testSkip(6, "No INT64"); +#endif // DBR_UINT64 +} + +void testPutProc() +{ + testDiag("%s", __func__); + TestClient ctxt; + + testdbGetFieldEqual("test:counter", DBF_LONG, 0); + + // assume calcRecord has HIGH marked pp(TRUE), and LOPR marked pp(FALSE) + + ctxt.put("test:counter.LOPR").set("value", 0).exec()->wait(5.0); + testdbGetFieldEqual("test:counter", DBF_LONG, 0); + + ctxt.put("test:counter.LOPR").set("value", 0).pvRequest("record[process=passive]").exec()->wait(5.0); + testdbGetFieldEqual("test:counter", DBF_LONG, 0); + + ctxt.put("test:counter.LOPR").set("value", 0).pvRequest("record[process=false]").exec()->wait(5.0); + testdbGetFieldEqual("test:counter", DBF_LONG, 0); + + ctxt.put("test:counter.LOPR").set("value", 0).pvRequest("record[process=true]").exec()->wait(5.0); + testdbGetFieldEqual("test:counter", DBF_LONG, 1); +} + +void testPutLog() +{ + testDiag("%s", __func__); + TestClient ctxt; + + static std::ostringstream messages; + struct AsLog { + const asTrapWriteId id; + static + void message(asTrapWriteMessage *pmessage,int after) + { + // caPutLog assumes "serverSpecific" is always available, and a dbChannel* + // (or dbAddr* with 3.14) + auto *pchan = (dbChannel*)pmessage->serverSpecific; + + char val[MAX_STRING_SIZE+1u]; + + dbChannelGetField(pchan, DBR_STRING, val, nullptr, nullptr, nullptr); + val[MAX_STRING_SIZE] = '\0'; + + if(!after) { + // for repeatability, don't include host and user + messages<<"host:user "< "<wait(5.0); + ctxt.put("test:log").set("value.index", 0).exec()->wait(5.0); + ctxt.put("test:log").set("value.index", 0).exec()->wait(5.0); + + auto log(messages.str()); + + testStrEq(log, + "host:user test:log Something -> Else\n" + "host:user test:log Else -> Something\n" + "host:user test:log Something -> Something\n"); +} + +void testPutBlock() +{ +#if EPICS_VERSION_INT >= VERSION_INT(3, 16, 0, 1) + testDiag("%s", __func__); + TestClient ctxt; + + testdbGetFieldEqual("test:slowmo", DBR_DOUBLE, 0.0); + + auto start(epicsTime::getCurrent()); + ctxt.put("test:slowmo.PROC").set("value", 0).pvRequest("record[block=true]").exec()->wait(5.0); + auto elapsed(epicsTime::getCurrent()-start); + + testdbGetFieldEqual("test:slowmo", DBR_DOUBLE, 1.0); + testTrue(elapsed>=0.75)<<"time elapsed "<pact); + dbScanUnlock(prec); + if(pact) + break; + testDiag("waiting for slowmo"); + epicsThreadSleep(1.0); + } + } + + testTrue(op->cancel()); + op.reset(); + + // ensure processing still in progress + { + auto prec = testdbRecordPtr("test:slowmo"); + dbScanLock(prec); + auto pact(prec->pact); + dbScanUnlock(prec); + testTrue(pact)<<" still busy"; + } + + testdbGetFieldEqual("test:slowmo", DBR_DOUBLE, 2.0); + +#else + testSkip(7, "dbNotify testing broken on 3.15"); + /* epics-base 3.15 circa a249561677de73e3f174ec8e4478937a7a55a9b2 + * contains ddaa6e4eb6647545db7a43c9b83ca7e2c497f3b8 + * but not a7a87372aab2c086f7ac60db4a5d9e39f08b9f05 + */ +#endif +} + +void testMonitorAI(TestClient& ctxt) +{ + testDiag("%s", __func__); + + TestSubscription sub(ctxt.monitor("test:ai") + .maskConnected(true) + .maskDisconnected(true)); + + auto val(sub.waitForUpdate()); + checkUTAG(val); + testStrEq(std::string(SB()<