From 8a50565d3ec818ec9a758fe1db1206f5f9944a49 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 6 Nov 2026 17:36:58 +0000 Subject: [PATCH 01/14] restart_services --- src/server_common/pv_names.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server_common/pv_names.py b/src/server_common/pv_names.py index cea8b35..ec2bfe8 100644 --- a/src/server_common/pv_names.py +++ b/src/server_common/pv_names.py @@ -75,6 +75,7 @@ class BlockserverPVNames: CURR_CONFIG_NAME = prepend_blockserver("CURR_CONFIG_NAME") CURR_CONFIG_NAME_SEVR = prepend_blockserver("CURR_CONFIG_NAME.SEVR") HEARTBEAT = "IOC:BLOCKSVR:DEVIOS:HEARTBEAT" + RESTART_PR_IOC = prepend_blockserver("RESTART_PR_IOC") @staticmethod def get_config_details_pv(pv_key): From 2b1414cbe9c04598d6075c7ed5c8a742c0f02269 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Tue, 10 Nov 2026 14:55:54 +0000 Subject: [PATCH 02/14] restart working --- src/server_common/pv_names.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server_common/pv_names.py b/src/server_common/pv_names.py index ec2bfe8..57bb1de 100644 --- a/src/server_common/pv_names.py +++ b/src/server_common/pv_names.py @@ -40,6 +40,7 @@ class DatabasePVNames: MOXA_MAPPINGS = prepend_blockserver("MOXA_MAPPINGS") UPDATE_MOXA_MAPPINGS = prepend_blockserver("UPDATE_MM") NUM_MOXAS = prepend_blockserver("NUM_MOXAS") + RESTART = prepend_blockserver("RESTART_DBSRV") class BlockserverPVNames: From 2eb9e7e2f9e0d09311bf538fd7469ce956a57260 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Wed, 11 Nov 2026 10:07:24 +0000 Subject: [PATCH 03/14] rename pv --- src/server_common/pv_names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server_common/pv_names.py b/src/server_common/pv_names.py index 57bb1de..6cfb4bd 100644 --- a/src/server_common/pv_names.py +++ b/src/server_common/pv_names.py @@ -76,7 +76,7 @@ class BlockserverPVNames: CURR_CONFIG_NAME = prepend_blockserver("CURR_CONFIG_NAME") CURR_CONFIG_NAME_SEVR = prepend_blockserver("CURR_CONFIG_NAME.SEVR") HEARTBEAT = "IOC:BLOCKSVR:DEVIOS:HEARTBEAT" - RESTART_PR_IOC = prepend_blockserver("RESTART_PR_IOC") + RESTART_PROTECTED_IOC = prepend_blockserver("RESTART_PROTECTED_IOC") @staticmethod def get_config_details_pv(pv_key): From 660462b8461d0406bd2b7176593bbdd72c270b8f Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Wed, 11 Nov 2026 10:24:41 +0000 Subject: [PATCH 04/14] ruff --- src/server_common/pv_names.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/server_common/pv_names.py b/src/server_common/pv_names.py index 6cfb4bd..bc70b36 100644 --- a/src/server_common/pv_names.py +++ b/src/server_common/pv_names.py @@ -17,7 +17,7 @@ BLOCKSERVER = "BLOCKSERVER:" -def prepend_blockserver(base_name): +def prepend_blockserver(base_name: str) -> str: return BLOCKSERVER + base_name @@ -79,13 +79,13 @@ class BlockserverPVNames: RESTART_PROTECTED_IOC = prepend_blockserver("RESTART_PROTECTED_IOC") @staticmethod - def get_config_details_pv(pv_key): + def get_config_details_pv(pv_key: str) -> str: return prepend_blockserver(pv_key + ":GET_CONFIG_DETAILS") @staticmethod - def get_component_details_pv(pv_key): + def get_component_details_pv(pv_key: str) -> str: return prepend_blockserver(pv_key + ":GET_COMPONENT_DETAILS") @staticmethod - def get_dependencies_pv(pv_key): + def get_dependencies_pv(pv_key: str) -> str: return prepend_blockserver(pv_key + ":DEPENDENCIES") From 46df6b21f788c66060589b9c8f9d169dfb7aeaac Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Wed, 11 Nov 2026 12:14:32 +0000 Subject: [PATCH 05/14] new pv collection --- src/server_common/constants.py | 12 ++++++++++++ src/server_common/pv_names.py | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/server_common/constants.py b/src/server_common/constants.py index 85f6881..446099a 100644 --- a/src/server_common/constants.py +++ b/src/server_common/constants.py @@ -30,4 +30,16 @@ "ALARM", "BSKAFKA", ) +SERVICE_IOCS = ( + "INSTETC", + "ISISDAE", + "BLOCKSVR", + "ARINST", + "ARBLOCK", + "GWBLOCK", + "RUNCTRL", + "ALARM", + "BSKAFKA", + "DBSVR", +) IS_LINUX = os.name != "nt" diff --git a/src/server_common/pv_names.py b/src/server_common/pv_names.py index bc70b36..e5ed886 100644 --- a/src/server_common/pv_names.py +++ b/src/server_common/pv_names.py @@ -40,7 +40,7 @@ class DatabasePVNames: MOXA_MAPPINGS = prepend_blockserver("MOXA_MAPPINGS") UPDATE_MOXA_MAPPINGS = prepend_blockserver("UPDATE_MM") NUM_MOXAS = prepend_blockserver("NUM_MOXAS") - RESTART = prepend_blockserver("RESTART_DBSRV") + RESTART = prepend_blockserver("RESTART_DBSVR") class BlockserverPVNames: @@ -76,7 +76,7 @@ class BlockserverPVNames: CURR_CONFIG_NAME = prepend_blockserver("CURR_CONFIG_NAME") CURR_CONFIG_NAME_SEVR = prepend_blockserver("CURR_CONFIG_NAME.SEVR") HEARTBEAT = "IOC:BLOCKSVR:DEVIOS:HEARTBEAT" - RESTART_PROTECTED_IOC = prepend_blockserver("RESTART_PROTECTED_IOC") + RESTART_SERVICE_IOC = prepend_blockserver("RESTART_SERVICE_IOC") @staticmethod def get_config_details_pv(pv_key: str) -> str: From ea8ae47ce7a165f0d0ee22e5f31b2e871e5ab4a1 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Thu, 12 Nov 2026 14:59:33 +0000 Subject: [PATCH 06/14] ruff/pyright --- src/server_common/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server_common/utilities.py b/src/server_common/utilities.py index 2136d2c..eddbd42 100644 --- a/src/server_common/utilities.py +++ b/src/server_common/utilities.py @@ -152,7 +152,7 @@ def convert_to_json(value: object) -> str: return json.dumps(value) -def convert_from_json(value: str) -> object: +def convert_from_json(value: str) -> Any: """Converts the inputted string into a JSON object. Args: @@ -309,7 +309,7 @@ def ioc_restart_pending(ioc_pv: Any, channel_access: Any) -> Any: return channel_access.caget(ioc_pv + ":RESTART", as_string=True) == "Busy" -def retry(max_attempts: int, interval: int, exception: Any) -> Any: +def retry(max_attempts: int, interval: float, exception: Any) -> Any: """ Attempt to perform a function a number of times in specified intervals before failing. From 7d7bf2fdf6b2c832c90c04bed7ddd38ccee0a45a Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 13 Nov 2026 11:03:14 +0000 Subject: [PATCH 07/14] add create pv --- src/server_common/mocks/mock_ca_server.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server_common/mocks/mock_ca_server.py b/src/server_common/mocks/mock_ca_server.py index f19f26b..6273d9f 100644 --- a/src/server_common/mocks/mock_ca_server.py +++ b/src/server_common/mocks/mock_ca_server.py @@ -15,12 +15,15 @@ # http://opensource.org/licenses/eclipse-1.0.php -class MockCAServer(object): +class MockCAServer: def __init__(self): - self.pv_list = dict() + self.pv_list = {} def updatePV(self, pv_name, data): self.pv_list[pv_name] = data def deletePV(self, pv_name): del self.pv_list[pv_name] + + def createPV(self, prefix, pvdb): + self.pv_list[pvdb["name"]] = pvdb From dd33ebe05f52a99c24be40a6647b4f969ab49f13 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 13 Nov 2026 11:13:50 +0000 Subject: [PATCH 08/14] ruff/pyright --- src/server_common/mocks/mock_ca_server.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/server_common/mocks/mock_ca_server.py b/src/server_common/mocks/mock_ca_server.py index 6273d9f..25a95e3 100644 --- a/src/server_common/mocks/mock_ca_server.py +++ b/src/server_common/mocks/mock_ca_server.py @@ -13,17 +13,18 @@ # along with this program; if not, you can obtain a copy from # https://www.eclipse.org/org/documents/epl-v10.php or # http://opensource.org/licenses/eclipse-1.0.php +from typing import Any class MockCAServer: - def __init__(self): + def __init__(self) -> None: self.pv_list = {} - def updatePV(self, pv_name, data): + def updatePV(self, pv_name: str, data: dict[str, str | list[int] | Any]) -> None: # noqa: N802 self.pv_list[pv_name] = data - def deletePV(self, pv_name): + def deletePV(self, pv_name: str) -> None: # noqa: N802 del self.pv_list[pv_name] - def createPV(self, prefix, pvdb): + def createPV(self, prefix: str, pvdb: dict[str, dict[str, str | list[int] | Any]]) -> None: # noqa: N802 self.pv_list[pvdb["name"]] = pvdb From 564a050055a9e6e870a757552045550ea2564a87 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 13 Nov 2026 11:30:52 +0000 Subject: [PATCH 09/14] ruff/pyright --- src/server_common/utilities.py | 41 +++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/server_common/utilities.py b/src/server_common/utilities.py index eddbd42..5c7a5cd 100644 --- a/src/server_common/utilities.py +++ b/src/server_common/utilities.py @@ -19,12 +19,14 @@ import binascii import datetime +import functools import json import re import threading import time import zlib -from typing import Any +from collections.abc import Callable +from typing import Any, ParamSpec, TypeVar from xml.etree import ElementTree from server_common.common_exceptions import MaxAttemptsExceededException @@ -32,7 +34,12 @@ # Default to base class - does not actually log anything LOGGER = Logger() -_LOGGER_LOCK = threading.RLock() # To prevent message interleaving between different threads. +_LOGGER_LOCK = ( + threading.RLock() +) # To prevent message interleaving between different threads. + +P = ParamSpec("P") # Captures function arguments +R = TypeVar("R") # Captures return type class SEVERITY: @@ -93,13 +100,12 @@ def compress_and_hex(value: str) -> bytes: Returns: bytes : A compressed and hexed version of the inputted string """ - ( - isinstance(value, str), - ( + if not isinstance(value, str): + raise TypeError( "Non-str argument passed to compress_and_hex, maybe Python 2/3 compatibility issue\n" f"Argument was type {value.__class__.__name__} with value {value}" - ), - ) + ) + compr = zlib.compress(bytes(value, "utf-8")) return binascii.hexlify(compr) @@ -309,7 +315,9 @@ def ioc_restart_pending(ioc_pv: Any, channel_access: Any) -> Any: return channel_access.caget(ioc_pv + ":RESTART", as_string=True) == "Busy" -def retry(max_attempts: int, interval: float, exception: Any) -> Any: +def retry( + max_attempts: int, interval: float, exception: type[Exception] +) -> Callable[[Callable[P, R]], Callable[P, R]]: """ Attempt to perform a function a number of times in specified intervals before failing. @@ -323,10 +331,13 @@ def retry(max_attempts: int, interval: float, exception: Any) -> Any: """ - def _tags_decorator(func: Any) -> Any: - def _wrapper(*args: Any, **kwargs: Any) -> Any: + def _tags_decorator(func: Callable[P, R]) -> Callable[P, R]: + @functools.wraps(func) + def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R: attempts = 0 - last_exception = ValueError(f"Max attempts should be > 0, it is {max_attempts}") + last_exception: Exception = ValueError( + f"Max attempts should be > 0, it is {max_attempts}" + ) while attempts < max_attempts: try: return func(*args, **kwargs) @@ -371,7 +382,9 @@ def lowercase_and_make_unique(in_list: list[str]) -> set[str]: return {x.lower() for x in in_list} -def parse_date_time_arg_exit_on_fail(date_arg: str, error_code: int = 1) -> datetime.datetime: +def parse_date_time_arg_exit_on_fail( + date_arg: str, error_code: int = 1 +) -> datetime.datetime: """ Parse a date argument and exit the program with an error code if that argument is not a date Args: @@ -404,4 +417,6 @@ def dehex_and_decompress_waveform_value(value: str | bytes) -> str: if value and len(value) % 2 == 0: return zlib.decompress(binascii.unhexlify(value)).decode("utf-8") else: - raise ValueError(f"Invalid hex string: value is empty or has odd length ({len(value)})") + raise ValueError( + f"Invalid hex string: value is empty or has odd length ({len(value)})" + ) From 63236775ae1caa5c1aa9c9815c69ddabed228aed Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 13 Nov 2026 11:32:55 +0000 Subject: [PATCH 10/14] ruff/pyright --- src/server_common/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server_common/utilities.py b/src/server_common/utilities.py index 5c7a5cd..7a406ef 100644 --- a/src/server_common/utilities.py +++ b/src/server_common/utilities.py @@ -283,7 +283,7 @@ def parse_xml_removing_namespace(file_path: str) -> Any: for _, el in it: if ":" in el.tag: el.tag = el.tag.split("}", 1)[1] - return it.root + return it.root # type: ignore[union-attr] def waveform_to_string(data: Any) -> str: From 8d19e3bc73d8cfad8aa2f1cdce4e5fc56200f3cc Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 13 Nov 2026 11:43:33 +0000 Subject: [PATCH 11/14] ruff/pyright --- src/server_common/utilities.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/server_common/utilities.py b/src/server_common/utilities.py index 7a406ef..a70ce7c 100644 --- a/src/server_common/utilities.py +++ b/src/server_common/utilities.py @@ -25,18 +25,17 @@ import threading import time import zlib -from collections.abc import Callable +from collections.abc import Callable, Iterable from typing import Any, ParamSpec, TypeVar from xml.etree import ElementTree +from server_common.channel_access import ChannelAccess from server_common.common_exceptions import MaxAttemptsExceededException from server_common.loggers.logger import Logger # Default to base class - does not actually log anything LOGGER = Logger() -_LOGGER_LOCK = ( - threading.RLock() -) # To prevent message interleaving between different threads. +_LOGGER_LOCK = threading.RLock() # To prevent message interleaving between different threads. P = ParamSpec("P") # Captures function arguments R = TypeVar("R") # Captures return type @@ -146,7 +145,7 @@ def dehex_and_decompress_waveform(value: list) -> bytes: return dehex_and_decompress(bytes_rep) -def convert_to_json(value: object) -> str: +def convert_to_json(value: Any) -> str: # noqa: ANN401 """Converts the inputted object to JSON format. Args: @@ -158,7 +157,7 @@ def convert_to_json(value: object) -> str: return json.dumps(value) -def convert_from_json(value: str) -> Any: +def convert_from_json(value: str) -> Any: # noqa: ANN401 """Converts the inputted string into a JSON object. Args: @@ -270,7 +269,7 @@ def create_pv_name( return pv -def parse_xml_removing_namespace(file_path: str) -> Any: +def parse_xml_removing_namespace(file_path: str) -> Any: # noqa: ANN401 """Creates an Element object from a given xml file, removing the namespace. Args: @@ -286,7 +285,7 @@ def parse_xml_removing_namespace(file_path: str) -> Any: return it.root # type: ignore[union-attr] -def waveform_to_string(data: Any) -> str: +def waveform_to_string(data: Iterable[int | str]) -> str: """ Args: data: waveform as null terminated string @@ -298,11 +297,14 @@ def waveform_to_string(data: Any) -> str: for i in data: if i == 0: break - output += chr(i) + if isinstance(i, str): + output += i + else: + output += str(chr(i)) return output -def ioc_restart_pending(ioc_pv: Any, channel_access: Any) -> Any: +def ioc_restart_pending(ioc_pv: str, channel_access: ChannelAccess) -> bool: """Check if a particular IOC is restarting. Assumes it has suitable restart PV Args: @@ -382,9 +384,7 @@ def lowercase_and_make_unique(in_list: list[str]) -> set[str]: return {x.lower() for x in in_list} -def parse_date_time_arg_exit_on_fail( - date_arg: str, error_code: int = 1 -) -> datetime.datetime: +def parse_date_time_arg_exit_on_fail(date_arg: str, error_code: int = 1) -> datetime.datetime: """ Parse a date argument and exit the program with an error code if that argument is not a date Args: @@ -417,6 +417,4 @@ def dehex_and_decompress_waveform_value(value: str | bytes) -> str: if value and len(value) % 2 == 0: return zlib.decompress(binascii.unhexlify(value)).decode("utf-8") else: - raise ValueError( - f"Invalid hex string: value is empty or has odd length ({len(value)})" - ) + raise ValueError(f"Invalid hex string: value is empty or has odd length ({len(value)})") From 1cd91257958a5939d57351d3e47360ce06b1d623 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 13 Nov 2026 11:51:20 +0000 Subject: [PATCH 12/14] ruff/pyright --- src/server_common/utilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server_common/utilities.py b/src/server_common/utilities.py index a70ce7c..68144f1 100644 --- a/src/server_common/utilities.py +++ b/src/server_common/utilities.py @@ -304,7 +304,7 @@ def waveform_to_string(data: Iterable[int | str]) -> str: return output -def ioc_restart_pending(ioc_pv: str, channel_access: ChannelAccess) -> bool: +def ioc_restart_pending(ioc_pv: str, channel_access: type[ChannelAccess]) -> bool: """Check if a particular IOC is restarting. Assumes it has suitable restart PV Args: From d6cbc350564fea7b1edcac398c5a6b2871ed5ee9 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 13 Nov 2026 11:57:12 +0000 Subject: [PATCH 13/14] ruff/pyright --- src/server_common/utilities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server_common/utilities.py b/src/server_common/utilities.py index 68144f1..77db10c 100644 --- a/src/server_common/utilities.py +++ b/src/server_common/utilities.py @@ -86,7 +86,7 @@ def print_and_log( src (string, optional): Gives the source of the message. Default source is BLOCKSVR. """ with _LOGGER_LOCK: - message = f"[{datetime.datetime.now()}] {severity}: {message}" # ruff: ignore [DTZ005] + message = f"[{datetime.datetime.now()}] {severity}: {message}" print(message) LOGGER.write_to_log(message, severity, src) @@ -396,7 +396,7 @@ def parse_date_time_arg_exit_on_fail(date_arg: str, error_code: int = 1) -> date """ try: - return datetime.datetime.strptime(date_arg, "%Y-%m-%dT%H:%M:%S") # ruff:ignore [DTZ007] + return datetime.datetime.strptime(date_arg, "%Y-%m-%dT%H:%M:%S") except (ValueError, TypeError) as ex: print(f"Can not interpret date '{date_arg}' error: {ex}") raise SystemExit(error_code) From fe89cc301fe3fb2edfa09ca717bbeddaf7f94885 Mon Sep 17 00:00:00 2001 From: Jack Doughty Date: Fri, 13 Nov 2026 12:05:11 +0000 Subject: [PATCH 14/14] ruff/pyright --- src/server_common/utilities.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/server_common/utilities.py b/src/server_common/utilities.py index 77db10c..12e7a2e 100644 --- a/src/server_common/utilities.py +++ b/src/server_common/utilities.py @@ -26,13 +26,15 @@ import time import zlib from collections.abc import Callable, Iterable -from typing import Any, ParamSpec, TypeVar +from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar from xml.etree import ElementTree -from server_common.channel_access import ChannelAccess from server_common.common_exceptions import MaxAttemptsExceededException from server_common.loggers.logger import Logger +if TYPE_CHECKING: + from server_common.channel_access import ChannelAccess + # Default to base class - does not actually log anything LOGGER = Logger() _LOGGER_LOCK = threading.RLock() # To prevent message interleaving between different threads. @@ -304,7 +306,7 @@ def waveform_to_string(data: Iterable[int | str]) -> str: return output -def ioc_restart_pending(ioc_pv: str, channel_access: type[ChannelAccess]) -> bool: +def ioc_restart_pending(ioc_pv: str, channel_access: "type[ChannelAccess]") -> bool: """Check if a particular IOC is restarting. Assumes it has suitable restart PV Args: