From baecc6d17b3e5c20d9107a3a61e1aefb283ca7d5 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 7 Oct 2025 09:51:25 -0700 Subject: [PATCH 01/45] update release procedure --- release.md | 38 ++++++++++---------------------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/release.md b/release.md index 7c4f69d3a..4160f2766 100644 --- a/release.md +++ b/release.md @@ -1,8 +1,9 @@ # Preparation -1. Generate ABI change report from previous release tag to latest commit. +1. Check that candidate revision passes all GHA jobs. +1. Review ABI change report from previous release tag to latest commit. Ensure `PVXS_MINOR_VERSION` incrementes if not 100% (or if other ABI change - is known) + is known). See GHA `.github/workflows/release.yml` job. ```sh ./abi-diff.sh A.A.A HEAD @@ -27,33 +28,13 @@ Don't change in `details.rst` and `releasenotes.rst` git tag -s -m B.B.B B.B.B ``` -6. Generate ABI change report for upload +6. Push branches/tag (point of no return...) ```sh -./abi-diff.sh A.A.A B.B.B +git push origin B.B.B master ``` -7. Generate test coverage report for upload - -```sh -./coverage.sh B.B.B -``` - -8. Generate documentation and update `gh-pages` branch. - -```sh -make -make -C documentation clean -make -C documentation commit -``` - -9. Push branches/tag (point of no return...) - -```sh -git push origin B.B.B master +gh-pages -``` - -10. Verify GHA builds and pypi uploads +7. Verify GHA builds and pypi uploads ```sh virtualenv /tmp/p4p-bin @@ -65,12 +46,13 @@ virtualenv /tmp/p4p-src cd /tmp && /tmp/p4p-src/bin/python -m nose2 -v pvxslibs ``` - -11. Create github.com release B.B.B +8. Create github.com release B.B.B Summarize changes and attach coverage and ABI difference reports. -12. Announce on tech-talk +Download ABI diff and coverage reports from successful GHA job for tag, and attach to release. + +9. Announce on tech-talk Reply to previous announcement mail. From 289f508af6fe3645fbd5331f7a478070fc00ca11 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 13 Oct 2025 16:18:43 -0700 Subject: [PATCH 02/45] server: plug channel leak --- src/serverchan.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/serverchan.cpp b/src/serverchan.cpp index 36a08656f..6a6702236 100644 --- a/src/serverchan.cpp +++ b/src/serverchan.cpp @@ -392,6 +392,8 @@ void ServerConn::handle_DESTROY_CHANNEL() unsigned(sid), unsigned(chan->cid), unsigned(cid), chan->name.c_str()); } + chanBySID.erase(it); + chan->cleanup(); { From 8d58409481efd3f6d648abe168ff483775f32359 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 16 Oct 2023 18:32:26 -0700 Subject: [PATCH 03/45] server: check tx buffer limit to throttle The TX buffer could grow while nothing is being received. Practically bounded by the timeout interval, but could still get quite large in that time. --- src/servermon.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/servermon.cpp b/src/servermon.cpp index dae40c761..27c42449b 100644 --- a/src/servermon.cpp +++ b/src/servermon.cpp @@ -90,7 +90,9 @@ struct MonitorOp final : public ServerOp if(!conn || conn->state==ConnBase::Disconnected) return; - if(conn->connection() && (bufferevent_get_enabled(conn->connection())&EV_READ)) { + auto bev(conn->connection()); + + if(bev && evbuffer_get_length(bufferevent_get_output(bev)) < conn->tcp_tx_limit) { doReply(op); } else { // connection TX queue is too full From 4249885f8e8c1b04c9ce1b30b386ea2d099a2be1 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 13 Oct 2025 17:04:17 -0700 Subject: [PATCH 04/45] server: disable one-sided attempt to handle saturated connection Does prevent further creations from making the saturation worse, but also prevents destructions from reducing bandwidth usage. --- src/conn.h | 2 +- src/serverconn.cpp | 27 +++------------------------ src/serverconn.h | 1 - 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/conn.h b/src/conn.h index 53491b96c..7bfff26a5 100644 --- a/src/conn.h +++ b/src/conn.h @@ -91,7 +91,7 @@ struct ConnBase virtual std::shared_ptr self_from_this() =0; virtual void cleanup() =0; virtual void bevEvent(short events); - virtual void bevRead(); + void bevRead(); virtual void bevWrite(); static void bevEventS(struct bufferevent *bev, short events, void *ptr); static void bevReadS(struct bufferevent *bev, void *ptr); diff --git a/src/serverconn.cpp b/src/serverconn.cpp index d3c49f883..c4e2711a2 100644 --- a/src/serverconn.cpp +++ b/src/serverconn.cpp @@ -84,6 +84,9 @@ ServerConn::ServerConn(ServIface* iface, evutil_socket_t sock, struct sockaddr * timeval tmo(totv(iface->server->effective.tcpTimeout)); bufferevent_set_timeouts(bev.get(), &tmo, &tmo); + // set threshold to begin clearing backlog + bufferevent_setwatermark(this->bev.get(), EV_WRITE, tcp_tx_limit, 0); + auto tx = bufferevent_get_output(bev.get()); std::vector buf(128); @@ -378,23 +381,6 @@ void ServerConn::cleanup() } } -void ServerConn::bevRead() -{ - ConnBase::bevRead(); - - if(bev) { - auto tx = bufferevent_get_output(bev.get()); - - if(evbuffer_get_length(tx)>=tcp_tx_limit) { - // write buffer "full". stop reading until it drains - // TODO configure - (void)bufferevent_disable(bev.get(), EV_READ); - bufferevent_setwatermark(bev.get(), EV_WRITE, tcp_tx_limit/2, 0); - log_debug_printf(connio, "%s suspend READ\n", peerName.c_str()); - } - } -} - void ServerConn::bevWrite() { log_debug_printf(connio, "%s process backlog\n", peerName.c_str()); @@ -408,13 +394,6 @@ void ServerConn::bevWrite() fn(); } - - // TODO configure - if(evbuffer_get_length(tx) Date: Wed, 19 Nov 2025 17:53:43 -0800 Subject: [PATCH 05/45] update ci-scripts --- .ci | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci b/.ci index 130e88b70..261f218e0 160000 --- a/.ci +++ b/.ci @@ -1 +1 @@ -Subproject commit 130e88b7095812da93e423c602651e30f39da11a +Subproject commit 261f218e094e39550e3a7c54b98e34adcf42ad9b From 35c7cc5d15db00bb973d48dee2ba277c948485d1 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 14 Oct 2025 18:07:04 -0700 Subject: [PATCH 06/45] ioc: add pvxs_log_config() and pvxs_log_reset() --- documentation/ioc.rst | 14 ++++++++++++++ documentation/util.rst | 5 +++++ ioc/iochooks.cpp | 20 +++++++++++++++++--- src/log.cpp | 15 ++++++++++----- src/utilpvt.h | 3 +++ 5 files changed, 49 insertions(+), 8 deletions(-) diff --git a/documentation/ioc.rst b/documentation/ioc.rst index d9bfd1335..190bef656 100644 --- a/documentation/ioc.rst +++ b/documentation/ioc.rst @@ -37,6 +37,20 @@ served by the Integrated PVA server. Print information about module versions, target, and toolchain. May be requested when reporting a bug. +.. cpp:function:: void pvxs_log_config(const char *config) + + Enable additional :ref:`logconfig` as if more comma separated key=VALUE pairs + were appended to the **$PVXS_LOG** environment variable. + + Since UNRELEASED + +.. cpp:function:: void pvxs_log_reset() + + Reset logging to defaults. Negates the effects of **$PVXS_LOG** and later + configuration. + + Since UNRELEASED + Adding custom PVs to Server --------------------------- diff --git a/documentation/util.rst b/documentation/util.rst index 68a813740..5e7ff1edf 100644 --- a/documentation/util.rst +++ b/documentation/util.rst @@ -23,6 +23,11 @@ To enable all logging at full detail. :: export PVXS_LOG="*=DEBUG" +To enable all internal logging at INFO detail, +and any entries with the prefix "pvxs.foo*" at DEBUG detail. :: + + export PVXS_LOG="pvxs.*=INFO,pvxs.foo*=DEBUG" + .. doxygenenum:: pvxs::Level Controlling Logging diff --git a/ioc/iochooks.cpp b/ioc/iochooks.cpp index fe50e6d77..ae0455d94 100644 --- a/ioc/iochooks.cpp +++ b/ioc/iochooks.cpp @@ -165,9 +165,17 @@ void testCleanupPrepare() resetGroups(); } -//////////////////////////////////// -// Two ioc shell commands for pvxs -//////////////////////////////////// +static +void pvxs_log_config(const char *str) +{ + logger_config_str(str); +} + +static +void pvxs_log_reset() +{ + logger_level_clear(); +} /** * Show the PVXS server report. @@ -456,6 +464,12 @@ void pvxsBaseRegistrar() noexcept { bool enableQ = enable2(); + IOCShCommand("pvxs_log_config", "KEY=VAL,KEY=VAL,...", + "Append logger configuration. eg. pvxs_log_config \"pvxs.*=DEBUG\"") + .implementation<&pvxs_log_config>(); + IOCShCommand<>("pvxs_log_clear", + "Reset logger configuration to defaults") + .implementation<&pvxs_log_reset>(); IOCShCommand("pvxsr", "[show_detailed_information?]", "PVXS Server Report. " "Shows information about server config (level==0)\n" "or about connected clients (level>0).\n") diff --git a/src/log.cpp b/src/log.cpp index 46e495222..b3d3afc1f 100644 --- a/src/log.cpp +++ b/src/log.cpp @@ -340,12 +340,8 @@ void logger_level_clear() logger_gbl->config.clear(); } -void logger_config_env() +void logger_config_str(const char *env) { - const char *env = getenv("PVXS_LOG"); - if(!env || !*env) - return; - threadOnce<&logger_prepare>(); Guard G(logger_gbl->lock); @@ -389,6 +385,15 @@ void logger_config_env() errlogFlush(); } +void logger_config_env() +{ + const char *env = getenv("PVXS_LOG"); + if(!env || !*env) + return; + + logger_config_str(env); +} + } // namespace pvxs namespace pvxs {namespace impl { diff --git a/src/utilpvt.h b/src/utilpvt.h index e8ffb819a..fd7a261bd 100644 --- a/src/utilpvt.h +++ b/src/utilpvt.h @@ -252,6 +252,9 @@ using aligned_union = std::aligned_union; } // namespace impl using namespace impl; +PVXS_API +void logger_config_str(const char *env); + inline timeval totv(double t) { From b0b0bc8d7e97bf987a8b134c4c1a18b8243ea692 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Thu, 16 Oct 2025 07:26:53 -0700 Subject: [PATCH 07/45] client: respect forcedServer on failed CREATE_CHANNEL --- src/clientconn.cpp | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/clientconn.cpp b/src/clientconn.cpp index 1be977e54..2539204c4 100644 --- a/src/clientconn.cpp +++ b/src/clientconn.cpp @@ -381,13 +381,24 @@ void Connection::handle_CREATE_CHANNEL() chan->statRx += rxlen; if(!sts.isSuccess()) { - // server refuses to create a channel, but presumably responded positively to search + if(chan->forcedServer.family()==AF_UNSPEC) { + // server refuses to create a channel, but presumably responded positively to search. + // try again - chan->state = Channel::Searching; - context->searchBuckets[context->currentBucket].push_back(chan); + log_warn_printf(io, "Server %s refuses channel to '%s' : %s\n", peerName.c_str(), + chan->name.c_str(), sts.msg.c_str()); - log_warn_printf(io, "Server %s refuses channel to '%s' : %s\n", peerName.c_str(), - chan->name.c_str(), sts.msg.c_str()); + chan->state = Channel::Searching; + context->searchBuckets[context->currentBucket].push_back(chan); + + } else { + // server refused after we bypassed search, so can't use usual retry method. + // refuse to create a tight retry loop, and drop on the floor for now. + // retry on reconnect. + log_err_printf(io, "Server %s refuses direct channel to '%s' : %s\n", peerName.c_str(), + chan->name.c_str(), sts.msg.c_str()); + return; + } } else { chan->state = Channel::Active; From 6f5b51129524a0170a85d93438f212afb837ffd7 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Thu, 6 Nov 2025 15:32:40 -0800 Subject: [PATCH 08/45] pvxvct use endpoint --- tools/pvxvct.cpp | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tools/pvxvct.cpp b/tools/pvxvct.cpp index 99e4b6744..0aab98c65 100644 --- a/tools/pvxvct.cpp +++ b/tools/pvxvct.cpp @@ -28,6 +28,7 @@ #include namespace pva = pvxs; +using pvxs::impl::SB; namespace { DEFINE_LOGGER(out, "pvxvct"); @@ -124,7 +125,7 @@ int main(int argc, char *argv[]) } } opts; - std::vector bindaddrs; + std::vector bindaddrs; { int opt; @@ -149,16 +150,8 @@ int main(int argc, char *argv[]) case 'S': opts.server = true; break; - case 'B': { - pva::SockAddr addr; - int slen = addr.size(); - if(evutil_parse_sockaddr_port(optarg, &addr->sa, &slen)) { - throw std::runtime_error(pva::SB()<<"Expected address[:port] to bind. Not "<start(); listeners.back().second->start(); - log_debug_printf(out, "Bind: %s\n", baddr.tostring().c_str()); + log_debug_printf(out, "Bind: %s\n", (SB()< Date: Thu, 6 Nov 2025 15:33:28 -0800 Subject: [PATCH 09/45] pacify cppcheck --- src/udp_collector.h | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/udp_collector.h b/src/udp_collector.h index 9a6ec61c8..542bb0821 100644 --- a/src/udp_collector.h +++ b/src/udp_collector.h @@ -9,10 +9,7 @@ #include #include -#include -#include #include -#include #include #include "evhelper.h" @@ -37,7 +34,7 @@ struct PVXS_API UDPManager std::string proto; SockAddr server; ServerGUID guid; - uint8_t peerVersion; + uint8_t peerVersion=0; Beacon(const SockAddr& src) :src(src) {} }; //! Create subscription for Beacon messages. @@ -53,10 +50,10 @@ struct PVXS_API UDPManager SockAddr dest; // destination IP used by client SockAddr server; const IfaceMap::Iface* srcIface = nullptr; - uint32_t searchID; - uint8_t peerVersion; + uint32_t searchID=0; + uint8_t peerVersion=0; bool protoTCP = false; // included protocol "tcp" - bool mustReply; + bool mustReply=false; struct Name { const char *name; uint32_t id; From 8fb2931e05bad375748ada78681b870d680d8dd9 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Fri, 14 Nov 2025 20:45:49 -0800 Subject: [PATCH 10/45] pvxsr show libevent reactor method name --- src/server.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server.cpp b/src/server.cpp index da5e44842..f9c57fe7a 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -322,6 +322,9 @@ std::ostream& operator<<(std::ostream& strm, const Server& serv) auto& first = serv.pvt->interfaces.front(); strm<<" TCP_Port: "<acceptor_loop.base)) { + strm<<" evmethod: "< Date: Wed, 15 Oct 2025 18:29:47 -0700 Subject: [PATCH 11/45] doc --- documentation/client.rst | 4 ++-- documentation/netconfig.rst | 2 +- documentation/releasenotes.rst | 9 +++++++++ documentation/value.rst | 16 ++++++++-------- src/pvxs/data.h | 2 +- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/documentation/client.rst b/documentation/client.rst index 2ff193ba0..7e42de748 100644 --- a/documentation/client.rst +++ b/documentation/client.rst @@ -17,8 +17,8 @@ The recommended starting point is creating new context configured from ``EPICS_P Use `pvxs::client::Context::fromEnv`. EPICS_PVA_ADDR_LIST - A list of destination addresses to which UDP search messages will be sent. - May contain unicast and/or broadcast addresses. + A space separated list of destination addresses to which UDP search messages will be sent. + May contain unicast, multicast, and/or broadcast addresses. EPICS_PVA_AUTO_ADDR_LIST If "YES" then all local broadcast addresses will be implicitly appended to $EPICS_PVA_ADDR_LIST. diff --git a/documentation/netconfig.rst b/documentation/netconfig.rst index 73e3f7720..579eaf3ff 100644 --- a/documentation/netconfig.rst +++ b/documentation/netconfig.rst @@ -83,7 +83,7 @@ or fallback to use the associated ``$EPICS_PVA_*`` if set. Address Spec. ------------- -Entries in **EPICS_PVA*_ADDR_LIST** variables must be in one of the following forms: +Space separated entries in **EPICS_PVA*_ADDR_LIST** variables must be in one of the following forms: * ``[:][,TTL#][@ifacename]`` * ``"[""]"[:][,TTL#][@ifacename]`` diff --git a/documentation/releasenotes.rst b/documentation/releasenotes.rst index 0fd4b830b..e57008645 100644 --- a/documentation/releasenotes.rst +++ b/documentation/releasenotes.rst @@ -3,6 +3,15 @@ Release Notes ============= +1.4.2 (UNRELEASED) +------------------ + +* server: plug channel cache leak when close Channel while reusing Connection. +* server: disable one-sided attempt to handle saturated connection. +* ioc: add `pvxs_log_config()` and `pvxs_log_reset()` IOCsh functions. +* tools: pvxvct can use endpoint syntax to listen for multicast on a specific interface. + eg. ``pvxvct -B 224.1.2.23@eth0`` or ``pvxvct -B 224.1.2.23@10.1.1.100``. + 1.4.1 (Aug 2025) ---------------- diff --git a/documentation/value.rst b/documentation/value.rst index 280f01016..ef09be2fb 100644 --- a/documentation/value.rst +++ b/documentation/value.rst @@ -15,12 +15,12 @@ Value Container API `pvxs::Value` is the primary data container type used with PVXS. A `pvxs::Value` may be obtained via the remote peer (client or server), -or created locally. See `ntapi` or `typedefapi`. +or created locally. See :ref:`ntapi` or :ref:`typedefapi`. `pvxs::Value` is a safe pointer-like object which, maybe, references a node in a tree of sub-structures and leaf fields. This tree will be referred to as a Structure as it behaves -in many ways like a C 'struct'. +in many ways like a C struct. For example, the following code: @@ -35,7 +35,7 @@ For example, the following code: fld = top["fldname"]; fld = 2; -Is analogous to the following pseudo code. +Is analogous to the following pseudo C code. .. code-block:: c++ @@ -67,10 +67,10 @@ All operations on an invalid Value should be safe and well defined. .. code-block:: c++ Value top(nt::NTScalar{TypeCode::Int32}.create()); - int32_t val = top["nonexistent"].as(); + int32_t val = top["nonexistent"].as(); // throws NoField In this example, the operator[] lookup of a non-existent field returns an invalid Value. -Attempting to extract an integer from this will then throw a `pvxs::NoField` exception. +Attempting to extract an integer from this will then throw a `pvxs::NoField` exception for the ``as()`` method. Value ----- @@ -90,12 +90,12 @@ Iteration may be iterated. Iteration comes in three variations: `pvxs::Value::iall`, `pvxs::Value::ichildren`, and `pvxs::Value::imarked`. -For a Struct, iall() is a depth first traversal of all fields. -ichildren() traverses all child fields (excluding eg. grandchildren +For a Struct, ``iall()`` is a depth first traversal of all fields. +ichildren() traverses only child fields (excluding eg. grandchildren and further). imarked() considers all fields, but only visits those which have beem marked (`pvxs::Value::isMarked`). -For a Union. iall() and ichildren() are identical, and will +For a Union. ``iall()`` and ``ichildren()`` are identical, and will visit all possible Union members, excluding the implicit NULL member. Traversal does not effect member selection. imarked() for a Union will visit at most one member (if one is selected)> diff --git a/src/pvxs/data.h b/src/pvxs/data.h index cb1672fb3..1affdeb11 100644 --- a/src/pvxs/data.h +++ b/src/pvxs/data.h @@ -739,7 +739,7 @@ class PVXS_API Value { const Value lookup(const std::string& name) const; //! Number of child fields. - //! only Struct, StructA, Union, UnionA return non-zero + //! Only Struct, StructA, Union, UnionA return non-zero //! \since 1.1.3 correctly return non-zero for StructA and UnionA size_t nmembers() const; From 08a2491d4150225e5956e9d80577967f44756e09 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 2 Dec 2025 07:30:42 -0800 Subject: [PATCH 12/45] GHA: fix escaping in codespell job Also ignore 'copyIn' method with codespell circa Debian 13. --- .ci-local/codespell.dic | 1 + .github/workflows/codespell.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.ci-local/codespell.dic b/.ci-local/codespell.dic index b19233194..1143861b0 100644 --- a/.ci-local/codespell.dic +++ b/.ci-local/codespell.dic @@ -1,3 +1,4 @@ als SLAC slac +copyIn diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index a36e846c3..3735b6fbc 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -27,4 +27,4 @@ jobs: sudo apt-get update sudo apt-get -y install codespell - name: codespell - run: git ls-files | grep -vE 'test|bundle|.ci|.\db' | xargs codespell -I .ci-local/codespell.dic + run: git ls-files | grep -vE 'test|bundle|\.ci|\.db' | xargs codespell -I .ci-local/codespell.dic From 1d3eb5dc0d201b6ca8d9f2d775693bc60f390d81 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 16 Dec 2025 18:14:19 -0800 Subject: [PATCH 13/45] GHA refresh python jobs --- .github/workflows/python.yml | 597 ++++++++++++++--------------------- gha-set-pre.py | 24 ++ 2 files changed, 258 insertions(+), 363 deletions(-) create mode 100755 gha-set-pre.py diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 16da11984..5c95b87ac 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -1,409 +1,280 @@ -# .github/workflows/ci-scripts-build.yml for use with EPICS Base ci-scripts -# (see: https://github.com/epics-base/ci-scripts) +name: Python -# This is YAML - indentation levels are crucial +on: [push, pull_request, workflow_dispatch] -name: PVXS Python - -on: - push: - paths-ignore: - - tools/* - - test/* - - example/* - - documentation/* - pull_request: - workflow_dispatch: - -env: - _PVXS_ABORT_ON_CRIT: 1 - PVXS_LOG: pvxs.*=WARN +defaults: + run: + shell: bash jobs: - native: - name: ${{ matrix.name }} - runs-on: ${{ matrix.os }} - env: - PRE: "--pre" + manylinux: + name: py${{ matrix.py }} ${{ matrix.ml }} + + runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - - name: linux64 test - os: ubuntu-latest - python: "3.9" - source: true - - # OSX py builds - - name: osx 3.6 intel - os: macos-13 - python: "3.6" - piparch: macosx_10_9_intel - - - name: osx 3.7 intel - os: macos-13 - python: "3.7" - piparch: macosx_10_9_intel - - - name: osx 3.8 arm64 - os: macos-latest - python: "3.8" - piparch: macosx_11_0_universal2 - - - name: osx 3.9 arm64 - os: macos-latest - python: "3.9" - piparch: macosx_11_0_universal2 - - - name: osx 3.10 arm64 - os: macos-latest - python: "3.10" - piparch: macosx_11_0_universal2 - - - name: osx 3.11 arm64 - os: macos-latest - python: "3.11" - piparch: macosx_11_0_universal2 - - - name: osx 3.12 arm64 - os: macos-latest - python: "3.12" - piparch: macosx_11_0_universal2 - - - name: osx 3.13 arm64 - os: macos-latest - python: "3.13" - piparch: macosx_11_0_universal2 - - # Windows py builds - - - name: win64 3.6 - os: windows-latest - python: "3.6" - piparch: win_amd64 - - - name: win64 3.7 - os: windows-latest - python: "3.7" - piparch: win_amd64 - - - name: win64 3.8 - os: windows-latest - python: "3.8" - piparch: win_amd64 - - - name: win64 3.9 - os: windows-latest - python: "3.9" - piparch: win_amd64 - - - name: win64 3.10 - os: windows-latest - python: "3.10" - piparch: win_amd64 - - - name: win64 3.11 - os: windows-latest - python: "3.11" - piparch: win_amd64 - - - name: win64 3.12 - os: windows-latest - python: "3.12" - piparch: win_amd64 - - - name: win64 3.13 - os: windows-latest - python: "3.13" - piparch: win_amd64 + # see compat matrix: https://github.com/pypa/manylinux - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - name: Automatic core dumper analysis - uses: mdavidsaver/ci-core-dumper@master + - ml: manylinux1_x86_64 + py: cp27-cp27m + cy: "Cython<3.0" + skip: true - - name: Setup native python - if: matrix.python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - #architecture: x64 + - ml: manylinux2010_x86_64 + py: cp37-cp37m + skip: true - # TLS 1.0 and 1.1 support was removed from pypi so the cached pip won't work - - name: Python 3.5 Fix - if: ${{ matrix.python == '3.5' }} - run: | - curl https://bootstrap.pypa.io/pip/3.5/get-pip.py | python + - ml: manylinux2014_x86_64 + py: cp38-cp38 - - name: Prepare - shell: bash - run: | - python -m pip install -U pip - python -m pip install twine - python --version - python -m pip --version - python -m twine -h + - ml: manylinux2014_x86_64 + py: cp39-cp39 - python -m pip install setuptools wheel nose2 - python -m pip install $PRE setuptools_dso>=2.1a1 epicscorelibs>=7.0.3.99.2.0a1 + - ml: manylinux2014_x86_64 + py: cp310-cp310 - - name: Source - run: python setup.py sdist --formats=gztar + - ml: manylinux2014_x86_64 + py: cp311-cp311 - - name: Wheel - shell: bash - run: | - set -x - [ "${{ matrix.piparch }}" ] && export SETUPTOOLS_DSO_PLAT_NAME="${{ matrix.piparch }}" - python -m pip wheel -v -w dist dist/pvxslibs-*.tar.gz + - ml: manylinux2014_x86_64 + py: cp312-cp312 - - name: Test Wheel - shell: bash - run: | - cd dist - ls - python -m pip install pvxslibs-*.whl - python -m nose2 pvxslibs + - ml: manylinux2014_x86_64 + py: cp313-cp313 + + - ml: manylinux2014_x86_64 + py: cp313-cp313t - - name: List Artifacts - shell: bash - run: ls dist/* + - ml: manylinux_2_28_x86_64 + py: cp314-cp314 - - name: Save Artifacts - uses: actions/upload-artifact@v4 + - ml: manylinux_2_28_x86_64 + py: cp314-cp314t + src: true + + steps: + - uses: actions/checkout@v6 with: - name: "epicscorelibs ${{ matrix.name }}" - path: dist/* + submodules: recursive - - name: Check wheels - run: python -m twine check dist/pvxslibs-*.whl + - name: Automatic core dumper analysis + uses: mdavidsaver/ci-core-dumper@master - - name: Check source - if: matrix.source - run: python -m twine check dist/pvxslibs-*.tar.* + - name: Detect pre-release + run: python ./gha-set-pre.py - - name: Upload wheels - if: env.TWINE_USERNAME && github.event_name=='push' && github.ref=='refs/heads/master' && matrix.piparch - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: python -m twine upload --skip-existing dist/pvxslibs-*.whl + - name: Build + run: | + # emit script to be executed in container. + cat < runit.sh + #!/bin/sh + set -e -x + ls /opt/python/ + /opt/python/${{ matrix.py }}/bin/python --version + + yum -y install gdb || dnf -y install gdb || (apt update && apt -y install gdb) + + export PATH="/opt/python/${{ matrix.py }}/bin:\$PATH" + export SETUPTOOLS_DSO_PLAT_NAME="${{ matrix.ml }}" + + cd /io + + pip install -U pip + # pre-download for later steps + pip download $PRE -d output \ + --only-binary numpy,Cython \ + --only-binary epicscorelibs \ + "${{ matrix.cy || 'Cython' }}" \ + setuptools_dso epicscorelibs wheel numpy ply nose2 + + # create source tar + pip install $PRE --no-index -f ./output setuptools_dso epicscorelibs + python setup.py sdist -d ./output + ls -lh output + + # build wheel from source tar + pip wheel $PRE -w ./output --no-index -f ./output pvxslibs + ls -lh output + + # install wheel for testing + pip install $PRE --no-index -f ./output pvxslibs nose2 + + # switch away from root when running test to avoid inclusion in PYTHONPATH + cd output + python -m nose2 -v pvxslibs + + EOF + # end of script. Now execute in container + + chmod +x runit.sh + mkdir output + podman run --rm \ + -v `pwd`:/io \ + quay.io/pypa/${{ matrix.ml }} \ + /io/runit.sh + + - name: Result + run: ls -lhtr output + + - name: Save wheels + if: ${{ ! matrix.skip }} + uses: actions/upload-artifact@v6 + with: + retention-days: 1 + name: whl-${{ matrix.py }}-${{ matrix.ml }} + path: output/pvxslibs*.whl - - name: Upload source - if: env.TWINE_USERNAME && github.event_name=='push' && github.ref=='refs/heads/master' && matrix.source - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: python -m twine upload --skip-existing dist/pvxslibs-*.tar.* + - name: Save source + if: ${{ matrix.src }} + uses: actions/upload-artifact@v6 + with: + retention-days: 1 + name: whl-source + path: output/pvxslibs*.gz - docker: - name: ${{ matrix.name }} - runs-on: ubuntu-latest - env: - PRE: "--pre" + native: + name: py${{ matrix.py }} ${{ matrix.arch }} + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - # Linux py builds x64 - - name: linux 2.7 amd64 - pyver: cp27-cp27m - manylinux: manylinux1 - arch: x86_64 - - - name: linux 2.7u amd64 - os: ubuntu-latest - pyver: cp27-cp27mu - manylinux: manylinux1 - arch: x86_64 - - - name: linux 3.5 amd64 - os: ubuntu-latest - pyver: cp35-cp35m - manylinux: manylinux1 - arch: x86_64 - - - name: linux 3.6 amd64 - os: ubuntu-latest - pyver: cp36-cp36m - manylinux: manylinux1 - arch: x86_64 - - - name: linux 3.7 amd64 - os: ubuntu-latest - pyver: cp37-cp37m - manylinux: manylinux1 - arch: x86_64 - - - name: linux 3.8 amd64 - os: ubuntu-latest - pyver: cp38-cp38 - manylinux: manylinux1 - arch: x86_64 - - - name: linux 3.9 amd64 - os: ubuntu-latest - pyver: cp39-cp39 - manylinux: manylinux2010 - arch: x86_64 - - - name: linux 3.10 amd64 - os: ubuntu-latest - pyver: cp310-cp310 - manylinux: manylinux2014 - arch: x86_64 - - - name: linux 3.11 amd64 - os: ubuntu-latest - pyver: cp311-cp311 - manylinux: manylinux2014 - arch: x86_64 - - - name: linux 3.12 amd64 - os: ubuntu-latest - pyver: cp312-cp312 - manylinux: manylinux2014 - arch: x86_64 - - - name: linux 3.13 amd64 - os: ubuntu-latest - pyver: cp313-cp313 - manylinux: manylinux2014 - arch: x86_64 - - # Linux py builds x32 - - name: linux 2.7 i686 - pyver: cp27-cp27m - manylinux: manylinux1 - arch: i686 - pre: linux32 - - - name: linux 2.7u i686 - os: ubuntu-latest - pyver: cp27-cp27mu - manylinux: manylinux1 - arch: i686 - pre: linux32 - - - name: linux 3.5 i686 - os: ubuntu-latest - pyver: cp35-cp35m - manylinux: manylinux1 - arch: i686 - pre: linux32 - - - name: linux 3.6 i686 - os: ubuntu-latest - pyver: cp36-cp36m - manylinux: manylinux1 - arch: i686 - pre: linux32 - - - name: linux 3.7 i686 - os: ubuntu-latest - pyver: cp37-cp37m - manylinux: manylinux1 - arch: i686 - pre: linux32 - - - name: linux 3.8 i686 - os: ubuntu-latest - pyver: cp38-cp38 - manylinux: manylinux1 - arch: i686 - pre: linux32 - - - name: linux 3.9 i686 - os: ubuntu-latest - pyver: cp39-cp39 - manylinux: manylinux2010 - arch: i686 - pre: linux32 + - os: windows-latest + py: "3.8" + arch: win_amd64 + + # windows + 3.9 omitted due to numpy bug "invalid preprocessor command 'warning'" + + - os: windows-latest + py: "3.10" + arch: win_amd64 + + - os: windows-latest + py: "3.11" + arch: win_amd64 + + - os: windows-latest + py: "3.12" + arch: win_amd64 + + - os: windows-latest + py: "3.13" + arch: win_amd64 + + - os: windows-latest + py: "3.14" + arch: win_amd64 + + - os: macos-latest + py: "3.8" + arch: macosx_11_0_universal2 + + - os: macos-latest + py: "3.9" + arch: macosx_11_0_universal2 + + - os: macos-latest + py: "3.10" + arch: macosx_11_0_universal2 + + - os: macos-latest + py: "3.11" + arch: macosx_11_0_universal2 + + - os: macos-latest + py: "3.12" + arch: macosx_11_0_universal2 + + - os: macos-latest + py: "3.13" + arch: macosx_11_0_universal2 + + - os: macos-latest + py: "3.14" + arch: macosx_11_0_universal2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: - submodules: recursive + submodules: recursive - - name: Setup native python - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - # used to run twine for uploads - python-version: '3.11' - #architecture: x64 + python-version: ${{ matrix.py }} - - name: Native Prep/Info - run: | - python -m pip install --upgrade pip - python -m pip install setuptools wheel twine - which python - python --version - python -m pip --version - python -m twine -h + - name: Prepare + run: python -m pip install -U pip virtualenv - - name: PY Source - run: | - echo "PRE=$PRE" - python -m pip install setuptools wheel nose2 - python -m pip install --only-binary numpy $PRE setuptools_dso>=2.1a1 epicscorelibs>=7.0.3.99.2.0a1 - python setup.py sdist --formats=gztar - ls dist/* - - - name: Docker PY build - if: matrix.pyver && !matrix.source + - name: PIP Debug + run: pip debug -v + + - name: Detect pre-release + run: python ./gha-set-pre.py + + - name: Build run: | - # can't use GHA native docker support since GHA magic binaries need .so absent from old manylinux images :( - cat < runit.sh - #!/bin/sh - set -e -x - yum -y install gdb - cd /io - [ -d dist ] - ls dist/* - export PATH="/opt/python/${{ matrix.pyver }}/bin:\$PATH" - export SETUPTOOLS_DSO_PLAT_NAME="${{ matrix.manylinux }}_${{ matrix.arch }}" - which python - python -m pip install -U pip - python -m pip install setuptools wheel nose2 - python -m pip install --only-binary numpy $PRE setuptools_dso>=2.1a1 epicscorelibs>=7.0.3.99.2.0a1 - - python -m pip wheel -v --only-binary numpy -w dist dist/pvxslibs-*.tar.gz - - cd dist - python -m pip install pvxslibs-*.whl - python -m nose2 pvxslibs - cd .. - - EOF - cat runit.sh - chmod +x runit.sh - docker pull quay.io/pypa/${{ matrix.manylinux }}_${{ matrix.arch }} - docker run --rm -v `pwd`:/io quay.io/pypa/${{ matrix.manylinux }}_${{ matrix.arch }} ${{ matrix.pre }} /io/runit.sh - - - name: List Artifacts - run: ls dist/* - - - name: Save Artifacts - uses: actions/upload-artifact@v4 + set -x -e + python -m pip install -U pip + pip download $PRE -d output \ + --only-binary numpy,Cython \ + --only-binary epicscorelibs \ + Cython setuptools_dso epicscorelibs wheel numpy ply nose2 + + export SETUPTOOLS_DSO_PLAT_NAME=${{ matrix.arch }} + + pip install $PRE --no-index -f ./output setuptools_dso epicscorelibs + python setup.py sdist -d ./output + ls -lh output + + # build wheel from source tar + pip wheel $PRE -w ./output --no-index -f ./output pvxslibs + ls -lh output + + # install wheel for testing + pip install $PRE --no-index -f ./output pvxslibs nose2 + + # switch away from root when running test to avoid inclusion in PYTHONPATH + cd output + python -m nose2 -v pvxslibs + + - run: ls -lhtr output + + - name: Save wheels + if: ${{ ! matrix.skip }} + uses: actions/upload-artifact@v6 with: - name: "epicscorelibs ${{ matrix.name }}" - path: dist/* + retention-days: 1 + name: whl-${{ matrix.py }}-${{ matrix.os }} + path: output/pvxslibs*.whl - - name: Check wheels - run: python -m twine check dist/pvxslibs-*.whl + combine: + runs-on: ubuntu-latest + needs: [manylinux, native] + steps: + - name: Download + uses: actions/download-artifact@v7 + with: + merge-multiple: true + + - run: ls -lhtr - - name: Check for impure wheel - shell: bash + - uses: actions/setup-python@v6 + with: + python-version: 3.11 + + - name: Setup run: | - python -m wheel unpack -d junk dist/pvxslibs-*.whl - ls junk/*/*.dist-info/WHEEL - ! ls junk/*/*/purelib + pip install -U pip twine + python -m twine -h + + - name: Check + run: twine check pvxslibs*.gz pvxslibs*.whl - name: Upload wheels if: env.TWINE_USERNAME && github.event_name=='push' && github.ref=='refs/heads/master' env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: python -m twine upload --skip-existing dist/pvxslibs-*.whl + run: twine upload --skip-existing pvxslibs*.gz pvxslibs*.whl diff --git a/gha-set-pre.py b/gha-set-pre.py new file mode 100755 index 000000000..a2e487ddd --- /dev/null +++ b/gha-set-pre.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python +"""Push PRE=--pre +to the GHA environment for subsequent actions if building a pre-release. +""" + +from __future__ import print_function + +import os +import re + +with open('setup.py', 'r') as F: + comment, ver = re.search(r"(?m)^\s*(#)?\s*pvxs_ver\s*\+=\s*'([^']*)'.*", F.read()).groups() + +if not comment: + assert ver.find('a')!=-1, ver + print('Is pre-release', ver) + # https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable + #echo "{name}={value}" >> $GITHUB_ENV + + if 'GITHUB_ENV' in os.environ: + with open(os.environ['GITHUB_ENV'], 'a') as F: + F.write('PRE=--pre\n') + else: + print('Would export PRE=--pre') From a4b6e2acf6ef5eb56bab7ea10f13e52f4e3b7276 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 22 Dec 2025 18:33:04 -0800 Subject: [PATCH 14/45] 1.4.2a1 --- configure/CONFIG_PVXS_VERSION | 5 +++-- setup.py | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configure/CONFIG_PVXS_VERSION b/configure/CONFIG_PVXS_VERSION index 5d2fcd2a0..20e63b607 100644 --- a/configure/CONFIG_PVXS_VERSION +++ b/configure/CONFIG_PVXS_VERSION @@ -1,6 +1,6 @@ PVXS_MAJOR_VERSION = 1 PVXS_MINOR_VERSION = 4 -PVXS_MAINTENANCE_VERSION = 1 +PVXS_MAINTENANCE_VERSION = 2 # Version range conditions in Makefiles # @@ -12,7 +12,8 @@ PVXS_MAINTENANCE_VERSION = 1 # # ifneq ($(PVXS_X_Y_Z),YES) # PVXS != X.Y.Z # -PVXS_1_4_1 = YES +PVXS_1_4_2 = YES +PVXS_1_4_1 = NO PVXS_1_4_0 = NO PVXS_1_3_3 = NO PVXS_1_3_2 = NO diff --git a/setup.py b/setup.py index af450074c..b8eec6049 100755 --- a/setup.py +++ b/setup.py @@ -696,7 +696,7 @@ def define_DSOS(self): pvxs_ver = '%(PVXS_MAJOR_VERSION)s.%(PVXS_MINOR_VERSION)s.%(PVXS_MAINTENANCE_VERSION)s'%pvxsversion -# pvxs_ver += 'a3' +pvxs_ver += 'a1' with open(os.path.join(os.path.dirname(__file__), 'README.md')) as F: long_description = F.read() @@ -716,7 +716,6 @@ def define_DSOS(self): 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: Implementation :: CPython', - 'License :: OSI Approved :: BSD License', 'Intended Audience :: Science/Research', 'Topic :: Scientific/Engineering', 'Topic :: Software Development :: Libraries', From 3f5673624757f496b7fc11fe0141b1243bc95174 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Fri, 26 Dec 2025 15:17:04 -0800 Subject: [PATCH 15/45] maybe fix DBD mis-generation Add "pvxsIoc.dbd$(DEP):" rule to hopefully override "%.dbd$(DEP)" rule in Base. Also clean generated dbd --- ioc/Makefile | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ioc/Makefile b/ioc/Makefile index 5cc7f5e78..d5626d3d4 100644 --- a/ioc/Makefile +++ b/ioc/Makefile @@ -18,6 +18,7 @@ USR_CPPFLAGS += -I$(TOP)/src USR_CPPFLAGS += -DPVXS_IOC_API_BUILDING DBD += pvxsIoc.dbd +CLEANS += pvxsIoc.dbd INC += pvxs/iochooks.h @@ -80,14 +81,19 @@ LIB_LIBS += pvxs LIB_LIBS += $(EPICS_BASE_IOC_LIBS) #=========================== +# select DBD file depending on supported syntax +ifdef BASE_7_0 +PVXS_DBD = pvxs7x.dbd +else +PVXS_DBD = pvxs3x.dbd +endif + include $(TOP)/configure/RULES #---------------------------------------- # ADD RULES AFTER THIS LINE -ifdef BASE_7_0 -../O.Common/pvxsIoc.dbd: ../pvxs7x.dbd +$(COMMON_DIR)/pvxsIoc.dbd: ../$(PVXS_DBD) $(CP) $< $@ -else -../O.Common/pvxsIoc.dbd: ../pvxs3x.dbd - $(CP) $< $@ -endif + +pvxsIoc.dbd$(DEP): + echo "$(COMMONDEP_TARGET): ../Makefile" > $@ From cb627971984eeb390a3723716c34bf47630236bd Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 30 Dec 2025 17:15:29 -0800 Subject: [PATCH 16/45] rename lsetPVA -> lsetPVX --- ioc/pvalink.cpp | 2 +- ioc/pvalink.h | 2 +- ioc/pvalink_jlif.cpp | 6 +++--- ioc/pvxs7x.dbd | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ioc/pvalink.cpp b/ioc/pvalink.cpp index 121fc08f1..f661e2012 100644 --- a/ioc/pvalink.cpp +++ b/ioc/pvalink.cpp @@ -104,7 +104,7 @@ std::shared_ptr testGetPVALink(struct link *plink) { DBLocker lock(plink->precord); - if(plink->type!=JSON_LINK || !plink->value.json.jlink || plink->value.json.jlink->pif!=&lsetPVA) { + if(plink->type!=JSON_LINK || !plink->value.json.jlink || plink->value.json.jlink->pif!=&lsetPVX) { testAbort("Not a PVA link"); } pvaLink *pval = static_cast(plink->value.json.jlink); diff --git a/ioc/pvalink.h b/ioc/pvalink.h index 9a795b160..bad31ab53 100644 --- a/ioc/pvalink.h +++ b/ioc/pvalink.h @@ -60,7 +60,7 @@ struct pvaLink; struct pvaLinkChannel; extern lset pva_lset; -extern jlif lsetPVA; +extern jlif lsetPVX; struct pvaLinkConfig : public jlink { diff --git a/ioc/pvalink_jlif.cpp b/ioc/pvalink_jlif.cpp index 8eb8b7482..bde6a8c70 100644 --- a/ioc/pvalink_jlif.cpp +++ b/ioc/pvalink_jlif.cpp @@ -283,7 +283,7 @@ void pva_report(const jlink *rpjlink, int lvl, int indent) noexcept } //namespace -jlif lsetPVA = { +jlif lsetPVX = { "pva", &pva_alloc_jlink, &pva_free_jlink, @@ -306,6 +306,6 @@ jlif lsetPVA = { }} //namespace pvxs::ioc extern "C" { -using pvxs::ioc::lsetPVA; -epicsExportAddress(jlif, lsetPVA); +using pvxs::ioc::lsetPVX; +epicsExportAddress(jlif, lsetPVX); } diff --git a/ioc/pvxs7x.dbd b/ioc/pvxs7x.dbd index aa4957652..62a58c98d 100644 --- a/ioc/pvxs7x.dbd +++ b/ioc/pvxs7x.dbd @@ -1,5 +1,5 @@ registrar(pvxsBaseRegistrar) -link("pva", "lsetPVA") +link("pva", "lsetPVX") # from demo.cpp device(waveform, CONSTANT, devWfPDBQ2Demo, "QSRV2 Demo") From f764e00e16687511954bd433c64ee107b1f8d83d Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 30 Dec 2025 17:17:47 -0800 Subject: [PATCH 17/45] rename pvaLinkNWorkers -> pvxLinkNWorkers --- ioc/pvalink.cpp | 2 +- ioc/pvalink.h | 2 +- ioc/pvalink_channel.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ioc/pvalink.cpp b/ioc/pvalink.cpp index f661e2012..3fe8769fd 100644 --- a/ioc/pvalink.cpp +++ b/ioc/pvalink.cpp @@ -320,7 +320,7 @@ const iocshVarDef pvaLinkNWorkersDef[] = { { "pvaLinkNWorkers", iocshArgInt, - &pvaLinkNWorkers + &pvxLinkNWorkers }, {0, iocshArgInt, 0} }; diff --git a/ioc/pvalink.h b/ioc/pvalink.h index bad31ab53..eaafceae9 100644 --- a/ioc/pvalink.h +++ b/ioc/pvalink.h @@ -47,7 +47,7 @@ typedef epicsUInt64 epicsUTag; #endif extern "C" { - extern int pvaLinkNWorkers; + extern int pvxLinkNWorkers; } namespace pvxs { diff --git a/ioc/pvalink_channel.cpp b/ioc/pvalink_channel.cpp index d4d4a7866..960eeafe5 100644 --- a/ioc/pvalink_channel.cpp +++ b/ioc/pvalink_channel.cpp @@ -17,7 +17,7 @@ DEFINE_LOGGER(_logger, "pvxs.ioc.link.channel"); DEFINE_LOGGER(_logupdate, "pvxs.ioc.link.channel.update"); -int pvaLinkNWorkers = 1; +int pvxLinkNWorkers = 1; namespace pvxs { namespace ioc { @@ -42,7 +42,7 @@ linkGlobal_t::linkGlobal_t() // worker should be above PVA worker priority? epicsThreadPriorityMedium) { - // TODO respect pvaLinkNWorkers? + // TODO respect pvxLinkNWorkers? worker.start(); } From 9a13662e08bf3d97f5d17ad6eb0db287f3689ea6 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 30 Dec 2025 17:19:08 -0800 Subject: [PATCH 18/45] rename dbpvar -> dbpvxr --- ioc/pvalink.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ioc/pvalink.cpp b/ioc/pvalink.cpp index 3fe8769fd..1b43f7582 100644 --- a/ioc/pvalink.cpp +++ b/ioc/pvalink.cpp @@ -182,7 +182,7 @@ QSrvWaitForLinkUpdate::~QSrvWaitForLinkUpdate() } extern "C" -void dbpvar(const char *precordname, int level) +void dbpvxr(const char *precordname, int level) { try { if(!linkGlobal) { @@ -327,8 +327,8 @@ const iocshVarDef pvaLinkNWorkersDef[] = { void pvalink_enable() { - IOCShCommand("dbpvar", "dbpvar", "record name", "level") - .implementation<&dbpvar>(); + IOCShCommand("dbpvar", "record name", "level") + .implementation<&dbpvxr>(); iocshRegisterVariable(pvaLinkNWorkersDef); } From 8c07933fe0eb087ffb49c92e0576563b761ef8dc Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 30 Dec 2025 18:33:14 -0800 Subject: [PATCH 19/45] doc --- documentation/releasenotes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/documentation/releasenotes.rst b/documentation/releasenotes.rst index e57008645..cd98228d7 100644 --- a/documentation/releasenotes.rst +++ b/documentation/releasenotes.rst @@ -9,6 +9,9 @@ Release Notes * server: plug channel cache leak when close Channel while reusing Connection. * server: disable one-sided attempt to handle saturated connection. * ioc: add `pvxs_log_config()` and `pvxs_log_reset()` IOCsh functions. +* ioc: renamed semi-internal C symbol names to avoid conflicts with QSRV1: + `dbpvar()` -> ``dbpvxr()`, ``pvaLinkNWorkers`` -> ``pvxLinkNWorkers``. + Names in IOC shell remain unchanged. * tools: pvxvct can use endpoint syntax to listen for multicast on a specific interface. eg. ``pvxvct -B 224.1.2.23@eth0`` or ``pvxvct -B 224.1.2.23@10.1.1.100``. From 42ec1602a3385dfd1103d45790cb99780ca71bc1 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 5 Jan 2026 10:35:33 -0800 Subject: [PATCH 20/45] pvxput: do not mark all fields... --- tools/put.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/put.cpp b/tools/put.cpp index 5ee201fda..2d4b299cb 100644 --- a/tools/put.cpp +++ b/tools/put.cpp @@ -116,6 +116,8 @@ int main(int argc, char *argv[]) .pvRequest(request) .build([&values](Value&& prototype) -> Value { auto val = std::move(prototype); + // clear all defined fields, but retain "current" values for NTEnum lookup. + val.unmark(false, true); for(auto& pair : values) { try{ val[pair.first] = pair.second; From 91dd4d4592925a1edce35551c41f646f7bcba773 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 5 Jan 2026 10:38:56 -0800 Subject: [PATCH 21/45] pvxput: verbose flag show marked fields --- tools/put.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/put.cpp b/tools/put.cpp index 2d4b299cb..1298e7901 100644 --- a/tools/put.cpp +++ b/tools/put.cpp @@ -114,7 +114,7 @@ int main(int argc, char *argv[]) auto op =ctxt.put(pvname) .pvRequest(request) - .build([&values](Value&& prototype) -> Value { + .build([&values, verbose](Value&& prototype) -> Value { auto val = std::move(prototype); // clear all defined fields, but retain "current" values for NTEnum lookup. val.unmark(false, true); @@ -125,6 +125,11 @@ int main(int argc, char *argv[]) throw std::runtime_error(SB()<<"Unable to assign "< Date: Mon, 5 Jan 2026 11:00:01 -0800 Subject: [PATCH 22/45] ioc: test PUT to scalar mapping --- test/Makefile | 1 + test/batch.db | 41 +++++++++++++++++++++++++++++++++++++++++ test/testqgroup.cpp | 19 ++++++++++++++++++- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 test/batch.db diff --git a/test/Makefile b/test/Makefile index d26f93597..80b62a134 100644 --- a/test/Makefile +++ b/test/Makefile @@ -156,6 +156,7 @@ TESTFILES += ../image.json TESTFILES += ../iq.db TESTFILES += ../ntenum.db TESTFILES += ../const.db +TESTFILES += ../batch.db TESTFILES += ../qgroup.cmd TESTFILES += ../qgroup.json TESTS += testqgroup diff --git a/test/batch.db b/test/batch.db new file mode 100644 index 000000000..8471622c9 --- /dev/null +++ b/test/batch.db @@ -0,0 +1,41 @@ + +record(ao, "$(P)A") { + info(Q:group, { + "$(P)":{ + "A":{+channel:"VAL", +putorder:0} + } + }) +} + +record(ao, "$(P)B") { + field(FLNK, "$(P)SUM") + info(Q:group, { + "$(P)":{ + "B":{+channel:"VAL", +putorder:1} + } + }) +} + +record(ao, "$(P)C") { + info(Q:group, { + "$(P)":{ + "C":{+channel:"VAL"} # omit +putorder to prevent write + } + }) +} + +record(calc, "$(P)SUM") { + field(INPA, "$(P)A NPP MSS") + field(INPB, "$(P)B NPP MSS") + field(INPC, "$(P)C NPP MSS") + field(CALC, "A+B+C") + info(Q:group, { + "$(P)":{ + "SUM":{ + +channel:"VAL", + +putorder:2, + +trigger:"*" + } + } + }) +} diff --git a/test/testqgroup.cpp b/test/testqgroup.cpp index b9f61e38d..c6699d560 100644 --- a/test/testqgroup.cpp +++ b/test/testqgroup.cpp @@ -716,6 +716,21 @@ void testConst() ); } +void testBatch() +{ + testDiag("%s", __func__); + TestClient ctxt; + + ctxt.put("tst:b:") + .set("A.value", 1.0) + .set("B.value", 2.0) + .set("C.value", 4.0) // ignored + .exec()->wait(5.0); + + auto ret(ctxt.get("tst:b:").exec()->wait(5.0)); + testEq(ret["SUM.value"].as(), 3); +} + void testDbLoadGroup() { testDiag("%s", __func__); @@ -738,7 +753,7 @@ void testDbLoadGroup() MAIN(testqgroup) { - testPlan(38); + testPlan(39); testSetup(); { generalTimeRegisterCurrentProvider("test", 1, &testTimeCurrent); @@ -753,6 +768,7 @@ MAIN(testqgroup) testdbReadDatabase("ntenum.db", nullptr, "P=enm"); testdbReadDatabase("iq.db", nullptr, "N=iq:"); testdbReadDatabase("const.db", nullptr, "P=tst:"); + testdbReadDatabase("batch.db", nullptr, "P=tst:b:"); iocsh("../qgroup.cmd"); ioc.init(); testTable(); @@ -760,6 +776,7 @@ MAIN(testqgroup) testImage(); testIQ(); testConst(); + testBatch(); testDbLoadGroup(); } // call epics atexits explicitly to handle older base w/o de-init hooks From 597330c949f9eabb8a0ce2ea7a46a2ff2cfdb81a Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 5 Jan 2026 11:08:17 -0800 Subject: [PATCH 23/45] ioc: fix PUT to scalar mapping Mapping of eg. "X" needs update when "X.value" changes. Check for if any children of "X" are marked. Also send remote warning on attempt to write to unwritable field. --- ioc/groupsource.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/ioc/groupsource.cpp b/ioc/groupsource.cpp index 105a42a46..2a6d36d2f 100644 --- a/ioc/groupsource.cpp +++ b/ioc/groupsource.cpp @@ -498,17 +498,24 @@ static bool putGroupField(const Value& value, const Field& field, const SecurityClient& securityClient, - const GroupSecurityCache& groupSecurityCache) { + const GroupSecurityCache& groupSecurityCache, + server::RemoteLogger& notify) { // find the leaf node that the field refers to in the given value auto leafNode = field.findIn(value); - bool marked = leafNode.isMarked() && field.value && field.info.putOrder!=std::numeric_limits::min(); + bool putable = field.info.putOrder!=std::numeric_limits::min(); + bool marked = leafNode.isMarked(true, true) && field.value; + bool changing = marked && putable; + + if(marked && !putable) { + notify.logRemote(Level::Warn, SB()<& putOpe // Put the field didSomething |= putGroupField(value, field, groupSecurityCache.securityClients[fieldIndex], - groupSecurityCache); + groupSecurityCache, + *putOperation); fieldIndex++; } @@ -587,7 +595,8 @@ void GroupSource::putGroup(Group& group, std::unique_ptr& putOpe // Put the field didSomething |= putGroupField(value, field, groupSecurityCache.securityClients[fieldIndex], - groupSecurityCache); + groupSecurityCache, + *putOperation); fieldIndex++; // Unlock this field when locker goes out of scope } From 444d1ff1f12e5e92ce673bd171b82ebe05ec13b4 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 5 Jan 2026 13:00:22 -0800 Subject: [PATCH 24/45] colorize outer exception printing --- ioc/groupsourcehooks.cpp | 4 +--- qsrv/Makefile | 2 ++ qsrv/softMain.cpp | 3 ++- src/utilpvt.h | 5 +++++ tools/call.cpp | 8 ++++---- tools/get.cpp | 2 +- tools/info.cpp | 2 +- tools/list.cpp | 2 +- tools/monitor.cpp | 2 +- tools/mshim.cpp | 6 +++--- tools/put.cpp | 6 +++--- tools/pvxvct.cpp | 2 +- 12 files changed, 25 insertions(+), 19 deletions(-) diff --git a/ioc/groupsourcehooks.cpp b/ioc/groupsourcehooks.cpp index 8a2bd893b..d7ab319df 100644 --- a/ioc/groupsourcehooks.cpp +++ b/ioc/groupsourcehooks.cpp @@ -23,6 +23,7 @@ #include #include "qsrvpvt.h" +#include "utilpvt.h" #include "groupsource.h" #include "groupconfigprocessor.h" #include "iocshcommand.h" @@ -30,9 +31,6 @@ #if EPICS_VERSION_INT < VERSION_INT(7, 0, 3, 1) # define iocshSetError(ret) do { (void)ret; }while(0) #endif -#ifndef ERL_ERROR -# define ERL_ERROR "ERROR" -#endif // include last to avoid clash of #define printf with other headers #include diff --git a/qsrv/Makefile b/qsrv/Makefile index 03b3f1b86..9d231cf6f 100644 --- a/qsrv/Makefile +++ b/qsrv/Makefile @@ -5,6 +5,8 @@ include $(TOP)/configure/CONFIG # ADD MACRO DEFINITIONS AFTER THIS LINE #============================= +USR_CPPFLAGS += -I$(TOP)/src + PROD_IOC += softIocPVX softIocPVX_SRCS += softMain.cpp diff --git a/qsrv/softMain.cpp b/qsrv/softMain.cpp index aa8b0bbcf..7f3df8f09 100644 --- a/qsrv/softMain.cpp +++ b/qsrv/softMain.cpp @@ -32,6 +32,7 @@ #include "iocsh.h" #include "osiFileName.h" #include "epicsInstallDir.h" +#include "utilpvt.h" #include @@ -289,7 +290,7 @@ int main(int argc, char *argv[]) return 0; }catch(std::exception& e){ - std::cerr<<"Error: "< #include +#include #include #include @@ -46,6 +47,10 @@ # endif #endif +#ifndef ERL_ERROR +# define ERL_ERROR "ERROR" +#endif + #include namespace pvxs {namespace impl { diff --git a/tools/call.cpp b/tools/call.cpp index fa171ef14..9a853e551 100644 --- a/tools/call.cpp +++ b/tools/call.cpp @@ -86,14 +86,14 @@ int main(int argc, char *argv[]) auto sep = fv.find_first_of('='); if(sep==std::string::npos) { - std::cerr<<"Error: expected = not \""<= not \""<= not \""<= not \""< Date: Thu, 6 Nov 2025 17:09:24 -0800 Subject: [PATCH 25/45] udp: clarify orig/reply addressing, fix mcast handling ... in mshim and vct vct expand from addr to endpoint parsing --- src/server.cpp | 14 +++---- src/udp_collector.cpp | 90 +++++++++++++++++++++++-------------------- src/udp_collector.h | 7 ++-- tools/mshim.cpp | 10 ++--- tools/pvxvct.cpp | 7 ++-- 5 files changed, 69 insertions(+), 59 deletions(-) diff --git a/src/server.cpp b/src/server.cpp index f9c57fe7a..98afd9286 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -669,20 +669,20 @@ void Server::Pvt::onSearch(const UDPManager::Search& msg) // on UDPManager worker for(const auto& addr : ignoreList) { // expected to be a short list - if(msg.src.family()!=addr.family()) { + if(msg.origSrc.family()!=addr.family()) { // skip - } else if(msg.src->in.sin_addr.s_addr != addr->in.sin_addr.s_addr) { + } else if(msg.origSrc->in.sin_addr.s_addr != addr->in.sin_addr.s_addr) { // skip } else if(addr->in.sin_port==0) { // ignore all ports return; - } else if(msg.src->in.sin_port == addr->in.sin_port) { + } else if(msg.origSrc->in.sin_port == addr->in.sin_port) { // ignore specific sender port return; } } - log_debug_printf(serverio, "%s searching\n", msg.src.tostring().c_str()); + log_debug_printf(serverio, "%s searching\n", msg.origSrc.tostring().c_str()); searchOp._names.resize(msg.names.size()); for(auto i : range(msg.names.size())) { @@ -690,13 +690,13 @@ void Server::Pvt::onSearch(const UDPManager::Search& msg) searchOp._names[i]._claim = false; } static_assert(sizeof(searchOp._src) >= INET6_ADDRSTRLEN+1, ""); - switch(msg.server.family()) { + switch(msg.replyDest.family()) { case AF_INET: - evutil_inet_ntop(AF_INET, &msg.server->in.sin_addr, + evutil_inet_ntop(AF_INET, &msg.replyDest->in.sin_addr, searchOp._src, sizeof(searchOp._src)-1); break; case AF_INET6: - evutil_inet_ntop(AF_INET6, msg.server->in6.sin6_addr.s6_addr, + evutil_inet_ntop(AF_INET6, msg.replyDest->in6.sin6_addr.s6_addr, searchOp._src, sizeof(searchOp._src)-1); break; default: diff --git a/src/udp_collector.cpp b/src/udp_collector.cpp index fbd81f57e..054c78568 100644 --- a/src/udp_collector.cpp +++ b/src/udp_collector.cpp @@ -129,7 +129,7 @@ UDPCollector::UDPCollector(UDPManager::Pvt *manager, int af, uint16_t requested_ ,sock(af, SOCK_DGRAM, 0) ,rx(__FILE__, __LINE__, event_new(manager->loop.base, sock.sock, EV_READ|EV_PERSIST, &handle_static, this)) - ,beaconMsg(src) + ,beaconMsg(origSrc) { manager->loop.assertInLoop(); @@ -238,7 +238,7 @@ bool UDPCollector::handle_one(const IfaceMap::Current& ifinfo) // For Search messages, we use PV name strings in-place by adding nils. // Ensure one extra byte at the end of the buffer for a nil after the last PV name - recvfromx rx{sock.sock, (char*)rxbuf, rxlen, &src, &dest}; + recvfromx rx{sock.sock, (char*)rxbuf, rxlen, &origSrc, &origDest}; const int nrx = rx.call(); if(nrx>=0 && rx.ndrop!=0u && prevndrop!=rx.ndrop) { @@ -255,12 +255,12 @@ bool UDPCollector::handle_one(const IfaceMap::Current& ifinfo) return false; // wait for more I/O } - if(dest.family()!=AF_UNSPEC) - dest.setPort(bind_addr.port()); + if(origDest.family()!=AF_UNSPEC) + origDest.setPort(bind_addr.port()); - if(src.isMCast()) { + if(origSrc.isMCast()) { // should never happen. It it does, we won't be tricked into amplifying a DDoS. - log_debug_printf(logio, "Ignoring UDP with mcast source %s.\n", src.tostring().c_str()); + log_debug_printf(logio, "Ignoring UDP with mcast source %s.\n", origSrc.tostring().c_str()); return true; } @@ -271,14 +271,16 @@ bool UDPCollector::handle_one(const IfaceMap::Current& ifinfo) } srcIface = &ifit->second; + replySrc->in.sin_family = replyDest->in.sin_family = AF_UNSPEC; // spoil, will be set later + // detect "origin" and reply-from address. (dest in request becomes source in reply) origin_t origin = Broadcast; - if(srcIface->isLO && dest.compare(lo_mcast_addr.addr,false)==0) { + if(srcIface->isLO && origDest.compare(lo_mcast_addr.addr,false)==0) { // packet forwarded by a local PVA peer (maybe us) as IPv4 local multicast origin = Forwarded; // UDP header info of forwarder not relevant to reply. Spoil... - src = SockAddr(src.family(), src.port()); - dest = SockAddr(dest.family(), dest.port()); + origSrc = SockAddr(origSrc.family(), origSrc.port()); + origDest = replySrc = SockAddr(origDest.family(), origDest.port()); srcIface = nullptr; } else { @@ -287,36 +289,38 @@ bool UDPCollector::handle_one(const IfaceMap::Current& ifinfo) // broadcast, look up corresponding local iface addr // multicast, // ensure that dest is an interface address - auto ifit(srcIface->bcast.find(dest)); + auto ifit(srcIface->bcast.find(origDest)); if(ifit!=srcIface->bcast.end()) { // dest is bcast, so replace with associated iface address - dest = ifit->second.withPort(dest.port()); + replySrc = ifit->second.withPort(origDest.port()); - } else if((ifit=srcIface->addrs.find(dest))!=srcIface->addrs.end()) { + } else if((ifit=srcIface->addrs.find(origDest))!=srcIface->addrs.end()) { // dest is interface address. Nothing to do. origin = Forwarding; + replySrc = origDest; } else { // mcast // reply from an arbitrary address on the source interface bool found = false; for(auto& it : srcIface->addrs) { - if(it.first.family()==dest.family()) { - dest = it.first.withPort(dest.port()); + if(it.first.family()==origDest.family()) { + replySrc = it.first.withPort(origDest.port()); found = true; break; } } if(!found) { // let host mcast routing try... - dest = SockAddr(dest.family(), dest.port()); + replySrc = SockAddr(origDest.family(), origDest.port()); } } } - log_hex_printf(logio, Level::Debug, rxbuf, nrx, "UDP Rx %d, %s -> %s @%u (%s) : orig %d\n", - nrx, src.tostring().c_str(), dest.tostring().c_str(), unsigned(rx.dstif), bind_addr.tostring().c_str(), - origin); + log_hex_printf(logio, Level::Debug, rxbuf, nrx, "UDP Rx %d, %s -> %s @%u (%s) : orig %d replyfrom %s\n", + nrx, origSrc.tostring().c_str(), origDest.tostring().c_str(), + unsigned(rx.dstif), bind_addr.tostring().c_str(), + origin, replySrc.tostring().c_str()); process_one(rxbuf, nrx, origin, ifinfo); return true; @@ -332,7 +336,7 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, if(!M.good() || (head.flags&(pva_flags::Control|pva_flags::SegMask))) { // UDP packets can't contain control messages, or use segmentation - log_hex_printf(logio, Level::Debug, &buf[0], nrx, "Ignore UDP message from %s\n", src.tostring().c_str()); + log_hex_printf(logio, Level::Debug, &buf[0], nrx, "Ignore UDP message from %s\n", origSrc.tostring().c_str()); return; } @@ -360,22 +364,25 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, M.skip(3, __FILE__, __LINE__); // unused/reserved + SockAddr server; auto save_replyAddr = M.save(); from_wire(M, server); from_wire(M, port); if(server.isAny()) { - server = src; + replyDest = origSrc; // default if(origin==Forwarded || origin==OriginTag) { log_warn_printf(logio, "Forwarded SEARCH with reply to sender never works. Ignore.%s", "\n"); return; } + } else { + replyDest = server; } - server.setPort(port); + replyDest.setPort(port); if(!M.good()) return; - if(origin==Broadcast || dest.family()!=AF_INET) { + if(origin==Broadcast || origDest.family()!=AF_INET) { // bcast, mcast, or not ipv4 } else if(origin==Forwarding) { @@ -385,7 +392,7 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, // recipient of forwarded message must use, and trust, replyAddr in body :( { FixedBuf R(M.be, save_replyAddr, 16u); - to_wire(R, server); + to_wire(R, replyDest); assert(R.good()); } forwardM(buf, nrx); @@ -396,7 +403,7 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, * some PVA implementations don't prefix forwarded messages with CMD_ORIGIN_TAG */ log_debug_printf(logio, "Ignore as originated for %s\n", - dest.tostring().c_str()); + origDest.tostring().c_str()); } // so far, only "tcp" transport has ever been seen. @@ -436,15 +443,12 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, M.skip(chlen.size, __FILE__, __LINE__); } - // used by our reply() - src = server; - if(M.good()) { // ensure nil for final PV name *M.save() = '\0'; for(auto L : listeners) { - if(L->searchCB && (L->dest.addr.isAny() || L->dest.addr==dest)) { + if(L->searchCB && (L->dest.addr.isAny() || L->dest.addr==origDest)) { (L->searchCB)(*this); } } @@ -467,7 +471,7 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, from_wire(M, beaconMsg.server); from_wire(M, port); if(beaconMsg.server.isAny()) { - beaconMsg.server = src; + beaconMsg.server = origSrc; } beaconMsg.server.setPort(port); @@ -477,7 +481,7 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, if(M.good()) { for(auto L : listeners) { - if(L->beaconCB && (L->dest.addr.isAny() || L->dest.addr==dest)) { + if(L->beaconCB && (L->dest.addr.isAny() || L->dest.addr==origDest)) { (L->beaconCB)(beaconMsg); } } @@ -510,9 +514,13 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, if(isany || ifit!=ifinfo.byAddr.end()) { // original destination is wildcard, or local interface address originaddr.setPort(bind_addr.port()); - dest = originaddr; - if(!isany) + origDest = originaddr; + if(!isany) { srcIface = ifit->second.first; + replySrc = origDest; + } else { + replySrc = SockAddr::any(origSrc.family(), bind_addr.port()); + } process_one(M.save(), M.size(), OriginTag, ifinfo); return; @@ -536,7 +544,7 @@ void UDPCollector::process_one(const uint8_t *buf, size_t nrx, origin_t origin, void UDPCollector::forwardM(const uint8_t *pbuf, size_t plen) { log_debug_printf(logio, "Forward as originated for %s\n", - dest.tostring().c_str()); + origDest.tostring().c_str()); assert(buf.size() > cmd_origin_tag_size); assert(pbuf==&buf[cmd_origin_tag_size]); @@ -545,17 +553,17 @@ void UDPCollector::forwardM(const uint8_t *pbuf, size_t plen) FixedBuf M(true, &buf[0], cmd_origin_tag_size); to_wire(M, Header{CMD_ORIGIN_TAG, 0, 16u}); - to_wire(M, dest); + to_wire(M, origDest); assert(M.good()); assert(M.save()==&buf[cmd_origin_tag_size]); } // mcast_prep_sendto() will override routing srcIface = nullptr; - dest = SockAddr(src.family()); + replySrc = SockAddr(replySrc.family(), replySrc.port()); sock.mcast_prep_sendto(lo_mcast_addr); - src = lo_mcast_addr.addr; + replyDest = lo_mcast_addr.addr; reply(&buf[0], cmd_origin_tag_size+plen); } @@ -564,11 +572,11 @@ bool UDPCollector::reply(const void *msg, size_t msglen) const manager->loop.assertInLoop(); log_hex_printf(logio, Level::Debug, msg, msglen, "Send %s -> %s, %s,%s\n", - bind_addr.tostring().c_str(), src.tostring().c_str(), - dest.tostring().c_str(), srcIface ? srcIface->name.c_str() : "N/A"); + bind_addr.tostring().c_str(), replyDest.tostring().c_str(), + replySrc.tostring().c_str(), srcIface ? srcIface->name.c_str() : "N/A"); // reply to original source, through the original interface, as from original destination - auto ntx = sendtox{sock.sock, (char*)msg, msglen, &src, &dest, srcIface ? srcIface->index : 0}.call(); + auto ntx = sendtox{sock.sock, (char*)msg, msglen, &replyDest, &replySrc, srcIface ? srcIface->index : 0}.call(); if(ntx<0) { int err = evutil_socket_geterror(sock.sock); if(err==SOCK_EWOULDBLOCK || err==EAGAIN || err==SOCK_EINTR) { @@ -576,8 +584,8 @@ bool UDPCollector::reply(const void *msg, size_t msglen) const } else { log_warn_printf(logio, "UDP TX Error: bound:%s src:%s,%s dst:%s : (%d) %s\n", name.c_str(), - dest.tostring().c_str(), srcIface ? srcIface->name.c_str() : "N/A", - src.tostring().c_str(), + replySrc.tostring().c_str(), srcIface ? srcIface->name.c_str() : "N/A", + replyDest.tostring().c_str(), err, evutil_socket_error_to_string(err)); } return false; // wait for more I/O diff --git a/src/udp_collector.h b/src/udp_collector.h index 542bb0821..a4425dfdd 100644 --- a/src/udp_collector.h +++ b/src/udp_collector.h @@ -46,9 +46,10 @@ struct PVXS_API UDPManager struct PVXS_API Search { std::vector otherproto; // any protocols other than "tcp" - SockAddr src; // sender/client address - SockAddr dest; // destination IP used by client - SockAddr server; + SockAddr origSrc; // sender/client address + SockAddr origDest; // destination IP used by client + SockAddr replySrc; // reply source address selects outgoing interface (port not used) + SockAddr replyDest; const IfaceMap::Iface* srcIface = nullptr; uint32_t searchID=0; uint8_t peerVersion=0; diff --git a/tools/mshim.cpp b/tools/mshim.cpp index 71ebfd4dc..0ec7b8995 100644 --- a/tools/mshim.cpp +++ b/tools/mshim.cpp @@ -103,9 +103,9 @@ struct App { uint8_t(msg.mustReply ? pva_search_flags::MustReply : 0u), 0,0,0 }); - assert(!msg.server.isAny() && msg.server.family()==AF_INET); // UDPManager has already handled this case - to_wire(buf, msg.server); - to_wire(buf, uint16_t(msg.server.port())); + assert(!msg.replyDest.isAny() && msg.replyDest.family()==AF_INET); // UDPManager has already handled this case + to_wire(buf, msg.replyDest); + to_wire(buf, uint16_t(msg.replyDest.port())); size_t nproto = msg.otherproto.size(); if(msg.protoTCP) @@ -166,8 +166,8 @@ struct App { } else { log_debug_printf(applog, "Forwarded search to %s -> %s -> %s?\n", - msg.src.tostring().c_str(), - msg.server.tostring().c_str(), + msg.origSrc.tostring().c_str(), + msg.replyDest.tostring().c_str(), std::string(SB()<[:port] Listen on the given interface(s). May be repeated.\n" + " -B [,ttl#][@iface][:port]\n" " -H host Show only message sent from this peer. May be repeated.\n" " -P pvname Show only searches for this PV name. May be repeated.\n" < Date: Mon, 5 Jan 2026 16:08:20 -0800 Subject: [PATCH 26/45] minor --- configure/RELEASE | 3 --- 1 file changed, 3 deletions(-) diff --git a/configure/RELEASE b/configure/RELEASE index dae2803be..eaf6a8b6a 100644 --- a/configure/RELEASE +++ b/configure/RELEASE @@ -25,9 +25,6 @@ #MODULES = /path/to/modules #MYMODULE = $(MODULES)/my-module -# If using the sequencer, point SNCSEQ at its top directory: -#SNCSEQ = $(MODULES)/seq-ver - # EPICS_BASE should appear last so earlier modules can override stuff: #EPICS_BASE = /path/to/epics-base From 6168c8ff4d6d2ae2e555d73059bcf593a8a10c2b Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 5 Jan 2026 13:26:37 -0800 Subject: [PATCH 27/45] 1.5.0a1 Public ABI change in cb627971984eeb390a3723716c34bf47630236bd triggers jump past 1.4.2 (a4b6e2acf6ef5eb56bab7ea10f13e52f4e3b7276) --- configure/CONFIG_PVXS_VERSION | 6 +++--- documentation/releasenotes.rst | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configure/CONFIG_PVXS_VERSION b/configure/CONFIG_PVXS_VERSION index 20e63b607..991f4f393 100644 --- a/configure/CONFIG_PVXS_VERSION +++ b/configure/CONFIG_PVXS_VERSION @@ -1,6 +1,6 @@ PVXS_MAJOR_VERSION = 1 -PVXS_MINOR_VERSION = 4 -PVXS_MAINTENANCE_VERSION = 2 +PVXS_MINOR_VERSION = 5 +PVXS_MAINTENANCE_VERSION = 0 # Version range conditions in Makefiles # @@ -12,7 +12,7 @@ PVXS_MAINTENANCE_VERSION = 2 # # ifneq ($(PVXS_X_Y_Z),YES) # PVXS != X.Y.Z # -PVXS_1_4_2 = YES +PVXS_1_5_0 = YES PVXS_1_4_1 = NO PVXS_1_4_0 = NO PVXS_1_3_3 = NO diff --git a/documentation/releasenotes.rst b/documentation/releasenotes.rst index cd98228d7..0d0d6cab3 100644 --- a/documentation/releasenotes.rst +++ b/documentation/releasenotes.rst @@ -3,7 +3,7 @@ Release Notes ============= -1.4.2 (UNRELEASED) +1.5.0 (UNRELEASED) ------------------ * server: plug channel cache leak when close Channel while reusing Connection. From 9c3a9ca8ee486f828e6c9f132abac31193a98ef7 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Wed, 7 Jan 2026 17:51:39 -0800 Subject: [PATCH 28/45] 1.5.0 --- documentation/ioc.rst | 4 ++-- documentation/releasenotes.rst | 5 +++-- setup.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/documentation/ioc.rst b/documentation/ioc.rst index 190bef656..0f7d78136 100644 --- a/documentation/ioc.rst +++ b/documentation/ioc.rst @@ -42,14 +42,14 @@ served by the Integrated PVA server. Enable additional :ref:`logconfig` as if more comma separated key=VALUE pairs were appended to the **$PVXS_LOG** environment variable. - Since UNRELEASED + Since 1.5.0 .. cpp:function:: void pvxs_log_reset() Reset logging to defaults. Negates the effects of **$PVXS_LOG** and later configuration. - Since UNRELEASED + Since 1.5.0 Adding custom PVs to Server --------------------------- diff --git a/documentation/releasenotes.rst b/documentation/releasenotes.rst index 0d0d6cab3..286cb1b2f 100644 --- a/documentation/releasenotes.rst +++ b/documentation/releasenotes.rst @@ -3,8 +3,8 @@ Release Notes ============= -1.5.0 (UNRELEASED) ------------------- +1.5.0 (Jan 2026) +---------------- * server: plug channel cache leak when close Channel while reusing Connection. * server: disable one-sided attempt to handle saturated connection. @@ -12,6 +12,7 @@ Release Notes * ioc: renamed semi-internal C symbol names to avoid conflicts with QSRV1: `dbpvar()` -> ``dbpvxr()`, ``pvaLinkNWorkers`` -> ``pvxLinkNWorkers``. Names in IOC shell remain unchanged. +* ioc: fix PUT to scalar mapping * tools: pvxvct can use endpoint syntax to listen for multicast on a specific interface. eg. ``pvxvct -B 224.1.2.23@eth0`` or ``pvxvct -B 224.1.2.23@10.1.1.100``. diff --git a/setup.py b/setup.py index b8eec6049..26699b4a3 100755 --- a/setup.py +++ b/setup.py @@ -696,7 +696,7 @@ def define_DSOS(self): pvxs_ver = '%(PVXS_MAJOR_VERSION)s.%(PVXS_MINOR_VERSION)s.%(PVXS_MAINTENANCE_VERSION)s'%pvxsversion -pvxs_ver += 'a1' +#pvxs_ver += 'a1' with open(os.path.join(os.path.dirname(__file__), 'README.md')) as F: long_description = F.read() From 48b260d008bd1d65c5f132c54523e27b41d0f4c3 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 6 Jan 2026 16:31:49 -0800 Subject: [PATCH 29/45] remove residual ifdef AF_INET6 Incomplete leftovers of an early attempt to support RTEMS "legacy" IP stack, which has no IPv6 support. --- src/osiSockExt.h | 2 -- src/util.cpp | 18 ------------------ 2 files changed, 20 deletions(-) diff --git a/src/osiSockExt.h b/src/osiSockExt.h index 8e10361ff..712dce35e 100644 --- a/src/osiSockExt.h +++ b/src/osiSockExt.h @@ -42,9 +42,7 @@ struct PVXS_API SockAddr { union store_t { sockaddr sa; sockaddr_in in; -#ifdef AF_INET6 sockaddr_in6 in6; -#endif }; private: store_t store; diff --git a/src/util.cpp b/src/util.cpp index 8f1c09175..76f473098 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -416,9 +416,7 @@ size_t SockAddr::size() const noexcept { switch(store.sa.sa_family) { case AF_INET: return sizeof(store.in); -#ifdef AF_INET6 case AF_INET6: return sizeof(store.in6); -#endif default: // AF_UNSPEC and others return sizeof(store); } @@ -428,9 +426,7 @@ unsigned short SockAddr::port() const noexcept { switch(store.sa.sa_family) { case AF_INET: return ntohs(store.in.sin_port); -#ifdef AF_INET6 case AF_INET6:return ntohs(store.in6.sin6_port); -#endif default: return 0; } } @@ -439,9 +435,7 @@ void SockAddr::setPort(unsigned short port) { switch(store.sa.sa_family) { case AF_INET: store.in.sin_port = htons(port); break; -#ifdef AF_INET6 case AF_INET6:store.in6.sin6_port = htons(port); break; -#endif default: throw std::logic_error("SockAddr: set family before port"); } @@ -559,9 +553,7 @@ bool SockAddr::isAny() const noexcept { switch(store.sa.sa_family) { case AF_INET: return store.in.sin_addr.s_addr==htonl(INADDR_ANY); -#ifdef AF_INET6 case AF_INET6: return IN6_IS_ADDR_UNSPECIFIED(&store.in6.sin6_addr); -#endif default: return false; } } @@ -570,9 +562,7 @@ bool SockAddr::isLO() const noexcept { switch(store.sa.sa_family) { case AF_INET: return store.in.sin_addr.s_addr==htonl(INADDR_LOOPBACK); -#ifdef AF_INET6 case AF_INET6: return IN6_IS_ADDR_LOOPBACK(&store.in6.sin6_addr); -#endif default: return false; } } @@ -581,9 +571,7 @@ bool SockAddr::isMCast() const noexcept { switch(store.sa.sa_family) { case AF_INET: return IN_MULTICAST(ntohl(store.in.sin_addr.s_addr)); -#ifdef AF_INET6 case AF_INET6: return IN6_IS_ADDR_MULTICAST(&store.in6.sin6_addr); -#endif default: return false; } } @@ -641,12 +629,10 @@ SockAddr SockAddr::any(int af, unsigned port) ret->in.sin_addr.s_addr = htonl(INADDR_ANY); ret->in.sin_port = htons(port); break; -#ifdef AF_INET6 case AF_INET6: ret->in6.sin6_addr = IN6ADDR_ANY_INIT; ret->in6.sin6_port = htons(port); break; -#endif default: throw std::invalid_argument("Unsupported address family"); } @@ -661,12 +647,10 @@ SockAddr SockAddr::loopback(int af, unsigned port) ret->in.sin_addr.s_addr = htonl(INADDR_LOOPBACK); ret->in.sin_port = htons(port); break; -#ifdef AF_INET6 case AF_INET6: ret->in6.sin6_addr = IN6ADDR_LOOPBACK_INIT; ret->in6.sin6_port = htons(port); break; -#endif default: throw std::invalid_argument("Unsupported address family"); } @@ -688,7 +672,6 @@ std::ostream& operator<<(std::ostream& strm, const SockAddr& addr) strm<<':'<in.sin_port); break; } -#ifdef AF_INET6 case AF_INET6: { char buf[INET6_ADDRSTRLEN+1]; if(evutil_inet_ntop(AF_INET6, &addr->in6.sin6_addr, buf, sizeof(buf))) { @@ -704,7 +687,6 @@ std::ostream& operator<<(std::ostream& strm, const SockAddr& addr) strm<<':'<"; break; From 6a53998a1c95db367032d884f750fb1e911edd37 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 19 Jan 2026 18:34:01 -0800 Subject: [PATCH 30/45] epicsSignalInstallSigPipeIgnore() Maybe only relevant with OSX now? https://github.com/epics-base/pvxs/issues/149 --- src/os/WIN32/osdSockExt.cpp | 3 +++ src/os/default/osdSockExt.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/os/WIN32/osdSockExt.cpp b/src/os/WIN32/osdSockExt.cpp index ccaf7d15c..6b21d5ae6 100644 --- a/src/os/WIN32/osdSockExt.cpp +++ b/src/os/WIN32/osdSockExt.cpp @@ -17,6 +17,7 @@ #include "evhelper.h" #include +#include #include # include @@ -69,6 +70,8 @@ void oseDoOnce() evsocket::canIPv6 = evsocket::init_canIPv6(); evsocket::ipstack = is_wine() ? evsocket::Linsock : evsocket::Winsock; + + epicsSignalInstallSigPipeIgnore(); // so far a no-op } void osiSockAttachExt() diff --git a/src/os/default/osdSockExt.cpp b/src/os/default/osdSockExt.cpp index 840dd7836..1684a1a5d 100644 --- a/src/os/default/osdSockExt.cpp +++ b/src/os/default/osdSockExt.cpp @@ -24,6 +24,8 @@ extern "C" { } #endif +#include + // some *BSD (OSX but not RTEMS5/libbsd) use IPV6_PKTINFO to enable RX #if defined(IPV6_PKTINFO) && !defined(IPV6_RECVPKTINFO) # define IPV6_RECVPKTINFO IPV6_PKTINFO @@ -47,6 +49,7 @@ void oseDoOnce() #else evsocket::ipstack = evsocket::GenericBSD; #endif + epicsSignalInstallSigPipeIgnore(); } void osiSockAttachExt() { From a7d77da776d1efa1dd1ab1c0719f0deb6bd0b8ca Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 19 Jan 2026 18:32:38 -0800 Subject: [PATCH 31/45] SO_NOSIGPIPE OSX specific means to disable SIGPIPE for individual sockets --- src/clientconn.cpp | 5 ++++- src/evhelper.cpp | 16 ++++++++++++++++ src/evhelper.h | 3 +++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/clientconn.cpp b/src/clientconn.cpp index 2539204c4..16aaeaae9 100644 --- a/src/clientconn.cpp +++ b/src/clientconn.cpp @@ -61,10 +61,13 @@ void Connection::startConnecting() { assert(!this->bev); + evsocket sock(peerAddr.family(), SOCK_STREAM, 0); decltype(this->bev) bev(__FILE__, __LINE__, - bufferevent_socket_new(context->tcp_loop.base, -1, + bufferevent_socket_new(context->tcp_loop.base, sock.sock, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + sock.release(); // hand-off ownership to bev + bufferevent_setcb(bev.get(), &bevReadS, nullptr, &bevEventS, this); timeval tmo(totv(context->effective.tcpTimeout)); diff --git a/src/evhelper.cpp b/src/evhelper.cpp index 6001d486f..41e8f5282 100644 --- a/src/evhelper.cpp +++ b/src/evhelper.cpp @@ -397,6 +397,17 @@ evsocket::evsocket(int af, evutil_socket_t sock, bool blocking) evutil_make_socket_closeonexec(sock); +#ifdef SO_NOSIGPIPE + // probably OSX only + { + int val = 1; + if(setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, (char*)&val, sizeof(val))) { + log_warn_printf(logerr, "Unable to set SO_NOSIGPIPE (err=%d).\n", + evutil_socket_geterror(sock)); + } + } +#endif + if(!blocking && evutil_make_socket_nonblocking(sock)) { evutil_closesocket(sock); throw std::runtime_error("Unable to make non-blocking socket"); @@ -453,6 +464,11 @@ evsocket::~evsocket() evutil_closesocket(sock); } +void evsocket::release() +{ + sock = evutil_socket_t(-1); +} + SockAddr evsocket::sockname() const { SockAddr addr; diff --git a/src/evhelper.h b/src/evhelper.h index d90762b59..dcdbcc206 100644 --- a/src/evhelper.h +++ b/src/evhelper.h @@ -251,6 +251,9 @@ struct PVXS_API evsocket ~evsocket(); + // caller takes ownership of socket + void release(); + SockAddr sockname() const; // test validity From 12eeb42f6b4e70e66ea5c8d1ae22424802b582ad Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 19 Jan 2026 19:16:09 -0800 Subject: [PATCH 32/45] test explicitly typed ANY assignment --- test/testdata.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/testdata.cpp b/test/testdata.cpp index 51b295f01..1c11a4036 100644 --- a/test/testdata.cpp +++ b/test/testdata.cpp @@ -124,8 +124,20 @@ void testAssignAny() val = 42; + // automagic ANY assignment promotes to StoreType + testEq(val["->"].type(), TypeCode::Int64); + testEq(val.as(), 42); + + { + auto us = TypeDef(TypeCode::UInt32).create(); + us = -1; + val.from(us); // typed ANY assignment + } + testEq(val["->"].type(), TypeCode::UInt32); + testEq(val.as(), 0xffffffff); + testThrows([&val](){ - val.from(val); + val.from(val); // self assignment would create infinite recursion }); } @@ -550,7 +562,7 @@ void test_cache_sync() MAIN(testdata) { - testPlan(189); + testPlan(193); testSetup(); testTraverse(); testAssign(); From a351943927148b569f0daa0abf08dc3f9f33090e Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 26 Jan 2026 13:28:42 -0800 Subject: [PATCH 33/45] minor --- src/clientget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clientget.cpp b/src/clientget.cpp index 7922427c6..515adf555 100644 --- a/src/clientget.cpp +++ b/src/clientget.cpp @@ -315,7 +315,7 @@ struct GPROp : public OperationBase // nothing more needed } else { - throw std::logic_error("Invalid state in GPR sendReply()"); + throw std::logic_error(SB()<<"Invalid state "<statTx += chan->conn->enqueueTxBody(state==GPROp::Done ? CMD_DESTROY_REQUEST : (pva_app_msg_t)op); From 3145388df5c358e3d9ad6cbd17623eebb235a809 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 27 Jan 2026 14:07:56 -0800 Subject: [PATCH 34/45] testStrMatch wrong argument order --- test/testput.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/testput.cpp b/test/testput.cpp index d0246103d..0f5a57f76 100644 --- a/test/testput.cpp +++ b/test/testput.cpp @@ -426,7 +426,7 @@ void testError(ErrorSource::phase_t phase) auto val = actual(); testTrue(false)<<"unexpected result\n"< Date: Wed, 28 Jan 2026 09:39:50 -0800 Subject: [PATCH 35/45] fix minor Implicitly treated as 'char' due to fun type casting rules... a351943927148b569f0daa0abf08dc3f9f33090e --- src/clientget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/clientget.cpp b/src/clientget.cpp index 515adf555..adaf396f8 100644 --- a/src/clientget.cpp +++ b/src/clientget.cpp @@ -315,7 +315,7 @@ struct GPROp : public OperationBase // nothing more needed } else { - throw std::logic_error(SB()<<"Invalid state "<statTx += chan->conn->enqueueTxBody(state==GPROp::Done ? CMD_DESTROY_REQUEST : (pva_app_msg_t)op); From d90e19e350226676d6042969bfa2e6fec8cc7b4a Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 26 Jan 2026 19:27:12 -0800 Subject: [PATCH 36/45] doc: fix markdown style links in rST --- documentation/details.rst | 14 +++++++------- documentation/releasenotes.rst | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/documentation/details.rst b/documentation/details.rst index 510a06738..7bbeef94d 100644 --- a/documentation/details.rst +++ b/documentation/details.rst @@ -151,7 +151,7 @@ When committing changes please do: Contributors ------------ -Who did the [work](https://github.com/epics-base/pvxs/graphs/contributors) to make PVXS what it is. +Who did the `work `_ to make PVXS what it is. .. comment: git log --format=format:%aN|sort -u|while read aa; do echo "* $aa"; done @@ -170,12 +170,12 @@ Who did the [work](https://github.com/epics-base/pvxs/graphs/contributors) to ma Those who supported this work. -* [ALS-U](https://als.lbl.gov/als-u/overview/) project at [Berkeley Lab](https://www.lbl.gov/) -* [Diamond Light Source](https://www.diamond.ac.uk/) -* [European Spallation Source](https://europeanspallationsource.se/) -* [Fermilab](https://fnal.gov/) -* [SLAC National Accelerator Laboratory](https://www6.slac.stanford.edu/) -* [SNS](https://neutrons.ornl.gov/sns) at [Oak Ridge National Lab](https://www.ornl.gov/) +* `ALS-U `_ project at `Berkeley Lab `_ +* `Diamond Light Source `_ +* `European Spallation Source `_ +* `Fermilab `_ +* `SLAC National Accelerator Laboratory `_ +* `SNS `_ at `Oak Ridge National Lab `_ Implementation Notes ==================== diff --git a/documentation/releasenotes.rst b/documentation/releasenotes.rst index 286cb1b2f..40647539b 100644 --- a/documentation/releasenotes.rst +++ b/documentation/releasenotes.rst @@ -89,7 +89,7 @@ Release Notes released promptly when that operation is ended. * server: relax post() after finish(). Return false instead of throwing ``std::logic_error``. * ioc: ensure db_cancel_event() before ~MonitorControlOp - * Workaround for [db_cancel_event()](https://github.com/epics-base/epics-base/issues/423) bug. + * Workaround for `db_cancel_event() `_ bug. * ioc: Fix typo preventing processing of DBR_STRING fields. * ioc: fix group put always `dbProcess()`. * ioc: fix block=true to DBF_ENUM. From 1e43dfd8db3b7d113fcaa779a3aefaf9c876eaf4 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 26 Jan 2026 17:19:10 -0800 Subject: [PATCH 37/45] doc: switch to sphinxdoc theme --- documentation/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/conf.py b/documentation/conf.py index 8b05315bd..af7cc3098 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -110,7 +110,7 @@ def read_version(fmt): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = 'sphinxdoc' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the From a4259c7376372c0376c5ca9190b46ff12a04bea6 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Thu, 22 Jan 2026 12:24:09 -0800 Subject: [PATCH 38/45] doc: reorganize netconfig --- documentation/client.rst | 2 +- documentation/netconfig.rst | 68 ++++++++++++++++++++----------------- documentation/server.rst | 1 + 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/documentation/client.rst b/documentation/client.rst index 7e42de748..087bac8c7 100644 --- a/documentation/client.rst +++ b/documentation/client.rst @@ -13,7 +13,7 @@ Client API Configuration ------------- -The recommended starting point is creating new context configured from ``EPICS_PVA_*`` :ref:`environ`. +The recommended starting point is creating new context configured from ``$EPICS_PVA_*`` :ref:`environ`. Use `pvxs::client::Context::fromEnv`. EPICS_PVA_ADDR_LIST diff --git a/documentation/netconfig.rst b/documentation/netconfig.rst index 579eaf3ff..53ac960ad 100644 --- a/documentation/netconfig.rst +++ b/documentation/netconfig.rst @@ -3,38 +3,9 @@ PVA Network Configuration ========================= -Big Picture ------------ - -A PV Access network protocol operation proceeds in two phases: -PV name resolution, and data transfer. -Name resolution is the process is determining which PVA server claims to provide each PV name. -Once this is known, a TCP connection is open to that server, and the operation(s) are executed. - -The PVA Name resolution process is similar to Channel Access protocol. - -When a name needs to be resolved, a PVA client will begin sending UDP search messages to any addresses -listed in **EPICS_PVA_ADDR_LIST** and also via TCP to any servers listed in **EPICS_PVA_NAME_SERVERS** -which can be reached. - -UDP searches are by default sent to port **5076**, subject to **EPICS_PVA_BROADCAST_PORT** and -port numbers explicitly given in **EPICS_PVA_ADDR_LIST**. - -The addresses in **EPICS_PVA_ADDR_LIST** may include IPv4/6 unicast, multicast, and/or broadcast addresses. -By default (cf. **EPICS_PVA_AUTO_ADDR_LIST**) the address list is automatically populated -with the IPv4 broadcast addresses of all local network interfaces. - -Searches will be repeated periodically in perpetuity until a positive response is received, -or the operation is cancelled. - -In order to reduce the number of broadcast packets, which every PVA host must process, -the time between searches will initially by short, but gradually increase -as time passes without a positive response. -This interval may be reduced when a new PVA server begins sending Beacon messages, -or when `pvxs::client::Context::hurryUp` is called. - -Server beacon destinations are by default configured using the client configuration. -This may be overridden with **EPICS_PVAS_BEACON_ADDR_LIST** and **EPICS_PVAS_AUTO_BEACON_ADDR_LIST**. +PVA network configuration is conventionally expressed through environment variables. +New API users are suggested to start with `pvxs::client::Context::fromEnv` +or `pvxs::server::Config::fromEnv`. .. _environ: @@ -111,3 +82,36 @@ Examples include: IPv6 multicast address, with Time To Live set to 1 (roughly equivalent to IPv4 broadcast). Send via the network interface named ``br0``. Use default port number. + +.. _netconfbg: + +PV Search Process +----------------- + +A PV Access network protocol operation proceeds in two phases: +PV name resolution, and data transfer. +Name resolution is the process is determining which PVA server claims to provide each PV name. +Once this is known, a TCP connection is open to that server, and the operation(s) are executed. + +The PVA Name resolution process is similar to Channel Access protocol. + +When a name needs to be resolved, a PVA client will begin sending UDP search messages to any addresses +listed in ``$EPICS_PVA_ADDR_LIST`` and also via TCP to any servers listed in ``$EPICS_PVA_NAME_SERVERS`` +which can be reached. + +UDP searches are by default sent to port **5076**, subject to ``$EPICS_PVA_BROADCAST_PORT`` and +port numbers explicitly given in ``$EPICS_PVA_ADDR_LIST``. + +The addresses in ``$EPICS_PVA_ADDR_LIST`` may include IPv4/6 unicast, multicast, and/or broadcast addresses. +By default (cf. ``$EPICS_PVA_AUTO_ADDR_LIST``) the address list is automatically populated +with the IPv4 broadcast addresses of all local network interfaces. + +Searches will be repeated periodically in perpetuity until a positive response is received, +or the operation is cancelled. + +In order to reduce the number of broadcast packets, which every PVA host must process, +the time between searches will initially be short, then gradually increase +as time passes without a positive response. + +Server beacon destinations are by default configured using the client configuration. +This may be overridden with ``$EPICS_PVAS_BEACON_ADDR_LIST`` and ``$EPICS_PVAS_AUTO_BEACON_ADDR_LIST``. diff --git a/documentation/server.rst b/documentation/server.rst index 29868ba1f..2f2117e2e 100644 --- a/documentation/server.rst +++ b/documentation/server.rst @@ -79,6 +79,7 @@ EPICS_PVAS_IGNORE_ADDR_LIST EPICS_PVA_CONN_TMO Inactivity timeout for TCP connections. For compatibility with pvAccessCPP a multiplier of 4/3 is applied. So a value of 30 results in a 40 second timeout. + This is an expert configuration, which will not normally be changed by end users. .. versionadded:: 0.3.0 All ***_ADDR_LIST** may contain IPv4 multicast, and IPv6 uni/multicast addresses. From b7c2d1b2c4ee172ed64e18ec5ca79f71fab8605d Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 26 Jan 2026 18:10:02 -0800 Subject: [PATCH 39/45] doc: shared_array --- documentation/value.rst | 21 ++++++---- src/pvxs/sharedArray.h | 90 +++++++++++++++++++++++++++++++++++------ 2 files changed, 91 insertions(+), 20 deletions(-) diff --git a/documentation/value.rst b/documentation/value.rst index ef09be2fb..fcd0bade1 100644 --- a/documentation/value.rst +++ b/documentation/value.rst @@ -120,9 +120,16 @@ Array fields ------------ Array fields are represented with the `pvxs::shared_array` container -using void vs. non-void, and const vs. non-const element types. +using void vs. non-void, and const vs. mutable element types. -Arrays are initially created as non-const and non-void. +.. code-block:: c++ + + shared_array typed_mutable; + shared_array typed_const; + shared_array void_mutable; + shared_array void_const; + +Arrays may be initially created as mutable and non-void. After being populated, an array must be transformed using `pvxs::shared_array::freeze` to become const before being stored in a `pvxs::Value`. @@ -133,8 +140,8 @@ being stored in a `pvxs::Value`. Value top = nt::NTScalar{TypeCode::Float64A}.create(); top["value"] = arr.freeze(); - # freeze() acts like std::move(). arr is now empty - # only the read-only reference remains! + // freeze() acts like std::move(). arr is now empty + // only the read-only reference remains! The `pvxs::shared_array::freeze` method is special in that it acts like std::move() in that it moves the array reference into the returned object. @@ -146,7 +153,7 @@ The const non-void option is a convenience which may **allocate** and do an elem .. code-block:: c++ - # extract reference, or converted copy + // extract reference, or converted copy arr = top["value"].as>(); When it is desirable to avoid an implicit allocate and convert, @@ -156,10 +163,10 @@ of the underlying array prior to using `pvxs::shared_array::castTo`. .. code-block:: c++ - # extract untyped reference. Never copies + // extract untyped reference. Never copies shared_array varr = top["value"].as>(); if(varr.original_type()==ArrayType::Float64) { - # castTo() throws std::logic_error if the underlying type is not 'double'. + // castTo() throws std::logic_error if the underlying type is not 'double'. shared_array temp = varr.castTo(); } diff --git a/src/pvxs/sharedArray.h b/src/pvxs/sharedArray.h index 0c85c07be..a7d6f3c50 100644 --- a/src/pvxs/sharedArray.h +++ b/src/pvxs/sharedArray.h @@ -209,6 +209,8 @@ shared_array copyAs(ArrayType dtype, ArrayType stype, const void *sbase, s } // namespace detail /** std::vector-like contiguous array of items passed by reference. + * + * Somewhat analogous to ``std::shared_ptr>``. * * shared_array comes in const and non-const, as well as void and non-void variants. * @@ -257,9 +259,18 @@ class shared_array : public detail::sa_base { typedef E element_type; + //! Pointer to no array. + //! @post size()==0 constexpr shared_array() noexcept :base_t() {} - //! allocate new array and populate from initializer list + /** Allocate new array and populate from initializer list. + * + * @code + * shared_array arr({1,2,3}); + * @endcode + * + * @post size()==L.size() + */ template shared_array(std::initializer_list L) :base_t(new _E_non_const[L.size()], L.size()) @@ -268,8 +279,16 @@ class shared_array : public detail::sa_base { std::copy(L.begin(), L.end(), raw); } - //! Construct a copy of another a sequence. - //! Requires random access iterators. + /** Construct a copy of a sequence or range of random access iterators. + * + * @code + * special_vectorish X(...); + * shared_array Y(X.begin(), X.end()); + * + * uint32_t cvals[] = {1,2,3,4}; + * shared_array Z(cvals, cvals + NELEMENTS(cvals)); + * @endcode + */ template::difference_type=0> shared_array(Iter begin, Iter end) :shared_array(std::distance(begin, end)) @@ -277,12 +296,12 @@ class shared_array : public detail::sa_base { std::copy(begin, end, const_cast<_E_non_const*>(this->begin())); } - //! @brief Allocate (with new[]) a new vector of size c + //! Allocate (with new[]) a new vector of c elements, which are default constructed. explicit shared_array(size_t c) :base_t(new _E_non_const[c], c) {} - //! @brief Allocate (with new[]) a new vector of size c and fill with value e + //! @brief Allocate (with new[]) a new vector of size c and fill with copies of e template shared_array(size_t c, V e) :base_t(new _E_non_const[c], c) @@ -290,23 +309,54 @@ class shared_array : public detail::sa_base { std::fill_n((_E_non_const*)this->_data.get(), this->_count, e); } - //! use existing alloc with delete[] + /** Wrap existing alloc with delete[] + * + * @param a Array allocated with ``new E[len]``. + * @param len Number of elements + */ shared_array(E* a, size_t len) :base_t(a, len) {} - //! use existing alloc w/ custom deletor + /** Wrap existing alloc w/ custom deletor + * + * @param a Array base pointer + * @param b Deletor object. ``b(a)`` must be valid. + * @param len Number of elements + */ template shared_array(E* a, B b, size_t len) :base_t(a, b, len) {} - //! build around existing shared_ptr + /** Wrap around existing shared_ptr + * + * Argument shared_ptr will likely be creating using that class's aliasing constructor. + */ shared_array(const std::shared_ptr& a, size_t len) :base_t(a, len) {} - //! alias existing shared_array + /** Alias existing shared_ptr + * + * A powerful tool for adapting external data containers to shared_array without copying. + * + * @param a Ownership object + * @param b Base pointer. Must remain valid until ownership object is destructed. + * @param len Number of elements + * + * @post data()==b + * + * @code + * auto stdvec(std::make_shared>({1,2,3})); + * + * shared_array aliasvec(stdvec, stdvec->data(), stdvec->size()); + * + * stdvec.reset(); // release original reference + * + * aliasvec.reset(); // ~vector runs! + * @endcode + */ template shared_array(const std::shared_ptr& a, E* b, size_t len) :base_t(a, b, len) @@ -398,10 +448,24 @@ class shared_array : public detail::sa_base { return (*this)[i]; } - //! Cast to const, consuming this - //! @pre unique()==true - //! @post empty()==true - //! @throws std::logic_error if !unique() + /** Cast single mutable reference to const, consuming this + * + * Transfers reference from this object to the returned object. + * + * @pre unique()==true + * @post empty()==true + * @throws std::logic_error if !unique() + * + * @code + * shared_array mutator({1,2,3}); + * mutator[0] = 42; + * + * shared_array frozen(mutator.freeze()); + * + * assert(mutator.empty()); // array reference transferred to 'frozen' + * assert(frozen.size()==3); + * @endcode + */ shared_array::type> freeze() { if(!this->unique()) From 98737e2c9477544ea3d5f2d053cdd6c6e224e0eb Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 27 Jan 2026 11:23:02 -0800 Subject: [PATCH 40/45] doc: unittest.h --- documentation/util.rst | 1 + ioc/pvxs/iochooks.h | 1 + src/pvxs/unittest.h | 84 ++++++++++++++++++++++++++++-------------- src/pvxs/util.h | 2 +- 4 files changed, 60 insertions(+), 28 deletions(-) diff --git a/documentation/util.rst b/documentation/util.rst index 5e7ff1edf..8dc9fdb65 100644 --- a/documentation/util.rst +++ b/documentation/util.rst @@ -152,6 +152,7 @@ When possible, use of the ``TestIOC`` class is recommended for both forward and backward compatibility with EPICS Base (>= 3.15.0.1). .. doxygenclass:: pvxs::ioc::TestIOC + :members: Utilities diff --git a/ioc/pvxs/iochooks.h b/ioc/pvxs/iochooks.h index f78f6808e..e497024e0 100644 --- a/ioc/pvxs/iochooks.h +++ b/ioc/pvxs/iochooks.h @@ -145,6 +145,7 @@ void testCleanupPrepare(); * } * epicsExitCallAtExits(); * pvxs::cleanup_for_valgrind(); + * return testDone(); * } @endcode * diff --git a/src/pvxs/unittest.h b/src/pvxs/unittest.h index 51b64d833..d085afa8f 100644 --- a/src/pvxs/unittest.h +++ b/src/pvxs/unittest.h @@ -44,7 +44,7 @@ void testSetup(); PVXS_API void cleanup_for_valgrind(); -/** A single test case (or diagnostic line). +/** A single test case and context messages. * * Acts as an output string to accumulate test comment. * Multi-line output results in one test line, and subsequent diagnostic lines. @@ -243,14 +243,14 @@ testCase testThrows(FN fn) /** Assert that an exception is throw with a certain message. * * @tparam Exception The exception type which should be thrown - * @param expr A regular expression + * @param expr A regular expression which the exception message must match * @param fn A callable * * @returns A testCase which passes if an Exception instance was caught * and std::exception::what() matched the provided regular expression. * * @code - * testThrowsMatch("happened", []() { + * testThrowsMatch(".*happened", []() { * testShow()<<"Now you see me"; * throw std::runtime_error("I happened"); * testShow()<<"Now you don't"; @@ -276,47 +276,77 @@ testCase testThrowsMatch(const std::string& expr, FN fn) } // namespace pvxs -//! Macro which assert that an expression evaluate to 'true'. -//! Evaluates to a pvxs::testCase +/** Macro which assert that an expression evaluate to 'true'. + * + * Evaluates to a pvxs::testCase, which may be used to add notes. + * + * @code + * testTrue(1+1==2)<<" Yes, really!"; + * @endcode + */ #define testTrue(EXPR) ::pvxs::testCase(EXPR)<<(" " #EXPR) -//! Macro which assert that an expression evaluate to 'true'. -//! Evaluates to a pvxs::testCase +//! Macro which assert that an expression evaluate to 'false'. +//! Equivalent to @code testTrue(!(EXPR)); @endcode #define testFalse(EXPR) ::pvxs::testCase(!(EXPR))<<(" !" #EXPR) -//! Macro which asserts equality between LHS and RHS. -//! Evaluates to a pvxs::testCase -//! Roughly equivalent to @code testOk((LHS)==(RHS), "..."); @endcode +/** Macro which asserts equality between LHS and RHS. + * + * Evaluates to a pvxs::testCase, which may be used to add notes. + * Also, both left and right hand side values will be printed. + * + * @code + * testEq(1+1, 2)<<" Yes, really!"; + * @endcode + */ #define testEq(LHS, RHS) ::pvxs::detail::testEq(#LHS, LHS, #RHS, RHS) //! Macro which asserts in-equality between LHS and RHS. -//! Evaluates to a pvxs::testCase -//! Roughly equivalent to @code testOk((LHS)!=(RHS), "..."); @endcode +//! Inverse of testEq() #define testNotEq(LHS, RHS) ::pvxs::detail::testNotEq(#LHS, LHS, #RHS, RHS) -//! Macro which asserts equality between LHS and RHS. -//! Evaluates to a pvxs::testCase -//! Functionally equivalent to testEq() with two std::string instances. -//! Prints diff-like output which is friendlier to multi-line strings. +/** Macro which asserts equality between LHS and RHS. + * + * Evaluates to a pvxs::testCase, which may be used to add notes. + * Functionally equivalent to testEq() with two std::string instances. + * Prints diff-like output which is friendlier for multi-line strings. + */ #define testStrEq(LHS, RHS) ::pvxs::detail::_testStrTest(1, #LHS, ::pvxs::detail::asStr(LHS), #RHS, ::pvxs::detail::asStr(RHS)) //! Macro which asserts inequality between LHS and RHS. -//! Evaluates to a pvxs::testCase -//! Functionally equivalent to testNotEq() with two std::string instances. -//! Prints diff-like output which is friendlier to multi-line strings. +//! Inverse of testStrEq() //! @since 0.2.0 #define testStrNotEq(LHS, RHS) ::pvxs::detail::_testStrTest(0, #LHS, ::pvxs::detail::asStr(LHS), #RHS, ::pvxs::detail::asStr(RHS)) -//! Macro which asserts that STR matches the regular expression EXPR -//! Evaluates to a pvxs::testCase -//! @since 0.2.1 Expression syntax is POSIX extended. -//! @since 0.1.1 +/** Macro which asserts that STR matches completely the regular expression EXPR + * + * Evaluates to a pvxs::testCase, which may be used to add notes. + * + * @param EXPR regex string + * @param STR Input to be matched + * + * @code + * testStrMatch("h.*world", "hello world"); // ok + * testStrMatch("h.*world", "hello world!"); // not ok + * @endcode + * + * @since 0.2.1 Expression syntax is POSIX extended. + * @since 0.1.1 Added + */ #define testStrMatch(EXPR, STR) ::pvxs::detail::_testStrMatch(#EXPR, EXPR, #STR, STR) -//! Macro which asserts equality between LHS and RHS. -//! Evaluates to a pvxs::testCase -//! Functionally equivalent to testEq() for objects with .size() and operator[]. -//! Prints element by element differences +/** Macro which asserts equality between LHS and RHS. + * + * Evaluates to a pvxs::testCase, which may be used to add notes. + * Functionally equivalent to testEq() for objects with .size() and operator[]. + * Prints element by element differences + * + * @code + * std::vector left({...}); + * pvxs::shared_array right(...); + * testArrEq(left, right)<<" context"; + * @endcode + */ #define testArrEq(LHS, RHS) ::pvxs::detail::testArrEq(#LHS, LHS, #RHS, RHS) //! Macro which prints diagnostic (non-test) lines. diff --git a/src/pvxs/util.h b/src/pvxs/util.h index 34ce7f59e..82bfffe23 100644 --- a/src/pvxs/util.h +++ b/src/pvxs/util.h @@ -169,7 +169,7 @@ struct PVXS_API Detailed { /** Describe build and runtime configuration of current system. * - * Print information which may be using for when troubleshooting, + * Print information which may be useful when troubleshooting, * or creating a bug report. * * Printed by CLI "pvxinfo -D" and iocsh "pvxs_target_information". From a6f75c10158d59fc016f0a75cd9d365c76d7de12 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Tue, 27 Jan 2026 14:07:39 -0800 Subject: [PATCH 41/45] doc: client operation exceptions --- documentation/client.rst | 4 ++++ src/pvxs/client.h | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/documentation/client.rst b/documentation/client.rst index 087bac8c7..3a563cf31 100644 --- a/documentation/client.rst +++ b/documentation/client.rst @@ -308,3 +308,7 @@ Misc .. doxygenstruct:: pvxs::client::RemoteError :members: + +.. doxygenstruct:: pvxs::client::Timeout + +.. doxygenstruct:: pvxs::client::Interrupted diff --git a/src/pvxs/client.h b/src/pvxs/client.h index 75fdc0bdc..91cde741a 100644 --- a/src/pvxs/client.h +++ b/src/pvxs/client.h @@ -64,12 +64,14 @@ struct PVXS_API Connected : public std::runtime_error const epicsTime time; }; +//! Operation::interrupt() called struct PVXS_API Interrupted : public std::runtime_error { Interrupted(); virtual ~Interrupted(); }; +//! Operation::wait() exceeded timeout struct PVXS_API Timeout : public std::runtime_error { Timeout(); @@ -134,8 +136,8 @@ struct PVXS_API Operation { * * @param timeout Time to wait prior to throwing TimeoutError. cf. epicsEvent::wait(double) * @return result Value. Always empty/invalid for put() - * @throws Timeout Timeout exceeded - * @throws Interrupted interrupt() called + * @throws pvxs::client::Timeout Timeout exceeded + * @throws pvxs::client::Interrupted interrupt() called */ virtual Value wait(double timeout) =0; From 19a3441c884b89b735d8e20f6f7275d8d95d4a3d Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 26 Jan 2026 19:09:25 -0800 Subject: [PATCH 42/45] doc: toc reorg --- documentation/index.rst | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/documentation/index.rst b/documentation/index.rst index de092f699..55382ef1d 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -41,19 +41,32 @@ Contents -------- .. toctree:: - :maxdepth: 3 + :maxdepth: 1 overview - netconfig example building - cli + +.. toctree:: + :maxdepth: 2 + + netconfig ioc + cli + +.. toctree:: + :caption: API References + :maxdepth: 2 + value client server util details + +.. toctree:: + :maxdepth: 1 + releasenotes Indices and tables From e4293c795a8aa5cf636a6ef5c6d1105a3a52fbf2 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 9 Feb 2026 18:01:40 -0800 Subject: [PATCH 43/45] pkg_resources removal --- python/pvxslibs/version.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/python/pvxslibs/version.py b/python/pvxslibs/version.py index c6002cdbe..931f2a9b1 100644 --- a/python/pvxslibs/version.py +++ b/python/pvxslibs/version.py @@ -5,7 +5,6 @@ """ import re from collections import namedtuple -from pkg_resources import get_distribution, parse_version __all__ = ( 'version', @@ -13,7 +12,16 @@ 'abi_requires', ) -version = get_distribution('pvxslibs').version # as a string +def version(): + try: + from importlib.metadata import version # >= py 3.8 + except ImportError: # removed from setuptools v82 + from pkg_resources import get_distribution + return get_distribution('pvxslibs').version + else: + return version('pvxslibs') + +version = version() # as a string version_info = re.match(r'([\d]+)\.([\d]+)\.([\d]+)([ab]\d+)?', version).groups() From 49b73c1069bfd39a65624ee7a22e4175f05926be Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Mon, 9 Feb 2026 18:05:32 -0800 Subject: [PATCH 44/45] 1.5.1a1 --- configure/CONFIG_PVXS_VERSION | 5 +++-- documentation/releasenotes.rst | 7 +++++++ setup.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/configure/CONFIG_PVXS_VERSION b/configure/CONFIG_PVXS_VERSION index 991f4f393..5b655709c 100644 --- a/configure/CONFIG_PVXS_VERSION +++ b/configure/CONFIG_PVXS_VERSION @@ -1,6 +1,6 @@ PVXS_MAJOR_VERSION = 1 PVXS_MINOR_VERSION = 5 -PVXS_MAINTENANCE_VERSION = 0 +PVXS_MAINTENANCE_VERSION = 1 # Version range conditions in Makefiles # @@ -12,7 +12,8 @@ PVXS_MAINTENANCE_VERSION = 0 # # ifneq ($(PVXS_X_Y_Z),YES) # PVXS != X.Y.Z # -PVXS_1_5_0 = YES +PVXS_1_5_1 = YES +PVXS_1_5_0 = NO PVXS_1_4_1 = NO PVXS_1_4_0 = NO PVXS_1_3_3 = NO diff --git a/documentation/releasenotes.rst b/documentation/releasenotes.rst index 40647539b..323f2ab6c 100644 --- a/documentation/releasenotes.rst +++ b/documentation/releasenotes.rst @@ -3,6 +3,13 @@ Release Notes ============= +1.5.1 (UNRELEASED) +------------------ + +* Call ``epicsSignalInstallSigPipeIgnore()``. +* When available, set ``SO_NOSIGPIPE`` on TCP sockets. +* python: Handle setuptools v80 pkg_resources removal. + 1.5.0 (Jan 2026) ---------------- diff --git a/setup.py b/setup.py index 26699b4a3..b8eec6049 100755 --- a/setup.py +++ b/setup.py @@ -696,7 +696,7 @@ def define_DSOS(self): pvxs_ver = '%(PVXS_MAJOR_VERSION)s.%(PVXS_MINOR_VERSION)s.%(PVXS_MAINTENANCE_VERSION)s'%pvxsversion -#pvxs_ver += 'a1' +pvxs_ver += 'a1' with open(os.path.join(os.path.dirname(__file__), 'README.md')) as F: long_description = F.read() From d0ee5b2ab61d705c8c64c52c023f70ce5c509860 Mon Sep 17 00:00:00 2001 From: Michael Davidsaver Date: Wed, 11 Feb 2026 07:20:03 -0800 Subject: [PATCH 45/45] 1.5.1 --- documentation/releasenotes.rst | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/releasenotes.rst b/documentation/releasenotes.rst index 323f2ab6c..0a5b18bf5 100644 --- a/documentation/releasenotes.rst +++ b/documentation/releasenotes.rst @@ -3,8 +3,8 @@ Release Notes ============= -1.5.1 (UNRELEASED) ------------------- +1.5.1 (Feb 2026) +---------------- * Call ``epicsSignalInstallSigPipeIgnore()``. * When available, set ``SO_NOSIGPIPE`` on TCP sockets. diff --git a/setup.py b/setup.py index b8eec6049..26699b4a3 100755 --- a/setup.py +++ b/setup.py @@ -696,7 +696,7 @@ def define_DSOS(self): pvxs_ver = '%(PVXS_MAJOR_VERSION)s.%(PVXS_MINOR_VERSION)s.%(PVXS_MAINTENANCE_VERSION)s'%pvxsversion -pvxs_ver += 'a1' +#pvxs_ver += 'a1' with open(os.path.join(os.path.dirname(__file__), 'README.md')) as F: long_description = F.read()