- ConsoleButtonsWidget ABORT: SIGINT to the client (only if the parent
CI for csaxs_bec / test (push) Successful in 1m28s

process looks like a BEC client), plus a delayed (500 ms) backup
  stop_devices request, defaulting to stop-all so the widget doubles as
  a generic emergency stop when opened standalone from the launcher menu
This commit is contained in:
x12sa
2026-07-07 21:56:58 +02:00
parent 936fc294a0
commit 0cc11e6bfb
@@ -26,10 +26,13 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
parent of the GUI server process), equivalent to pressing Ctrl+C in
the console. This works even if the client is blocked inside a motor
move or other long call, since it is a real OS signal rather than a
polled flag. As a backup, 500 ms later a device stop request is
published to the device server (same mechanism as the PositionerBox
stop button), so the hardware is stopped even if the client process
is hung or dead.
polled flag. The SIGINT is only sent if the parent process actually
looks like a BEC client -- it does not when the widget is opened
standalone from the launcher menu. In addition, 500 ms later a device
stop request is published to the device server (same mechanism as the
PositionerBox stop button): by default for ALL devices, so the widget
acts as a generic emergency stop from any context, even if the client
process is hung or dead.
"""
USER_ACCESS = ["message", "message.setter", "response", "clear_response"]
@@ -37,11 +40,13 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
def __init__(self, parent=None, **kwargs):
# Devices for which a backup stop request is published when ABORT is
# pressed (in addition to the SIGINT). An empty list means "stop ALL
# devices". For flomni, stopping ftransy is enough for the sample
# transfer: XQ#STOP is controller-wide, so it aborts all axes on
# transfer controller 1 and halts the #GRGET/#GRPUT thread.
self._backup_stop_devices = list(kwargs.pop("backup_stop_devices", ["ftransy"]))
# pressed. An empty list (default) means "stop ALL devices" -- the
# same device-server path BEC uses on scan abort -- which makes the
# widget a generic emergency stop, e.g. when opened standalone from
# the launcher menu. Stop-all also covers the flomni sample transfer:
# XQ#STOP via ftransy is controller-wide and halts the #GRGET/#GRPUT
# thread. Pass an explicit list to restrict the stop.
self._backup_stop_devices = list(kwargs.pop("backup_stop_devices", []))
super().__init__(parent=parent, **kwargs)
self._response = ""
# Captured once at construction time: the GUI server process is a
@@ -93,10 +98,31 @@ class ConsoleButtonsWidget(BECWidget, QWidget):
def _on_no(self):
self._response = "no"
def _client_is_bec_process(self) -> bool:
"""
Heuristic check whether the parent process (captured at construction
time) is a BEC IPython client. Only then does SIGINT make sense: when
the widget is opened from the standalone launcher instead of a
client-spawned GUI server, the parent is e.g. a shell or the launcher
process, which must not receive the signal.
"""
try:
with open(f"/proc/{self._client_pid}/cmdline", "rb") as f:
cmdline = f.read().replace(b"\x00", b" ").decode(errors="ignore")
except OSError:
return False
return ("bec" in cmdline) or ("ipython" in cmdline)
@SafeSlot()
def _on_abort(self):
logger.warning(f"ConsoleButtonsWidget: sending SIGINT to client pid {self._client_pid}")
os.kill(self._client_pid, signal.SIGINT)
if self._client_is_bec_process():
logger.warning(f"ConsoleButtonsWidget: sending SIGINT to client pid {self._client_pid}")
os.kill(self._client_pid, signal.SIGINT)
else:
logger.warning(
"ConsoleButtonsWidget: parent process does not look like a BEC client;"
" skipping SIGINT and only sending the device stop request."
)
# Backup: direct device stop via the device server, independent of
# the client process. Delayed so the SIGINT-triggered abort handler
# in the client (which still sees mntprgs=1 and aborts in a