From 9eb29d037d5ec700bcb2616b8b3bfea16b6b64dd Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 27 Mar 2025 10:42:50 +0000 Subject: [PATCH 1/2] Add ability to disable PVA server in IOC There seems to be no need to load the DBD and DSO early, so doing it just before iocInit allows a programmatic switch to enable/disable it. --- CHANGELOG.rst | 1 + Pipfile.lock | 6 +-- pyproject.toml | 2 +- softioc/__init__.py | 3 -- softioc/softioc.py | 20 +++++++-- tests/test_pvaccess.py | 94 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 tests/test_pvaccess.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04856ad1..a60a78e9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,7 @@ Added: - `Add int64In/Out record support <../../pull/161>`_ - `Enable setting alarm status of Out records <../../pull/157>`_ - `Adding the non_interactive_ioc function <../../pull/156>`_ +- `Allow starting IOC without PVA <../../pull/186>`_ Removed: diff --git a/Pipfile.lock b/Pipfile.lock index 28c9cbf2..68759854 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3de736bf110af1d77f0f31569339aaa126619efb770d1269e4b0e90fce4bb32d" + "sha256": "a1e9d3213ef5b3b13ab3dc1f032d14f2d32eb2293e6dda1f773356cc9da8ce70" }, "pipfile-spec": 6, "requires": {}, @@ -105,7 +105,7 @@ "sha256:fbfcae4243cb2ee24a0c3dda801903794cd805befe656626ff24410d9422539d", "sha256:fd4b41b40f041579af05885dd02cda2829999c0a5fd8dc6471afb7114aded120" ], - "version": "==7.0.7.99.1.1a3" + "version": "==7.0.7.99.1.2a1" }, "epicsdbbuilder": { "hashes": [ @@ -474,7 +474,7 @@ "sha256:fbfcae4243cb2ee24a0c3dda801903794cd805befe656626ff24410d9422539d", "sha256:fd4b41b40f041579af05885dd02cda2829999c0a5fd8dc6471afb7114aded120" ], - "version": "==7.0.7.99.1.1a3" + "version": "==7.0.7.99.1.2a1" }, "epicsdbbuilder": { "hashes": [ diff --git a/pyproject.toml b/pyproject.toml index f83a3dc9..30f2df9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs>=7.0.7.99.1.1a3"] +requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs>=7.0.7.99.1.2a1"] build-backend = "setuptools.build_meta:__legacy__" diff --git a/softioc/__init__.py b/softioc/__init__.py index 5eabe59f..689d13e7 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -22,12 +22,9 @@ iocshRegisterCommon() base_dbd_path = os.path.join(epicscorelibs.path.base_path, 'dbd') dbLoadDatabase('base.dbd', base_dbd_path, None) -dbLoadDatabase('pvxsIoc.dbd', pvxslibs.path.dbd_path, None) iocStats = os.path.join(os.path.dirname(__file__), "iocStats", "devIocStats") dbLoadDatabase('devIocStats.dbd', iocStats, None) -ctypes.CDLL(find_dso('pvxslibs.lib.pvxsIoc'), ctypes.RTLD_GLOBAL) -os.environ.setdefault('PVXS_QSRV_ENABLE', 'YES') if registerRecordDeviceDriver(pdbbase): raise RuntimeError('Error registering') diff --git a/softioc/softioc.py b/softioc/softioc.py index 7867d50f..4f7bf30d 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -1,9 +1,14 @@ +import ctypes import os import sys import atexit from ctypes import * from tempfile import NamedTemporaryFile +import pvxslibs.path +from epicscorelibs.ioc import registerRecordDeviceDriver, pdbbase +from setuptools_dso.runtime import find_dso + from . import autosave, imports, device from . import cothread_dispatcher @@ -16,14 +21,15 @@ def epicsAtPyExit(): imports.epicsExitCallAtExits() -def iocInit(dispatcher=None): +def iocInit(dispatcher=None, enable_pva=True): '''This must be called exactly once after loading all EPICS database files. After this point the EPICS IOC is running and serving PVs. Args: dispatcher: A callable with signature ``dispatcher(func, *args)``. Will - be called in response to caput on a record. If not supplied use - `cothread` as a dispatcher. + be called in response to caput on a record. If not supplied uses + ``cothread`` as the dispatcher. + enable_pva: Specify whether to enable the PV Access Server in this IOC. See Also: `softioc.asyncio_dispatcher` is a dispatcher for `asyncio` applications @@ -33,6 +39,14 @@ def iocInit(dispatcher=None): dispatcher = cothread_dispatcher.CothreadDispatcher() # Set the dispatcher for record processing callbacks device.dispatcher = dispatcher + + if enable_pva: + dbLoadDatabase('pvxsIoc.dbd', pvxslibs.path.dbd_path, None) + ctypes.CDLL(find_dso('pvxslibs.lib.pvxsIoc'), ctypes.RTLD_GLOBAL) + + if registerRecordDeviceDriver(pdbbase): + raise RuntimeError('Error registering') + imports.iocInit() autosave.start_autosave_thread() diff --git a/tests/test_pvaccess.py b/tests/test_pvaccess.py new file mode 100644 index 00000000..d5e492b2 --- /dev/null +++ b/tests/test_pvaccess.py @@ -0,0 +1,94 @@ +import asyncio +from contextlib import nullcontext + +import pytest + +from conftest import ( + aioca_cleanup, + log, + create_random_prefix, + TIMEOUT, + select_and_recv, + get_multiprocessing_context, +) + +from softioc import builder, softioc + + +class TestPVAccess: + """Tests related to PVAccess""" + + record_name = "PVA_AOut" + record_value = 10 + + def pva_test_func(self, device_name, conn, use_pva): + builder.SetDeviceName(device_name) + + builder.aOut(self.record_name, initial_value=self.record_value) + + builder.LoadDatabase() + softioc.iocInit(enable_pva=use_pva) + + conn.send("R") # "Ready" + log("CHILD: Sent R over Connection to Parent") + + # Keep process alive while main thread works. + while (True): + if conn.poll(TIMEOUT): + val = conn.recv() + if val == "D": # "Done" + break + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "use_pva,expectation", + [ + (True, nullcontext()), + (False, pytest.raises(asyncio.TimeoutError)) + ] + ) + async def test_pva_enable_disable(self, use_pva, expectation): + """Test that we can enable and disable PVA, perform PVAccess requests + when enabled, and that we can always do Channel Access requests""" + ctx = get_multiprocessing_context() + parent_conn, child_conn = ctx.Pipe() + + device_name = create_random_prefix() + + process = ctx.Process( + target=self.pva_test_func, + args=(device_name, child_conn, use_pva), + ) + + process.start() + + from aioca import caget + from p4p.client.asyncio import Context + try: + # Wait for message that IOC has started + select_and_recv(parent_conn, "R") + + record_full_name = device_name + ":" + self.record_name + + ret_val = await caget(record_full_name, timeout=TIMEOUT) + + assert ret_val == self.record_value + + with expectation as _: + with Context("pva") as ctx: + # Short timeout as, if the above CA connection has happened + # there's no need to wait a very long time for the PVA + # connection + pva_val = await asyncio.wait_for( + ctx.get(record_full_name), + timeout=2 + ) + assert pva_val == self.record_value + + + finally: + # Clear the cache before stopping the IOC stops + # "channel disconnected" error messages + aioca_cleanup() + parent_conn.send("D") # "Done" + process.join(timeout=TIMEOUT) From 026ed69c20d9a92c729e022fbfbc999aef546d08 Mon Sep 17 00:00:00 2001 From: AlexWells Date: Thu, 27 Mar 2025 12:09:28 +0000 Subject: [PATCH 2/2] Do code review and CI fixes We don't need the newest version of epicscorelibs. Fix attempting to use cothread on Windows. Import registerAllRecordDeviceDrivers ourselves so we can do error checking the same way as other functions, without relying on epicscorelibs or needing additional error handling --- Pipfile.lock | 6 +++--- pyproject.toml | 2 +- softioc/__init__.py | 11 +++-------- softioc/imports.py | 3 +++ softioc/softioc.py | 5 ++--- tests/test_pvaccess.py | 5 +++-- 6 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 68759854..28c9cbf2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a1e9d3213ef5b3b13ab3dc1f032d14f2d32eb2293e6dda1f773356cc9da8ce70" + "sha256": "3de736bf110af1d77f0f31569339aaa126619efb770d1269e4b0e90fce4bb32d" }, "pipfile-spec": 6, "requires": {}, @@ -105,7 +105,7 @@ "sha256:fbfcae4243cb2ee24a0c3dda801903794cd805befe656626ff24410d9422539d", "sha256:fd4b41b40f041579af05885dd02cda2829999c0a5fd8dc6471afb7114aded120" ], - "version": "==7.0.7.99.1.2a1" + "version": "==7.0.7.99.1.1a3" }, "epicsdbbuilder": { "hashes": [ @@ -474,7 +474,7 @@ "sha256:fbfcae4243cb2ee24a0c3dda801903794cd805befe656626ff24410d9422539d", "sha256:fd4b41b40f041579af05885dd02cda2829999c0a5fd8dc6471afb7114aded120" ], - "version": "==7.0.7.99.1.2a1" + "version": "==7.0.7.99.1.1a3" }, "epicsdbbuilder": { "hashes": [ diff --git a/pyproject.toml b/pyproject.toml index 30f2df9f..f83a3dc9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs>=7.0.7.99.1.2a1"] +requires = ["setuptools", "wheel", "setuptools_dso>=2.1", "epicscorelibs>=7.0.7.99.1.1a3"] build-backend = "setuptools.build_meta:__legacy__" diff --git a/softioc/__init__.py b/softioc/__init__.py index 689d13e7..11ec06a2 100644 --- a/softioc/__init__.py +++ b/softioc/__init__.py @@ -1,12 +1,8 @@ '''Python soft IOC module.''' import os -import ctypes -from setuptools_dso.runtime import find_dso import epicscorelibs.path -import pvxslibs.path -from epicscorelibs.ioc import \ - iocshRegisterCommon, registerRecordDeviceDriver, pdbbase +from epicscorelibs.ioc import iocshRegisterCommon, pdbbase # Do this as early as possible, in case we happen to use cothread # This will set the CATOOLS_LIBCA_PATH environment variable in case we use @@ -15,7 +11,7 @@ # This import will also pull in the extension, which is needed # before we call iocshRegisterCommon -from .imports import dbLoadDatabase +from .imports import dbLoadDatabase, registerRecordDeviceDriver from ._version_git import __version__ # Need to do this before calling anything in device.py @@ -25,7 +21,6 @@ iocStats = os.path.join(os.path.dirname(__file__), "iocStats", "devIocStats") dbLoadDatabase('devIocStats.dbd', iocStats, None) -if registerRecordDeviceDriver(pdbbase): - raise RuntimeError('Error registering') +registerRecordDeviceDriver(pdbbase) __all__ = ['__version__'] diff --git a/softioc/imports.py b/softioc/imports.py index 60f27257..24754349 100644 --- a/softioc/imports.py +++ b/softioc/imports.py @@ -105,6 +105,9 @@ def from_param(cls, value): epicsExitCallAtExits.argtypes = () epicsExitCallAtExits.restype = None +registerRecordDeviceDriver = dbCore.registerAllRecordDeviceDrivers +registerRecordDeviceDriver.argtypes = (c_void_p,) +registerRecordDeviceDriver.errcheck = expect_success __all__ = [ 'get_field_offsets', diff --git a/softioc/softioc.py b/softioc/softioc.py index 4f7bf30d..8a439a2f 100644 --- a/softioc/softioc.py +++ b/softioc/softioc.py @@ -6,7 +6,7 @@ from tempfile import NamedTemporaryFile import pvxslibs.path -from epicscorelibs.ioc import registerRecordDeviceDriver, pdbbase +from epicscorelibs.ioc import pdbbase from setuptools_dso.runtime import find_dso from . import autosave, imports, device @@ -44,8 +44,7 @@ def iocInit(dispatcher=None, enable_pva=True): dbLoadDatabase('pvxsIoc.dbd', pvxslibs.path.dbd_path, None) ctypes.CDLL(find_dso('pvxslibs.lib.pvxsIoc'), ctypes.RTLD_GLOBAL) - if registerRecordDeviceDriver(pdbbase): - raise RuntimeError('Error registering') + imports.registerRecordDeviceDriver(pdbbase) imports.iocInit() autosave.start_autosave_thread() diff --git a/tests/test_pvaccess.py b/tests/test_pvaccess.py index d5e492b2..e90e49e8 100644 --- a/tests/test_pvaccess.py +++ b/tests/test_pvaccess.py @@ -12,7 +12,7 @@ get_multiprocessing_context, ) -from softioc import builder, softioc +from softioc import asyncio_dispatcher, builder, softioc class TestPVAccess: @@ -26,8 +26,9 @@ def pva_test_func(self, device_name, conn, use_pva): builder.aOut(self.record_name, initial_value=self.record_value) + dispatcher = asyncio_dispatcher.AsyncioDispatcher() builder.LoadDatabase() - softioc.iocInit(enable_pva=use_pva) + softioc.iocInit(dispatcher=dispatcher, enable_pva=use_pva) conn.send("R") # "Ready" log("CHILD: Sent R over Connection to Parent")