Skip to content

Commit 92c10ad

Browse files
authored
Merge pull request #3530 from jimklimov/issue-3509
NUT-Monitor: Add automatic reconnection mechanism for Py2Gtk2 as well as Py3Qt{5,6} variants
2 parents 918cf0e + db05b67 commit 92c10ad

5 files changed

Lines changed: 418 additions & 104 deletions

File tree

NEWS.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,9 @@ https://github.com/networkupstools/nut/milestone/13
248248
* Fixed Qt tray tooltips to render in plain text, not rich text which
249249
ended up as literal HTML on KDE Plasma 6; now that rich text formatting
250250
is only used for main window status labels. [PR #3430]
251+
* Fixed both Python 2 and 3 variants to handle automatic client reconnection
252+
(e.g. after system suspend/resume, network interruption, or NUT server
253+
restart). [issue #3509, PR #3523]
251254

252255
- `upssched` client/tool updates:
253256
* Added a `clean_exit()` handler similar to that in `upsmon`. [PR #3499]

scripts/python/app/NUT-Monitor-py2gtk2.in

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import gtk, gtk.glade, gobject
3636
import sys
37+
import socket
3738
import base64
3839
import os, os.path
3940
import stat
@@ -116,6 +117,7 @@ class interface :
116117
__ups_rw_vars = None
117118
__gui_thread = None
118119
__current_ups = None
120+
__reconnect_attempted = False
119121

120122
def __init__( self ) :
121123

@@ -389,8 +391,11 @@ class interface :
389391
def quit( self, widget=None ) :
390392
# If we are connected to an UPS, disconnect first...
391393
if self.__connected :
392-
self.gui_status_message( _("Disconnecting from device") )
393-
self.disconnect_from_ups()
394+
try:
395+
self.gui_status_message( _("Disconnecting from device") )
396+
self.disconnect_from_ups()
397+
except Exception:
398+
pass
394399

395400
gtk.main_quit()
396401

@@ -705,24 +710,28 @@ class interface :
705710
# Display a message on the status bar. The message is also set as
706711
# tooltip to enable users to see long messages.
707712
def gui_status_message( self, msg="" ) :
708-
context_id = self.__widgets["status_bar"].get_context_id("Infos")
709-
self.__widgets["status_bar"].pop( context_id )
713+
try:
714+
context_id = self.__widgets["status_bar"].get_context_id("Infos")
715+
self.__widgets["status_bar"].pop( context_id )
710716

711-
if ( platform.system() == "Windows" ) :
712-
text = msg.decode("cp1250").encode("utf8")
713-
else :
714-
text = msg
717+
if ( platform.system() == "Windows" ) :
718+
text = msg.decode("cp1250").encode("utf8")
719+
else :
720+
text = msg
715721

716-
message_id = self.__widgets["status_bar"].push( context_id, text.replace("\n", "") )
717-
self.__widgets["status_bar"].set_tooltip_text( text )
722+
message_id = self.__widgets["status_bar"].push( context_id, text.replace("\n", "") )
723+
self.__widgets["status_bar"].set_tooltip_text( text )
724+
except :
725+
pass
718726

719727
#-------------------------------------------------------------------
720728
# Display a notification using PyNotify with an optional icon
721729
def gui_status_notification( self, message="", icon_file="" ) :
722730
# Try to init pynotify
723731
try :
724732
import pynotify
725-
pynotify.init( "NUT Monitor" )
733+
if not pynotify.init( "NUT Monitor" ):
734+
return
726735

727736
if ( icon_file != "" ) :
728737
icon = "file://%s" % os.path.abspath( os.path.join( self.__resdir, "pixmaps", icon_file ) )
@@ -746,6 +755,7 @@ class interface :
746755
# Connect to the selected UPS using parameters (host,port,login,pass)
747756
def connect_to_ups( self, widget=None ) :
748757

758+
self.__reconnect_attempted = False
749759
host = self.__widgets["ups_host_entry"].get_text()
750760
port = int( self.__widgets["ups_port_entry"].get_value() )
751761
login = None
@@ -862,6 +872,7 @@ class gui_updater( threading.Thread ) :
862872
__parent_class = None
863873
__stop_thread = False
864874
was_online = True
875+
__reconnect_attempted = False
865876

866877
def __init__( self, parent_class ) :
867878
threading.Thread.__init__( self )
@@ -977,12 +988,88 @@ class gui_updater( threading.Thread ) :
977988
# Display UPS status as tooltip for tray icon
978989
self.__parent_class._interface__widgets["status_icon"].set_tooltip_markup( status_text )
979990

980-
except :
981-
self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) )
982-
self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" )
991+
except (socket.error, EOFError, PyNUT.PyNUTError) as e:
992+
# Connection error detected - attempt automatic reconnection
993+
if not self.__reconnect_attempted and self.__parent_class._interface__connected:
994+
self.__reconnect_attempted = True
995+
self.__parent_class.gui_status_message( _("Connection lost - attempting to reconnect...") )
996+
self.__parent_class.gui_status_notification( _("Connection lost"), "warning.png" )
997+
998+
# Close the socket to avoid "Broken pipe" in subsequent calls if any
999+
if self.__parent_class._interface__ups_handler:
1000+
self.__parent_class._interface__ups_handler.disconnect()
1001+
1002+
# Wait for 2 seconds before attempting reconnection
1003+
time.sleep( 2 )
1004+
self.__attempt_reconnect()
1005+
else:
1006+
self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) )
1007+
self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" )
9831008

9841009
time.sleep( 1 )
9851010

1011+
def __attempt_reconnect( self ) :
1012+
"""Attempt to reconnect to the UPS after a connection loss."""
1013+
if self.__stop_thread:
1014+
return
1015+
1016+
try:
1017+
# Get connection parameters from the parent class
1018+
host = self.__parent_class._interface__widgets["ups_host_entry"].get_text()
1019+
port = int(self.__parent_class._interface__widgets["ups_port_entry"].get_value())
1020+
login = None
1021+
password = None
1022+
1023+
if self.__parent_class._interface__widgets["ups_authentication_check"].get_active():
1024+
login = self.__parent_class._interface__widgets["ups_authentication_login"].get_text()
1025+
password = self.__parent_class._interface__widgets["ups_authentication_password"].get_text()
1026+
1027+
# Teardown old connection handler properly
1028+
old_handler = self.__parent_class._interface__ups_handler
1029+
if old_handler is not None:
1030+
old_handler.disconnect()
1031+
del old_handler
1032+
1033+
# Create a new connection handler
1034+
ups_handler = PyNUT.PyNUTClient(host=host, port=port, login=login, password=password)
1035+
1036+
# Verify the UPS is available
1037+
ups = self.__parent_class._interface__current_ups
1038+
srv_upses = ups_handler.GetUPSList()
1039+
1040+
if ups not in srv_upses:
1041+
self.__parent_class.gui_status_message( _("UPS '{0}' not found after reconnection").format(ups) )
1042+
self.__reconnect_attempted = False
1043+
return
1044+
1045+
if not ups_handler.CheckUPSAvailable(ups):
1046+
self.__parent_class.gui_status_message( _("UPS '{0}' is still not reachable").format(ups) )
1047+
self.__reconnect_attempted = False
1048+
return
1049+
1050+
# Connection successful - update the parent class
1051+
self.__parent_class._interface__ups_handler = ups_handler
1052+
self.__parent_class._interface__connected = True
1053+
self.__reconnect_attempted = False
1054+
1055+
# Refresh UPS data
1056+
commands = ups_handler.GetUPSCommands(ups)
1057+
self.__parent_class._interface__ups_commands = list(commands.keys())
1058+
self.__parent_class._interface__ups_commands.sort()
1059+
1060+
self.__parent_class._interface__ups_vars = ups_handler.GetUPSVars(ups)
1061+
self.__parent_class._interface__ups_rw_vars = ups_handler.GetRWVars(ups)
1062+
1063+
# Update the GUI (must be done on the main thread in GTK)
1064+
gobject.idle_add( self.__parent_class._interface__gui_update_ups_vars_view )
1065+
gobject.idle_add( self.__parent_class.change_status_icon, "on_line", False )
1066+
self.__parent_class.gui_status_message( _("Reconnected to '{0}' on {1}").format(ups, host) )
1067+
self.__parent_class.gui_status_notification( _("Reconnected to UPS"), "on_line.png" )
1068+
1069+
except (socket.error, EOFError, PyNUT.PyNUTError) as e:
1070+
self.__parent_class.gui_status_message( _("Reconnection failed: {0}").format(str(e)) )
1071+
self.__reconnect_attempted = False
1072+
9861073
def stop_thread( self ) :
9871074
self.__stop_thread = True
9881075

scripts/python/app/NUT-Monitor-py3qt5.in

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ from PyQt5.QtCore import *
4343
from PyQt5.QtGui import *
4444
from PyQt5.QtWidgets import *
4545
import sys
46+
import socket
4647
import base64
4748
import os, os.path
4849
import stat
@@ -53,6 +54,7 @@ import optparse
5354
import configparser
5455
import locale
5556
import gettext
57+
import PyNUT
5658

5759
# Try local development/accompanying packaging first,
5860
# then system installation
@@ -407,8 +409,11 @@ class interface :
407409
def quit( self, widget=None ) :
408410
# If we are connected to an UPS, disconnect first...
409411
if self.__connected :
410-
self.gui_status_message( _("Disconnecting from device") )
411-
self.disconnect_from_ups()
412+
try:
413+
self.gui_status_message( _("Disconnecting from device") )
414+
self.disconnect_from_ups()
415+
except RuntimeError:
416+
pass # widgets may already be deleted during shutdown
412417

413418
self.__app.quit()
414419

@@ -748,20 +753,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
748753
# Display a message on the status bar. The message is also set as
749754
# tooltip to enable users to see long messages.
750755
def gui_status_message( self, msg="" ) :
751-
text = msg
752-
753-
message_id = self.__widgets["status_bar"].showMessage( text.replace("\n", "") )
754-
self.__widgets["status_bar"].setToolTip( text )
756+
try:
757+
text = msg
758+
if self.__widgets.get("status_bar"):
759+
self.__widgets["status_bar"].showMessage( text.replace("\n", "") )
760+
self.__widgets["status_bar"].setToolTip( text )
761+
except Exception:
762+
pass
755763

756764
#-------------------------------------------------------------------
757765
# Display a notification using QSystemTrayIcon with an optional icon
758766
def gui_status_notification( self, message="", icon_file="" ) :
759-
if ( icon_file != "" ) :
760-
icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) )
761-
else :
762-
icon = None
763-
764-
self.__widgets["status_icon"].showMessage( "NUT Monitor", message, icon )
767+
try:
768+
if not self.__widgets.get("status_icon"):
769+
return
770+
if ( icon_file != "" ) :
771+
icon = QIcon( os.path.abspath( self.__find_res_file( "pixmaps", icon_file ) ) )
772+
else :
773+
icon = None
774+
775+
self.__widgets["status_icon"].showMessage( "NUT Monitor", message, icon )
776+
except Exception:
777+
pass
765778

766779
#-------------------------------------------------------------------
767780
# Connect to the selected UPS using parameters (host,port,login,pass)
@@ -895,6 +908,7 @@ class gui_updater :
895908
__parent_class = None
896909
__stop_thread = False
897910
was_online = True
911+
__reconnect_attempted = False
898912

899913
def __init__( self, parent_class ) :
900914
threading.Thread.__init__( self )
@@ -1031,12 +1045,101 @@ class gui_updater :
10311045
# Display UPS status as tooltip for tray icon
10321046
self.__parent_class._interface__widgets["status_icon"].setToolTip( "\n".join( tooltip_lines ) )
10331047

1034-
except :
1035-
self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) )
1036-
self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" )
1048+
except (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) as e:
1049+
# Connection error detected — attempt automatic reconnection
1050+
if not self.__reconnect_attempted and self.__parent_class._interface__connected:
1051+
self.__reconnect_attempted = True
1052+
self.__parent_class.gui_status_message( _("Connection lost — attempting to reconnect...") )
1053+
self.__parent_class.gui_status_notification( _("Connection lost"), "warning.png" )
1054+
1055+
# Stop the current timer
1056+
self.__timer.stop()
1057+
1058+
# Close the socket to avoid "Broken pipe" in subsequent calls if any
1059+
if self.__parent_class._interface__ups_handler:
1060+
self.__parent_class._interface__ups_handler.disconnect()
1061+
1062+
# Schedule reconnection attempt after 2 seconds
1063+
QTimer.singleShot(2000, self.__attempt_reconnect)
1064+
elif self.__reconnect_attempted:
1065+
# Reconnection already in progress, just ignore this update
1066+
pass
1067+
else:
1068+
self.__parent_class.gui_status_message( _("Error from '{0}' ({1})").format( ups, sys.exc_info()[1] ) )
1069+
self.__parent_class.gui_status_notification( _("Error from '{0}'\n{1}").format( ups, sys.exc_info()[1] ), "warning.png" )
1070+
1071+
def __attempt_reconnect( self ) :
1072+
"""Attempt to reconnect to the UPS after a connection loss."""
1073+
if self.__stop_thread:
1074+
return
1075+
1076+
try:
1077+
# Get connection parameters from the parent class
1078+
host = self.__parent_class._interface__widgets["ups_host_entry"].text()
1079+
port = int(self.__parent_class._interface__widgets["ups_port_entry"].value())
1080+
login = None
1081+
password = None
1082+
1083+
if self.__parent_class._interface__widgets["ups_authentication_check"].isChecked():
1084+
login = self.__parent_class._interface__widgets["ups_authentication_login"].text()
1085+
password = self.__parent_class._interface__widgets["ups_authentication_password"].text()
1086+
1087+
# Teardown old connection handler properly
1088+
old_handler = self.__parent_class._interface__ups_handler
1089+
if old_handler is not None:
1090+
old_handler.disconnect()
1091+
del old_handler
1092+
1093+
# Create a new connection handler
1094+
ups_handler = PyNUT.PyNUTClient(host=host, port=port, login=login, password=password)
1095+
1096+
# Verify the UPS is available
1097+
ups = self.__parent_class._interface__current_ups
1098+
srv_upses = ups_handler.GetUPSList()
1099+
1100+
if ups.encode('ascii') not in srv_upses:
1101+
self.__parent_class.gui_status_message( _("UPS '{0}' not found after reconnection").format(ups) )
1102+
self.__reconnect_attempted = False
1103+
# Restart the timer to continue polling
1104+
self.__timer.start(1000)
1105+
return
1106+
1107+
if not ups_handler.CheckUPSAvailable(ups):
1108+
self.__parent_class.gui_status_message( _("UPS '{0}' is still not reachable").format(ups) )
1109+
self.__reconnect_attempted = False
1110+
self.__timer.start(1000)
1111+
return
1112+
1113+
# Connection successful — update the parent class
1114+
self.__parent_class._interface__ups_handler = ups_handler
1115+
self.__parent_class._interface__connected = True
1116+
self.__reconnect_attempted = False
1117+
1118+
# Refresh UPS data
1119+
commands = ups_handler.GetUPSCommands(ups)
1120+
self.__parent_class._interface__ups_commands = list(commands.keys())
1121+
self.__parent_class._interface__ups_commands.sort()
1122+
1123+
self.__parent_class._interface__ups_vars = ups_handler.GetUPSVars(ups)
1124+
self.__parent_class._interface__ups_rw_vars = ups_handler.GetRWVars(ups)
1125+
1126+
# Update the GUI
1127+
self.__parent_class._interface__gui_update_ups_vars_view()
1128+
self.__parent_class.change_status_icon("on_line", blink=False)
1129+
self.__parent_class.gui_status_message( _("Reconnected to '{0}' on {1}").format(ups, host) )
1130+
self.__parent_class.gui_status_notification( _("Reconnected to UPS"), "on_line.png" )
1131+
1132+
except (socket.error, ConnectionError, EOFError, OSError, PyNUT.PyNUTError) as e:
1133+
self.__parent_class.gui_status_message( _("Reconnection failed: {0}").format(str(e)) )
1134+
self.__reconnect_attempted = False
1135+
1136+
# Restart the timer to continue polling
1137+
self.__timer.start(1000)
10371138

10381139
def stop_thread( self ) :
1039-
self.__timer.stop()
1140+
self.__stop_thread = True
1141+
if hasattr(self, '_gui_updater__timer'):
1142+
self.__timer.stop()
10401143

10411144

10421145
#-----------------------------------------------------------------------

0 commit comments

Comments
 (0)