diff --git a/Makefile b/Makefile index 609c46e..d32169b 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,9 @@ tools_DEPEND_DIRS = src DIRS += ioc ioc_DEPEND_DIRS = src +DIRS += qsrv +qsrv_DEPEND_DIRS = src ioc + DIRS += test test_DEPEND_DIRS = src ioc diff --git a/ioc/Makefile b/ioc/Makefile index f2e64b8..43f9a3f 100644 --- a/ioc/Makefile +++ b/ioc/Makefile @@ -1,7 +1,15 @@ +# +# 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 +# + TOP=.. include $(TOP)/configure/CONFIG -# cfg/ sometimes isn't correctly included due to a Base bug +# cfg/ sometimes isn't correctly included due to an issue in epics-base # so we do here (maybe again) as workaround include $(TOP)/configure/CONFIG_PVXS_MODULE include $(TOP)/configure/CONFIG_PVXS_VERSION @@ -18,14 +26,35 @@ DBD += pvxsIoc.dbd INC += pvxs/iochooks.h LIBRARY += pvxsIoc +PROD_LIBS = Com SHRLIB_VERSION = $(PVXS_MAJOR_VERSION).$(PVXS_MINOR_VERSION) +pvxsIoc_SRCS += credentials.cpp +pvxsIoc_SRCS += channel.cpp +pvxsIoc_SRCS += demo.cpp +pvxsIoc_SRCS += dberrormessage.cpp +pvxsIoc_SRCS += field.cpp +pvxsIoc_SRCS += fielddefinition.cpp +pvxsIoc_SRCS += fieldname.cpp +pvxsIoc_SRCS += fieldsubscriptionctx.cpp +pvxsIoc_SRCS += group.cpp +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 LIB_LIBS += pvxs LIB_LIBS += $(EPICS_BASE_IOC_LIBS) - #=========================== include $(TOP)/configure/RULES diff --git a/ioc/channel.cpp b/ioc/channel.cpp new file mode 100644 index 0000000..0278b65 --- /dev/null +++ b/ioc/channel.cpp @@ -0,0 +1,48 @@ +/* + * 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 + * + */ + +#include + +#include "channel.h" +#include "utilpvt.h" + +namespace pvxs { +namespace ioc { +/** + * Construct a group channel from a given db channel name + * + * @param name the db channel name + */ +Channel::Channel(const std::string& name) + :pDbChannel(std::shared_ptr(dbChannelCreate(name.c_str()), + [](dbChannel* ch) { + if (ch) { + dbChannelDelete(ch); + } + })) { + if (pDbChannel) { + prepare(); + } +} + +/** + * Internal function to prepare the dbChannel for operation by opening it + */ +void Channel::prepare() { + if (!pDbChannel) { + throw std::invalid_argument(SB() << "NULL channel while opening group channel"); + } + if (dbChannelOpen(pDbChannel.get())) { + throw std::invalid_argument(SB() << "Failed to open group channel " << dbChannelName(pDbChannel)); + } +} + + +} // pvxs +} // ioc diff --git a/ioc/channel.h b/ioc/channel.h new file mode 100644 index 0000000..cf37e65 --- /dev/null +++ b/ioc/channel.h @@ -0,0 +1,92 @@ + +/* + * 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_CHANNEL_H +#define PVXS_CHANNEL_H + +#include +#include + +#include + +namespace pvxs { +namespace ioc { + +/** + * This class encapsulates a shared pointer to a dbChannel but provides constructors + * from string and dbChannel to make its use simpler. It can be used wherever a dbChannel is used. + * As a bonus when constructed with parameters it provides an already open dbChannel. + */ +class Channel { +private: + std::shared_ptr pDbChannel; + void prepare(); + +public: + // This constructor calls dbChannelOpen() + explicit Channel(const std::string& name); + +/** + * Destructor is default because pDbChannel cleans up after itself. + */ + ~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(); + } +/** + * 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 +} // ioc + +#endif //PVXS_CHANNEL_H diff --git a/ioc/credentials.cpp b/ioc/credentials.cpp new file mode 100644 index 0000000..3427b06 --- /dev/null +++ b/ioc/credentials.cpp @@ -0,0 +1,48 @@ +/* + * 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 + * + */ + +#include + +#include "credentials.h" + +namespace pvxs { +namespace ioc { + +/** + * eg. + * "username" implies "ca/" prefix + * "krb/principle" + * "role/groupname" + * + * @param clientCredentials + */ + +Credentials::Credentials(const server::ClientCredentials& clientCredentials) { + // Extract host name part (or whole thing if no colon present) + auto pos = clientCredentials.peer.find_first_of(':'); + host = clientCredentials.peer.substr(0, pos); + + // "ca" style credentials + if (clientCredentials.method == "ca") { + pos = clientCredentials.account.find_last_of('/'); + if (pos == std::string::npos) { + cred.emplace_back(clientCredentials.account); + } else { + cred.emplace_back(clientCredentials.account.substr(pos + 1)); + } + } else { + cred.emplace_back(SB() << clientCredentials.method << '/' << clientCredentials.account); + } + + for (const auto& role: clientCredentials.roles()) { + cred.emplace_back(SB() << "role/" << role); + } +} +} // pvxs +} // ioc diff --git a/ioc/credentials.h b/ioc/credentials.h new file mode 100644 index 0000000..00a9eab --- /dev/null +++ b/ioc/credentials.h @@ -0,0 +1,39 @@ +/* + * 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_CREDENTIALS_H +#define PVXS_CREDENTIALS_H + +#include +#include + +#include + +namespace pvxs { +namespace ioc { + +/** + * eg. + * "username" implies "ca/" prefix + * "krb/principle" + * "role/groupname" + */ +class Credentials { +public: + std::vector cred; + std::string host; + explicit Credentials(const server::ClientCredentials& clientCredentials); + Credentials(const Credentials&) = delete; + Credentials(Credentials&&) = default; +}; + +} // pvxs +} // ioc + +#endif //PVXS_CREDENTIALS_H diff --git a/ioc/dbentry.h b/ioc/dbentry.h new file mode 100644 index 0000000..852e262 --- /dev/null +++ b/ioc/dbentry.h @@ -0,0 +1,47 @@ +/* + * 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_DBENTRY_H +#define PVXS_DBENTRY_H + +#include +#include + +#include "dbentry.h" + +namespace pvxs { +namespace ioc { + +/** + * Wrapper class for DBENTRY that is a type that encapsulates an IOC database entry. + */ +class DBEntry { + DBENTRY ent{}; +public: + DBEntry() { + dbInitEntry(pdbbase, &ent); + } + + ~DBEntry() { + dbFinishEntry(&ent); + } + + operator DBENTRY*() { + return &ent; + } + + DBENTRY* operator->() { + return &ent; + } + +}; + +} // ioc +} // pvxs +#endif //PVXS_DBENTRY_H diff --git a/ioc/dberrormessage.cpp b/ioc/dberrormessage.cpp new file mode 100644 index 0000000..94967d4 --- /dev/null +++ b/ioc/dberrormessage.cpp @@ -0,0 +1,36 @@ +/* + * 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 + * + */ + +#include +#include + +#include "dberrormessage.h" + +namespace pvxs { +namespace ioc { + +/** + * Set value of this DBErrorMessage object from the specified database status code + * + * @param dbStatus database command status code + * @return updated DBErrorMessage object + */ +DBErrorMessage& DBErrorMessage::operator=(const long& dbStatus) { + status = dbStatus; + if (!dbStatus) { + message[0] = '\0'; + } else { + errSymLookup(dbStatus, message, sizeof(message)); + message[sizeof(message) - 1] = '\0'; + } + return *this; +} + +} // ioc +} // pvxs diff --git a/ioc/dberrormessage.h b/ioc/dberrormessage.h new file mode 100644 index 0000000..a3380e8 --- /dev/null +++ b/ioc/dberrormessage.h @@ -0,0 +1,60 @@ +/* + * 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_DBERRORMESSAGE_H +#define PVXS_DBERRORMESSAGE_H + +#include +#include + +#include "dberrormessage.h" + +namespace pvxs { +namespace ioc { + +/** + * Wrapper class for status returned from base IOC database commands. + */ +class DBErrorMessage { + long status = 0; + char message[MAX_STRING_SIZE]{}; +public: +/** + * Construct a new DBErrorMessage from a native database command status code + * + * @param dbStatus database command status code + */ + explicit DBErrorMessage(const long& dbStatus = 0) { + (*this) = dbStatus; + } + + DBErrorMessage& operator=(const long& dbStatus); +/** + * bool cast operation returns true if the status indicates a failure + * + * @return returns true if the status indicates a failure + */ + explicit operator bool() const { + return status; + } + +/** + * Return the text of the database status as a string pointer + * + * @return the text of the database status as a string pointer + */ + const char* c_str() const { + return message; + } + +}; + +} // ioc +} // pvxs +#endif //PVXS_DBERRORMESSAGE_H diff --git a/ioc/dbeventcontextdeleter.h b/ioc/dbeventcontextdeleter.h new file mode 100644 index 0000000..cea973e --- /dev/null +++ b/ioc/dbeventcontextdeleter.h @@ -0,0 +1,31 @@ +/* + * 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_DBEVENTCONTEXTDELETER_H +#define PVXS_DBEVENTCONTEXTDELETER_H + +#include +#include + +#include + +namespace pvxs { +namespace ioc { +class DBEventContextDeleter { +public: + void operator()(const dbEventCtx& eventContext) { + db_close_events(eventContext); + } +}; + +typedef std::unique_ptr::type, DBEventContextDeleter> DBEventContext; + +} +} +#endif //PVXS_DBEVENTCONTEXTDELETER_H diff --git a/ioc/dblocker.h b/ioc/dblocker.h new file mode 100644 index 0000000..a450e6b --- /dev/null +++ b/ioc/dblocker.h @@ -0,0 +1,46 @@ +/* + * 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_DBLOCKER_H +#define PVXS_DBLOCKER_H + +#include + +namespace pvxs { +namespace ioc { + +/** + * To lock access to a single DB record. + * Use by creating a new object. It will lock the record referenced by the constructor parameter while the object is + * in scope. + * + * e.g. + * { + * DBLocker F(pDbChannel->addr.precord); // Lock + * IOCSource::put(pDbChannel, ...); + * ... + * } // Unlocked + */ +class DBLocker { +public: + dbCommon* const lock; + explicit DBLocker(dbCommon* L) + :lock(L) { + dbScanLock(lock); + } + + ~DBLocker() { + dbScanUnlock(lock); + } +}; + +} // pvxs +} // ioc + +#endif //PVXS_DBLOCKER_H diff --git a/ioc/dbmanylocker.h b/ioc/dbmanylocker.h new file mode 100644 index 0000000..e933c4a --- /dev/null +++ b/ioc/dbmanylocker.h @@ -0,0 +1,115 @@ +/* + * 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_DBMANYLOCKER_H +#define PVXS_DBMANYLOCKER_H + +#include + +#include +#include + +namespace pvxs { +namespace ioc { + +/** + * A lock that can lock multiple DB records simultaneously. To be used with DBManyLocker + * + * e.g. + * DBManyLock lock = DBManyLock(channels); + */ +class DBManyLock { + +public: + dbLocker* pLocker{}; + +/** + * Empty lock + */ + DBManyLock() + :pLocker(nullptr) { + } + +/** + * Create a many lock from a list of channels + * + * @param channels the list of channels to lock + * @param flags the lock flags to be passed on to dbLockerAlloc() + */ + explicit DBManyLock(const std::vector& channels, unsigned flags = 0) { + pLocker = dbLockerAlloc(channels.data(), channels.size(), flags); + if (!pLocker) { + throw std::invalid_argument("Failed to create locker"); + } + } + +/** + * When the lock goes out of scope, then free the lock + */ + ~DBManyLock() { + if (pLocker) { + dbLockerFree(pLocker); + pLocker = nullptr; + } + } + + explicit operator dbLocker*() const { + return pLocker; + } + + DBManyLock(DBManyLock&& other) noexcept + :pLocker(other.pLocker) { + other.pLocker = nullptr; + } + + DBManyLock& operator=(DBManyLock&& other) noexcept { + if (pLocker) { + dbLockerFree(pLocker); + } + pLocker = other.pLocker; + other.pLocker = nullptr; + return *this; + } + + // Prevent copy construction and assignment + DBManyLock(const DBManyLock&) = delete; + DBManyLock& operator=(const DBManyLock& other) = delete; +}; + +/** + * To lock access to multiple DB records simultaneously. + * Use by creating a new object. It will lock the records locked by the constructor parameter while the object is + * in scope. First you need to create a lock using the DBManyLock(). + * + * e.g. + * { + * DBManyLock lock = DBManyLock(channels); + * DBManyLocker F(lock); // Lock all channels + * for ( auto& pDbChannel: channels ) { + * IOCSource::put(pDbChannel, ...); + * } + * ... + * } // Unlocked + */ +class DBManyLocker { +public: + const DBManyLock& lock; + explicit DBManyLocker(DBManyLock& L) + :lock(L) { + dbScanLockMany(lock.pLocker); + } + ~DBManyLocker() { + dbScanUnlockMany(lock.pLocker); + } +}; + +} // pvxs +} // ioc + +#endif //PVXS_DBMANYLOCKER_H diff --git a/ioc/demo.cpp b/ioc/demo.cpp new file mode 100644 index 0000000..865d560 --- /dev/null +++ b/ioc/demo.cpp @@ -0,0 +1,86 @@ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + +namespace { + +// pi/180 +static const double pi_180 = 0.017453292519943295; + +int dummy; + +long init_spin(waveformRecord* prec) { + if (prec->ftvl == menuFtypeDOUBLE) + prec->dpvt = &dummy; + return 0; +} + +long process_spin(waveformRecord* prec) { + if (prec->dpvt != &dummy) { + (void)recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); + return 0; + } + + const double freq = 360.0 * pi_180 / 100; // rad/sample + double phase = 0; + double* val = static_cast(prec->bptr); + + long ret = dbGetLink(&prec->inp, DBF_DOUBLE, &phase, 0, 0); + if (ret) { + (void)recGblSetSevr(prec, LINK_ALARM, INVALID_ALARM); + return ret; + } + + phase *= pi_180; // deg -> rad + + for (size_t i = 0, N = prec->nelm; i < N; i++) + val[i] = sin(freq * i + phase); + + prec->nord = prec->nelm; + +#ifdef DBRutag + prec->utag = (prec->utag + 1u) & 0x7fffffff; +#endif + + return 0; +} + +long process_utag(longinRecord* prec) { + long status = dbGetLink(&prec->inp, DBR_LONG, &prec->val, 0, 0); +#ifdef DBRutag + prec->utag = prec->val; +#else + (void)recGblSetSevr(prec, COMM_ALARM, INVALID_ALARM); +#endif + return status; +} + +template +struct dset5 { + long count; + long (* report)(int); + long (* init)(int); + long (* init_record)(REC*); + long (* get_ioint_info)(int, REC*, IOSCANPVT*); + long (* process)(REC*); +}; + +dset5 devWfPDBDemo = { 5, 0, 0, &init_spin, 0, &process_spin }; +dset5 devLoPDBUTag = { 5, 0, 0, 0, 0, &process_utag }; + +} // namespace + +extern "C" { +epicsExportAddress(dset, devWfPDBDemo); +epicsExportAddress(dset, devLoPDBUTag); +} diff --git a/ioc/field.cpp b/ioc/field.cpp new file mode 100644 index 0000000..3a5c3b0 --- /dev/null +++ b/ioc/field.cpp @@ -0,0 +1,77 @@ +/* + * 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 + * + */ + +#include +#include + +#include "field.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) { + + if (!fieldName.fieldNameComponents.empty()) { + name = fieldName.fieldNameComponents[0].name; + fullName = std::string(fieldName.to_string()); + + if (fieldName.fieldNameComponents[fieldName.fieldNameComponents.size() - 1].isArray()) { + isArray = true; + } + + } +} + +/** + * Using the field components configured in this Field, walk down from the given value, + * to arrive at the part of the value referenced by this field. + * + * @param valueTarget the given value to search in + * @return the Value referenced by this field within the given value + */ +Value Field::findIn(Value valueTarget) const { + if (!fieldName.empty()) { + for (const auto& component: fieldName.fieldNameComponents) { + valueTarget = valueTarget[component.name]; + if (component.isArray()) { + // Get required array capacity + auto index = component.index; + shared_array constValueArray = valueTarget.as>(); + valueTarget = shared_array(); + shared_array valueArray(constValueArray.thaw()); + auto size = valueArray.size(); + if ((index + 1) > size) { + valueArray.resize(index + 1); + } + + // Put new data into array + auto newElement = valueArray[index]; + if (!newElement) { + // Only allocate new member if it is not already allocated + valueArray[index] = newElement = valueTarget.allocMember(); + } + valueTarget = valueArray.freeze(); + valueTarget = newElement; + } + } + } + return valueTarget; +} + +} // pvxs +} // ioc diff --git a/ioc/field.h b/ioc/field.h new file mode 100644 index 0000000..3fdc036 --- /dev/null +++ b/ioc/field.h @@ -0,0 +1,64 @@ +/* + * 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_FIELD_H +#define PVXS_FIELD_H + +#include +#include +#include +#include + +#include + +#include "dblocker.h" +#include "channel.h" +#include "dbmanylocker.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: + std::string id; // For structure functionality + 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 + + Field(const std::string& stringFieldName, const std::string& stringChannelName, std::string id); + Value findIn(Value valueTarget) const; +}; + +typedef std::vector Fields; + +} // pvxs +} // ioc + +#endif //PVXS_FIELD_H diff --git a/ioc/fieldconfig.h b/ioc/fieldconfig.h new file mode 100644 index 0000000..ea37cb0 --- /dev/null +++ b/ioc/fieldconfig.h @@ -0,0 +1,34 @@ +/* + * 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_FIELDCONFIG_H +#define PVXS_FIELDCONFIG_H + +#include +#include + +namespace pvxs { +namespace ioc { + +/** + * 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; +}; + +typedef std::map FieldConfigMap; + +} // pvxs +} // ioc + +#endif //PVXS_FIELDCONFIG_H diff --git a/ioc/fielddefinition.cpp b/ioc/fielddefinition.cpp new file mode 100644 index 0000000..7de484a --- /dev/null +++ b/ioc/fielddefinition.cpp @@ -0,0 +1,34 @@ +/* + * 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 + * + */ + +#include + +#include "fielddefinition.h" + +namespace pvxs { +namespace ioc { + +/** + * Part of the second pass group configuration processing. This is the constructor for a group field configuration + * object. + * + * @param fieldConfig the first stage field configuration object it will be based on + * @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; +} + +} +} diff --git a/ioc/fielddefinition.h b/ioc/fielddefinition.h new file mode 100644 index 0000000..b74e0c7 --- /dev/null +++ b/ioc/fielddefinition.h @@ -0,0 +1,50 @@ +/* + * 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_FIELDDEFINITION_H +#define PVXS_FIELDDEFINITION_H + +#include +#include +#include + +#include + +#include "fieldconfig.h" + +namespace pvxs { +namespace ioc { + +typedef std::set TriggerNames; + +/** + * Class to store group fields definitions while they are being processed after being read from files into FieldConfig. + */ +class FieldDefinition { +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); + + bool operator<(const FieldDefinition& o) const { + return putOrder < o.putOrder; + } +}; + +typedef std::vector FieldDefinitions; +typedef std::map FieldDefinitionMap; + +} // pvxs +} // ioc +#endif //PVXS_FIELDDEFINITION_H diff --git a/ioc/fieldname.cpp b/ioc/fieldname.cpp new file mode 100644 index 0000000..b7637f1 --- /dev/null +++ b/ioc/fieldname.cpp @@ -0,0 +1,110 @@ +/* + * 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 + * + */ + +#include +#include +#include +#include + +#include "fieldname.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. + * + * This constructor breaks the string on periods and stores each component in the fieldNameComponents member, + * while extracting the array reference where specified. + * @param fieldName + */ +FieldName::FieldName(const std::string& fieldName) { + if (!fieldName.empty()) { + // Split field name on periods + std::stringstream splitter(fieldName); + std::string fieldNamePart; + while (std::getline(splitter, fieldNamePart, '.')) { + if (fieldNamePart.empty()) { + throw std::runtime_error("Empty field component in: " + fieldName); + } + + // If this is an array reference then extract the index + auto endArraySpecifier = fieldNamePart.size(); + if (fieldNamePart[endArraySpecifier - 1] == ']') { + const size_t startArraySpecifier = fieldNamePart.find_last_of('['); + if (startArraySpecifier == std::string::npos) { + throw std::runtime_error("Invalid field array sub-script in : " + fieldName); + } + + auto arrayIndex = fieldNamePart.substr(startArraySpecifier + 1); + long index = 0; + char* endScan; + index = strtol(arrayIndex.c_str(), &endScan, 10); + if (*endScan != ']') { + throw std::runtime_error("Invalid field array sub-script in : " + fieldName); + } + + fieldNameComponents.emplace_back(fieldNamePart.substr(0, startArraySpecifier), index); + } else { + // Otherwise this is a regular field part + fieldNameComponents.emplace_back(fieldNamePart); + } + } + + // If empty then throw an error + if (fieldNameComponents.empty()) { + throw std::runtime_error("Empty field name"); + } + } +} + +/** + * Convert this group field name to a string. + * + * @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 = "/"; + } else { + bool first = true; + for (const auto& fieldNameComponent: fieldNameComponents) { + if (!first) { + fieldName += "."; + } else { + first = false; + } + fieldName += fieldNameComponent.name; + if (fieldNameComponent.isArray()) { + fieldName += "[" + std::to_string((unsigned)fieldNameComponent.index) + "]"; + } + } + } + pad(fieldName, padLength); + return fieldName; +} + +/** + * Utility function to pad given string with spaces + * + * @param stringToPad the string to be padded + * @param padLength the amount of spaces to pad with + */ +static void pad(std::string& stringToPad, const size_t padLength) { + if (padLength > stringToPad.size()) { + stringToPad.insert(stringToPad.size(), padLength - stringToPad.size(), PADDING_CHARACTER); + } +} + +} // pvxs +} // ioc diff --git a/ioc/fieldname.h b/ioc/fieldname.h new file mode 100644 index 0000000..8e499bf --- /dev/null +++ b/ioc/fieldname.h @@ -0,0 +1,107 @@ +/* + * 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_FIELDNAME_H +#define PVXS_FIELDNAME_H + +#include + +#include "fieldnamecomponent.h" + +#define PADDING_CHARACTER ' ' +#define PADDING_WIDTH 15 + +namespace pvxs { +namespace ioc { + +/** + * Implements a group field as a delegate over a vector of group field components. + * Therefore it can be used as a vector with size(), empty(), operator[], back() and swap() methods implemented. + * + * The group field is a vector of group field components. + * + */ +class FieldName { +private: +public: + FieldNameComponents fieldNameComponents; + + explicit FieldName(const std::string& fieldName); + std::string to_string(size_t padLength = 0) const; + +/** + * Show this field name. All components are shown as they were configured. + * + * @param suffix the suffix to add to the field name, defaults to none + */ + void show(const std::string& suffix = {}) const { + printf("%s%s", to_string(PADDING_WIDTH - suffix.size()).c_str(), suffix.c_str()); + } + +/** + * swap delegate + * + * @param o + */ + void swap(FieldName& o) { + fieldNameComponents.swap(o.fieldNameComponents); + } + +/** + * empty delegate + * + * @return + */ + bool empty() const { + return fieldNameComponents.empty() || (fieldNameComponents.size() == 1 && fieldNameComponents[0].name.empty()); + } + +/** + * size delegate + * + * @return + */ + size_t size() const { + return fieldNameComponents.size(); + } + +/** + * back() delegate + * + * @return + */ + const FieldNameComponent& back() const { + return fieldNameComponents.back(); + } + +/** + * operator[] delegate + * + * @param i + * @return + */ + const FieldNameComponent& operator[](size_t i) const { + return fieldNameComponents[i]; + } + +/** + * Get the leaf field name of this field + * + * @return the leaf field name + */ + const std::string& leafFieldName() const { + return fieldNameComponents[fieldNameComponents.size() - 1].name; + } + +}; + +} // pvxs +} // ioc + +#endif //PVXS_FIELDNAME_H diff --git a/ioc/fieldnamecomponent.h b/ioc/fieldnamecomponent.h new file mode 100644 index 0000000..c77431d --- /dev/null +++ b/ioc/fieldnamecomponent.h @@ -0,0 +1,72 @@ +/* + * 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_FIELDNAMECOMPONENT_H +#define PVXS_FIELDNAMECOMPONENT_H + +#include +#include +#include + +namespace pvxs { +namespace ioc { + +/** + * A field component. Fields can be made up of any number of components. e.g. a.b[1].c + * Each of the components (a, b, or c) are represented by a component. + * isArray() determines whether this component is an array of structures or not, by looking + * at the index field. If it is -1 then its not an array of structures otherwise it is a simple scalar + * or an array of scalars. + * + * An array of structures is an array whose elements are themselves structures. + */ +class FieldNameComponent { +/** + * Construct an simple Field name component holder. -1 means not a structure array + */ + FieldNameComponent() + :index((uint32_t)-1) { + } +public: + // the name of this field component + std::string name; + // If this is a structure array then this is the index that is referred to by this field name component. + // -1 means that it is not a structure array + uint32_t index; + +/** + * Construct an Field Name Component from the given name and index + * + * @param name the field name component + * @param index the index of the field name component if the component is an array of structures. Note + * that index will only ever be specified in configuration if this is an array of structures. + */ + explicit FieldNameComponent(std::string name, uint32_t index = (uint32_t)-1) + :name(std::move(name)), index(index) { + } + +/** + * Is this an array of structures. Determines whether this component is an array of structures or not, by looking + * at the index field. If it is -1 then its not an array of structures otherwise it is a simple scalar + * or an array of scalars + * + * @return true if this is an array of structures + */ + bool isArray() const { + return index != (uint32_t)-1; + } + +}; + +typedef std::vector FieldNameComponents; + +} // pvxs +} // ioc + +#endif //PVXS_FIELDNAMECOMPONENT_H diff --git a/ioc/fieldsubscriptionctx.cpp b/ioc/fieldsubscriptionctx.cpp new file mode 100644 index 0000000..9403177 --- /dev/null +++ b/ioc/fieldsubscriptionctx.cpp @@ -0,0 +1,47 @@ +/* + * 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 + * + */ + +#include "fieldsubscriptionctx.h" + +namespace pvxs { +namespace ioc { + +/** + * Called when a client wishes to subscribe to a group. The onSubscribe method calls this method for each + * field within the group. This method will create a new event subscription and attach it to this field + * subscription context. + * + * @param pEventCtx the global event context which references the db event propagation framework + * @param subscriptionCallback reference to a callback function to be called when the field is updated. + * @param selectOptions the selection options to determine events to be monitored. DBE_VALUE | DBE_ALARM | DBE_PROPERTY + * @param forValues true if this should monitor value changes, false for property changes. + */ +void FieldSubscriptionCtx::subscribeField(dbEventCtx pEventCtx, EVENTFUNC (* subscriptionCallback), + unsigned int selectOptions, bool forValues) { + auto& pDbChannel = (forValues ? field->value.channel : field->properties.channel).shared_ptr(); + 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"); + } +} + +} // pvcs +} // ioc diff --git a/ioc/fieldsubscriptionctx.h b/ioc/fieldsubscriptionctx.h new file mode 100644 index 0000000..4f2dc76 --- /dev/null +++ b/ioc/fieldsubscriptionctx.h @@ -0,0 +1,60 @@ +/* + * 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_FIELDSUBSCRIPTIONCTX_H +#define PVXS_FIELDSUBSCRIPTIONCTX_H + +#include + +#include + +#include "dbeventcontextdeleter.h" +#include "group.h" +#include "subscriptionctx.h" + +namespace pvxs { +namespace ioc { + +class GroupSourceSubscriptionCtx; + +/** + * Field subscription context. This object is the user object that is supplied when one of a group subscription's + * fields are updated, and their subscription event is triggered. + * + * It contains a pointer to the group subscription of which it forms a part, as well as the field it is monitoring. + */ +class FieldSubscriptionCtx : public SubscriptionCtx { +public: + GroupSourceSubscriptionCtx* pGroupCtx; + Field* field; + + // Map channel to field index in group.fields + void subscribeField(dbEventCtx pEventCtx, EVENTFUNC (* subscriptionCallback), + unsigned int selectOptions, bool forValues = true); + +/** + * Constructor for a field subscription context takes a field and a group subscription context + * + * @param field the field this subscription context will be used to monitor + * @param groupSourceSubscriptionCtx the group subscription context this is a part of + */ + explicit FieldSubscriptionCtx(Field& field, GroupSourceSubscriptionCtx* groupSourceSubscriptionCtx) + :pGroupCtx(groupSourceSubscriptionCtx), field(&field) { + }; + + FieldSubscriptionCtx(FieldSubscriptionCtx&&) = default; + + FieldSubscriptionCtx(const FieldSubscriptionCtx&) = delete; +}; + +} // pvcs +} // ioc + +#endif //PVXS_FIELDSUBSCRIPTIONCTX_H diff --git a/ioc/group.cpp b/ioc/group.cpp new file mode 100644 index 0000000..3e4262d --- /dev/null +++ b/ioc/group.cpp @@ -0,0 +1,87 @@ +/* + * 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 + * + */ + +#include +#include + +#include "group.h" + +namespace pvxs { +namespace ioc { + +/** + * Show details for this group. + * This displays information to the terminal and is to be used by the IOC command shell + * + * @param level the level of detail to show. + * 0 group names only, + * 1 group names and top level information, + * 2 everything + */ +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", + (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()); + } + + 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)); + } + } + } + } + } +} + +/** + * De-reference the field in the current group by providing the field name. + * + * @param fieldName of the field to be de-referenced + * @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; +} + +} // pvxs +} // ioc diff --git a/ioc/group.h b/ioc/group.h new file mode 100644 index 0000000..e6e4ee5 --- /dev/null +++ b/ioc/group.h @@ -0,0 +1,65 @@ +/* + * 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_GROUP_H +#define PVXS_GROUP_H + +#include + +#include + +#include "dbmanylocker.h" +#include "field.h" + +namespace pvxs { +namespace ioc { + +class ChannelLocks { +public: + std::vector channels; + DBManyLock lock; + ChannelLocks() = default; +}; + +class Group { +private: +public: + std::string name; + Fields fields; + bool atomicPutGet, atomicMonitor; + 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; + Field& operator[](const std::string& fieldName); + + Group(const Group&) = delete; +}; + +// A map of group name to Group +typedef std::map GroupMap; + +} // pvxs +} // ioc + +#endif //PVXS_GROUP_H diff --git a/ioc/groupconfig.h b/ioc/groupconfig.h new file mode 100644 index 0000000..10f3e5e --- /dev/null +++ b/ioc/groupconfig.h @@ -0,0 +1,40 @@ +/* + * 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_GROUPCONFIG_H +#define PVXS_GROUPCONFIG_H + +#include + +#include "fieldconfig.h" + +namespace pvxs { +namespace ioc { +/** + * Class to store the group configuration as it is read in. It is subsequently + * read into the Group Definition class, for intermediate use, before finally Group for runtime use + * Initialise: GroupConfig ==> GroupDefinition ==> Group :Running + */ +class GroupConfig { +public: + bool atomic, atomicIsSet; + std::string structureId; + FieldConfigMap fieldConfigMap; + GroupConfig() + :atomic(true), atomicIsSet(false) { + } +}; + +// A map of group name to GroupConfig +typedef std::map GroupConfigMap; + +} // pvxs +} // ioc + +#endif //PVXS_GROUPCONFIG_H diff --git a/ioc/groupconfigprocessor.cpp b/ioc/groupconfigprocessor.cpp new file mode 100644 index 0000000..213ca6a --- /dev/null +++ b/ioc/groupconfigprocessor.cpp @@ -0,0 +1,1106 @@ +/* + * 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 + * + */ + +#include +#include +#include + +#include + +#include +#include + +#include +#include + +#include "dbentry.h" +#include "groupconfigprocessor.h" +#include "groupdefinition.h" +#include "groupprocessorcontext.h" +#include "iocshcommand.h" +#include "iocsource.h" +#include "utilpvt.h" +#include "yajlcallbackhandler.h" + +namespace pvxs { +namespace ioc { + +DEFINE_LOGGER(_logname, "pvxs.ioc.group.processor"); + +/** + * Parse group configuration that has been defined in db configuration files. + * This involves extracting info fields named "Q:Group" from the database configuration + * and converting them to Group Configuration objects. + */ +void GroupConfigProcessor::loadConfigFromDb() { + // process info blocks named Q:Group to get group configuration + DBEntry dbEntry; + for (long status = dbFirstRecordType(dbEntry); !status; status = dbNextRecordType(dbEntry)) { + for (status = dbFirstRecord(dbEntry); !status; status = dbNextRecord(dbEntry)) { + const char* jsonGroupDefinition = infoField(dbEntry, "Q:group"); + if (jsonGroupDefinition != nullptr) { + auto& dbRecordName(dbEntry->precnode->recordname); + log_debug_printf(_logname, "%s: info(Q:Group, ...\n", dbRecordName); + + try { + parseConfigString(jsonGroupDefinition, dbRecordName); + if (!groupProcessingWarnings.empty()) { + fprintf(stderr, "%s: warning(s) from info(\"Q:group\", ...\n%s", dbRecordName, + groupProcessingWarnings.c_str()); + } + } catch (std::exception& e) { + fprintf(stderr, "%s: Error parsing info(\"Q:group\", ...\n%s", dbRecordName, e.what()); + } + } + } + } +} + +/** + * Parse group definitions from the collected list of group definition files. + * + * 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; + + // For each file load the configuration file + auto it = groupConfigFiles.begin(); + while (it != groupConfigFiles.end()) { + std::string groupConfigFileName(*it); + groupConfigFiles.erase(it++); + + // 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()); + } + } catch (std::exception& e) { + throw std::runtime_error( + SB() << "Error reading group definition file \"" << groupConfigFileName << "\"\n" << e.what()); + } + } + }); +} + +/** + * After the group configuration has been read in + * this function is called to evaluate it and create group definitions + */ +void GroupConfigProcessor::defineGroups() { + for (auto& groupConfigIterator: groupConfigMap) { + const std::string& groupName = groupConfigIterator.first; + const GroupConfig& groupConfig = groupConfigIterator.second; + + try { + // If the configured group name is the same as a record name then ignore it + if (dbChannelTest(groupName.c_str()) == 0) { + fprintf(stderr, "%s : Error: Group name conflicts with record name. Ignoring...\n", + groupName.c_str()); + continue; + } + + // Create group when it is first referenced + auto&& groupDefinition = groupDefinitionMap[groupName]; + + // If the structure ID is not already set then set it + if (!groupConfig.structureId.empty()) { + groupDefinitionMap[groupName].structureId = groupConfig.structureId; + } + + // configure the group fields + defineFields(groupDefinition, groupConfig, groupName); + + if (groupConfig.atomicIsSet) { + defineAtomicity(groupDefinition, groupConfig, groupName); + } + + } catch (std::exception& e) { + fprintf(stderr, "Error configuring group \"%s\" : %s\n", groupName.c_str(), e.what()); + } + } + + // re-sort fields to ensure the shorter names appear first + defineFieldSortOrder(); +} + +/** + * Define the group fields. Use the given group config to define group's fields + * + * @param groupDefinition the group whose fields will be configured + * @param groupConfig the group configuration to read from + * @param groupName the name of the group being configured + * @return reference to the current group + */ +void GroupConfigProcessor::defineFields(GroupDefinition& groupDefinition, const GroupConfig& groupConfig, + const std::string& groupName) { + for (auto&& fieldConfigMapEntry: groupConfig.fieldConfigMap) { + const std::string& fieldName = fieldConfigMapEntry.first; + const FieldConfig& fieldConfig = fieldConfigMapEntry.second; + + if (groupDefinition.fieldMap.count(fieldName)) { + fprintf(stderr, "%s.%s Warning: ignoring duplicate mapping %s\n", + groupName.c_str(), fieldName.c_str(), fieldConfig.channel.c_str()); + continue; + } + + groupDefinition.fields.emplace_back(fieldConfig, fieldName); + auto& currentField = groupDefinition.fields.back(); + + groupDefinition.fieldMap[fieldName] = (size_t)-1; // placeholder + + log_debug_printf(_logname, " pvxs map '%s.%s' <-> '%s'\n", + groupName.c_str(), + fieldName.c_str(), + currentField.channel.c_str()); + + defineTriggers(groupDefinition, fieldConfig, fieldName); + } +} + +/* + * Sort Group fields to ensure putOrder + */ +void GroupConfigProcessor::defineFieldSortOrder() { + for (auto&& groupDefinitionMapEntry: groupDefinitionMap) { + auto& groupDefinition = groupDefinitionMapEntry.second; + std::sort(groupDefinition.fields.begin(), groupDefinition.fields.end()); + groupDefinition.fieldMap.clear(); + + auto groupFieldIndex = 0; + for (auto& fieldDefinition: groupDefinition.fields) { + groupDefinition.fieldMap[fieldDefinition.name] = groupFieldIndex++; + } + } +} + +/** + * Configure group atomicity. + * + * @param groupDefinition The group definition to update + * @param groupConfig the source group configuration + * @param groupName the group's name + */ +void GroupConfigProcessor::defineAtomicity(GroupDefinition& groupDefinition, const GroupConfig& groupConfig, + const std::string& groupName) { + assert(groupConfig.atomicIsSet); + TriState atomicity = groupConfig.atomic ? True : False; + + if (groupDefinition.atomic != Unset && groupDefinition.atomic != atomicity) { + fprintf(stderr, "%s Warning: pvxs atomic setting inconsistent\n", groupName.c_str()); + } + + groupDefinition.atomic = atomicity; + + log_debug_printf(_logname, " pvxs atomic '%s' %s\n", + groupName.c_str(), + groupDefinition.atomic ? "YES" : "NO"); +} + +/** + * Load field triggers for a group field. + * + * @param groupDefinition The group definition to update + * @param fieldConfig the field configuration to read trigger configuration from + * @param fieldName the field name in the group + */ +void GroupConfigProcessor::defineTriggers(GroupDefinition& groupDefinition, const FieldConfig& fieldConfig, + const std::string& fieldName) { + TriggerNames triggers; + if (!fieldConfig.trigger.empty()) { + std::string trigger; + std::stringstream splitter(fieldConfig.trigger); + groupDefinition.hasTriggers = true; + + while (std::getline(splitter, trigger, ',')) { + triggers.insert(trigger); + } + } + groupDefinition.fieldTriggerMap[fieldName] = triggers; +} + +/** + * Resolve all trigger references to the fields that they point to. Walk the group definition map, + * and for each group that has triggers resolve the references, and if it does not have + * any triggers then set all fields to self reference. + */ +void GroupConfigProcessor::resolveTriggerReferences() { + // For all groups + for (auto&& groupDefinitionMapEntry: groupDefinitionMap) { + auto& groupName = groupDefinitionMapEntry.first; + auto& groupDefinition = groupDefinitionMapEntry.second; + + // If it has triggers + if (groupDefinition.hasTriggers) { + // Configure its triggers + resolveGroupTriggerReferences(groupDefinition, groupName); + } else { + // If no trigger specified for this group then set all fields to trigger themselves + log_debug_printf(_logname, " pvxs default triggers for '%s'\n", groupName.c_str()); + resolveSelfTriggerReferences(groupDefinition); + } + } +} + +/** + * When triggers are unspecified for a group, call this function to configure all its fields to + * trigger themselves + * + * @param groupDefinition the group to define triggers for + */ +void GroupConfigProcessor::resolveSelfTriggerReferences(GroupDefinition& groupDefinition) { + for (auto&& field: groupDefinition.fields) { + if (!field.channel.empty()) { + field.triggerNames.insert(field.name); // default is self trigger + } + } +} + +/** + * Configure a group's triggers. This involves looping over the map of all triggers and configuring + * the field triggers that are defined there. + * + * @param groupDefinition The group definition to update + * @param groupName the group name + */ +void +GroupConfigProcessor::resolveGroupTriggerReferences(GroupDefinition& groupDefinition, const std::string& groupName) { + for (auto&& triggerMapEntry: groupDefinition.fieldTriggerMap) { + const std::string& fieldName = triggerMapEntry.first; + const auto& targets = triggerMapEntry.second; + + if (groupDefinition.fieldMap.count(fieldName) == 0) { + fprintf(stderr, "Error: Group \"%s\" defines triggers from nonexistent field \"%s\" \n", + groupName.c_str(), fieldName.c_str()); + continue; + } + + auto& index = groupDefinition.fieldMap[fieldName]; + auto& fieldDefinition = groupDefinition.fields[index]; + + log_debug_printf(_logname, " pvxs trigger '%s.%s' -> ", groupName.c_str(), fieldName.c_str()); + + // For all of this trigger's targets + defineGroupTriggers(fieldDefinition, groupDefinition, targets, groupName); + log_debug_printf(_logname, "%s\n", ""); + } +} + +/** + * Define trigger for a given field to reference the given targets. + * + * @param fieldDefinition the field definition who's trigger definition will be updated + * @param groupDefinition the group definition to reference + * @param triggerNames the field's trigger target names + * @param groupName the name of the group + */ +void GroupConfigProcessor::defineGroupTriggers(FieldDefinition& fieldDefinition, const GroupDefinition& groupDefinition, + const TriggerNames& triggerNames, const std::string& groupName) { + for (auto&& triggerName: triggerNames) { + // If the target is star then map to all fields + if (triggerName == "*") { + for (auto& targetedFieldDefinition: groupDefinition.fields) { + if (!targetedFieldDefinition.channel.empty()) { + fieldDefinition.triggerNames.insert(targetedFieldDefinition.name); + log_debug_printf(_logname, "%s, ", targetedFieldDefinition.name.c_str()); + } + } + } else { + // otherwise map to the specific target if it exists + if (groupDefinition.fieldMap.count(triggerName) == 0) { + 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]; + assert(targetedField.name == triggerName); + + // And if it references a PV + if (targetedField.channel.empty()) { + log_debug_printf(_logname, ", ", targetedField.name.c_str()); + } else { + fieldDefinition.triggerNames.insert(targetedField.name); + log_debug_printf(_logname, "%s, ", targetedField.name.c_str()); + } + } + } +} + +/** + * Process the defined groups to create the final Group objects containing PVStructure templates and all the + * infrastructure needed to respond to PVAccess requests linked to the underlying IOC database + * + * 1. Builds Groups and Fields from Group Definitions + * 2. Build PVStructures for each Group and discard those w/o a dbChannel + * 3. Build the lockers for each group and field based on their triggers + */ +void GroupConfigProcessor::createGroups() { + runOnPvxsServer([this](IOCServer* pPvxsServer) { + auto& groupMap = pPvxsServer->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()); + } + } + + // 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 configuration. + * + * The group configuration contains a set of fields. These fields define the structure of the group. + * Dot notation (a.b.c) define substructures with subfields, and bracket notation (a[1].b) define + * structure arrays. Each configuration reference points to a database record (channel). + * This function uses the configuration to define the group fields that are required + * to link to the specified database records. This means that one group field is created for each + * referenced database record. + * + * @param group the group to store fields->channel mappings + * @param groupDefinition the group definition we're reading group information from + */ +void GroupConfigProcessor::initialiseGroupFields(Group& group, const GroupDefinition& groupDefinition) { + // Reserve enough space for fields with channels + group.fields.reserve(groupDefinition.fields.size()); + + // for each field + for (auto& fieldDefinition: groupDefinition.fields) { + group.fields.emplace_back(fieldDefinition.name, fieldDefinition.channel, fieldDefinition.structureId); + } +} + +/** + * Initialise the given group's value template from the given group definition. + * Creates the top level PVStructure for the group and stores it in valueTemplate. + * + * @param group the group we're setting + * @param groupDefinition the group definition we're reading from + */ +void GroupConfigProcessor::initialiseValueTemplate(Group& group, const GroupDefinition& groupDefinition) { + using namespace pvxs::members; + // We will go add members to this list, and then add them to the group's valueTemplate before returning + std::vector groupMembersToAdd; + + // Add default member: record + groupMembersToAdd.push_back({ + Struct("record", { + Struct("_options", { + Int32("queueSize"), + Bool("atomic") + }) + }) + }); + + // for each field add any required members to the list + addTemplatesForDefinedFields(groupMembersToAdd, group, groupDefinition); + + // Add all the collected group members to the group type + TypeDef groupType(TypeCode::Struct, groupDefinition.structureId, {}); + groupType += groupMembersToAdd; + + // create the group's valueTemplate from the group type + auto groupValueTemplate = groupType.create(); + group.valueTemplate = std::move(groupValueTemplate); +} + +/** + * Initialise triggers. This function will initialize the triggers so that each field contains the list of fields + * that subscription updates will also trigger to be fetched. It will also create lockers in each field that will + * be prepared to lock those fields during the subscription update. The configuration information for the + * triggers has already been loaded into the provided group definition. + * Note that this function must be called after the fields have been created in group as the triggers are + * initialized with a set of pointers to other fields. + * + * @param group the group of fields who's triggers are to be configured + * @param groupDefinition the group definition + */ +void GroupConfigProcessor::initialiseTriggers(Group& group, const GroupDefinition& groupDefinition) { + // 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]; + // Look at the fields that it triggers + for (auto& referencedFieldName: fieldDefinition.triggerNames) { + auto referencedFieldIt = groupDefinition.fieldMap.find(referencedFieldName); + if (referencedFieldIt != groupDefinition.fieldMap.end()) { + auto& referencedFieldIndex = referencedFieldIt->second; + auto& referencedField = group.fields[referencedFieldIndex]; + // 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); + } + } + } + + // Make the locks + field.value.lock = DBManyLock(field.value.references); + field.properties.lock = DBManyLock(field.properties.references); + } + } +} + +/** + * Add members to the given vector of members, for any fields in the given group. + * + * @param groupMembers the vector to add members to + * @param group the given group + * @param groupDefinition the source group definition + */ +void GroupConfigProcessor::addTemplatesForDefinedFields(std::vector& groupMembers, Group& group, + 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); + } else { + throw std::runtime_error(std::string("Unknown +type=") + type); + } + } + } +} + +/** + * Parse the given json string as a group configuration part for the given dbRecord + * name and extract group definition into our groupDefinitionMap + * + * @param jsonGroupDefinition the given json string representing a group configuration + * @param dbRecordName the name of the dbRecord + */ +void GroupConfigProcessor::parseConfigString(const char* jsonGroupDefinition, const char* dbRecordName) { +#ifndef EPICS_YAJL_VERSION + yajl_parser_config parserConfig; + memset(&parserConfig, 0, sizeof(parserConfig)); + parserConfig.allowComments = 1; + parserConfig.checkUTF8 = 1; +#endif + + // Convert the json string to a stream to be passed to the json parser + std::istringstream jsonGroupDefinitionStream(jsonGroupDefinition); + + std::string channelPrefix; + + if (dbRecordName) { + channelPrefix = dbRecordName; + channelPrefix += '.'; + } + + // Create a parser context for the parser + GroupProcessorContext parserContext(channelPrefix, this); + +#ifndef EPICS_YAJL_VERSION + YajlHandler handle(yajl_alloc(&yajlParserCallbacks, &parserConfig, NULL, &parserContext)); +#else + + // Create a callback handler for the parser + YajlCallbackHandler callbackHandler(yajl_alloc(&yajlParserCallbacks, nullptr, &parserContext)); + + // Configure the parser with the handler and some options (allow comments) + yajl_config(callbackHandler, yajl_allow_comments, 1); +#endif + + // Parse the json stream for group definitions using the configured parser + if (!yajlParseHelper(jsonGroupDefinitionStream, callbackHandler)) { + throw std::runtime_error(parserContext.errorMessage); + } +} + +/** + * 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. + * + * @param dbEntry the given dbEntry + * @param key the key to get the info field for + * @param defaultValue the default value to return in case its not found + * @return the string for the info key + */ +const char* GroupConfigProcessor::infoField(DBEntry& dbEntry, const char* key, const char* defaultValue) { + // If field not found then return default value + if (dbFindInfo(dbEntry, key)) { + return defaultValue; + } + + // Otherwise return the info string + 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 + * + * e.g: fieldName: "a.b", type => NTScalar, leaf = {NTScalar{}} - a single structure with ID, and members corresponding to NTScalar + * return {Struct{a: Struct{b: NTScalar{}}}} - single element vector + * effect: group members += {Struct{a: Struct{b: NTScalar{}}}} - adds NTScalar at a.b + * + * @param groupMembers the given group members to update + * @param groupField the field used to determine the members to add and how to create them + * @param pDbChannel the db channel to get information on what scalar type to create + */ +void GroupConfigProcessor::addMembersForScalarType(std::vector& groupMembers, const Field& groupField, + const dbChannel* pDbChannel) { + using namespace pvxs::members; + assert(!groupField.fieldName.empty()); // Must not call with empty field name + + TypeDef leaf = getTypeDefForChannel(pDbChannel); + + std::vector newScalarMembers({ leaf.as(groupField.fieldName.leafFieldName()) }); + setFieldTypeDefinition(groupMembers, groupField.fieldName, newScalarMembers); +} + +/** + * Add members to the given vector of members for a plain type field (not Normative Type), that is referenced by the + * given group field. The provided channel is used to get the type of the leaf member to create. + * + * @param groupMembers the vector of members to add to + * @param groupField the given group field + * @param pDbChannel the channel used to get the type of the leaf member + */ +void GroupConfigProcessor::addMembersForPlainType(std::vector& groupMembers, const Field& groupField, + const dbChannel* pDbChannel) { + assert(!groupField.fieldName.empty()); // Must not call with empty field name + + // Get the type for the leaf + auto leafCode(IOCSource::getChannelValueType(pDbChannel, true)); + TypeDef leaf(leafCode); + std::vector newScalarMembers({ leaf.as(groupField.fieldName.leafFieldName()) }); + setFieldTypeDefinition(groupMembers, groupField.fieldName, newScalarMembers); +} + +/** +* Add members to the given vector of members for an `any` type field - a field that contains any scalar type, +* that is referenced by the given group field. +* +* @param groupMembers the vector of members to add to +* @param groupField the given group field + */ +void GroupConfigProcessor::addMembersForAnyType(std::vector& groupMembers, + const Field& groupField) { + assert(!groupField.fieldName.empty()); // Must not call with empty field name + std::vector newScalarMembers({ + Member(TypeCode::Any, groupField.fieldName.leafFieldName()) + }); + setFieldTypeDefinition(groupMembers, groupField.fieldName, newScalarMembers); +} + +/** + * Add ID fields to the prescribed subfield by adding the appropriate members to the + * given members list. This will work by creating a leaf node Struct/StructA that has + * the ID specified for the field. This only works if the referenced field is a structure i.e an NT type. + * Throws an error if we try to apply this to the top level as there is already a mechanism for that. + * + * @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::addMembersForStructureType(std::vector& groupMembers, + const Field& groupField) { + using namespace pvxs::members; + + std::vector newIdMembers( + { groupField.isArray ? StructA("", groupField.id, {}) : Struct("", 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. + * + * @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::addMembersForMetaData(std::vector& groupMembers, const Field& groupField) { + using namespace pvxs::members; + std::vector newMetaMembers({ + Struct("alarm", "alarm_t", { + Int32("severity"), + Int32("status"), + String("message"), + }), + nt::TimeStamp{}.build().as("timeStamp"), + }); + + // Add metadata to the group members at the position determined by group field name + setFieldTypeDefinition(groupMembers, groupField.fieldName, newMetaMembers, false); +} + +/** + * Get the type definition to use for a given channel. This must only be used for Normative Types. + * @param pDbChannel the channel to define the type definition for + * @return the TypeDef for the channel + */ +TypeDef GroupConfigProcessor::getTypeDefForChannel(const dbChannel* pDbChannel) { + // Get the type for the leaf + auto leafCode(IOCSource::getChannelValueType(pDbChannel, true)); + TypeDef leaf; + + // Create the leaf + auto dbfType = dbChannelFinalFieldType(pDbChannel); + if (dbfType == DBF_ENUM || dbfType == DBF_MENU) { + leaf = nt::NTEnum{}.build(); + } else { + bool display = true; + bool control = true; + bool valueAlarm = (dbfType != DBF_STRING); + leaf = nt::NTScalar{ leafCode, display, control, valueAlarm }.build(); + } + return leaf; +} + +/** + * Update the given group members by creating a new members list that uses the given field name + * to determine the nesting of members required to place the given leaf members. + * + * Examples: + * 1) fieldName: "", type => metadata, leaf = {Struct{alarm}, Struct{timestamp}} + * return {Struct{alarm}, Struct{timestamp}} + * effect: group members += {Struct{alarm}, Struct{timestamp}} + * + * 2) fieldName: "a.b", type => NTScalar, leaf = {NTScalar{}} - a single structure with ID, and members corresponding to NTScalar + * return {Struct{a: Struct{b: NTScalar{}}}} - single element vector + * effect: group members += {Struct{a: Struct{b: NTScalar{}}}} - adds NTScalar at a.b + * + * 3) fieldName: "a.c", type => plain double, leaf = {Float64} + * return {Struct{a: Struct{c: {Float64}}}} - single element vector + * effect group members += {Struct{a: Struct{c: {Float64}}}} - adds plain double at a.c + * + * 4) fieldName: "a.b", type => metadata, leaf = {Struct{alarm}, Struct{timestamp}} + * return {Struct{a: Struct{b: {Struct{alarm}, Struct{timestamp}}}}} - single element vector + * effect group members += {Struct{a: Struct{b: {Struct{alarm}, Struct{timestamp}}}}} - add alarm and timestamp info to existing NTScalar at a.b + * + * @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 + */ +void GroupConfigProcessor::setFieldTypeDefinition(std::vector& groupMembers, const FieldName& fieldName, + const std::vector& leafMembers, bool isLeaf) { + using namespace pvxs::members; + + // Make up the full structure starting from the leaf + if (fieldName.empty()) { + // Add all the members (or just one) to the list of group members + groupMembers.insert(groupMembers.end(), leafMembers.begin(), leafMembers.end()); + } else { + std::vector childrenToAdd; + + if (!isLeaf) { + childrenToAdd = leafMembers; + } + + for (auto componentNumber = fieldName.size(); componentNumber > 0; componentNumber--) { + const auto& component = fieldName[componentNumber - 1]; + + // If this is the leaf then use the leaf members + if (isLeaf) { + isLeaf = false; + childrenToAdd = leafMembers; + } else if (component.isArray()) { + // if this is an array then enclose in a structure array + childrenToAdd = { StructA(component.name, childrenToAdd) }; + } else { // otherwise a simple structure + childrenToAdd = { Struct(component.name, childrenToAdd) }; + } + } + groupMembers.insert(groupMembers.end(), childrenToAdd.begin(), childrenToAdd.end()); + } +} + +/** + * Helper function to wrap processing of json lexical elements. + * All exceptions are caught and translated into processing context messages + * + * @param parserContext the parser context + * @param pFunction the lambda to call to process the given element + * @return the value returned from the lambda function + */ +int +GroupConfigProcessor::yajlProcess(void* parserContext, const std::function& pFunction) { + auto* pContext = (GroupProcessorContext*)parserContext; + int returnValue = -1; + try { + returnValue = pFunction(pContext); + } catch (std::exception& e) { + if (pContext->errorMessage.empty()) { + pContext->errorMessage = e.what(); + } + } + return returnValue; +} + +/** + * Parse the given stream as a json group definition using the given json parser handler + * + * @param jsonGroupDefinitionStream the given json group definition stream + * @param handle the handler + * @return true if successful + */ +bool GroupConfigProcessor::yajlParseHelper(std::istream& jsonGroupDefinitionStream, yajl_handle handle) { + unsigned linenum = 0; +#ifndef EPICS_YAJL_VERSION + bool done = false; +#endif + + std::string line; + while (std::getline(jsonGroupDefinitionStream, line)) { + linenum++; + +#ifndef EPICS_YAJL_VERSION + if(done) { + check_trailing(line); + continue; + } +#endif + + // Parse the next line from the json group definition + yajl_status status = yajl_parse(handle, (const unsigned char*)line.c_str(), line.size()); + + switch (status) { + case yajl_status_ok: { + size_t consumed = yajl_get_bytes_consumed(handle); + + if (consumed < line.size()) { + checkForTrailingCommentsAtEnd(line.substr(consumed)); + } + +#ifndef EPICS_YAJL_VERSION + done = true; +#endif + break; + } + case yajl_status_client_canceled: + return false; +#ifndef EPICS_YAJL_VERSION + case yajl_status_insufficient_data: + // continue with next line + break; +#endif + case yajl_status_error: { + std::ostringstream errorMessage; + unsigned char* raw = yajl_get_error(handle, 1, (const unsigned char*)line.c_str(), line.size()); + if (!raw) { + errorMessage << "Unknown error on line " << linenum; + } else { + try { + errorMessage << "Error on line " << linenum << " : " << (const char*)raw; + } catch (...) { + yajl_free_error(handle, raw); + throw; + } + yajl_free_error(handle, raw); + } + throw std::runtime_error(errorMessage.str()); + } + } + } + + if (!jsonGroupDefinitionStream.eof() || jsonGroupDefinitionStream.bad()) { + std::ostringstream msg; + msg << "I/O error after line " << linenum; + throw std::runtime_error(msg.str()); + +#ifndef EPICS_YAJL_VERSION + } else if(!done) { + switch(yajl_parse_complete(handle)) { +#else + } else { + switch (yajl_complete_parse(handle)) { +#endif + case yajl_status_ok: + break; + case yajl_status_client_canceled: + return false; +#ifndef EPICS_YAJL_VERSION + case yajl_status_insufficient_data: + throw std::runtime_error("unexpected end of input"); +#endif + case yajl_status_error: + throw std::runtime_error("Error while completing parsing"); + } + } + return true; +} + +/** + * Initialise the dbLocker in the group. List all the channels in the group and add them to a list. Then + * create the locker from this list. + * + * @param group the group to create the locker for + */ +void GroupConfigProcessor::initialiseDbLocker(Group& group) { + for (auto& field: group.fields) { + dbChannel* pValueChannel = field.value.channel; + dbChannel* pPropertiesChannel = field.properties.channel; + if (pValueChannel) { + group.value.channels.emplace_back(pValueChannel->addr.precord); + } + if (pPropertiesChannel) { + group.properties.channels.emplace_back(pPropertiesChannel->addr.precord); + } + } + group.value.lock = DBManyLock(group.value.channels); + group.properties.lock = DBManyLock(group.properties.channels); +} + +} // ioc +} // pvxs diff --git a/ioc/groupconfigprocessor.h b/ioc/groupconfigprocessor.h new file mode 100644 index 0000000..037e225 --- /dev/null +++ b/ioc/groupconfigprocessor.h @@ -0,0 +1,118 @@ +/* + * 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_GROUPCONFIGPROCESSOR_H +#define PVXS_GROUPCONFIGPROCESSOR_H + +#include + +#include + +#include "dbentry.h" +#include "groupconfig.h" +#include "groupdefinition.h" +#include "iocserver.h" + +namespace pvxs { +namespace ioc { + +// Pre-declare context class +class GroupProcessorContext; + +/** + * Class to parse group configuration that has been defined in db configuration files. + * This involves extracting info fields named "Q:Group" from the database configuration + * 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, + }; + +public: + GroupConfigMap groupConfigMap; + + // Group processing warning messages if not empty + std::string groupProcessingWarnings; + + GroupConfigProcessor() = default; + + static void checkForTrailingCommentsAtEnd(const std::string& line); + void defineGroups(); + void createGroups(); + static const char* infoField(DBEntry& dbEntry, const char* key, const char* defaultValue = nullptr); + static void initialiseGroupFields(Group& group, const GroupDefinition& groupDefinition); + static void initialiseValueTemplate(Group& group, const GroupDefinition& groupDefinition); + void loadConfigFiles(); + void loadConfigFromDb(); + void resolveTriggerReferences(); + static void setFieldTypeDefinition(std::vector& groupMembers, const FieldName& fieldName, + const std::vector& leafMembers, bool isLeaf = true); + static int yajlProcess(void* parserContext, const std::function& pFunction); + +private: + static void + 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); + static void addMembersForScalarType(std::vector& groupMembers, const Field& groupField, + const dbChannel* pDbChannel); + static void addMembersForStructureType(std::vector& groupMembers, const Field& groupField); + static void defineGroupTriggers(FieldDefinition& fieldDefinition, const GroupDefinition& groupDefinition, + const TriggerNames& triggerNames, const std::string& groupName); + static void defineFields(GroupDefinition& groupDefinition, const GroupConfig& groupConfig, + const std::string& groupName); + static void resolveGroupTriggerReferences(GroupDefinition& groupDefinition, const std::string& groupName); + static void defineAtomicity(GroupDefinition& groupDefinition, const GroupConfig& groupConfig, + 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); + static bool yajlParseHelper(std::istream& jsonGroupDefinitionStream, yajl_handle handle); + static void initialiseDbLocker(Group& group); + static void initialiseTriggers(Group& group, const GroupDefinition& groupDefinition); + static TypeDef getTypeDefForChannel(const dbChannel* pDbChannel); +}; + +} // ioc +} // pvxs + +#endif //PVXS_GROUPCONFIGPROCESSOR_H + diff --git a/ioc/groupdefinition.h b/ioc/groupdefinition.h new file mode 100644 index 0000000..5b615fe --- /dev/null +++ b/ioc/groupdefinition.h @@ -0,0 +1,54 @@ +/* + * 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_GROUPDEFINITION_H +#define PVXS_GROUPDEFINITION_H + +#include +#include +#include +#include + +#include "fielddefinition.h" +#include "typeutils.h" + +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 + * `GroupPvChannels` that link the group to regular db channels. Each of these channels + * define fields that are scalar, array or processing placeholders + */ +class GroupDefinition { +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 + + GroupDefinition() = default; + virtual ~GroupDefinition() = default; +}; + +// A map of group name to GroupPv +typedef std::map GroupDefinitionMap; + +} // pvxs +} // ioc + +#endif //PVXS_GROUPDEFINITION_H diff --git a/ioc/groupprocessorcontext.cpp b/ioc/groupprocessorcontext.cpp new file mode 100644 index 0000000..1e34bf7 --- /dev/null +++ b/ioc/groupprocessorcontext.cpp @@ -0,0 +1,67 @@ +/* + * 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 + * + */ +#include + +#include "groupprocessorcontext.h" + +namespace pvxs { +namespace ioc { + +/** + * Assign the given value appropriately given the current context. + * The context holds the current field, key, depth, etc. + * + * @param value the value to assign + */ +void GroupProcessorContext::assign(const Value& value) { + canAssign(); + auto& groupPvConfig = groupConfigProcessor->groupConfigMap[groupName]; + + if (depth == 2) { + if (field == "+atomic") { + groupPvConfig.atomic = value.as(); + groupPvConfig.atomicIsSet = true; + + } else if (field == "+id") { + groupPvConfig.structureId = value.as(); + + } else { + groupConfigProcessor->groupProcessingWarnings += "Unknown group option "; + groupConfigProcessor->groupProcessingWarnings += field; + } + field.clear(); + + } else if (depth == 3) { + auto& groupField = groupPvConfig.fieldConfigMap[field]; + + if (key == "+type") { + groupField.type = value.as(); + + } else if (key == "+channel") { + groupField.channel = channelPrefix + value.as(); + + } else if (key == "+id") { + groupField.structureId = value.as(); + + } else if (key == "+trigger") { + groupField.trigger = value.as(); + + } else if (key == "+putorder") { + groupField.putOrder = value.as(); + + } else { + groupConfigProcessor->groupProcessingWarnings += "Unknown group field option "; + groupConfigProcessor->groupProcessingWarnings += field + ":" + key; + } + key.clear(); + } +} + +} // pvxs +} // ioc diff --git a/ioc/groupprocessorcontext.h b/ioc/groupprocessorcontext.h new file mode 100644 index 0000000..baa3d55 --- /dev/null +++ b/ioc/groupprocessorcontext.h @@ -0,0 +1,55 @@ +/* + * 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_GROUPPROCESSORCONTEXT_H +#define PVXS_GROUPPROCESSORCONTEXT_H + +#include +#include + +#include "groupconfigprocessor.h" +#include "iocserver.h" + +namespace pvxs { +namespace ioc { + +/** + * Object to store contextual information while parsing the group configuration + */ +class GroupProcessorContext { + const std::string channelPrefix; + GroupConfigProcessor* groupConfigProcessor; + +public: + std::string groupName, field, key; + unsigned depth; // number of '{'s + std::string errorMessage; + + GroupProcessorContext(std::string& channelPrefix, GroupConfigProcessor* groupConfigProcessor) + :channelPrefix(channelPrefix), groupConfigProcessor(groupConfigProcessor), depth(0u) { + } + +/** + * Check whether anything can be assigned at the current depth within the json stream being processed. + * Throw an exception if not + */ + void canAssign() const { + if (depth < 2 || depth > 3) { + throw std::runtime_error("Can't assign value in this context"); + } + } + + void assign(const Value& value); + +}; + +} // pvxs +} // ioc + +#endif //PVXS_GROUPPROCESSORCONTEXT_H diff --git a/ioc/groupsource.cpp b/ioc/groupsource.cpp new file mode 100644 index 0000000..0c0e092 --- /dev/null +++ b/ioc/groupsource.cpp @@ -0,0 +1,573 @@ +/* + * 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 + * + */ + +#include + +#include +#include +#include + +#include +#include + +#include "credentials.h" +#include "dberrormessage.h" +#include "dblocker.h" +#include "dbmanylocker.h" +#include "fieldsubscriptionctx.h" +#include "groupsource.h" +#include "groupsrcsubscriptionctx.h" +#include "iocshcommand.h" +#include "iocsource.h" +#include "securitylogger.h" +#include "securityclient.h" +#include "localfieldlog.h" + +namespace pvxs { +namespace ioc { + +DEFINE_LOGGER(_logname, "pvxs.ioc.group.source"); + +/** + * Constructor for GroupSource registrar. + */ +GroupSource::GroupSource() + :eventContext(db_init_events()) // Initialise event context +{ + // Get GroupPv configuration and register each pv name in the server + runOnPvxsServer([this](IOCServer* pPvxsServer) { + auto names(std::make_shared>()); + + // Lock map and get names + { + epicsGuard G(pPvxsServer->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); + } + } + + allRecords.names = names; + + // 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()"); + } + }); +} + +/** + * Handle the create source operation. This is called once when the source is created. + * We will register all of the database records that have been loaded until this time as pv names in this + * source. + * + * @param channelControl channel control object provided by the pvxs framework + */ +void GroupSource::onCreate(std::unique_ptr&& channelControl) { + 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); + }); + +} + +/** + * Respond to search requests. For each matching pv, claim that pv + * + * @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()); + } + } + }); +} + +/** + * Respond to the show request by displaying a list of all the PVs hosted in this ioc + * + * @param outputStream the stream to show the list on + */ +void GroupSource::show(std::ostream& outputStream) { + outputStream << "IOC"; + for (auto& name: *GroupSource::allRecords.names) { + outputStream << "\n" << indent{} << name; + } +} + +/** + * 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)); + }); + + auto subscriptionContext(std::make_shared(group)); + channelControl + ->onSubscribe([this, subscriptionContext](std::unique_ptr&& subscriptionOperation) { + 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. + * + * @param groupSubscriptionCtx the group subscription context + */ +void GroupSource::onDisableSubscription(const std::shared_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); + } +} + +/** + * 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 group to the call. + * + * @param group the group to which the get/put operation pertains + * @param channelConnectOperation the channel connect operation object + */ +void GroupSource::onOp(Group& group, + std::unique_ptr&& channelConnectOperation) { + // First stage for handling any request is to announce the channel type with a `connect()` call + // @note The type signalled here must match the eventual type returned by a pvxs get + channelConnectOperation->connect(group.valueTemplate); + + // register handler for pvxs group get + channelConnectOperation->onGet([&group](std::unique_ptr&& getOperation) { + get(group, getOperation); + }); + + // Make a security cache for this client's connection to this group + // 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 group pv + auto securityCache = std::make_shared(); + + // register handler for pvxs group put + channelConnectOperation + ->onPut([&group, securityCache](std::unique_ptr&& putOperation, Value&& value) { + if (!securityCache->done) { + // First time we call put we need to initialise the security cache + securityCache->securityClients.resize(group.fields.size()); + securityCache->credentials.reset(new Credentials(*putOperation->credentials())); + auto fieldIndex = 0u; + for (auto& field: group.fields) { + if (field.value.channel) { + securityCache->securityClients[fieldIndex] + .update(field.value.channel, *securityCache->credentials); + } + fieldIndex++; + } + auto& pvRequest = putOperation->pvRequest(); + IOCSource::setForceProcessingFlag(pvRequest, securityCache); + securityCache->done = true; + } + + putGroup(group, putOperation, value, *securityCache); + }); +} + +/** + * Called by the framework when the monitoring client issues a start or stop subscription. We + * intercept the framework's call prior to entering here, and add the group subscription context + * containing a list of field contexts and their event subscriptions to manage. + * + * @param groupSubscriptionCtx the group subscription context + * @param isStarting true if the client issued a start subscription request, false otherwise + */ +void GroupSource::onStart(const std::shared_ptr& groupSubscriptionCtx, bool isStarting) { + if (isStarting) { + onStartSubscription(groupSubscriptionCtx); + } else { + onDisableSubscription(groupSubscriptionCtx); + } +} + +/** + * 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. + * + * @param groupSubscriptionCtx the group subscription context containing the field event subscriptions to start + */ +void GroupSource::onStartSubscription(const std::shared_ptr& groupSubscriptionCtx) { + 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); + } +} + +/** + * Called by the framework when a client subscribes to a channel. We intercept the call before this function is called + * to add a new group subscription context containing a reference to the group. + * This function must initialise all of the field's subscription contexts. + * + * @param groupSubscriptionCtx a new group subscription context + * @param subscriptionOperation the group subscription operation + */ +void GroupSource::onSubscribe(const std::shared_ptr& groupSubscriptionCtx, + std::unique_ptr&& subscriptionOperation) const { + // inform peer of data type and acquire control of the subscription queue + groupSubscriptionCtx->subscriptionControl = subscriptionOperation + ->connect(groupSubscriptionCtx->group.valueTemplate); + + // Initialise the field subscription contexts. One for each group field. + // This is stored in the group context + groupSubscriptionCtx->fieldSubscriptionContexts.reserve(groupSubscriptionCtx->group.fields.size()); + for (auto& field: groupSubscriptionCtx->group.fields) { + groupSubscriptionCtx->fieldSubscriptionContexts.emplace_back(field, groupSubscriptionCtx.get()); + auto& fieldSubscriptionContext = groupSubscriptionCtx->fieldSubscriptionContexts.back(); + + // Two subscription are made for each group channel for pvxs + if (field.isMeta) { + 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); + } + + // If all goes well, set up handlers for start and stop monitoring events + groupSubscriptionCtx->subscriptionControl->onStart([groupSubscriptionCtx](bool isStarting) { + onStart(groupSubscriptionCtx, isStarting); + }); +} + +/** + * 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) { + + // Make an empty value to return + auto returnValue(group.valueTemplate.cloneEmpty()); + + // 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) { + // 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) { + 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; + } + } + } + + // Unlock the all group fields when the locker goes out of scope + + } else { + // Otherwise, this is a non-atomic operation, and we need to `put` each field individually, + // locking each of them independently of each other. + + // 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; + } + + // find the leaf node in which to set the value + auto leafNode = field.findIn(returnValue); + + if (leafNode) { + // Lock this field + dbChannel* pDbChannel = field.value.channel; + DBLocker F(pDbChannel->addr.precord); + if (!getGroupField(field, leafNode, group.name, errorFn)) { + return; + } + } + } + } + + // 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; +} + +/** + * Handler invoked when a peer sends data on a PUT + * + * @param group the group to which the data is posted + * @param putOperation the put operation object to use to interact with the client + * @param value the value being posted + * @param groupSecurityCache the object that caches the security context of client connections + */ +void GroupSource::putGroup(Group& group, std::unique_ptr& putOperation, const Value& value, + const GroupSecurityCache& groupSecurityCache) { + try { + 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) { + IOCSource::doPreProcessing(pDbChannel, + securityLoggers[fieldIndex], *groupSecurityCache.credentials, + groupSecurityCache.securityClients[fieldIndex]); + if (dbChannelFinalFieldType(pDbChannel) >= DBF_INLINK + && dbChannelFinalFieldType(pDbChannel) <= DBF_FWDLINK) { + throw std::runtime_error("Links not supported for put"); + } + } + fieldIndex++; + } + + // Reset index for subsequent loops + fieldIndex = 0; + + // 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) { + // Lock all the fields + DBManyLocker G(group.value.lock); + // Loop through all fields + for (auto& field: group.fields) { + // Put the field + putGroupField(value, field, groupSecurityCache.securityClients[fieldIndex]); + // Do processing if required + IOCSource::doPostProcessing(field.value.channel, groupSecurityCache.forceProcessing); + fieldIndex++; + } + + // Unlock the all group fields when the locker goes out of scope + + } else { + // Otherwise, this is a non-atomic operation, and we need to `put` each field individually, + // locking each of them independently of each other. + + // Loop through all fields + for (auto& field: group.fields) { + dbChannel* pDbChannel = field.value.channel; + // 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); + // Unlock this field when locker goes out of scope + fieldIndex++; + } + } + + } catch (std::exception& e) { + // Unlock all locked fields when lockers go out of scope + // Post error message to put operation object + putOperation->error(e.what()); + return; + } + + // If all went ok then let the client know + putOperation->reply(); +} + +/** + * Called by putGroup() to perform the actual put of the given value into the group field specified. + * The value will be the whole value template that the group represents but only the fields passed in by + * the client will be set. So we simply check to see whether the parts of value that are referenced by the + * provided field parameter are included in the given value, and if so, we pull them out and do a low level + * database put. + * + * @param value the sparsely populated value to put into the group's field + * @param field the group field to check against + * @param securityClient the security client to use to authorise the operation + */ +void GroupSource::putGroupField(const Value& value, const Field& field, const SecurityClient& securityClient) { + // find the leaf node that the field refers to in the given value + auto leafNode = field.findIn(value); + + // 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); + } +} + +} // ioc +} // pvxs diff --git a/ioc/groupsource.h b/ioc/groupsource.h new file mode 100644 index 0000000..91f2f82 --- /dev/null +++ b/ioc/groupsource.h @@ -0,0 +1,91 @@ +/* + * 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_GROUPSOURCE_H +#define PVXS_GROUPSOURCE_H + +#include "dbeventcontextdeleter.h" +#include "groupsrcsubscriptionctx.h" +#include "iocserver.h" +#include "iocsource.h" +#include "securityclient.h" + +namespace pvxs { +namespace ioc { + +class GroupSource : public server::Source { +public: + GroupSource(); + void onCreate(std::unique_ptr&& channelControl) final; + +/** + * Implementation of the onList() interface of pvxs::server::Source to return a list of all records + * managed by this source. + * + * @return all records managed by this source + */ + List onList() final { + return allRecords; + } + + void onSearch(Search& searchOperation) final; + void show(std::ostream& outputStream) final; + +private: + // List of all database records that this single source serves + 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, + Group& group); + // 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); + + ////////////////////////////// + // Put + ////////////////////////////// + static void putGroup(Group& group, std::unique_ptr& putOperation, const Value& value, + const GroupSecurityCache& groupSecurityCache); + + ////////////////////////////// + // 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, + std::unique_ptr&& subscriptionOperation) const; + static void onStart(const std::shared_ptr& groupSubscriptionCtx, bool isStarting); + static void putGroupField(const Value& value, const Field& field, const SecurityClient& securityClient); +}; + +} // ioc +} // pvxs + + +#endif //PVXS_GROUPSOURCE_H diff --git a/ioc/groupsourcehooks.cpp b/ioc/groupsourcehooks.cpp new file mode 100644 index 0000000..44ad022 --- /dev/null +++ b/ioc/groupsourcehooks.cpp @@ -0,0 +1,188 @@ +/* + * 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 + * + */ + +#include + +#include +#include + +#include + +#include +#include + +#include "groupsource.h" +#include "groupconfigprocessor.h" +#include "iocshcommand.h" + +// must include after log.h has been included to avoid clash with printf macro +#include +namespace pvxs { +namespace ioc { + +/** + * IOC command wrapper for dbLoadGroup function + * + * @param jsonFileName + */ +void dbLoadGroupCmd(const char* jsonFileName) { + (void)dbLoadGroup(jsonFileName); + auto gp = GroupConfigProcessor(); + gp.loadConfigFiles(); +} + +/** + * List group db record/field names that are registered with the pvxs IOC server. + * With no arguments this will list all the group record names. + * + * @param level optional depth to show details for + * @param pattern optionally only show records matching the regex pattern + */ +void pvxsgl(int level, const char* pattern) { + runOnPvxsServer(([level, &pattern](IOCServer* pPvxsServer) { + try { + // Default pattern to match everything + if (!pattern) { + pattern = ""; + } + + { + epicsGuard G(pPvxsServer->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); + } + } + } + } + } 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) { + try { + if (!jsonFilename || !jsonFilename[0]) { + printf("dbLoadGroup(\"file.json\")\n" + "Load additional DB group definitions from file.\n"); + fprintf(stderr, "Missing filename\n"); + return 1; + } + + 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); + } + }); + return 0; + } catch (std::exception& e) { + fprintf(stderr, "Error: %s\n", e.what()); + return 1; + } +} + +} +} // namespace pvxs::ioc + +using namespace pvxs::ioc; + +namespace { +using namespace pvxs; + +/** + * Initialise qsrv database group 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 qsrvGroupSourceInit(initHookState theInitHookState) { + if (theInitHookState == initHookAfterInitDatabase) { + GroupConfigProcessor processor; + // Parse all info(Q:Group... records to configure groups + processor.loadConfigFromDb(); + + // Load group configuration files + processor.loadConfigFiles(); + + // 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::iocServer().addSource("qsrvGroup", std::make_shared(), 1); + } +} + +/** + * IOC pvxs Group Source registrar. This implements the required registrar function that is called by xxxx_registerRecordDeviceDriver, + * the auto-generated stub created for all IOC implementations. + *

+ * It is registered by using the `epicsExportRegistrar()` macro. + *

+ * 1. Register your hook handler to handle any state hooks that you want to implement. Here we install + * an `initHookState` handler connected to the `initHookAfterIocBuilt` state. It will add all of the + * group record type sources defined so far. Note that you can define sources up until the `iocInit()` call, + * after which point the `initHookAfterIocBuilt` handlers are called and will register all the defined records. + */ +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.") + .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") + .implementation<&dbLoadGroupCmd>(); + + initHookRegister(&qsrvGroupSourceInit); +} + +} // namespace + +// in .dbd file +//registrar(pvxsGroupSourceRegistrar) +extern "C" { +epicsExportRegistrar(pvxsGroupSourceRegistrar); +} diff --git a/ioc/groupsrcsubscriptionctx.h b/ioc/groupsrcsubscriptionctx.h new file mode 100644 index 0000000..6404662 --- /dev/null +++ b/ioc/groupsrcsubscriptionctx.h @@ -0,0 +1,47 @@ +/* + * 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_GROUPSRCSUBSCRIPTIONCTX_H +#define PVXS_GROUPSRCSUBSCRIPTIONCTX_H + +#include +#include + +#include + +#include "dbeventcontextdeleter.h" +#include "fieldsubscriptionctx.h" +#include "group.h" +#include "subscriptionctx.h" + +namespace pvxs { +namespace ioc { + +class GroupSourceSubscriptionCtx { +public: + Group& group; + epicsMutex eventLock{}; + bool eventsPrimed = false, firstEvent = true; + std::unique_ptr subscriptionControl{}; + + // This is as a special case for storing the initial value prior to both initial subscription events returning + // This is so that we can merge this with the subsequent values that come in before all initial events are in + Value currentValue; + + std::vector fieldSubscriptionContexts{}; + explicit GroupSourceSubscriptionCtx(Group& subscribedGroup) + :group(subscribedGroup), currentValue(subscribedGroup.valueTemplate.cloneEmpty()) { + } + +}; + +} // pvxs +} // ioc + +#endif //PVXS_GROUPSRCSUBSCRIPTIONCTX_H diff --git a/ioc/imagedemo.c b/ioc/imagedemo.c new file mode 100644 index 0000000..fda70f7 --- /dev/null +++ b/ioc/imagedemo.c @@ -0,0 +1,46 @@ + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include + +/** Generate a test pattern + * + * A - width (ULONG) + * B - height (ULONG) + * VALA - pixel array (USHORT) + */ +static +long QSRV_image_demo(aSubRecord* prec) { + epicsUInt32 H = *(epicsUInt32*)prec->a, + W = *(epicsUInt32*)prec->b; + epicsUInt16* I = (epicsUInt16*)prec->vala; + epicsUInt32 i, j; + + if (W * H > prec->nova) { + (void)recGblSetSevr(prec, READ_ALARM, INVALID_ALARM); + return 0; + } + + for (i = 0; i < W; i++) { + for (j = 0; j < H; j++) { + if (i % 50 == 49 || j % 50 == 49) + I[i * H + j] = 65535; + else + I[i * H + j] = ((epicsUInt32)j) * 65535 / H; + } + } + + prec->neva = W * H; + return 0; +} + +epicsRegisterFunction(QSRV_image_demo); diff --git a/ioc/iochooks.cpp b/ioc/iochooks.cpp index 2c43a48..05dad70 100644 --- a/ioc/iochooks.cpp +++ b/ioc/iochooks.cpp @@ -1,261 +1,259 @@ -/** +/* * 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 + * */ +/** + * This source file defines the pvxs IOC server instance and also defines a few high level IOC shell commands from PVXS + * It does not register any database sources defined in any user DB file or user group file, that is + * handled in records.cpp and groups.cpp respectively. + */ #include #include #include -#include +#include +#include +#include + +#include #include #include #include -#include -#include -#include +#include "iocserver.h" +#include "iocshcommand.h" + +// must include after log.h has been included to avoid clash with printf macro #include -#include -#include -using namespace pvxs; - -namespace { -std::atomic instance{}; - -DEFINE_LOGGER(log, "pvxs.ioc"); - -void pvxsl(int detail) -{ - try { - if(auto serv = instance.load()) { - for(auto& pair : serv->listSource()) { - auto src = serv->getSource(pair.first, pair.second); - if(!src) - continue; // race? - - auto list = src->onList(); - - if(detail>0) - printf("# Source %s@%d%s\n", - pair.first.c_str(), pair.second, - list.dynamic ? " [dynamic]":""); - - if(!list.names) { - if(detail>0) - printf("# no PVs\n"); - } else { - for(auto& name : *list.names) { - printf("%s\n", name.c_str()); - } - } - } - } - } catch(std::exception& e) { - fprintf(stderr, "Error in %s : %s\n", __func__, e.what()); - } -} - -void pvxsr(int detail) -{ - try { - if(auto serv = instance.load()) { - std::ostringstream strm; - Detailed D(strm, detail); - strm<<*serv; - printf("%s", strm.str().c_str()); - } - } catch(std::exception& e) { - fprintf(stderr, "Error in %s : %s\n", __func__, e.what()); - } -} - -void pvxs_target_info() -{ - try { - std::ostringstream capture; - target_information(capture); - printf("%s", capture.str().c_str()); - } catch(std::exception& e) { - fprintf(stderr, "Error in %s : %s\n", __func__, e.what()); - } -} - -// index_sequence from: -//http://stackoverflow.com/questions/17424477/implementation-c14-make-integer-sequence - -template< std::size_t ... I > -struct index_sequence { - using type = index_sequence; - using value_type = std::size_t; - static constexpr std::size_t size() { - return sizeof ... (I); - } -}; - -template< typename Seq1, typename Seq2 > -struct concat_sequence; - -template< std::size_t ... I1, std::size_t ... I2 > -struct concat_sequence< index_sequence< I1 ... >, index_sequence< I2 ... > > : public index_sequence< I1 ..., (sizeof ... (I1)+I2) ... > {}; - -template< std::size_t I > -struct make_index_sequence : public concat_sequence< typename make_index_sequence< I/2 >::type, - typename make_index_sequence< I-I/2 >::type > {}; - -template<> -struct make_index_sequence< 0 > : public index_sequence<> {}; - -template<> -struct make_index_sequence< 1 > : public index_sequence< 0 > {}; - -template -struct Arg; - -template<> -struct Arg { - static constexpr iocshArgType code = iocshArgInt; - static int get(const iocshArgBuf& buf) { return buf.ival; } -}; - -template<> -struct Arg { - static constexpr iocshArgType code = iocshArgDouble; - static double get(const iocshArgBuf& buf) { return buf.dval; } -}; - -template<> -struct Arg { - static constexpr iocshArgType code = iocshArgString; - static const char* get(const iocshArgBuf& buf) { return buf.sval; } -}; - -template -struct ToStr { typedef const char* type; }; - -template -struct Reg { - const char* const name; - const char* const argnames[1+sizeof...(Args)]; - - constexpr explicit Reg(const char* name, typename ToStr::type... descs) - :name(name) - ,argnames{descs..., 0} - {} - - template - static - void call(const iocshArgBuf* args) - { - (*fn)(Arg::get(args[Idxs])...); - } - - template - void doit(index_sequence) - { - static const iocshArg argstack[1+sizeof...(Args)] = {{argnames[Idxs], Arg::code}...}; - static const iocshArg * const args[] = {&argstack[Idxs]..., 0}; - static const iocshFuncDef def = {name, sizeof...(Args), args}; - - iocshRegister(&def, &call); - } - - template - void ister() - { - doit(make_index_sequence{}); - } -}; - -void pvxsAtExit(void* unused) -{ - try { - if(auto serv = instance.load()) { - if(instance.compare_exchange_strong(serv, nullptr)) { - // take ownership - std::unique_ptr trash(serv); - trash->stop(); - log_debug_printf(log, "Stopped Server?%s", "\n"); - } - } - } catch(std::exception& e) { - fprintf(stderr, "Error in %s : %s\n", __func__, e.what()); - } -} - -void pvxsInitHook(initHookState state) -{ - try { - // iocBuild() - if(state==initHookAfterInitDatabase) { - // we want to run before exitDatabase - epicsAtExit(&pvxsAtExit, nullptr); - } - // iocRun()/iocPause() - if(state==initHookAfterCaServerRunning) { - if(auto serv = instance.load()) { - serv->start(); - log_debug_printf(log, "Started Server %p", serv); - } - } - if(state==initHookAfterCaServerPaused) { - if(auto serv = instance.load()) { - serv->stop(); - log_debug_printf(log, "Stopped Server %p", serv); - } - } - } catch(std::exception& e) { - fprintf(stderr, "Error in %s : %s\n", __func__, e.what()); - } -} - -void pvxsRegistrar() -{ - try { - pvxs::logger_config_env(); - - Reg("pvxsl", "detail").ister<&pvxsl>(); - Reg("pvxsr", "detail").ister<&pvxsr>(); - Reg<>("pvxs_target_info").ister<&pvxs_target_info>(); - - auto serv = instance.load(); - if(!serv) { - std::unique_ptr temp(new server::Server(server::Config::from_env())); - - if(instance.compare_exchange_strong(serv, temp.get())) { - log_debug_printf(log, "Installing Server %p\n", temp.get()); - temp.release(); - } else { - log_crit_printf(log, "Race installing Server? %p\n", serv); - } - } else { - log_err_printf(log, "Stale Server? %p\n", serv); - } - - initHookRegister(&pvxsInitHook); - } catch(std::exception& e) { - fprintf(stderr, "Error in %s : %s\n", __func__, e.what()); - } -} - -} // namespace +#if EPICS_VERSION_INT >= VERSION_INT(7, 0, 4, 0) +# define USE_DEINIT_HOOKS +#endif namespace pvxs { namespace ioc { -server::Server server() -{ - if(auto serv = instance.load()) { - return *serv; +DEFINE_LOGGER(_logname, "pvxs.ioc"); + +// The pvxs server singleton +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() { + if (auto pPvxsServer = pvxsServer.load()) { + return *pPvxsServer; } else { throw std::logic_error("No Instance"); } } -}} // namespace pvxs::ioc +/** + * 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 + * object. + * + * @param pep - The pointer to the exit parameter list - unused + */ +void pvxsAtExit(void* pep) { + runOnPvxsServerWhile_("In IOC exit event handler", [](IOCServer* pPvxsServer) { + if (pvxsServer.compare_exchange_strong(pPvxsServer, nullptr)) { + // take ownership + std::unique_ptr serverInstance(pPvxsServer); + serverInstance->stop(); + log_debug_printf(_logname, "Stopped Server%s", "\n"); + } + }); +} + +//////////////////////////////////// +// Two ioc shell commands for pvxs +//////////////////////////////////// + +/** + * Show the PVXS server report. + * The server report is a short list of the EPICS PVA environment variables and + * a list of registered sources with their IOIDs. + * + * @param detail + */ +void pvxsr(int detail) { + runOnPvxsServer([&detail](IOCServer* pPvxsServer) { + std::ostringstream strm; + Detailed D(strm, detail); + strm << *pPvxsServer; + printf("%s", strm.str().c_str()); + }); +} + +/** + * Show information about the PVXS host. + * + * Includes: + * - OS, + * - build toolchain, + * - library versions, + * - runtime environment information including: + * - network address and + * - thread count, and + * - EPICS PVA environment variable settings + */ +void pvxsi() { + try { + std::ostringstream capture; + target_information(capture); + printf("%s", capture.str().c_str()); + } catch (std::exception& e) { + fprintf(stderr, "Error in %s : %s\n", __func__, e.what()); + } +} + +/** + * Initialise and control state of pvxs ioc server instance in response to iocInitHook events. + * Installed on the initHookState hook this function will respond to the following events: + * - initHookAfterInitDatabase: Set the exit callback only when we have initialized the database + * - initHookAfterCaServerRunning: Start the pvxs server instance after the CA server starts running + * - initHookAfterCaServerPaused: Pause the pvxs server instance if the CA server pauses + * + * @param theInitHookState the initHook state to respond to + */ +void pvxsInitHook(initHookState theInitHookState) { + // iocBuild() + if (theInitHookState == initHookAfterInitDatabase) { + // function to run before exitDatabase +#ifndef USE_DEINIT_HOOKS + epicsAtExit(&pvxsAtExit, nullptr); +#endif + } else + // iocRun() + if (theInitHookState == initHookAfterCaServerRunning || theInitHookState == initHookAfterIocRunning) { + runOnPvxsServer([](IOCServer* pPvxsServer) { + pPvxsServer->start(); + log_debug_printf(_logname, "Started Server %p", pPvxsServer); + }); + } else + // iocPause() + if (theInitHookState == initHookAfterCaServerPaused) { + runOnPvxsServer([](IOCServer* pPvxsServer) { + pPvxsServer->stop(); + log_debug_printf(_logname, "Stopped Server %p", pPvxsServer); + }); + } else + +#ifdef USE_DEINIT_HOOKS + // iocShutdown() (called from exitDatabase() at exit, and testIocShutdownOk() ) + if (theInitHookState == initHookAtShutdown) { + pvxsAtExit(nullptr); + } +#endif +} + +} +} // namespace pvxs::ioc + +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 + */ +void initialisePvxsServer() { + using namespace pvxs::server; + auto serv = pvxsServer.load(); + if (!serv) { + std::unique_ptr temp(new IOCServer(Config::from_env())); + + if (pvxsServer.compare_exchange_strong(serv, temp.get())) { + log_debug_printf(_logname, "Installing Server %p\n", temp.get()); + (void)temp.release(); + } else { + log_crit_printf(_logname, "Race installing Server? %p\n", serv); + } + } else { + log_err_printf(_logname, "Stale Server? %p\n", serv); + } +} + +/** + * IOC pvxs base registrar. This implements the required registrar function that is called by xxxx_registerRecordDeviceDriver, + * the auto-generated stub created for all IOC implementations. + * + * It is registered by using the `epicsExportRegistrar()` macro. + * + * 1. Specify here all of the commands that you want to be registered and available in the IOC shell. + * 2. Also make sure that you initialize your server implementation - PVXS in our case - so that it will be available for the shell. + * 3. Lastly register your hook handler to handle any state hooks that you want to implement + */ +void pvxsBaseRegistrar() { + try { + pvxs::logger_config_env(); + + IOCShCommand("pvxsr", "[show_detailed_information?]", "PVXS Server Report. " + "Shows information about server config (level==0)\n" + "or about connected clients (level>0).\n") + .implementation<&pvxsr>(); + IOCShCommand<>("pvxsi", "Show detailed server information\n").implementation<&pvxsi>(); + + // Initialise the PVXS Server + initialisePvxsServer(); + + // Register our hook handler to intercept certain state changes + initHookRegister(&pvxsInitHook); + } catch (std::exception& e) { + fprintf(stderr, "Error in %s : %s\n", __func__, e.what()); + } +} +} // namespace extern "C" { -epicsExportRegistrar(pvxsRegistrar); +epicsExportRegistrar(pvxsBaseRegistrar); } diff --git a/ioc/iocserver.h b/ioc/iocserver.h new file mode 100644 index 0000000..81d159b --- /dev/null +++ b/ioc/iocserver.h @@ -0,0 +1,41 @@ +/* + * 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/iocshargument.h b/ioc/iocshargument.h new file mode 100644 index 0000000..a4475f7 --- /dev/null +++ b/ioc/iocshargument.h @@ -0,0 +1,82 @@ +/* + * 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_IOCSHARGUMENT_H +#define PVXS_IOCSHARGUMENT_H + +#include +#include +#include +#include + +#include + +namespace pvxs { +namespace ioc { + +/** + * Generic argument type used to encapsulate a variadic parameter of arbitrary type + * These are used to define callback functions with varying numbers of parameters of varying types. + * + * Supported types are: `int`, `double`, and `const char*` + * + * @tparam T the type required for the variadic parameter + */ +template +struct IOCShFunctionArgument; + +/** + * Specialization for int variadic parameters + * + * @tparam T for `int` variadic parameters + */ +template<> struct IOCShFunctionArgument { + static constexpr iocshArgType + code = iocshArgInt; + static int get(const iocshArgBuf& buf) { + return buf.ival; + } +}; + +/** + * Specialization for double precision variadic parameters + * + * @tparam T for `double` variadic parameters + */ +template<> struct IOCShFunctionArgument { + static constexpr iocshArgType + code = iocshArgDouble; + static double get(const iocshArgBuf& buf) { + return buf.dval; + } +}; + +/** + * Specialization for string variadic parameters + * + * @tparam T for `const char*` variadic parameters + */ +template<> struct IOCShFunctionArgument { + static constexpr iocshArgType + code = iocshArgString; + static const char* get(const iocshArgBuf& buf) { + return buf.sval; + } +}; + +/** + * Convert the given template type name into a const char* parameter. + * For use as a type variable type inside a variadic templated function. + */ +template +using ConstString = const char*; + +} // ioc +} // pvxs +#endif //PVXS_IOCSHARGUMENT_H diff --git a/ioc/iocshcommand.h b/ioc/iocshcommand.h new file mode 100644 index 0000000..4e2cac6 --- /dev/null +++ b/ioc/iocshcommand.h @@ -0,0 +1,109 @@ +/* + * 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_IOCSHCOMMAND_H +#define PVXS_IOCSHCOMMAND_H + +#include +#include +#include +#include + +#include + +#include "iocshargument.h" +#include "iocshindex.h" +#include "iocserver.h" + +namespace pvxs { +namespace ioc { + +// All shell commands return void and take a variable number of arguments of any supported type +template +using IOCShFunction = void (*)(IOCShFunctionArgumentTypes...); + +/** + * Class that encapsulates an IOC command. + * The command has a name and a description. + * + * A method allows you to register the implementation of the command. + * + * The constructor takes the name of the function and the help text + * Call implementation() with a reference to your implementation function to complete registration + * + * e.g.: + * pvxs::ioc::IOCShRegister("pvxsl", "show detailed info?").implementation<&pvxsl>(); + * + * @tparam IOCShFunctionArgumentTypes the list of 0 or more argument types for the shell function to be registered + */ +template +class IOCShCommand { +public: + const char* const name; + const char* const argumentNames[1 + sizeof...(IOCShFunctionArgumentTypes)]; + const char* const usage = nullptr; + +// Construct a new IOC shell command with a name and description + constexpr explicit IOCShCommand(const char* name, ConstString... argumentDescriptions) + :name(name), argumentNames{ argumentDescriptions..., 0 } { + } + +// Construct a new IOC shell command with a name and description + constexpr explicit IOCShCommand(const char* name, ConstString... argumentDescriptions, + const char* usage) + :name(name), argumentNames{ argumentDescriptions..., 0 }, usage(usage) { + } + +// Create an implementation for this IOC command + template function> + void implementation() { + implement(make_index_sequence{}); + } + +// Implement the command by registering the callback with EPICS iocshRegister() + template function, size_t... Idxs> + void implement(index_sequence) { + 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 }; + + iocshRegister(&functionDefinition, &call < function, Idxs... >); + } + +// The actual callback that is executed for the registered command +// The function is called with a variadic argument list of heterogeneous types based on the +// declared registration template types +// by calling the appropriate get methods on the templated Arg(s) + template function, size_t... Idxs> + static void call(const iocshArgBuf* iocShArgumentsBuffer) { + (*function)(IOCShFunctionArgument::get(iocShArgumentsBuffer[Idxs])...); + } +}; + +void runOnServer(const std::function& function, const char* method = nullptr, + const char* context = nullptr); + +} // pvxs +} // ioc + +/** + * Run given lambda function against the provided pvxs server instance + * @param _lambda the lambda function to run against the provided pvxs server instance + */ +#define runOnPvxsServer(_lambda) runOnServer(_lambda, __func__) + +/** + * Run given lambda function against the provided pvxs server instance with the given context string + * @param _context the context string, used in error reporting. e.g. "Updating tables" + * @param _lambda the lambda function to run against the provided pvxs server instance + */ +#define runOnPvxsServerWhile_(_context, _lambda) runOnServer(_lambda, __func__, _context) +#endif //PVXS_IOCSHCOMMAND_H diff --git a/ioc/iocshindex.h b/ioc/iocshindex.h new file mode 100644 index 0000000..60f07b6 --- /dev/null +++ b/ioc/iocshindex.h @@ -0,0 +1,54 @@ +/* + * 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_IOCSHINDEX_H +#define PVXS_IOCSHINDEX_H + +#include + +// index_sequence from: +//http://stackoverflow.com/questions/17424477/implementation-c14-make-integer-sequence + +namespace pvxs { +namespace ioc { + +template +struct index_sequence { + using type = index_sequence; + using value_type = std::size_t; + static constexpr std::size_t size() { + return sizeof ... (I); + } +}; + +template +struct concat_sequence; + +template +struct concat_sequence, index_sequence > + : public index_sequence { +}; + +template +struct make_index_sequence : public concat_sequence::type, + typename make_index_sequence::type> { +}; + +template<> +struct make_index_sequence<0> : public index_sequence<> { +}; + +template<> +struct make_index_sequence<1> : public index_sequence<0> { +}; + +} +} +#endif //PVXS_IOCSHINDEX_H diff --git a/ioc/iocsource.cpp b/ioc/iocsource.cpp new file mode 100644 index 0000000..81891d8 --- /dev/null +++ b/ioc/iocsource.cpp @@ -0,0 +1,682 @@ +/* + * 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 + * + */ + +#include +#include + +#include +#include + +#include "iocsource.h" +#include "dbentry.h" +#include "dberrormessage.h" +#include "typeutils.h" +#include "credentials.h" +#include "securityclient.h" +#include "securitylogger.h" + +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(); + } + 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); + } +} + +/** + * 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; + + // 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 (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]; + } + valueTarget["index"] = values.freeze(); + } else { + getValueFromBuffer(valueTarget, pValueBuffer, nElements); + } + } +} + +/** + * 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; + } + 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()); + } +} + +/** + * Do necessary preprocessing before put operations. Check if put is allowed. + * + * @param pDbChannel channel to do preprocessing for + * @param securityLogger the logger that will audit security events + * @param credentials client credentials that are applied to this execution context + * @param securityClient the security client. Keep in scope around the put operation + */ +void +IOCSource::doPreProcessing(dbChannel* pDbChannel, SecurityLogger& securityLogger, const Credentials& credentials, + const SecurityClient& securityClient) { + if (pDbChannel->addr.special == SPC_ATTRIBUTE) { + throw std::runtime_error("Unable to put value: Modifications not allowed: S_db_noMod"); + } else if (pDbChannel->addr.precord->disp && pDbChannel->addr.pfield != &pDbChannel->addr.precord->disp) { + throw std::runtime_error("Unable to put value: Field Disabled: S_db_putDisabled"); + } + + SecurityLogger asWritePvt( + asTrapWriteWithData((securityClient.cli)[0], // The user is the first element + credentials.cred[0].c_str(), // The user is the first element + credentials.host.c_str(), + pDbChannel, + dbChannelFinalFieldType(pDbChannel), + dbChannelFinalElements(pDbChannel), + nullptr + ) + ); + + securityLogger.swap(asWritePvt); + +} + +/** + * Do necessary preprocessing before put operations. Check if put is allowed. + * + * @param securityClient security client applied to this execution context + */ +void IOCSource::doFieldPreProcessing(const SecurityClient& securityClient) { + if (!securityClient.canWrite()) { + // TODO this will abort the whole group put operation, so may be a behavior change, need to check + throw std::runtime_error("Put not permitted"); + } +} + +/** + * Do necessary post processing after put operations. If this field is a processing record then do processing + * and set status + * Note: Only called when dbPutField() is not called. + * + * @param pDbChannel channel to do post processing for + * @param forceProcessing whether to force processing, True, False + */ +void IOCSource::doPostProcessing(dbChannel* pDbChannel, TriState forceProcessing) { + if (pDbChannel->addr.pfield == &pDbChannel->addr.precord->proc || + (forceProcessing == True) || + (pDbChannel->addr.pfldDes->process_passive && + pDbChannel->addr.precord->scan == 0 && + dbChannelFinalFieldSize(pDbChannel) < DBR_PUT_ACKT && + forceProcessing == Unset)) { + if (pDbChannel->addr.precord->pact) { + if (dbAccessDebugPUTF && pDbChannel->addr.precord->tpro) { + printf("%s: single source onPut to Active '%s', setting RPRO=1\n", + epicsThreadGetNameSelf(), pDbChannel->addr.precord->name); + } + pDbChannel->addr.precord->rpro = TRUE; + } else { + pDbChannel->addr.precord->putf = TRUE; + DBErrorMessage dbErrorMessage(dbProcess(pDbChannel->addr.precord)); + if (dbErrorMessage) { + throw std::runtime_error(dbErrorMessage.c_str()); + } + } + } +} + +/** + * Set a flag that will force processing of record in the specified security control object + * + * @param pvRequest the request + * @param securityControlObject the security control object to update + */ +void IOCSource::setForceProcessingFlag(const Value& pvRequest, + const std::shared_ptr& securityControlObject) { + pvRequest["record._options.process"] + .as([&securityControlObject](const std::string& forceProcessingOption) { + if (forceProcessingOption == "true") { + securityControlObject->forceProcessing = True; + } else if (forceProcessingOption == "false") { + securityControlObject->forceProcessing = False; + } + }); +} + +/** + * 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); + } else { + SwitchTypeCodeForTemplatedCall(valueType, getValueFromBuffer, (valueTarget, pValueBuffer)); + } +} + +/** + * Set a return value from the given database value buffer. This is the array version of the function + * + * @param valueTarget the value to set + * @param pValueBuffer the database value buffer + * @param nElements the number of elements in the buffer + */ +void IOCSource::getValueFromBuffer(Value& valueTarget, const void* pValueBuffer, const long& nElements) { + auto valueType(valueTarget.type()); + if (valueType == TypeCode::StringA) { + shared_array 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(); + } 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]; + } +} + +/** + * Utility function to get the TypeCode that the given database channel is configured for + * + * @param pDbChannel the pointer to the database channel to get the TypeCode for + * @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 valueType; + + if (dbChannelFieldType(dbChannel) == DBF_STRING && nElements == 1 && dbrType && nFinalElements > 1) { + // single character long DBF_STRING being cast to DBF_CHAR array. + valueType = TypeCode::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; + } + } + + 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; + + // 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); + } + } + + // Alarm message + if (requestedOptions & DBR_AMSG) { + getMetadataString(pValueBuffer, metadata.metadata.amsg); + if (actualOptions & DBR_AMSG) { + checkedSetStringField(metadata.metadata.amsg, alarm.message); + } + } + + // 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); + } + } + } +} + +} // pvxs +} // ioc diff --git a/ioc/iocsource.h b/ioc/iocsource.h new file mode 100644 index 0000000..e4a3cc0 --- /dev/null +++ b/ioc/iocsource.h @@ -0,0 +1,139 @@ +/* + * 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_IOCSOURCE_H +#define PVXS_IOCSOURCE_H + +#include + +#include "dbeventcontextdeleter.h" +#include "field.h" +#include "metadata.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) + +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; + +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 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); +}; + +} // pvxs +} // ioc + +#endif //PVXS_IOCSOURCE_H diff --git a/ioc/localfieldlog.cpp b/ioc/localfieldlog.cpp new file mode 100644 index 0000000..41e6ae2 --- /dev/null +++ b/ioc/localfieldlog.cpp @@ -0,0 +1,30 @@ +/* + * 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 + * + */ + +#include "localfieldlog.h" + +namespace pvxs { +namespace ioc { + +LocalFieldLog::LocalFieldLog(dbChannel* pDbChannel, db_field_log* existingFieldLog) + :pFieldLog(existingFieldLog) { + if (!pFieldLog && (ellCount(&pDbChannel->pre_chain) != 0 || ellCount(&pDbChannel->pre_chain) == 0)) { + pFieldLog = db_create_read_log(pDbChannel); + if (pFieldLog) { + pFieldLog = dbChannelRunPreChain(pDbChannel, pFieldLog); + if (pFieldLog) { + pFieldLog = dbChannelRunPostChain(pDbChannel, pFieldLog); + owned = true; + } + } + } +} + +} // pvxs +} // ioc diff --git a/ioc/localfieldlog.h b/ioc/localfieldlog.h new file mode 100644 index 0000000..86b1fef --- /dev/null +++ b/ioc/localfieldlog.h @@ -0,0 +1,34 @@ +/* + * 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_LOCALFIELDLOG_H +#define PVXS_LOCALFIELDLOG_H + +#include +#include +namespace pvxs { +namespace ioc { + +class LocalFieldLog { +public: + db_field_log* pFieldLog; + bool owned = false; + explicit LocalFieldLog(dbChannel* pDbChannel, db_field_log* existingFieldLog = nullptr); + ~LocalFieldLog() { + if (owned) { + db_delete_field_log(pFieldLog); + } + } + +}; + +} // pvxs +} // ioc + +#endif //PVXS_LOCALFIELDLOG_H diff --git a/ioc/metadata.h b/ioc/metadata.h new file mode 100644 index 0000000..6d182fe --- /dev/null +++ b/ioc/metadata.h @@ -0,0 +1,115 @@ +/* + * 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 b21aabb..f2dc25d 100644 --- a/ioc/pvxs/iochooks.h +++ b/ioc/pvxs/iochooks.h @@ -1,4 +1,4 @@ -/** +/* * 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. @@ -26,7 +26,6 @@ # define PVXS_IOC_API #endif - namespace pvxs { namespace server { class Server; @@ -55,7 +54,7 @@ namespace ioc { * return; * * server::SharedPV mypv(...); - * ioc::server() + * ioc::iocServer() * .addPV("my:pv:name", mypv); * } * static void myregistrar() { @@ -67,8 +66,20 @@ namespace ioc { * @endcode */ PVXS_IOC_API -server::Server server(); +server::Server& server(); -}} // namespace pvxs::ioc +/** + * 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 + * @return 0 for success, 1 for failure + * @since UNRELEASED + */ +PVXS_IOC_API +long dbLoadGroup(const char* jsonFilename); +} +} // namespace pvxs::ioc #endif // PVXS_IOCHOOKS_H diff --git a/ioc/pvxsIoc.dbd b/ioc/pvxsIoc.dbd index fad48df..4705874 100644 --- a/ioc/pvxsIoc.dbd +++ b/ioc/pvxsIoc.dbd @@ -1 +1,9 @@ -registrar(pvxsRegistrar) +registrar(pvxsBaseRegistrar) +registrar(pvxsSingleSourceRegistrar) +registrar(pvxsGroupSourceRegistrar) + +# from demo.cpp +device(waveform, CONSTANT, devWfPDBDemo, "QSRV Demo") +device(longin, CONSTANT, devLoPDBUTag, "QSRV Set UTag") +# from imagedemo.c +function(QSRV_image_demo) diff --git a/ioc/securityclient.cpp b/ioc/securityclient.cpp new file mode 100644 index 0000000..eb21c7b --- /dev/null +++ b/ioc/securityclient.cpp @@ -0,0 +1,55 @@ +/* + * 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 + * + */ + +#include +#include +#include + +#include "securityclient.h" + +namespace pvxs { +namespace ioc { + +void SecurityClient::update(dbChannel* ch, Credentials& cred) { + SecurityClient temp; + temp.cli.resize(cred.cred.size(), nullptr); + + for (size_t i = 0, N = temp.cli.size(); i < N; i++) { + /* asAddClient() fails secure to no-permission */ + (void)asAddClient(&temp.cli[i], + dbChannelRecord(ch)->asp, + dbChannelFldDes(ch)->as_level, + cred.cred[i].c_str(), + // TODO switch to vector of char to accommodate inplace modifications to string + const_cast(cred.host.data())); + } + + cli.swap(temp.cli); +} + +SecurityClient::~SecurityClient() { + for (auto asc: cli) { + asRemoveClient(&asc); + } +} + +bool SecurityClient::canWrite() const { + return std::all_of(cli.begin(), cli.end(), [](ASCLIENTPVT asc) { + return asCheckPut(asc); + }); +} + +PutOperationCache::~PutOperationCache() { + // To avoid bug epics-base: unchecked access to notify.chan + if (notify.chan) { + dbNotifyCancel(¬ify); + } +} +} // pvxs +} // ioc diff --git a/ioc/securityclient.h b/ioc/securityclient.h new file mode 100644 index 0000000..f54e005 --- /dev/null +++ b/ioc/securityclient.h @@ -0,0 +1,74 @@ +/* + * 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_SECURITYCLIENT_H +#define PVXS_SECURITYCLIENT_H + +#include +#include +#include +#include + +#include "credentials.h" +#include "typeutils.h" + +namespace pvxs { +namespace ioc { + +class SecurityClient { +public: + std::vector cli; + ~SecurityClient(); + void update(dbChannel* ch, Credentials& cred); + bool canWrite() const; +}; + +/** + * Security objects that can be controlled + */ +class SecurityControlObject { +public: + bool done = false; + TriState forceProcessing{ Unset }; +}; + +/** + * group security cache - for storing group security credentials and clients + */ +class GroupSecurityCache : public SecurityControlObject { +public: + std::vector securityClients; + std::unique_ptr credentials; +}; + +/** + * sing security cache - for storing single a source security credential and client + */ +class SingleSecurityCache : public SecurityControlObject { +public: + SecurityClient securityClient; + std::unique_ptr credentials; +}; + +/** + * The put operation cache for caching information about the current client put connection + * Includes a single security cache as well as information pertaining to asynchronous put operations + */ +struct PutOperationCache : public SingleSecurityCache { + bool doWait{ false }; + processNotify notify{}; + Value valueToSet; + std::unique_ptr putOperation; + ~PutOperationCache(); +}; + +} // pvxs +} // ioc + +#endif //PVXS_SECURITYCLIENT_H diff --git a/ioc/securitylogger.h b/ioc/securitylogger.h new file mode 100644 index 0000000..6510b5b --- /dev/null +++ b/ioc/securitylogger.h @@ -0,0 +1,45 @@ +/* + * 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_SECURITYLOGGER_H +#define PVXS_SECURITYLOGGER_H + +#include +#include + +namespace pvxs { +namespace ioc { + +class SecurityLogger { + void* pvt; +public: + ~SecurityLogger() { + asTrapWriteAfterWrite(pvt); + } + + void swap(SecurityLogger& o) { + std::swap(pvt, o.pvt); + } + + explicit SecurityLogger(void* pvt) + :pvt(pvt) { + } + + SecurityLogger() + :pvt(nullptr) { + } + + SecurityLogger(const SecurityLogger&) = delete; + SecurityLogger& operator=(const SecurityLogger&) = delete; +}; + +} // pvxs +} // ioc + +#endif //PVXS_SECURITYLOGGER_H diff --git a/ioc/singlesource.cpp b/ioc/singlesource.cpp new file mode 100644 index 0000000..d17dac6 --- /dev/null +++ b/ioc/singlesource.cpp @@ -0,0 +1,430 @@ +/* + * 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 + * + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "dbentry.h" +#include "dberrormessage.h" +#include "dblocker.h" +#include "iocsource.h" +#include "singlesource.h" +#include "singlesrcsubscriptionctx.h" +#include "credentials.h" +#include "securitylogger.h" +#include "securityclient.h" +#include "typeutils.h" +#include "localfieldlog.h" + +namespace pvxs { +namespace ioc { + +DEFINE_LOGGER(_logname, "pvxs.ioc.single.source"); + +/** + * Constructor for SingleSource registrar. + */ +SingleSource::SingleSource() + :eventContext(db_init_events()) // Initialise event context +{ + auto names(std::make_shared>()); + + // For each record type and for each record in that type, add record name to the list of all records + DBEntry dbEntry; + for (long status = dbFirstRecordType(dbEntry); !status; status = dbNextRecordType(dbEntry)) { + for (status = dbFirstRecord(dbEntry); !status; status = dbNextRecord(dbEntry)) { + names->insert(dbEntry->precnode->recordname); + } + } + + allRecords.names = names; + + // Start event pump + if (!eventContext) { + throw std::runtime_error("Single Source: Event Context failed to initialise: db_init_events()"); + } + + if (db_start_events(eventContext.get(), "qsrvSingle", nullptr, nullptr, epicsThreadPriorityCAServerLow - 1)) { + throw std::runtime_error("Could not start event thread: db_start_events()"); + } +} + +/** + * Handle the create source operation. This is called once when the source is created. + * We will register all of the database records that have been loaded until this time as pv names in this + * source. + * @param channelControl + */ +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); + 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()); + } + + // Create callbacks for handling requests and channel subscriptions + createRequestAndSubscriptionHandlers(std::move(channelControl), dbChannelSharedPtr); +} + +/** + * Respond to search requests. For each matching pv, claim that pv + * + * @param searchOperation the search operation + */ +void SingleSource::onSearch(Search& searchOperation) { + for (auto& pv: searchOperation) { + if (!dbChannelTest(pv.name())) { + pv.claim(); + log_debug_printf(_logname, "Claiming '%s'\n", pv.name()); + } + } +} + +/** + * Respond to the show request by displaying a list of all the PVs hosted in this ioc + * + * @param outputStream the stream to show the list on + */ +void SingleSource::show(std::ostream& outputStream) { + outputStream << "IOC"; + for (auto& name: *SingleSource::allRecords.names) { + outputStream << "\n" << indent{} << name; + } +} + +/** + * 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) { + auto subscriptionContext(std::make_shared(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 + // Shared ptr for one of captured vars below + subscriptionContext->currentValue = valuePrototype; + channelControl + ->onSubscribe([this, subscriptionContext](std::unique_ptr&& subscriptionOperation) { + 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 + 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 new file mode 100644 index 0000000..1933f10 --- /dev/null +++ b/ioc/singlesource.h @@ -0,0 +1,122 @@ +/* + * 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_SINGLESOURCE_H +#define PVXS_SINGLESOURCE_H + +#include +#include + +#include "dbeventcontextdeleter.h" +#include "iocsource.h" +#include "metadata.h" +#include "singlesrcsubscriptionctx.h" + +namespace pvxs { +namespace ioc { + +/** + * Single Source class to handle initialisation, processing, and shutdown of single source database record support + * - Handlers for get, put and subscriptions + * - type converters to and from pvxs and db + */ +class SingleSource : public server::Source { +public: + SingleSource(); + void onCreate(std::unique_ptr&& channelControl) final; + List onList() final { + return allRecords; + } + + void onSearch(Search& searchOperation) final; + void show(std::ostream& outputStream) final; + +private: + // List of all database records that this single source serves + 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 +} // pvxs + + +#endif //PVXS_SINGLESOURCE_H diff --git a/ioc/singlesourcehooks.cpp b/ioc/singlesourcehooks.cpp new file mode 100644 index 0000000..65b2af4 --- /dev/null +++ b/ioc/singlesourcehooks.cpp @@ -0,0 +1,125 @@ +/* + * 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 + * + */ + +#include +#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 + +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; + } + } + + // For each registered source/IOID pair print a line of either detailed or regular information + for (auto& pair: pPvxsServer->listSource()) { + auto& record = pair.first; + auto& ioId = pair.second; + + auto source = pPvxsServer->getSource(record, ioId); + if (!source) { + // if the source is not yet available in the server then we're in a race condition + // silently skip source + continue; + } + + auto list = source->onList(); + + if (list.names && !list.names->empty()) { + if (showDetails) { + 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) { + printf(" "); + } + printf("%s\n", name.c_str()); + } + } + } + }); +} + +} +} // namespace pvxs::ioc + +using namespace pvxs::ioc; + +namespace { + +/** + * 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 (theInitHookState == initHookAfterIocBuilt) { + pvxs::ioc::iocServer().addSource("qsrvSingle", std::make_shared(), 0); + } +} + +/** + * IOC pvxs Single Source registrar. This implements the required registrar function that is called by xxxx_registerRecordDeviceDriver, + * the auto-generated stub created for all IOC implementations. + * + * It is registered by using the `epicsExportRegistrar()` macro. + * + * 1. Specify here all of the commands that you want to be registered and available in the IOC shell. + * 2. Register your hook handler to handle any state hooks that you want to implement. Here we install + * an `initHookState` handler connected to the `initHookAfterIocBuilt` state. It will add all of the + * single record type sources defined so far. Note that you can define sources up until the `iocInit()` call, + * after which point the `initHookAfterIocBuilt` handlers are called and will register all the defined records. + */ +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") + .implementation<&pvxsl>(); + + initHookRegister(&qsrvSingleSourceInit); +} + +} // namespace + +// in .dbd file +//registrar(pvxsSingleSourceRegistrar) +extern "C" { +epicsExportRegistrar(pvxsSingleSourceRegistrar); +} + diff --git a/ioc/singlesrcsubscriptionctx.cpp b/ioc/singlesrcsubscriptionctx.cpp new file mode 100644 index 0000000..c6b171b --- /dev/null +++ b/ioc/singlesrcsubscriptionctx.cpp @@ -0,0 +1,31 @@ +/* + * 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 + * + */ + +#include "singlesrcsubscriptionctx.h" + +namespace pvxs { +namespace ioc { + +/** + * Constructor for single source subscription context using a pointer to a db channel + * + * @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(); + } + +} +} // iocs +} // pvxs diff --git a/ioc/singlesrcsubscriptionctx.h b/ioc/singlesrcsubscriptionctx.h new file mode 100644 index 0000000..ee89708 --- /dev/null +++ b/ioc/singlesrcsubscriptionctx.h @@ -0,0 +1,43 @@ +/* + * 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_SINGLESRCSUBSCRIPTIONCTX_H +#define PVXS_SINGLESRCSUBSCRIPTIONCTX_H + +#include + +#include + +#include "subscriptionctx.h" + +namespace pvxs { +namespace ioc { + +/** + * 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; + + // This is used to store the current value. Each subscription event simply merges + // new fields into this value + Value currentValue{}; + epicsMutex eventLock{}; + std::unique_ptr subscriptionControl{}; +}; + +} // ioc +} // pvxs + +#endif //PVXS_SINGLESRCSUBSCRIPTIONCTX_H diff --git a/ioc/subscriptionctx.h b/ioc/subscriptionctx.h new file mode 100644 index 0000000..94e13f3 --- /dev/null +++ b/ioc/subscriptionctx.h @@ -0,0 +1,58 @@ +/* + * 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_SUBSCRIPTIONCTX_H +#define PVXS_SUBSCRIPTIONCTX_H + +#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); \ + } \ + }) + +namespace pvxs { +namespace ioc { + +/** + * A subscription context + */ +class SubscriptionCtx { +public: +// For locking access to subscription context + std::shared_ptr pValueEventSubscription{}; + std::shared_ptr pPropertiesEventSubscription{}; + bool hadValueEvent = false; + bool hadPropertyEvent = false; +}; + +} // ioc +} // pvxs + +#endif //PVXS_SUBSCRIPTIONCTX_H diff --git a/ioc/typeutils.cpp b/ioc/typeutils.cpp new file mode 100644 index 0000000..3f23b9a --- /dev/null +++ b/ioc/typeutils.cpp @@ -0,0 +1,100 @@ +/* + * 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 + * + */ + +#include + +#include + +#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 + * + * @param dbrType the database record type code + * @return a pvxs type code + * + */ +TypeCode fromDbrType(short dbrType) { + switch (dbrType) { + case DBR_CHAR: + return TypeCode::Int8; + case DBR_UCHAR: + return TypeCode::UInt8; + case DBR_SHORT: + return TypeCode::Int16; + case DBR_USHORT: + case DBR_ENUM: + return TypeCode::UInt16; + case DBR_LONG: + return TypeCode::Int32; + case DBR_ULONG: + return TypeCode::UInt32; + case DBR_INT64: + return TypeCode::Int64; + case DBR_UINT64: + return TypeCode::UInt64; + case DBR_FLOAT: + return TypeCode::Float32; + case DBR_DOUBLE: + return TypeCode::Float64; + case DBR_STRING: + return TypeCode::String; + case DBR_NOACCESS: + default: + return TypeCode::Null; + } +} + +} diff --git a/ioc/typeutils.h b/ioc/typeutils.h new file mode 100644 index 0000000..3dfcc75 --- /dev/null +++ b/ioc/typeutils.h @@ -0,0 +1,70 @@ +/* + * 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_TYPEUTILS_H +#define PVXS_TYPEUTILS_H + +#include +#include + +#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) ); \ +} + +namespace pvxs { + +TypeCode fromDbfType(dbfType dbfType); +TypeCode fromDbrType(short dbrType); + +namespace ioc { + +/** + * Tristate value for status flags + */ +typedef enum { + Unset, + True, + False +} TriState; + +} +} +#endif //PVXS_TYPEUTILS_H diff --git a/ioc/yajlcallbackhandler.h b/ioc/yajlcallbackhandler.h new file mode 100644 index 0000000..2523b7f --- /dev/null +++ b/ioc/yajlcallbackhandler.h @@ -0,0 +1,51 @@ +/* + * 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_YAJLCALLBACKHANDLER_H +#define PVXS_YAJLCALLBACKHANDLER_H + +#include + +#include + +namespace pvxs { +namespace ioc { + +class YajlCallbackHandler { + yajl_handle handle; +public: +/** + * Set the callback handler for the yajl parser + * + * @param yajlHandler the allocated handler to set + */ + explicit YajlCallbackHandler(yajl_handle yajlHandler) + :handle(yajlHandler) { + if (!handle) { + throw std::runtime_error("Failed to allocate yajl handle"); + } + } + +/** + * Destructor for the callback handler + */ + ~YajlCallbackHandler() { + yajl_free(handle); + } + + // NOLINT(google-explicit-constructor) + operator yajl_handle() { + return handle; + } +}; + +} // pvxs +} // ioc + +#endif //PVXS_YAJLCALLBACKHANDLER_H diff --git a/qsrv/Makefile b/qsrv/Makefile new file mode 100644 index 0000000..e8033f6 --- /dev/null +++ b/qsrv/Makefile @@ -0,0 +1,40 @@ +TOP=.. + +include $(TOP)/configure/CONFIG +# cfg/ sometimes isn't correctly included due to a Base bug +# so we do here (maybe again) as workaround +include $(TOP)/configure/CONFIG_PVXS_MODULE +include $(TOP)/configure/CONFIG_PVXS_VERSION +#---------------------------------------- +# ADD MACRO DEFINITIONS AFTER THIS LINE +#============================= + +PROD_IOC = qsrv + +DBDDEPENDS_FILES += qsrv.dbd$(DEP) +qsrv_DBD = base.dbd pvxsIoc.dbd +DBD_INSTALLS = $(COMMON_DIR)/qsrv.dbd + +qsrv_DB = qsrvExit.db + +FINAL_LOCATION ?= $(shell $(PERL) $(TOOLS)/fullPathName.pl $(INSTALL_LOCATION)) + +# EPICS_BASE macro for our location +USR_CPPFLAGS += -DEPICS_BASE="\"$(FINAL_LOCATION)\"" + +qsrv_SRCS_DEFAULT += qsrvMain.cpp +qsrv_SRCS += qsrv_registerRecordDeviceDriver.cpp +qsrv_SRCS_vxWorks += -nil- + +# Finally link IOC to the EPICS Base libraries +qsrv_LIBS += pvxsIoc +qsrv_LIBS += pvxs +qsrv_LIBS += $(EPICS_BASE_IOC_LIBS) + +#=========================== + +include $(TOP)/configure/RULES +include $(TOP)/configure/RULES_PVXS_MODULE +#---------------------------------------- +# ADD RULES AFTER THIS LINE + diff --git a/qsrv/qsrvExit.db b/qsrv/qsrvExit.db new file mode 100644 index 0000000..5c310e1 --- /dev/null +++ b/qsrv/qsrvExit.db @@ -0,0 +1,15 @@ +# qsrvExit.db + +record(sub,"$(IOC):exit") { + field(DESC,"Exit subroutine") + field(SCAN,"Passive") + field(SNAM,"exit") +} + +record(stringin,"$(IOC):BaseVersion") { + field(DESC,"EPICS Base Version") + field(DTYP,"getenv") + field(INP,"@EPICS_VERSION_FULL") + field(PINI,"YES") + field(DISP,1) +} diff --git a/qsrv/qsrvMain.cpp b/qsrv/qsrvMain.cpp new file mode 100644 index 0000000..c4f3070 --- /dev/null +++ b/qsrv/qsrvMain.cpp @@ -0,0 +1,361 @@ +/* + * 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. + * + * qsrvMain.cpp: The main entry point for the pvxs qsrv soft IOC. + * Use this as is, or as the base for your customised IOC application + */ +#include +#include "epicsExit.h" +#include "epicsThread.h" +#include "iocsh.h" + +#include +#include +#include +#include +#include +#include +#include "pvxs/iochooks.h" + +#include "qsrvMain.h" + +namespace pvxs { +namespace qsrv { + +static void exitCallback(subRecord* pRecord); + +// Verbose flag - if true then show verbose output +bool verboseFlag = false; +// Macro to use to show verbose output +// e.g. VERBOSE_MESSAGE "some verbose message: " << variable << " more verbosity\n"; +#define VERBOSE_MESSAGE if (verboseFlag) std::cout << + +// DB Loaded flag - true if the database has already been loaded +bool isDbLoaded = false; + +/** + * Print out usage information for the QSRV application + * + * @param executableName the name of the executable as passed to main + * @param initialisationFile the name of the database initialization file. Either the one + * specified on the commandline or the default one + */ +void usage(std::string& executableName, const std::string& initialisationFile) { + std::string executableBaseName = executableName + .substr(executableName.find_last_of(OSI_PATH_SEPARATOR) + 1) // NOLINT(performance-faster-string-find) + ; + std::cout << "PVXS configurable IOC server\n\n" + "Usage: " << executableBaseName << + " [-h] [-S] [-v] \n" + " [-m =[,=]...] ... \n" + " [-D ] [-G ] [-a ] [-d ] \n" + " [-x ] []\n" + "\nDescription:\n" + " Start an in-memory database of PV records which can be accessed via PVAccess, and start an IOC shell.\n\n" + " After configuring the in-memory database with " << initialisationFile.c_str() + << "\n (or overriden with the -D option) this command starts an interactive IOC shell, unless the -S flag \n" + " is specified. Group configuration can optionally be specified using the -G flag, and security can be \n" + " configured using the -a flag. An initial database of PV records can be established using the -d flag. \n" + " Finally some startup commands can be run if an optional script-path is specified." + "\n" + "\nCommon flags:\n" + " -h Print this message and exit.\n" + " -S Prevents an interactive shell being started. \n" + " -v Verbose, display steps taken during startup.\n" + " -D This overrides the default database configuration file.\n" + " If specified, it must come before any (-G), (-a), or (-d) flags. Specify \n" + " the path to the configuration file as well as the\n" + " extension. By convention, this file has a .dbd \n" + " extension. The compile-time default configuration file\n" + " is " FULL_PATH_TO_INITIALISATION_FILE ".\n" + " -m =[,=]... \n" + " Set/replace macro definitions. Macros are used in the \n" + " iocsh as well as when parsing the access security configuration \n" + " (-a) and database records file (-d). You can provide a (-m) flag \n" + " multiple times on the same command line. Each occurrence applies to any \n" + " (-a) and (-d) options that follow it until the next (-m) overrides. The last\n" + " macros defined are used in the iocsh\n" + " -a Access security configuration file. Use this flag \n" + " to configure access security. The security definitions specified \n" + " are subject to macro substitution.\n" + " -d Load database record-definitions and group-definitions from file. Each record-definition " + " contains a set of field-definitions: `record(,\"\") { field(,\"\")... }...`. " + " Additionally group-info-definitions are accepted in the place of field-definitions.\n" + " group-info-definition := `info(Q:group, {\"\": {...}})`. \n" + " These group-info-definitions define group-names and map into them the \n" + " specified fields from the record-definition in which they appear. By convention, \n" + " the extension is .db. The definitions specified in the given file are subject to macro substitution \n" + " -G Load database group-definitions from a JSON file. If path starts with `-` then the remaining portion \n" + " is treated as a group-definitions file to be removed from the list of files to load. \n" + " e.g. `-G -grpFile.json` will remove grpFile.json from the list of group-definitions \n" + " files to load. If `-G -*` or `-G -` is specified, then all group-definitions files \n" + " that have been specified so far will be removed. By convention, the extension is .json. \n" + " -x Specifies the prefix for the database exit record Load. It is used as a substitution for \n" + " the $(IOC) macro in " FULL_PATH_TO_EXIT_FILE ". \n" + " This file is used to configure database shutdown.\n" + " script-path A command script is optional. The iocsh commands will be run *after*\n" + " calling iocInit(). If you want to run the script before iocInit() then \n" + " don't specify any (-d), (-G) or (-x) flags and perform all database loading in \n" + " the script itself, or in the interactive shell including calling iocInit().\n" + "\n" + "Examples:\n" + " " << executableBaseName + << " -d my.db\n" + " use default configuration, load database record-definitions \n" + " and group-definitions from `my.db`, and start an interactive IOC shell \n" + " " << executableBaseName + << " -m NAME=PV -d my.db\n" + " use default configuration, and load database record-definitions \n" + " and group-definitions from `my.db`, after setting macro `NAME` to `PV`\n" + " " << executableBaseName + << " -D my-config.dbd -d my.db -G my-group-mappings.json \n" + " use custom configuration `my-config.dbd` to configure the IOC, \n" + " load database record-definitions and group-definitions from `my.db`, \n" + " then load additional group-definitions from `my-group-mappings.json,` \n" + " and start an interactive shell \n"; + +} + +/** + * Configure the database if it has not been configured previously + * + * @param databaseConfigurationFile the name of the file containing configuration information + */ +void configureDatabase(const std::string& databaseConfigurationFile) { + // Only load configuration file if it has been configured previously + if (isDbLoaded) { + return; + } + isDbLoaded = true; + + VERBOSE_MESSAGE "dbLoadDatabase(\"" << databaseConfigurationFile << "\")\n"; + if (dbLoadDatabase(databaseConfigurationFile.c_str(), nullptr, nullptr)) { + throw std::runtime_error( + std::string("Failed to load database configuration file: ") + databaseConfigurationFile); + } + + // Must match the dbd you've established in your header file and Makefile as the default configuration file + VERBOSE_MESSAGE "qsrv_registerRecordDeviceDriver(pdbbase)\n"; + qsrv_registerRecordDeviceDriver(pdbbase); + registryFunctionAdd("exit", (REGISTRYFUNCTION)exitCallback); +} + +/** + * The exit callback function + * + * @param pRecord + */ +void exitCallback(subRecord* pRecord) { + epicsExitLater((pRecord->a == 0.0) ? EXIT_SUCCESS : EXIT_FAILURE); +} + +/** + * Get the relative path prefix (the directory that this executable is running from) + * + * @return the prefix to add to any relative paths + */ +std::string getPrefix() { + std::string prefix; + char* cPrefix = epicsGetExecDir(); + if (cPrefix) { + try { + prefix = cPrefix; + } catch (...) { + free(cPrefix); + throw; + } + } + free(cPrefix); + return prefix; +} + +/** + * Parse the command line arguments + * + * @param argc argument count + * @param argv argument values + * @param databaseInitialisationFile the default database initialization file, overridden by (-D) option if specified + * @param dbIsLoaded set to true if database is loaded by a (-d) or (-x) option + * @param shouldStartAnInteractiveSession set to true if an interactive session should be started, overridden by (-S) option if specified + * @param scriptName set to the scriptName if specified + * + * @return positive integer if unsuccessful, negative if successful but needs to exit, zero means success + */ +int parseOptions(int argc, char* argv[], std::string& databaseInitialisationFile, bool& dbIsLoaded, + bool& shouldStartAnInteractiveSession, std::string& scriptName) { + std::string iocExecutableName(argv[0]); + std::string databaseShutdownFile(FULL_PATH_TO_EXIT_FILE); + + std::string commaSeparatedListOfMacroDefinitions; // This is set if a (-m) option is specified + + // compute relative paths + { + std::string prefix = getPrefix(); + + databaseInitialisationFile = prefix + RELATIVE_PATH_TO_INITIALISATION_FILE; + databaseShutdownFile = prefix + RELATIVE_PATH_TO_SHUTDOWN_FILE; + } + + // Parse the command line and configure and start the IOC + int opt; // parsed option from the command line + while ((opt = getopt(argc, argv, "a:D:d:G:hm:Svx:")) != -1) { + switch (opt) { + case 'a': + configureDatabase(databaseInitialisationFile); + if (!commaSeparatedListOfMacroDefinitions.empty()) { + VERBOSE_MESSAGE "asSetSubstitutions(\"" << commaSeparatedListOfMacroDefinitions << "\")\n"; + if (asSetSubstitutions(commaSeparatedListOfMacroDefinitions.c_str())) + throw std::bad_alloc(); + } + VERBOSE_MESSAGE "asSetFilename(\"" << optarg << "\")\n"; + if (asSetFilename(optarg)) { + throw std::bad_alloc(); + } + break; + case 'D': + if (isDbLoaded) { + throw std::runtime_error("database configuration file override specified " + "after " FULL_PATH_TO_INITIALISATION_FILE " has already been loaded.\n" + "Add the -D option prior to any -d or -x options and try again"); + } + databaseInitialisationFile = optarg; + break; + case 'd': + configureDatabase(databaseInitialisationFile); + VERBOSE_MESSAGE "dbLoadRecords(\"" << optarg << "\"" + << ((commaSeparatedListOfMacroDefinitions.empty()) ? "" : + std::string(", \"").append(commaSeparatedListOfMacroDefinitions) + .append("\"")) + << ")\n"; + + if (dbLoadRecords(optarg, commaSeparatedListOfMacroDefinitions.c_str())) { + throw std::runtime_error(std::string("Failed to load: ") + optarg); + } + + dbIsLoaded = true; + break; + case 'G': + pvxs::ioc::dbLoadGroup(optarg); + break; + case 'h': + usage(iocExecutableName, databaseInitialisationFile); + epicsExit(0); + return -1; + case 'm': + commaSeparatedListOfMacroDefinitions = optarg; + break; + case 'S': + shouldStartAnInteractiveSession = false; + break; + case 'v': + verboseFlag = true; + break; + case 'x': { + std::string xmacro; + configureDatabase(databaseInitialisationFile); + xmacro = "IOC="; + xmacro += optarg; + + if (dbLoadRecords(databaseShutdownFile.c_str(), xmacro.c_str())) { + throw std::runtime_error(std::string("Failed to load: ") + databaseShutdownFile); + } + + dbIsLoaded = true; + } + break; + default: + usage(iocExecutableName, databaseInitialisationFile); + std::cerr << "Unknown argument: -" << char(optopt) << "\n"; + epicsExit(2); + return 2; + } + } + + // If script specified then return name + if (optind < argc) { + scriptName = argv[optind]; + } + + return 0; +} + +} +} // pvxs::qsrv + +using namespace pvxs::qsrv; + +/** + * Main entry point for the qsrv executable. + * + * @param argc the number of command line arguments + * @param argv the command line arguments + * @return 0 for successful exit, nonzero otherwise + */ +int main(int argc, char* argv[]) { + try { + std::string databaseInitialisationFile(FULL_PATH_TO_INITIALISATION_FILE); + std::string scriptName; + bool shouldStartAnInteractiveSession = true; // Default is true, unless (-S) option is specified + bool dbIsLoaded = false; // Is database loaded + auto status = parseOptions(argc, argv, + databaseInitialisationFile, dbIsLoaded, shouldStartAnInteractiveSession, scriptName); + if (status != 0) { + return status > 0 ? status : 0; + } + + // Configure the database with the specified configuration file + configureDatabase(databaseInitialisationFile); + + // If we've loaded a database file or configured the exit callback, then do an iocInit() + if (dbIsLoaded) { + VERBOSE_MESSAGE "iocInit()\n"; + iocInit(); + epicsThreadSleep(0.2); + } + + // If we've specified a script on the command line then run it + bool userScriptHasBeenExecuted = false; + if (!scriptName.empty()) { + VERBOSE_MESSAGE "# Begin execution of: " << scriptName << "\n"; + if (iocsh(scriptName.c_str())) { + throw std::runtime_error(std::string("Error in ") + scriptName); + } + VERBOSE_MESSAGE "# End execution of: " << scriptName << "\n"; + + epicsThreadSleep(0.2); + userScriptHasBeenExecuted = true; + } + + // If we haven't disabled the interactive shell then enter it + if (shouldStartAnInteractiveSession) { + std::cout.flush(); + std::cerr.flush(); + if (iocsh(nullptr)) { + // if error status then propagate error to epics and shell + epicsExit(1); + return 1; + } + } else { + // If non-interactive then exit + if (dbIsLoaded || userScriptHasBeenExecuted) { + epicsExitCallAtExits(); + epicsThreadSleep(0.1); + epicsThreadSuspendSelf(); + } else { + // Indicate that there was probably an error if nothing was loaded or executed + std::cerr << "Nothing to do!\n"; + epicsExit(1); + return 1; + } + } + + epicsExit(0); + return (0); + } catch (std::exception& e) { + std::cerr << "Error: " << e.what() << "\n"; + epicsExit(2); + return 2; + } +} diff --git a/qsrv/qsrvMain.h b/qsrv/qsrvMain.h new file mode 100644 index 0000000..abcca25 --- /dev/null +++ b/qsrv/qsrvMain.h @@ -0,0 +1,33 @@ +/* + * 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. + */ + +#ifndef PVXS_QSRVMAIN_H +#define PVXS_QSRVMAIN_H + +#include "osiFileName.h" + +#ifndef EPICS_BASE +#define EPICS_BASE "" +#error -DEPICS_BASE= required while building this component +#endif + +#define IOC_SERVER_NAME "qsrv" + +// The name of the database initialization file used for startup +#define DEFAULT_INITIALISATION_FILENAME IOC_SERVER_NAME ".dbd" +#define INITIALISATION_FILENAME "dbd" OSI_PATH_SEPARATOR DEFAULT_INITIALISATION_FILENAME +#define FULL_PATH_TO_INITIALISATION_FILE EPICS_BASE OSI_PATH_SEPARATOR INITIALISATION_FILENAME +#define RELATIVE_PATH_TO_INITIALISATION_FILE ".." OSI_PATH_SEPARATOR ".." OSI_PATH_SEPARATOR INITIALISATION_FILENAME +// automatically generated from dbd file. This must match the initialization filename you choose above. xxxx_registerRecordDeviceDriver() +extern "C" int qsrv_registerRecordDeviceDriver(struct dbBase* pdbbase); + +// The name of the database shutdown file used for exit +// Must match with `xxxx_DBD = xxxx.dbd` in your Makefile +#define SHUTDOWN_FILENAME "db" OSI_PATH_SEPARATOR IOC_SERVER_NAME "Exit.db" +#define FULL_PATH_TO_EXIT_FILE EPICS_BASE OSI_PATH_SEPARATOR SHUTDOWN_FILENAME +#define RELATIVE_PATH_TO_SHUTDOWN_FILE ".." OSI_PATH_SEPARATOR ".." OSI_PATH_SEPARATOR SHUTDOWN_FILENAME + +#endif //PVXS_QSRVMAIN_H diff --git a/test/Makefile b/test/Makefile index b802dc1..641ef41 100644 --- a/test/Makefile +++ b/test/Makefile @@ -119,6 +119,8 @@ 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 PROD_SRCS_RTEMS += rtemsTestData.c diff --git a/test/image.db b/test/image.db new file mode 100644 index 0000000..b912c64 --- /dev/null +++ b/test/image.db @@ -0,0 +1,91 @@ + + +record(longout, "$(N):ArraySize0_RBV") { + field(VAL, "100") + info(Q:group, { + "$(N):Array":{ + "dimension[0].size":{+channel:"VAL", +type:"plain", +putorder:0} + } + }) + field(FLNK, "$(N):ArraySize1_RBV") +} + +record(longout, "$(N):ArraySize1_RBV") { + field(VAL, "100") + info(Q:group, { + "$(N):Array":{ + "dimension[1].size":{+channel:"VAL", +type:"plain", +putorder:0} + } + }) + field(FLNK, "$(N):ArrayData_") +} + +record(aSub, "$(N):ArrayData_") { + field(SNAM, "QSRV_image_demo") + field(PINI, "YES") + field(FTA, "ULONG") + field(FTB, "ULONG") + field(FTVA, "USHORT") + field(NOVA, "262144") # eg. 512x512 + field(INPA, "$(N):ArraySize0_RBV NPP MSI") + field(INPB, "$(N):ArraySize1_RBV NPP MSI") + field(OUTA, "$(N):ArrayData PP MSI") +} + +record(waveform, "$(N):ArrayData") { + field(FTVL, "USHORT") + field(NELM, "262144") + info(Q:group, { + "$(N):Array":{ + +id:"epics:nt/NTNDArray:1.0", + "value":{+type:"any", + +channel:"VAL", + +trigger:"*"}, + "":{+type:"meta", +channel:"SEVR"}, + "x":{+type:"meta", +channel:"SEVR"} + } + }) +} + + +record(stringin, "$(N):ColorMode_") { + field(VAL, "ColorMode") + field(PINI, "YES") + info(Q:group, { + "$(N):Array":{ + "attribute[0].name":{+type:"plain", +channel:"VAL"} + } + }) +} + +record(mbbi, "$(N):ColorMode") { + field(ZRST, "Mono") + field(ONST, "Bayer") + field(TWST, "RGB1") + field(THST, "RGB2") + field(FRST, "RGB3") + field(FVST, "YUV444") + field(SXST, "YUV422") + field(SVST, "YUV411") + field(VAL, "0") # Gray + field(PINI, "YES") + info(Q:group, { + "$(N):Array":{ + "attribute[0].value":{+type:"any", +channel:"VAL"} + } + }) +} + +record(bo, "$(N):extra") { + field(ZNAM, "foo") + field(ONAM, "bar") + info(Q:group, { + "$(N):Array":{ + "attribute[1].value":{+type:"any", + +channel:"VAL", + +putorder:0, + +trigger:"attribute[1].value"}, + "attribute[1]":{+type:"meta", +channel:"SEVR"} + } + }) +} diff --git a/test/iq.db b/test/iq.db new file mode 100644 index 0000000..73895f0 --- /dev/null +++ b/test/iq.db @@ -0,0 +1,65 @@ +record(ao, "$(N)Rate") { + field(VAL, "1.0") + field(PINI, "YES") + field(PREC, "3") + field(DRVH, "2.0") + field(DRVL, "0.000000000001") + field( OUT, "$(N)dly_.ODLY NPP") +} + +record(ao, "$(N)Delta") { + field(VAL, "0.0") + field(PINI, "YES") +} + +record(calc, "$(N)Phase:I") { + field(PINI, "RUNNING") + field(INPA, "$(N)Phase:I") + field(CALC, "A+1") + field( EGU, "rad") + field(PREC, "3") + field(FLNK, "$(N)Phase:Q") + info(Q:group, { + "$(N)iq":{"phas.i": {+type:"plain", +channel:"VAL"}} + }) + #field(TPRO, "1") +} + +record(calc, "$(N)Phase:Q") { + field(INPA, "$(N)Phase:I") + field(INPB, "$(N)Delta NPP") + field(CALC, "A+B") + field( EGU, "rad") + field(PREC, "3") + field(FLNK, "$(N)I") + info(Q:group, { + "$(N)iq":{"phas.q": {+type:"plain", +channel:"VAL"}} + }) +} + +record(waveform, "$(N)I") { + field(DTYP, "QSRV Demo") + field( INP, "$(N)Phase:I") + field(FTVL, "DOUBLE") + field(NELM, "500") + field(FLNK, "$(N)Q") + info(Q:group, { + "$(N)iq":{"I": {+channel:"VAL"}} + }) +} + +record(waveform, "$(N)Q") { + field(DTYP, "QSRV Demo") + field( INP, "$(N)Phase:Q") + field(FTVL, "DOUBLE") + field(NELM, "500") + field(FLNK, "$(N)dly_") + info(Q:group, { + "$(N)iq":{"Q": {+channel:"VAL", +trigger:"*"}} + }) +} + +record(calcout, "$(N)dly_") { + field(ODLY, "1.0") + field(OUT , "$(N)Phase:I.PROC CA") +} diff --git a/test/ntenum.db b/test/ntenum.db new file mode 100644 index 0000000..0356e2f --- /dev/null +++ b/test/ntenum.db @@ -0,0 +1,26 @@ +# Example of constructing an NTEnum with a longer choices list. + +record(longout, "$(P):ENUM:INDEX") { + field(VAL, "1") + field(PINI, "YES") + info(Q:group, { + "$(P):ENUM":{ + +id:"epics:nt/NTEnum:1.0", + "value":{+type:"structure", +id:"enum_t"}, + "value.index":{+type:"plain", +channel:"VAL"}, + "":{+type:"meta", +channel:"VAL"} + } + }) +} + +record(aai, "$(P):ENUM:CHOICES") { + field(FTVL, "STRING") + field(NELM, "64") + field(INP , {const:["ZERO", "ONE"]}) + info(Q:group, { + "$(P):ENUM":{ + +id:"epics:nt/NTEnum:1.0", + "value.choices":{+type:"plain", +channel:"VAL"} + } + }) +} diff --git a/test/table.db b/test/table.db new file mode 100644 index 0000000..86a1480 --- /dev/null +++ b/test/table.db @@ -0,0 +1,49 @@ + +record(aai, "$(N)Labels_") { + field(FTVL, "STRING") + field(NELM, "2") + field(INP , {const:["Column A", "Column B"]}) + info(Q:group, { + "$(N)Tbl":{ + +id:"epics:nt/NTTable:1.0", + "labels":{+type:"plain", +channel:"VAL"} + } + }) + field(TPRO, "1") +} + +record(aao, "$(N)A") { + field(FTVL, "DOUBLE") + field(NELM, "10") + info(Q:group, { + "$(N)Tbl":{ + "value.A":{+type:"plain", +channel:"VAL", +putorder:1} + } + }) + field(TPRO, "1") +} + +record(aao, "$(N)B") { + field(FTVL, "DOUBLE") + field(NELM, "10") + info(Q:group, { + "$(N)Tbl":{ + "":{+type:"meta", +channel:"VAL"}, + "value.B":{+type:"plain", +channel:"VAL", +putorder:1} + } + }) + field(TPRO, "1") +} + +record(longout, "$(N)Save") { + field(MDEL, "-1") # ensure we always trigger group monitor + field(TPRO, "1") + info(Q:group, { + "$(N)Tbl":{ + "_save":{+type:"proc", + +channel:"VAL", + +putorder:2, + +trigger:"*"} + } + }) +} diff --git a/test/testioc.acf b/test/testioc.acf new file mode 100644 index 0000000..c5c09de --- /dev/null +++ b/test/testioc.acf @@ -0,0 +1,17 @@ +UAG(MYSELF) { + "$(user)" +} + +ASG(DEFAULT) { + RULE(1,WRITE,TRAPWRITE) +} + +ASG(SPECIAL) { + RULE(1,WRITE,TRAPWRITE) { + UAG(MYSELF) + } +} + +ASG(RO) { + RULE(1, READ) +} diff --git a/test/testioc.cpp b/test/testioc.cpp index 80a88f5..ebd934e 100644 --- a/test/testioc.cpp +++ b/test/testioc.cpp @@ -4,11 +4,14 @@ * in file LICENSE that is included with this distribution. */ -#include -#include +#include #include +#include +#include #include +#include +#include #include #include #include @@ -23,29 +26,287 @@ namespace { } // namespace -MAIN(testioc) -{ - testPlan(5); - testSetup(); +#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) - testdbPrepare(); +static void boxLeft(); - testThrows([]{ - ioc::server(); - }); +static pvxs::client::Context clientContext; - testdbReadDatabase("testioc.dbd", nullptr, nullptr); - testEq(0, testioc_registerRecordDeviceDriver(pdbbase)); - testEq(0, iocshCmd("pvxsr()")); - testEq(0, iocshCmd("pvxs_target_info()")); +// 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: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); - testTrue(!!ioc::server()); + 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: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: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); + }, +}; - testIocInitOk(); +/** + * Test runner + * + * @return overall test status + */ +MAIN(testioc) { + auto testNum = 0; + testPlan((int)tests.size()); + 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(); + return testDone(); +} + +//static void testDbLoadGroupOk(const char* file) { +// testOk(!pvxs::ioc::dbLoadGroup(file), "%s scheduled to be loaded during IocInit()", file); +//} + +static void boxLeft() { } diff --git a/test/testioc.db b/test/testioc.db new file mode 100644 index 0000000..23a61b1 --- /dev/null +++ b/test/testioc.db @@ -0,0 +1,105 @@ +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