From c670f9c567a99a68a27160da1801a10124d7b421 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 04:25:07 +0200 Subject: [PATCH 01/70] AI PoC: implement the remainder of NUT Networked protocol (as of NUT v2.8.5) for Python and Perl bindings Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 124 ++++++++++++++++++++++++++++-- scripts/python/module/PyNUT.py.in | 106 ++++++++++++++++++++++++- 2 files changed, 222 insertions(+), 8 deletions(-) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index 9ddefd50d6..e887e84fe0 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -25,7 +25,7 @@ my $_eol = "\n"; BEGIN { use Exporter (); use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); - $VERSION = 1.51; + $VERSION = 1.60; @ISA = qw(Exporter IO::Socket::INET); @EXPORT = qw(); @EXPORT_OK = qw(); @@ -80,6 +80,34 @@ sub Temperature { # get the internal temperature of UPS # control functions: they control our relationship to upsd, and send # commands to upsd. +sub StartTLS { + my $self = shift; + my %arg = @_; + my $ans; + + eval { require IO::Socket::SSL; }; + if ($@) { + $self->{err} = "IO::Socket::SSL not available"; + return undef; + } + + $ans = $self->_send("STARTTLS"); + if (defined $ans && $ans =~ /^OK STARTTLS/) { + $self->_debug("STARTTLS accepted, upgrading socket."); + IO::Socket::SSL->start_SSL($self->{srvsock}, + SSL_verify_mode => $arg{CERTVERIFY} ? IO::Socket::SSL::SSL_VERIFY_PEER() : IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_ca_file => $arg{CAPATH}, + %arg + ) or do { + $self->{err} = "SSL upgrade failed: " . IO::Socket::SSL->errstr(); + return undef; + }; + return 1; + } + $self->{err} = "STARTTLS failed: $ans"; + return undef; +} + sub Login { # login to upsd, so that it won't shutdown unless we say we're # ok. This should only be used if you're actually connected # to the ups that upsd is monitoring. @@ -393,6 +421,46 @@ sub ListUPS { return $self->_get_list("LIST UPS", 2, 1); } +sub GetTracking { + my $self = shift; + my $id = shift; + my $ans = $self->_send("GET TRACKING $id"); + unless (defined $ans) { + $self->{err} = "Network error: $!"; + return undef; + }; + if ($ans =~ /^TRACKING/) { + my @fields = split(' ', $ans); + return $fields[2]; + } + $self->{err} = "Error: $ans"; + return undef; +} + +sub ListClient { + my $self = shift; + my $ups = shift || $self->{name}; + return $self->_get_list("LIST CLIENT $ups", 2, 2); +} + +sub GetUPSDesc { + my $self = shift; + my $ups = shift || $self->{name}; + my $ans = $self->_send("GET UPSDESC $ups"); + unless (defined $ans) { + $self->{err} = "Network error: $!"; + return undef; + }; + if ($ans =~ /^UPSDESC/) { + my @fields = split(' ', $ans, 3); + my $desc = $fields[2]; + $desc =~ s/^"(.*)"$/$1/; + return $desc; + } + $self->{err} = "Error: $ans"; + return undef; +} + sub ListVar { my $self = shift; my $vars = $self->_get_list("LIST VAR $self->{name}", 3, 2); @@ -416,6 +484,37 @@ sub ListEnum { return $self->_get_list("LIST ENUM $self->{name} $var", 3); } +sub ListRange { + my $self = shift; + my $var = shift; + my $req = "LIST RANGE $self->{name} $var"; + my $ans = $self->_send($req); + + unless (defined $ans) { + $self->{err} = "Network error: $!"; + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->{err} = "Error: $ans"; + return undef; + } + elsif ($ans =~ /^BEGIN LIST RANGE/) { + my $retval = []; + my $line; + while ($line = $self->_getline) { + last if $line =~ /^END LIST RANGE/; + # RANGE "" "" + if ($line =~ /^RANGE \S+ \S+ "([^"]+)" "([^"]+)"/) { + push(@$retval, { min => $1, max => $2 }); + } + } + return $retval; + } + $self->{err} = "Unrecognized response: $ans"; + return undef; +} + sub _get_list { my $self = shift; my ($req, $valueidx, $keyidx) = @_; @@ -587,15 +686,17 @@ sub Error { # what was the last thing that went bang? else { return "No error explanation available."; } } +sub Primary { goto &Master; } + sub Master { # check for MASTER level access # Author: Kit Peters # ### changelog: uses the new _send command # -# TODO: API change pending to replace MASTER with PRIMARY +# NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY # (and backwards-compatible alias handling) my $self = shift; - my $req = "MASTER $self->{name}"; # build request + my $req = "PRIMARY $self->{name}"; # build request my $ans = $self->_send( $req ); unless (defined $ans) { @@ -603,14 +704,25 @@ sub Master { # check for MASTER level access return undef; }; + if ($ans =~ /^OK/) { # access granted + $self->_debug("PRIMARY level access granted. Upsd reports: $ans"); + return 1; + } + + # Retry with MASTER if PRIMARY failed + $req = "MASTER $self->{name}"; + $ans = $self->_send( $req ); + unless (defined $ans) { + $self->{err} = "Network error: $!"; + return undef; + }; + if ($ans =~ /^OK/) { # access granted $self->_debug("MASTER level access granted. Upsd reports: $ans"); return 1; } else { # access denied, or unrecognized reponse - $self->{err} = "MASTER level access denied. Upsd responded: $ans"; -# ### changelog: 8/3/2002 - KP - Master() returns undef rather than 0 on -# ### failure. this makes it consistent with other methods + $self->{err} = "Access denied. Upsd responded: $ans"; return undef; } } diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index b86c2d3799..a76c046885 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -97,8 +97,8 @@ class PyNUTClient : __use_ssl = False __ssl_context = None - __version = "1.9.0" - __release = "2026-03-16" + __version = "1.10.0" + __release = "2026-04-08" try: NUT_DEFAULT_CONNECT_TIMEOUT = float(os.getenv("NUT_DEFAULT_CONNECT_TIMEOUT", "5.0")) @@ -256,6 +256,108 @@ if something goes wrong. else: raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + def GetTrackingResult( self, tracking_id="" ) : + """ Returns the result of an asynchronous command execution + """ + if self.__debug : + print( "[DEBUG] GetTrackingResult for %s" % tracking_id ) + + self.__srv_handler.send( ("GET TRACKING %s\n" % tracking_id).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result[:8] == b"TRACKING" : + # TRACKING + return result.replace( b"\n", b"" ).split(b' ')[2].decode('ascii') + else : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + def GetVariableType( self, ups="", var="" ) : + """ Returns the type of the specified variable + """ + if self.__debug : + print( "[DEBUG] GetVariableType for %s / %s" % ( ups, var ) ) + + self.__srv_handler.send( ("GET TYPE %s %s\n" % ( ups, var )).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result[:4] == b"TYPE" : + # TYPE + off = len( ("TYPE %s %s " % ( ups, var )).encode('ascii') ) + return result[off:-1].decode('ascii') + else : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + def GetVariableDescription( self, ups="", var="" ) : + """ Returns the description of the specified variable + """ + if self.__debug : + print( "[DEBUG] GetVariableDescription for %s / %s" % ( ups, var ) ) + + self.__srv_handler.send( ("GET DESC %s %s\n" % ( ups, var )).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result[:4] == b"DESC" : + # DESC "" + off = len( ("DESC %s %s " % ( ups, var )).encode('ascii') ) + return result[off:-1].split(b'"')[1].decode('ascii') + else : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + def GetEnumList( self, ups="", var="" ) : + """ Returns the list of possible values for an ENUM variable + """ + if self.__debug : + print( "[DEBUG] GetEnumList for %s / %s" % ( ups, var ) ) + + self.__srv_handler.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') ) + + enum_list = [] + result = self.__read_until( ("END LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') ) + offset = len( ("ENUM %s %s " % ( ups, var )).encode('ascii') ) + end_offset = 0 - ( len( ("END LIST ENUM %s %s\n" % ( ups, var )).encode('ascii') ) + 1 ) + + for current in result[:end_offset].split( b"\n" ) : + enum_list.append( current[offset:].split( b'"' )[1].decode('ascii') ) + + return enum_list + + def GetRangeList( self, ups="", var="" ) : + """ Returns the list of possible ranges for a RANGE variable + """ + if self.__debug : + print( "[DEBUG] GetRangeList for %s / %s" % ( ups, var ) ) + + self.__srv_handler.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') ) + + range_list = [] + result = self.__read_until( ("END LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') ) + offset = len( ("RANGE %s %s " % ( ups, var )).encode('ascii') ) + end_offset = 0 - ( len( ("END LIST RANGE %s %s\n" % ( ups, var )).encode('ascii') ) + 1 ) + + for current in result[:end_offset].split( b"\n" ) : + # RANGE "" "" + ranges = current[offset:].split( b'"' ) + range_list.append( { 'min' : ranges[1].decode('ascii'), 'max' : ranges[3].decode('ascii') } ) + + return range_list + + def GetDeviceNumLogins( self, ups="" ) : + """ Returns the number of connected clients for the specified UPS + """ + if self.__debug : + print( "[DEBUG] GetDeviceNumLogins for %s" % ups ) + + self.__srv_handler.send( ("GET NUMLOGINS %s\n" % ups).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result[:9] == b"NUMLOGINS" : + # NUMLOGINS + return int( result.replace( b"\n", b"" ).split(b' ')[2] ) + else : + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + def GetUPSList( self ) : """ Returns the list of available UPS from the NUT server From c4dd5d69b4cdb270f3be49b5cd98ce6af018f328 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 10:49:45 +0200 Subject: [PATCH 02/70] scripts/perl/Nut.pm: revise AI creations, complete the TRACKING+waiting dialog support [#1348] Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 164 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 159 insertions(+), 5 deletions(-) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index e887e84fe0..f45404cd5a 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -6,6 +6,7 @@ # ### changelog: made debug messages slightly more descriptive, improved # ### changelog: comments in code # ### changelog: Removed timeleft() function. +# ### changelog: 1.60: JK 2026-04-08: Added basic STARTTLS, as well as TRACKING support, LIST CLIENT, LIST RANGE, GET UPSDESC and PRIMARY/MASTER aliasing. package UPS::Nut; use strict; @@ -80,6 +81,52 @@ sub Temperature { # get the internal temperature of UPS # control functions: they control our relationship to upsd, and send # commands to upsd. +sub SetTrackingMode { + # Enable/disable TRACKING ability for SETVAR/INSTCMD ('ON'/'OFF') + # Remember in $self->{tracking} if we could set it with this upsd + # version, and then to which value? + my $self = shift; + my $value = shift; + my $ans; # scalar to hold responses from upsd + my $errmsg; # error message, sent to _debug and $self->{err} + + # 'ON'/'OFF'/undef + if (!(defined $value) || ($value != 'ON' && $value != 'OFF')) { + $self->{err} = "Invalid setting for TRACKING mode was requested"; + return undef; + } + } + + $ans = $self->_send("SET TRACKING $value"); + unless (defined $ans) { + $self->{err} = "Network error: $!"; + return undef; + }; + + if ($ans =~ /^OK/) { + $self->{tracking} = $value; + return $value; + } + + $self->{tracking} = undef; + $self->{err} = "Error: $ans"; + return undef; +} + +sub EnableTrackingModeOnce { + my $self = shift; + + if (defined $self->{tracking} && $self->{tracking} == 'ON') { + return 1; + } + + if (self->SetTrackingMode('ON') == 'ON') + return 1; + + # Unsupported by server? Other errors? + return undef; +} + sub StartTLS { my $self = shift; my %arg = @_; @@ -94,7 +141,9 @@ sub StartTLS { $ans = $self->_send("STARTTLS"); if (defined $ans && $ans =~ /^OK STARTTLS/) { $self->_debug("STARTTLS accepted, upgrading socket."); - IO::Socket::SSL->start_SSL($self->{srvsock}, + # NOTE: Currently nothing fancy like client's own certificate databases... + IO::Socket::SSL->start_SSL( + $self->{srvsock}, SSL_verify_mode => $arg{CERTVERIFY} ? IO::Socket::SSL::SSL_VERIFY_PEER() : IO::Socket::SSL::SSL_VERIFY_NONE(), SSL_ca_file => $arg{CAPATH}, %arg @@ -104,6 +153,7 @@ sub StartTLS { }; return 1; } + $self->{err} = "STARTTLS failed: $ans"; return undef; } @@ -199,6 +249,10 @@ sub _initialize { my $pass = $arg{PASSWORD} || undef; # password passed to upsd my $login = $arg{LOGIN} || 0; # login to upsd on init? + # Explicitly enable/disable TRACKING mode for SETVAR/INSTCMD on init? + # Remember in $self->{tracking} if we could toggle it with + # this upsd version, and to which value? ('ON'/'OFF'/undef) + my $tracking = $arg{TRACKING} || undef; $self->{name} = $arg{NAME} || 'default'; # UPS name in etc/ups.conf on $host $self->{timeout} = $arg{TIMEOUT} || 30; # timeout @@ -237,6 +291,9 @@ sub _initialize { return undef; } + # Can error out on invalid "TRACKING" value setting, returns undef then: + $self->{tracking} = $self->SetTrackingMode($tracking); + return $self; } @@ -339,6 +396,16 @@ sub Set { my $var = shift; (my $value = shift) =~ s/^"?(.*)"?$/"$1"/; # add quotes if missing + # Optional TRACKING wait support: + my $wait_interval_sec = shift || undef; + my $wait_max_count = shift || undef; + my $do_wait = 0; + + if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_max_count > 0) { + $self->EnableTrackingModeOnce; + $do_wait = 1; + } + my $req = "SET VAR $self->{name} $var $value"; # build request my $ans = $self->_send( $req ); @@ -351,7 +418,23 @@ sub Set { $self->{err} = "Error: $ans"; return undef; } + elsif ($ans =~ /^OK TRACKING /) { # command successful + my $id; + (undef, undef, $id) = split(' ', $ans, 3); + if (defined $id && $id =~ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + ) { + # UUID + $self->_debug("Variable setting $var $value sent successfully, got tracking ID '$id'."); + if ($do_wait && $self->WaitTrackingResult($id, $wait_interval_sec, $wait_max_count)) { + return $value; + } + return ($value, $id); + } + $self->_debug("Variable setting $var $value sent successfully, but got bogus tracking ID: $ans"); + return $value; + } elsif ($ans =~ /^OK/) { + $self->_debug("Variable setting $var $value sent successfully."); return $value; } else { # unrecognized response @@ -394,6 +477,16 @@ sub InstCmd { # send instant command to ups chomp (my $cmd = shift); + # Optional TRACKING wait support: + my $wait_interval_sec = shift || undef; + my $wait_max_count = shift || undef; + my $do_wait = 0; + + if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_max_count > 0) { + $self->EnableTrackingModeOnce; + $do_wait = 1; + } + my $req = "INSTCMD $self->{name} $cmd"; my $ans = $self->_send( $req ); @@ -406,6 +499,21 @@ sub InstCmd { # send instant command to ups $self->{err} = "Can't send instant command $cmd. Reason: $ans"; return undef; } + elsif ($ans =~ /^OK TRACKING /) { # command successful + my $id; + (undef, undef, $id) = split(' ', $ans, 3); + if (defined $id && $id =~ /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ + ) { + # UUID + $self->_debug("Instant command $cmd sent successfully, got tracking ID '$id'."); + if ($do_wait && $self->WaitTrackingResult($id, $wait_interval_sec, $wait_max_count)) { + return 1; + } + return (1, $id); + } + $self->_debug("Instant command $cmd sent successfully, but got bogus tracking ID: $ans"); + return 1; + } elsif ($ans =~ /^OK/) { # command successful $self->_debug("Instant command $cmd sent successfully."); return 1; @@ -421,7 +529,7 @@ sub ListUPS { return $self->_get_list("LIST UPS", 2, 1); } -sub GetTracking { +sub GetTrackingResult { my $self = shift; my $id = shift; my $ans = $self->_send("GET TRACKING $id"); @@ -431,12 +539,55 @@ sub GetTracking { }; if ($ans =~ /^TRACKING/) { my @fields = split(' ', $ans); + # SUCCESS, PENDING, ERR... return $fields[2]; } $self->{err} = "Error: $ans"; return undef; } +sub WaitTrackingResult { + my $self = shift; + + chomp (my $id = shift); + + my $wait_interval_sec = shift || 1; + my $wait_max_count = shift || 10; + + if (!(defined $id && defined $wait_interval_sec && defined $wait_max_count)) { + return undef; + } + + do { + my $value = $self->GetTrackingResult($id); + if (defined $value) { + if ($value == 'SUCCESS') { + $self->_debug("Request with TRACKING ID $id has successfully completed"); + return 1; + } else + if ($value =~ 'ERR') { + $self->_debug("Request with TRACKING ID $id has completed with a failure: $value"); + return -1; + } else + if ($value == 'PENDING') { + $self->_debug("Still waiting for TRACKING ID $id..."); + } else { + $self->_debug("Got bogus reply while waiting for TRACKING ID $id: $value"); + } + } else { + # TOTHINK: Keep retrying? Here in case of network or explicit error... + $self->_debug("Got bogus reply while waiting for TRACKING ID $id: undef"); + } + + sleep($wait_interval_sec); + $wait_max_count = $wait_max_count - 1; + } while ($wait_max_count > 0); + + # Timed out?.. + $self->_debug("Timed out while waiting for TRACKING ID $id"); + return 0; +} + sub ListClient { my $self = shift; my $ups = shift || $self->{name}; @@ -511,6 +662,7 @@ sub ListRange { } return $retval; } + $self->{err} = "Unrecognized response: $ans"; return undef; } @@ -691,6 +843,8 @@ sub Primary { goto &Master; } sub Master { # check for MASTER level access # Author: Kit Peters # ### changelog: uses the new _send command +# ### changelog: 8/3/2002 - KP - Master() returns undef rather than 0 on +# ### failure. this makes it consistent with other methods # # NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY # (and backwards-compatible alias handling) @@ -708,7 +862,7 @@ sub Master { # check for MASTER level access $self->_debug("PRIMARY level access granted. Upsd reports: $ans"); return 1; } - + # Retry with MASTER if PRIMARY failed $req = "MASTER $self->{name}"; $ans = $self->_send( $req ); @@ -716,13 +870,13 @@ sub Master { # check for MASTER level access $self->{err} = "Network error: $!"; return undef; }; - + if ($ans =~ /^OK/) { # access granted $self->_debug("MASTER level access granted. Upsd reports: $ans"); return 1; } else { # access denied, or unrecognized reponse - $self->{err} = "Access denied. Upsd responded: $ans"; + $self->{err} = "PRIMARY/MASTER level access denied. Upsd responded: $ans"; return undef; } } From 02236c2422f47a93f9aed9e0f38d9f4f1f65cf3a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 12:13:18 +0200 Subject: [PATCH 03/70] scripts/python/module/PyNUT.py.in: revise AI creations, complete the TRACKING+waiting dialog support [#1349] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 122 +++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 9 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index a76c046885..71a5b74de9 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -105,7 +105,7 @@ class PyNUTClient : except: NUT_DEFAULT_CONNECT_TIMEOUT = 5.0 - def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=NUT_DEFAULT_CONNECT_TIMEOUT, + def __init__( self, host="127.0.0.1", port=3493, login=None, password=None, debug=False, timeout=NUT_DEFAULT_CONNECT_TIMEOUT, tracking=False, use_ssl=False, ssl_context=None, cert_verify=None, ca_file=None, ca_path=None, cert_file=None, key_file=None, key_pass=None, force_ssl=False ) : """ Class initialization method @@ -116,6 +116,7 @@ login : Login used to connect to NUT server (default to None for no authen password : Password used when using authentication (default to None) debug : Boolean, put class in debug mode (prints everything on console, default to False) timeout : Timeout used to wait for network response +tracking : Boolean or 'ON'/'OFF', try to enable TRACKING support right away use_ssl : Boolean, use SSL/TLS for connection (default to False; subject to 'ssl' module availability) ssl_context : ssl.SSLContext object to use for SSL/TLS connection (default to None; subject to 'ssl' module availability) cert_verify : Boolean, verify server certificate (default to None, which means True if CA info is provided) @@ -173,6 +174,8 @@ force_ssl : Boolean, if True, the connection must be secure (default to False) self.__connect() + self.__tracking = self.SetTrackingMode(tracking) + # Try to disconnect cleanly when class is deleted ;) def __del__( self ) : """ Class destructor method """ @@ -256,6 +259,48 @@ if something goes wrong. else: raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + def SetTrackingMode( self, value="" ) : + """ Enable/disable TRACKING ability for SETVAR/INSTCMD ('ON'/'OFF') + + Remember in $self.__tracking if we could set it with this upsd + version, and then to which value? + """ + if value is None or str(value).strip() == "": + return None + + if str(value) == 'False': + value = 'OFF' + + if str(value) == 'True': + value = 'ON' + + if value not in ['ON', 'OFF']: + if self.__debug : + print( "[DEBUG] Invalid setting for TRACKING mode was requested" ) + # TOTHINK # raise PyNUTError( "Invalid setting for TRACKING mode was requested" ) + return None + + self.__srv_handler.send( ("SET TRACKING %s\n" % value).encode('ascii') ) + result = self.__read_until( b"\n" ) + if result == b"OK\n" : + self.__tracking = value + return value + + if self.__debug : + print( "[DEBUG] Failed to set TRACKING mode: %s" % result.replace( b"\n", b"" ).decode('ascii') ) + + self.__tracking = None + return None + + def EnableTrackingModeOnce( self ) : + if self.__tracking is not None and self.__tracking == 'ON': + return True + + if self.SetTrackingMode('ON') == 'ON': + return True + + return False + def GetTrackingResult( self, tracking_id="" ) : """ Returns the result of an asynchronous command execution """ @@ -264,12 +309,41 @@ if something goes wrong. self.__srv_handler.send( ("GET TRACKING %s\n" % tracking_id).encode('ascii') ) result = self.__read_until( b"\n" ) - if result[:8] == b"TRACKING" : - # TRACKING - return result.replace( b"\n", b"" ).split(b' ')[2].decode('ascii') + if result in [b"PENDING\n", b"SUCCESS\n"]: + return result.replace( b"\n", b"" ).decode('ascii') else : + # ERR... raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + def WaitTrackingResult( self, tracking_id="", wait_interval_sec = 1, wait_max_count = 10) : + if tracking_id is None or str(tracking_id).trim() == "" or + wait_interval_sec is None or wait_interval_sec < 1 or + wait_max_count is None or wait_max_count < 1 : + # Non-fatal skip; TOTHINK: raise a special exception for bad args? + return None + + while wait_max_count > 0: + value = self.GetTracking(tracking_id) + + if value == 'SUCCESS': + if self.__debug : + print( "[DEBUG] Request with TRACKING ID %s has successfully completed" % tracking_id ) + return True + + if value == 'PENDING': + if self.__debug : + print( "[DEBUG] Still waiting for TRACKING ID %s..." % tracking_id ) + else: + print( "[DEBUG] Got bogus reply while waiting for TRACKING ID %s: %s" % (tracking_id, str(value)) ) + + sleep(wait_interval_sec) + wait_max_count = wait_max_count - 1 + + if self.__debug : + print( "[DEBUG] Timed out waiting for TRACKING ID %s" % tracking_id ) + + return False + def GetVariableType( self, ups="", var="" ) : """ Returns the type of the specified variable """ @@ -511,21 +585,36 @@ The result is presented as a dictionary containing 'key->val' pairs return( rw_vars ) - def SetRWVar( self, ups="", var="", value="" ): + def SetRWVar( self, ups="", var="", value="", wait_interval_sec = None, wait_max_count = None ): """ Set a variable to the specified value on selected UPS The variable must be a writable value (cf GetRWVars) and you must have the proper rights to set it (maybe login/password). """ + do_wait = False + if not (wait_interval_sec is None or wait_interval_sec < 1 or + wait_max_count is None or wait_max_count < 1) : + do_wait = True + self.EnableTrackingModeOnce() + self.__srv_handler.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" ) - else : - raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + else: + if ( result[:12] == b"OK TRACKING " ) : + tracking_id = result.replace( b"\n", b"" ).split(b" ")[2].decode('ascii') + if self.__debug : + print( "[DEBUG] Variable setting %s %s sent successfully, got tracking ID '%s'" % (var, value, tracking_id) ) - def RunUPSCommand( self, ups="", command="" ) : + if do_wait and self.WaitTrackingResult(tracking_id, wait_interval_sec, wait_max_count): + return( "OK" ) + return( tracking_id ) + else: + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + + def RunUPSCommand( self, ups="", command="", wait_interval_sec = None, wait_max_count = None ) : """ Send a command to the specified UPS Returns OK on success or raises an error @@ -534,12 +623,27 @@ Returns OK on success or raises an error if self.__debug : print( "[DEBUG] RunUPSCommand called..." ) + do_wait = False + if not (wait_interval_sec is None or wait_interval_sec < 1 or + wait_max_count is None or wait_max_count < 1) : + do_wait = True + self.EnableTrackingModeOnce() + self.__srv_handler.send( ("INSTCMD %s %s\n" % ( ups, command )).encode('ascii') ) result = self.__read_until( b"\n" ) if ( result == b"OK\n" ) : return( "OK" ) else : - raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) + if ( result[:12] == b"OK TRACKING " ) : + tracking_id = result.replace( b"\n", b"" ).split(b" ")[2].decode('ascii') + if self.__debug : + print( "[DEBUG] Instant command %s sent successfully, got tracking ID '%s'" % (command, tracking_id) ) + + if do_wait and self.WaitTrackingResult(tracking_id, wait_interval_sec, wait_max_count): + return( "OK" ) + return( tracking_id ) + else: + raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) def DeviceLogin( self, ups="") : """ Establish a login session with a device (like upsmon does) From a8d8fce3bf3009005c5dc9e1c97cb0e7e049e61d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 23:21:51 +0200 Subject: [PATCH 04/70] scripts/perl/Nut.pm: fix syntax [#1348] Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index f45404cd5a..3cb60d0f60 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -91,10 +91,9 @@ sub SetTrackingMode { my $errmsg; # error message, sent to _debug and $self->{err} # 'ON'/'OFF'/undef - if (!(defined $value) || ($value != 'ON' && $value != 'OFF')) { + if (!(defined $value) || ($value ne 'ON' && $value ne 'OFF')) { $self->{err} = "Invalid setting for TRACKING mode was requested"; return undef; - } } $ans = $self->_send("SET TRACKING $value"); @@ -116,12 +115,13 @@ sub SetTrackingMode { sub EnableTrackingModeOnce { my $self = shift; - if (defined $self->{tracking} && $self->{tracking} == 'ON') { + if (defined $self->{tracking} && $self->{tracking} eq 'ON') { return 1; } - if (self->SetTrackingMode('ON') == 'ON') + if ($self->SetTrackingMode('ON') eq 'ON') { return 1; + } # Unsupported by server? Other errors? return undef; @@ -401,7 +401,7 @@ sub Set { my $wait_max_count = shift || undef; my $do_wait = 0; - if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_max_count > 0) { + if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_interval_sec > 0) { $self->EnableTrackingModeOnce; $do_wait = 1; } @@ -482,7 +482,7 @@ sub InstCmd { # send instant command to ups my $wait_max_count = shift || undef; my $do_wait = 0; - if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_max_count > 0) { + if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_interval_sec > 0) { $self->EnableTrackingModeOnce; $do_wait = 1; } @@ -561,15 +561,13 @@ sub WaitTrackingResult { do { my $value = $self->GetTrackingResult($id); if (defined $value) { - if ($value == 'SUCCESS') { + if ($value eq 'SUCCESS') { $self->_debug("Request with TRACKING ID $id has successfully completed"); return 1; - } else - if ($value =~ 'ERR') { + } elsif ($value =~ 'ERR') { $self->_debug("Request with TRACKING ID $id has completed with a failure: $value"); return -1; - } else - if ($value == 'PENDING') { + } elsif ($value eq 'PENDING') { $self->_debug("Still waiting for TRACKING ID $id..."); } else { $self->_debug("Got bogus reply while waiting for TRACKING ID $id: $value"); From 068ad5a18f0a3bbc12eb81e992b8fc540b9f3c3e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 23:23:08 +0200 Subject: [PATCH 05/70] scripts/perl/Nut.pm: Implement TrackingID as a separate class [#1348] Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 48 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index 3cb60d0f60..4047c63921 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -7,6 +7,7 @@ # ### changelog: comments in code # ### changelog: Removed timeleft() function. # ### changelog: 1.60: JK 2026-04-08: Added basic STARTTLS, as well as TRACKING support, LIST CLIENT, LIST RANGE, GET UPSDESC and PRIMARY/MASTER aliasing. +# ### changelog: 1.61: JK 2026-04-08: Make TrackingID a class, similar to C++. package UPS::Nut; use strict; @@ -26,7 +27,7 @@ my $_eol = "\n"; BEGIN { use Exporter (); use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); - $VERSION = 1.60; + $VERSION = 1.61; @ISA = qw(Exporter IO::Socket::INET); @EXPORT = qw(); @EXPORT_OK = qw(); @@ -425,10 +426,11 @@ sub Set { ) { # UUID $self->_debug("Variable setting $var $value sent successfully, got tracking ID '$id'."); - if ($do_wait && $self->WaitTrackingResult($id, $wait_interval_sec, $wait_max_count)) { + my $tid = UPS::Nut::TrackingID->new($id); + if ($do_wait && $self->WaitTrackingResult($tid, $wait_interval_sec, $wait_max_count)) { return $value; } - return ($value, $id); + return ($value, $tid); } $self->_debug("Variable setting $var $value sent successfully, but got bogus tracking ID: $ans"); return $value; @@ -506,10 +508,11 @@ sub InstCmd { # send instant command to ups ) { # UUID $self->_debug("Instant command $cmd sent successfully, got tracking ID '$id'."); - if ($do_wait && $self->WaitTrackingResult($id, $wait_interval_sec, $wait_max_count)) { + my $tid = UPS::Nut::TrackingID->new($id); + if ($do_wait && $self->WaitTrackingResult($tid, $wait_interval_sec, $wait_max_count)) { return 1; } - return (1, $id); + return (1, $tid); } $self->_debug("Instant command $cmd sent successfully, but got bogus tracking ID: $ans"); return 1; @@ -531,7 +534,8 @@ sub ListUPS { sub GetTrackingResult { my $self = shift; - my $id = shift; + my $tid = shift; + my $id = ref($tid) eq 'UPS::Nut::TrackingID' ? $tid->id : $tid; my $ans = $self->_send("GET TRACKING $id"); unless (defined $ans) { $self->{err} = "Network error: $!"; @@ -548,8 +552,8 @@ sub GetTrackingResult { sub WaitTrackingResult { my $self = shift; - - chomp (my $id = shift); + my $tid = shift; + my $id = ref($tid) eq 'UPS::Nut::TrackingID' ? $tid->id : $tid; my $wait_interval_sec = shift || 1; my $wait_max_count = shift || 10; @@ -559,7 +563,7 @@ sub WaitTrackingResult { } do { - my $value = $self->GetTrackingResult($id); + my $value = $self->GetTrackingResult($tid); if (defined $value) { if ($value eq 'SUCCESS') { $self->_debug("Request with TRACKING ID $id has successfully completed"); @@ -1223,6 +1227,32 @@ This module is distributed under the same license as Perl itself. =cut +package UPS::Nut::TrackingID; +use strict; + +sub new { + my $class = shift; + my $id = shift; + my $self = { id => $id }; + bless $self, $class; + return $self; +} + +sub id { + my $self = shift; + return $self->{id}; +} + +sub toString { + my $self = shift; + return $self->{id}; +} + +sub isValid { + my $self = shift; + return defined $self->{id} && $self->{id} ne ""; +} + 1; __END__ From aa75a7cd2af0e522654b3051c1f716e9a18493fc Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 23:25:36 +0200 Subject: [PATCH 06/70] scripts/python/module/PyNUT.py.in: fix syntax [#1349] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 71a5b74de9..2a0f730704 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -316,8 +316,8 @@ if something goes wrong. raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) def WaitTrackingResult( self, tracking_id="", wait_interval_sec = 1, wait_max_count = 10) : - if tracking_id is None or str(tracking_id).trim() == "" or - wait_interval_sec is None or wait_interval_sec < 1 or + if tracking_id is None or str(tracking_id).strip() == "" or \ + wait_interval_sec is None or wait_interval_sec < 1 or \ wait_max_count is None or wait_max_count < 1 : # Non-fatal skip; TOTHINK: raise a special exception for bad args? return None @@ -334,9 +334,10 @@ if something goes wrong. if self.__debug : print( "[DEBUG] Still waiting for TRACKING ID %s..." % tracking_id ) else: - print( "[DEBUG] Got bogus reply while waiting for TRACKING ID %s: %s" % (tracking_id, str(value)) ) + if self.__debug: + print( "[DEBUG] Got bogus reply while waiting for TRACKING ID %s: %s" % (tracking_id, str(value)) ) - sleep(wait_interval_sec) + time.sleep(wait_interval_sec) wait_max_count = wait_max_count - 1 if self.__debug : From 09ede2aa2ce60571849b84ebf3a09ab41d72de8f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 23:26:03 +0200 Subject: [PATCH 07/70] scripts/python/module/PyNUT.py.in: Implement TrackingID as a separate class [#1349] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 33 ++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 2a0f730704..5ebebb90f3 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -78,6 +78,20 @@ try: except: pass +class TrackingID: + """ Cookie given when performing async action, used to redeem result at a later date. """ + def __init__(self, tracking_id): + self.__id = tracking_id + + def id(self): + return self.__id + + def __str__(self): + return self.__id + + def isValid(self): + return self.__id is not None and self.__id != "" + class PyNUTError( Exception ) : """ Base class for custom exceptions """ @@ -304,6 +318,9 @@ if something goes wrong. def GetTrackingResult( self, tracking_id="" ) : """ Returns the result of an asynchronous command execution """ + if isinstance(tracking_id, TrackingID): + tracking_id = tracking_id.id() + if self.__debug : print( "[DEBUG] GetTrackingResult for %s" % tracking_id ) @@ -316,6 +333,10 @@ if something goes wrong. raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) def WaitTrackingResult( self, tracking_id="", wait_interval_sec = 1, wait_max_count = 10) : + tid = tracking_id + if isinstance(tracking_id, TrackingID): + tracking_id = tracking_id.id() + if tracking_id is None or str(tracking_id).strip() == "" or \ wait_interval_sec is None or wait_interval_sec < 1 or \ wait_max_count is None or wait_max_count < 1 : @@ -323,7 +344,7 @@ if something goes wrong. return None while wait_max_count > 0: - value = self.GetTracking(tracking_id) + value = self.GetTrackingResult(tid) if value == 'SUCCESS': if self.__debug : @@ -609,9 +630,10 @@ rights to set it (maybe login/password). if self.__debug : print( "[DEBUG] Variable setting %s %s sent successfully, got tracking ID '%s'" % (var, value, tracking_id) ) - if do_wait and self.WaitTrackingResult(tracking_id, wait_interval_sec, wait_max_count): + tid = TrackingID(tracking_id) + if do_wait and self.WaitTrackingResult(tid, wait_interval_sec, wait_max_count): return( "OK" ) - return( tracking_id ) + return( tid ) else: raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) @@ -640,9 +662,10 @@ Returns OK on success or raises an error if self.__debug : print( "[DEBUG] Instant command %s sent successfully, got tracking ID '%s'" % (command, tracking_id) ) - if do_wait and self.WaitTrackingResult(tracking_id, wait_interval_sec, wait_max_count): + tid = TrackingID(tracking_id) + if do_wait and self.WaitTrackingResult(tid, wait_interval_sec, wait_max_count): return( "OK" ) - return( tracking_id ) + return( tid ) else: raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) From 215a5257cf0cba72b665b7858d336dc4858c1508 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 23:30:13 +0200 Subject: [PATCH 08/70] clients/nutclient.h, clients/Makefile.am: Implement TrackingID as a separate class, including timestamp tracking and age reporting [#656] Signed-off-by: Jim Klimov --- clients/Makefile.am | 2 +- clients/nutclient.h | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/clients/Makefile.am b/clients/Makefile.am index 47b11baee7..b5d115c234 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -292,7 +292,7 @@ libupsclient-version.h: libupsclient.la if HAVE_CXX11 # libnutclient version information and build libnutclient_la_SOURCES = nutclient.h nutclient.cpp -libnutclient_la_LDFLAGS = -version-info 3:0:1 +libnutclient_la_LDFLAGS = -version-info 4:0:2 # Needed in not-standalone builds with -DHAVE_NUTCOMMON=1 # which is defined for in-tree CXX builds above: if ENABLE_SHARED_PRIVATE_LIBS diff --git a/clients/nutclient.h b/clients/nutclient.h index 20c6d7a2d1..7635fe4c5e 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -289,7 +289,26 @@ class TimeoutException : public IOException /** * Cookie given when performing async action, used to redeem result at a later date. */ -typedef std::string TrackingID; +class TrackingID +{ +public: + TrackingID(const std::string& id = "") : _id(id), _created(std::time(nullptr)) {} + virtual ~TrackingID() {} + + const std::string& id() const { return _id; } + std::time_t created() const { return _created; } + double age() const { return std::difftime(std::time(nullptr), _created); } + + bool isValid() const { return !_id.empty(); } + + operator std::string() const { return _id; } + bool operator==(const TrackingID& other) const { return _id == other._id; } + bool operator<(const TrackingID& other) const { return _id < other._id; } + +private: + std::string _id; + std::time_t _created; +}; /** * Result of an async action. From d17ccb62cdf240d1f0ac29f11815619935cf2b75 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 23:51:35 +0200 Subject: [PATCH 09/70] clients/nutclient{,mem}.{h,cpp}, tests/*.cpp: implement waiting for TRACKING result if requested [#656] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 200 ++++++++++++++++++++++++++++++++++----- clients/nutclient.h | 131 +++++++++++++++++++------ clients/nutclientmem.cpp | 38 +++++++- clients/nutclientmem.h | 10 +- 4 files changed, 316 insertions(+), 63 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 0dd0cb8897..7c359c58f0 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -27,6 +27,8 @@ #include "nutclient.h" #include +#include +#include /* TODO: Make it a run-time option like upsdebugx(), * probably with a verbosity level variable in each @@ -1857,20 +1859,20 @@ std::map > > TcpClient return map; } -TrackingID TcpClient::setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) +TrackingID TcpClient::setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec, int waitMaxCount) { std::string query = "SET VAR " + dev + " " + name + " " + escape(value); - return sendTrackingQuery(query); + return sendTrackingQuery(query, waitIntervalSec, waitMaxCount); } -TrackingID TcpClient::setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values) +TrackingID TcpClient::setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values, int waitIntervalSec, int waitMaxCount) { std::string query = "SET VAR " + dev + " " + name; for(size_t n=0; n TcpClient::getDeviceCommandNames(const std::string& dev) @@ -1891,9 +1893,9 @@ std::string TcpClient::getDeviceCommandDescription(const std::string& dev, const return get("CMDDESC", dev + " " + name)[0]; } -TrackingID TcpClient::executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param) +TrackingID TcpClient::executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param, int waitIntervalSec, int waitMaxCount) { - return sendTrackingQuery("INSTCMD " + dev + " " + name + " " + param); + return sendTrackingQuery("INSTCMD " + dev + " " + name + " " + param, waitIntervalSec, waitMaxCount); } std::map> TcpClient::listDeviceClients(void) @@ -1983,11 +1985,13 @@ TrackingResult TcpClient::getTrackingResult(const TrackingID& id) { if (id.empty()) { - return TrackingResult::SUCCESS; + return TrackingResult::UNSET; + /* TOTHINK // return TrackingResult::SUCCESS;*/ } - std::string result = sendQuery("GET TRACKING " + id); + std::string result = sendQuery("GET TRACKING " + id.id()); + /* TOTHINK: Update id.setStatus() ? */ if (result == "PENDING") { return TrackingResult::PENDING; @@ -2010,6 +2014,48 @@ TrackingResult TcpClient::getTrackingResult(const TrackingID& id) } } +void TcpClient::enableTrackingModeOnce() +{ + setFeature(TRACKING, true); +} + +TrackingResult TcpClient::waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) +{ + if (id.empty()) + { + return TrackingResult::SUCCESS; + } + + int count = 0; + while (true) + { + TrackingResult res = getTrackingResult(id); + if (res != TrackingResult::PENDING) + { + return res; + } + + if (waitMaxCount > 0 && ++count >= waitMaxCount) + { + return TrackingResult::PENDING; + } + + if (waitIntervalSec > 0) + { + std::this_thread::sleep_for(std::chrono::seconds(waitIntervalSec)); + } + else + { + // Default to some small sleep if not specified but we are waiting? + // Actually Perl/Python just loop or use the interval. + // If interval is 0, we might busy loop, which is bad. + // But let's follow the provided parameters. + if (waitIntervalSec == 0) break; + } + } + return TrackingResult::PENDING; +} + bool TcpClient::isFeatureEnabled(const Feature& feature) { std::string result = sendQuery("GET " + feature); @@ -2273,8 +2319,13 @@ std::string TcpClient::escape(const std::string& str) return res; } -TrackingID TcpClient::sendTrackingQuery(const std::string& req) +TrackingID TcpClient::sendTrackingQuery(const std::string& req, int waitIntervalSec, int waitMaxCount) { + if (waitIntervalSec > 0) + { + enableTrackingModeOnce(); + } + std::string reply = sendQuery(req); detectError(reply); std::vector res = explode(reply); @@ -2285,7 +2336,50 @@ TrackingID TcpClient::sendTrackingQuery(const std::string& req) } else if (res.size() == 3 && res[0] == "OK" && res[1] == "TRACKING") { - return TrackingID(res[2]); + TrackingID id = TrackingID(res[2]); + if (waitIntervalSec > 0 && !id.empty()) + { + TrackingResult wtres = waitTrackingResult(id, waitIntervalSec, waitMaxCount); + switch (wtres) { + case TrackingResult::SUCCESS: + case TrackingResult::UNSET: + case TrackingResult::PENDING: + id.setStatus(wtres); + break; + + case TrackingResult::UNKNOWN: + case TrackingResult::FAILURE: + case TrackingResult::INVALID_ARGUMENT: + id.setStatus(wtres); + throw NutException("TRACKING query failed: " + reply); + +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic push +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT +# pragma GCC diagnostic ignored "-Wcovered-switch-default" +#endif +#ifdef HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE +# pragma GCC diagnostic ignored "-Wunreachable-code" +#endif +/* Older CLANG (e.g. clang-3.4) seems to not support the GCC pragmas above */ +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunreachable-code" +# pragma clang diagnostic ignored "-Wcovered-switch-default" +#endif + default: + /* Must not occur. */ + throw NutException("TRACKING query failed: " + reply); +#ifdef __clang__ +# pragma clang diagnostic pop +#endif +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && ( (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_COVERED_SWITCH_DEFAULT) || (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_UNREACHABLE_CODE) ) +# pragma GCC diagnostic pop +#endif + } + } + return id; } else { @@ -2396,20 +2490,18 @@ std::set Device::getRWVariableNames() return getClient()->getDeviceRWVariableNames(getName()); } -void Device::setVariable(const std::string& name, const std::string& value) +TrackingID Device::setVariable(const std::string& name, const std::string& value, int waitIntervalSec, int waitMaxCount) { if (!isOk()) throw NutException("Invalid device"); - getClient()->setDeviceVariable(getName(), name, value); + return getClient()->setDeviceVariable(getName(), name, value, waitIntervalSec, waitMaxCount); } -void Device::setVariable(const std::string& name, const std::vector& values) +TrackingID Device::setVariable(const std::string& name, const std::vector& values, int waitIntervalSec, int waitMaxCount) { if (!isOk()) throw NutException("Invalid device"); - getClient()->setDeviceVariable(getName(), name, values); + return getClient()->setDeviceVariable(getName(), name, values, waitIntervalSec, waitMaxCount); } - - Variable Device::getVariable(const std::string& name) { if (!isOk()) throw NutException("Invalid device"); @@ -2475,10 +2567,10 @@ Command Device::getCommand(const std::string& name) return Command(nullptr, ""); } -TrackingID Device::executeCommand(const std::string& name, const std::string& param) +TrackingID Device::executeCommand(const std::string& name, const std::string& param, int waitIntervalSec, int waitMaxCount) { if (!isOk()) throw NutException("Invalid device"); - return getClient()->executeDeviceCommand(getName(), name, param); + return getClient()->executeDeviceCommand(getName(), name, param, waitIntervalSec, waitMaxCount); } std::set Device::getClients() @@ -2603,14 +2695,14 @@ std::string Variable::getDescription() return getDevice()->getClient()->getDeviceVariableDescription(getDevice()->getName(), getName()); } -void Variable::setValue(const std::string& value) +TrackingID Variable::setValue(const std::string& value, int waitIntervalSec, int waitMaxCount) { - getDevice()->setVariable(getName(), value); + return getDevice()->setVariable(getName(), value, waitIntervalSec, waitMaxCount); } -void Variable::setValues(const std::vector& values) +TrackingID Variable::setValues(const std::vector& values, int waitIntervalSec, int waitMaxCount) { - getDevice()->setVariable(getName(), values); + return getDevice()->setVariable(getName(), values, waitIntervalSec, waitMaxCount); } @@ -2692,9 +2784,9 @@ std::string Command::getDescription() return getDevice()->getClient()->getDeviceCommandDescription(getDevice()->getName(), getName()); } -void Command::execute(const std::string& param) +TrackingID Command::execute(const std::string& param, int waitIntervalSec, int waitMaxCount) { - getDevice()->executeCommand(getName(), param); + return getDevice()->executeCommand(getName(), param, waitIntervalSec, waitMaxCount); } } /* namespace nut */ @@ -3488,7 +3580,23 @@ void nutclient_set_device_variable_value(NUTCLIENT_t client, const char* dev, co { try { - cl->setDeviceVariable(dev, var, value); + cl->setDeviceVariable(dev, var, value, -1, -1); + } + catch(...){} + } + } +} + +void nutclient_set_device_variable_value_wait(NUTCLIENT_t client, const char* dev, const char* var, const char* value, int waitIntervalSec, int waitMaxCount) +{ + if(client) + { + nut::Client* cl = static_cast(client); + if(cl) + { + try + { + cl->setDeviceVariable(dev, var, value, waitIntervalSec, waitMaxCount); } catch(...){} } @@ -3519,6 +3627,30 @@ void nutclient_set_device_variable_values(NUTCLIENT_t client, const char* dev, c } } +void nutclient_set_device_variable_values_wait(NUTCLIENT_t client, const char* dev, const char* var, const strarr values, int waitIntervalSec, int waitMaxCount) +{ + if(client) + { + nut::Client* cl = static_cast(client); + if(cl) + { + try + { + std::vector vals; + strarr pstr = static_cast(values); + while(*pstr) + { + vals.push_back(std::string(*pstr)); + ++pstr; + } + + cl->setDeviceVariable(dev, var, vals, waitIntervalSec, waitMaxCount); + } + catch(...){} + } + } +} + strarr nutclient_get_device_commands(NUTCLIENT_t client, const char* dev) { if(client) @@ -3579,7 +3711,23 @@ void nutclient_execute_device_command(NUTCLIENT_t client, const char* dev, const { try { - cl->executeDeviceCommand(dev, cmd, param); + cl->executeDeviceCommand(dev, cmd, param, -1, -1); + } + catch(...){} + } + } +} + +void nutclient_execute_device_command_wait(NUTCLIENT_t client, const char* dev, const char* cmd, const char* param, int waitIntervalSec, int waitMaxCount) +{ + if(client) + { + nut::Client* cl = static_cast(client); + if(cl) + { + try + { + cl->executeDeviceCommand(dev, cmd, param, waitIntervalSec, waitMaxCount); } catch(...){} } diff --git a/clients/nutclient.h b/clients/nutclient.h index 7635fe4c5e..bf74b13d19 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -286,20 +286,38 @@ class TimeoutException : public IOException virtual ~TimeoutException() noexcept override; }; +/** + * Result of an async action. + */ +typedef enum +{ + UNSET, + UNKNOWN, + PENDING, + SUCCESS, + INVALID_ARGUMENT, + FAILURE, +} TrackingResult; + /** * Cookie given when performing async action, used to redeem result at a later date. */ class TrackingID { public: - TrackingID(const std::string& id = "") : _id(id), _created(std::time(nullptr)) {} - virtual ~TrackingID() {} + TrackingID(const std::string& id = "", TrackingResult status = UNSET) : _id(id), _status(status), _created(std::time(nullptr)), _finished(0) {} const std::string& id() const { return _id; } std::time_t created() const { return _created; } + std::time_t finished() const { return _finished; } double age() const { return std::difftime(std::time(nullptr), _created); } + double duration() const { if (_finished > 0) { return std::difftime(_finished, _created); } else { return -1; } } bool isValid() const { return !_id.empty(); } + bool empty() const { return _id.empty(); } + + void setStatus(TrackingResult status) { _status = status; if (status != TrackingResult::PENDING && status != TrackingResult::UNSET) { _finished = std::time(nullptr); } } + TrackingResult getStatus() const { return _status; } operator std::string() const { return _id; } bool operator==(const TrackingID& other) const { return _id == other._id; } @@ -307,21 +325,11 @@ class TrackingID private: std::string _id; + TrackingResult _status; std::time_t _created; + std::time_t _finished; }; -/** - * Result of an async action. - */ -typedef enum -{ - UNKNOWN, - PENDING, - SUCCESS, - INVALID_ARGUMENT, - FAILURE, -} TrackingResult; - typedef std::string Feature; /** @@ -445,14 +453,14 @@ class Client * \param name Variable name * \param value Variable value */ - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) = 0; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0) = 0; /** * Intend to set the value of a variable. * \param dev Device name * \param name Variable name * \param values Vector of variable values */ - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values) = 0; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values, int waitIntervalSec = 0, int waitMaxCount = 0) = 0; /** \} */ /** @@ -486,7 +494,7 @@ class Client * \param name Command name * \param param Additional command parameter */ - virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="") = 0; + virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0) = 0; /** \} */ /** @@ -525,12 +533,38 @@ class Client */ virtual std::map> listDeviceClients(void) = 0; + /** + * Retrieve the result of a tracking ID. + * \param id Tracking ID. + */ + virtual TrackingResult getTrackingResult(const std::string id) { return getTrackingResult(TrackingID(id)); } + + /** + * Retrieve the result of a tracking ID. + * \param id Tracking ID. + */ + virtual TrackingResult getTrackingResult(const char *id) { return getTrackingResult(TrackingID(std::string(id))); } + /** * Retrieve the result of a tracking ID. * \param id Tracking ID. */ virtual TrackingResult getTrackingResult(const TrackingID& id) = 0; + /** + * Enable tracking mode once. + */ + virtual void enableTrackingModeOnce() = 0; + + /** + * Wait for a tracking result. + * \param id Tracking ID to wait for. + * \param waitIntervalSec Interval between checks in seconds. + * \param waitMaxCount Maximum number of checks. + * \return The tracking result. + */ + virtual TrackingResult waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) = 0; + virtual bool hasFeature(const Feature& feature); virtual bool isFeatureEnabled(const Feature& feature) = 0; virtual void setFeature(const Feature& feature, bool status) = 0; @@ -665,12 +699,12 @@ class TcpClient : public Client virtual std::vector getDeviceVariableValue(const std::string& dev, const std::string& name) override; virtual std::map > getDeviceVariableValues(const std::string& dev) override; virtual std::map > > getDevicesVariableValues(const std::set& devs) override; - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) override; - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values) override; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0) override; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::vector& values, int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual std::set getDeviceCommandNames(const std::string& dev) override; virtual std::string getDeviceCommandDescription(const std::string& dev, const std::string& name) override; - virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="") override; + virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual void deviceLogin(const std::string& dev) override; /* FIXME: Protocol update needed to handle master/primary alias @@ -684,7 +718,11 @@ class TcpClient : public Client virtual std::map> listDeviceClients(void) override; + using Client::getTrackingResult; + virtual TrackingResult getTrackingResult(const TrackingID& id) override; + virtual void enableTrackingModeOnce() override; + virtual TrackingResult waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) override; /** * Return a bitmask of SSL capabilities supported by this build of @@ -746,7 +784,7 @@ class TcpClient : public Client std::string sendQuery(const std::string& req); void sendAsyncQueries(const std::vector& req); static void detectError(const std::string& req); - TrackingID sendTrackingQuery(const std::string& req); + TrackingID sendTrackingQuery(const std::string& req, int waitIntervalSec, int waitMaxCount); std::vector get(const std::string& subcmd, const std::string& params = ""); @@ -917,13 +955,13 @@ class Device * \param name Variable name. * \param value New variable value. */ - void setVariable(const std::string& name, const std::string& value); + TrackingID setVariable(const std::string& name, const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0); /** * Intend to set values of a variable of the device. * \param name Variable name. * \param values Vector of new variable values. */ - void setVariable(const std::string& name, const std::vector& values); + TrackingID setVariable(const std::string& name, const std::vector& values, int waitIntervalSec = 0, int waitMaxCount = 0); /** * Retrieve a Variable object representing the specified variable. @@ -963,7 +1001,7 @@ class Device * \param name Command name. * \param param Additional command parameter */ - TrackingID executeCommand(const std::string& name, const std::string& param=""); + TrackingID executeCommand(const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0); /** * Login current client's user for the device. @@ -1061,13 +1099,19 @@ class Variable /** * Intend to set a value to the variable. * \param value New variable value. + * \param waitIntervalSec If set, wait for result for this interval (seconds) + * \param waitMaxCount If set, wait for result up to this many times + * \return TrackingID if tracking is enabled. */ - void setValue(const std::string& value); + TrackingID setValue(const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0); /** * Intend to set (multiple) values to the variable. * \param values Vector of new variable values. + * \param waitIntervalSec If set, wait for result for this interval (seconds) + * \param waitMaxCount If set, wait for result up to this many times + * \return TrackingID if tracking is enabled. */ - void setValues(const std::vector& values); + TrackingID setValues(const std::vector& values, int waitIntervalSec = 0, int waitMaxCount = 0); protected: Variable(Device* dev, const std::string& name); @@ -1141,8 +1185,11 @@ class Command /** * Intend to execute the instant command on device. * \param param Optional additional command parameter + * \param waitIntervalSec If set, wait for result for this interval (seconds) + * \param waitMaxCount If set, wait for result up to this many times + * \return TrackingID if tracking is enabled. */ - void execute(const std::string& param=""); + TrackingID execute(const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0); protected: Command(Device* dev, const std::string& name); @@ -1324,14 +1371,32 @@ strarr nutclient_get_device_variable_values(NUTCLIENT_t client, const char* dev, void nutclient_set_device_variable_value(NUTCLIENT_t client, const char* dev, const char* var, const char* value); /** - * Intend to set device variable multiple values. + * Intend to set device variable value and wait for result. + * \param client Nut client handle. + * \param dev Device name. + * \param var Variable name. + * \param value Value to set. + */ +void nutclient_set_device_variable_value_wait(NUTCLIENT_t client, const char* dev, const char* var, const char* value, int waitIntervalSec, int waitMaxCount); + +/** + * Intend to set device variable multiple values. * \param client Nut client handle. * \param dev Device name. * \param var Variable name. - * \param values Values to set. The cller is responsible to free it after call. + * \param values Values to set. The caller is responsible to free it after call. */ void nutclient_set_device_variable_values(NUTCLIENT_t client, const char* dev, const char* var, const strarr values); +/** + * Intend to set device variable multiple values and wait for result. + * \param client Nut client handle. + * \param dev Device name. + * \param var Variable name. + * \param values Values to set. The caller is responsible to free it after call. + */ +void nutclient_set_device_variable_values_wait(NUTCLIENT_t client, const char* dev, const char* var, const strarr values, int waitIntervalSec, int waitMaxCount); + /** * Intend to retrieve device command names. * \param client Nut client handle. @@ -1366,6 +1431,14 @@ char* nutclient_get_device_command_description(NUTCLIENT_t client, const char* d */ void nutclient_execute_device_command(NUTCLIENT_t client, const char* dev, const char* cmd, const char* param=""); +/** + * Intend to execute device command and wait for result. + * \param client Nut client handle. + * \param dev Device name. + * \param cmd Command name. + */ +void nutclient_execute_device_command_wait(NUTCLIENT_t client, const char* dev, const char* cmd, const char* param="", int waitIntervalSec=-1, int waitMaxCount=-1); + /** \} */ diff --git a/clients/nutclientmem.cpp b/clients/nutclientmem.cpp index 1b8492a06e..0e7048a83a 100644 --- a/clients/nutclientmem.cpp +++ b/clients/nutclientmem.cpp @@ -110,8 +110,11 @@ ListDevice MemClientStub::getDevicesVariableValues(const std::set& return res; } -TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) +TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec, int waitMaxCount) { + NUT_UNUSED_VARIABLE(waitIntervalSec); + NUT_UNUSED_VARIABLE(waitMaxCount); + auto it_dev = _values.find(dev); if (it_dev == _values.end()) { @@ -134,11 +137,15 @@ TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::s map->emplace(name, list_value); } } - return ""; + + return TrackingID(""); } -TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::string& name, const ListValue& values) +TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::string& name, const ListValue& values, int waitIntervalSec, int waitMaxCount) { + NUT_UNUSED_VARIABLE(waitIntervalSec); + NUT_UNUSED_VARIABLE(waitMaxCount); + auto it_dev = _values.find(dev); if (it_dev != _values.end()) { @@ -153,7 +160,8 @@ TrackingID MemClientStub::setDeviceVariable(const std::string& dev, const std::s map->emplace(name, values); } } - return ""; + + return TrackingID(""); } std::set MemClientStub::getDeviceCommandNames(const std::string& dev) @@ -169,11 +177,13 @@ std::string MemClientStub::getDeviceCommandDescription(const std::string& dev, c throw NutException("Not implemented"); } -TrackingID MemClientStub::executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param) +TrackingID MemClientStub::executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param, int waitIntervalSec, int waitMaxCount) { NUT_UNUSED_VARIABLE(dev); NUT_UNUSED_VARIABLE(name); NUT_UNUSED_VARIABLE(param); + NUT_UNUSED_VARIABLE(waitIntervalSec); + NUT_UNUSED_VARIABLE(waitMaxCount); throw NutException("Not implemented"); } @@ -230,6 +240,24 @@ TrackingResult MemClientStub::getTrackingResult(const TrackingID& id) //return TrackingResult::SUCCESS; } +void MemClientStub::enableTrackingModeOnce(void) +{ + /* Hush warning: function 'enableTrackingModeOnce' could be declared with attribute 'noreturn' [-Wmissing-noreturn] */ + int id; + NUT_UNUSED_VARIABLE(id); + throw NutException("Not implemented"); + //return TrackingResult::SUCCESS; +} + +TrackingResult MemClientStub::waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) +{ + NUT_UNUSED_VARIABLE(id); + NUT_UNUSED_VARIABLE(waitIntervalSec); + NUT_UNUSED_VARIABLE(waitMaxCount); + throw NutException("Not implemented"); + //return TrackingResult::SUCCESS; +} + bool MemClientStub::isFeatureEnabled(const Feature& feature) { NUT_UNUSED_VARIABLE(feature); diff --git a/clients/nutclientmem.h b/clients/nutclientmem.h index 8584a9ca45..1f13f8758e 100644 --- a/clients/nutclientmem.h +++ b/clients/nutclientmem.h @@ -61,12 +61,12 @@ class MemClientStub : public Client virtual ListValue getDeviceVariableValue(const std::string& dev, const std::string& name) override; virtual ListObject getDeviceVariableValues(const std::string& dev) override; virtual ListDevice getDevicesVariableValues(const std::set& devs) override; - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value) override; - virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const ListValue& values) override; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const std::string& value, int waitIntervalSec = 0, int waitMaxCount = 0) override; + virtual TrackingID setDeviceVariable(const std::string& dev, const std::string& name, const ListValue& values, int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual std::set getDeviceCommandNames(const std::string& dev) override; virtual std::string getDeviceCommandDescription(const std::string& dev, const std::string& name) override; - virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="") override; + virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual void deviceLogin(const std::string& dev) override; /* Note: "master" is deprecated, but supported @@ -78,7 +78,11 @@ class MemClientStub : public Client virtual std::set deviceGetClients(const std::string& dev) override; virtual std::map> listDeviceClients(void) override; + using Client::getTrackingResult; + virtual TrackingResult getTrackingResult(const TrackingID& id) override; + virtual void enableTrackingModeOnce(void) override; + virtual TrackingResult waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) override; virtual bool isFeatureEnabled(const Feature& feature) override; virtual void setFeature(const Feature& feature, bool status) override; From 3eb777baa4f7ea5ba3f791d8b406d0a35c876951 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 23:32:58 +0200 Subject: [PATCH 10/70] scripts/perl/Nut.pm: TrackingID class: include timestamp tracking and age reporting [#1348] Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index 4047c63921..761cf88295 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -1233,7 +1233,10 @@ use strict; sub new { my $class = shift; my $id = shift; - my $self = { id => $id }; + my $self = { + id => $id, + created => time() + }; bless $self, $class; return $self; } @@ -1243,6 +1246,16 @@ sub id { return $self->{id}; } +sub created { + my $self = shift; + return $self->{created}; +} + +sub age { + my $self = shift; + return time() - $self->{created}; +} + sub toString { my $self = shift; return $self->{id}; From 56e2e16b62d23fbb147a73a193c62e6605628619 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Wed, 8 Apr 2026 23:33:20 +0200 Subject: [PATCH 11/70] scripts/python/module/PyNUT.py.in: TrackingID class: include timestamp tracking and age reporting [#1349] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 5ebebb90f3..daf059cf4d 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -70,6 +70,7 @@ # NUT STARTTLS protocol action. import socket +import time ssl_available = False try: @@ -82,10 +83,17 @@ class TrackingID: """ Cookie given when performing async action, used to redeem result at a later date. """ def __init__(self, tracking_id): self.__id = tracking_id + self.__created = time.time() def id(self): return self.__id + def created(self): + return self.__created + + def age(self): + return time.time() - self.__created + def __str__(self): return self.__id From 990397b3a106361cc9926cb155f978120df97970 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 00:04:04 +0200 Subject: [PATCH 12/70] scripts/python/module/test_nutclient.py.in: add test with SET VAR TRACKING [#1711, #1349] Signed-off-by: Jim Klimov --- scripts/python/module/test_nutclient.py.in | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/scripts/python/module/test_nutclient.py.in b/scripts/python/module/test_nutclient.py.in index f92f4baae0..28953b70c4 100755 --- a/scripts/python/module/test_nutclient.py.in +++ b/scripts/python/module/test_nutclient.py.in @@ -101,6 +101,36 @@ if __name__ == "__main__" : failed.append('SetUPSVar') print( "\033[01;33m%s\033[0m\n" % result ) + print( 80*"-" + "\nTesting 'SetRWVar' with TRACKING for 'driver.debug' (1s interval, 10s timeout):") + try : + # Note: UPS1 is often used in NUT tests as a common device name + # Also try 'dummy' which is used in this test script + target_ups = "UPS1" + try: + nut.GetUPSVars(target_ups) + except: + target_ups = "dummy" + + tid = nut.SetRWVar( target_ups, "driver.debug", "1", wait_interval_sec=1, wait_max_count=10 ) + if (NUT_USER is None): + raise AssertionError("Secure operation should have failed due to lack of credentials, but did not") + + if isinstance(tid, PyNUT.TrackingID): + print( "Got TRACKING ID: %s, created: %s, age: %0.2fs" % (str(tid), tid.created(), tid.age()) ) + result = str(tid) + else: + result = tid + except : + ex = str(sys.exc_info()[1]) + result = "EXCEPTION: " + ex + if (NUT_USER is None and ex == 'ERR USERNAME-REQUIRED'): + result = result + "\n(anticipated error: no credentials were provided)" + else: + if (ex != 'ERR VAR-NOT-SUPPORTED' and (NUT_USER is not None and ex != 'ERR ACCESS-DENIED') ): + result = result + "\nTEST-CASE FAILED" + failed.append('SetUPSVar-Tracking') + print( "\033[01;33m%s\033[0m\n" % result ) + # testing who has an upsmon-like log-in session to a device print( 80*"-" + "\nTesting 'ListClients' for 'dummy' (should be registered in upsd.conf) before test client is connected :") try : From 4fd7a5115a41fcb049018e7d3fb03cf8a2db9f64 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 00:05:22 +0200 Subject: [PATCH 13/70] tests/cpputest-client.cpp: add test with SET VAR TRACKING [#1711, #656] Signed-off-by: Jim Klimov --- tests/cpputest-client.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/cpputest-client.cpp b/tests/cpputest-client.cpp index 3fef89413d..ab9e91e09a 100644 --- a/tests/cpputest-client.cpp +++ b/tests/cpputest-client.cpp @@ -587,6 +587,31 @@ void NutActiveClientTest::test_auth_user() { if (noException) { std::cerr << "[D] Tweaked device variable value OK" << std::endl; } + + /* Specific test: SET VAR driver.debug 1 with TRACKING (1s interval, 10s timeout) */ + std::cerr << "[D] Testing SET VAR " << env_NUT_SETVAR_DEVICE << " driver.debug 1 with TRACKING..." << std::endl; + try { + tid = c.setDeviceVariable(env_NUT_SETVAR_DEVICE, "driver.debug", "1", 1, 10); + if (tid.empty()) { + std::cerr << "[D] Failed to get tracking ID for driver.debug" << std::endl; + noException = false; + } else { + tres = tid.getStatus(); + std::cerr << "[D] Got tracking ID: " << (std::string)tid << ", created: " << tid.created() << ", status: " << tres << std::endl; + if (tres == PENDING || tres == UNSET) + tres = c.getTrackingResult(tid); + std::cerr << "[D] Final tracking result: " << tres << " (age: " << tid.age() << "s, duration: " << tid.duration() << "s)" << std::endl; + if (tres != SUCCESS) { + std::cerr << "[D] TRACKING failed for driver.debug" << std::endl; + noException = false; + } + } + } + catch(nut::NutException& ex) + { + std::cerr << "[D] Failed to set driver.debug with tracking: " << ex.what() << std::endl; + noException = false; + } } catch(nut::NutException& ex) { From 53a7bf753b49d847ef0a6a0be28fab0cf56b41fd Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 17:44:34 +0200 Subject: [PATCH 14/70] clients/nutclient{,mem}.{h,cpp}: rename primary()=>becomePrimary, add becomeSecondary() for completeness [#656] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 37 ++++++++++++++++++++++++++++++++++++- clients/nutclient.h | 28 +++++++++++++++++----------- clients/nutclientmem.cpp | 6 ++++++ clients/nutclientmem.h | 3 ++- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 7c359c58f0..11b9fc982f 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -1970,6 +1970,20 @@ void TcpClient::devicePrimary(const std::string& dev) } } +void TcpClient::deviceSecondary(const std::string& dev) +{ + try { + detectError(sendQuery("SECONDARY " + dev)); + } catch (NutException &exOrig) { + try { + detectError(sendQuery("SLAVE " + dev)); + } catch (NutException &exRetry) { + NUT_UNUSED_VARIABLE(exRetry); + throw exOrig; + } + } +} + void TcpClient::deviceForcedShutdown(const std::string& dev) { detectError(sendQuery("FSD " + dev)); @@ -2593,12 +2607,18 @@ void Device::master() getClient()->deviceMaster(getName()); } -void Device::primary() +void Device::becomePrimary() { if (!isOk()) throw NutException("Invalid device"); getClient()->devicePrimary(getName()); } +void Device::becomeSecondary() +{ + if (!isOk()) throw NutException("Invalid device"); + getClient()->deviceSecondary(getName()); +} + void Device::forcedShutdown() { if (!isOk()) throw NutException("Invalid device"); @@ -3419,6 +3439,21 @@ void nutclient_device_primary(NUTCLIENT_t client, const char* dev) } } +void nutclient_device_secondary(NUTCLIENT_t client, const char* dev) +{ + if(client) + { + nut::Client* cl = static_cast(client); + if(cl) + { + try + { + cl->deviceSecondary(dev); + } + catch(...){} + } + } +} void nutclient_device_forced_shutdown(NUTCLIENT_t client, const char* dev) { if(client) diff --git a/clients/nutclient.h b/clients/nutclient.h index bf74b13d19..f4d7d5128c 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -518,13 +518,17 @@ class Client * \return List of clients e.g. {'127.0.0.1', 'admin-workstation.local.domain'} */ virtual std::set deviceGetClients(const std::string& dev) = 0; - /* NOTE: "master" is deprecated since NUT v2.8.0 in favor of "primary". + /** NOTE: "master" is deprecated since NUT v2.8.0 in favor of "primary". * For the sake of old/new server/client interoperability, * practical implementations should try to use one and fall * back to the other, and only fail if both return "ERR". */ +#if (defined __cplusplus) && (__cplusplus >= 201400) + [[deprecated]] +#endif virtual void deviceMaster(const std::string& dev) = 0; virtual void devicePrimary(const std::string& dev) = 0; + virtual void deviceSecondary(const std::string& dev) = 0; virtual void deviceForcedShutdown(const std::string& dev) = 0; /** @@ -707,11 +711,12 @@ class TcpClient : public Client virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual void deviceLogin(const std::string& dev) override; - /* FIXME: Protocol update needed to handle master/primary alias - * and probably an API bump also, to rename/alias the routine. - */ +#if (defined __cplusplus) && (__cplusplus >= 201400) + [[deprecated]] +#endif virtual void deviceMaster(const std::string& dev) override; virtual void devicePrimary(const std::string& dev) override; + virtual void deviceSecondary(const std::string& dev) override; virtual void deviceForcedShutdown(const std::string& dev) override; virtual int deviceGetNumLogins(const std::string& dev) override; virtual std::set deviceGetClients(const std::string& dev) override; @@ -1011,11 +1016,14 @@ class Device * Who did a login() to this dev? */ std::set getClients(); - /* FIXME: Protocol update needed to handle master/primary alias - * and probably an API bump also, to rename/alias the routine. - */ + +#if (defined __cplusplus) && (__cplusplus >= 201400) + [[deprecated]] +#endif void master(); - void primary(); + void becomePrimary(); + void becomeSecondary(); + void forcedShutdown(); /** * Retrieve the number of logged user for the device. @@ -1282,11 +1290,9 @@ int nutclient_get_device_num_logins(NUTCLIENT_t client, const char* dev); * \param client Nut client handle. * \param dev Device name to test. */ -/* FIXME: Protocol update needed to handle master/primary alias - * and probably an API bump also, to rename/alias the routine. - */ void nutclient_device_master(NUTCLIENT_t client, const char* dev); void nutclient_device_primary(NUTCLIENT_t client, const char* dev); +void nutclient_device_secondary(NUTCLIENT_t client, const char* dev); /** * Set the FSD flag for the device. diff --git a/clients/nutclientmem.cpp b/clients/nutclientmem.cpp index 0e7048a83a..7fe6114759 100644 --- a/clients/nutclientmem.cpp +++ b/clients/nutclientmem.cpp @@ -221,6 +221,12 @@ void MemClientStub::devicePrimary(const std::string& dev) throw NutException("Not implemented"); } +void MemClientStub::deviceSecondary(const std::string& dev) +{ + NUT_UNUSED_VARIABLE(dev); + throw NutException("Not implemented"); +} + void MemClientStub::deviceForcedShutdown(const std::string& dev) { NUT_UNUSED_VARIABLE(dev); diff --git a/clients/nutclientmem.h b/clients/nutclientmem.h index 1f13f8758e..95932677aa 100644 --- a/clients/nutclientmem.h +++ b/clients/nutclientmem.h @@ -69,10 +69,11 @@ class MemClientStub : public Client virtual TrackingID executeDeviceCommand(const std::string& dev, const std::string& name, const std::string& param="", int waitIntervalSec = 0, int waitMaxCount = 0) override; virtual void deviceLogin(const std::string& dev) override; - /* Note: "master" is deprecated, but supported + /** Note: "master" is deprecated, but supported * for mixing old/new client/server combos: */ virtual void deviceMaster(const std::string& dev) override; virtual void devicePrimary(const std::string& dev) override; + virtual void deviceSecondary(const std::string& dev) override; virtual void deviceForcedShutdown(const std::string& dev) override; virtual int deviceGetNumLogins(const std::string& dev) override; virtual std::set deviceGetClients(const std::string& dev) override; From c078629f246b85c204dae83db91156c90d0db22b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 18:37:15 +0200 Subject: [PATCH 15/70] scripts/perl/Nut.pm: rename primary()=>becomePrimary, add becomeSecondary() for completeness [#1348] Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index 761cf88295..c55822d462 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -840,7 +840,7 @@ sub Error { # what was the last thing that went bang? else { return "No error explanation available."; } } -sub Primary { goto &Master; } +sub becomePrimary { goto &Master; } sub Master { # check for MASTER level access # Author: Kit Peters @@ -883,6 +883,47 @@ sub Master { # check for MASTER level access } } +sub becomeSecondary { # check for SLAVE level access + # Author: Kit Peters + # ### changelog: uses the new _send command + # ### changelog: 8/3/2002 - KP - Master() returns undef rather than 0 on + # ### failure. this makes it consistent with other methods + # + # NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY + # (and backwards-compatible alias handling) + my $self = shift; + + my $req = "SECONDARY $self->{name}"; # build request + my $ans = $self->_send( $req ); + + unless (defined $ans) { + $self->{err} = "Network error: $!"; + return undef; + }; + + if ($ans =~ /^OK/) { # access granted + $self->_debug("SECONDARY level access granted. Upsd reports: $ans"); + return 1; + } + + # Retry with SLAVE if SECONDARY failed + $req = "SLAVE $self->{name}"; + $ans = $self->_send( $req ); + unless (defined $ans) { + $self->{err} = "Network error: $!"; + return undef; + }; + + if ($ans =~ /^OK/) { # access granted + $self->_debug("SLAVE level access granted. Upsd reports: $ans"); + return 1; + } + else { # access denied, or unrecognized response + $self->{err} = "SECONDARY/SLAVE level access denied. Upsd responded: $ans"; + return undef; + } +} + sub AUTOLOAD { # Contributor: Wayne Wylupski my $self = shift; From 48f76740889c89086d0e694275527f05f33e1134 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 18:37:32 +0200 Subject: [PATCH 16/70] scripts/perl/Nut.pm: fix typos in comments Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index c55822d462..9a90042f78 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -877,7 +877,7 @@ sub Master { # check for MASTER level access $self->_debug("MASTER level access granted. Upsd reports: $ans"); return 1; } - else { # access denied, or unrecognized reponse + else { # access denied, or unrecognized response $self->{err} = "PRIMARY/MASTER level access denied. Upsd responded: $ans"; return undef; } From 73cbaf33c26df59b366cce499f7061c0a2f32c7f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 18:37:50 +0200 Subject: [PATCH 17/70] scripts/perl/Nut.pm: add warnings toggle Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index 9a90042f78..9a0235eba0 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -11,6 +11,7 @@ package UPS::Nut; use strict; +use warnings FATAL => 'all'; use Carp; use FileHandle; use IO::Socket; From bbdc4a2ff6f59e2c3dab5a7c95d07ef79144567d Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 19:25:57 +0200 Subject: [PATCH 18/70] scripts/perl/Nut.pm: becomeSecondary() was overkill, there is no such verb in NUT protocol (LOGIN already reaches that role level) Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 45 ++++----------------------------------------- 1 file changed, 4 insertions(+), 41 deletions(-) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index 9a0235eba0..8297e64e2e 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -163,6 +163,10 @@ sub StartTLS { sub Login { # login to upsd, so that it won't shutdown unless we say we're # ok. This should only be used if you're actually connected # to the ups that upsd is monitoring. + # This assumes the upsmon SECONDARY role so we can be alerted + # and initiate a shutdown if someone else sends the FSD command + # to this UPS; we can further becomePrimary() if we are the + # system which manages it. # Author: Kit Peters # ### changelog: modified login logic a bit. Now it doesn't check to see @@ -884,47 +888,6 @@ sub Master { # check for MASTER level access } } -sub becomeSecondary { # check for SLAVE level access - # Author: Kit Peters - # ### changelog: uses the new _send command - # ### changelog: 8/3/2002 - KP - Master() returns undef rather than 0 on - # ### failure. this makes it consistent with other methods - # - # NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY - # (and backwards-compatible alias handling) - my $self = shift; - - my $req = "SECONDARY $self->{name}"; # build request - my $ans = $self->_send( $req ); - - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^OK/) { # access granted - $self->_debug("SECONDARY level access granted. Upsd reports: $ans"); - return 1; - } - - # Retry with SLAVE if SECONDARY failed - $req = "SLAVE $self->{name}"; - $ans = $self->_send( $req ); - unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; - }; - - if ($ans =~ /^OK/) { # access granted - $self->_debug("SLAVE level access granted. Upsd reports: $ans"); - return 1; - } - else { # access denied, or unrecognized response - $self->{err} = "SECONDARY/SLAVE level access denied. Upsd responded: $ans"; - return undef; - } -} - sub AUTOLOAD { # Contributor: Wayne Wylupski my $self = shift; From 7746fa6b86f861fc63345aad09ff326b9f21801c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 19:27:16 +0200 Subject: [PATCH 19/70] scripts/python/module/PyNUT.py.in: refactor FSD() with an explicit becomePrimary() method Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 44 ++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index daf059cf4d..fd8e580a9a 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -680,11 +680,16 @@ Returns OK on success or raises an error def DeviceLogin( self, ups="") : """ Establish a login session with a device (like upsmon does) +This assumes the upsmon SECONDARY role so we can be alerted and initiate a shutdown +if someone else sends the FSD command to this UPS; we can further becomePrimary() +if we are the system which manages it. + Returns OK on success or raises an error + USERNAME and PASSWORD must have been specified earlier in the session (once) and upsd.conf should permit that user with one of `upsmon` role types. -Note there is no "device LOGOUT" in the protocol, just one for general end +Note there is no "device LOGOUT" in the protocol, just one for a general end of connection. """ @@ -707,29 +712,42 @@ of connection. else : raise PyNUTError( result.replace( b"\n", b"" ).decode('ascii') ) - def FSD( self, ups="") : - """ Send FSD command + def becomePrimary( self, ups="") : + """ Assume the upsmon PRIMARY role so we can send FSD command Returns OK on success or raises an error NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY (and backwards-compatible alias handling) """ - if self.__debug : print( "[DEBUG] PRIMARY called..." ) self.__srv_handler.send( ("PRIMARY %s\n" % ups).encode('ascii') ) result = self.__read_until( b"\n" ) - if ( result != b"OK PRIMARY-GRANTED\n" ) : - if self.__debug : - print( "[DEBUG] Retrying: MASTER called..." ) - self.__srv_handler.send( ("MASTER %s\n" % ups).encode('ascii') ) - result = self.__read_until( b"\n" ) - if ( result != b"OK MASTER-GRANTED\n" ) : - if self.__debug : - print( "[DEBUG] Primary level functions are not available" ) - raise PyNUTError( "ERR ACCESS-DENIED" ) + 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') ) + result = self.__read_until( b"\n" ) + if ( result == b"OK MASTER-GRANTED\n" ) : + return( "OK" ) + + if self.__debug : + print( "[DEBUG] Primary level functions are not available" ) + raise PyNUTError( "ERR ACCESS-DENIED" ) + + def FSD( self, ups="", becomePrimaryFirst = True) : + """ Send FSD command + +Returns OK on success or raises an error + """ + + if becomePrimaryFirst: + # raises an error if not "OK", so not checking return value + self.becomePrimary(ups) if self.__debug : print( "[DEBUG] FSD called..." ) From f447072a0da2b191e021732ea96048a7f70c5537 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 19:31:30 +0200 Subject: [PATCH 20/70] clients/nutclient{,mem}.{h,cpp}: becomeSecondary() was overkill, there is no such verb in NUT protocol (LOGIN already reaches that role level) Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 35 ----------------------------------- clients/nutclient.h | 10 ++++------ clients/nutclientmem.cpp | 6 ------ clients/nutclientmem.h | 1 - 4 files changed, 4 insertions(+), 48 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 11b9fc982f..c40797ee98 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -1970,20 +1970,6 @@ void TcpClient::devicePrimary(const std::string& dev) } } -void TcpClient::deviceSecondary(const std::string& dev) -{ - try { - detectError(sendQuery("SECONDARY " + dev)); - } catch (NutException &exOrig) { - try { - detectError(sendQuery("SLAVE " + dev)); - } catch (NutException &exRetry) { - NUT_UNUSED_VARIABLE(exRetry); - throw exOrig; - } - } -} - void TcpClient::deviceForcedShutdown(const std::string& dev) { detectError(sendQuery("FSD " + dev)); @@ -2613,12 +2599,6 @@ void Device::becomePrimary() getClient()->devicePrimary(getName()); } -void Device::becomeSecondary() -{ - if (!isOk()) throw NutException("Invalid device"); - getClient()->deviceSecondary(getName()); -} - void Device::forcedShutdown() { if (!isOk()) throw NutException("Invalid device"); @@ -3439,21 +3419,6 @@ void nutclient_device_primary(NUTCLIENT_t client, const char* dev) } } -void nutclient_device_secondary(NUTCLIENT_t client, const char* dev) -{ - if(client) - { - nut::Client* cl = static_cast(client); - if(cl) - { - try - { - cl->deviceSecondary(dev); - } - catch(...){} - } - } -} void nutclient_device_forced_shutdown(NUTCLIENT_t client, const char* dev) { if(client) diff --git a/clients/nutclient.h b/clients/nutclient.h index f4d7d5128c..485e72a390 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -502,7 +502,10 @@ class Client * \{ */ /** - * Log the current user (if authenticated) for a device. + * Log the current user (if authenticated) for a device, + * initially as equivalent to upsmon SECONDARY role for it. + * We can further becomePrimary() if we are the system + * which manages the device. * \param dev Device name. */ virtual void deviceLogin(const std::string& dev) = 0; @@ -528,7 +531,6 @@ class Client #endif virtual void deviceMaster(const std::string& dev) = 0; virtual void devicePrimary(const std::string& dev) = 0; - virtual void deviceSecondary(const std::string& dev) = 0; virtual void deviceForcedShutdown(const std::string& dev) = 0; /** @@ -716,7 +718,6 @@ class TcpClient : public Client #endif virtual void deviceMaster(const std::string& dev) override; virtual void devicePrimary(const std::string& dev) override; - virtual void deviceSecondary(const std::string& dev) override; virtual void deviceForcedShutdown(const std::string& dev) override; virtual int deviceGetNumLogins(const std::string& dev) override; virtual std::set deviceGetClients(const std::string& dev) override; @@ -1022,8 +1023,6 @@ class Device #endif void master(); void becomePrimary(); - void becomeSecondary(); - void forcedShutdown(); /** * Retrieve the number of logged user for the device. @@ -1292,7 +1291,6 @@ int nutclient_get_device_num_logins(NUTCLIENT_t client, const char* dev); */ void nutclient_device_master(NUTCLIENT_t client, const char* dev); void nutclient_device_primary(NUTCLIENT_t client, const char* dev); -void nutclient_device_secondary(NUTCLIENT_t client, const char* dev); /** * Set the FSD flag for the device. diff --git a/clients/nutclientmem.cpp b/clients/nutclientmem.cpp index 7fe6114759..0e7048a83a 100644 --- a/clients/nutclientmem.cpp +++ b/clients/nutclientmem.cpp @@ -221,12 +221,6 @@ void MemClientStub::devicePrimary(const std::string& dev) throw NutException("Not implemented"); } -void MemClientStub::deviceSecondary(const std::string& dev) -{ - NUT_UNUSED_VARIABLE(dev); - throw NutException("Not implemented"); -} - void MemClientStub::deviceForcedShutdown(const std::string& dev) { NUT_UNUSED_VARIABLE(dev); diff --git a/clients/nutclientmem.h b/clients/nutclientmem.h index 95932677aa..b2430c0279 100644 --- a/clients/nutclientmem.h +++ b/clients/nutclientmem.h @@ -73,7 +73,6 @@ class MemClientStub : public Client * for mixing old/new client/server combos: */ virtual void deviceMaster(const std::string& dev) override; virtual void devicePrimary(const std::string& dev) override; - virtual void deviceSecondary(const std::string& dev) override; virtual void deviceForcedShutdown(const std::string& dev) override; virtual int deviceGetNumLogins(const std::string& dev) override; virtual std::set deviceGetClients(const std::string& dev) override; From dbb1aeddd643947e30a27f610e7189d439099dc0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 20:31:56 +0200 Subject: [PATCH 21/70] Client libraries (C, C++, Python, PERL): introduce an isValidProtocolVersion() method to actively ping a connection [#3387] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 25 ++++++++++++++++ clients/nutclient.h | 7 +++++ clients/nutclientmem.cpp | 6 ++++ clients/nutclientmem.h | 2 ++ clients/upsclient.c | 48 +++++++++++++++++++++++++++++++ clients/upsclient.h | 4 +++ scripts/perl/Nut.pm | 28 ++++++++++++++++++ scripts/python/module/PyNUT.py.in | 30 +++++++++++++++++++ 8 files changed, 150 insertions(+) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index c40797ee98..50d0e87fdc 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -1720,6 +1720,31 @@ void TcpClient::logout() _socket->disconnect(); } +bool TcpClient::isValidProtocolVersion(const std::string& version_re) +{ + std::string version; + try { + version = sendQuery("PROTVER"); + } catch (NutException &ignored) { + NUT_UNUSED_VARIABLE(ignored); + /* Deprecated and hidden, but may be what ancient NUT servers say + * May throw if the error is due to (non-)connection */ + version = sendQuery("NETVER"); + } + + if (version_re.empty()) { + // Basic check for 1.0 through 1.3, as of NUT v2.8.2 + if (version == "1.0" || version == "1.1" || version == "1.2" || version == "1.3") { + return true; + } + } else { + // TODO: Regex + return (version_re == version); + } + + return false; +} + Device TcpClient::getDevice(const std::string& name) { try diff --git a/clients/nutclient.h b/clients/nutclient.h index 485e72a390..9bb2a4f04a 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -361,6 +361,11 @@ class Client */ virtual void logout() = 0; + /** Query the (already established) connection to UPSD for its version + * and check it against given expectations. + */ + virtual bool isValidProtocolVersion(const std::string& version_re = std::string()) = 0; + /** * Device manipulations. * \see nut::Device @@ -695,6 +700,8 @@ class TcpClient : public Client virtual void authenticate(const std::string& user, const std::string& passwd) override; virtual void logout() override; + virtual bool isValidProtocolVersion(const std::string& version_re = std::string()) override; + virtual Device getDevice(const std::string& name) override; virtual std::set getDeviceNames() override; virtual std::string getDeviceDescription(const std::string& name) override; diff --git a/clients/nutclientmem.cpp b/clients/nutclientmem.cpp index 0e7048a83a..82fa9b52b1 100644 --- a/clients/nutclientmem.cpp +++ b/clients/nutclientmem.cpp @@ -187,6 +187,12 @@ TrackingID MemClientStub::executeDeviceCommand(const std::string& dev, const std throw NutException("Not implemented"); } +bool MemClientStub::isValidProtocolVersion(const std::string& version_re) +{ + NUT_UNUSED_VARIABLE(version_re); + throw NutException("Not implemented"); +} + std::map> MemClientStub::listDeviceClients(void) { volatile bool not_implemented = true; diff --git a/clients/nutclientmem.h b/clients/nutclientmem.h index b2430c0279..8af365d2a1 100644 --- a/clients/nutclientmem.h +++ b/clients/nutclientmem.h @@ -51,6 +51,8 @@ class MemClientStub : public Client } virtual void logout() override {} + virtual bool isValidProtocolVersion(const std::string& version_re = std::string()) override; + virtual Device getDevice(const std::string& name) override; virtual std::set getDeviceNames() override; virtual std::string getDeviceDescription(const std::string& name) override; diff --git a/clients/upsclient.c b/clients/upsclient.c index 381b1252d8..d523f5b344 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -2163,6 +2163,54 @@ int upscli_splitaddr(const char *buf, char **hostname, uint16_t *port) return 0; } +int upscli_is_valid_protocol_version(UPSCONN_t *ups, const char *version_re) +{ + char version[UPSCLI_NETBUF_LEN]; + + if (!ups) { + return -1; + } + + net_write(ups, "PROTVER\n", 8, 0); + memset(version, 0, sizeof(version)); + if (net_read(ups, version, sizeof(version), DEFAULT_NETWORK_TIMEOUT) > 0) { + if (!strncmp(version, "ERR", 3)) { + version[0] = '\0'; + } + } + + if (!version[0]) { + /* Deprecated and hidden, but may be what ancient NUT servers say + * May throw if the error is due to (non-)connection */ + net_write(ups, "NETVER\n", 8, 0); + memset(version, 0, sizeof(version)); + if (net_read(ups, version, sizeof(version), DEFAULT_NETWORK_TIMEOUT) > 0) { + if (!strncmp(version, "ERR", 3)) { + version[0] = '\0'; + } + } + } + + if (!version[0]) { + upsdebugx(3, "%s: PROTVER and NETVER queries returned an error, assuming disconnection or non-compliant NUT server", __func__); + return -1; + } + + upsdebugx(3, "%s: PROTVER or NETVER returned '%s', matching against '%s'", + __func__, version, version_re); + + if (!version_re) { + /* Basic check for 1.0 through 1.3, as of NUT v2.8.2 */ + return ( + !strcmp(version, "1.0") || !strcmp(version, "1.1") || + !strcmp(version, "1.2") || !strcmp(version, "1.3") + ); + } + + // TODO: Regex + return (!strcmp(version_re, version)); +} + int upscli_disconnect(UPSCONN_t *ups) { char tmp[UPSCLI_NETBUF_LEN]; diff --git a/clients/upsclient.h b/clients/upsclient.h index e84c28c5f0..f10a93b41e 100644 --- a/clients/upsclient.h +++ b/clients/upsclient.h @@ -172,6 +172,10 @@ int upscli_disconnect(UPSCONN_t *ups); int upscli_fd(UPSCONN_t *ups); int upscli_upserror(UPSCONN_t *ups); +/** Query the (already established) connection to UPSD for its version + * and check it against given expectations. */ +int upscli_is_valid_protocol_version(UPSCONN_t *ups, const char *version_re); + /* returns 1 if SSL mode is active for this connection */ int upscli_ssl(UPSCONN_t *ups); diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index 8297e64e2e..f7f1cf4ae9 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -129,6 +129,34 @@ sub EnableTrackingModeOnce { return undef; } +sub isValidProtocolVersion { + # Sends a PROTVER/NETVER query using the active connection and + # returns True if the returned version string matches a valid + # NUT protocol version regex (defaults to an "X(.Y)" number aka + # "^\\d+(?:\\.\\d+)?$" if version_re is None). + my ($self, $version_re) = @_; + + my $ans = $self->_send("PROTVER"); + if (!defined $ans) { + # Deprecated and hidden, but may be what ancient NUT servers say + # May throw if the error is due to (non-)connection? + $ans = $self->_send("NETVER"); + } + + if (!defined $ans) { + return undef; + } + chomp $ans; + + if (!defined $version_re) { + # Valid versions as of NUT 2.8.2: 1.0, 1.1, 1.2, 1.3 + # Is it an X(.Y) number? + $version_re = qr/^\d+(\.\d+)?$/; + } + + return ($ans =~ $version_re); +} + sub StartTLS { my $self = shift; my %arg = @_; diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index fd8e580a9a..3d37b4141e 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -71,6 +71,7 @@ import socket import time +import re ssl_available = False try: @@ -374,6 +375,35 @@ if something goes wrong. return False + def isValidProtocolVersion(self, version_re = None): + """ Sends a PROTVER/NETVER query using the active connection and + returns True if the returned version string matches a valid + NUT protocol version regex (defaults to an "X(.Y)" number aka + "^\\d+(?:\\.\\d+)?$" if version_re is None). + """ + result = None + try: + self.__srv_handler.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') ) + result = self.__read_until( b"\n" ) + + if result is None: + return False + + if version_re is None: + # Currently supported: 1.0, 1.1, 1.2, 1.3 => r'^1\.[0-3]$' + # Is it an X(.Y) number? + version_re = r'^\d+(?:\.\d+)?$' + + if re.match(version_re, str(result)): + return True + + return False + def GetVariableType( self, ups="", var="" ) : """ Returns the type of the specified variable """ From 6f4ce1f60175a1571a06201fe0e8e92f5bf510d1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 21:07:33 +0200 Subject: [PATCH 22/70] Client libraries (C, C++, Python): use isValidProtocolVersion() method to actively ping a connection after STARTTLS claimed success (but handshake might be broken in fact) [#3387] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 14 ++++++++++++++ clients/upsclient.c | 30 ++++++++++++++++++++++++------ scripts/python/module/PyNUT.py.in | 11 +++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 50d0e87fdc..123cfe06e0 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -1499,7 +1499,21 @@ void TcpClient::connect() _socket->setSSLConfig_NSS(_force_ssl, _certverify, _certstore_path, _key_pass, _certstore_prefix, _certhost_name, _certident_name); } + /* May throw in case of low-level problems */ _socket->startTLS(); + + /* Make sure handshake succeeded or abort early + * (there is currently no way for the server to + * report its fault to the client when connection + * is half-way secure): + */ + if (!isValidProtocolVersion()) { + if (_force_ssl) { + disconnect(); + throw nut::SSLException("STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required"); + } + /* TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? */ + } } } diff --git a/clients/upsclient.c b/clients/upsclient.c index d523f5b344..03c02cad63 100644 --- a/clients/upsclient.c +++ b/clients/upsclient.c @@ -1253,8 +1253,6 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) upsdebugx(3, "%s: Succeeded to STARTTLS (OpenSSL)", __func__); - return 1; - # elif defined(WITH_NSS) /* WITH_OPENSSL */ socket = PR_ImportTCPSocket(ups->fd); @@ -1344,9 +1342,22 @@ static int upscli_sslinit(UPSCONN_t *ups, int verifycert) upsdebugx(3, "%s: Succeeded to STARTTLS (NSS)", __func__); - return 1; - # endif /* WITH_OPENSSL | WITH_NSS */ + + /* Make sure handshake succeeded or abort early + * (there is currently no way for the server to + * report its fault to the client when connection + * is half-way secure): + */ + if (!upscli_is_valid_protocol_version(ups, NULL)) { + upslogx(LOG_WARNING, "%s: STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required", __func__); + ups->ssl = NULL; + /* Reaction to forceSSL etc. is up to the caller */ + /* TODO: Should caller drop SSL context or restart the connection as plaintext if SSL is not required? */ + return -2; + } + + return 1; #endif /* WITH_SSL */ } @@ -1584,7 +1595,8 @@ int upscli_tryconnect(UPSCONN_t *ups, const char *host, uint16_t port, int flags ups->upserror = UPSCLI_ERR_SSLFAIL; upscli_disconnect(ups); return -1; - } else if (tryssl && ret == -1) { + } else if (tryssl && ret < 0) { + /* TODO: (ret == -2) Drop SSL context or restart the connection as plaintext if SSL is not required? */ upslogx(LOG_NOTICE, "Error while connecting to NUT server %s, disconnect", host); upscli_disconnect(ups); return -1; @@ -2166,6 +2178,7 @@ int upscli_splitaddr(const char *buf, char **hostname, uint16_t *port) int upscli_is_valid_protocol_version(UPSCONN_t *ups, const char *version_re) { char version[UPSCLI_NETBUF_LEN]; + size_t len; if (!ups) { return -1; @@ -2196,8 +2209,13 @@ int upscli_is_valid_protocol_version(UPSCONN_t *ups, const char *version_re) return -1; } + len = strlen(version); + if (len > 0 && version[len-1] == '\n') { + version[len-1] = '\0'; + } + upsdebugx(3, "%s: PROTVER or NETVER returned '%s', matching against '%s'", - __func__, version, version_re); + __func__, version, NUT_STRARG(version_re)); if (!version_re) { /* Basic check for 1.0 through 1.3, as of NUT v2.8.2 */ diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 3d37b4141e..8b867e2c6d 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -252,6 +252,17 @@ if something goes wrong. self.__srv_handler, server_hostname=self.__host ) + + # Make sure handshake succeeded or abort early + # (there is currently no way for the server to + # report its fault to the client when connection + # is half-way secure): + if not self.isValidProtocolVersion(): + if self.__debug : + print( "[DEBUG] STARTTLS setup claimed to succeed, but protocol version check in the secured session failed" ) + if self.__force_ssl: + raise PyNUTError("STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required") + # TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? else : if self.__debug : print( "[DEBUG] STARTTLS failed: %s" % result ) From f0c16a95cef035ad798aa3eb7fd0c34012f90bc2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 21:08:32 +0200 Subject: [PATCH 23/70] scripts/perl/Nut.pm: call StartTLS() from _initialize(), and use isValidProtocolVersion() method to actively ping a connection after STARTTLS claimed success (but handshake might be broken in fact) [#3387, #1348] Signed-off-by: Jim Klimov --- scripts/perl/Nut.pm | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/perl/Nut.pm b/scripts/perl/Nut.pm index f7f1cf4ae9..d59e247700 100644 --- a/scripts/perl/Nut.pm +++ b/scripts/perl/Nut.pm @@ -307,6 +307,27 @@ sub _initialize { $self->{select} = IO::Select->new( $srvsock ); + # Always try to elevate, do not bother if this fails unless required by args + my $startedTLS = $self->StartTLS(%arg); + if (defined $startedTLS && $startedTLS) { + # Make sure handshake succeeded or abort early + # (there is currently no way for the server to + # report its fault to the client when connection + # is half-way secure): + if (!$self->isValidProtocolVersion()) { + if ($arg{FORCESSL}) { + $self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required"; + return undef; + } + # TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? + } + } else { + if ($arg{FORCESSL}) { + $self->{err} = "SSL setup failed but it is required"; + return undef; + } + } + if ($user and $pass) { # attempt login to upsd if that option is specified if ($login) { # attempt login to upsd if that option is specified $self->Login($user, $pass) or carp $self->{err}; From e0e849a8efe52c88d818f91aab7efb415df6b0ac Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 21:26:57 +0200 Subject: [PATCH 24/70] scripts/perl/test_nutclient.pl: introduce tests, move Nut.pm to UPS subdir [#1348, #1711] Signed-off-by: Jim Klimov --- COPYING | 2 +- scripts/HP-UX/nut.psf.in | 2 +- scripts/Makefile.am | 3 +- scripts/obs/debian.libups-nut-perl.install | 2 +- scripts/perl/{ => UPS}/Nut.pm | 0 scripts/perl/test_nutclient.pl | 270 +++++++++++++++++++++ 6 files changed, 275 insertions(+), 4 deletions(-) rename scripts/perl/{ => UPS}/Nut.pm (100%) create mode 100755 scripts/perl/test_nutclient.pl diff --git a/COPYING b/COPYING index ba7bff0614..45355d3a4c 100644 --- a/COPYING +++ b/COPYING @@ -7,7 +7,7 @@ Public License (GPL) version 3, or (at your option) any later version. See "LICENSE-GPL3" in the root of this distribution. - The Perl client module (scripts/perl/Nut.pm) is released under the same + The Perl client module (scripts/perl/UPS/Nut.pm) is released under the same license as Perl itself. That is to say either GPL version 1 or (at your option) any later version, or the "Artistic License". diff --git a/scripts/HP-UX/nut.psf.in b/scripts/HP-UX/nut.psf.in index feb98a5971..926ba924b7 100644 --- a/scripts/HP-UX/nut.psf.in +++ b/scripts/HP-UX/nut.psf.in @@ -133,7 +133,7 @@ product title "libups-nut-perl" revision @PACKAGE_VERSION@ - file -u 644 -g bin -o bin @top_srcdir@/scripts/perl/Nut.pm @prefix@/share/perl5/UPS/Nut.pm + file -u 644 -g bin -o bin @top_srcdir@/scripts/perl/UPS/Nut.pm @prefix@/share/perl5/UPS/Nut.pm end # ---------------------------------------- diff --git a/scripts/Makefile.am b/scripts/Makefile.am index d24c20adb7..09565e864e 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -12,7 +12,8 @@ EXTRA_DIST = \ logrotate/nutlogd \ misc/nut.bash_completion.in \ misc/osd-notify \ - perl/Nut.pm \ + perl/UPS/Nut.pm \ + perl/test_nutclient.pl \ RedHat/halt.patch \ RedHat/README.adoc \ RedHat/ups.in \ diff --git a/scripts/obs/debian.libups-nut-perl.install b/scripts/obs/debian.libups-nut-perl.install index 355c6fe0a5..5c141c73c0 100644 --- a/scripts/obs/debian.libups-nut-perl.install +++ b/scripts/obs/debian.libups-nut-perl.install @@ -1 +1 @@ -scripts/perl/Nut.pm /usr/share/perl5/UPS/ +scripts/perl/UPS/Nut.pm /usr/share/perl5/UPS/ diff --git a/scripts/perl/Nut.pm b/scripts/perl/UPS/Nut.pm similarity index 100% rename from scripts/perl/Nut.pm rename to scripts/perl/UPS/Nut.pm diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl new file mode 100755 index 0000000000..a4f31930ab --- /dev/null +++ b/scripts/perl/test_nutclient.pl @@ -0,0 +1,270 @@ +#!/usr/bin/perl +# -*- coding: utf-8 -*- + +# This source code is provided for testing/debugging purpose ;) +# This script is a Perl equivalent of scripts/python/module/test_nutclient.py.in + +use strict; +use warnings; +use UPS::Nut; +use Term::ANSIColor; + +# Main logic +if (1) { + my $NUT_HOST = $ENV{'NUT_HOST'} || '127.0.0.1'; + my $NUT_PORT = $ENV{'NUT_PORT'} || '3493'; + my $NUT_USER = $ENV{'NUT_USER'} || undef; + my $NUT_PASS = $ENV{'NUT_PASS'} || undef; + my $NUT_SSL = ($ENV{'NUT_SSL'} eq "true") ? 1 : 0; + my $NUT_FORCESSL = ($ENV{'NUT_FORCESSL'} eq "true" || $ENV{'NUT_FORCESSL'} eq "1") ? 1 : 0; + my $NUT_CERTVERIFY = ($ENV{'NUT_CERTVERIFY'} eq "true" || $ENV{'NUT_CERTVERIFY'} eq "1") ? 1 : 0; + my $NUT_CAFILE = $ENV{'NUT_CAFILE'} || undef; + my $NUT_CAPATH = $ENV{'NUT_CAPATH'} || undef; + # Note: Python's cert_file, key_file, key_pass are not directly supported by current Nut.pm STARTTLS as independent args, + # but passed via %arg. Nut.pm uses STARTTLS method which takes %arg. + + my $NUT_DEBUG = ($ENV{'DEBUG'} eq "true" || defined($ENV{'NUT_DEBUG_LEVEL'})) ? 1 : 0; + + # Account "unexpected" failures (more due to coding than circumstances) + # e.g. lack of protected access when no credentials were passed is okay + my @failed = (); + + print "UPS::Nut test...\n"; + + my $nut; + eval { + $nut = UPS::Nut->new( + HOST => $NUT_HOST, + PORT => $NUT_PORT, + USERNAME => $NUT_USER, + PASSWORD => $NUT_PASS, + DEBUG => $NUT_DEBUG, + # STARTTLS related (passed via %arg to StartTLS in Nut.pm) + CERTVERIFY => $NUT_CERTVERIFY, + CAPATH => $NUT_CAPATH, # Nut.pm uses CAPATH for SSL_ca_file + FORCESSL => $NUT_FORCESSL, + # In case PyNUT's cert_file, key_file are needed: + SSL_cert_file => $ENV{'NUT_CERTFILE'}, + SSL_key_file => $ENV{'NUT_KEYFILE'}, + SSL_key_pass => $ENV{'NUT_KEYPASS'}, + SSL_ca_file => $NUT_CAFILE, + ); + }; + if ($@ || !defined($nut)) { + my $ex = $@ || $nut->Error(); + print "EXCEPTION during initialization: $ex\n"; + if ($NUT_SSL && ($ex =~ /FEATURE-NOT-CONFIGURED/ || $ex =~ /FEATURE-NOT-SUPPORTED/)) { + print "(anticipated error: server does not support STARTTLS)\n"; + exit(0); + } + die $ex; + } + + print "-" x 80 . "\nTesting 'ListUPS' :\n"; + my $result = $nut->ListUPS(); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', keys %$result) : "NULL" ); + + # [dummy] + # driver = dummy-ups + # desc = "Test device" + # port = /src/nut/data/evolution500.seq + print "-" x 80 . "\nTesting 'ListVar' for 'dummy' (should be registered in upsd.conf) :\n"; + $result = $nut->ListVar("dummy"); # Note: Nut.pm's ListVar uses $self->{name} by default, but we can't easily change name without re-init or reaching into internals. + # Actually, Nut.pm ListVar uses $self->{name}. Let's check if we can override it. + # In Nut.pm: sub ListVar { my $self = shift; my $vars = $self->_get_list("LIST VAR $self->{name}", 3, 2); ... } + # It does NOT take a UPS name as argument to override $self->{name}. + # We might need to set $nut->{name} = "dummy" or similar. + $nut->{name} = "dummy"; + $result = $nut->ListVar(); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', map { "$_ => $result->{$_}" } keys %$result) : "NULL" ); + + print "-" x 80 . "\nTesting 'CheckUPSAvailable' (via ListUPS) :\n"; + my $ups_list = $nut->ListUPS(); + $result = exists($ups_list->{"dummy"}) ? "Available" : "Not Available"; + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", $result ); + + print "-" x 80 . "\nTesting 'ListCmd' :\n"; + $result = $nut->ListCmd(); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', @$result) : "NULL" ); + + print "-" x 80 . "\nTesting 'ListRW' :\n"; + $result = $nut->ListRW(); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', keys %$result) : "NULL" ); + + print "-" x 80 . "\nTesting 'InstCmd' (Test front panel) :\n"; + eval { + $nut->{name} = "UPS1"; + $result = $nut->InstCmd("test.panel.start"); + if (!defined($NUT_USER)) { + die "Secure operation should have failed due to lack of credentials, but did not"; + } + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if (!defined($NUT_USER) && $ex =~ /USERNAME-REQUIRED/) { + $result .= "\n(anticipated error: no credentials were provided)"; + } else { + if ($ex !~ /CMD-NOT-SUPPORTED/ && (defined($NUT_USER) && $ex !~ /ACCESS-DENIED/)) { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'InstCmd'; + } + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + print "-" x 80 . "\nTesting 'Set' (set ups.id to test):\n"; + eval { + $nut->{name} = "UPS1"; + $result = $nut->Set("ups.id", "test"); + if (!defined($NUT_USER)) { + die "Secure operation should have failed due to lack of credentials, but did not"; + } + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if (!defined($NUT_USER) && $ex =~ /USERNAME-REQUIRED/) { + $result .= "\n(anticipated error: no credentials were provided)"; + } else { + if ($ex !~ /VAR-NOT-SUPPORTED/ && (defined($NUT_USER) && $ex !~ /ACCESS-DENIED/)) { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'SetVar'; + } + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + print "-" x 80 . "\nTesting 'Set' with TRACKING for 'driver.debug' (1s interval, 10s timeout):\n"; + eval { + my $target_ups = "UPS1"; + $nut->{name} = $target_ups; + # Check if UPS exists + my $vars = $nut->ListVar(); + if (!defined($vars)) { + $target_ups = "dummy"; + $nut->{name} = $target_ups; + } + + # Set with tracking + my ($tid_res, $tid) = $nut->Set("driver.debug", "1", 1, 10); + if (!defined($NUT_USER)) { + die "Secure operation should have failed due to lack of credentials, but did not"; + } + + if (ref($tid) eq 'UPS::Nut::TrackingID') { + printf("Got TRACKING ID: %s, created: %s, age: %0.2fs\n", $tid->id, $tid->created, $tid->age); + $result = $tid->id; + } else { + $result = $tid_res; + } + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if (!defined($NUT_USER) && $ex =~ /USERNAME-REQUIRED/) { + $result .= "\n(anticipated error: no credentials were provided)"; + } else { + if ($ex !~ /VAR-NOT-SUPPORTED/ && (defined($NUT_USER) && $ex !~ /ACCESS-DENIED/)) { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'SetVar-Tracking'; + } + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + # testing who has an upsmon-like log-in session to a device + print "-" x 80 . "\nTesting 'ListClient' for 'dummy' (should be registered in upsd.conf) before test client is connected :\n"; + eval { + $result = $nut->ListClient("dummy"); + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex\nTEST-CASE FAILED"; + push @failed, 'ListClient-dummy-before'; + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', keys %$result) : "NULL" ); + + print "-" x 80 . "\nTesting 'ListClient' for missing device (should raise an exception) :\n"; + eval { + $result = $nut->ListClient("MissingBogusDummy"); + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if ($ex =~ /UNKNOWN-UPS/) { + $result .= "\n(anticipated error: bogus device name was tested)"; + } else { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'ListClient-MissingBogusDummy'; + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + my $loggedIntoDummy = 0; + print "-" x 80 . "\nTesting 'Login' (DeviceLogin) for 'dummy' (should be registered in upsd.conf; current credentials should have an upsmon role in upsd.users) :\n"; + eval { + $nut->{name} = "dummy"; + $result = $nut->Login($NUT_USER, $NUT_PASS); + if (!defined($NUT_USER)) { + die "Secure operation should have failed due to lack of credentials, but did not"; + } + $loggedIntoDummy = 1; + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex"; + if (!defined($NUT_USER) && $ex =~ /USERNAME-REQUIRED/) { + $result .= "\n(anticipated error: no credentials were provided)"; + } else { + if (defined($NUT_USER) && $ex !~ /ACCESS-DENIED/) { + $result .= "\nTEST-CASE FAILED"; + push @failed, 'Login-dummy'; + } + } + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); + + print "-" x 80 . "\nTesting 'ListClient' for all (passing no args to ListClient uses \$nut->{name}) :\n"; + eval { + # Python's nut.ListClients() with no args lists all devices and their clients. + # In Nut.pm, ListClient(undef) uses $self->{name}. + # PyNUT's ListClients(None) iterates over all UPSes. + # Let's implement similar logic here. + my $ups_list_all = $nut->ListUPS(); + my %all_clients = (); + foreach my $ups_name (keys %$ups_list_all) { + my $clients = $nut->ListClient($ups_name); + if (defined($clients)) { + $all_clients{$ups_name} = $clients; + } + } + $result = \%all_clients; + + if (ref($result) ne 'HASH') { + die "ListClient() did not return a hash ref"; + } else { + if ($loggedIntoDummy) { + if (!exists($result->{'dummy'})) { + die "ListClient() result missing 'dummy' key"; + } + if (scalar keys %{$result->{'dummy'}} < 1) { + die "ListClient() returned an empty hash for 'dummy' where at least one client was expected"; + } + } + } + }; + if ($@) { + my $ex = $@; + $result = "EXCEPTION: $ex\nTEST-CASE FAILED"; + push @failed, 'ListClient-all-after'; + } + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', map { "$_ => [" . join(', ', keys %{$result->{$_}}) . "]" } keys %$result) : "NULL" ); + + print "-" x 80 . "\nTesting 'UPS::Nut' instance teardown (end of test script)\n"; + + if (scalar @failed > 0) { + print "SOME TEST CASES FAILED in an unexpected manner: " . join(', ', @failed) . "\n"; + exit(1); + } +} From b260564905ff10232232f7e3a1636f53ca7f1683 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 21:48:18 +0200 Subject: [PATCH 25/70] scripts/perl/UPS/Nut.pm: wrap all remaining $self->{err} assignments into debug() calls [#1348, #1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 124 ++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index d59e247700..01c2c1c7d7 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -28,7 +28,7 @@ my $_eol = "\n"; BEGIN { use Exporter (); use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); - $VERSION = 1.61; + $VERSION = 1.62; @ISA = qw(Exporter IO::Socket::INET); @EXPORT = qw(); @EXPORT_OK = qw(); @@ -90,18 +90,17 @@ sub SetTrackingMode { my $self = shift; my $value = shift; my $ans; # scalar to hold responses from upsd - my $errmsg; # error message, sent to _debug and $self->{err} # 'ON'/'OFF'/undef if (!(defined $value) || ($value ne 'ON' && $value ne 'OFF')) { - $self->{err} = "Invalid setting for TRACKING mode was requested"; - return undef; + $self->_debug($self->{err} = "Invalid setting for TRACKING mode was requested"); + return undef; } $ans = $self->_send("SET TRACKING $value"); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^OK/) { @@ -110,7 +109,7 @@ sub SetTrackingMode { } $self->{tracking} = undef; - $self->{err} = "Error: $ans"; + $self->_debug($self->{err} = "Error: $ans"); return undef; } @@ -164,7 +163,7 @@ sub StartTLS { eval { require IO::Socket::SSL; }; if ($@) { - $self->{err} = "IO::Socket::SSL not available"; + $self->_debug($self->{err} = "IO::Socket::SSL not available"); return undef; } @@ -178,13 +177,13 @@ sub StartTLS { SSL_ca_file => $arg{CAPATH}, %arg ) or do { - $self->{err} = "SSL upgrade failed: " . IO::Socket::SSL->errstr(); + $self->_debug($self->{err} = "SSL upgrade failed: " . IO::Socket::SSL->errstr()); return undef; }; return 1; } - $self->{err} = "STARTTLS failed: $ans"; + $self->_debug($self->{err} = "STARTTLS failed: $ans"); return undef; } @@ -301,7 +300,7 @@ sub _initialize { ); unless ( defined $srvsock) { # can't connect - $self->{err} = "Unable to connect via $proto to $host:$port: $!"; + $self->_debug($self->{err} = "Unable to connect via $proto to $host:$port: $!"); return undef; } @@ -316,14 +315,15 @@ sub _initialize { # is half-way secure): if (!$self->isValidProtocolVersion()) { if ($arg{FORCESSL}) { - $self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required"; + $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required"); return undef; } + $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, but SSL is not required"); # TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? } } else { if ($arg{FORCESSL}) { - $self->{err} = "SSL setup failed but it is required"; + $self->_debug($self->{err} = "SSL setup failed but it is required"); return undef; } } @@ -342,7 +342,7 @@ sub _initialize { $self->{vars} = $self->ListVar; unless ( defined $self->{vars} ) { - $self->{err} = "Network error: $!"; + $self->_debug($self->{err} = "Network error: $!"); return undef; } @@ -417,12 +417,12 @@ sub GetVar { # request a variable from the UPS my $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans. Requested $var."; + $self->_debug($self->{err} = "Error: $ans. Requested $var."); return undef; } elsif ($ans =~ /^VAR/) { @@ -431,14 +431,14 @@ sub GetVar { # request a variable from the UPS (undef, undef, $checkvar, $retval) = split(' ', $ans, 4); # get checkvar and retval from the answer if ($checkvar ne $var) { # did not get expected var - $self->{err} = "requested $var, received $checkvar"; + $self->_debug($self->{err} = "Requested $var, received $checkvar"); return undef; } $retval =~ s/^"(.*)"$/$1/; return $retval; # return the requested value } else { # unrecognized response - $self->{err} = "Unrecognized response from upsd: $ans"; + $self->_debug($self->{err} = "Unrecognized response from upsd: $ans"); return undef; } } @@ -465,12 +465,12 @@ sub Set { my $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; + $self->_debug($self->{err} = "Error: $ans"); return undef; } elsif ($ans =~ /^OK TRACKING /) { # command successful @@ -494,7 +494,7 @@ sub Set { return $value; } else { # unrecognized response - $self->{err} = "Unrecognized response from upsd: $ans"; + $self->_debug($self->{err} = "Unrecognized response from upsd: $ans"); return undef; } } @@ -509,12 +509,12 @@ sub FSD { # set forced shutdown flag my $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^ERR/) { # can't set forced shutdown flag - $self->{err} = "Can't set FSD flag. Upsd reports: $ans"; + $self->_debug($self->{err} = "Can't set FSD flag. Upsd reports: $ans"); return undef; } elsif ($ans =~ /^OK FSD-SET/) { # forced shutdown flag set @@ -522,7 +522,7 @@ sub FSD { # set forced shutdown flag return 1; } else { - $self->{err} = "Unrecognized response from upsd: $ans"; + $self->_debug($self->{err} = "Unrecognized response from upsd: $ans"); return undef; } } @@ -547,12 +547,12 @@ sub InstCmd { # send instant command to ups my $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^ERR/) { # error reported from upsd - $self->{err} = "Can't send instant command $cmd. Reason: $ans"; + $self->_debug($self->{err} = "Can't send instant command $cmd. Reason: $ans"); return undef; } elsif ($ans =~ /^OK TRACKING /) { # command successful @@ -576,7 +576,7 @@ sub InstCmd { # send instant command to ups return 1; } else { # unrecognized response - $self->{err} = "Can't send instant command $cmd. Unrecognized response from upsd: $ans"; + $self->_debug($self->{err} = "Can't send instant command $cmd. Unrecognized response from upsd: $ans"); return undef; } } @@ -592,15 +592,16 @@ sub GetTrackingResult { my $id = ref($tid) eq 'UPS::Nut::TrackingID' ? $tid->id : $tid; my $ans = $self->_send("GET TRACKING $id"); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^TRACKING/) { my @fields = split(' ', $ans); # SUCCESS, PENDING, ERR... return $fields[2]; } - $self->{err} = "Error: $ans"; + + $self->_debug($self->{err} = "Error: $ans"); return undef; } @@ -655,8 +656,8 @@ sub GetUPSDesc { my $ups = shift || $self->{name}; my $ans = $self->_send("GET UPSDESC $ups"); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^UPSDESC/) { my @fields = split(' ', $ans, 3); @@ -664,7 +665,8 @@ sub GetUPSDesc { $desc =~ s/^"(.*)"$/$1/; return $desc; } - $self->{err} = "Error: $ans"; + + $self->_debug($self->{err} = "Error: $ans"); return undef; } @@ -698,12 +700,12 @@ sub ListRange { my $ans = $self->_send($req); unless (defined $ans) { - $self->{err} = "Network error: $!"; + $self->_debug($self->{err} = "Network error: $!"); return undef; }; if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; + $self->_debug($self->{err} = "Error: $ans"); return undef; } elsif ($ans =~ /^BEGIN LIST RANGE/) { @@ -719,7 +721,7 @@ sub ListRange { return $retval; } - $self->{err} = "Unrecognized response: $ans"; + $self->_debug($self->{err} = "Unrecognized response: $ans"); return undef; } @@ -729,12 +731,12 @@ sub _get_list { my $ans = $self->_send($req); unless (defined $ans) { - $self->{err} = "Network error: $!"; + $self->_debug($self->{err} = "Network error: $!"); return undef; }; if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; + $self->_debug($self->{err} = "Error: $ans"); return undef; } elsif ($ans =~ /^BEGIN LIST/) { # command successful @@ -752,14 +754,14 @@ sub _get_list { } } unless ($line) { - $self->{err} = "Network error: $!"; + $self->_debug($self->{err} = "Network error: $!"); return undef; }; $self->_debug("$req command sent successfully."); return $retval; } else { # unrecognized response - $self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"; + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); return undef; } } @@ -776,12 +778,12 @@ sub GetDesc { my $req = "GET DESC $self->{name} $var"; my $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; + $self->_debug($self->{err} = "Error: $ans"); return undef; } elsif ($ans =~ /^DESC/) { # command successful @@ -791,7 +793,7 @@ sub GetDesc { return $ans; } else { # unrecognized response - $self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"; + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); return undef; } } @@ -808,12 +810,12 @@ sub GetType { my $req = "GET TYPE $self->{name} $var"; my $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; + $self->_debug($self->{err} = "Error: $ans"); return undef; } elsif ($ans =~ /^TYPE/) { # command successful @@ -822,7 +824,7 @@ sub GetType { return $ans; } else { # unrecognized response - $self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"; + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); return undef; } } @@ -839,12 +841,12 @@ sub GetCmdDesc { my $req = "GET CMDDESC $self->{name} $cmd"; my $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^ERR/) { - $self->{err} = "Error: $ans"; + $self->_debug($self->{err} = "Error: $ans"); return undef; } elsif ($ans =~ /^DESC/) { # command successful @@ -854,7 +856,7 @@ sub GetCmdDesc { return $ans; } else { # unrecognized response - $self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"; + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); return undef; } } @@ -910,8 +912,8 @@ sub Master { # check for MASTER level access my $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^OK/) { # access granted @@ -923,8 +925,8 @@ sub Master { # check for MASTER level access $req = "MASTER $self->{name}"; $ans = $self->_send( $req ); unless (defined $ans) { - $self->{err} = "Network error: $!"; - return undef; + $self->_debug($self->{err} = "Network error: $!"); + return undef; }; if ($ans =~ /^OK/) { # access granted @@ -932,7 +934,7 @@ sub Master { # check for MASTER level access return 1; } else { # access denied, or unrecognized response - $self->{err} = "PRIMARY/MASTER level access denied. Upsd responded: $ans"; + $self->_debug($self->{err} = "PRIMARY/MASTER level access denied. Upsd responded: $ans"); return undef; } } From 387aaf4a0084a7677a641c4a399c34ddb0680736 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 22:07:02 +0200 Subject: [PATCH 26/70] scripts/python/module/test_nutclient.py.in: fix typo in debug traces Signed-off-by: Jim Klimov --- scripts/python/module/test_nutclient.py.in | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/python/module/test_nutclient.py.in b/scripts/python/module/test_nutclient.py.in index 28953b70c4..701f09c853 100755 --- a/scripts/python/module/test_nutclient.py.in +++ b/scripts/python/module/test_nutclient.py.in @@ -53,7 +53,7 @@ if __name__ == "__main__" : # driver = dummy-ups # desc = "Test device" # port = /src/nut/data/evolution500.seq - print( 80*"-" + "\nTesting 'GetUPSVars' for 'dummy' (should be registered in upsd.conf) :") + print( 80*"-" + "\nTesting 'GetUPSVars' for 'dummy' (should be registered in ups.conf) :") result = nut.GetUPSVars( "dummy" ) print( "\033[01;33m%s\033[0m\n" % result ) @@ -132,7 +132,7 @@ if __name__ == "__main__" : print( "\033[01;33m%s\033[0m\n" % result ) # testing who has an upsmon-like log-in session to a device - print( 80*"-" + "\nTesting 'ListClients' for 'dummy' (should be registered in upsd.conf) before test client is connected :") + print( 80*"-" + "\nTesting 'ListClients' for 'dummy' (should be registered in ups.conf) before test client is connected :") try : result = nut.ListClients( "dummy" ) except : @@ -156,7 +156,7 @@ if __name__ == "__main__" : print( "\033[01;33m%s\033[0m\n" % result ) loggedIntoDummy = False - print( 80*"-" + "\nTesting 'DeviceLogin' for 'dummy' (should be registered in upsd.conf; current credentials should have an upsmon role in upsd.users) :") + print( 80*"-" + "\nTesting 'DeviceLogin' for 'dummy' (should be registered in ups.conf; current credentials should have an upsmon role in upsd.users) :") try : result = nut.DeviceLogin( "dummy" ) if (NUT_USER is None): From 4aeba6a5a800c8219a2e2dc6cd950821b2b1d96f Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 22:09:06 +0200 Subject: [PATCH 27/70] scripts/perl/UPS/Nut.pm: change commands that address an UPS to optionally accept the UPS name as argument, only using "$self->{name}" as default [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 80 ++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 01c2c1c7d7..9d5c9d5117 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -206,11 +206,15 @@ sub Login { # login to upsd, so that it won't shutdown unless we say we're my $self = shift; # myself my $user = shift; # username my $pass = shift; # password + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + my $errmsg; # error message, sent to _debug and $self->{err} my $ans; # scalar to hold responses from upsd $self->Authenticate($user, $pass) or return; - $ans = $self->_send( "LOGIN $self->{name}" ); + $ans = $self->_send( "LOGIN $ups" ); if (defined $ans && $ans =~ /^OK/) { # Login successful. $self->_debug("LOGIN successful."); return 1; @@ -413,7 +417,11 @@ sub GetVar { # request a variable from the UPS # # Modified by Gabor Kiss according to protocol version 1.5+ my $var = shift; - my $req = "GET VAR $self->{name} $var"; # build request + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $req = "GET VAR $ups $var"; # build request my $ans = $self->_send( $req ); unless (defined $ans) { @@ -454,14 +462,18 @@ sub Set { # Optional TRACKING wait support: my $wait_interval_sec = shift || undef; my $wait_max_count = shift || undef; - my $do_wait = 0; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $do_wait = 0; if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_interval_sec > 0) { $self->EnableTrackingModeOnce; $do_wait = 1; } - my $req = "SET VAR $self->{name} $var $value"; # build request + my $req = "SET VAR $ups $var $value"; # build request my $ans = $self->_send( $req ); unless (defined $ans) { @@ -504,8 +516,9 @@ sub FSD { # set forced shutdown flag # ### changelog: uses the new _send command # my $self = shift; + my $ups = shift || $self->{name}; - my $req = "FSD $self->{name}"; # build request + my $req = "FSD $ups"; # build request my $ans = $self->_send( $req ); unless (defined $ans) { @@ -536,14 +549,18 @@ sub InstCmd { # send instant command to ups # Optional TRACKING wait support: my $wait_interval_sec = shift || undef; my $wait_max_count = shift || undef; - my $do_wait = 0; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $do_wait = 0; if (defined $wait_interval_sec && defined $wait_max_count && $wait_max_count > 0 && $wait_interval_sec > 0) { $self->EnableTrackingModeOnce; $do_wait = 1; } - my $req = "INSTCMD $self->{name} $cmd"; + my $req = "INSTCMD $ups $cmd"; my $ans = $self->_send( $req ); unless (defined $ans) { @@ -648,12 +665,14 @@ sub WaitTrackingResult { sub ListClient { my $self = shift; my $ups = shift || $self->{name}; + return $self->_get_list("LIST CLIENT $ups", 2, 2); } sub GetUPSDesc { my $self = shift; my $ups = shift || $self->{name}; + my $ans = $self->_send("GET UPSDESC $ups"); unless (defined $ans) { $self->_debug($self->{err} = "Network error: $!"); @@ -672,31 +691,45 @@ sub GetUPSDesc { sub ListVar { my $self = shift; - my $vars = $self->_get_list("LIST VAR $self->{name}", 3, 2); + my $ups = shift || $self->{name}; + + my $vars = $self->_get_list("LIST VAR $ups", 3, 2); return $vars unless @_; # return all variables return {map { $_ => $vars->{$_} } @_}; # return selected ones } sub ListRW { my $self = shift; - return $self->_get_list("LIST RW $self->{name}", 3, 2); + my $ups = shift || $self->{name}; + + return $self->_get_list("LIST RW $ups", 3, 2); } sub ListCmd { my $self = shift; - return $self->_get_list("LIST CMD $self->{name}", 2); + my $ups = shift || $self->{name}; + + return $self->_get_list("LIST CMD $ups", 2); } sub ListEnum { my $self = shift; my $var = shift; - return $self->_get_list("LIST ENUM $self->{name} $var", 3); + # NOTE: This clumsily goes after $var, to avoid breaking + # API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + return $self->_get_list("LIST ENUM $ups $var", 3); } sub ListRange { my $self = shift; my $var = shift; - my $req = "LIST RANGE $self->{name} $var"; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $req = "LIST RANGE $ups $var"; my $ans = $self->_send($req); unless (defined $ans) { @@ -774,9 +807,13 @@ sub GetDesc { # Modified by Gabor Kiss according to protocol version 1.5+ my $self = shift; my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; - my $req = "GET DESC $self->{name} $var"; + my $req = "GET DESC $ups $var"; my $ans = $self->_send( $req ); + unless (defined $ans) { $self->_debug($self->{err} = "Network error: $!"); return undef; @@ -806,9 +843,13 @@ sub GetType { # Modified by Gabor Kiss according to protocol version 1.5+ my $self = shift; my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; - my $req = "GET TYPE $self->{name} $var"; + my $req = "GET TYPE $ups $var"; my $ans = $self->_send( $req ); + unless (defined $ans) { $self->_debug($self->{err} = "Network error: $!"); return undef; @@ -837,9 +878,13 @@ sub GetCmdDesc { # Modified by Gabor Kiss according to protocol version 1.5+ my $self = shift; my $cmd = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; - my $req = "GET CMDDESC $self->{name} $cmd"; + my $req = "GET CMDDESC $ups $cmd"; my $ans = $self->_send( $req ); + unless (defined $ans) { $self->_debug($self->{err} = "Network error: $!"); return undef; @@ -907,8 +952,9 @@ sub Master { # check for MASTER level access # NOTE: API changed since NUT 2.8.0 to replace MASTER with PRIMARY # (and backwards-compatible alias handling) my $self = shift; + my $ups = shift || $self->{name}; - my $req = "PRIMARY $self->{name}"; # build request + my $req = "PRIMARY $ups"; # build request my $ans = $self->_send( $req ); unless (defined $ans) { @@ -922,7 +968,7 @@ sub Master { # check for MASTER level access } # Retry with MASTER if PRIMARY failed - $req = "MASTER $self->{name}"; + $req = "MASTER $ups"; $ans = $self->_send( $req ); unless (defined $ans) { $self->_debug($self->{err} = "Network error: $!"); From bfa459f7a3bfea5ca188edf5a31d705e631940d1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 22:28:51 +0200 Subject: [PATCH 28/70] scripts/perl/UPS/Nut.pm: fix string work with TRACKING logic Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 9d5c9d5117..f1115442f4 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -92,8 +92,13 @@ sub SetTrackingMode { my $ans; # scalar to hold responses from upsd # 'ON'/'OFF'/undef - if (!(defined $value) || ($value ne 'ON' && $value ne 'OFF')) { - $self->_debug($self->{err} = "Invalid setting for TRACKING mode was requested"); + if (!(defined $value)) { + $self->_debug($self->{err} = "Invalid setting for TRACKING mode was requested: undef"); + return undef; + } + + if ($value ne 'ON' && $value ne 'OFF') { + $self->_debug($self->{err} = "Invalid setting for TRACKING mode was requested: '$value'"); return undef; } @@ -120,7 +125,8 @@ sub EnableTrackingModeOnce { return 1; } - if ($self->SetTrackingMode('ON') eq 'ON') { + my $actualMode = $self->SetTrackingMode('ON'); + if (defined $actualMode && $actualMode eq 'ON') { return 1; } From 2400917e898e86f54909d1e6a4019d3b1a4c2fb5 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 22:30:18 +0200 Subject: [PATCH 29/70] scripts/perl/test_nutclient.pl: fix test case setup, update comments [#1711, #1348] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 2 ++ scripts/perl/test_nutclient.pl | 21 +++++++++++---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index f1115442f4..fd4015523e 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -352,6 +352,8 @@ sub _initialize { $self->{vars} = $self->ListVar; unless ( defined $self->{vars} ) { + # FIXME: Can well be `ERR UNKNOWN-UPS` due to NAME=default + # Better report that as such... $self->_debug($self->{err} = "Network error: $!"); return undef; } diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index a4f31930ab..b6a41f72f5 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -34,11 +34,17 @@ my $nut; eval { $nut = UPS::Nut->new( + # A name is used right where we initialize the object, + # otherwise it falls back to "default". It should be + # registered in ups.conf; one UPS at a time per connection! + # We fiddle those for NIT script (dummy, UPS1, UPS2) below. + NAME => "dummy", HOST => $NUT_HOST, PORT => $NUT_PORT, USERNAME => $NUT_USER, PASSWORD => $NUT_PASS, DEBUG => $NUT_DEBUG, + # TRACKING => 'ON', # undef by default, enabled in certain tests below # STARTTLS related (passed via %arg to StartTLS in Nut.pm) CERTVERIFY => $NUT_CERTVERIFY, CAPATH => $NUT_CAPATH, # Nut.pm uses CAPATH for SSL_ca_file @@ -68,14 +74,9 @@ # driver = dummy-ups # desc = "Test device" # port = /src/nut/data/evolution500.seq - print "-" x 80 . "\nTesting 'ListVar' for 'dummy' (should be registered in upsd.conf) :\n"; - $result = $nut->ListVar("dummy"); # Note: Nut.pm's ListVar uses $self->{name} by default, but we can't easily change name without re-init or reaching into internals. - # Actually, Nut.pm ListVar uses $self->{name}. Let's check if we can override it. - # In Nut.pm: sub ListVar { my $self = shift; my $vars = $self->_get_list("LIST VAR $self->{name}", 3, 2); ... } - # It does NOT take a UPS name as argument to override $self->{name}. - # We might need to set $nut->{name} = "dummy" or similar. - $nut->{name} = "dummy"; - $result = $nut->ListVar(); + print "-" x 80 . "\nTesting 'ListVar' for 'dummy' (should be registered in ups.conf) :\n"; + # TOTHINK: Extend into a test for ListVar("bogus") - that it should fail? + $result = $nut->ListVar("dummy"); printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', map { "$_ => $result->{$_}" } keys %$result) : "NULL" ); print "-" x 80 . "\nTesting 'CheckUPSAvailable' (via ListUPS) :\n"; @@ -174,7 +175,7 @@ printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); # testing who has an upsmon-like log-in session to a device - print "-" x 80 . "\nTesting 'ListClient' for 'dummy' (should be registered in upsd.conf) before test client is connected :\n"; + print "-" x 80 . "\nTesting 'ListClient' for 'dummy' (should be registered in ups.conf) before test client is connected :\n"; eval { $result = $nut->ListClient("dummy"); }; @@ -202,7 +203,7 @@ printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? $result : "NULL" ); my $loggedIntoDummy = 0; - print "-" x 80 . "\nTesting 'Login' (DeviceLogin) for 'dummy' (should be registered in upsd.conf; current credentials should have an upsmon role in upsd.users) :\n"; + print "-" x 80 . "\nTesting 'Login' (DeviceLogin) for 'dummy' (should be registered in ups.conf; current credentials should have an upsmon role in upsd.users) :\n"; eval { $nut->{name} = "dummy"; $result = $nut->Login($NUT_USER, $NUT_PASS); From b74de4525ee54681d4f8fc6a48692fb957b84a67 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 22:51:29 +0200 Subject: [PATCH 30/70] scripts/perl/UPS/Nut.pm: fix error messages when Authenticate() and Login() fail [#1348, #1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index fd4015523e..ce08e9eb6e 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -219,7 +219,11 @@ sub Login { # login to upsd, so that it won't shutdown unless we say we're my $errmsg; # error message, sent to _debug and $self->{err} my $ans; # scalar to hold responses from upsd - $self->Authenticate($user, $pass) or return; + if (!($self->Authenticate($user, $pass))) { + $self->_debug("Authenticate before LOGIN failed."); + return undef; + } + $ans = $self->_send( "LOGIN $ups" ); if (defined $ans && $ans =~ /^OK/) { # Login successful. $self->_debug("LOGIN successful."); @@ -254,6 +258,9 @@ sub Authenticate { # Announce to the UPS who we are to set up the proper $ans = $self->_send("PASSWORD $pass"); return 1 if (defined $ans && $ans =~ /^OK/); } + } else { + $self->_debug($self->{err} = "Authentication failed: username and/or password not provided, internal equivalent of USERNAME-REQUIRED"); + return undef; } if (defined $ans) { $errmsg = "Authentication failed. Last message from upsd: $ans"; From 5f1992e76a9ce94773b3dd1428c7c5f6186a2ba2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 22:51:59 +0200 Subject: [PATCH 31/70] scripts/perl/test_nutclient.pl: fix access to possibly undefined envvars [#1348, #1711] Signed-off-by: Jim Klimov --- scripts/perl/test_nutclient.pl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index b6a41f72f5..1331d2d6c1 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -15,15 +15,15 @@ my $NUT_PORT = $ENV{'NUT_PORT'} || '3493'; my $NUT_USER = $ENV{'NUT_USER'} || undef; my $NUT_PASS = $ENV{'NUT_PASS'} || undef; - my $NUT_SSL = ($ENV{'NUT_SSL'} eq "true") ? 1 : 0; - my $NUT_FORCESSL = ($ENV{'NUT_FORCESSL'} eq "true" || $ENV{'NUT_FORCESSL'} eq "1") ? 1 : 0; - my $NUT_CERTVERIFY = ($ENV{'NUT_CERTVERIFY'} eq "true" || $ENV{'NUT_CERTVERIFY'} eq "1") ? 1 : 0; + my $NUT_SSL = (($ENV{'NUT_SSL'} || "false") eq "true") ? 1 : 0; + my $NUT_FORCESSL = (($ENV{'NUT_FORCESSL'} || "false") eq "true" || ($ENV{'NUT_FORCESSL'} || "false") eq "1") ? 1 : 0; + my $NUT_CERTVERIFY = (($ENV{'NUT_CERTVERIFY'} || "false") eq "true" || ($ENV{'NUT_CERTVERIFY'} || "false") eq "1") ? 1 : 0; my $NUT_CAFILE = $ENV{'NUT_CAFILE'} || undef; my $NUT_CAPATH = $ENV{'NUT_CAPATH'} || undef; # Note: Python's cert_file, key_file, key_pass are not directly supported by current Nut.pm STARTTLS as independent args, # but passed via %arg. Nut.pm uses STARTTLS method which takes %arg. - my $NUT_DEBUG = ($ENV{'DEBUG'} eq "true" || defined($ENV{'NUT_DEBUG_LEVEL'})) ? 1 : 0; + my $NUT_DEBUG = (($ENV{'DEBUG'} || "false") eq "true" || defined($ENV{'NUT_DEBUG_LEVEL'})) ? 1 : 0; # Account "unexpected" failures (more due to coding than circumstances) # e.g. lack of protected access when no credentials were passed is okay From 6ae186a0002f3368e18ccedcae5656400ee48313 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 22:52:30 +0200 Subject: [PATCH 32/70] scripts/perl/test_nutclient.pl: report $nut->{err} in the exception message, so we can parse it [#1348, #1711] Signed-off-by: Jim Klimov --- scripts/perl/test_nutclient.pl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index 1331d2d6c1..528d91ef1a 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -97,7 +97,7 @@ $nut->{name} = "UPS1"; $result = $nut->InstCmd("test.panel.start"); if (!defined($NUT_USER)) { - die "Secure operation should have failed due to lack of credentials, but did not"; + die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; } }; if ($@) { @@ -119,7 +119,7 @@ $nut->{name} = "UPS1"; $result = $nut->Set("ups.id", "test"); if (!defined($NUT_USER)) { - die "Secure operation should have failed due to lack of credentials, but did not"; + die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; } }; if ($@) { @@ -150,7 +150,7 @@ # Set with tracking my ($tid_res, $tid) = $nut->Set("driver.debug", "1", 1, 10); if (!defined($NUT_USER)) { - die "Secure operation should have failed due to lack of credentials, but did not"; + die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; } if (ref($tid) eq 'UPS::Nut::TrackingID') { @@ -208,7 +208,7 @@ $nut->{name} = "dummy"; $result = $nut->Login($NUT_USER, $NUT_PASS); if (!defined($NUT_USER)) { - die "Secure operation should have failed due to lack of credentials, but did not"; + die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; } $loggedIntoDummy = 1; }; @@ -243,14 +243,14 @@ $result = \%all_clients; if (ref($result) ne 'HASH') { - die "ListClient() did not return a hash ref"; + die "ListClient() did not return a hash ref: $nut->{err}"; } else { if ($loggedIntoDummy) { if (!exists($result->{'dummy'})) { - die "ListClient() result missing 'dummy' key"; + die "ListClient() result missing 'dummy' key: $nut->{err}"; } if (scalar keys %{$result->{'dummy'}} < 1) { - die "ListClient() returned an empty hash for 'dummy' where at least one client was expected"; + die "ListClient() returned an empty hash for 'dummy' where at least one client was expected: $nut->{err}"; } } } From b87a14353ff29d20e25cd26ddfe8ec4779608b14 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 23:09:05 +0200 Subject: [PATCH 33/70] tests/NIT/nit.sh: introduce tests for PERL [#1348, #1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 136 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 2acc69cbd0..62c9494d2d 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2782,6 +2782,131 @@ testcases_sandbox_python() { #################################### +setenv_ssl_perl() { + setenv_ssl_python +} + +PL_SHEBANG="" +PL_RES=127 +isTestablePerl() { + # Currently we use any PERL on path and do not detect it in configure script: + case x"${PL_SHEBANG}" in + x"") ;; # Fall through to detection + *) return $PL_RES ;; # Probably resolved (if not a comment)? + esac + + if [ x"${TOP_SRCDIR}" = x ] \ + || [ ! -x "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" ] \ + ; then + return 1 + fi + + if [ ! -s "${TOP_SRCDIR}/scripts/perl/UPS/Nut.pm" ] \ + ; then + return 1 + fi + + if [ x"${PERL}" != x ] ; then + PL_SHEBANG="#!${PERL}" + PL_RES=0 + return 0 + fi + + PL_SHEBANG="`head -1 \"${TOP_SRCDIR}/scripts/perl/test_nutclient.pl\"`" + PL_RES=3 + case x"${PL_SHEBANG}" in + x"#!"/*|x"#!"?":\\"*|x"#!"?":/"*) PL_RES=0 ;; # Seems like a full path + x"#!no") PL_RES=1 ;; # Explicitly skipped + x"#!@") PL_RES=2 ;; # Unresolved + *) PL_RES=3 ;; # Unexpected twist + esac + if [ x"${PL_RES}" = x0 ] ; then + log_debug "=======\nDetected perl shebang: '${PL_SHEBANG}' (result=${PL_RES})" + # Currently we use any PERL on path and do not detect it in configure script, + # so the hard-coded value may be bogus: + PERL="`echo \"${PL_SHEBANG}\" | sed 's,^#!,,'`" + if [ -x "$PERL" ] ; then : ; else PERL="`command -v perl`" ; fi + if [ -n "$PERL" ] && [ -x "$PERL" ] ; then : ; else PL_RES=3 ; fi + else + log_error "[isTestablePerl] Detected perl shebang: '${PL_SHEBANG}' (result=${PL_RES})" + fi + return $PL_RES +} + +testcase_sandbox_perl_without_credentials() { + isTestablePerl && [ -n "${PERL}" ] || return 0 + + log_separator + log_info "[testcase_sandbox_perl_without_credentials] Call Perl module test suite: UPS::Nut (NUT Perl bindings) without login credentials" + if ( unset NUT_USER || true + unset NUT_PASS || true + setenv_ssl_perl + $PERL -I"${TOP_SRCDIR}/scripts/perl" "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + ) ; then + log_info "[testcase_sandbox_perl_without_credentials] PASSED: UPS::Nut did not complain" + PASSED="`expr $PASSED + 1`" + else + log_error "[testcase_sandbox_perl_without_credentials] UPS::Nut complained, check above" + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS testcase_sandbox_perl_without_credentials" + fi +} + +testcase_sandbox_perl_with_credentials() { + isTestablePerl && [ -n "${PERL}" ] || return 0 + + # That script says it expects data/evolution500.seq (as the UPS1 dummy) + # but the dummy data does not currently let issue the commands and + # setvars tested from perl script. + log_separator + log_info "[testcase_sandbox_perl_with_credentials] Call Perl module test suite: UPS::Nut (NUT Perl bindings) with login credentials" + if ( + NUT_USER='admin' + NUT_PASS="${TESTPASS_ADMIN}" + export NUT_USER NUT_PASS + setenv_ssl_perl + $PERL -I"${TOP_SRCDIR}/scripts/perl" "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + ) ; then + log_info "[testcase_sandbox_perl_with_credentials] PASSED: UPS::Nut did not complain" + PASSED="`expr $PASSED + 1`" + else + log_error "[testcase_sandbox_perl_with_credentials] UPS::Nut complained, check above" + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS testcase_sandbox_perl_with_credentials" + fi +} + +testcase_sandbox_perl_with_upsmon_credentials() { + isTestablePerl && [ -n "${PERL}" ] || return 0 + + log_separator + log_info "[testcase_sandbox_perl_with_upsmon_credentials] Call Perl module test suite: UPS::Nut (NUT Perl bindings) with upsmon role login credentials" + if ( + NUT_USER='dummy-admin' + NUT_PASS="${TESTPASS_UPSMON_PRIMARY}" + export NUT_USER NUT_PASS + setenv_ssl_perl + $PERL -I"${TOP_SRCDIR}/scripts/perl" "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + ) ; then + log_info "[testcase_sandbox_perl_with_upsmon_credentials] PASSED: UPS::Nut did not complain" + PASSED="`expr $PASSED + 1`" + else + log_error "[testcase_sandbox_perl_with_upsmon_credentials] UPS::Nut complained, check above" + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS testcase_sandbox_perl_with_upsmon_credentials" + fi +} + +testcases_sandbox_perl() { + isTestablePerl && [ -n "${PERL}" ] || return 0 + + testcase_sandbox_perl_without_credentials + testcase_sandbox_perl_with_credentials + testcase_sandbox_perl_with_upsmon_credentials +} + +#################################### + isTestableCppNIT() { # We optionally make and here can run C++ client tests: if [ x"${TOP_BUILDDIR}" = x ] \ @@ -3056,6 +3181,7 @@ testgroup_sandbox() { testcase_sandbox_upsc_query_timer testcases_sandbox_python testcases_sandbox_cppnit + testcases_sandbox_perl testcases_sandbox_nutscanner log_separator @@ -3071,6 +3197,15 @@ testgroup_sandbox_python() { sandbox_forget_configs } +testgroup_sandbox_perl() { + # Arrange for quick test iterations + testcase_sandbox_start_drivers_after_upsd + testcases_sandbox_perl + + log_separator + sandbox_forget_configs +} + testgroup_sandbox_cppnit() { # Arrange for quick test iterations testcase_sandbox_start_drivers_after_upsd @@ -3114,6 +3249,7 @@ case "${NIT_CASE}" in isBusy_NUT_PORT) DEBUG=yes isBusy_NUT_PORT ;; cppnit) testgroup_sandbox_cppnit ;; python) testgroup_sandbox_python ;; + perl) testgroup_sandbox_perl ;; nutscanner|nut-scanner) testgroup_sandbox_nutscanner ;; testcase_*|testgroup_*|testcases_*|testgroups_*) log_warn "========================================================" From 0efb97e968cf91fc87110ab03d95f26cefae9011 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 23:17:17 +0200 Subject: [PATCH 34/70] tests/NIT/nit.sh: when we ask for specific NIT_CASE values (cppnit, python, perl, nutscanner) the lack of prerequisites should be a FAILURE [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 62c9494d2d..607cf0fedb 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -3247,10 +3247,38 @@ testgroup_sandbox_upsmon_master() { case "${NIT_CASE}" in isBusy_NUT_PORT) DEBUG=yes isBusy_NUT_PORT ;; - cppnit) testgroup_sandbox_cppnit ;; - python) testgroup_sandbox_python ;; - perl) testgroup_sandbox_perl ;; - nutscanner|nut-scanner) testgroup_sandbox_nutscanner ;; + cppnit) + if isTestableCppNIT ; then + testgroup_sandbox_cppnit + else + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS $NIT_CASE:missing-prerequisites" + fi + ;; + python) + if isTestablePython && [ -n "${PYTHON}" ] ; then + testgroup_sandbox_python + else + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS $NIT_CASE:missing-prerequisites" + fi + ;; + perl) + if isTestablePerl && [ -n "${PERL}" ] ; then + testgroup_sandbox_perl + else + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS $NIT_CASE:missing-prerequisites" + fi + ;; + nutscanner|nut-scanner) + if isTestableNutScanner && [ -n "${PERL}" ] ; then + testgroup_sandbox_nutscanner + else + FAILED="`expr $FAILED + 1`" + FAILED_FUNCS="$FAILED_FUNCS $NIT_CASE:missing-prerequisites" + fi + ;; testcase_*|testgroup_*|testcases_*|testgroups_*) log_warn "========================================================" log_warn "You asked to run just a specific testcase* or testgroup*" From 6d6008bdcc6b8e7fae6f7aa459116c900cc788f2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 23:40:54 +0200 Subject: [PATCH 35/70] scripts/perl/UPS/Nut.pm: GetTrackingResult(): fix parsing results of GET TRACKING [#1348] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index ce08e9eb6e..d22353f03d 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -621,20 +621,27 @@ sub ListUPS { sub GetTrackingResult { my $self = shift; my $tid = shift; + my $id = ref($tid) eq 'UPS::Nut::TrackingID' ? $tid->id : $tid; my $ans = $self->_send("GET TRACKING $id"); unless (defined $ans) { $self->_debug($self->{err} = "Network error: $!"); return undef; }; - if ($ans =~ /^TRACKING/) { - my @fields = split(' ', $ans); - # SUCCESS, PENDING, ERR... - return $fields[2]; + + chomp $ans; + if ($ans eq 'SUCCESS') { + $self->_debug("Request with TRACKING ID $id has successfully completed"); + } elsif ($ans =~ 'ERR') { + $self->_debug($self->{err} = "Request with TRACKING ID $id has completed with a failure: $ans"); + } elsif ($ans eq 'PENDING') { + $self->_debug("Still waiting for TRACKING ID $id"); + } else { + $self->_debug($self->{err} = "Got bogus reply while waiting for TRACKING ID $id: $ans"); + return undef; } - $self->_debug($self->{err} = "Error: $ans"); - return undef; + return $ans; } sub WaitTrackingResult { @@ -652,16 +659,12 @@ sub WaitTrackingResult { do { my $value = $self->GetTrackingResult($tid); if (defined $value) { + # Note: debug messages are printed by GetTrackingResult() already + chomp $value; if ($value eq 'SUCCESS') { - $self->_debug("Request with TRACKING ID $id has successfully completed"); return 1; } elsif ($value =~ 'ERR') { - $self->_debug("Request with TRACKING ID $id has completed with a failure: $value"); return -1; - } elsif ($value eq 'PENDING') { - $self->_debug("Still waiting for TRACKING ID $id..."); - } else { - $self->_debug("Got bogus reply while waiting for TRACKING ID $id: $value"); } } else { # TOTHINK: Keep retrying? Here in case of network or explicit error... From 2fd6eb12c3cecfa99767052c200e6b1137f6a8fc Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Thu, 9 Apr 2026 23:45:04 +0200 Subject: [PATCH 36/70] scripts/perl/test_nutclient.pl: fix reporting of @failed methods [#1711] Signed-off-by: Jim Klimov --- scripts/perl/test_nutclient.pl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index 528d91ef1a..2f0c063644 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -68,7 +68,7 @@ print "-" x 80 . "\nTesting 'ListUPS' :\n"; my $result = $nut->ListUPS(); - printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', keys %$result) : "NULL" ); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', keys %$result) : (defined($result) ? $result : "NULL") ); # [dummy] # driver = dummy-ups @@ -77,7 +77,7 @@ print "-" x 80 . "\nTesting 'ListVar' for 'dummy' (should be registered in ups.conf) :\n"; # TOTHINK: Extend into a test for ListVar("bogus") - that it should fail? $result = $nut->ListVar("dummy"); - printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', map { "$_ => $result->{$_}" } keys %$result) : "NULL" ); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', map { "$_ => $result->{$_}" } keys %$result) : (defined($result) ? $result : "NULL") ); print "-" x 80 . "\nTesting 'CheckUPSAvailable' (via ListUPS) :\n"; my $ups_list = $nut->ListUPS(); @@ -86,11 +86,11 @@ print "-" x 80 . "\nTesting 'ListCmd' :\n"; $result = $nut->ListCmd(); - printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', @$result) : "NULL" ); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'ARRAY') ? join(', ', @$result) : (defined($result) ? $result : "NULL") ); print "-" x 80 . "\nTesting 'ListRW' :\n"; $result = $nut->ListRW(); - printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', keys %$result) : "NULL" ); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', keys %$result) : (defined($result) ? $result : "NULL") ); print "-" x 80 . "\nTesting 'InstCmd' (Test front panel) :\n"; eval { @@ -184,7 +184,7 @@ $result = "EXCEPTION: $ex\nTEST-CASE FAILED"; push @failed, 'ListClient-dummy-before'; } - printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', keys %$result) : "NULL" ); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', keys %$result) : (defined($result) ? $result : "NULL") ); print "-" x 80 . "\nTesting 'ListClient' for missing device (should raise an exception) :\n"; eval { @@ -260,7 +260,7 @@ $result = "EXCEPTION: $ex\nTEST-CASE FAILED"; push @failed, 'ListClient-all-after'; } - printf( color('bold yellow') . "%s" . color('reset') . "\n\n", defined($result) ? join(', ', map { "$_ => [" . join(', ', keys %{$result->{$_}}) . "]" } keys %$result) : "NULL" ); + printf( color('bold yellow') . "%s" . color('reset') . "\n\n", (defined($result) && ref($result) eq 'HASH') ? join(', ', map { "$_ => [" . join(', ', keys %{$result->{$_}}) . "]" } keys %$result) : (defined($result) ? $result : "NULL") ); print "-" x 80 . "\nTesting 'UPS::Nut' instance teardown (end of test script)\n"; From e4a01d71281bac45ffede866914427bc0b9d956b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 00:15:59 +0200 Subject: [PATCH 37/70] scripts/perl/UPS/Nut.pm: we can only authenticate once per connection, skip other attempts [#1711, #1348] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index d22353f03d..4062a57d5d 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -220,8 +220,10 @@ sub Login { # login to upsd, so that it won't shutdown unless we say we're my $ans; # scalar to hold responses from upsd if (!($self->Authenticate($user, $pass))) { + if ($self->{err} !~ /ERR ALREADY-SET-USERNAME/) { $self->_debug("Authenticate before LOGIN failed."); return undef; + } } $ans = $self->_send( "LOGIN $ups" ); @@ -247,6 +249,11 @@ sub Authenticate { # Announce to the UPS who we are to set up the proper my $user = shift; # username my $pass = shift; # password + if ($self->{authenticated}) { + $self->_debug("Already authenticated, skip"); + return 1; + } + my $errmsg; # error message, sent to _debug and $self->{err} my $ans; # scalar to hold responses from upsd @@ -256,7 +263,10 @@ sub Authenticate { # Announce to the UPS who we are to set up the proper if (defined $ans && $ans =~ /^OK/) { # username OK, send password $ans = $self->_send("PASSWORD $pass"); - return 1 if (defined $ans && $ans =~ /^OK/); + if (defined $ans && $ans =~ /^OK/) { + $self->{authenticated} = 1; + return 1 + } } } else { $self->_debug($self->{err} = "Authentication failed: username and/or password not provided, internal equivalent of USERNAME-REQUIRED"); @@ -272,7 +282,7 @@ sub Authenticate { # Announce to the UPS who we are to set up the proper return undef; } -sub Logout { # logout of upsd +sub Logout { # logout of upsd and close connection # Author: Kit Peters # ### changelog: uses the new _send command # @@ -281,6 +291,7 @@ sub Logout { # logout of upsd my $ans = $self->_send( "LOGOUT" ); close ($self->{srvsock}); delete ($self->{srvsock}); + $self->{authenticated} = 0; } } @@ -345,6 +356,7 @@ sub _initialize { } } + $self->{authenticated} = 0; if ($user and $pass) { # attempt login to upsd if that option is specified if ($login) { # attempt login to upsd if that option is specified $self->Login($user, $pass) or carp $self->{err}; From cb2d024c85a16d62cec0399c251cc432609d7347 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 00:16:31 +0200 Subject: [PATCH 38/70] scripts/perl/UPS/Nut.pm: _send(): reset the error buffer before networking away [#1711, #1348] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 4062a57d5d..9f492a3844 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -400,6 +400,8 @@ sub _send my $socket = $self->{srvsock}; my $select = $self->{select}; + $self->{err} = ""; + @handles = IO::Select->select( undef, $select, $select, $self->{timeout} ); return undef if ( !scalar $handles[1] ); From 47a31c8c60b1aa7ed0e02d2a0979f529c6b67ef3 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 00:55:11 +0200 Subject: [PATCH 39/70] scripts/perl/test_nutclient.pl: fix DeviceLogin=>ListClient for "admin" without a role [#1711] Signed-off-by: Jim Klimov --- scripts/perl/test_nutclient.pl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index 2f0c063644..ab775449d2 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -210,6 +210,9 @@ if (!defined($NUT_USER)) { die "Secure operation should have failed due to lack of credentials, but did not: $nut->{err}"; } + if (!defined($result)) { + die "Failed to LOGIN: $nut->{err}" + } $loggedIntoDummy = 1; }; if ($@) { From f9cf47056c70a36eb22ceef75b032c06c94d8175 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 01:32:00 +0200 Subject: [PATCH 40/70] clients/nutclient{,mem}.{h,cpp}, tests/cpputest-client.cpp: remember if we enabled TRACKING so it is really done once, confirm with isTrackingModeEnabled() [#656] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 17 +++++++++++++++-- clients/nutclient.h | 11 +++++++++-- clients/nutclientmem.cpp | 8 ++++++++ clients/nutclientmem.h | 1 + tests/cpputest-client.cpp | 18 ++++++++++++++---- 5 files changed, 47 insertions(+), 8 deletions(-) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 123cfe06e0..9ee06b583b 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2053,9 +2053,22 @@ TrackingResult TcpClient::getTrackingResult(const TrackingID& id) } } -void TcpClient::enableTrackingModeOnce() +void TcpClient::enableTrackingModeOnce(void) { - setFeature(TRACKING, true); + if (_tracking != "ON") + { + setFeature(TRACKING, true); + _tracking = sendQuery("GET " + TRACKING); + } +} + +bool TcpClient::isTrackingModeEnabled(void) +{ + if (_tracking != "ON") + { + _tracking = sendQuery("GET " + TRACKING); + } + return (_tracking == "ON"); } TrackingResult TcpClient::waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) diff --git a/clients/nutclient.h b/clients/nutclient.h index 9bb2a4f04a..dc95db6e19 100644 --- a/clients/nutclient.h +++ b/clients/nutclient.h @@ -565,7 +565,12 @@ class Client /** * Enable tracking mode once. */ - virtual void enableTrackingModeOnce() = 0; + virtual void enableTrackingModeOnce(void) = 0; + + /** + * Check if tracking mode is enabled. + */ + virtual bool isTrackingModeEnabled(void) = 0; /** * Wait for a tracking result. @@ -584,6 +589,7 @@ class Client protected: Client(); + std::string _tracking; }; /** @@ -734,7 +740,8 @@ class TcpClient : public Client using Client::getTrackingResult; virtual TrackingResult getTrackingResult(const TrackingID& id) override; - virtual void enableTrackingModeOnce() override; + virtual void enableTrackingModeOnce(void) override; + virtual bool isTrackingModeEnabled(void) override; virtual TrackingResult waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) override; /** diff --git a/clients/nutclientmem.cpp b/clients/nutclientmem.cpp index 82fa9b52b1..8f8dc2638a 100644 --- a/clients/nutclientmem.cpp +++ b/clients/nutclientmem.cpp @@ -255,6 +255,14 @@ void MemClientStub::enableTrackingModeOnce(void) //return TrackingResult::SUCCESS; } +bool MemClientStub::isTrackingModeEnabled(void) { + /* Hush warning: function 'enableTrackingModeOnce' could be declared with attribute 'noreturn' [-Wmissing-noreturn] */ + int id; + NUT_UNUSED_VARIABLE(id); + throw NutException("Not implemented"); + //return false; +} + TrackingResult MemClientStub::waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) { NUT_UNUSED_VARIABLE(id); diff --git a/clients/nutclientmem.h b/clients/nutclientmem.h index 8af365d2a1..747f52b2ca 100644 --- a/clients/nutclientmem.h +++ b/clients/nutclientmem.h @@ -84,6 +84,7 @@ class MemClientStub : public Client virtual TrackingResult getTrackingResult(const TrackingID& id) override; virtual void enableTrackingModeOnce(void) override; + virtual bool isTrackingModeEnabled(void) override; virtual TrackingResult waitTrackingResult(const TrackingID& id, int waitIntervalSec, int waitMaxCount) override; virtual bool isFeatureEnabled(const Feature& feature) override; diff --git a/tests/cpputest-client.cpp b/tests/cpputest-client.cpp index ab9e91e09a..f470b82ae4 100644 --- a/tests/cpputest-client.cpp +++ b/tests/cpputest-client.cpp @@ -526,9 +526,14 @@ void NutActiveClientTest::test_auth_user() { usleep(100); } if (tres != SUCCESS) { - std::cerr << "[D] Failed to set device variable: " + std::cerr << "[D] Failed to confirm setting device variable: " << "tracking result is " << tres << std::endl; - noException = false; + if (c.isTrackingModeEnabled()) { + noException = false; + } else { + std::cerr << "[D] Tracking mode is NOT enabled in this session so far" + << std::endl; + } } /* Check what we got after set */ /* Note that above we told the server to tell the driver @@ -555,9 +560,14 @@ void NutActiveClientTest::test_auth_user() { usleep(100); } if (tres != SUCCESS) { - std::cerr << "[D] Failed to set device variable: " + std::cerr << "[D] Failed to confirm setting device variable: " << "tracking result is " << tres << std::endl; - noException = false; + if (c.isTrackingModeEnabled()) { + noException = false; + } else { + std::cerr << "[D] Tracking mode is NOT enabled in this session so far" + << std::endl; + } } std::string s3; for (i = 0; i < 100 ; i++) { From 738614d08799b9674ef95093c0ae831e0f0f1abb Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 01:34:55 +0200 Subject: [PATCH 41/70] tests/NIT/nit.sh: add a note about jNut using this script [#1711, #1350] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 607cf0fedb..779ebfb160 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -11,6 +11,10 @@ # To speed up this practical developer-testing aspect, you can just # `make check-NIT-sandbox{,-devel}` (optionally with custom DEBUG_SLEEP). # +# Also note that it can be used not only for in-tree checks, but for +# development of other NUT clients against even a packaged installation, +# for a practical example see https://github.com/networkupstools/jNut +# # WARNING: Current working directory when starting the script should be # the location where it may create temporary data (e.g. the BUILDDIR). # Caller can export envvars to impact the script behavior, e.g.: From 6055ed2b6c5787f61acef01213b3b92152138b2c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 02:00:30 +0200 Subject: [PATCH 42/70] clients/Makefile.am: bump libupsclient ABI version Signed-off-by: Jim Klimov --- clients/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/Makefile.am b/clients/Makefile.am index b5d115c234..b5870c2097 100644 --- a/clients/Makefile.am +++ b/clients/Makefile.am @@ -219,7 +219,7 @@ endif WITH_SSL # for the run-time dynamic linker resolution? For now the shared-library # builds are "exotic", but it makes sense to deprecate this export in a # future release. -libupsclient_la_LDFLAGS = -version-info 8:0:1 +libupsclient_la_LDFLAGS = -version-info 9:0:2 libupsclient_la_LDFLAGS += -export-symbols-regex '^(upscli_|nut_debug_level)' #|s_upsdebug|fatalx|fatal_with_errno|xcalloc|xbasename|print_banner_once)' if HAVE_WINDOWS From 03e3b494e3a1a0732dec0f73e5c15e7aace288bf Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 00:48:25 +0000 Subject: [PATCH 43/70] scripts/python/module/PyNUT.py.in: isValidProtocolVersion(): stringify and strip the "result" before matching in regex [#1349] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 8b867e2c6d..93ccdece53 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -402,15 +402,19 @@ if something goes wrong. self.__srv_handler.send( ("NETVER\n").encode('ascii') ) result = self.__read_until( b"\n" ) + # print ( "[DEBUG] isValidProtocolVersion: version_re='%s' result='%s'" % (str(version_re), str(result))) + if result is None: return False + result = result.replace( b"\n", b"" ).decode('ascii') + if version_re is None: # Currently supported: 1.0, 1.1, 1.2, 1.3 => r'^1\.[0-3]$' # Is it an X(.Y) number? - version_re = r'^\d+(?:\.\d+)?$' + version_re = r'^\d+(\.\d+)?$' - if re.match(version_re, str(result)): + if re.match(version_re, result): return True return False From d471e60eead35558ae525467ef44826046d44b76 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 08:18:44 +0000 Subject: [PATCH 44/70] server/netssl.c: net_starttls(): report which SSL backend is in place (if any) [#3331] Signed-off-by: Jim Klimov --- server/netssl.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/netssl.c b/server/netssl.c index 56c74c04cf..4bdeaea46c 100644 --- a/server/netssl.c +++ b/server/netssl.c @@ -375,9 +375,21 @@ void net_starttls(nut_ctype_t *client, size_t numarg, const char **arg) PRFileDesc *socket; # endif /* WITH_OPENSSL | WITH_NSS */ + static char msg_id_ssl[] = +# ifdef WITH_OPENSSL + " OpenSSL" +# elif defined(WITH_NSS) /* WITH_OPENSSL */ + " NSS" +# else /* neither */ + "out" +# endif /* WITH_OPENSSL | WITH_NSS */ + ; + NUT_UNUSED_VARIABLE(numarg); NUT_UNUSED_VARIABLE(arg); + upsdebugx(2, "%s: handling a connection upgrade request, server side built with%s SSL support", __func__, msg_id_ssl); + if (client->ssl) { upsdebugx(2, "%s: NUT_ERR_ALREADY_SSL_MODE because this connection is already initialized as SSL", __func__); From 7d778b6923280866f61599af66693f371905c81a Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 08:48:58 +0000 Subject: [PATCH 45/70] scripts/python/module/PyNUT.py.in: retry SSL handshake if it timed out, a few times [#3401] Signed-off-by: Jim Klimov --- scripts/python/module/PyNUT.py.in | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/scripts/python/module/PyNUT.py.in b/scripts/python/module/PyNUT.py.in index 93ccdece53..8553943144 100644 --- a/scripts/python/module/PyNUT.py.in +++ b/scripts/python/module/PyNUT.py.in @@ -248,10 +248,26 @@ if something goes wrong. print( "[DEBUG] STARTTLS accepted, wrapping socket" ) if self.__ssl_context is None : self.__ssl_context = ssl.create_default_context() - self.__srv_handler = self.__ssl_context.wrap_socket( - self.__srv_handler, - server_hostname=self.__host - ) + + # Not sure if we can enter this river twice (e.g. if the server + # side of the handshake would still be listening), but we can try: + for i in range(5, 0, -1): + try: + self.__srv_handler = self.__ssl_context.wrap_socket( + self.__srv_handler, + server_hostname=self.__host + ) + break + except TimeoutError as te: + if self.__debug: + print( "[DEBUG] STARTTLS handshake timed out%s: %s" %( + ("" if (i < 1) else (", retrying (%d)" % i)), + str(te)) ) + + if (i > 1): + sleep(1) + else: + raise te # Make sure handshake succeeded or abort early # (there is currently no way for the server to From 86a8d77404d62a6b13f15ba485693a11e0fcfe0b Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 09:30:37 +0000 Subject: [PATCH 46/70] scripts/perl/UPS/Nut.pm: use CAPATH to provide SSL_ca_path, not SSL_ca_file [#1348, #1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 9f492a3844..88d5704f5c 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -8,6 +8,7 @@ # ### changelog: Removed timeleft() function. # ### changelog: 1.60: JK 2026-04-08: Added basic STARTTLS, as well as TRACKING support, LIST CLIENT, LIST RANGE, GET UPSDESC and PRIMARY/MASTER aliasing. # ### changelog: 1.61: JK 2026-04-08: Make TrackingID a class, similar to C++. +# ### changelog: 1.62: JK 2026-04-10: Added a testing script nearby; revised API and NUT protocol support, notably TRACKING and STARTTLS. package UPS::Nut; use strict; @@ -180,7 +181,7 @@ sub StartTLS { IO::Socket::SSL->start_SSL( $self->{srvsock}, SSL_verify_mode => $arg{CERTVERIFY} ? IO::Socket::SSL::SSL_VERIFY_PEER() : IO::Socket::SSL::SSL_VERIFY_NONE(), - SSL_ca_file => $arg{CAPATH}, + SSL_ca_path => $arg{CAPATH}, %arg ) or do { $self->_debug($self->{err} = "SSL upgrade failed: " . IO::Socket::SSL->errstr()); From 06bbf7f2edf9db5c68cf2e94301c07ec35f949f1 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 10:00:12 +0000 Subject: [PATCH 47/70] scripts/perl/UPS/Nut.pm: STARTTLS: debug-print %arg list that is passed to the SSL library [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 88d5704f5c..40857363f3 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -177,6 +177,16 @@ sub StartTLS { $ans = $self->_send("STARTTLS"); if (defined $ans && $ans =~ /^OK STARTTLS/) { $self->_debug("STARTTLS accepted, upgrading socket."); + my $strarg = "[" . scalar(%arg) . "]"; + for (keys %arg) { + $strarg .= " $_=>" . ($arg{$_}//"undef"); + } + $self->_debug("STARTTLS args: SSL_verify_mode=>" + . ($arg{CERTVERIFY} ? "SSL_VERIFY_PEER" : "SSL_VERIFY_NONE") + . " SSL_ca_path=>'$arg{CAPATH}' " + . " Other args: " . $strarg + ); + # NOTE: Currently nothing fancy like client's own certificate databases... IO::Socket::SSL->start_SSL( $self->{srvsock}, From 0807604b36c18ed92e6c7d7ba426278e67518672 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 10:16:01 +0000 Subject: [PATCH 48/70] scripts/perl/UPS/Nut.pm, scripts/perl/test_nutclient.pl: SSL_ca_path should be defined in args by caller, not by a hack in production code [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 2 -- scripts/perl/test_nutclient.pl | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 40857363f3..e9cbb50753 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -183,7 +183,6 @@ sub StartTLS { } $self->_debug("STARTTLS args: SSL_verify_mode=>" . ($arg{CERTVERIFY} ? "SSL_VERIFY_PEER" : "SSL_VERIFY_NONE") - . " SSL_ca_path=>'$arg{CAPATH}' " . " Other args: " . $strarg ); @@ -191,7 +190,6 @@ sub StartTLS { IO::Socket::SSL->start_SSL( $self->{srvsock}, SSL_verify_mode => $arg{CERTVERIFY} ? IO::Socket::SSL::SSL_VERIFY_PEER() : IO::Socket::SSL::SSL_VERIFY_NONE(), - SSL_ca_path => $arg{CAPATH}, %arg ) or do { $self->_debug($self->{err} = "SSL upgrade failed: " . IO::Socket::SSL->errstr()); diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index ab775449d2..457e06337e 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -47,13 +47,13 @@ # TRACKING => 'ON', # undef by default, enabled in certain tests below # STARTTLS related (passed via %arg to StartTLS in Nut.pm) CERTVERIFY => $NUT_CERTVERIFY, - CAPATH => $NUT_CAPATH, # Nut.pm uses CAPATH for SSL_ca_file FORCESSL => $NUT_FORCESSL, # In case PyNUT's cert_file, key_file are needed: + SSL_ca_file => $NUT_CAFILE, + SSL_ca_path => $NUT_CAPATH, SSL_cert_file => $ENV{'NUT_CERTFILE'}, SSL_key_file => $ENV{'NUT_KEYFILE'}, - SSL_key_pass => $ENV{'NUT_KEYPASS'}, - SSL_ca_file => $NUT_CAFILE, + SSL_key_pass => $ENV{'NUT_KEYPASS'} ); }; if ($@ || !defined($nut)) { From ebf34ea8507d9bcf6312b29790e72656c7046961 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 10:20:14 +0000 Subject: [PATCH 49/70] scripts/perl/UPS/Nut.pm, scripts/perl/test_nutclient.pl: support NUT_SSL envvar toggle to disable attempts at SSL altogether [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 34 +++++++++++++++++++--------------- scripts/perl/test_nutclient.pl | 1 + 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index e9cbb50753..77df67d1de 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -343,26 +343,30 @@ sub _initialize { $self->{select} = IO::Select->new( $srvsock ); - # Always try to elevate, do not bother if this fails unless required by args - my $startedTLS = $self->StartTLS(%arg); - if (defined $startedTLS && $startedTLS) { - # Make sure handshake succeeded or abort early - # (there is currently no way for the server to - # report its fault to the client when connection - # is half-way secure): - if (!$self->isValidProtocolVersion()) { + if ($arg{USESSL} || $arg{FORCESSL}) { + # Always try to elevate, do not bother if this fails unless required by args + my $startedTLS = $self->StartTLS(%arg); + if (defined $startedTLS && $startedTLS) { + # Make sure handshake succeeded or abort early + # (there is currently no way for the server to + # report its fault to the client when connection + # is half-way secure): + if (!$self->isValidProtocolVersion()) { + if ($arg{FORCESSL}) { + $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required"); + return undef; + } + $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, but SSL is not required"); + # TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? + } + } else { if ($arg{FORCESSL}) { - $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required"); + $self->_debug($self->{err} = "SSL setup failed but it is required"); return undef; } - $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, but SSL is not required"); - # TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? } } else { - if ($arg{FORCESSL}) { - $self->_debug($self->{err} = "SSL setup failed but it is required"); - return undef; - } + $self->_debug("SSL setup neither requested nor required"); } $self->{authenticated} = 0; diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index 457e06337e..a6753bee30 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -46,6 +46,7 @@ DEBUG => $NUT_DEBUG, # TRACKING => 'ON', # undef by default, enabled in certain tests below # STARTTLS related (passed via %arg to StartTLS in Nut.pm) + USESSL => $NUT_SSL, CERTVERIFY => $NUT_CERTVERIFY, FORCESSL => $NUT_FORCESSL, # In case PyNUT's cert_file, key_file are needed: From ded62cb8df2906b77c5a9320b9ffb4b4989b4d25 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 10:25:14 +0000 Subject: [PATCH 50/70] scripts/perl/UPS/Nut.pm: apply indentation consistenly (3 styles were intermixed) Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 472 ++++++++++++++++++++-------------------- 1 file changed, 236 insertions(+), 236 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 77df67d1de..d8fbb72c9a 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -27,13 +27,13 @@ use Dumpvalue; my $dumper = Dumpvalue->new; my $_eol = "\n"; BEGIN { - use Exporter (); - use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); - $VERSION = 1.62; - @ISA = qw(Exporter IO::Socket::INET); - @EXPORT = qw(); - @EXPORT_OK = qw(); - %EXPORT_TAGS = (); + use Exporter (); + use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); + $VERSION = 1.62; + @ISA = qw(Exporter IO::Socket::INET); + @EXPORT = qw(); + @EXPORT_OK = qw(); + %EXPORT_TAGS = (); } sub new { @@ -54,31 +54,31 @@ sub new { # otherwise. sub BattPercent { # get battery percentage - return shift->GetVar('battery.charge'); + return shift->GetVar('battery.charge'); } sub LoadPercent { # get load percentage - my $self = shift; - my $context = shift; - $context = "L$context" if $context =~ /^[123]$/; - $context = ".$context" if $context; - return $self->GetVar("output$context.power.percent"); + my $self = shift; + my $context = shift; + $context = "L$context" if $context =~ /^[123]$/; + $context = ".$context" if $context; + return $self->GetVar("output$context.power.percent"); } sub LineVoltage { # get line voltage - my $self = shift; - my $context = shift; - $context = "L$context-N" if $context =~ /^[123]$/; - $context = ".$context" if $context; - return $self->GetVar("input$context.voltage"); + my $self = shift; + my $context = shift; + $context = "L$context-N" if $context =~ /^[123]$/; + $context = ".$context" if $context; + return $self->GetVar("input$context.voltage"); } sub Status { # get status of UPS - return shift->GetVar('ups.status'); + return shift->GetVar('ups.status'); } sub Temperature { # get the internal temperature of UPS - return shift->GetVar('battery.temperature'); + return shift->GetVar('battery.temperature'); } # control functions: they control our relationship to upsd, and send @@ -405,46 +405,46 @@ sub _initialize { sub _send { # Contributor: Wayne Wylupski - my $self = shift; - my $cmd = shift; - my @handles; - my $result; # undef by default + my $self = shift; + my $cmd = shift; + my @handles; + my $result; # undef by default - my $socket = $self->{srvsock}; - my $select = $self->{select}; + my $socket = $self->{srvsock}; + my $select = $self->{select}; - $self->{err} = ""; + $self->{err} = ""; - @handles = IO::Select->select( undef, $select, $select, $self->{timeout} ); - return undef if ( !scalar $handles[1] ); + @handles = IO::Select->select( undef, $select, $select, $self->{timeout} ); + return undef if ( !scalar $handles[1] ); - $socket->print( $cmd . $_eol ); + $socket->print( $cmd . $_eol ); - @handles = IO::Select->select( $select, undef, $select, $self->{timeout} ); - return undef if ( !scalar $handles[0]); + @handles = IO::Select->select( $select, undef, $select, $self->{timeout} ); + return undef if ( !scalar $handles[0]); - $result = $socket->getline; - return undef if ( !defined ( $result ) ); - chomp $result; + $result = $socket->getline; + return undef if ( !defined ( $result ) ); + chomp $result; - return $result; + return $result; } sub _getline { # Contributor: Wayne Wylupski - my $self = shift; - my $result; # undef by default + my $self = shift; + my $result; # undef by default - my $socket = $self->{srvsock}; - my $select = $self->{select}; + my $socket = $self->{srvsock}; + my $select = $self->{select}; - # Different versions of IO::Socket has different error detection routines. - return undef if ( $IO::Socket::{has_error} && $select->has_error(0) ); - return undef if ( $IO::Socket::{has_exception} && $select->has_exception(0) ); + # Different versions of IO::Socket has different error detection routines. + return undef if ( $IO::Socket::{has_error} && $select->has_error(0) ); + return undef if ( $IO::Socket::{has_exception} && $select->has_exception(0) ); - chomp ( $result = $socket->getline ); - return $result; + chomp ( $result = $socket->getline ); + return $result; } # Compatibility layer @@ -641,8 +641,8 @@ sub InstCmd { # send instant command to ups } sub ListUPS { - my $self = shift; - return $self->_get_list("LIST UPS", 2, 1); + my $self = shift; + return $self->_get_list("LIST UPS", 2, 1); } sub GetTrackingResult { @@ -735,113 +735,113 @@ sub GetUPSDesc { } sub ListVar { - my $self = shift; - my $ups = shift || $self->{name}; + my $self = shift; + my $ups = shift || $self->{name}; - my $vars = $self->_get_list("LIST VAR $ups", 3, 2); - return $vars unless @_; # return all variables - return {map { $_ => $vars->{$_} } @_}; # return selected ones + my $vars = $self->_get_list("LIST VAR $ups", 3, 2); + return $vars unless @_; # return all variables + return {map { $_ => $vars->{$_} } @_}; # return selected ones } sub ListRW { - my $self = shift; - my $ups = shift || $self->{name}; + my $self = shift; + my $ups = shift || $self->{name}; - return $self->_get_list("LIST RW $ups", 3, 2); + return $self->_get_list("LIST RW $ups", 3, 2); } sub ListCmd { - my $self = shift; - my $ups = shift || $self->{name}; + my $self = shift; + my $ups = shift || $self->{name}; - return $self->_get_list("LIST CMD $ups", 2); + return $self->_get_list("LIST CMD $ups", 2); } sub ListEnum { - my $self = shift; - my $var = shift; - # NOTE: This clumsily goes after $var, to avoid breaking - # API compatibility for older release consumers: - my $ups = shift || $self->{name}; + my $self = shift; + my $var = shift; + # NOTE: This clumsily goes after $var, to avoid breaking + # API compatibility for older release consumers: + my $ups = shift || $self->{name}; - return $self->_get_list("LIST ENUM $ups $var", 3); + return $self->_get_list("LIST ENUM $ups $var", 3); } sub ListRange { - my $self = shift; - my $var = shift; - # NOTE: This clumsily goes after other args above, to avoid - # breaking API compatibility for older release consumers: - my $ups = shift || $self->{name}; - - my $req = "LIST RANGE $ups $var"; - my $ans = $self->_send($req); - - unless (defined $ans) { - $self->_debug($self->{err} = "Network error: $!"); - return undef; - }; - - if ($ans =~ /^ERR/) { - $self->_debug($self->{err} = "Error: $ans"); - return undef; - } - elsif ($ans =~ /^BEGIN LIST RANGE/) { - my $retval = []; - my $line; - while ($line = $self->_getline) { - last if $line =~ /^END LIST RANGE/; - # RANGE "" "" - if ($line =~ /^RANGE \S+ \S+ "([^"]+)" "([^"]+)"/) { - push(@$retval, { min => $1, max => $2 }); - } - } - return $retval; - } - - $self->_debug($self->{err} = "Unrecognized response: $ans"); - return undef; + my $self = shift; + my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; + + my $req = "LIST RANGE $ups $var"; + my $ans = $self->_send($req); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^BEGIN LIST RANGE/) { + my $retval = []; + my $line; + while ($line = $self->_getline) { + last if $line =~ /^END LIST RANGE/; + # RANGE "" "" + if ($line =~ /^RANGE \S+ \S+ "([^"]+)" "([^"]+)"/) { + push(@$retval, { min => $1, max => $2 }); + } + } + return $retval; + } + + $self->_debug($self->{err} = "Unrecognized response: $ans"); + return undef; } sub _get_list { - my $self = shift; - my ($req, $valueidx, $keyidx) = @_; - my $ans = $self->_send($req); - - unless (defined $ans) { - $self->_debug($self->{err} = "Network error: $!"); - return undef; - }; - - if ($ans =~ /^ERR/) { - $self->_debug($self->{err} = "Error: $ans"); - return undef; - } - elsif ($ans =~ /^BEGIN LIST/) { # command successful - my $retval = $keyidx ? {} : []; - my $line; - while ($line = $self->_getline) { - last if $line =~ /^END LIST/; - my @fields = split(' ', $line, $valueidx+1); - (my $value = $fields[$valueidx]) =~ s/^"(.*)"$/$1/; - if ($keyidx) { - $retval->{$fields[$keyidx]} = $value; - } - else { - push(@$retval, $value); - } - } - unless ($line) { - $self->_debug($self->{err} = "Network error: $!"); - return undef; - }; - $self->_debug("$req command sent successfully."); - return $retval; - } - else { # unrecognized response - $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); - return undef; - } + my $self = shift; + my ($req, $valueidx, $keyidx) = @_; + my $ans = $self->_send($req); + + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^BEGIN LIST/) { # command successful + my $retval = $keyidx ? {} : []; + my $line; + while ($line = $self->_getline) { + last if $line =~ /^END LIST/; + my @fields = split(' ', $line, $valueidx+1); + (my $value = $fields[$valueidx]) =~ s/^"(.*)"$/$1/; + if ($keyidx) { + $retval->{$fields[$keyidx]} = $value; + } + else { + push(@$retval, $value); + } + } + unless ($line) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; + $self->_debug("$req command sent successfully."); + return $retval; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); + return undef; + } } # Compatibility layer @@ -850,34 +850,34 @@ sub VarDesc { goto &GetDesc; } sub GetDesc { # Contributor: Wayne Wylupski # Modified by Gabor Kiss according to protocol version 1.5+ - my $self = shift; - my $var = shift; - # NOTE: This clumsily goes after other args above, to avoid - # breaking API compatibility for older release consumers: - my $ups = shift || $self->{name}; + my $self = shift; + my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; - my $req = "GET DESC $ups $var"; - my $ans = $self->_send( $req ); + my $req = "GET DESC $ups $var"; + my $ans = $self->_send( $req ); - unless (defined $ans) { - $self->_debug($self->{err} = "Network error: $!"); - return undef; - }; + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; - if ($ans =~ /^ERR/) { - $self->_debug($self->{err} = "Error: $ans"); - return undef; - } - elsif ($ans =~ /^DESC/) { # command successful - $self->_debug("$req command sent successfully."); - (undef, undef, undef, $ans) = split(' ', $ans, 4); - $ans =~ s/^"(.*)"$/$1/; - return $ans; - } - else { # unrecognized response - $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); - return undef; - } + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^DESC/) { # command successful + $self->_debug("$req command sent successfully."); + (undef, undef, undef, $ans) = split(' ', $ans, 4); + $ans =~ s/^"(.*)"$/$1/; + return $ans; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); + return undef; + } } # Compatibility layer @@ -886,33 +886,33 @@ sub VarType { goto &GetType; } sub GetType { # Contributor: Wayne Wylupski # Modified by Gabor Kiss according to protocol version 1.5+ - my $self = shift; - my $var = shift; - # NOTE: This clumsily goes after other args above, to avoid - # breaking API compatibility for older release consumers: - my $ups = shift || $self->{name}; + my $self = shift; + my $var = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; - my $req = "GET TYPE $ups $var"; - my $ans = $self->_send( $req ); + my $req = "GET TYPE $ups $var"; + my $ans = $self->_send( $req ); - unless (defined $ans) { - $self->_debug($self->{err} = "Network error: $!"); - return undef; - }; + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; - if ($ans =~ /^ERR/) { - $self->_debug($self->{err} = "Error: $ans"); - return undef; - } - elsif ($ans =~ /^TYPE/) { # command successful - $self->_debug("$req command sent successfully."); - (undef, undef, undef, $ans) = split(' ', $ans, 4); - return $ans; - } - else { # unrecognized response - $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); - return undef; - } + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^TYPE/) { # command successful + $self->_debug("$req command sent successfully."); + (undef, undef, undef, $ans) = split(' ', $ans, 4); + return $ans; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); + return undef; + } } # Compatibility layer @@ -921,34 +921,34 @@ sub InstCmdDesc { goto &GetCmdDesc; } sub GetCmdDesc { # Contributor: Wayne Wylupski # Modified by Gabor Kiss according to protocol version 1.5+ - my $self = shift; - my $cmd = shift; - # NOTE: This clumsily goes after other args above, to avoid - # breaking API compatibility for older release consumers: - my $ups = shift || $self->{name}; + my $self = shift; + my $cmd = shift; + # NOTE: This clumsily goes after other args above, to avoid + # breaking API compatibility for older release consumers: + my $ups = shift || $self->{name}; - my $req = "GET CMDDESC $ups $cmd"; - my $ans = $self->_send( $req ); + my $req = "GET CMDDESC $ups $cmd"; + my $ans = $self->_send( $req ); - unless (defined $ans) { - $self->_debug($self->{err} = "Network error: $!"); - return undef; - }; + unless (defined $ans) { + $self->_debug($self->{err} = "Network error: $!"); + return undef; + }; - if ($ans =~ /^ERR/) { - $self->_debug($self->{err} = "Error: $ans"); - return undef; - } - elsif ($ans =~ /^DESC/) { # command successful - $self->_debug("$req command sent successfully."); - (undef, undef, undef, $ans) = split(' ', $ans, 4); - $ans =~ s/^"(.*)"$/$1/; - return $ans; - } - else { # unrecognized response - $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); - return undef; - } + if ($ans =~ /^ERR/) { + $self->_debug($self->{err} = "Error: $ans"); + return undef; + } + elsif ($ans =~ /^DESC/) { # command successful + $self->_debug("$req command sent successfully."); + (undef, undef, undef, $ans) = split(' ', $ans, 4); + $ans =~ s/^"(.*)"$/$1/; + return $ans; + } + else { # unrecognized response + $self->_debug($self->{err} = "Can't send $req. Unrecognized response from upsd: $ans"); + return undef; + } } sub DESTROY { # destructor, all it does is call Logout @@ -1032,18 +1032,18 @@ sub Master { # check for MASTER level access sub AUTOLOAD { # Contributor: Wayne Wylupski - my $self = shift; - my $name = $UPS::Nut::AUTOLOAD; - $name =~ s/^.*:://; + my $self = shift; + my $name = $UPS::Nut::AUTOLOAD; + $name =~ s/^.*:://; - # for a change we will only load cmds if needed. - if (!defined $self->{cmds} ) { - %{$self->{cmds}} = map{ $_ =>1 } @{$self->ListCmd}; - } + # for a change we will only load cmds if needed. + if (!defined $self->{cmds} ) { + %{$self->{cmds}} = map{ $_ =>1 } @{$self->ListCmd}; + } - croak "No such InstCmd: $name" if (! $self->{cmds}{$name} ); + croak "No such InstCmd: $name" if (! $self->{cmds}{$name} ); - return $self->InstCmd( $name ); + return $self->InstCmd( $name ); } #------------------------------------------------------------------------- @@ -1064,50 +1064,50 @@ sub AUTOLOAD { # #------------------------------------------------------------------------- sub TIEHASH { - my $class = shift || 'UPS::Nut'; - return $class->new( @_ ); + my $class = shift || 'UPS::Nut'; + return $class->new( @_ ); } sub FETCH { - my $self = shift; - my $key = shift; + my $self = shift; + my $key = shift; - return $self->Request( $key ); + return $self->Request( $key ); } sub STORE { - my $self = shift; - my $key = shift; - my $value = shift; + my $self = shift; + my $key = shift; + my $value = shift; - return $self->Set( $key, $value ); + return $self->Set( $key, $value ); } sub DELETE { - croak "DELETE operation not supported"; + croak "DELETE operation not supported"; } sub CLEAR { - croak "CLEAR operation not supported"; + croak "CLEAR operation not supported"; } sub EXISTS { - exists shift->{vars}{shift}; + exists shift->{vars}{shift}; } sub FIRSTKEY { - my $self = shift; - my $a = keys %{$self->{vars}}; - return scalar each %{$self->{vars}}; + my $self = shift; + my $a = keys %{$self->{vars}}; + return scalar each %{$self->{vars}}; } sub NEXTKEY { - my $self = shift; - return scalar each %{$self->{vars}}; + my $self = shift; + return scalar each %{$self->{vars}}; } sub UNTIE { - $_[0]->Logout; + $_[0]->Logout; } =head1 NAME From e4f7027a8ee3996980a5e25e7acb70455e4a8e60 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 10:36:05 +0000 Subject: [PATCH 51/70] scripts/perl/UPS/Nut.pm: _initialize(): include previously reported error in SSL setup fail messages [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index d8fbb72c9a..166cd1d83f 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -353,20 +353,20 @@ sub _initialize { # is half-way secure): if (!$self->isValidProtocolVersion()) { if ($arg{FORCESSL}) { - $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required"); + $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, and SSL is required: $self->{err}"); return undef; } - $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, but SSL is not required"); + $self->_debug($self->{err} = "STARTTLS setup claimed to succeed, but protocol version check in the secured session failed, but SSL is not required: $self->{err}"); # TODO: Drop SSL context or restart the connection as plaintext if SSL is not required? } } else { if ($arg{FORCESSL}) { - $self->_debug($self->{err} = "SSL setup failed but it is required"); + $self->_debug($self->{err} = "SSL setup failed but it is required: $self->{err}"); return undef; } } } else { - $self->_debug("SSL setup neither requested nor required"); + $self->_debug("SSL setup neither requested nor required, skipped StartTLS altogether"); } $self->{authenticated} = 0; From f30866bdccc1872bac10ab1c236046c8589b94be Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 11:16:21 +0000 Subject: [PATCH 52/70] scripts/perl/UPS/Nut.pm: support USESSL=undef, check that we can_ssl and follow up with that [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 166cd1d83f..6f1fcf161a 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -343,7 +343,19 @@ sub _initialize { $self->{select} = IO::Select->new( $srvsock ); - if ($arg{USESSL} || $arg{FORCESSL}) { + my $can_ssl = 1; + eval "require IO::Socket::SSL"; + if ($@) { + $can_ssl = 0; + } + + my $use_ssl = $arg{USESSL}; + if (!defined $use_ssl) { + $self->_debug("USESSL option was undef, flipping to IO::Socket::SSL module availability: $can_ssl"); + $use_ssl = $can_ssl; + } + + if ($use_ssl || $arg{FORCESSL}) { # Always try to elevate, do not bother if this fails unless required by args my $startedTLS = $self->StartTLS(%arg); if (defined $startedTLS && $startedTLS) { From 8ee9d4bdf55f95833f737d1251acd43a814fcfbb Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 11:19:39 +0000 Subject: [PATCH 53/70] scripts/perl/test_nutclient.pl: support USESSL=undef if no value provided in env [#1711] Signed-off-by: Jim Klimov --- scripts/perl/test_nutclient.pl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index a6753bee30..bf1f7e2113 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -15,7 +15,10 @@ my $NUT_PORT = $ENV{'NUT_PORT'} || '3493'; my $NUT_USER = $ENV{'NUT_USER'} || undef; my $NUT_PASS = $ENV{'NUT_PASS'} || undef; - my $NUT_SSL = (($ENV{'NUT_SSL'} || "false") eq "true") ? 1 : 0; + my $NUT_SSL = $ENV{'NUT_SSL'} || undef; + if (defined $NUT_SSL) { + $NUT_SSL = ($NUT_SSL eq "true" ? 1 : ($NUT_SSL eq "false" ? 0 : undef)); + } my $NUT_FORCESSL = (($ENV{'NUT_FORCESSL'} || "false") eq "true" || ($ENV{'NUT_FORCESSL'} || "false") eq "1") ? 1 : 0; my $NUT_CERTVERIFY = (($ENV{'NUT_CERTVERIFY'} || "false") eq "true" || ($ENV{'NUT_CERTVERIFY'} || "false") eq "1") ? 1 : 0; my $NUT_CAFILE = $ENV{'NUT_CAFILE'} || undef; From 4fb4762b768e930a078a44ebf92d9150c27aced6 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 11:20:26 +0000 Subject: [PATCH 54/70] tests/NIT/nit.sh: setenv_ssl_perl(): check if IO::Socket::SSL is available, neuter NUT_FORCESSL based on that [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 779ebfb160..c88159e727 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2787,7 +2787,17 @@ testcases_sandbox_python() { #################################### setenv_ssl_perl() { - setenv_ssl_python + setenv_ssl_python + + if isTestablePerl && [ -n "${PERL}" ] ; then + $PERL -e "use IO::Socket::SSL;" || { + log_warn "The perl interpreter '$PERL' can not use IO::Socket::SSL module, so we will not FORCESSL in the test" + NUT_FORCESSL=0 + export NUT_FORCESSL + # Let the test script and eventually module auto-detect undef => can_ssl + unset NUT_SSL + } + fi } PL_SHEBANG="" From 322c84ca18e038389d8a9d1255a80af08ae69052 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 11:36:23 +0000 Subject: [PATCH 55/70] scripts/perl/UPS/Nut.pm: when IO::Socket::SSL is not available, report the error with "FEATURE-NOT-SUPPORTED" in text [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 6f1fcf161a..89407a9eed 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -170,7 +170,7 @@ sub StartTLS { eval { require IO::Socket::SSL; }; if ($@) { - $self->_debug($self->{err} = "IO::Socket::SSL not available"); + $self->_debug($self->{err} = "IO::Socket::SSL not available: FEATURE-NOT-SUPPORTED on client side"); return undef; } From 8ff6d467b79ef97d39ee7701ae7e3156d738e067 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 11:39:49 +0000 Subject: [PATCH 56/70] scripts/perl/test_nutclient.pl: avoid "Can't call method "Error" on an undefined value" messages in failed tests; clearly say that $nut is no more [#1711] Signed-off-by: Jim Klimov --- scripts/perl/test_nutclient.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index bf1f7e2113..8f555ef09b 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -61,7 +61,7 @@ ); }; if ($@ || !defined($nut)) { - my $ex = $@ || $nut->Error(); + my $ex = $@ || (defined($nut) ? $nut->Error() : "N/A: \$nut object already discarded, can not retrieve its Error()"); print "EXCEPTION during initialization: $ex\n"; if ($NUT_SSL && ($ex =~ /FEATURE-NOT-CONFIGURED/ || $ex =~ /FEATURE-NOT-SUPPORTED/)) { print "(anticipated error: server does not support STARTTLS)\n"; From dc419511da2e28a6c38a04351e2fd06e31940617 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Fri, 10 Apr 2026 11:50:41 +0000 Subject: [PATCH 57/70] NEWS.adoc: announce updates to client binding libraries [#3402] Signed-off-by: Jim Klimov --- NEWS.adoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/NEWS.adoc b/NEWS.adoc index 4dd5544251..572493e98a 100644 --- a/NEWS.adoc +++ b/NEWS.adoc @@ -59,6 +59,20 @@ https://github.com/networkupstools/nut/milestone/13 [issue #3387, PR #3402] + - NUT client libraries: + * Complete support for actions documented in `docs/net-protocol.txt` + was implemented in C++, Python and PERL bindings in-tree, and for Java + in link:https://github.com/networkupstools/jNut[jNut] nearby. Among + other things, all these libraries now support `STARTTLS` and `TRACKING` + to wait for server confirmation of a `SET VAR` or `INSTCMD` request, + and this is tested by the NIT script. [issues #656, #1348, #1349, #1350, + #1613, #1711, PR #3402] + * Enhanced client side of `STARTTLS` dialog to follow up by a simple + query (for protocol version) to verify that handshake succeeded. + This change impacted also the classic C `libupsclient` library. + [issue #3387, PR #3402] + + Release notes for NUT 2.8.5 - what's new since 2.8.4 ---------------------------------------------------- From 8e0b5ea523f56fb071c1c407c3efbcab2fab3782 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sat, 11 Apr 2026 17:10:20 +0200 Subject: [PATCH 58/70] m4/ax_c_pragmas.m4, clients/nutclient.cpp: introduce HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS{,_BESIDEFUNC} to implement methods for deprecated "master" operation [#840, #3402] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 15 +++++++++++++++ m4/ax_c_pragmas.m4 | 27 +++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index 9ee06b583b..a3f00f12e7 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2642,7 +2642,15 @@ void Device::login() void Device::master() { if (!isOk()) throw NutException("Invalid device"); + +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif getClient()->deviceMaster(getName()); +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS +#pragma GCC diagnostic pop +#endif } void Device::becomePrimary() @@ -3448,7 +3456,14 @@ void nutclient_device_master(NUTCLIENT_t client, const char* dev) { try { +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif cl->deviceMaster(dev); +#if (defined HAVE_PRAGMA_GCC_DIAGNOSTIC_PUSH_POP) && defined HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS +#pragma GCC diagnostic pop +#endif } catch(...){} } diff --git a/m4/ax_c_pragmas.m4 b/m4/ax_c_pragmas.m4 index b93b63dede..18da10ee93 100644 --- a/m4/ax_c_pragmas.m4 +++ b/m4/ax_c_pragmas.m4 @@ -1120,6 +1120,33 @@ dnl ### [CFLAGS="${CFLAGS_SAVED} -Werror=pragmas -Werror=unknown-warning" AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_EXIT_TIME_DESTRUCTORS_BESIDEFUNC], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wexit-time-destructors" (outside functions)]) ]) + AC_CACHE_CHECK([for C++ pragma GCC diagnostic ignored "-Wdeprecated-declarations"], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations], + [AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[void func(void) { +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +} +]], [])], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations=yes], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations=no] + )] + ) + AS_IF([test "$ax_cv__pragma__gcc__diags_ignored_deprecated_declarations" = "yes"],[ + AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wdeprecated-declarations"]) + ]) + + AC_CACHE_CHECK([for C++ pragma GCC diagnostic ignored "-Wdeprecated-declarations" (outside functions)], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations_besidefunc], + [AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[#pragma GCC diagnostic ignored "-Wdeprecated-declarations"]], [])], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations_besidefunc=yes], + [ax_cv__pragma__gcc__diags_ignored_deprecated_declarations_besidefunc=no] + )] + ) + AS_IF([test "$ax_cv__pragma__gcc__diags_ignored_deprecated_declarations_besidefunc" = "yes"],[ + AC_DEFINE([HAVE_PRAGMA_GCC_DIAGNOSTIC_IGNORED_DEPRECATED_DECLARATIONS_BESIDEFUNC], 1, [define if your compiler has #pragma GCC diagnostic ignored "-Wdeprecated-declarations" (outside functions)]) + ]) + AC_CACHE_CHECK([for C++ pragma GCC diagnostic ignored "-Wsuggest-override" (outside functions)], [ax_cv__pragma__gcc__diags_ignored_suggest_override_besidefunc], [AC_COMPILE_IFELSE( From 4b179b85d8c157d6f3fdaac0c9c0a56b99bd5987 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 06:30:45 +0200 Subject: [PATCH 59/70] tests/NIT/nit.sh: report the non-default NIT_CASE being handled [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index c88159e727..91d7c67f65 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -3263,6 +3263,8 @@ case "${NIT_CASE}" in isBusy_NUT_PORT) DEBUG=yes isBusy_NUT_PORT ;; cppnit) if isTestableCppNIT ; then + log_separator + log_info "Running NIT_CASE='$NIT_CASE': testgroup_sandbox_cppnit" testgroup_sandbox_cppnit else FAILED="`expr $FAILED + 1`" @@ -3271,6 +3273,8 @@ case "${NIT_CASE}" in ;; python) if isTestablePython && [ -n "${PYTHON}" ] ; then + log_separator + log_info "Running NIT_CASE='$NIT_CASE': testgroup_sandbox_python" testgroup_sandbox_python else FAILED="`expr $FAILED + 1`" @@ -3279,6 +3283,8 @@ case "${NIT_CASE}" in ;; perl) if isTestablePerl && [ -n "${PERL}" ] ; then + log_separator + log_info "Running NIT_CASE='$NIT_CASE': testgroup_sandbox_perl" testgroup_sandbox_perl else FAILED="`expr $FAILED + 1`" @@ -3287,6 +3293,8 @@ case "${NIT_CASE}" in ;; nutscanner|nut-scanner) if isTestableNutScanner && [ -n "${PERL}" ] ; then + log_separator + log_info "Running NIT_CASE='$NIT_CASE': testgroup_sandbox_nutscanner" testgroup_sandbox_nutscanner else FAILED="`expr $FAILED + 1`" @@ -3301,6 +3309,7 @@ case "${NIT_CASE}" in log_warn "========================================================" # NOTE: Not quoted, can have further arguments # e.g. NIT_CASE="testgroup_sandbox_upsmon_master 1" + log_info "Running NIT_CASE='$NIT_CASE'" eval ${NIT_CASE} ;; generatecfg_*|is*) @@ -3312,6 +3321,7 @@ case "${NIT_CASE}" in log_warn "Notably, NUT_CONFPATH='$NUT_CONFPATH' now" log_warn "========================================================" # NOTE: Not quoted, can have further arguments + log_info "Running NIT_CASE='$NIT_CASE'" eval ${NIT_CASE} if [ $? = 0 ] ; then PASSED="`expr $PASSED + 1`" From 7b14c2587bf3e9c7266659cd8aa49d3212e25267 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 07:24:46 +0200 Subject: [PATCH 60/70] scripts/perl/UPS/Nut.pm: support debugging messages of IO::Socket::SSL [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 89407a9eed..1519b768ca 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -168,7 +168,11 @@ sub StartTLS { my %arg = @_; my $ans; - eval { require IO::Socket::SSL; }; + if ($self->{debugssl}) { + eval { use IO::Socket::SSL qw(debug6); }; + } else { + eval { require IO::Socket::SSL; }; + } if ($@) { $self->_debug($self->{err} = "IO::Socket::SSL not available: FEATURE-NOT-SUPPORTED on client side"); return undef; @@ -327,6 +331,7 @@ sub _initialize { $self->{name} = $arg{NAME} || 'default'; # UPS name in etc/ups.conf on $host $self->{timeout} = $arg{TIMEOUT} || 30; # timeout $self->{debug} = $arg{DEBUG} || 0; # debugging? + $self->{debugssl} = $arg{DEBUGSSL} || $self->{debug}; # debugging IO::Socket::SSL upon use? $self->{debugout} = $arg{DEBUGOUT} || undef; # where to send debug messages my $srvsock = $self->{srvsock} = # establish connection to upsd From f066d98b6895b4e217baad66a592527e3d68f3bf Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 07:26:10 +0200 Subject: [PATCH 61/70] scripts/perl/UPS/Nut.pm: pass only defined args to IO::Socket::SSL->start_SSL() [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 1519b768ca..fa30d2f8a7 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -181,20 +181,28 @@ sub StartTLS { $ans = $self->_send("STARTTLS"); if (defined $ans && $ans =~ /^OK STARTTLS/) { $self->_debug("STARTTLS accepted, upgrading socket."); + my %argdef = (); my $strarg = "[" . scalar(%arg) . "]"; for (keys %arg) { $strarg .= " $_=>" . ($arg{$_}//"undef"); + if (defined $arg{$_}) { + $argdef{$_} = $arg{$_}; + } } $self->_debug("STARTTLS args: SSL_verify_mode=>" . ($arg{CERTVERIFY} ? "SSL_VERIFY_PEER" : "SSL_VERIFY_NONE") . " Other args: " . $strarg ); + if ($self->{debug}) { + $dumper->dumpValue(\%argdef); + } + # NOTE: Currently nothing fancy like client's own certificate databases... IO::Socket::SSL->start_SSL( $self->{srvsock}, SSL_verify_mode => $arg{CERTVERIFY} ? IO::Socket::SSL::SSL_VERIFY_PEER() : IO::Socket::SSL::SSL_VERIFY_NONE(), - %arg + %argdef ) or do { $self->_debug($self->{err} = "SSL upgrade failed: " . IO::Socket::SSL->errstr()); return undef; From adf020eb4e63ffc1d096e6c6eb09ff5cf9c5284e Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 07:55:47 +0200 Subject: [PATCH 62/70] scripts/perl/UPS/Nut.pm: allow SSL cert validation for IP addresses as host names [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index fa30d2f8a7..549f369c54 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -194,6 +194,11 @@ sub StartTLS { . " Other args: " . $strarg ); + if (!defined($argdef{SSL_hostname}) && defined($argdef{HOST})) { + # If specified, allows cert validation for host names *and* IP addresses + $argdef{SSL_hostname} = $argdef{HOST}; + } + if ($self->{debug}) { $dumper->dumpValue(\%argdef); } From 08fb95dcc4244c87030efd778186e7e64ea63843 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 08:43:40 +0200 Subject: [PATCH 63/70] tests/NIT/nit.sh: centralize definition of PERL_OPTS [#1711] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 91d7c67f65..7713ef9ef3 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2844,6 +2844,18 @@ isTestablePerl() { else log_error "[isTestablePerl] Detected perl shebang: '${PL_SHEBANG}' (result=${PL_RES})" fi + + PERL_OPTS_INC="-I${TOP_SRCDIR}/scripts/perl" + PERL_OPTS_DEBUG='' + if [ x"$NIT_DEBUG_PERL" = xtrue ] ; then + if [ -d "${HOME}/perl5/lib/perl5" ] ; then + PERL_OPTS_DEBUG="-I${HOME}/perl5/lib/perl5" + fi + $PERL $PERL_OPTS_DEBUG -e 'use Devel::DumpTrace;' && PERL_OPTS_DEBUG="$PERL_OPTS_DEBUG -d:DumpTrace" \ + || { $PERL $PERL_OPTS_DEBUG -e 'use Devel::Trace;' && PERL_OPTS_DEBUG="$PERL_OPTS_DEBUG -d:Trace" ; } \ + || { log_warn "Could not find Devel::DumpTrace nor Devel::Trace" ; unset PERL_OPTS_DEBUG ; } + fi + return $PL_RES } @@ -2855,7 +2867,7 @@ testcase_sandbox_perl_without_credentials() { if ( unset NUT_USER || true unset NUT_PASS || true setenv_ssl_perl - $PERL -I"${TOP_SRCDIR}/scripts/perl" "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + $PERL $PERL_OPTS_INC $PERL_OPTS_DEBUG "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" ) ; then log_info "[testcase_sandbox_perl_without_credentials] PASSED: UPS::Nut did not complain" PASSED="`expr $PASSED + 1`" @@ -2879,7 +2891,7 @@ testcase_sandbox_perl_with_credentials() { NUT_PASS="${TESTPASS_ADMIN}" export NUT_USER NUT_PASS setenv_ssl_perl - $PERL -I"${TOP_SRCDIR}/scripts/perl" "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + $PERL $PERL_OPTS_INC $PERL_OPTS_DEBUG "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" ) ; then log_info "[testcase_sandbox_perl_with_credentials] PASSED: UPS::Nut did not complain" PASSED="`expr $PASSED + 1`" @@ -2900,7 +2912,7 @@ testcase_sandbox_perl_with_upsmon_credentials() { NUT_PASS="${TESTPASS_UPSMON_PRIMARY}" export NUT_USER NUT_PASS setenv_ssl_perl - $PERL -I"${TOP_SRCDIR}/scripts/perl" "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" + $PERL $PERL_OPTS_INC $PERL_OPTS_DEBUG "${TOP_SRCDIR}/scripts/perl/test_nutclient.pl" ) ; then log_info "[testcase_sandbox_perl_with_upsmon_credentials] PASSED: UPS::Nut did not complain" PASSED="`expr $PASSED + 1`" From 2f67a77f296ed1828d7346658d4c7ad3799961ef Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 13:58:45 +0200 Subject: [PATCH 64/70] tests/NIT/nit.sh: neuter CERTVERIFY on darwin for now [#3404] Signed-off-by: Jim Klimov --- tests/NIT/nit.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 7713ef9ef3..4dfa8651c8 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2798,6 +2798,15 @@ setenv_ssl_perl() { unset NUT_SSL } fi + + # Numeric result of equality (1) inverted vs shell success code (0), so `ne`: + if $PERL -e 'exit ( $^O ne "darwin" );' ; then + # TODO: Fix https://github.com/networkupstools/nut/issues/3404 + log_warn "Disabling CERTVERIFY on darwin platform" + NUT_CERTVERIFY=0 + export NUT_CERTVERIFY + #unset NUT_CERTVERIFY + fi } PL_SHEBANG="" From 07380696f69fa33e4dff3699946f85bac6340b47 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 14:17:57 +0200 Subject: [PATCH 65/70] scripts/perl/*, tests/NIT/nit.sh: streamline passing NUT_DEBUG_SSL_PERL (or defaulting to no debug) [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 6 ++++-- scripts/perl/test_nutclient.pl | 4 ++++ tests/NIT/nit.sh | 6 ++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 549f369c54..670adf6fcc 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -168,7 +168,9 @@ sub StartTLS { my %arg = @_; my $ans; - if ($self->{debugssl}) { + if (defined $self->{debugssl} && $self->{debugssl} > 0) { + # FIXME: Pass as numeric level "debug": + $self->_debug("debugssl = $self->{debugssl}"); eval { use IO::Socket::SSL qw(debug6); }; } else { eval { require IO::Socket::SSL; }; @@ -344,7 +346,7 @@ sub _initialize { $self->{name} = $arg{NAME} || 'default'; # UPS name in etc/ups.conf on $host $self->{timeout} = $arg{TIMEOUT} || 30; # timeout $self->{debug} = $arg{DEBUG} || 0; # debugging? - $self->{debugssl} = $arg{DEBUGSSL} || $self->{debug}; # debugging IO::Socket::SSL upon use? + $self->{debugssl} = defined $arg{DEBUGSSL} ? $arg{DEBUGSSL} : $self->{debug}; # debugging IO::Socket::SSL upon use? $self->{debugout} = $arg{DEBUGOUT} || undef; # where to send debug messages my $srvsock = $self->{srvsock} = # establish connection to upsd diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index 8f555ef09b..c560d145e1 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -27,6 +27,9 @@ # but passed via %arg. Nut.pm uses STARTTLS method which takes %arg. my $NUT_DEBUG = (($ENV{'DEBUG'} || "false") eq "true" || defined($ENV{'NUT_DEBUG_LEVEL'})) ? 1 : 0; + # Numeric if set, values defined by SSL.pm module + # (actual value is for now ignored by the UPS::Nut module though): + my $NUT_DEBUG_SSL = $ENV{'NUT_DEBUG_SSL_PERL'} ; #(($ENV{'NUT_DEBUG_SSL_PERL'} || "") eq "" ? undef : $ENV{'NUT_DEBUG_SSL_PERL'}; # Account "unexpected" failures (more due to coding than circumstances) # e.g. lack of protected access when no credentials were passed is okay @@ -47,6 +50,7 @@ USERNAME => $NUT_USER, PASSWORD => $NUT_PASS, DEBUG => $NUT_DEBUG, + DEBUGSSL => $NUT_DEBUG_SSL, # TRACKING => 'ON', # undef by default, enabled in certain tests below # STARTTLS related (passed via %arg to StartTLS in Nut.pm) USESSL => $NUT_SSL, diff --git a/tests/NIT/nit.sh b/tests/NIT/nit.sh index 4dfa8651c8..1f84ce8fb7 100755 --- a/tests/NIT/nit.sh +++ b/tests/NIT/nit.sh @@ -2807,6 +2807,12 @@ setenv_ssl_perl() { export NUT_CERTVERIFY #unset NUT_CERTVERIFY fi + + if [ x"${NUT_DEBUG_SSL_PERL}" = x ] ; then + log_info "Neutering NUT_DEBUG_SSL_PERL to make less noise by default" + NUT_DEBUG_SSL_PERL=-1 + export NUT_DEBUG_SSL_PERL + fi } PL_SHEBANG="" From 36d26b58917e79a5411c1e08db459bbea4c07c75 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 14:27:05 +0200 Subject: [PATCH 66/70] scripts/perl/UPS/Nut.pm: align closer with can_ssl [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 670adf6fcc..407c0dd63c 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -375,7 +375,7 @@ sub _initialize { $use_ssl = $can_ssl; } - if ($use_ssl || $arg{FORCESSL}) { + if (($can_ssl && $use_ssl) || $arg{FORCESSL}) { # Always try to elevate, do not bother if this fails unless required by args my $startedTLS = $self->StartTLS(%arg); if (defined $startedTLS && $startedTLS) { From d7907c4662107cbc0f3bafa80c8751c18d30e9d2 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 14:33:09 +0200 Subject: [PATCH 67/70] clients/nutclient.cpp: work around lack of threads in some mingw versions [#3402] Signed-off-by: Jim Klimov --- clients/nutclient.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clients/nutclient.cpp b/clients/nutclient.cpp index a3f00f12e7..9bc64b158f 100644 --- a/clients/nutclient.cpp +++ b/clients/nutclient.cpp @@ -2094,7 +2094,13 @@ TrackingResult TcpClient::waitTrackingResult(const TrackingID& id, int waitInter if (waitIntervalSec > 0) { + // https://stackoverflow.com/questions/37358856/does-mingw-w64-support-stdthread-out-of-the-box-when-using-the-win32-threading + // Does not work on some, not all, mingw versions (headers lack threads): +#ifndef WIN32 std::this_thread::sleep_for(std::chrono::seconds(waitIntervalSec)); +#else + Sleep(waitIntervalSec * 1000L); +#endif } else { From c4d620fce1cd53e4e11909597aacd01ffa6c7ea0 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 13:40:40 +0000 Subject: [PATCH 68/70] scripts/perl/UPS/Nut.pm: fix to loading IO::Socket::SSL via eval with quotes not braces [#1711] It is then evaluated at run-time as code reaches that line (or not - conditionals), not at interpretation time where it fails too early. Also use "1;" in the end as a tidy practice. Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 407c0dd63c..771d4d0d96 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -171,9 +171,9 @@ sub StartTLS { if (defined $self->{debugssl} && $self->{debugssl} > 0) { # FIXME: Pass as numeric level "debug": $self->_debug("debugssl = $self->{debugssl}"); - eval { use IO::Socket::SSL qw(debug6); }; + eval "use IO::Socket::SSL qw(debug6); 1;"; } else { - eval { require IO::Socket::SSL; }; + eval "require IO::Socket::SSL; 1;"; } if ($@) { $self->_debug($self->{err} = "IO::Socket::SSL not available: FEATURE-NOT-SUPPORTED on client side"); @@ -364,7 +364,7 @@ sub _initialize { $self->{select} = IO::Select->new( $srvsock ); my $can_ssl = 1; - eval "require IO::Socket::SSL"; + eval "require IO::Socket::SSL; 1"; if ($@) { $can_ssl = 0; } From af84ef7c4577f25fa5d32b846458f6b443843837 Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 13:42:38 +0000 Subject: [PATCH 69/70] scripts/perl/UPS/Nut.pm: now we can pass SSL debug level as a number [#1711] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 4 ++-- scripts/perl/test_nutclient.pl | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index 771d4d0d96..d4a8837c06 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -169,9 +169,9 @@ sub StartTLS { my $ans; if (defined $self->{debugssl} && $self->{debugssl} > 0) { - # FIXME: Pass as numeric level "debug": + # Pass to SSL lib as numeric level "debug": $self->_debug("debugssl = $self->{debugssl}"); - eval "use IO::Socket::SSL qw(debug6); 1;"; + eval "use IO::Socket::SSL qw(debug$self->{debugssl}); 1;"; } else { eval "require IO::Socket::SSL; 1;"; } diff --git a/scripts/perl/test_nutclient.pl b/scripts/perl/test_nutclient.pl index c560d145e1..d3120e698e 100755 --- a/scripts/perl/test_nutclient.pl +++ b/scripts/perl/test_nutclient.pl @@ -3,6 +3,7 @@ # This source code is provided for testing/debugging purpose ;) # This script is a Perl equivalent of scripts/python/module/test_nutclient.py.in +# Copyright (C) 2026- Jim Klimov use strict; use warnings; @@ -23,12 +24,12 @@ my $NUT_CERTVERIFY = (($ENV{'NUT_CERTVERIFY'} || "false") eq "true" || ($ENV{'NUT_CERTVERIFY'} || "false") eq "1") ? 1 : 0; my $NUT_CAFILE = $ENV{'NUT_CAFILE'} || undef; my $NUT_CAPATH = $ENV{'NUT_CAPATH'} || undef; - # Note: Python's cert_file, key_file, key_pass are not directly supported by current Nut.pm STARTTLS as independent args, - # but passed via %arg. Nut.pm uses STARTTLS method which takes %arg. + # Note: Python's cert_file, key_file, key_pass are not directly + # supported by current Nut.pm STARTTLS as independent args, but + # passed via %arg. Nut.pm uses STARTTLS method which takes %arg. my $NUT_DEBUG = (($ENV{'DEBUG'} || "false") eq "true" || defined($ENV{'NUT_DEBUG_LEVEL'})) ? 1 : 0; - # Numeric if set, values defined by SSL.pm module - # (actual value is for now ignored by the UPS::Nut module though): + # Numeric if set, values defined by SSL.pm module: my $NUT_DEBUG_SSL = $ENV{'NUT_DEBUG_SSL_PERL'} ; #(($ENV{'NUT_DEBUG_SSL_PERL'} || "") eq "" ? undef : $ENV{'NUT_DEBUG_SSL_PERL'}; # Account "unexpected" failures (more due to coding than circumstances) From a654ba30cd8631c60da26a1c98cca4cad07b172c Mon Sep 17 00:00:00 2001 From: Jim Klimov Date: Sun, 12 Apr 2026 16:15:18 +0200 Subject: [PATCH 70/70] scripts/perl/UPS/Nut.pm: warn if CERTVERIFY and custom CA options are enabled on Darwin platform [#3404] Signed-off-by: Jim Klimov --- scripts/perl/UPS/Nut.pm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/perl/UPS/Nut.pm b/scripts/perl/UPS/Nut.pm index d4a8837c06..c5f236e100 100644 --- a/scripts/perl/UPS/Nut.pm +++ b/scripts/perl/UPS/Nut.pm @@ -205,6 +205,11 @@ sub StartTLS { $dumper->dumpValue(\%argdef); } + if ($arg{CERTVERIFY} && ($argdef{SSL_ca_file} || $argdef{SSL_ca_path}) && ($^O eq "darwin" )) { + # https://github.com/networkupstools/nut/issues/3404 + print STDERR "WARNING: Custom CA certificate verification may fail on $^O platform, in that case unset CERTVERIFY in your client configuration"; + } + # NOTE: Currently nothing fancy like client's own certificate databases... IO::Socket::SSL->start_SSL( $self->{srvsock},