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 daf48f5..8ae9bbd 100644
--- a/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb
+++ b/WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb
@@ -1,8 +1,15 @@
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
+ Private ReadOnly NUT_CHARENCODING As Text.Encoding = Text.Encoding.ASCII
+
#Region "Properties"
Public ReadOnly Property ConnectionStatus As Boolean
Get
@@ -53,25 +60,27 @@ Public Class Nut_Socket
Throw New InvalidOperationException("Host and Port must be specified to connect.")
End If
+ LogFile.LogTracing(String.Format("Attempting TCP socket connection to {0}:{1}...", Host, Port), LogLvl.LOG_NOTICE, Me)
+
Try
- LogFile.LogTracing(String.Format("Attempting TCP socket connection to {0}:{1}...", Host, Port), LogLvl.LOG_NOTICE, Me)
+ client = New TcpClient(Host, Port) With
+ {
+ .SendTimeout = TIMEOUT_MS,
+ .ReceiveTimeout = TIMEOUT_MS
+ }
- client = New TcpClient(Host, Port)
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)
-
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)
@@ -79,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()
@@ -145,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.")
@@ -165,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