From 06d3955ccb0e10c288f19b3a54ab97ef67ee3aaa Mon Sep 17 00:00:00 2001 From: gbakeman Date: Fri, 28 Nov 2025 15:47:53 -0500 Subject: [PATCH 1/4] Specify timeout for Socket comms. Declare a constant value of 5 seconds for communications with the NUT server (temporary patch until we have true async communication) --- WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb index daf48f5..9aa81f9 100644 --- a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb +++ b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb @@ -3,6 +3,7 @@ Imports System.Net.Sockets Public Class Nut_Socket + Private Const TIMEOUT_MS = 5000 #Region "Properties" Public ReadOnly Property ConnectionStatus As Boolean Get @@ -56,7 +57,12 @@ Public Class Nut_Socket Try LogFile.LogTracing(String.Format("Attempting TCP socket connection to {0}:{1}...", Host, Port), LogLvl.LOG_NOTICE, Me) - client = New TcpClient(Host, Port) + client = New TcpClient(Host, Port) With + { + .SendTimeout = TIMEOUT_MS, + .ReceiveTimeout = TIMEOUT_MS + } + NutStream = client.GetStream() ReaderStream = New StreamReader(NutStream) WriterStream = New StreamWriter(NutStream) From 9df93e283f129365b5905ac3ef4dac2b4ae61e45 Mon Sep 17 00:00:00 2001 From: gbakeman Date: Fri, 28 Nov 2025 16:09:35 -0500 Subject: [PATCH 2/4] Specify NUT protocol encoding in connection --- WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb index 9aa81f9..e3c87df 100644 --- a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb +++ b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb @@ -4,6 +4,8 @@ Imports System.Net.Sockets Public Class Nut_Socket Private Const TIMEOUT_MS = 5000 + Private ReadOnly NUT_CHARENCODING As Text.Encoding = Text.Encoding.ASCII + #Region "Properties" Public ReadOnly Property ConnectionStatus As Boolean Get @@ -64,8 +66,8 @@ Public Class Nut_Socket } NutStream = client.GetStream() - ReaderStream = New StreamReader(NutStream) - WriterStream = New StreamWriter(NutStream) + ReaderStream = New StreamReader(NutStream, NUT_CHARENCODING) + WriterStream = New StreamWriter(NutStream, NUT_CHARENCODING) LogFile.LogTracing("Connection established and streams ready.", LogLvl.LOG_NOTICE, Me) From bce0638056b2d93356b057eff52b5c0d7f31348f Mon Sep 17 00:00:00 2001 From: gbakeman Date: Sun, 30 Nov 2025 11:33:31 -0500 Subject: [PATCH 3/4] Nut_Socket header, VER query change - Added a general description to the header of the Nut_Socket class - Removed redundant check during query for server version (NutException or others thrown in adverse conditions) - Entire response of VER query is accepted, rather than trying to locate the semver string specifically --- WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb index e3c87df..da0cfe5 100644 --- a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb +++ b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb @@ -1,6 +1,10 @@ Imports System.IO Imports System.Net.Sockets +''' +''' Manages low-level interaction with an endpoint communicating in the NUT protocol (upsd). +''' Passes up most encountered exceptions, while resetting its state if necessary. +''' Public Class Nut_Socket Private Const TIMEOUT_MS = 5000 @@ -74,12 +78,10 @@ Public Class Nut_Socket LogFile.LogTracing("Gathering basic info about the NUT server...", LogLvl.LOG_DEBUG, Me) Try + 'Response: Network UPS Tools upsd 2.8.1 - https://www.networkupstools.org/ Dim Nut_Query = Query_Data("VER") - - If Nut_Query.ResponseType = NUTResponse.OK Then - _NUTVersion = (Nut_Query.RawResponse.Split(" "c))(4) - LogFile.LogTracing("Server version: " & NUTVersion, LogLvl.LOG_NOTICE, Me) - End If + _NUTVersion = Nut_Query.RawResponse + LogFile.LogTracing("Server version: " & NUTVersion, LogLvl.LOG_NOTICE, Me) Catch nutEx As NutException LogFile.LogTracing("Error retrieving server version.", LogLvl.LOG_WARNING, Me) LogFile.LogException(nutEx, Me) From 8293de92786dbf9fa8c1d16f67175d9de931bce8 Mon Sep 17 00:00:00 2001 From: gbakeman Date: Thu, 4 Dec 2025 14:34:56 -0500 Subject: [PATCH 4/4] Consistent NUT error handling, other changes Common_Enums: - Removed "EMPTY" NUTResponse; `EndOfStreamException` thrown instead. Common_Classes: - Adding String array to Transaction class to store split responses so split operation doesn't need to be repeated for later processing. - Remove extra NutError constructor Nut_Socket: - Remove redundant check in NETVER query since errors are thrown - Slightly adjust order of operations in Connect subroutine - Adding `OnSocketBroken` subroutine to handle errors during Query_Data - Catch exceptions when reading and writing to the socket so the object can reset its state and disconnect - Remove unnecessary operations on response string - Opportunistically throw exceptions when necessary during response parsing UPS_Device: - Start Update_Data timer later on in connection sequence, and retrieve a copy of the data right away. - Enhanced parameter checking in GetUPSVar - Simplified response verification - Combined exception handling and record the last exception - For Loop is only continued under specific circumstances now - Better error logging towards end of GetUPSVar --- .../WinNUT-Client_Common/Common_Classes.vb | 21 ++- .../WinNUT-Client_Common/Common_Enums.vb | 1 - WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb | 86 +++++++---- WinNUT_V2/WinNUT-Client_Common/UPS_Device.vb | 135 ++++++++++++------ 4 files changed, 155 insertions(+), 88 deletions(-) diff --git a/WinNUT_V2/WinNUT-Client_Common/Common_Classes.vb b/WinNUT_V2/WinNUT-Client_Common/Common_Classes.vb index 7b0943f..6b0d606 100644 --- a/WinNUT_V2/WinNUT-Client_Common/Common_Classes.vb +++ b/WinNUT_V2/WinNUT-Client_Common/Common_Classes.vb @@ -59,10 +59,16 @@ Public Class Transaction ''' Public ReadOnly Property RawResponse As String - Public Sub New(query As String, rawResponse As String, responseType As NUTResponse) + ''' + ''' A that has been split around the delimeter character (space) + ''' + Public ReadOnly Property SplitResponse As String() + + Public Sub New(query As String, response As String, responseType As NUTResponse, Optional splitResponse As String() = Nothing) Me.Query = query - Me.RawResponse = rawResponse + RawResponse = response Me.ResponseType = responseType + Me.SplitResponse = splitResponse End Sub End Class @@ -72,17 +78,8 @@ Public Class NutException Public ReadOnly Property LastTransaction As Transaction ''' - ''' Raise a NutException that resulted from either an error as part of the NUT protocol, or a general error during - ''' the query. + ''' Raise an exception that resulted from a defined error in the NUT protocol. ''' - ''' - ''' - Public Sub New(query As String, protocolError As NUTResponse, queryResponse As String, - Optional innerException As Exception = Nothing) - MyBase.New(Nothing, innerException) - LastTransaction = New Transaction(query, queryResponse, protocolError) - End Sub - Public Sub New(transaction As Transaction) MyBase.New(String.Format("{0} ({1})" & vbNewLine & "Query: {2}", transaction.ResponseType, transaction.RawResponse, transaction.Query)) diff --git a/WinNUT_V2/WinNUT-Client_Common/Common_Enums.vb b/WinNUT_V2/WinNUT-Client_Common/Common_Enums.vb index abec61b..2a11270 100644 --- a/WinNUT_V2/WinNUT-Client_Common/Common_Enums.vb +++ b/WinNUT_V2/WinNUT-Client_Common/Common_Enums.vb @@ -61,7 +61,6 @@ End Enum ' Define possible responses according to NUT protcol v1.2 Public Enum NUTResponse - EMPTY UNRECOGNIZED OK VAR diff --git a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb index da0cfe5..8ae9bbd 100644 --- a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb +++ b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb @@ -60,9 +60,9 @@ Public Class Nut_Socket Throw New InvalidOperationException("Host and Port must be specified to connect.") End If - Try - LogFile.LogTracing(String.Format("Attempting TCP socket connection to {0}:{1}...", Host, Port), LogLvl.LOG_NOTICE, Me) + LogFile.LogTracing(String.Format("Attempting TCP socket connection to {0}:{1}...", Host, Port), LogLvl.LOG_NOTICE, Me) + Try client = New TcpClient(Host, Port) With { .SendTimeout = TIMEOUT_MS, @@ -74,7 +74,6 @@ Public Class Nut_Socket WriterStream = New StreamWriter(NutStream, NUT_CHARENCODING) LogFile.LogTracing("Connection established and streams ready.", LogLvl.LOG_NOTICE, Me) - LogFile.LogTracing("Gathering basic info about the NUT server...", LogLvl.LOG_DEBUG, Me) Try @@ -89,21 +88,20 @@ Public Class Nut_Socket Try Dim Nut_Query = Query_Data("NETVER") - - If Nut_Query.ResponseType = NUTResponse.OK Then - _NetVersion = Nut_Query.RawResponse - LogFile.LogTracing("Protocol version: " & NetVersion, LogLvl.LOG_NOTICE, Me) - End If + _NetVersion = Nut_Query.RawResponse + LogFile.LogTracing("Protocol version: " & NetVersion, LogLvl.LOG_NOTICE, Me) Catch nutEx As NutException LogFile.LogTracing("Error retrieving protocol version.", LogLvl.LOG_WARNING, Me) LogFile.LogException(nutEx, Me) End Try - - LogFile.LogTracing("Completed gathering basic info about NUT server.", LogLvl.LOG_DEBUG, Me) - Catch Excep As Exception + Catch ex As Exception + LogFile.LogTracing("Error connecting socket.", LogLvl.LOG_DEBUG, Me) + LogFile.LogException(ex, Me) Disconnect(True) - Throw ' Pass exception on up to UPS + Throw End Try + + LogFile.LogTracing("Completed gathering basic info about NUT server.", LogLvl.LOG_DEBUG, Me) End Sub Public Sub Login() @@ -155,13 +153,34 @@ Public Class Nut_Socket End Sub ''' - ''' Attempt to send a query to the NUT server, and do some basic parsing. + ''' React to a hard error while using the underlying Socket, and make sure this object is left in a consistent state. + ''' + ''' Any exception can throw. + ''' + Private Sub OnSocketBroken(ex As Exception) + LogFile.LogTracing("Socket breaking.", LogLvl.LOG_DEBUG, Me) + Disconnect(True) + RaiseEvent Socket_Broken() + If ex IsNot Nothing Then + LogFile.LogException(ex, Me) + Throw ex + End If + End Sub + + + ''' + ''' Synchronously send a query to the NUT server and collect the response. This method will throw all exceptions, + ''' including NUT protocol (ERR) responses. ''' ''' The query to be sent to the server, within specifications of the NUT protocol. ''' The full of this function call. ''' Thrown when calling this function while disconnected, or another ''' call is in progress. ''' Thrown when the NUT server returns an error or unexpected response. + ''' + ''' Attempted to read or write to a stream in an invalid state. + ''' An empty response was encountered, meaning the end of the stream. + ''' This likely indicates that the server closed the connection. Function Query_Data(Query_Msg As String) As Transaction If Not ConnectionStatus Then Throw New InvalidOperationException("Attempted to send query " & Query_Msg & " while disconnected.") @@ -175,47 +194,52 @@ Public Class Nut_Socket streamInUse = True WriterStream.WriteLine(Query_Msg) WriterStream.Flush() - Catch - Throw + Catch ex As Exception + LogFile.LogTracing("Error writing to Stream.", LogLvl.LOG_ERROR, Me) + OnSocketBroken(ex) Finally streamInUse = False End Try - Dim responseEnum = NUTResponse.EMPTY - Dim response = ReaderStream.ReadLine() + Dim response As String = Nothing + Dim responseEnum As NUTResponse + Dim splitResponse As String() = Nothing + + Try + response = ReaderStream.ReadLine() + Catch ex As Exception + LogFile.LogTracing("Error reading from Stream.", LogLvl.LOG_ERROR, Me) + OnSocketBroken(ex) + End Try If String.IsNullOrEmpty(response) Then ' End of stream reached, likely server terminated connection. - Disconnect(True) - RaiseEvent Socket_Broken() + OnSocketBroken(New EndOfStreamException("Server terminated connection.")) Else - Dim parseResponse = response.Trim().ToUpper().Split(" "c) ' TODO: Is Trim unnecessary? + splitResponse = response.Split({" "c}, 4) - Select Case parseResponse(0) + Select Case splitResponse(0) Case "OK", "VAR", "DESC", "UPS" responseEnum = NUTResponse.OK Case "BEGIN" responseEnum = NUTResponse.BEGINLIST Case "END" responseEnum = NUTResponse.ENDLIST - Case "NETWORK", "1.0", "1.1", "1.2", "1.3" + Case "Network", "1.0", "1.1", "1.2", "1.3" 'In case of "VER" or "NETVER" Query responseEnum = NUTResponse.OK Case "ERR" responseEnum = DirectCast([Enum].Parse(GetType(NUTResponse), - parseResponse(1).Replace("-", String.Empty)), NUTResponse) + splitResponse(1).Replace("-", String.Empty)), NUTResponse) + LogFile.LogTracing($"Parsed error response: { responseEnum }", LogLvl.LOG_DEBUG, Me) + Throw New NutException(New Transaction(Query_Msg, response, responseEnum, splitResponse)) Case Else - responseEnum = NUTResponse.UNRECOGNIZED + LogFile.LogTracing($"Unrecognized response while parsing: { response }", LogLvl.LOG_ERROR, Me) + Throw New NutException(New Transaction(Query_Msg, response, NUTResponse.UNRECOGNIZED, splitResponse)) End Select End If - Dim transaction = New Transaction(Query_Msg, response, responseEnum) - - If responseEnum = NUTResponse.OK OrElse responseEnum = NUTResponse.BEGINLIST OrElse responseEnum = NUTResponse.ENDLIST Then - Return transaction - End If - - Throw New NutException(transaction) + Return New Transaction(Query_Msg, response, responseEnum, splitResponse) End Function Public Function Query_List_Datas(Query_Msg As String) As List(Of UPS_List_Datas) diff --git a/WinNUT_V2/WinNUT-Client_Common/UPS_Device.vb b/WinNUT_V2/WinNUT-Client_Common/UPS_Device.vb index a133eae..506f019 100644 --- a/WinNUT_V2/WinNUT-Client_Common/UPS_Device.vb +++ b/WinNUT_V2/WinNUT-Client_Common/UPS_Device.vb @@ -1,6 +1,10 @@ Imports System.Globalization Imports System.Windows.Forms +''' +''' Represents a UPS device on a NUT protocol server (upsd). Is the highest-level object for operations in the +''' NUT protocol. Will not raise exceptions, only events. +''' Public Class UPS_Device #Region "Statics/Defaults" Private ReadOnly INVARIANT_CULTURE = CultureInfo.InvariantCulture @@ -141,13 +145,17 @@ Public Class UPS_Device Nut_Socket.Connect() ' If Nut_Socket.ExistsOnServer(Nut_Config.UPSName) Then UPS_Datas = GetUPSProductInfo() - Update_Data.Start() + RaiseEvent Connected(Me) If Not String.IsNullOrEmpty(Nut_Config.Login) Then Login() End If + ' Have UPS data available right away. + Retrieve_UPS_Datas(Me, Nothing) + Update_Data.Start() + Catch ex As NutException ' This is how we determine if we have a valid UPS name entered, among other errors. RaiseEvent EncounteredNUTException(Me, ex) @@ -162,6 +170,11 @@ Public Class UPS_Device End Try End Sub + ''' + ''' Indicates to the NUT server that this client is dependant upon this UPS for power, and registers for a FSD event. + ''' Not usually necessary for normal opeartion (reading UPS variables.) + ''' + ''' Any exception raised from Public Sub Login() If Not IsConnected OrElse IsLoggedIn Then Throw New InvalidOperationException("UPS is in an invalid state to login.") @@ -433,71 +446,105 @@ Public Class UPS_Device End Sub Private Const MAX_VAR_RETRIES = 3 + ''' + ''' Attempts to retrieve the value of a UPS variable using the `GET VAR` NUT API. + ''' + ''' One or more UPS variable name strings to query the server with. + ''' Gaurantee a returned value in the event of error. All encountered exceptions during + ''' variable query are suppressed. + ''' Special parameter used during DATASTALE error handling. + ''' Attempted to query UPS variable while not . + ''' Attempted query with no varNames provided. + ''' Any exception raised by , unless is given. + ''' Example: VAR dummy ups.status "OB HB" Public Function GetUPSVar(varNames As String(), Optional Fallback_value As Object = Nothing, Optional recursing As Boolean = False) As String + If varNames Is Nothing OrElse varNames.Length = 0 Then + Throw New InvalidOperationException("Attempted GetUPSVar is no names provided.") + End If + + LogFile.LogTracing($"Attempting to get UPS variable with { varNames.Length - 1 } alternatives.", LogLvl.LOG_DEBUG, Me) + If Not IsConnected Then Throw New InvalidOperationException("Tried to GetUPSVar while disconnected.") End If + Dim Nut_Query As Transaction = Nothing + Dim lastException As Exception = Nothing + ' Try each variable in the array sequentially For Each varName As String In varNames Try LogFile.LogTracing("Trying variable: " & varName, LogLvl.LOG_DEBUG, Me) + Nut_Query = Nut_Socket.Query_Data($"GET VAR { Name } { varName }") - Dim Nut_Query As Transaction - Nut_Query = Nut_Socket.Query_Data("GET VAR " & Name & " " & varName) - - If Nut_Query.ResponseType = NUTResponse.OK Then - LogFile.LogTracing("Success with " & varName, LogLvl.LOG_DEBUG, Me) - Return ExtractData(Nut_Query.RawResponse) + If Nut_Query.SplitResponse.Length = 4 Then + ' Extract the variable value from the response. + Dim response = Nut_Query.SplitResponse(3).Trim({""""c}) + LogFile.LogTracing("Returning good response: " & response, LogLvl.LOG_DEBUG, Me) + Return response Else - Throw New NutException(Nut_Query) + Throw New Exception("Received unexpected response, but no exception was thrown.") End If - Catch ex As NutException - Select Case ex.LastTransaction.ResponseType - Case NUTResponse.VARNOTSUPPORTED - LogFile.LogTracing(varName & " is not supported by server, trying next", LogLvl.LOG_WARNING, Me) - ' Continue to next variable - Continue For - - Case NUTResponse.DATASTALE - LogFile.LogTracing("DATA-STALE Error Result On Retrieving " & varName & " : " & ex.LastTransaction.RawResponse, LogLvl.LOG_ERROR, Me) - If recursing Then - ' Continue to next variable instead of returning Nothing + Catch ex As Exception + lastException = ex + + Dim nutEx = TryCast(ex, NutException) + If nutEx IsNot Nothing Then + Select Case nutEx.LastTransaction.ResponseType + Case NUTResponse.VARNOTSUPPORTED + LogFile.LogTracing(varName & " is not supported by server, trying next", LogLvl.LOG_WARNING, Me) + ' Continue to next variable Continue For - Else - Dim retryNum = 1 - Dim returnString As String = Nothing - While returnString Is Nothing AndAlso retryNum <= MAX_VAR_RETRIES - LogFile.LogTracing("Attempting retry " & retryNum & " to get variable " & varName, LogLvl.LOG_NOTICE, Me) - returnString = GetUPSVar({varName}, Fallback_value, True) - retryNum += 1 - End While - If returnString IsNot Nothing Then - Return returnString - Else - ' Retry failed, continue to next variable + + Case NUTResponse.DATASTALE + LogFile.LogTracing("DATA-STALE Error Result On Retrieving " & varName & " : " & nutEx.LastTransaction.RawResponse, LogLvl.LOG_ERROR, Me) + If recursing Then + ' Continue to next variable instead of returning Nothing Continue For + Else + Dim retryNum = 1 + Dim returnString As String = Nothing + While returnString Is Nothing AndAlso retryNum <= MAX_VAR_RETRIES + LogFile.LogTracing("Attempting retry " & retryNum & " to get variable " & varName, LogLvl.LOG_NOTICE, Me) + returnString = GetUPSVar({varName}, Fallback_value, True) + retryNum += 1 + End While + If returnString IsNot Nothing Then + Return returnString + Else + ' Retry failed, continue to next variable + Continue For + End If End If - End If - Case Else - Throw - End Select - - Catch ex As Exception - LogFile.LogTracing("Exception for variable " & varName & ": " & ex.Message & ", trying next", LogLvl.LOG_WARNING, Me) - ' Continue to next variable - Continue For + Case Else + LogFile.LogTracing("Unexpected NUT error response when retrieving variable: " & ex.Message, LogLvl.LOG_ERROR, Me) + Exit For + End Select + Else + LogFile.LogTracing("Socket or other unexpected error encountered. Expect a SocketBroken event to follow.", LogLvl.LOG_ERROR, Me) + Exit For + End If End Try Next - ' If we reach here, all variables failed + LogFile.LogTracing("Unable to get any UPS variable.", LogLvl.LOG_ERROR, Me) + + If lastException Is Nothing Then + LogFile.LogTracing("!! No exceptions were recorded.", LogLvl.LOG_ERROR, Me) + lastException = New InvalidOperationException("No exceptions were recorded by the end of GetUPSVar.") + ElseIf TryCast(lastException, NutException) Is Nothing Then + ' Print exception info for anyting other than NUT errors. + LogFile.LogTracing("Last exception recorded:", LogLvl.LOG_ERROR, Me) + LogFile.LogException(lastException, Me) + End If + If Not String.IsNullOrEmpty(Fallback_value) Then - LogFile.LogTracing("All variables failed, applying fallback value", LogLvl.LOG_WARNING, Me) + LogFile.LogTracing("Returning fallback value.", LogLvl.LOG_NOTICE, Me) Return Fallback_value Else - LogFile.LogTracing("All variables failed and no fallback provided", LogLvl.LOG_ERROR, Me) - Throw New NutException("All variables failed and no fallback provided", NUTResponse.VARNOTSUPPORTED, Nothing) + LogFile.LogTracing("No fallback provided. Throwing last exception.", LogLvl.LOG_ERROR, Me) + Throw lastException End If End Function