Latest Threat Research:SANDWORM_MODE: Shai-Hulud-Style npm Worm Hijacks CI Workflows and Poisons AI Toolchains.Details
Socket
Book a DemoSign in
Socket

softioc

Package Overview
Dependencies
Maintainers
0
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

softioc - pypi Package Compare versions

Comparing version
4.4.0
to
4.5.0
+18
-2
PKG-INFO
Metadata-Version: 2.1
Name: softioc
Version: 4.4.0
Version: 4.5.0
Summary: Embed an EPICS IOC in a Python process

@@ -18,5 +18,21 @@ Home-page: https://github.com/dls-controls/pythonSoftIOC

Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: epicscorelibs<7.0.7.99.1,>=7.0.7.99.0.2
Requires-Dist: pvxslibs>=1.2.2
Requires-Dist: numpy
Requires-Dist: epicsdbbuilder>=1.4
Provides-Extra: useful
Requires-Dist: cothread; extra == "useful"
Requires-Dist: scipy; extra == "useful"
Requires-Dist: aioca>=1.6; extra == "useful"
Provides-Extra: dev
License-File: LICENSE
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pytest-flake8; extra == "dev"
Requires-Dist: flake8<5.0.0; extra == "dev"
Requires-Dist: sphinx==4.3.2; extra == "dev"
Requires-Dist: sphinx-rtd-theme-github-versions; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: aioca>=1.6; extra == "dev"
Requires-Dist: cothread; sys_platform != "win32" and extra == "dev"
Requires-Dist: p4p; extra == "dev"

@@ -23,0 +39,0 @@ pythonSoftIOC

Metadata-Version: 2.1
Name: softioc
Version: 4.4.0
Version: 4.5.0
Summary: Embed an EPICS IOC in a Python process

@@ -18,5 +18,21 @@ Home-page: https://github.com/dls-controls/pythonSoftIOC

Description-Content-Type: text/x-rst
License-File: LICENSE
Requires-Dist: epicscorelibs<7.0.7.99.1,>=7.0.7.99.0.2
Requires-Dist: pvxslibs>=1.2.2
Requires-Dist: numpy
Requires-Dist: epicsdbbuilder>=1.4
Provides-Extra: useful
Requires-Dist: cothread; extra == "useful"
Requires-Dist: scipy; extra == "useful"
Requires-Dist: aioca>=1.6; extra == "useful"
Provides-Extra: dev
License-File: LICENSE
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: pytest-flake8; extra == "dev"
Requires-Dist: flake8<5.0.0; extra == "dev"
Requires-Dist: sphinx==4.3.2; extra == "dev"
Requires-Dist: sphinx-rtd-theme-github-versions; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: aioca>=1.6; extra == "dev"
Requires-Dist: cothread; sys_platform != "win32" and extra == "dev"
Requires-Dist: p4p; extra == "dev"

@@ -23,0 +39,0 @@ pythonSoftIOC

+2
-2

@@ -11,4 +11,4 @@ # Compute a version number from a git repo or archive

# These will be filled in if git archive is run or by setup.py cmdclasses
GIT_REFS = 'tag: 4.4.0'
GIT_SHA1 = '07b1168'
GIT_REFS = 'tag: 4.5.0'
GIT_SHA1 = 'eea8794'

@@ -15,0 +15,0 @@ # Git describe gives us sha1, last version-like tag, and commits since then

@@ -15,2 +15,3 @@ import os

db_put_field,
db_get_field,
)

@@ -87,3 +88,23 @@ from .device_core import DeviceSupportCore, RecordLookup

def get_field(self, field):
''' Returns the given field value as a string.'''
assert hasattr(self, "_record"), \
'get_field may only be called after iocInit'
data = (c_char * 40)()
name = self._name + '.' + field
db_get_field(name, fields.DBF_STRING, addressof(data), 1)
return _string_at(data, 40)
def set_field(self, field, value):
'''Sets the given field to the given value. Value will be transported as
a DBF_STRING.'''
assert hasattr(self, "_record"), \
'set_field may only be called after iocInit'
data = (c_char * 40)()
data.value = str(value).encode() + b'\0'
name = self._name + '.' + field
db_put_field(name, fields.DBF_STRING, addressof(data), 1)
class ProcessDeviceSupportIn(ProcessDeviceSupportCore):

@@ -90,0 +111,0 @@ _link_ = 'INP'

@@ -110,9 +110,51 @@ /* Provide EPICS functions in Python format */

PyExc_RuntimeError, "dbNameToAddr failed for %s", name);
if (dbPutField(&dbAddr, dbrType, pbuffer, length))
long put_result;
/* There are two important locks to consider at this point: The Global
* Interpreter Lock (GIL) and the EPICS record lock. A deadlock is possible if
* this thread holds the GIL and wants the record lock (which happens inside
* dbPutField), and there exists another EPICS thread that has the record lock
* and wants to call Python (which requires the GIL).
* This can occur if this code is called as part of an asynchronous on_update
* callback.
* Therefore, we must ensure we relinquish the GIL while we perform this
* EPICS call, to avoid potential deadlocks.
* See https://github.com/dls-controls/pythonSoftIOC/issues/119. */
Py_BEGIN_ALLOW_THREADS
put_result = dbPutField(&dbAddr, dbrType, pbuffer, length);
Py_END_ALLOW_THREADS
if (put_result)
return PyErr_Format(
PyExc_RuntimeError, "dbPutField failed for %s", name);
Py_RETURN_NONE;
else
Py_RETURN_NONE;
}
static PyObject *db_get_field(PyObject *self, PyObject *args)
{
const char *name;
short dbrType;
void *pbuffer;
long length;
if (!PyArg_ParseTuple(args, "shnl", &name, &dbrType, &pbuffer, &length))
return NULL;
struct dbAddr dbAddr;
if (dbNameToAddr(name, &dbAddr))
return PyErr_Format(
PyExc_RuntimeError, "dbNameToAddr failed for %s", name);
long get_result;
long options = 0;
/* See reasoning for Python macros in long comment in db_put_field. */
Py_BEGIN_ALLOW_THREADS
get_result = dbGetField(&dbAddr, dbrType, pbuffer, &options, &length, NULL);
Py_END_ALLOW_THREADS
if (get_result)
return PyErr_Format(
PyExc_RuntimeError, "dbGetField failed for %s", name);
else
Py_RETURN_NONE;
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

@@ -270,2 +312,4 @@ /* IOC PV put logging */

"Put a database field to a value"},
{"db_get_field", db_get_field, METH_VARARGS,
"Get a database field's value"},
{"install_pv_logging", install_pv_logging, METH_VARARGS,

@@ -272,0 +316,0 @@ "Install caput logging to stdout"},

@@ -23,5 +23,9 @@ '''External DLL imports used for implementing Python EPICS device support.

def db_put_field(name, dbr_type, pbuffer, length):
'''Put field where pbuffer is void* pointer. Returns RC'''
'''Put field where pbuffer is void* pointer. Returns None.'''
return _extension.db_put_field(name, dbr_type, pbuffer, length)
def db_get_field(name, dbr_type, pbuffer, length):
'''Get field where pbuffer is void* pointer. Returns None.'''
return _extension.db_get_field(name, dbr_type, pbuffer, length)
def install_pv_logging(acf_file):

@@ -28,0 +32,0 @@ '''Install pv logging'''

import asyncio
import multiprocessing
import subprocess
import sys
import numpy

@@ -350,3 +351,2 @@ import os

def validate_fixture_names(params):

@@ -908,1 +908,312 @@ """Provide nice names for the out_records fixture in TestValidate class"""

pytest.fail("Process did not terminate")
class TestGetSetField:
"""Tests related to get_field and set_field on records"""
test_result_rec = "TestResult"
def test_set_field_before_init_fails(self):
"""Test that calling set_field before iocInit() raises an exception"""
ao = builder.aOut("testAOut")
with pytest.raises(AssertionError) as e:
ao.set_field("EGU", "Deg")
assert "set_field may only be called after iocInit" in str(e.value)
def test_get_field_before_init_fails(self):
"""Test that calling get_field before iocInit() raises an exception"""
ao = builder.aOut("testAOut")
with pytest.raises(AssertionError) as e:
ao.get_field("EGU")
assert "get_field may only be called after iocInit" in str(e.value)
def get_set_test_func(self, device_name, conn):
"""Run an IOC and do simple get_field/set_field calls"""
builder.SetDeviceName(device_name)
lo = builder.longOut("TestLongOut", EGU="unset", DRVH=12)
# Record to indicate success/failure
bi = builder.boolIn(self.test_result_rec, ZNAM="FAILED", ONAM="SUCCESS")
dispatcher = asyncio_dispatcher.AsyncioDispatcher()
builder.LoadDatabase()
softioc.iocInit(dispatcher)
conn.send("R") # "Ready"
log("CHILD: Sent R over Connection to Parent")
# Set and then get the EGU field
egu = "TEST"
lo.set_field("EGU", egu)
log("CHILD: set_field successful")
readback_egu = lo.get_field("EGU")
log(f"CHILD: get_field returned {readback_egu}")
assert readback_egu == egu, \
f"EGU field was not {egu}, was {readback_egu}"
log("CHILD: assert passed")
# Test completed, report to listening camonitor
bi.set(True)
# Keep process alive while main thread works.
while (True):
if conn.poll(TIMEOUT):
val = conn.recv()
if val == "D": # "Done"
break
log("CHILD: Received exit command, child exiting")
@pytest.mark.asyncio
async def test_get_set(self):
"""Test a simple set_field/get_field is successful"""
ctx = get_multiprocessing_context()
parent_conn, child_conn = ctx.Pipe()
device_name = create_random_prefix()
process = ctx.Process(
target=self.get_set_test_func,
args=(device_name, child_conn),
)
process.start()
log("PARENT: Child started, waiting for R command")
from aioca import camonitor
try:
# Wait for message that IOC has started
select_and_recv(parent_conn, "R")
log("PARENT: received R command")
queue = asyncio.Queue()
record = device_name + ":" + self.test_result_rec
monitor = camonitor(record, queue.put)
log(f"PARENT: monitoring {record}")
new_val = await asyncio.wait_for(queue.get(), TIMEOUT)
log(f"PARENT: new_val is {new_val}")
assert new_val == 1, \
f"Test failed, value was not 1(True), was {new_val}"
finally:
monitor.close()
# Clear the cache before stopping the IOC stops
# "channel disconnected" error messages
aioca_cleanup()
log("PARENT: Sending Done command to child")
parent_conn.send("D") # "Done"
process.join(timeout=TIMEOUT)
log(f"PARENT: Join completed with exitcode {process.exitcode}")
if process.exitcode is None:
pytest.fail("Process did not terminate")
def get_set_too_long_value(self, device_name, conn):
"""Run an IOC and deliberately call set_field with a too-long value"""
builder.SetDeviceName(device_name)
lo = builder.longOut("TestLongOut", EGU="unset", DRVH=12)
# Record to indicate success/failure
bi = builder.boolIn(self.test_result_rec, ZNAM="FAILED", ONAM="SUCCESS")
dispatcher = asyncio_dispatcher.AsyncioDispatcher()
builder.LoadDatabase()
softioc.iocInit(dispatcher)
conn.send("R") # "Ready"
log("CHILD: Sent R over Connection to Parent")
# Set a too-long value and confirm it reports an error
try:
lo.set_field("EGU", "ThisStringIsFarTooLongToFitIntoTheEguField")
except ValueError as e:
# Expected error, report success to listening camonitor
assert "byte string too long" in e.args[0]
bi.set(True)
# Keep process alive while main thread works.
while (True):
if conn.poll(TIMEOUT):
val = conn.recv()
if val == "D": # "Done"
break
log("CHILD: Received exit command, child exiting")
@pytest.mark.asyncio
async def test_set_too_long_value(self):
"""Test that set_field with a too-long value raises the expected
error"""
ctx = get_multiprocessing_context()
parent_conn, child_conn = ctx.Pipe()
device_name = create_random_prefix()
process = ctx.Process(
target=self.get_set_too_long_value,
args=(device_name, child_conn),
)
process.start()
log("PARENT: Child started, waiting for R command")
from aioca import camonitor
try:
# Wait for message that IOC has started
select_and_recv(parent_conn, "R")
log("PARENT: received R command")
queue = asyncio.Queue()
record = device_name + ":" + self.test_result_rec
monitor = camonitor(record, queue.put)
log(f"PARENT: monitoring {record}")
new_val = await asyncio.wait_for(queue.get(), TIMEOUT)
log(f"PARENT: new_val is {new_val}")
assert new_val == 1, \
f"Test failed, value was not 1(True), was {new_val}"
finally:
monitor.close()
# Clear the cache before stopping the IOC stops
# "channel disconnected" error messages
aioca_cleanup()
log("PARENT: Sending Done command to child")
parent_conn.send("D") # "Done"
process.join(timeout=TIMEOUT)
log(f"PARENT: Join completed with exitcode {process.exitcode}")
if process.exitcode is None:
pytest.fail("Process did not terminate")
class TestRecursiveSet:
"""Tests related to recursive set() calls. See original issue here:
https://github.com/dls-controls/pythonSoftIOC/issues/119"""
recursive_record_name = "RecursiveLongOut"
def recursive_set_func(self, device_name, conn):
from cothread import Event
def useless_callback(value):
log("CHILD: In callback ", value)
useless_pv.set(0)
log("CHILD: Exiting callback")
def go_away(*args):
log("CHILD: received exit signal ", args)
event.Signal()
builder.SetDeviceName(device_name)
useless_pv = builder.aOut(
self.recursive_record_name,
initial_value=0,
on_update=useless_callback
)
event = Event()
builder.Action("GO_AWAY", on_update = go_away)
builder.LoadDatabase()
softioc.iocInit()
conn.send("R") # "Ready"
log("CHILD: Sent R over Connection to Parent")
log("CHILD: About to wait")
event.Wait()
log("CHILD: Exiting")
@requires_cothread
@pytest.mark.asyncio
async def test_recursive_set(self):
"""Test that recursive sets do not cause a deadlock"""
ctx = get_multiprocessing_context()
parent_conn, child_conn = ctx.Pipe()
device_name = create_random_prefix()
process = ctx.Process(
target=self.recursive_set_func,
args=(device_name, child_conn),
)
process.start()
log("PARENT: Child started, waiting for R command")
from aioca import caput, camonitor
try:
# Wait for message that IOC has started
select_and_recv(parent_conn, "R")
log("PARENT: received R command")
record = device_name + ":" + self.recursive_record_name
log(f"PARENT: monitoring {record}")
queue = asyncio.Queue()
monitor = camonitor(record, queue.put, all_updates=True)
log("PARENT: Beginning first wait")
# Expected initial state
new_val = await asyncio.wait_for(queue.get(), TIMEOUT)
log(f"PARENT: initial new_val: {new_val}")
assert new_val == 0
# Try a series of caput calls, to maximise chance to trigger
# the deadlock
i = 1
while i < 500:
log(f"PARENT: begin loop with i={i}")
await caput(record, i)
new_val = await asyncio.wait_for(queue.get(), 1)
assert new_val == i
new_val = await asyncio.wait_for(queue.get(), 1)
assert new_val == 0 # .set() should reset value
i += 1
# Signal the IOC to cleanly shut down
await caput(device_name + ":" + "GO_AWAY", 1)
except asyncio.TimeoutError as e:
raise asyncio.TimeoutError(
f"IOC did not send data back - loop froze on iteration {i} "
"- it has probably hung/deadlocked."
) from e
finally:
monitor.close()
# Clear the cache before stopping the IOC stops
# "channel disconnected" error messages
aioca_cleanup()
process.join(timeout=TIMEOUT)
log(f"PARENT: Join completed with exitcode {process.exitcode}")
if process.exitcode is None:
process.terminate()
pytest.fail("Process did not finish cleanly, terminating")