Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
6 changes: 3 additions & 3 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"]
Comment thread
AlexanderWells-diamond marked this conversation as resolved.
Outdated
build-backend = "setuptools.build_meta:__legacy__"
3 changes: 0 additions & 3 deletions softioc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
20 changes: 17 additions & 3 deletions softioc/softioc.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -16,14 +21,15 @@
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
Expand All @@ -33,6 +39,14 @@
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason we shouldn't defer all dbd and lib loading until iocInit?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this PR there is no longer any lib loading anywhere except in iocInit().

You must load the DBD files before creating records, otherwise epicsdbbuilder / EPICS itself doesn't know what to do with the record entries. So I believe we must do that as early as possible.

For research I tried moving the DBD loading into this function, and it results in a segmentation fault as soon as you try and create a record (presumably EPICS is attempting to call some kind of validation function related to the DBD and doesn't have proper null checks somewhere)

ctypes.CDLL(find_dso('pvxslibs.lib.pvxsIoc'), ctypes.RTLD_GLOBAL)

if registerRecordDeviceDriver(pdbbase):
raise RuntimeError('Error registering')

Check warning on line 48 in softioc/softioc.py

View check run for this annotation

Codecov / codecov/patch

softioc/softioc.py#L48

Added line #L48 was not covered by tests
Comment thread
AlexanderWells-diamond marked this conversation as resolved.
Outdated

imports.iocInit()
autosave.start_autosave_thread()

Expand Down
94 changes: 94 additions & 0 deletions tests/test_pvaccess.py
Original file line number Diff line number Diff line change
@@ -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)
Loading