From d6d991bb715c5affb93493cb4723897bc70211f3 Mon Sep 17 00:00:00 2001 From: Programer Date: Sun, 5 Jul 2026 14:42:59 +0200 Subject: [PATCH 01/12] NUT-Monitor: Add automatic reconnection mechanism for Qt5 and Qt6 - Add automatic reconnection when connection to NUT server is lost - Implement proper teardown of old PyNUTClient instances - Use specific exceptions (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) - Prevent multiple simultaneous reconnection attempts - Add 'was_online' flag to track connection state changes - Tested and confirmed working on both Qt5 and Qt6 Closes: #3509 Signed-off-by: Programer --- scripts/python/app/NUT-Monitor-py3qt5.in | 106 ++++++++++++++++- scripts/python/app/NUT-Monitor-py3qt6.in | 145 ++++++++++++++++++----- 2 files changed, 217 insertions(+), 34 deletions(-) mode change 100755 => 100644 scripts/python/app/NUT-Monitor-py3qt5.in mode change 100755 => 100644 scripts/python/app/NUT-Monitor-py3qt6.in diff --git a/scripts/python/app/NUT-Monitor-py3qt5.in b/scripts/python/app/NUT-Monitor-py3qt5.in old mode 100755 new mode 100644 index 727dd60763..380f47d818 --- a/scripts/python/app/NUT-Monitor-py3qt5.in +++ b/scripts/python/app/NUT-Monitor-py3qt5.in @@ -43,6 +43,7 @@ from PyQt5.QtCore import * from PyQt5.QtGui import * from PyQt5.QtWidgets import * import sys +import socket import base64 import os, os.path import stat @@ -53,6 +54,7 @@ import optparse import configparser import locale import gettext +import PyNUT # Try local development/accompanying packaging first, # then system installation @@ -407,8 +409,11 @@ class interface : def quit( self, widget=None ) : # If we are connected to an UPS, disconnect first... if self.__connected : - self.gui_status_message( _("Disconnecting from device") ) - self.disconnect_from_ups() + try: + self.gui_status_message( _("Disconnecting from device") ) + self.disconnect_from_ups() + except RuntimeError: + pass # widgets may already be deleted during shutdown self.__app.quit() @@ -895,6 +900,7 @@ class gui_updater : __parent_class = None __stop_thread = False was_online = True + __reconnect_attempted = False def __init__( self, parent_class ) : threading.Thread.__init__( self ) @@ -1031,12 +1037,100 @@ class gui_updater : # Display UPS status as tooltip for tray icon self.__parent_class._interface__widgets["status_icon"].setToolTip( "\n".join( tooltip_lines ) ) - except : - self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) ) - self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" ) + except (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) as e: + # Connection error detected — attempt automatic reconnection + if not self.__reconnect_attempted and self.__parent_class._interface__connected: + self.__reconnect_attempted = True + self.__parent_class.gui_status_message( _("Connection lost — attempting to reconnect...") ) + self.__parent_class.gui_status_notification( _("Connection lost"), "warning.png" ) + + # Stop the current timer + self.__timer.stop() + + # Schedule reconnection attempt after 2 seconds + QTimer.singleShot(2000, self.__attempt_reconnect) + else: + self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) ) + self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" ) + + def __attempt_reconnect( self ) : + """Attempt to reconnect to the UPS after a connection loss.""" + if self.__stop_thread: + return + + try: + # Get connection parameters from the parent class + host = self.__parent_class._interface__widgets["ups_host_entry"].text() + port = int(self.__parent_class._interface__widgets["ups_port_entry"].value()) + login = None + password = None + + if self.__parent_class._interface__widgets["ups_authentication_check"].isChecked(): + login = self.__parent_class._interface__widgets["ups_authentication_login"].text() + password = self.__parent_class._interface__widgets["ups_authentication_password"].text() + + # Teardown old connection handler properly + old_handler = self.__parent_class._interface__ups_handler + if old_handler is not None: + try: + # Close the underlying telnet connection if it exists + if hasattr(old_handler, '_PyNUTClient__tn') and old_handler._PyNUTClient__tn is not None: + old_handler._PyNUTClient__tn.close() + elif hasattr(old_handler, 'tn') and old_handler.tn is not None: + old_handler.tn.close() + except Exception: + pass # Best effort cleanup + del old_handler + + # Create a new connection handler + ups_handler = PyNUT.PyNUTClient(host=host, port=port, login=login, password=password) + + # Verify the UPS is available + ups = self.__parent_class._interface__current_ups + srv_upses = ups_handler.GetUPSList() + + if ups.encode('ascii') not in srv_upses: + self.__parent_class.gui_status_message( _("UPS '{0}' not found after reconnection").format(ups) ) + self.__reconnect_attempted = False + # Restart the timer to continue polling + self.__timer.start(1000) + return + + if not ups_handler.CheckUPSAvailable(ups): + self.__parent_class.gui_status_message( _("UPS '{0}' is still not reachable").format(ups) ) + self.__reconnect_attempted = False + self.__timer.start(1000) + return + + # Connection successful — update the parent class + self.__parent_class._interface__ups_handler = ups_handler + self.__parent_class._interface__connected = True + self.__reconnect_attempted = False + + # Refresh UPS data + commands = ups_handler.GetUPSCommands(ups) + self.__parent_class._interface__ups_commands = list(commands.keys()) + self.__parent_class._interface__ups_commands.sort() + + self.__parent_class._interface__ups_vars = ups_handler.GetUPSVars(ups) + self.__parent_class._interface__ups_rw_vars = ups_handler.GetRWVars(ups) + + # Update the GUI + self.__parent_class._interface__gui_update_ups_vars_view() + self.__parent_class.change_status_icon("on_line", blink=False) + self.__parent_class.gui_status_message( _("Reconnected to '{0}' on {1}").format(ups, host) ) + self.__parent_class.gui_status_notification( _("Reconnected to UPS"), "on_line.png" ) + + except (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) as e: + self.__parent_class.gui_status_message( _("Reconnection failed: {0}").format(str(e)) ) + self.__reconnect_attempted = False + + # Restart the timer to continue polling + self.__timer.start(1000) def stop_thread( self ) : - self.__timer.stop() + if hasattr(self, '_gui_updater__timer'): + self.__timer.stop() #----------------------------------------------------------------------- diff --git a/scripts/python/app/NUT-Monitor-py3qt6.in b/scripts/python/app/NUT-Monitor-py3qt6.in old mode 100755 new mode 100644 index f3141cc9da..4a6125f74a --- a/scripts/python/app/NUT-Monitor-py3qt6.in +++ b/scripts/python/app/NUT-Monitor-py3qt6.in @@ -47,6 +47,8 @@ from PyQt6.QtGui import * from PyQt6.QtWidgets import * import sys +import socket +import threading import base64 import os, os.path import stat @@ -132,7 +134,6 @@ class interface : __ups_rw_vars = None __gui_updater = None __current_ups = None - __quitting = False def __init__( self, argv ) : @@ -411,14 +412,13 @@ class interface : #------------------------------------------------------------------- # Quit program def quit( self, widget=None ) : - if self.__quitting: - return - self.__quitting = True - # If we are connected to an UPS, disconnect first... - if self.__widgets.get("main_window") and self.__connected : - self.gui_status_message( _("Disconnecting from device") ) - self.disconnect_from_ups() + if self.__connected : + try: + self.gui_status_message( _("Disconnecting from device") ) + self.disconnect_from_ups() + except RuntimeError: + pass # widgets may already be deleted during shutdown self.__app.quit() @@ -884,8 +884,6 @@ along with this program. If not, see . def gui_init_unconnected( self ) : - if self.__quitting: - return if not self.__widgets.get("main_window") or not self.__widgets.get("status_icon"): return @@ -908,8 +906,7 @@ along with this program. If not, see . self.gui_init_unconnected() # Stop the GUI updater - self.__gui_updater.stop() - self.__gui_updater = None + self.__gui_updater.stop_thread() del self.__ups_handler self.gui_status_message( _("Disconnected from '%s'") % self.__current_ups ) @@ -922,10 +919,15 @@ along with this program. If not, see . class gui_updater : __parent_class = None - __stop_updater = False + __stop_thread = False + was_online = True + __reconnect_attempted = False def __init__( self, parent_class ) : self.__parent_class = parent_class + self.__reconnect_attempted = False + + def start( self ) : self.__timer = QTimer() @@ -949,7 +951,7 @@ class gui_updater : b"BOOST" : ( _("Boost (UPS is boosting incoming voltage)"), plain_status_text( _("Boost (UPS is boosting incoming voltage)") ) ) } - if ups and not self.__stop_updater and not self.__parent_class._interface__quitting : + if not self.__stop_thread : try : vars = self.__parent_class._interface__ups_handler.GetUPSVars( ups ) self.__parent_class._interface__ups_vars = vars @@ -966,18 +968,17 @@ class gui_updater : if ( vars.get(b"ups.status").find(b"OL") != -1 ) : status_parts.append( "%s" % _("Online") ) tooltip_status_parts.append( _("Online") ) - if self.__parent_class._interface__online is not True : + if not self.was_online : self.__parent_class.change_status_icon( "on_line", blink=False ) - self.__parent_class.gui_status_notification( _("Device is online"), "on_line.png" ) - self.__parent_class._interface__online = True + self.was_online = True if ( vars.get(b"ups.status").find(b"OB") != -1 ) : status_parts.append( "%s" % _("On batteries") ) tooltip_status_parts.append( _("On batteries") ) - if self.__parent_class._interface__online is not False: + if self.was_online : self.__parent_class.change_status_icon( "on_battery", blink=True ) self.__parent_class.gui_status_notification( _("Device is running on batteries"), "on_battery.png" ) - self.__parent_class._interface__online = False + self.was_online = False # Check for additionnal information for k,v in status_mapper.items() : @@ -1059,15 +1060,103 @@ class gui_updater : # Display UPS status as tooltip for tray icon self.__parent_class._interface__widgets["status_icon"].setToolTip( "\n".join( tooltip_lines ) ) - except : - self.__parent_class._interface__online = None - self.__parent_class.change_status_icon( "warning", blink=True ) - self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) ) - self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" ) - - def stop( self ) : - self.__timer.stop() - self.__stop_updater = True + except (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) as e: + # Connection error detected \u2014 attempt automatic reconnection + if not self.__reconnect_attempted and self.__parent_class._interface__connected: + self.__reconnect_attempted = True + self.__parent_class.gui_status_message( _("Connection lost \u2014 attempting to reconnect...") ) + self.__parent_class.gui_status_notification( _("Connection lost"), "warning.png" ) + + # Stop the current timer + self.__timer.stop() + + # Schedule reconnection attempt after 2 seconds + QTimer.singleShot(2000, self.__attempt_reconnect) + else: + self.__parent_class._interface__online = None + self.__parent_class.change_status_icon( "warning", blink=True ) + self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) ) + self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" ) + + def __attempt_reconnect( self ) : + """Attempt to reconnect to the UPS after a connection loss.""" + if self.__stop_thread: + return + + try: + # Get connection parameters from the parent class + host = self.__parent_class._interface__widgets["ups_host_entry"].text() + port = int(self.__parent_class._interface__widgets["ups_port_entry"].value()) + login = None + password = None + + if self.__parent_class._interface__widgets["ups_authentication_check"].isChecked(): + login = self.__parent_class._interface__widgets["ups_authentication_login"].text() + password = self.__parent_class._interface__widgets["ups_authentication_password"].text() + + # Teardown old connection handler properly + old_handler = self.__parent_class._interface__ups_handler + if old_handler is not None: + try: + # Close the underlying telnet connection if it exists + if hasattr(old_handler, '_PyNUTClient__tn') and old_handler._PyNUTClient__tn is not None: + old_handler._PyNUTClient__tn.close() + elif hasattr(old_handler, 'tn') and old_handler.tn is not None: + old_handler.tn.close() + except Exception: + pass # Best effort cleanup + del old_handler + + # Create a new connection handler + ups_handler = PyNUT.PyNUTClient(host=host, port=port, login=login, password=password) + + # Verify the UPS is available + ups = self.__parent_class._interface__current_ups + srv_upses = ups_handler.GetUPSList() + + if ups.encode('ascii') not in srv_upses: + self.__parent_class.gui_status_message( _("UPS '{0}' not found after reconnection").format(ups) ) + self.__reconnect_attempted = False + # Restart the timer to continue polling + self.__timer.start(1000) + return + + if not ups_handler.CheckUPSAvailable(ups): + self.__parent_class.gui_status_message( _("UPS '{0}' is still not reachable").format(ups) ) + self.__reconnect_attempted = False + self.__timer.start(1000) + return + + # Connection successful \u2014 update the parent class + self.__parent_class._interface__ups_handler = ups_handler + self.__parent_class._interface__connected = True + self.__reconnect_attempted = False + + # Refresh UPS data + commands = ups_handler.GetUPSCommands(ups) + self.__parent_class._interface__ups_commands = list(commands.keys()) + self.__parent_class._interface__ups_commands.sort() + + self.__parent_class._interface__ups_vars = ups_handler.GetUPSVars(ups) + self.__parent_class._interface__ups_rw_vars = ups_handler.GetRWVars(ups) + + # Update the GUI + self.__parent_class._interface__gui_update_ups_vars_view() + self.__parent_class.change_status_icon("on_line", blink=False) + self.__parent_class.gui_status_message( _("Reconnected to '{0}' on {1}").format(ups, host) ) + self.__parent_class.gui_status_notification( _("Reconnected to UPS"), "on_line.png" ) + + except (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) as e: + self.__parent_class.gui_status_message( _("Reconnection failed: {0}").format(str(e)) ) + self.__reconnect_attempted = False + + # Restart the timer to continue polling + self.__timer.start(1000) + + def stop_thread( self ) : + if hasattr(self, '_gui_updater__timer'): + self.__timer.stop() + self.__stop_thread = True #----------------------------------------------------------------------- From 1996cf0ae283eea34b7916f019abed5c103b1412 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 15 Jul 2026 19:27:41 +0200 Subject: [PATCH 02/12] NEWS.adoc: NUT-Monitor updated with automatic reconnection [#3509, #3523] Signed-off-by: Jim Klimov --- NEWS.adoc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/NEWS.adoc b/NEWS.adoc index 4ee2dd91e7..92e173c942 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -246,6 +246,9 @@ https://github.com/networkupstools/nut/milestone/13 * Fixed Qt tray tooltips to render in plain text, not rich text which ended up as literal HTML on KDE Plasma 6; now that rich text formatting is only used for main window status labels. [PR #3430] + * Fixed Qt (Python 3) variants to handle automatic client reconnection + (e.g. after system suspend/resume, network interruption, or NUT server + restart). [issue #3509, PR #3523] - `upssched` client/tool updates: * Added a `clean_exit()` handler similar to that in `upsmon`. [PR #3499] From f6207eb23e6a17be18b2aa96f9c06f1aa9095c4d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 15 Jul 2026 19:52:16 +0200 Subject: [PATCH 03/12] scripts/python/app/NUT-Monitor-py2gtk2.in: propose a fix for reconnection modeled after PR #3523 for Python3 siblings [#3509] Co-Authored-By: Programer Signed-off-by: Jim Klimov --- NEWS.adoc | 2 +- scripts/python/app/NUT-Monitor-py2gtk2.in | 96 +++++++++++++++++++++-- 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/NEWS.adoc b/NEWS.adoc index 92e173c942..71074969c3 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -246,7 +246,7 @@ https://github.com/networkupstools/nut/milestone/13 * Fixed Qt tray tooltips to render in plain text, not rich text which ended up as literal HTML on KDE Plasma 6; now that rich text formatting is only used for main window status labels. [PR #3430] - * Fixed Qt (Python 3) variants to handle automatic client reconnection + * Fixed both Python 2 and 3 variants to handle automatic client reconnection (e.g. after system suspend/resume, network interruption, or NUT server restart). [issue #3509, PR #3523] diff --git a/scripts/python/app/NUT-Monitor-py2gtk2.in b/scripts/python/app/NUT-Monitor-py2gtk2.in index 3e0c94f314..d5876fffea 100755 --- a/scripts/python/app/NUT-Monitor-py2gtk2.in +++ b/scripts/python/app/NUT-Monitor-py2gtk2.in @@ -34,6 +34,7 @@ import gtk, gtk.glade, gobject import sys +import socket import base64 import os, os.path import stat @@ -116,6 +117,7 @@ class interface : __ups_rw_vars = None __gui_thread = None __current_ups = None + __reconnect_attempted = False def __init__( self ) : @@ -389,8 +391,11 @@ class interface : def quit( self, widget=None ) : # If we are connected to an UPS, disconnect first... if self.__connected : - self.gui_status_message( _("Disconnecting from device") ) - self.disconnect_from_ups() + try: + self.gui_status_message( _("Disconnecting from device") ) + self.disconnect_from_ups() + except Exception: + pass gtk.main_quit() @@ -746,6 +751,7 @@ class interface : # Connect to the selected UPS using parameters (host,port,login,pass) def connect_to_ups( self, widget=None ) : + self.__reconnect_attempted = False host = self.__widgets["ups_host_entry"].get_text() port = int( self.__widgets["ups_port_entry"].get_value() ) login = None @@ -862,6 +868,7 @@ class gui_updater( threading.Thread ) : __parent_class = None __stop_thread = False was_online = True + __reconnect_attempted = False def __init__( self, parent_class ) : threading.Thread.__init__( self ) @@ -977,12 +984,91 @@ class gui_updater( threading.Thread ) : # Display UPS status as tooltip for tray icon self.__parent_class._interface__widgets["status_icon"].set_tooltip_markup( status_text ) - except : - self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) ) - self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" ) + except (socket.error, EOFError, PyNUT.PyNUTError) as e: + # Connection error detected - attempt automatic reconnection + if not self.__reconnect_attempted and self.__parent_class._interface__connected: + self.__reconnect_attempted = True + self.__parent_class.gui_status_message( _("Connection lost - attempting to reconnect...") ) + self.__parent_class.gui_status_notification( _("Connection lost"), "warning.png" ) + + # Wait for 2 seconds before attempting reconnection + time.sleep( 2 ) + self.__attempt_reconnect() + else: + self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) ) + self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" ) time.sleep( 1 ) + def __attempt_reconnect( self ) : + """Attempt to reconnect to the UPS after a connection loss.""" + if self.__stop_thread: + return + + try: + # Get connection parameters from the parent class + host = self.__parent_class._interface__widgets["ups_host_entry"].get_text() + port = int(self.__parent_class._interface__widgets["ups_port_entry"].get_value()) + login = None + password = None + + if self.__parent_class._interface__widgets["ups_authentication_check"].get_active(): + login = self.__parent_class._interface__widgets["ups_authentication_login"].get_text() + password = self.__parent_class._interface__widgets["ups_authentication_password"].get_text() + + # Teardown old connection handler properly + old_handler = self.__parent_class._interface__ups_handler + if old_handler is not None: + try: + # Close the underlying telnet connection if it exists + if hasattr(old_handler, '_PyNUTClient__tn') and old_handler._PyNUTClient__tn is not None: + old_handler._PyNUTClient__tn.close() + elif hasattr(old_handler, 'tn') and old_handler.tn is not None: + old_handler.tn.close() + except Exception: + pass # Best effort cleanup + del old_handler + + # Create a new connection handler + ups_handler = PyNUT.PyNUTClient(host=host, port=port, login=login, password=password) + + # Verify the UPS is available + ups = self.__parent_class._interface__current_ups + srv_upses = ups_handler.GetUPSList() + + if ups not in srv_upses: + self.__parent_class.gui_status_message( _("UPS '{0}' not found after reconnection").format(ups) ) + self.__reconnect_attempted = False + return + + if not ups_handler.CheckUPSAvailable(ups): + self.__parent_class.gui_status_message( _("UPS '{0}' is still not reachable").format(ups) ) + self.__reconnect_attempted = False + return + + # Connection successful - update the parent class + self.__parent_class._interface__ups_handler = ups_handler + self.__parent_class._interface__connected = True + self.__reconnect_attempted = False + + # Refresh UPS data + commands = ups_handler.GetUPSCommands(ups) + self.__parent_class._interface__ups_commands = list(commands.keys()) + self.__parent_class._interface__ups_commands.sort() + + self.__parent_class._interface__ups_vars = ups_handler.GetUPSVars(ups) + self.__parent_class._interface__ups_rw_vars = ups_handler.GetRWVars(ups) + + # Update the GUI (must be done on the main thread in GTK) + gobject.idle_add( self.__parent_class._interface__gui_update_ups_vars_view ) + gobject.idle_add( self.__parent_class.change_status_icon, "on_line", False ) + self.__parent_class.gui_status_message( _("Reconnected to '{0}' on {1}").format(ups, host) ) + self.__parent_class.gui_status_notification( _("Reconnected to UPS"), "on_line.png" ) + + except (socket.error, EOFError, PyNUT.PyNUTError) as e: + self.__parent_class.gui_status_message( _("Reconnection failed: {0}").format(str(e)) ) + self.__reconnect_attempted = False + def stop_thread( self ) : self.__stop_thread = True From f46092e9374f76043c35bcf45f23cce6f1cfe194 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 12:24:40 +0200 Subject: [PATCH 04/12] scripts/python/app/NUT-Monitor-py3qt{5,6}.in: mark the templates as nominally executable Signed-off-by: Jim Klimov --- scripts/python/app/NUT-Monitor-py3qt5.in | 0 scripts/python/app/NUT-Monitor-py3qt6.in | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/python/app/NUT-Monitor-py3qt5.in mode change 100644 => 100755 scripts/python/app/NUT-Monitor-py3qt6.in diff --git a/scripts/python/app/NUT-Monitor-py3qt5.in b/scripts/python/app/NUT-Monitor-py3qt5.in old mode 100644 new mode 100755 diff --git a/scripts/python/app/NUT-Monitor-py3qt6.in b/scripts/python/app/NUT-Monitor-py3qt6.in old mode 100644 new mode 100755 From 3e675fedbbec7f8a38b477e20bb85f00d9a42226 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 12:57:24 +0200 Subject: [PATCH 05/12] scripts/python/module/PyNUT.py.in: __read_until(): handle the case of un-received chunk [#3509] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 3296d082b4..783a6c9e8f 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -721,7 +721,10 @@ certhost : Optional dict or list of dict/lists: {hostname: [certname, certver while True: data_end_index = data.find(finished_reading_data) if data_end_index == -1: - data += self.__srv_handler.recv(50) # nut_telnetlib.py uses 50 + chunk = self.__srv_handler.recv(50) # nut_telnetlib.py uses 50 + if not chunk: + raise EOFError("Connection closed by server") + data += chunk else: break data_end_index += len(finished_reading_data) From 1b5e77c1116f55057ae165effcf72fe1c9fdbed5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 13:04:21 +0200 Subject: [PATCH 06/12] scripts/python/module/PyNUT.py.in: refactor with an error-logging __send() [#3509] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 65 ++++++++++++++++++------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 783a6c9e8f..aba346dcfc 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -712,10 +712,19 @@ certhost : Optional dict or list of dict/lists: {hostname: [certname, certver def __del__( self ) : """ Class destructor method """ try : - self.__srv_handler.send( b"LOGOUT\n" ) + self.__send( b"LOGOUT\n" ) except : pass + def __send(self, data): + """ Internal method to send data through the socket with error handling """ + try: + self.__srv_handler.sendall(data) + except (AttributeError, socket.error) as e: + if self.__debug: + print("[DEBUG] Send failed: %s" % str(e)) + raise e + def __read_until(self, finished_reading_data): data = self.__recv_leftover while True: @@ -753,7 +762,7 @@ if something goes wrong. if self.__use_ssl : if self.__debug : print( "[DEBUG] Requesting STARTTLS" ) - self.__srv_handler.send( b"STARTTLS\n" ) + self.__send( b"STARTTLS\n" ) result = self.__read_until( b"\n" ) if result[:11] == b"OK STARTTLS" : if self.__debug : @@ -806,19 +815,19 @@ if something goes wrong. self.__use_ssl = False if self.__login != None : - self.__srv_handler.send( ("USERNAME %s\n" % self.__login).encode('ascii') ) + self.__send( ("USERNAME %s\n" % self.__login).encode('ascii') ) result = self.__read_until( b"\n" ) if result[:2] != b"OK" : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) if self.__password != None : - self.__srv_handler.send( ("PASSWORD %s\n" % self.__password).encode('ascii') ) + self.__send( ("PASSWORD %s\n" % self.__password).encode('ascii') ) result = self.__read_until( b"\n" ) if result[:2] != b"OK" : if result == b"ERR INVALID-ARGUMENT\n" : # Quote the password (if it has whitespace etc) # TODO: Escape special chard like NUT does? - self.__srv_handler.send( ("PASSWORD \"%s\"\n" % self.__password).encode('ascii') ) + self.__send( ("PASSWORD \"%s\"\n" % self.__password).encode('ascii') ) result = self.__read_until( b"\n" ) if result[:2] != b"OK" : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) @@ -849,7 +858,7 @@ if something goes wrong. # TOTHINK # raise PyNUTError( "Invalid setting for TRACKING mode was requested" ) return None - self.__srv_handler.send( ("SET TRACKING %s\n" % value).encode('ascii') ) + self.__send( ("SET TRACKING %s\n" % value).encode('ascii') ) result = self.__read_until( b"\n" ) if result == b"OK\n" : self.__tracking = value @@ -879,7 +888,7 @@ if something goes wrong. if self.__debug : print( "[DEBUG] GetTrackingResult for %s" % tracking_id ) - self.__srv_handler.send( ("GET TRACKING %s\n" % tracking_id).encode('ascii') ) + self.__send( ("GET TRACKING %s\n" % tracking_id).encode('ascii') ) result = self.__read_until( b"\n" ) if result in [b"PENDING\n", b"SUCCESS\n"]: return result.replace( b"\n", b"" ).decode('ascii') @@ -929,12 +938,14 @@ if something goes wrong. """ result = None try: - self.__srv_handler.send( ("PROTVER\n").encode('ascii') ) + self.__send( ("PROTVER\n").encode('ascii') ) result = self.__read_until( b"\n" ) except PyNUTError as ignored: # Deprecated and hidden, but may be what ancient NUT servers say # May throw if the error is due to (non-)connection - self.__srv_handler.send( ("NETVER\n").encode('ascii') ) + # Here we assume that any other exceptions are due to broken + # connection etc. and forward them to caller right away. + self.__send( ("NETVER\n").encode('ascii') ) result = self.__read_until( b"\n" ) # print ( "[DEBUG] isValidProtocolVersion: version_re='%s' result='%s'" % (str(version_re), str(result))) @@ -960,7 +971,7 @@ if something goes wrong. if self.__debug : print( "[DEBUG] GetVariableType for %s / %s" % ( ups, var ) ) - self.__srv_handler.send( ("GET TYPE %s %s\n" % ( ups, var )).encode('ascii') ) + self.__send( ("GET TYPE %s %s\n" % ( ups, var )).encode('ascii') ) result = self.__read_until( b"\n" ) if result[:4] == b"TYPE" : # TYPE @@ -975,7 +986,7 @@ if something goes wrong. if self.__debug : print( "[DEBUG] GetVariableDescription for %s / %s" % ( ups, var ) ) - self.__srv_handler.send( ("GET DESC %s %s\n" % ( ups, var )).encode('ascii') ) + self.__send( ("GET DESC %s %s\n" % ( ups, var )).encode('ascii') ) result = self.__read_until( b"\n" ) if result[:4] == b"DESC" : # DESC "" @@ -990,7 +1001,7 @@ if something goes wrong. if self.__debug : print( "[DEBUG] GetEnumList for %s / %s" % ( ups, var ) ) - self.__srv_handler.send( ("LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') ) + self.__send( ("LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') ) result = self.__read_until( b"\n" ) if result != ("BEGIN LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) @@ -1011,7 +1022,7 @@ if something goes wrong. if self.__debug : print( "[DEBUG] GetRangeList for %s / %s" % ( ups, var ) ) - self.__srv_handler.send( ("LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') ) + self.__send( ("LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') ) result = self.__read_until( b"\n" ) if result != ("BEGIN LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) @@ -1034,7 +1045,7 @@ if something goes wrong. if self.__debug : print( "[DEBUG] GetDeviceNumLogins for %s" % ups ) - self.__srv_handler.send( ("GET NUMLOGINS %s\n" % ups).encode('ascii') ) + self.__send( ("GET NUMLOGINS %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) if result[:9] == b"NUMLOGINS" : # NUMLOGINS @@ -1054,7 +1065,7 @@ which is of little concern for Python2 but is important in Python3 if self.__debug : print( "[DEBUG] GetUPSList from server" ) - self.__srv_handler.send( b"LIST UPS\n" ) + self.__send( b"LIST UPS\n" ) result = self.__read_until( b"\n" ) if result != b"BEGIN LIST UPS\n": raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) @@ -1094,7 +1105,7 @@ available vars. if self.__debug : print( "[DEBUG] GetUPSVars called..." ) - self.__srv_handler.send( ("LIST VAR %s\n" % ups).encode('ascii') ) + self.__send( ("LIST VAR %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) if result != ("BEGIN LIST VAR %s\n" % ups).encode('ascii') : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) @@ -1120,7 +1131,7 @@ The result is True (reachable) or False (unreachable) if self.__debug : print( "[DEBUG] CheckUPSAvailable called..." ) - self.__srv_handler.send( ("LIST CMD %s\n" % ups).encode('ascii') ) + self.__send( ("LIST CMD %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) if result != ("BEGIN LIST CMD %s\n" % ups).encode('ascii') : return False @@ -1137,7 +1148,7 @@ of the command as value if self.__debug : print( "[DEBUG] GetUPSCommands called..." ) - self.__srv_handler.send( ("LIST CMD %s\n" % ups).encode('ascii') ) + self.__send( ("LIST CMD %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) if result != ("BEGIN LIST CMD %s\n" % ups).encode('ascii') : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) @@ -1152,7 +1163,7 @@ of the command as value # For each var we try to get the available description try : - self.__srv_handler.send( ("GET CMDDESC %s %s\n" % ( ups, var )).encode('ascii') ) + self.__send( ("GET CMDDESC %s %s\n" % ( ups, var )).encode('ascii') ) temp = self.__read_until( b"\n" ) if temp[:7] != b"CMDDESC" : raise PyNUTError @@ -1208,7 +1219,7 @@ rights to set it (maybe login/password). do_wait = True self.EnableTrackingModeOnce() - self.__srv_handler.send( ("SET VAR %s %s %s\n" % ( ups, var, value )).encode('ascii') ) + self.__send( ("SET VAR %s %s %s\n" % ( ups, var, value )).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result == b"OK\n" ) : return( "OK" ) @@ -1240,7 +1251,7 @@ Returns OK on success or raises an error do_wait = True self.EnableTrackingModeOnce() - self.__srv_handler.send( ("INSTCMD %s %s\n" % ( ups, command )).encode('ascii') ) + self.__send( ("INSTCMD %s %s\n" % ( ups, command )).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result == b"OK\n" ) : return( "OK" ) @@ -1281,7 +1292,7 @@ of connection. print( "[DEBUG] DeviceLogin: %s is not a valid UPS" % ups ) raise PyNUTError( "ERR UNKNOWN-UPS" ) - self.__srv_handler.send( ("LOGIN %s\n" % ups).encode('ascii') ) + self.__send( ("LOGIN %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result.startswith( ("User %s@" % self.__login).encode('ascii')) and result.endswith (("[%s]\n" % ups).encode('ascii')) ): # User dummy-user@127.0.0.1 logged into UPS [dummy] @@ -1303,14 +1314,14 @@ NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY if self.__debug : print( "[DEBUG] PRIMARY called..." ) - self.__srv_handler.send( ("PRIMARY %s\n" % ups).encode('ascii') ) + self.__send( ("PRIMARY %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result == b"OK PRIMARY-GRANTED\n" ) : return( "OK" ) if self.__debug : print( "[DEBUG] Retrying: MASTER called..." ) - self.__srv_handler.send( ("MASTER %s\n" % ups).encode('ascii') ) + self.__send( ("MASTER %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result == b"OK MASTER-GRANTED\n" ) : return( "OK" ) @@ -1331,7 +1342,7 @@ Returns OK on success or raises an error if self.__debug : print( "[DEBUG] FSD called..." ) - self.__srv_handler.send( ("FSD %s\n" % ups).encode('ascii') ) + self.__send( ("FSD %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result == b"OK FSD-SET\n" ) : return( "OK" ) @@ -1374,11 +1385,11 @@ The result is a dictionary containing 'key->val' pairs of 'UPSName' and a list o raise PyNUTError( "ERR UNKNOWN-UPS" ) if ups: - self.__srv_handler.send( ("LIST CLIENT %s\n" % ups).encode('ascii') ) + self.__send( ("LIST CLIENT %s\n" % ups).encode('ascii') ) else: # NOTE: Currently NUT does not support just listing all clients # (not providing an "ups" argument) => NUT_ERR_INVALID_ARGUMENT - self.__srv_handler.send( b"LIST CLIENT\n" ) + self.__send( b"LIST CLIENT\n" ) result = self.__read_until( b"\n" ) if ( (ups and result != ("BEGIN LIST CLIENT %s\n" % ups).encode('ascii')) or (ups is None and result != b"BEGIN LIST CLIENT\n") ): if ups is None and (result == b"ERR INVALID-ARGUMENT\n") : From 575887401f2f06026407bac8fe335c0b4ced95d9 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 13:05:22 +0200 Subject: [PATCH 07/12] scripts/python/app/NUT-Monitor-py3qt{5,6}.in: fix "Broken pipe" and possible stall in reconnection attempts [#3509] Signed-off-by: Jim Klimov --- scripts/python/app/NUT-Monitor-py3qt5.in | 10 ++++++++++ scripts/python/app/NUT-Monitor-py3qt6.in | 11 ++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/scripts/python/app/NUT-Monitor-py3qt5.in b/scripts/python/app/NUT-Monitor-py3qt5.in index 380f47d818..024abe788f 100755 --- a/scripts/python/app/NUT-Monitor-py3qt5.in +++ b/scripts/python/app/NUT-Monitor-py3qt5.in @@ -1047,8 +1047,17 @@ class gui_updater : # Stop the current timer self.__timer.stop() + # Close the socket to avoid "Broken pipe" in subsequent calls if any + try: + self.__parent_class._interface__ups_handler._PyNUTClient__srv_handler.close() + except Exception: + pass + # Schedule reconnection attempt after 2 seconds QTimer.singleShot(2000, self.__attempt_reconnect) + elif self.__reconnect_attempted: + # Reconnection already in progress, just ignore this update + pass else: self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) ) self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" ) @@ -1129,6 +1138,7 @@ class gui_updater : self.__timer.start(1000) def stop_thread( self ) : + self.__stop_thread = True if hasattr(self, '_gui_updater__timer'): self.__timer.stop() diff --git a/scripts/python/app/NUT-Monitor-py3qt6.in b/scripts/python/app/NUT-Monitor-py3qt6.in index 4a6125f74a..5a58382f2d 100755 --- a/scripts/python/app/NUT-Monitor-py3qt6.in +++ b/scripts/python/app/NUT-Monitor-py3qt6.in @@ -1070,8 +1070,17 @@ class gui_updater : # Stop the current timer self.__timer.stop() + # Close the socket to avoid "Broken pipe" in subsequent calls if any + try: + self.__parent_class._interface__ups_handler._PyNUTClient__srv_handler.close() + except Exception: + pass + # Schedule reconnection attempt after 2 seconds QTimer.singleShot(2000, self.__attempt_reconnect) + elif self.__reconnect_attempted: + # Reconnection already in progress, just ignore this update + pass else: self.__parent_class._interface__online = None self.__parent_class.change_status_icon( "warning", blink=True ) @@ -1154,9 +1163,9 @@ class gui_updater : self.__timer.start(1000) def stop_thread( self ) : + self.__stop_thread = True if hasattr(self, '_gui_updater__timer'): self.__timer.stop() - self.__stop_thread = True #----------------------------------------------------------------------- From 60f3e396c451e69b8f04de1b677ea962985f8b29 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 14:06:24 +0200 Subject: [PATCH 08/12] scripts/python/app/NUT-Monitor-py2gtk2.in: gui_status_message(): fix indentation [#3509] Signed-off-by: Jim Klimov --- scripts/python/app/NUT-Monitor-py2gtk2.in | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/python/app/NUT-Monitor-py2gtk2.in b/scripts/python/app/NUT-Monitor-py2gtk2.in index d5876fffea..8af49409d5 100755 --- a/scripts/python/app/NUT-Monitor-py2gtk2.in +++ b/scripts/python/app/NUT-Monitor-py2gtk2.in @@ -710,16 +710,19 @@ class interface : # Display a message on the status bar. The message is also set as # tooltip to enable users to see long messages. def gui_status_message( self, msg="" ) : - context_id = self.__widgets["status_bar"].get_context_id("Infos") - self.__widgets["status_bar"].pop( context_id ) + try: + context_id = self.__widgets["status_bar"].get_context_id("Infos") + self.__widgets["status_bar"].pop( context_id ) - if ( platform.system() == "Windows" ) : - text = msg.decode("cp1250").encode("utf8") - else : - text = msg + if ( platform.system() == "Windows" ) : + text = msg.decode("cp1250").encode("utf8") + else : + text = msg - message_id = self.__widgets["status_bar"].push( context_id, text.replace("\n", "") ) - self.__widgets["status_bar"].set_tooltip_text( text ) + message_id = self.__widgets["status_bar"].push( context_id, text.replace("\n", "") ) + self.__widgets["status_bar"].set_tooltip_text( text ) + except : + pass #------------------------------------------------------------------- # Display a notification using PyNotify with an optional icon From e0c49207b816db503c62e3406c67488052c28be3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 14:08:32 +0200 Subject: [PATCH 09/12] scripts/python/app/NUT-Monitor-py3qt{5,6}.in: refactor gui_status_message() with try/except handling [#3509] Signed-off-by: Jim Klimov --- scripts/python/app/NUT-Monitor-py3qt5.in | 26 ++++++++++------ scripts/python/app/NUT-Monitor-py3qt6.in | 38 ++++++++++++++---------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/scripts/python/app/NUT-Monitor-py3qt5.in b/scripts/python/app/NUT-Monitor-py3qt5.in index 024abe788f..48d6afe748 100755 --- a/scripts/python/app/NUT-Monitor-py3qt5.in +++ b/scripts/python/app/NUT-Monitor-py3qt5.in @@ -753,20 +753,28 @@ along with this program. If not, see . # Display a message on the status bar. The message is also set as # tooltip to enable users to see long messages. def gui_status_message( self, msg="" ) : - text = msg - - message_id = self.__widgets["status_bar"].showMessage( text.replace("\n", "") ) - self.__widgets["status_bar"].setToolTip( text ) + try: + text = msg + if self.__widgets.get("status_bar"): + self.__widgets["status_bar"].showMessage( text.replace("\n", "") ) + self.__widgets["status_bar"].setToolTip( text ) + except Exception: + pass #------------------------------------------------------------------- # Display a notification using QSystemTrayIcon with an optional icon def gui_status_notification( self, message="", icon_file="" ) : - if ( icon_file != "" ) : - icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) ) - else : - icon = None + try: + if not self.__widgets.get("status_icon"): + return + if ( icon_file != "" ) : + icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) ) + else : + icon = None - self.__widgets["status_icon"].showMessage( "NUT Monitor", message, icon ) + self.__widgets["status_icon"].showMessage( "NUT Monitor", message, icon ) + except Exception: + pass #------------------------------------------------------------------- # Connect to the selected UPS using parameters (host,port,login,pass) diff --git a/scripts/python/app/NUT-Monitor-py3qt6.in b/scripts/python/app/NUT-Monitor-py3qt6.in index 5a58382f2d..083d32d14a 100755 --- a/scripts/python/app/NUT-Monitor-py3qt6.in +++ b/scripts/python/app/NUT-Monitor-py3qt6.in @@ -765,25 +765,33 @@ along with this program. If not, see . # Display a message on the status bar. The message is also set as # tooltip to enable users to see long messages. def gui_status_message( self, msg="" ) : - text = msg - - self.__widgets["status_bar"].showMessage( text.replace("\n", "") ) - self.__widgets["status_bar"].setToolTip( text ) + try: + text = msg + if self.__widgets.get("status_bar"): + self.__widgets["status_bar"].showMessage( text.replace("\n", "") ) + self.__widgets["status_bar"].setToolTip( text ) + except Exception: + pass #------------------------------------------------------------------- # Display a notification using QSystemTrayIcon with an optional icon def gui_status_notification( self, message="", icon_file="" ) : - if ( icon_file != "" ) : - icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) ) - else : - icon = None - - self.__widgets["status_icon"].showMessage( - "NUT Monitor", - message, - icon if icon else QSystemTrayIcon.MessageIcon.Information, - 5000 - ) + try: + if not self.__widgets.get("status_icon"): + return + if ( icon_file != "" ) : + icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) ) + else : + icon = None + + self.__widgets["status_icon"].showMessage( + "NUT Monitor", + message, + icon if icon else QSystemTrayIcon.MessageIcon.Information, + 5000 + ) + except Exception: + pass #------------------------------------------------------------------- # Connect to the selected UPS using parameters (host,port,login,pass) From 99715db9707ae1b953b0ad1cb2ca0780c080b71a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 14:09:00 +0200 Subject: [PATCH 10/12] scripts/python/app/NUT-Monitor-py2gtk2.in: handle lack of pynotify more gracefully [#3509] Signed-off-by: Jim Klimov --- scripts/python/app/NUT-Monitor-py2gtk2.in | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/python/app/NUT-Monitor-py2gtk2.in b/scripts/python/app/NUT-Monitor-py2gtk2.in index 8af49409d5..f5c716cd26 100755 --- a/scripts/python/app/NUT-Monitor-py2gtk2.in +++ b/scripts/python/app/NUT-Monitor-py2gtk2.in @@ -730,7 +730,8 @@ class interface : # Try to init pynotify try : import pynotify - pynotify.init( "NUT Monitor" ) + if not pynotify.init( "NUT Monitor" ): + return if ( icon_file != "" ) : icon = "file://%s" % os.path.abspath( os.path.join( self.__resdir, "pixmaps", icon_file ) ) From c8f82691ffd545e075ac6dceb02cef919d9fb090 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 14:11:07 +0200 Subject: [PATCH 11/12] scripts/python/app/NUT-Monitor-*.in, scripts/python/module/PyNUT.py.in: refactor with a graceful and harsher disconnect() handling [#3509] Signed-off-by: Jim Klimov --- scripts/python/app/NUT-Monitor-py2gtk2.in | 4 ++++ scripts/python/app/NUT-Monitor-py3qt5.in | 6 ++---- scripts/python/app/NUT-Monitor-py3qt6.in | 6 ++---- scripts/python/module/PyNUT.py.in | 18 ++++++++++++++---- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/scripts/python/app/NUT-Monitor-py2gtk2.in b/scripts/python/app/NUT-Monitor-py2gtk2.in index f5c716cd26..559d96a548 100755 --- a/scripts/python/app/NUT-Monitor-py2gtk2.in +++ b/scripts/python/app/NUT-Monitor-py2gtk2.in @@ -995,6 +995,10 @@ class gui_updater( threading.Thread ) : self.__parent_class.gui_status_message( _("Connection lost - attempting to reconnect...") ) self.__parent_class.gui_status_notification( _("Connection lost"), "warning.png" ) + # Close the socket to avoid "Broken pipe" in subsequent calls if any + if self.__parent_class._interface__ups_handler: + self.__parent_class._interface__ups_handler.disconnect() + # Wait for 2 seconds before attempting reconnection time.sleep( 2 ) self.__attempt_reconnect() diff --git a/scripts/python/app/NUT-Monitor-py3qt5.in b/scripts/python/app/NUT-Monitor-py3qt5.in index 48d6afe748..05dcae4cc1 100755 --- a/scripts/python/app/NUT-Monitor-py3qt5.in +++ b/scripts/python/app/NUT-Monitor-py3qt5.in @@ -1056,10 +1056,8 @@ class gui_updater : self.__timer.stop() # Close the socket to avoid "Broken pipe" in subsequent calls if any - try: - self.__parent_class._interface__ups_handler._PyNUTClient__srv_handler.close() - except Exception: - pass + if self.__parent_class._interface__ups_handler: + self.__parent_class._interface__ups_handler.disconnect() # Schedule reconnection attempt after 2 seconds QTimer.singleShot(2000, self.__attempt_reconnect) diff --git a/scripts/python/app/NUT-Monitor-py3qt6.in b/scripts/python/app/NUT-Monitor-py3qt6.in index 083d32d14a..2da9506097 100755 --- a/scripts/python/app/NUT-Monitor-py3qt6.in +++ b/scripts/python/app/NUT-Monitor-py3qt6.in @@ -1079,10 +1079,8 @@ class gui_updater : self.__timer.stop() # Close the socket to avoid "Broken pipe" in subsequent calls if any - try: - self.__parent_class._interface__ups_handler._PyNUTClient__srv_handler.close() - except Exception: - pass + if self.__parent_class._interface__ups_handler: + self.__parent_class._interface__ups_handler.disconnect() # Schedule reconnection attempt after 2 seconds QTimer.singleShot(2000, self.__attempt_reconnect) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index aba346dcfc..3005107c4a 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -711,10 +711,20 @@ certhost : Optional dict or list of dict/lists: {hostname: [certname, certver # Try to disconnect cleanly when class is deleted ;) def __del__( self ) : """ Class destructor method """ - try : - self.__send( b"LOGOUT\n" ) - except : - pass + self.disconnect() + + def disconnect(self): + """ Disconnects from the server cleanly """ + if hasattr(self, '__srv_handler') and self.__srv_handler: + try: + self.__send(b"LOGOUT\n") + except: + pass + try: + self.__srv_handler.close() + except: + pass + self.__srv_handler = None def __send(self, data): """ Internal method to send data through the socket with error handling """ From db05b671bfc685f5c86ad0d1c51cd4d833b5832a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 19 Jul 2026 14:12:24 +0200 Subject: [PATCH 12/12] scripts/python/app/NUT-Monitor-*.in: refactor old_handler teardown with new graceful and harsher disconnect() handling [#3509] Signed-off-by: Jim Klimov --- scripts/python/app/NUT-Monitor-py2gtk2.in | 9 +-------- scripts/python/app/NUT-Monitor-py3qt5.in | 9 +-------- scripts/python/app/NUT-Monitor-py3qt6.in | 9 +-------- 3 files changed, 3 insertions(+), 24 deletions(-) diff --git a/scripts/python/app/NUT-Monitor-py2gtk2.in b/scripts/python/app/NUT-Monitor-py2gtk2.in index 559d96a548..5a04fb6129 100755 --- a/scripts/python/app/NUT-Monitor-py2gtk2.in +++ b/scripts/python/app/NUT-Monitor-py2gtk2.in @@ -1027,14 +1027,7 @@ class gui_updater( threading.Thread ) : # Teardown old connection handler properly old_handler = self.__parent_class._interface__ups_handler if old_handler is not None: - try: - # Close the underlying telnet connection if it exists - if hasattr(old_handler, '_PyNUTClient__tn') and old_handler._PyNUTClient__tn is not None: - old_handler._PyNUTClient__tn.close() - elif hasattr(old_handler, 'tn') and old_handler.tn is not None: - old_handler.tn.close() - except Exception: - pass # Best effort cleanup + old_handler.disconnect() del old_handler # Create a new connection handler diff --git a/scripts/python/app/NUT-Monitor-py3qt5.in b/scripts/python/app/NUT-Monitor-py3qt5.in index 05dcae4cc1..9eb4b47090 100755 --- a/scripts/python/app/NUT-Monitor-py3qt5.in +++ b/scripts/python/app/NUT-Monitor-py3qt5.in @@ -1087,14 +1087,7 @@ class gui_updater : # Teardown old connection handler properly old_handler = self.__parent_class._interface__ups_handler if old_handler is not None: - try: - # Close the underlying telnet connection if it exists - if hasattr(old_handler, '_PyNUTClient__tn') and old_handler._PyNUTClient__tn is not None: - old_handler._PyNUTClient__tn.close() - elif hasattr(old_handler, 'tn') and old_handler.tn is not None: - old_handler.tn.close() - except Exception: - pass # Best effort cleanup + old_handler.disconnect() del old_handler # Create a new connection handler diff --git a/scripts/python/app/NUT-Monitor-py3qt6.in b/scripts/python/app/NUT-Monitor-py3qt6.in index 2da9506097..8f13ebb78c 100755 --- a/scripts/python/app/NUT-Monitor-py3qt6.in +++ b/scripts/python/app/NUT-Monitor-py3qt6.in @@ -1112,14 +1112,7 @@ class gui_updater : # Teardown old connection handler properly old_handler = self.__parent_class._interface__ups_handler if old_handler is not None: - try: - # Close the underlying telnet connection if it exists - if hasattr(old_handler, '_PyNUTClient__tn') and old_handler._PyNUTClient__tn is not None: - old_handler._PyNUTClient__tn.close() - elif hasattr(old_handler, 'tn') and old_handler.tn is not None: - old_handler.tn.close() - except Exception: - pass # Best effort cleanup + old_handler.disconnect() del old_handler # Create a new connection handler