Skip to content

Commit 8b7ad74

Browse files
Use our own implementation of dbPutField
This works around issues with potential infinite loops, even if process=False, as seen in issue #201.
1 parent 09ea964 commit 8b7ad74

File tree

4 files changed

+124
-13
lines changed

4 files changed

+124
-13
lines changed

softioc/device.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
dbLoadDatabase,
1212
signal_processing_complete,
1313
recGblResetAlarms,
14-
db_put_field,
14+
db_put_field_process,
1515
db_get_field,
1616
)
1717
from .device_core import DeviceSupportCore, RecordLookup
@@ -108,7 +108,7 @@ def set_field(self, field, value):
108108
data = (c_char * 40)()
109109
data.value = str(value).encode() + b'\0'
110110
name = self._name + '.' + field
111-
db_put_field(name, fields.DBF_STRING, addressof(data), 1)
111+
db_put_field_process(name, fields.DBF_STRING, addressof(data), 1, True)
112112

113113
class ProcessDeviceSupportIn(ProcessDeviceSupportCore):
114114
_link_ = 'INP'
@@ -288,7 +288,7 @@ def set(self, value, process=True,
288288
# The array parameter is used to keep the raw pointer alive
289289
dbf_code, length, data, array = self._value_to_dbr(value)
290290
self.__enable_write = process
291-
db_put_field(_record.NAME, dbf_code, data, length)
291+
db_put_field_process(_record.NAME, dbf_code, data, length, process)
292292
self.__enable_write = True
293293

294294
def get(self):

softioc/extension.c

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,17 @@ static PyObject *get_field_offsets(PyObject *self, PyObject *args)
9393
}
9494

9595

96-
/* Updates PV field with integrated db lookup. Safer to do this in C as we need
97-
* an intermediate copy of the dbAddr structure, which changes size between
98-
* EPICS releases. */
99-
static PyObject *db_put_field(PyObject *self, PyObject *args)
96+
/* This is our own re-implementation of EPICS's dbPutField function.
97+
We do this to allow us to control when dbProcess is called. We use the
98+
same logicical flow as the original function. */
99+
static PyObject *db_put_field_process(PyObject *self, PyObject *args)
100100
{
101101
const char *name;
102102
short dbrType;
103103
PyObject *buffer_ptr;
104104
long length;
105-
if (!PyArg_ParseTuple(args, "shOl", &name, &dbrType, &buffer_ptr, &length))
105+
short process;
106+
if (!PyArg_ParseTuple(args, "shOlh", &name, &dbrType, &buffer_ptr, &length, &process))
106107
return NULL;
107108
void *pbuffer = PyLong_AsVoidPtr(buffer_ptr);
108109
if (!pbuffer)
@@ -113,6 +114,8 @@ static PyObject *db_put_field(PyObject *self, PyObject *args)
113114
return PyErr_Format(
114115
PyExc_RuntimeError, "dbNameToAddr failed for %s", name);
115116

117+
struct dbCommon *precord = dbAddr.precord;
118+
116119
long put_result;
117120
/* There are two important locks to consider at this point: The Global
118121
* Interpreter Lock (GIL) and the EPICS record lock. A deadlock is possible
@@ -125,7 +128,22 @@ static PyObject *db_put_field(PyObject *self, PyObject *args)
125128
* EPICS call, to avoid potential deadlocks.
126129
* See https://github.com/DiamondLightSource/pythonSoftIOC/issues/119. */
127130
Py_BEGIN_ALLOW_THREADS
128-
put_result = dbPutField(&dbAddr, dbrType, pbuffer, length);
131+
dbScanLock(precord);
132+
put_result = dbPut(&dbAddr, dbrType, pbuffer, length);
133+
134+
if (put_result == 0 && process)
135+
{
136+
if (precord->pact)
137+
{
138+
precord->rpro = TRUE;
139+
}
140+
else
141+
{
142+
dbProcess(precord);
143+
}
144+
}
145+
146+
dbScanUnlock(precord);
129147
Py_END_ALLOW_THREADS
130148
if (put_result)
131149
return PyErr_Format(
@@ -314,7 +332,7 @@ static struct PyMethodDef softioc_methods[] = {
314332
"Get a map of DBF names to values"},
315333
{"get_field_offsets", get_field_offsets, METH_VARARGS,
316334
"Get offset, size and type for each record field"},
317-
{"db_put_field", db_put_field, METH_VARARGS,
335+
{"db_put_field_process", db_put_field_process, METH_VARARGS,
318336
"Put a database field to a value"},
319337
{"db_get_field", db_get_field, METH_VARARGS,
320338
"Get a database field's value"},

softioc/imports.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ def get_field_offsets(record_type):
1919
'''Return {field_name: (offset, size, field_type)}'''
2020
return _extension.get_field_offsets(record_type)
2121

22-
def db_put_field(name, dbr_type, pbuffer, length):
23-
'''Put field where pbuffer is void* pointer. Returns None.'''
24-
return _extension.db_put_field(name, dbr_type, pbuffer, length)
22+
def db_put_field_process(name, dbr_type, pbuffer, length, process):
23+
'''Put field where pbuffer is void* pointer, conditionally processing
24+
the record. Returns None.'''
25+
return _extension.db_put_field_process(
26+
name, dbr_type, pbuffer, length, process
27+
)
2528

2629
def db_get_field(name, dbr_type, pbuffer, length):
2730
'''Get field where pbuffer is void* pointer. Returns None.'''

tests/test_records.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncio
22
import numpy
33
import os
4+
import re
45
import pytest
56
from enum import Enum
67

@@ -682,6 +683,95 @@ def test_on_update_true_false(self, out_records):
682683
always_update is True and the put'ed value is always different"""
683684
self.on_update_runner(out_records, True, False)
684685

686+
def on_update_recursive_set_test_func(
687+
self, device_name, conn
688+
):
689+
log("CHILD: Child started")
690+
691+
builder.SetDeviceName(device_name)
692+
693+
async def on_update_func(new_val):
694+
log("CHILD: on_update_func started")
695+
record.set(0, process=False)
696+
conn.send("C") # "Callback"
697+
log("CHILD: on_update_func ended")
698+
699+
record = builder.Action(
700+
"ACTION",
701+
on_update=on_update_func,
702+
blocking=True,
703+
initial_value=1 # A non-zero value, to check it changes
704+
)
705+
706+
dispatcher = asyncio_dispatcher.AsyncioDispatcher()
707+
builder.LoadDatabase()
708+
softioc.iocInit(dispatcher)
709+
710+
conn.send("R") # "Ready"
711+
712+
log("CHILD: Sent R over Connection to Parent")
713+
714+
# Keep process alive while main thread runs CAGET
715+
if conn.poll(TIMEOUT):
716+
val = conn.recv()
717+
assert val == "D", "Did not receive expected Done character"
718+
719+
log("CHILD: Received exit command, child exiting")
720+
721+
async def test_on_update_recursive_set(self):
722+
"""Test that on_update functions correctly when the on_update
723+
callback sets the value of the record again (with process=False).
724+
See issue #201"""
725+
726+
ctx = get_multiprocessing_context()
727+
parent_conn, child_conn = ctx.Pipe()
728+
729+
device_name = create_random_prefix()
730+
731+
process = ctx.Process(
732+
target=self.on_update_recursive_set_test_func,
733+
args=(device_name, child_conn),
734+
)
735+
736+
process.start()
737+
738+
log("PARENT: Child started, waiting for R command")
739+
740+
from aioca import caget, caput
741+
742+
try:
743+
# Wait for message that IOC has started
744+
select_and_recv(parent_conn, "R")
745+
746+
log("PARENT: received R command")
747+
748+
record = f"{device_name}:ACTION"
749+
750+
val = await caget(record)
751+
752+
assert val == 1, "ACTION record did not start with value 1"
753+
754+
await caput(record, 1, wait=True)
755+
756+
val = await caget(record)
757+
758+
assert val == 0, "ACTION record did not return to zero value"
759+
760+
# Expect one "C"
761+
select_and_recv(parent_conn, "C")
762+
763+
# ...But if we receive another we know there's a problem
764+
if parent_conn.poll(5): # Shorter timeout to make this quicker
765+
pytest.fail("Received unexpected second message")
766+
767+
finally:
768+
log("PARENT:Sending Done command to child")
769+
parent_conn.send("D") # "Done"
770+
process.join(timeout=TIMEOUT)
771+
log(f"PARENT: Join completed with exitcode {process.exitcode}")
772+
if process.exitcode is None:
773+
pytest.fail("Process did not terminate")
774+
685775

686776

687777
class TestBlocking:

0 commit comments

Comments
 (0)