Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 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]

- `upssched` client/tool updates:
* Added a `clean_exit()` handler similar to that in `upsmon`. [PR #3499]
Expand Down
115 changes: 101 additions & 14 deletions scripts/python/app/NUT-Monitor-py2gtk2.in
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import gtk, gtk.glade, gobject
import sys
import socket
import base64
import os, os.path
import stat
Expand Down Expand Up @@ -116,6 +117,7 @@ class interface :
__ups_rw_vars = None
__gui_thread = None
__current_ups = None
__reconnect_attempted = False

def __init__( self ) :

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -705,24 +710,28 @@ 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
def gui_status_notification( self, message="", icon_file="" ) :
# 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 ) )
Expand All @@ -746,6 +755,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
Expand Down Expand Up @@ -862,6 +872,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 )
Expand Down Expand Up @@ -977,12 +988,88 @@ 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" )

# 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()
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:
old_handler.disconnect()
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

Expand Down
135 changes: 119 additions & 16 deletions scripts/python/app/NUT-Monitor-py3qt5.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,6 +54,7 @@ import optparse
import configparser
import locale
import gettext
import PyNUT

# Try local development/accompanying packaging first,
# then system installation
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -748,20 +753,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
# 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

self.__widgets["status_icon"].showMessage( "NUT Monitor", message, icon )
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 )
except Exception:
pass

#-------------------------------------------------------------------
# Connect to the selected UPS using parameters (host,port,login,pass)
Expand Down Expand Up @@ -895,6 +908,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 )
Expand Down Expand Up @@ -1031,12 +1045,101 @@ 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()

# 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()

# 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" )

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:
old_handler.disconnect()
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()
self.__stop_thread = True
if hasattr(self, '_gui_updater__timer'):
self.__timer.stop()


#-----------------------------------------------------------------------
Expand Down
Loading
Loading