import os
import subprocess
from collections import OrderedDict
import cothread
from annotypes import Anno
from malcolm import __version__
from malcolm.core import (
Alarm,
AlarmSeverity,
BadValueError,
ProcessStartHook,
ProcessStopHook,
StringMeta,
Widget,
)
from malcolm.modules import builtin, ca
from malcolm.modules.ca.util import catools
from ..parts.dirparsepart import DirParsePart
from ..parts.iociconpart import IocIconPart
def await_ioc_start(stats, prefix):
cothread.Yield()
pid_rbv = catools.caget(f"{prefix}:PID", timeout=5)
if int(pid_rbv) != os.getpid():
raise BadValueError(
"Got back different PID: "
+ "is there another system instance on the machine?"
)
catools.caput(
f"{prefix}:YAML:PATH", stats["yaml_path"], datatype=catools.DBR_CHAR_STR
)
catools.caput(
f"{prefix}:PYMALCOLM:PATH",
stats["pymalcolm_path"],
datatype=catools.DBR_CHAR_STR,
)
def start_ioc(stats, prefix):
db_macros = f"prefix='{prefix}'"
try:
epics_base = os.environ["EPICS_BASE"]
except KeyError:
raise BadValueError("EPICS base not defined in environment")
softIoc_bin = epics_base + "/bin/linux-x86_64/softIoc"
for key, value in stats.items():
db_macros += f",{key}='{value}'"
root = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0]
db_template = os.path.join(root, "db", "system.template")
ioc = subprocess.Popen(
[softIoc_bin, "-m", db_macros, "-d", db_template],
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
)
cothread.Spawn(await_ioc_start, stats, prefix)
return ioc
with Anno("prefix for self.system PVs"):
APvPrefix = str
with Anno("space-separated list of IOCs to monitor"):
AIocList = str
[docs]class ProcessController(builtin.controllers.ManagerController):
def __init__(
self,
mri: builtin.controllers.AMri,
prefix: APvPrefix,
config_dir: builtin.controllers.AConfigDir,
ioc_list: AIocList = "",
) -> None:
super().__init__(mri, config_dir)
self.ioc = None
self.ioc_blocks: OrderedDict = OrderedDict()
self.prefix = prefix
self.bl_iocs = ioc_list.split(" ")
if self.bl_iocs[-1] == "":
self.bl_iocs = self.bl_iocs[:-1]
self.stats = dict()
# TODO: the following stuff is all Linux-specific....
sys_call_bytes = open(f"/proc/{os.getpid()}/cmdline", "rb").read().split(b"\0")
sys_call = [el.decode("utf-8") for el in sys_call_bytes]
self.stats["pymalcolm_path"] = os.path.abspath(sys_call[1])
self.stats["yaml_path"] = os.path.abspath(sys_call[2])
self.stats["yaml_ver"] = self.parse_yaml_version(
self.stats["yaml_path"], "/dls_sw/work", "/dls_sw/prod"
)
self.stats["pymalcolm_ver"] = __version__
hostname = os.uname()[1]
self.stats["kernel"] = f"{os.uname()[0]} {os.uname()[2]}"
self.stats["hostname"] = (
hostname if len(hostname) < 39 else hostname[:35] + "..."
)
self.stats["pid"] = str(os.getpid())
self.pymalcolm_path = StringMeta(
"Path to pymalcolm executable", tags=[Widget.MULTILINETEXTUPDATE.tag()]
).create_attribute_model(self.stats["pymalcolm_path"])
self.pymalcolm_ver = StringMeta(
"Version of pymalcolm executable", tags=[Widget.TEXTUPDATE.tag()]
).create_attribute_model(self.stats["pymalcolm_ver"])
self.yaml_path = StringMeta(
"Path to yaml configuration file", tags=[Widget.MULTILINETEXTUPDATE.tag()]
).create_attribute_model(self.stats["yaml_path"])
self.yaml_ver = StringMeta(
"version of yaml configuration file", tags=[Widget.TEXTUPDATE.tag()]
).create_attribute_model(self.stats["yaml_ver"])
self.hostname = StringMeta(
"Name of host machine", tags=[Widget.TEXTUPDATE.tag()]
).create_attribute_model(self.stats["hostname"])
self.kernel = StringMeta(
"Kernel of host machine", tags=[Widget.TEXTUPDATE.tag()]
).create_attribute_model(self.stats["kernel"])
self.pid = StringMeta(
"process ID of pymalcolm instance", tags=[Widget.TEXTUPDATE.tag()]
).create_attribute_model(self.stats["pid"])
self.field_registry.add_attribute_model("pymalcolmPath", self.pymalcolm_path)
self.field_registry.add_attribute_model("pymalcolmVer", self.pymalcolm_ver)
self.field_registry.add_attribute_model("yamlPath", self.yaml_path)
self.field_registry.add_attribute_model("yamlVer", self.yaml_ver)
self.field_registry.add_attribute_model("hostname", self.hostname)
self.field_registry.add_attribute_model("kernel", self.kernel)
self.field_registry.add_attribute_model("pid", self.pid)
if self.stats["yaml_ver"] in ["work", "unknown"]:
message = "Non-prod YAML config"
alarm = Alarm(message=message, severity=AlarmSeverity.MINOR_ALARM)
self.update_health("", builtin.infos.HealthInfo(alarm))
self.register_hooked(ProcessStartHook, self.init)
self.register_hooked(ProcessStopHook, self.stop_ioc)
def init(self):
if self.ioc is None:
self.ioc = start_ioc(self.stats, self.prefix)
self.get_ioc_list()
super().init()
def set_default_layout(self):
name = []
mri = []
x = []
y = []
visible = []
for part_name in self.parts.keys():
if isinstance(self.parts[part_name], builtin.parts.ChildPart):
visible += [True]
x += [0]
y += [0]
name += [part_name]
mri += [self.parts[part_name].mri]
self.set_layout(builtin.util.LayoutTable(name, mri, x, y, visible))
def stop_ioc(self):
if self.ioc is not None:
self.ioc.terminate()
self.ioc = None
def get_ioc_list(self):
ioc_controllers = []
for ioc in self.bl_iocs:
ioc_controller = make_ioc_status(ioc)
ioc_controllers += [ioc_controller]
self.process.add_controllers(ioc_controllers)
for ioc in self.bl_iocs:
self.add_part(builtin.parts.ChildPart(name=ioc, mri=ioc + ":STATUS"))
def parse_yaml_version(self, file_path, work_area, prod_area):
ver = "unknown"
if file_path.startswith(work_area):
ver = "work"
elif file_path.startswith(prod_area):
ver = self._run_git_cmd(
"describe", "--tags", "--exact-match", cwd=os.path.split(file_path)[0]
)
if ver is None:
return "Prod (unknown version)"
ver = ver.strip(b"\n").decode("utf-8")
return ver
def _run_git_cmd(self, *args, **kwargs):
# Run git command, don't care if it fails, logging the output
cwd = kwargs.get("cwd", self.config_dir)
try:
output = subprocess.check_output(("git",) + args, cwd=cwd)
except subprocess.CalledProcessError as e:
self.log.warning("Git command failed: %s\n%s", e, e.output)
return None
else:
self.log.debug("Git command completed: %s", output)
return output
def make_ioc_status(ioc):
controller = builtin.controllers.StatefulController(ioc + ":STATUS")
controller.add_part(
ca.parts.CAStringPart(
name="epicsVersion",
description="EPICS version",
rbv=(ioc + ":EPICS_VERS"),
throw=False,
)
)
controller.add_part(
IocIconPart(ioc, (os.path.split(__file__)[0] + "/../icons/epics-logo.svg"))
)
controller.add_part(DirParsePart(ioc, ioc))
controller.add_part(
ca.parts.CAActionPart(
"restartIoc",
description="restart IOC via procServ",
pv=(ioc + ":RESTART"),
throw=False,
)
)
return controller