From a9df4b5f06838fced7012a9316552b23f5bda734 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:12:28 +1000 Subject: [PATCH 1/2] hal: leave an existing SIGTERM handler alone Importing _hal points SIGTERM at default_int_handler, so that a halcmd unload interrupts a component. It did so unconditionally, replacing a handler the program had already installed; only take the signal when nobody has. --- src/hal/halmodule.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/hal/halmodule.cc b/src/hal/halmodule.cc index 3f581d7de4b..3447335bd9c 100644 --- a/src/hal/halmodule.cc +++ b/src/hal/halmodule.cc @@ -2391,8 +2391,12 @@ PyMODINIT_FUNC PyInit__hal(void) return NULL; } + // A halcmd unload arrives as SIGTERM; deliver it to a component as + // a KeyboardInterrupt. A program that has already set a handler of + // its own keeps it. PyRun_SimpleString( "(lambda s=__import__('signal'):" + "s.getsignal(s.SIGTERM) is s.SIG_DFL and " "s.signal(s.SIGTERM, s.default_int_handler))()"); return m; } From 5b27a22aae98ad6e5dc0c975ddcae73fd0e49d23 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:12:28 +1000 Subject: [PATCH 2/2] gmoccapy: make the startup SIGTERM handler's exit stick The SystemExit it raises surfaces at whatever statement the constructor is on, and a bare except there swallows it, after which the GUI runs on with nobody asking again. Have the handler record the request and act on it in the two places the process can end up afterwards: main() exits once construction returns, and the excepthook exits instead of showing its dialog, since a half-built object left behind by the swallowed exit tends to fail a few statements later. Both are ordinary exits with the exit handlers run. The icon loader catches Exception rather than BaseException. --- src/emc/usr_intf/gmoccapy/gmoccapy.py | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/emc/usr_intf/gmoccapy/gmoccapy.py b/src/emc/usr_intf/gmoccapy/gmoccapy.py index b25396ff62c..b7ea74ba7d3 100644 --- a/src/emc/usr_intf/gmoccapy/gmoccapy.py +++ b/src/emc/usr_intf/gmoccapy/gmoccapy.py @@ -34,7 +34,15 @@ # construction: a shutdown SIGTERM landing there would otherwise kill # the process mid-startup with no cleanup. Exit through SystemExit so # atexit handlers run. The full handler below replaces this one. +# The SystemExit surfaces at whatever statement the constructor is on, +# and a try/except around that statement can swallow it. The flag +# remembers the request, and main() honours it once construction +# returns, so the exit is still the ordinary one. +_terminate_requested = False + def _early_sigterm(signum, frame): + global _terminate_requested + _terminate_requested = True sys.exit(0) signal.signal(signal.SIGTERM, _early_sigterm) @@ -62,9 +70,8 @@ def _early_sigterm(signum, frame): # Throws up a dialog with debug info when an error is encountered def excepthook(exc_type, exc_obj, exc_tb): - # A KeyboardInterrupt reaching the excepthook is a termination request: - # either SIGINT, or SIGTERM surfaced into the main loop by PyGObject's - # signal bridge. Quit cleanly instead of popping a modal error dialog, + # A KeyboardInterrupt reaching the excepthook is a termination request + # (SIGINT). Quit cleanly instead of popping a modal error dialog, # which would block in a nested loop and leave gmoccapy running until the # caller escalates to SIGKILL. if issubclass(exc_type, KeyboardInterrupt): @@ -76,6 +83,12 @@ def excepthook(exc_type, exc_obj, exc_tb): # C boundary, so returning resumes startup. Force the exit. os._exit(0) return + # A SystemExit that a try/except in the constructor swallowed can leave + # a half-built object behind, and the error from that must not park + # the process in the modal dialog: the exit was already requested. + if _terminate_requested: + LOG.info("gmoccapy received SIGTERM during startup, shutting down") + sys.exit(0) try: w = app.widgets.window1 except Exception: @@ -5223,7 +5236,7 @@ def _set_icon_theme(self, name): pixbuf = icon_theme_helper.load_symbolic_from_icon_theme(self.icon_theme, icon_name, size, default_style) image.set_from_pixbuf(pixbuf) image.set_size_request(size, size) - except BaseException as err: + except Exception as err: LOG.warning(f"Failed to change icon for <{widget_name}> to '{icon_name}': {str(err)}") failed_icons += 1 @@ -6587,6 +6600,12 @@ def _make_hal_pins(self): # instantiate gmoccapy app = gmoccapy(sys.argv) + # A SIGTERM during construction that a try/except in there swallowed + # would otherwise leave the GUI running. + if _terminate_requested: + LOG.info("gmoccapy received SIGTERM during startup, shutting down") + sys.exit(0) + # get the INI path inifile = sys.argv[2]