WIP state for Hugo to potentially fix a segfault

This commit is contained in:
2026-04-20 14:24:50 +02:00
parent 87003df25c
commit 28e5cd4ec4
7 changed files with 771 additions and 163 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
.vscode
build
.ioc
.ioc*
+44 -8
View File
@@ -1,6 +1,21 @@
#ifndef mEpicsCa_H
#define mEpicsCa_H
#include "cadef.h"
#include <optional>
#include <string>
/**
* @brief Information about the channel type (integer, double, string, ...)
*/
struct ChInfo {
// Channel type (like e.g. DBF_INT, DBF_DOUBLE, DBF_STRING etc.)
short type;
// Number of elements. Is 1 for most record types except for waveforms,
// whose length is defined in the record.
int count;
};
template <typename T> class mEpicsCa {
public:
/**
@@ -12,25 +27,25 @@ template <typename T> class mEpicsCa {
* @param priority
* @param pChanID
*/
mEpicsCa(const char *pChanName, caCh *pConnStateCallback,
mEpicsCa(const char *pChanName, bool subscribe, caCh *pConnStateCallback,
void *pUserPrivate, double timeout = 2.0);
mEpicsCa(const char *pChanName);
mEpicsCa(const char *pChanName, bool subscribe);
~mEpicsCa();
template <typename V> int put(V *value, double timeout);
template <typename V> int get(V *value);
int get(char *buf, u_long len);
template <typename V> int put(V *value);
template <typename V> int get(V *value);
template <typename V> int get(V *value, double timeout);
int put(const char *buf, u_long len);
bool connected();
double _timeout;
const std::optional<T> &cached() const;
private:
/**
* @brief A callback for reacting to channel access channel state changes.
@@ -44,8 +59,29 @@ template <typename T> class mEpicsCa {
*/
static void connStateCallback(struct connection_handler_args args);
static void eventCallback(struct event_handler_args args);
int getRaw(char *buf, size_t len);
int getRaw(int *value);
int getRaw(double *value);
int getRaw(uint16_t *value);
int putRaw(const char *buf, size_t len);
int putRaw(int *value);
int putRaw(double *value);
int putRaw(uint16_t *value);
std::optional<ChInfo> _channelInfo;
chid _pChanID;
std::string _chanName;
/*
In case the subscription mechanism is used, this variable holds the last
value returned by the callback.
*/
std::optional<T> _cached;
};
#include "m_epics_ca.tpp"
#include "m_epics_ca.tpp"
#endif
+378 -98
View File
@@ -1,6 +1,7 @@
#include "cadef.h"
#include "dbDefs.h"
#include "midas.h"
#include <cstring>
#include <string>
#include <iostream>
@@ -9,38 +10,33 @@
// Helper struct for dbfFromType
template <typename> struct alwaysFalse : std::false_type {};
/**
* @brief Check at compile time if T is a valid database field (DBF) type.
*
* This function will fail to compile if T is not a valid database field type.
*
* @tparam T
* @return int
*/
template <typename T> constexpr int dbfFromType() {
if constexpr (std::is_same_v<T, char *> ||
std::is_same_v<T, const char *> ||
std::is_same_v<T, std::string *>) {
return DBF_STRING;
} else if constexpr (std::is_same_v<T, int>) {
return DBF_INT; // Same as DBF_SHORT
} else if constexpr (std::is_same_v<T, float>) {
return DBF_FLOAT;
} else if constexpr (std::is_enum_v<T>) {
return DBF_ENUM;
} else if constexpr (std::is_same_v<T, char>) {
return DBF_CHAR;
} else if constexpr (std::is_same_v<T, long>) {
return DBF_LONG;
} else if constexpr (std::is_same_v<T, double>) {
return DBF_DOUBLE;
} else {
static_assert(alwaysFalse<T>::value,
"Unsupported EPICS database field type. See "
"epics-base/src/ca/client/db_access.h for the list "
"of allowed types");
}
}
template <typename T> struct dbf_type;
template <> struct dbf_type<int> {
static constexpr int value = DBF_INT;
};
template <> struct dbf_type<double> {
static constexpr int value = DBF_DOUBLE;
};
template <> struct dbf_type<char *> {
static constexpr int value = DBF_STRING;
};
template <> struct dbf_type<const char *> {
static constexpr int value = DBF_STRING;
};
template <> struct dbf_type<std::string> {
static constexpr int value = DBF_STRING;
};
template <> struct dbf_type<uint16_t> {
static constexpr int value = DBF_ENUM;
};
template <typename T> constexpr int dbfFromType() { return dbf_type<T>::value; }
template <typename T, typename V> constexpr void assertEqual() {
static_assert(dbfFromType<T>() == dbfFromType<V>());
@@ -48,93 +44,111 @@ template <typename T, typename V> constexpr void assertEqual() {
/**
* @brief Handle the ECA (error channel access) code returned by the ca_get and
* ca_put macros.
* ca_put macros and convert it to a MIDAS errror code.
*
* Handling means creating an appropriate MIDAS error message to inform the
* user.
*
* @param status
* @param status EPRICS status code
* @param pChanName: Name of the EPICS channel
* @return int Corresponding MIDAS status code
*/
void handleEcaCode(int status, const char *pChanName) {
int convertAndHandleEcaCode(int status, const char *pChanName) {
switch (status) {
case ECA_NORMAL:
break;
return CM_SUCCESS;
case ECA_BADCHID:
cm_msg(MERROR, __FILE__,
"PV '%s': Channel ID has been corrupted. Restart the IOC.",
pChanName);
break;
return FE_ERR_DISABLED;
case ECA_BADTYPE:
cm_msg(MERROR, __FILE__,
"PV '%s': Type mismatch. This is a driver bug, please call the "
"support.",
pChanName);
break;
return FE_ERR_DRIVER;
case ECA_BADCOUNT:
cm_msg(
MERROR, __FILE__,
"PV '%s': Invalid element count requested. This is a driver bug, "
"please call the support.",
pChanName);
break;
return FE_ERR_DRIVER;
case ECA_STRTOBIG:
cm_msg(MERROR, __FILE__,
"PV '%s': String length exceeds allowed maximum length. Try "
"providing a shorter string or call the support.",
pChanName);
break;
return FE_ERR_DRIVER;
case ECA_NOWTACCESS:
cm_msg(MERROR, __FILE__,
"PV '%s': Write access denied. Please call the support.",
pChanName);
break;
return FE_ERR_DRIVER;
case ECA_ALLOCMEM:
cm_msg(MERROR, __FILE__,
"PV '%s': Unable to allocate memory. Please call the support.",
pChanName);
break;
return FE_ERR_DRIVER;
case ECA_DISCONN:
cm_msg(MERROR, __FILE__,
"PV '%s': Channel disconnected. Check if the PV name is correct "
"and if the IOC is running.",
pChanName);
break;
return FE_ERR_DRIVER;
case ECA_TIMEOUT:
cm_msg(
MERROR, __FILE__,
"PV '%s': Timed out while trying to read. Check if the PV name is "
"correct and if the IOC is running.",
pChanName);
break;
return CM_TIMEOUT;
default:
cm_msg(MERROR, __FILE__, "PV '%s': Received unknown error code %d.",
pChanName, status);
break;
return FE_ERR_DRIVER;
}
}
template <typename T>
mEpicsCa<T>::mEpicsCa(const char *pChanName, caCh *pConnStateCallback,
void *pUserPrivate, double timeout)
: _timeout(timeout), _pChanID(nullptr), _chanName(pChanName) {
mEpicsCa<T>::mEpicsCa(const char *pChanName, bool subscribe,
caCh *pConnStateCallback, void *pUserPrivate,
double timeout)
: _timeout(timeout), _pChanID(nullptr), _chanName(pChanName),
_cached(std::nullopt) {
// Compile time check of T
dbfFromType<T>();
int status = CA_M_SUCCESS;
int status = CM_SUCCESS;
/*
ca_disable_preemptive_callback: No callback thread is created and CA context
has to be "polled" (e.g. via ca_pend_event or ca_pend_io). All callbacks
happen in the main thread.
ca_enable_preemptive_callback: EPICS creates a dedicated callback thread
where the callback is run.
Obviously, ca_enable_preemptive_callback is the variant we want for
subscription.
*/
auto choice = ca_preemptive_callback_select::ca_disable_preemptive_callback;
if (subscribe) {
choice = ca_preemptive_callback_select::ca_enable_preemptive_callback;
}
/*
Try to create the Channel access context explicitly:
https://docs.epics-controls.org/projects/base/en/latest/cadef_h.html#_CPPv417ca_context_create29ca_preemptive_callback_select
This function returns ECA_NOTTHREADED if a context has already been
created for the current thread. This is generally not an issue and can
safely be ignored. All other failures are handled in handleEcaCode.
Preemptive callbacks are disabled by default for this driver.
safely be ignored. All other failures are handled in
convertAndHandleEcaCode. Preemptive callbacks are disabled by default for
this driver.
*/
status = ca_context_create(
ca_preemptive_callback_select::ca_disable_preemptive_callback);
if (status != ECA_NORMAL && status != ECA_NOTTHREADED) {
handleEcaCode(status, this->_chanName.c_str());
status = ca_context_create(choice);
if (status != ECA_NOTTHREADED) {
status = convertAndHandleEcaCode(status, this->_chanName.c_str());
}
// Use the user-provided callback, if it is not NULL, otherwise use the
@@ -146,72 +160,332 @@ mEpicsCa<T>::mEpicsCa(const char *pChanName, caCh *pConnStateCallback,
// 0 is the lowest network priority, 99 is the highest.
int priority = 0;
status = ca_create_channel(pChanName, pConnStateCallback, pUserPrivate,
priority, &this->_pChanID);
handleEcaCode(status, this->_chanName.c_str());
status = convertAndHandleEcaCode(
ca_create_channel(pChanName, pConnStateCallback, pUserPrivate, priority,
&this->_pChanID),
this->_chanName.c_str());
// Store a pointer to "this" in the channel for retrieval in the
// connStateCallback function.
ca_set_puser(this->_pChanID, this);
if (subscribe) {
ca_create_subscription(dbf_type<T>::value, 1, this->_pChanID, DBE_VALUE,
&mEpicsCa<T>::eventCallback, this, nullptr);
}
SEVCHK(status, "ca_create_channel");
}
template <typename T>
mEpicsCa<T>::mEpicsCa(const char *pChanName)
: mEpicsCa(pChanName, nullptr, nullptr) {}
void mEpicsCa<T>::eventCallback(struct event_handler_args args) {
auto *self = static_cast<mEpicsCa<T> *>(ca_puser(args.chid));
if (!self)
return;
// Connection events are handled in connStateCallback, which also
// invalidates the cache when disconnecting.
if (args.status != ECA_NORMAL)
return;
if constexpr (std::is_same_v<T, std::string>) {
// Special handling of string type: args.dbr will be a char array, so
// we need to cast to a char array first which is then converted into
// a string.
self->_cached = static_cast<const char *>(args.dbr);
} else {
self->_cached = *static_cast<const T *>(args.dbr);
}
}
template <typename T> const std::optional<T> &mEpicsCa<T>::cached() const {
return _cached;
}
template <typename T>
mEpicsCa<T>::mEpicsCa(const char *pChanName, bool subscribe)
: mEpicsCa(pChanName, subscribe, nullptr, nullptr) {}
template <typename T> mEpicsCa<T>::~mEpicsCa() {
// Free up the channel resources
ca_clear_channel(this->_pChanID);
}
template <typename T> template <typename V> int mEpicsCa<T>::put(V *value) {
return put(value, _timeout);
}
template <typename T>
template <typename V>
int mEpicsCa<T>::put(V *value, double timeout) {
assertEqual<T, V>();
int dbfType = dbfFromType<V>();
int status = ca_put(dbfType, this->_pChanID, value);
handleEcaCode(status, this->_chanName.c_str());
if (status != ECA_NORMAL)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
status = ca_pend_io(timeout);
handleEcaCode(status, this->_chanName.c_str());
return status;
convertAndHandleEcaCode(ca_clear_channel(this->_pChanID),
this->_chanName.c_str());
}
template <typename T> template <typename V> int mEpicsCa<T>::get(V *value) {
return get(value, _timeout);
// If "this" has the type mEpicsCa<uint16_t>, then V needs to be a string
// or an uint16_t.
if constexpr (dbfFromType<T>() == DBF_ENUM) {
constexpr auto t = dbfFromType<T>();
constexpr auto v = dbfFromType<V>();
static_assert((t == v) || (t == DBF_ENUM && v == DBF_STRING) ||
(t == DBF_STRING && v == DBF_ENUM),
"Incompatible EPICS types");
} else {
assertEqual<T, V>();
}
if (!connected()) {
cm_msg(MERROR, __FILE__,
"PV '%s': Channel disconnected. Check if the PV name is correct "
"and if the IOC is running.",
_chanName.c_str());
return FE_ERR_DRIVER;
}
// If a subscription mechanism is active, use the last cached value instead
// of explictly invoking ca_get
if (_cached.has_value()) {
*value = _cached.value();
return CM_SUCCESS;
}
if constexpr (std::is_same_v<V, std::string>) {
char buf[40] = {0};
int status = getRaw(buf, sizeof(buf));
if (status == CM_SUCCESS)
*value = buf;
return status;
} else {
return getRaw(value);
}
}
template <typename T>
template <typename V>
int mEpicsCa<T>::get(V *value, double timeout) {
assertEqual<T, V>();
int dbfType = dbfFromType<V>();
template <typename T> int mEpicsCa<T>::get(char *buf, u_long len) {
int status = ca_get(dbfType, this->_pChanID, value);
handleEcaCode(status, this->_chanName.c_str());
// If "this" has the type mEpicsCa<uint16_t>, then the type check is omitted
if (dbfFromType<T>() != DBF_ENUM) {
assertEqual<T, char *>();
}
if (status != ECA_NORMAL)
if (!connected()) {
cm_msg(MERROR, __FILE__,
"PV '%s': Channel disconnected. Check if the PV name is correct "
"and if the IOC is running.",
_chanName.c_str());
return FE_ERR_DRIVER;
}
// If a subscription mechanism is active, use the last cached value instead
// of explictly invoking ca_get
if (_cached.has_value()) {
if constexpr (std::is_same_v<T, std::string>) {
strncpy(buf, _cached.value().c_str(), len);
} else {
strncpy(buf, _cached.value(), len);
}
return CM_SUCCESS;
}
return getRaw(buf, len);
}
template <typename T> int mEpicsCa<T>::getRaw(int *value) {
int status = convertAndHandleEcaCode(
ca_get(dbfFromType<int>(), this->_pChanID, value),
this->_chanName.c_str());
if (status != CM_SUCCESS)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
status = ca_pend_io(timeout);
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
handleEcaCode(status, this->_chanName.c_str());
return status;
template <typename T> int mEpicsCa<T>::getRaw(double *value) {
int status = convertAndHandleEcaCode(
ca_get(dbfFromType<double>(), this->_pChanID, value),
this->_chanName.c_str());
if (status != CM_SUCCESS)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
template <typename T> int mEpicsCa<T>::getRaw(uint16_t *value) {
int status = convertAndHandleEcaCode(
ca_get(dbfFromType<uint16_t>(), this->_pChanID, value),
this->_chanName.c_str());
if (status != CM_SUCCESS)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
template <typename T> int mEpicsCa<T>::getRaw(char *buf, u_long len) {
int status = CM_SUCCESS;
if (_channelInfo.value().count == 1) {
// String record
status = convertAndHandleEcaCode(
ca_get(dbfFromType<char *>(), this->_pChanID, buf),
this->_chanName.c_str());
} else {
// Waveform record
status = convertAndHandleEcaCode(
ca_array_get(DBR_CHAR, len, this->_pChanID, buf),
this->_chanName.c_str());
}
if (status != CM_SUCCESS)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
// =============================================================================
// put
// =============================================================================
template <typename T> template <typename V> int mEpicsCa<T>::put(V *value) {
// If "this" has the type mEpicsCa<uint16_t>, then V needs to be a string or
// an integer
if constexpr (dbfFromType<T>() == DBF_ENUM) {
constexpr auto t = dbfFromType<T>();
constexpr auto v = dbfFromType<V>();
static_assert((t == v) || (t == DBF_ENUM && v == DBF_STRING) ||
(t == DBF_STRING && v == DBF_ENUM),
"Incompatible EPICS types");
} else {
assertEqual<T, V>();
}
if (!connected()) {
cm_msg(MERROR, __FILE__,
"PV '%s': Channel disconnected. Check if the PV name is correct "
"and if the IOC is running.",
_chanName.c_str());
return FE_ERR_DRIVER;
}
if constexpr (std::is_same_v<V, std::string>) {
// Use the c-style string buffer put algorithm
return putRaw(value->c_str(), value->size());
} else {
return putRaw(value);
}
}
template <typename T> int mEpicsCa<T>::put(const char *buf, u_long len) {
// If "this" has the type mEpicsCa<uint16_t>, then the type check is omitted
if constexpr (dbfFromType<T>() != DBF_ENUM) {
assertEqual<T, char *>();
}
if (!connected()) {
cm_msg(MERROR, __FILE__,
"PV '%s': Channel disconnected. Check if the PV name is correct "
"and if the IOC is running.",
_chanName.c_str());
return FE_ERR_DRIVER;
}
return putRaw(buf, len);
}
template <typename T> int mEpicsCa<T>::putRaw(double *value) {
int status = convertAndHandleEcaCode(
ca_put(dbfFromType<double>(), this->_pChanID, value),
this->_chanName.c_str());
if (status != CM_SUCCESS)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
template <typename T> int mEpicsCa<T>::putRaw(u_int16_t *value) {
int status = convertAndHandleEcaCode(
ca_put(dbfFromType<uint16_t>(), this->_pChanID, value),
this->_chanName.c_str());
if (status != CM_SUCCESS)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
template <typename T> int mEpicsCa<T>::putRaw(const char *buf, u_long len) {
if constexpr (dbfFromType<T>() == DBF_ENUM) {
// Check if the given string corresponds to one of the enum variants and
// fetch the corresponding index, if it does. Otherwise, create a nice
// error message.
struct dbr_ctrl_enum data;
ca_get(DBR_CTRL_ENUM, this->_pChanID, &data);
ca_pend_io(_timeout);
std::string variants;
for (int i = 0; i < data.no_str; ++i) {
if (i > 0)
variants += ", ";
variants += data.strs[i];
if (strcmp(data.strs[i], buf) == 0)
return putRaw(&i);
}
// Given string did not match any of the variants -> return an error
cm_msg(MERROR, __FILE__,
"PV '%s': Given string '%s' does not match one of the enum "
"variants [%s]",
this->_chanName.c_str(), buf, variants.c_str());
return FE_ERR_DRIVER;
} else {
int status = CM_SUCCESS;
if (_channelInfo.value().count == 1) {
// String record
status = convertAndHandleEcaCode(
ca_put(dbfFromType<char *>(), this->_pChanID, buf),
this->_chanName.c_str());
} else {
// Waveform record
status = convertAndHandleEcaCode(
ca_array_put(DBR_CHAR, len, this->_pChanID, buf),
this->_chanName.c_str());
}
if (status != CM_SUCCESS)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
}
template <typename T> int mEpicsCa<T>::putRaw(int *value) {
int status = convertAndHandleEcaCode(
ca_put(dbfFromType<int>(), this->_pChanID, value),
this->_chanName.c_str());
if (status != CM_SUCCESS)
return status;
// caget is asynchronous, so the pointer value is only updated when
// ca_pend_io returns ECA_NORMAL
return convertAndHandleEcaCode(ca_pend_io(_timeout),
this->_chanName.c_str());
}
template <typename T>
@@ -219,12 +493,18 @@ void mEpicsCa<T>::connStateCallback(struct connection_handler_args args) {
mEpicsCa *self = static_cast<mEpicsCa *>(ca_puser(args.chid));
if (args.op == CA_OP_CONN_UP) {
struct ChInfo info;
info.type = ca_field_type(self->_pChanID);
info.count = ca_element_count(self->_pChanID);
self->_channelInfo = std::optional(info);
cm_msg(MDEBUG, __FILE__, "PV %s connected", self->_chanName.c_str());
} else {
self->_channelInfo.reset();
self->_cached.reset();
cm_msg(MDEBUG, __FILE__, "PV %s disconnected", self->_chanName.c_str());
}
}
template <typename T> bool mEpicsCa<T>::connected() {
return _pChanID && ca_state(_pChanID) == cs_conn;
return _channelInfo.has_value();
}
+19 -4
View File
@@ -1,10 +1,25 @@
record(longin, "TEST:INT") {
field(DESC, "Example record")
record(longin, "MEPICSCA:TEST:LONGIN") {
field(VAL, 42)
field(HOPR, 100)
field(LOPR, 0)
field(PINI, "YES")
}
record(ai, "TEST:DOUBLE") {
field(DESC, "Example record")
record(ai, "MEPICSCA:TEST:AI") {
field(VAL, 84)
}
record(stringin, "MEPICSCA:TEST:STRINGIN") {
field(VAL, "MyTestString")
}
record(waveform, "MEPICSCA:TEST:WAVEFORM") {
field(FTVL, "CHAR")
field(NELM, "200")
}
record(bo, "MEPICSCA:TEST:BO") {
field(VAL, 1)
field(ZNAM, "OFF")
field(ONAM, "ON")
}
+3
View File
@@ -3,3 +3,6 @@
dbLoadRecords("$(IOCDIR)/record.db")
iocInit()
# Initialize the waveform record
dbpf("MEPICSCA:TEST:WAVEFORM", "ABC")
+2 -1
View File
@@ -7,4 +7,5 @@ export EPICS_CA_ADDR_LIST=127.0.0.1
cd "$(dirname "$0")"
export IOCDIR=$(pwd)
./st.cmd
exec ./st.cmd
+324 -51
View File
@@ -2,7 +2,10 @@
#include "m_epics_ca.h"
#include <cassert>
#include <chrono>
#include <cstring>
#include <fcntl.h>
#include <iostream>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string>
@@ -10,7 +13,7 @@
#include <sys/wait.h>
#include <unistd.h>
#define CHECK(cond) \
#define ASSERT(cond) \
do { \
if (!(cond)) { \
std::cerr << "Test failed: " #cond << std::endl; \
@@ -21,9 +24,9 @@
#define EQUAL(left, right) \
do { \
if (!(left == right)) { \
std::cerr << "\nTest failed: " << (left) << " (" << #left \
<< ") != " << (right) << " (" << #right << *")" \
<< std::endl; \
std::cerr << "\nTest failed at line " << __LINE__ << ": " \
<< (left) << " (" << #left << ") != " << (right) << " (" \
<< #right << *")" << std::endl; \
return 1; \
} \
} while (0)
@@ -31,12 +34,24 @@
class IocProcess {
public:
explicit IocProcess(const std::string &path) {
/*
Important note: DO NOT USE setpgid HERE!
Even though it looks "cleaner" than the pkill solution in the destructor
below (because you could just terminate the group), this seems to cause
trouble deep within EPICS, causing the server not to be reachable
anymore with caget.
*/
_pid = fork();
if (_pid == 0) {
// Hide IOC output
int devnull = open("/dev/null", O_WRONLY);
dup2(devnull, STDOUT_FILENO);
close(devnull);
// Start the IOC from the child process
setpgid(0, 0);
execl(path.c_str(), path.c_str(), (char *)nullptr);
execl(path.c_str(), path.c_str(), (char *)NULL);
perror("execl failed");
_exit(1);
}
@@ -44,17 +59,13 @@ class IocProcess {
if (_pid < 0) {
throw std::runtime_error("fork failed");
}
// Set the ID of the entire process group so it can be torn down in the
// destructor later. This ensures IOC teardown.
setpgid(_pid, _pid);
}
~IocProcess() {
if (_pid <= 0)
return;
// Kill the entire process group due to the -1
kill(-_pid, SIGTERM);
kill(_pid, SIGTERM);
// Give the IOC 50 * 100 * 1000 = 5 seconds time for graceful
// termination.
@@ -66,25 +77,78 @@ class IocProcess {
usleep(100 * 1000);
}
// Force kill group if needed
kill(-_pid, SIGKILL);
waitpid(_pid, nullptr, 0);
kill(_pid, SIGKILL);
}
private:
pid_t _pid = -1;
};
void handler(int sig) { printf("Received signal: %d\n", sig); }
int main() {
using clock = std::chrono::steady_clock;
signal(SIGPIPE, handler);
signal(SIGTERM, handler);
signal(SIGINT, handler);
signal(SIGHUP, handler);
// Client setting for local testing
setenv("EPICS_CA_AUTO_ADDR_LIST", "NO", 1);
setenv("EPICS_CA_ADDR_LIST", "127.0.0.255", 1);
setenv("EPICS_CA_ADDR_LIST", "127.0.0.1", 1);
// Create a new instance of the mEpicsCa bus driver and use it to
// interact with the spawned IOC.
auto int_ca = mEpicsCa<int>("MEPICSCA:TEST:LONGIN", false);
auto int_hopr_ca = mEpicsCa<int>("MEPICSCA:TEST:LONGIN.HOPR", false);
auto double_ca = mEpicsCa<double>("MEPICSCA:TEST:AI", false);
auto string_ca = mEpicsCa<std::string>("MEPICSCA:TEST:STRINGIN", false);
auto waveform_ca = mEpicsCa<std::string>("MEPICSCA:TEST:WAVEFORM", false);
auto bo_ca = mEpicsCa<uint16_t>("MEPICSCA:TEST:BO", false);
// Ending s indicates subscription mechanism is active
auto int_cas = mEpicsCa<int>("MEPICSCA:TEST:LONGIN", true);
// Generic status variable which is reused for the error codes throughout
// the tests.
int status = CM_SUCCESS;
{
// =====================================================================
// Tests before IOC is running
// =====================================================================
// Drivers are not connected
ASSERT((!int_ca.connected()));
// Cached values are invalid
ASSERT((!int_cas.cached().has_value()));
// Attempting to write or read a value results in an error. stdout is
// suppressed for the duration of this test
int saved_stdout = dup(STDOUT_FILENO);
int devnull = open("/dev/null", O_WRONLY);
dup2(devnull, STDOUT_FILENO);
close(devnull);
int int_val = 0;
status = int_ca.get(&int_val);
EQUAL(status, FE_ERR_DRIVER);
double new_double_val = 4.2;
status = double_ca.put(&new_double_val);
EQUAL(status, FE_ERR_DRIVER);
// Restore stdout
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
}
// Scope everything so the IOC gets torn down by the IocProcess destructor
// before reporting that the tests were successfull.
{
using clock = std::chrono::steady_clock;
// Start the test IOC
std::string path = __FILE__;
@@ -92,11 +156,6 @@ int main() {
std::string ioc_exe = dir + "/ioc/startioc";
IocProcess proc = IocProcess(ioc_exe);
// Create a new instance of the mEpicsCa bus driver and use it to
// interact with the spawned IOC.
mEpicsCa<int> int_ca = mEpicsCa<int>("TEST:INT");
mEpicsCa<double> double_ca = mEpicsCa<double>("TEST:DOUBLE");
// Wait for the IOC to become available
double timeout = 20.0;
@@ -111,46 +170,260 @@ int main() {
auto now = clock::now();
double elapsed = std::chrono::duration<double>(now - start).count();
if (elapsed > timeout) {
printf(
"Could not connect to IOC in %.3lf seconds. The IOC likely "
"failed to start.\n",
timeout);
printf("Could not connect to IOC in %.3lf seconds. The IOC "
"likely "
"failed to start.\n",
timeout);
return -1;
}
};
int status = ECA_NORMAL;
// =====================================================================
// No subscription
// =====================================================================
// Read an integer from a record
int int_val = 0;
status = int_ca.get(&int_val);
EQUAL(status, ECA_NORMAL);
EQUAL(int_val, 42);
{
// -----------------------------------------------------------------
// Test get
// -----------------------------------------------------------------
// Read a double from a record
double double_val = 0.0;
status = double_ca.get(&double_val);
EQUAL(status, ECA_NORMAL);
EQUAL(double_val, 84.0);
// Read an integer from a record
int int_val = 0;
status = int_ca.get(&int_val);
EQUAL(status, CM_SUCCESS);
EQUAL(int_val, 42);
// Write an integer to a record and read it to check
int new_int_val = 21;
status = int_ca.put(&new_int_val);
EQUAL(status, ECA_NORMAL);
status = int_ca.get(&int_val);
EQUAL(status, ECA_NORMAL);
EQUAL(int_val, new_int_val);
// Read from a different field
status = int_hopr_ca.get(&int_val);
EQUAL(status, CM_SUCCESS);
EQUAL(int_val, 100);
// Write a double to a record and read it to check
double new_double_val = 4.2;
status = double_ca.put(&new_double_val);
EQUAL(status, ECA_NORMAL);
status = double_ca.get(&double_val);
EQUAL(status, ECA_NORMAL);
EQUAL(double_val, new_double_val);
// Read a double from a record
double double_val = 0.0;
status = double_ca.get(&double_val);
EQUAL(status, CM_SUCCESS);
EQUAL(double_val, 84.0);
// Read a string from a stringin record using a string
std::string string_val = "";
status = string_ca.get(&string_val);
EQUAL(status, CM_SUCCESS);
EQUAL(string_val, "MyTestString");
// Read a string from a stringin record using a char array
char char_arr_val[40] = {0};
status = string_ca.get(char_arr_val, sizeof(char_arr_val));
EQUAL(status, CM_SUCCESS);
EQUAL(std::string(char_arr_val), "MyTestString");
// Read a string from a waveform record
status = waveform_ca.get(&string_val);
EQUAL(status, CM_SUCCESS);
EQUAL(string_val, "ABC");
// Read an enum as a value and as a string
uint16_t short_uint_val = 0;
status = bo_ca.get(&short_uint_val);
EQUAL(status, CM_SUCCESS);
EQUAL(short_uint_val, 1);
status = bo_ca.get(&string_val);
EQUAL(status, CM_SUCCESS);
EQUAL(string_val, "ON");
// -----------------------------------------------------------------
// Test put
// -----------------------------------------------------------------
// Write an integer to a record and read it to check
int new_int_val = 21;
status = int_ca.put(&new_int_val);
EQUAL(status, CM_SUCCESS);
status = int_ca.get(&int_val);
EQUAL(status, CM_SUCCESS);
EQUAL(int_val, new_int_val);
// Write to the limit field of the longin record
new_int_val = 40;
status = int_hopr_ca.put(&new_int_val);
EQUAL(status, CM_SUCCESS);
int_val = 0; // Overwrite with other value
status = int_hopr_ca.get(&int_val);
EQUAL(status, CM_SUCCESS);
EQUAL(int_val, new_int_val);
// Write a double to a record and read it to check
double new_double_val = 4.2;
status = double_ca.put(&new_double_val);
EQUAL(status, CM_SUCCESS);
double_val = 0; // Overwrite with other value
status = double_ca.get(&double_val);
EQUAL(status, CM_SUCCESS);
EQUAL(double_val, new_double_val);
// Write a string to the stringin record using a string
std::string new_string_val = "NewStringValue";
status = string_ca.put(&new_string_val);
EQUAL(status, CM_SUCCESS);
string_val = ""; // Overwrite with other value
status = string_ca.get(&string_val);
EQUAL(status, CM_SUCCESS);
EQUAL(string_val, new_string_val);
// Write a string to the stringin record using a char array
char new_char_arr_val[40] = "NewCharArrValue";
status = string_ca.put(new_char_arr_val,
(u_long)strlen(new_char_arr_val));
EQUAL(status, CM_SUCCESS);
char_arr_val[0] = 0; // Overwrite with other value
status = string_ca.get(char_arr_val, (u_long)strlen(char_arr_val));
EQUAL(status, CM_SUCCESS);
EQUAL(std::string(char_arr_val), std::string(new_char_arr_val));
// Write to an enum with an integer value
short_uint_val = 0;
status = bo_ca.put(&short_uint_val);
EQUAL(status, CM_SUCCESS);
short_uint_val = 2; // Overwrite with other value
status = bo_ca.get(&short_uint_val);
EQUAL(status, CM_SUCCESS);
EQUAL(short_uint_val, 0);
status = bo_ca.get(&string_val);
EQUAL(status, CM_SUCCESS);
EQUAL(string_val, "OFF");
// Write to an enum with a string
std::string new_enum_string = "ON";
status = bo_ca.put(&new_enum_string);
EQUAL(status, CM_SUCCESS);
status = bo_ca.get(&short_uint_val);
EQUAL(status, CM_SUCCESS);
EQUAL(short_uint_val, 1);
status = bo_ca.get(&string_val);
EQUAL(status, CM_SUCCESS);
EQUAL(string_val, "ON");
// Attempt to write an invalid string. stdout is suppressed for the
// duration of this test
int saved_stdout = dup(STDOUT_FILENO);
int devnull = open("/dev/null", O_WRONLY);
dup2(devnull, STDOUT_FILENO);
close(devnull);
new_enum_string = "INVALID";
status = bo_ca.put(&new_enum_string);
EQUAL(status, FE_ERR_DRIVER); // Attempted to write invalid variant!
// Assert that nothing has changed in the record
status = bo_ca.get(&short_uint_val);
EQUAL(status, CM_SUCCESS);
EQUAL(short_uint_val, 1);
status = bo_ca.get(&string_val);
EQUAL(status, CM_SUCCESS);
EQUAL(string_val, "ON");
// Restore stdout
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
}
// =====================================================================
// With subscription
// =====================================================================
{
int int_val = 0;
int new_int_val = 13;
// Initial status of cached
ASSERT(int_cas.cached().has_value());
status = int_cas.get(&int_val);
EQUAL(status, CM_SUCCESS);
EQUAL(int_val, 21); // Value which was put in the record by int_ca
status = int_cas.put(&new_int_val);
EQUAL(status, CM_SUCCESS);
// Wait a bit to allow the subscription thread to update
double timeout = 5.0;
auto start = clock::now();
while (true) {
// Assert that the cached value has changed
status = int_cas.get(&int_val);
EQUAL(status, CM_SUCCESS);
if (int_val == new_int_val)
break;
ca_pend_event(0.1);
auto now = clock::now();
double elapsed =
std::chrono::duration<double>(now - start).count();
if (elapsed > timeout) {
printf("Failed to update int_cas value in %lf seconds.\n",
timeout);
return -1;
}
};
}
}
printf("\nTest was successful\n");
// Wait for the IOC to go offline
double timeout = 20.0;
auto start = clock::now();
while (true) {
if (!int_ca.connected()) {
break;
}
ca_pend_event(0.1);
auto now = clock::now();
double elapsed = std::chrono::duration<double>(now - start).count();
if (elapsed > timeout) {
printf("Could not connect to IOC in %.3lf seconds. The IOC "
"likely "
"failed to start.\n",
timeout);
return -1;
}
};
{
// =====================================================================
// Tests after IOC has stopped
// =====================================================================
// Drivers are not connected
ASSERT((!int_ca.connected()));
// Cached values are invalid
ASSERT((!int_cas.cached().has_value()));
// Attempting to write or read a value results in an error. stdout
// is suppressed for the duration of this test
int saved_stdout = dup(STDOUT_FILENO);
int devnull = open("/dev/null", O_WRONLY);
dup2(devnull, STDOUT_FILENO);
close(devnull);
int int_val = 0;
status = int_ca.get(&int_val);
EQUAL(status, FE_ERR_DRIVER);
double new_double_val = 4.2;
status = double_ca.put(&new_double_val);
EQUAL(status, FE_ERR_DRIVER);
// Restore stdout
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
}
printf("Test was successful.\n");
return 0;
}