Skip to content

Commit c09ddc9

Browse files
authored
Merge pull request #230 from nutdotnet/223-fix-broken-socket-hang
Protocol, error handling, other improvements
2 parents 30aec1f + 8293de9 commit c09ddc9

File tree

4 files changed

+172
-95
lines changed

4 files changed

+172
-95
lines changed

WinNUT_V2/WinNUT-Client_Common/Common_Classes.vb

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,16 @@ Public Class Transaction
5959
''' <returns></returns>
6060
Public ReadOnly Property RawResponse As String
6161

62-
Public Sub New(query As String, rawResponse As String, responseType As NUTResponse)
62+
''' <summary>
63+
''' A <see cref="RawResponse"/> that has been split around the delimeter character (space)
64+
''' </summary>
65+
Public ReadOnly Property SplitResponse As String()
66+
67+
Public Sub New(query As String, response As String, responseType As NUTResponse, Optional splitResponse As String() = Nothing)
6368
Me.Query = query
64-
Me.RawResponse = rawResponse
69+
RawResponse = response
6570
Me.ResponseType = responseType
71+
Me.SplitResponse = splitResponse
6672
End Sub
6773
End Class
6874

@@ -72,17 +78,8 @@ Public Class NutException
7278
Public ReadOnly Property LastTransaction As Transaction
7379

7480
''' <summary>
75-
''' Raise a NutException that resulted from either an error as part of the NUT protocol, or a general error during
76-
''' the query.
81+
''' Raise an exception that resulted from a defined error in the NUT protocol.
7782
''' </summary>
78-
''' <param name="protocolError"></param>
79-
''' <param name="queryResponse"></param>
80-
Public Sub New(query As String, protocolError As NUTResponse, queryResponse As String,
81-
Optional innerException As Exception = Nothing)
82-
MyBase.New(Nothing, innerException)
83-
LastTransaction = New Transaction(query, queryResponse, protocolError)
84-
End Sub
85-
8683
Public Sub New(transaction As Transaction)
8784
MyBase.New(String.Format("{0} ({1})" & vbNewLine & "Query: {2}", transaction.ResponseType,
8885
transaction.RawResponse, transaction.Query))

WinNUT_V2/WinNUT-Client_Common/Common_Enums.vb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ End Enum
6161

6262
' Define possible responses according to NUT protcol v1.2
6363
Public Enum NUTResponse
64-
EMPTY
6564
UNRECOGNIZED
6665
OK
6766
VAR

WinNUT_V2/WinNUT-Client_Common/Nut_Socket.vb

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
Imports System.IO
22
Imports System.Net.Sockets
33

4+
''' <summary>
5+
''' Manages low-level interaction with an endpoint communicating in the NUT protocol (upsd).
6+
''' Passes up most encountered exceptions, while resetting its state if necessary.
7+
''' </summary>
48
Public Class Nut_Socket
59

10+
Private Const TIMEOUT_MS = 5000
11+
Private ReadOnly NUT_CHARENCODING As Text.Encoding = Text.Encoding.ASCII
12+
613
#Region "Properties"
714
Public ReadOnly Property ConnectionStatus As Boolean
815
Get
@@ -53,47 +60,48 @@ Public Class Nut_Socket
5360
Throw New InvalidOperationException("Host and Port must be specified to connect.")
5461
End If
5562

63+
LogFile.LogTracing(String.Format("Attempting TCP socket connection to {0}:{1}...", Host, Port), LogLvl.LOG_NOTICE, Me)
64+
5665
Try
57-
LogFile.LogTracing(String.Format("Attempting TCP socket connection to {0}:{1}...", Host, Port), LogLvl.LOG_NOTICE, Me)
66+
client = New TcpClient(Host, Port) With
67+
{
68+
.SendTimeout = TIMEOUT_MS,
69+
.ReceiveTimeout = TIMEOUT_MS
70+
}
5871

59-
client = New TcpClient(Host, Port)
6072
NutStream = client.GetStream()
61-
ReaderStream = New StreamReader(NutStream)
62-
WriterStream = New StreamWriter(NutStream)
73+
ReaderStream = New StreamReader(NutStream, NUT_CHARENCODING)
74+
WriterStream = New StreamWriter(NutStream, NUT_CHARENCODING)
6375

6476
LogFile.LogTracing("Connection established and streams ready.", LogLvl.LOG_NOTICE, Me)
65-
6677
LogFile.LogTracing("Gathering basic info about the NUT server...", LogLvl.LOG_DEBUG, Me)
6778

6879
Try
80+
'Response: Network UPS Tools upsd 2.8.1 - https://www.networkupstools.org/
6981
Dim Nut_Query = Query_Data("VER")
70-
71-
If Nut_Query.ResponseType = NUTResponse.OK Then
72-
_NUTVersion = (Nut_Query.RawResponse.Split(" "c))(4)
73-
LogFile.LogTracing("Server version: " & NUTVersion, LogLvl.LOG_NOTICE, Me)
74-
End If
82+
_NUTVersion = Nut_Query.RawResponse
83+
LogFile.LogTracing("Server version: " & NUTVersion, LogLvl.LOG_NOTICE, Me)
7584
Catch nutEx As NutException
7685
LogFile.LogTracing("Error retrieving server version.", LogLvl.LOG_WARNING, Me)
7786
LogFile.LogException(nutEx, Me)
7887
End Try
7988

8089
Try
8190
Dim Nut_Query = Query_Data("NETVER")
82-
83-
If Nut_Query.ResponseType = NUTResponse.OK Then
84-
_NetVersion = Nut_Query.RawResponse
85-
LogFile.LogTracing("Protocol version: " & NetVersion, LogLvl.LOG_NOTICE, Me)
86-
End If
91+
_NetVersion = Nut_Query.RawResponse
92+
LogFile.LogTracing("Protocol version: " & NetVersion, LogLvl.LOG_NOTICE, Me)
8793
Catch nutEx As NutException
8894
LogFile.LogTracing("Error retrieving protocol version.", LogLvl.LOG_WARNING, Me)
8995
LogFile.LogException(nutEx, Me)
9096
End Try
91-
92-
LogFile.LogTracing("Completed gathering basic info about NUT server.", LogLvl.LOG_DEBUG, Me)
93-
Catch Excep As Exception
97+
Catch ex As Exception
98+
LogFile.LogTracing("Error connecting socket.", LogLvl.LOG_DEBUG, Me)
99+
LogFile.LogException(ex, Me)
94100
Disconnect(True)
95-
Throw ' Pass exception on up to UPS
101+
Throw
96102
End Try
103+
104+
LogFile.LogTracing("Completed gathering basic info about NUT server.", LogLvl.LOG_DEBUG, Me)
97105
End Sub
98106

99107
Public Sub Login()
@@ -145,13 +153,34 @@ Public Class Nut_Socket
145153
End Sub
146154

147155
''' <summary>
148-
''' Attempt to send a query to the NUT server, and do some basic parsing.
156+
''' React to a hard error while using the underlying Socket, and make sure this object is left in a consistent state.
157+
''' </summary>
158+
''' <exception cref="Exception">Any exception <see cref="Query_Data(String)"/> can throw.</exception>
159+
''' <param name="ex"></param>
160+
Private Sub OnSocketBroken(ex As Exception)
161+
LogFile.LogTracing("Socket breaking.", LogLvl.LOG_DEBUG, Me)
162+
Disconnect(True)
163+
RaiseEvent Socket_Broken()
164+
If ex IsNot Nothing Then
165+
LogFile.LogException(ex, Me)
166+
Throw ex
167+
End If
168+
End Sub
169+
170+
171+
''' <summary>
172+
''' Synchronously send a query to the NUT server and collect the response. This method will throw all exceptions,
173+
''' including NUT protocol (ERR) responses.
149174
''' </summary>
150175
''' <param name="Query_Msg">The query to be sent to the server, within specifications of the NUT protocol.</param>
151176
''' <returns>The full <see cref="Transaction"/> of this function call.</returns>
152177
''' <exception cref="InvalidOperationException">Thrown when calling this function while disconnected, or another
153178
''' call is in progress.</exception>
154179
''' <exception cref="NutException">Thrown when the NUT server returns an error or unexpected response.</exception>
180+
''' <exception cref="ObjectDisposedException"></exception>
181+
''' <exception cref="IOException">Attempted to read or write to a stream in an invalid state. </exception>
182+
''' <exception cref="EndOfStreamException">An empty response was encountered, meaning the end of the stream.
183+
''' This likely indicates that the server closed the connection.</exception>
155184
Function Query_Data(Query_Msg As String) As Transaction
156185
If Not ConnectionStatus Then
157186
Throw New InvalidOperationException("Attempted to send query " & Query_Msg & " while disconnected.")
@@ -165,47 +194,52 @@ Public Class Nut_Socket
165194
streamInUse = True
166195
WriterStream.WriteLine(Query_Msg)
167196
WriterStream.Flush()
168-
Catch
169-
Throw
197+
Catch ex As Exception
198+
LogFile.LogTracing("Error writing to Stream.", LogLvl.LOG_ERROR, Me)
199+
OnSocketBroken(ex)
170200
Finally
171201
streamInUse = False
172202
End Try
173203

174-
Dim responseEnum = NUTResponse.EMPTY
175-
Dim response = ReaderStream.ReadLine()
204+
Dim response As String = Nothing
205+
Dim responseEnum As NUTResponse
206+
Dim splitResponse As String() = Nothing
207+
208+
Try
209+
response = ReaderStream.ReadLine()
210+
Catch ex As Exception
211+
LogFile.LogTracing("Error reading from Stream.", LogLvl.LOG_ERROR, Me)
212+
OnSocketBroken(ex)
213+
End Try
176214

177215
If String.IsNullOrEmpty(response) Then
178216
' End of stream reached, likely server terminated connection.
179-
Disconnect(True)
180-
RaiseEvent Socket_Broken()
217+
OnSocketBroken(New EndOfStreamException("Server terminated connection."))
181218
Else
182-
Dim parseResponse = response.Trim().ToUpper().Split(" "c) ' TODO: Is Trim unnecessary?
219+
splitResponse = response.Split({" "c}, 4)
183220

184-
Select Case parseResponse(0)
221+
Select Case splitResponse(0)
185222
Case "OK", "VAR", "DESC", "UPS"
186223
responseEnum = NUTResponse.OK
187224
Case "BEGIN"
188225
responseEnum = NUTResponse.BEGINLIST
189226
Case "END"
190227
responseEnum = NUTResponse.ENDLIST
191-
Case "NETWORK", "1.0", "1.1", "1.2", "1.3"
228+
Case "Network", "1.0", "1.1", "1.2", "1.3"
192229
'In case of "VER" or "NETVER" Query
193230
responseEnum = NUTResponse.OK
194231
Case "ERR"
195232
responseEnum = DirectCast([Enum].Parse(GetType(NUTResponse),
196-
parseResponse(1).Replace("-", String.Empty)), NUTResponse)
233+
splitResponse(1).Replace("-", String.Empty)), NUTResponse)
234+
LogFile.LogTracing($"Parsed error response: { responseEnum }", LogLvl.LOG_DEBUG, Me)
235+
Throw New NutException(New Transaction(Query_Msg, response, responseEnum, splitResponse))
197236
Case Else
198-
responseEnum = NUTResponse.UNRECOGNIZED
237+
LogFile.LogTracing($"Unrecognized response while parsing: { response }", LogLvl.LOG_ERROR, Me)
238+
Throw New NutException(New Transaction(Query_Msg, response, NUTResponse.UNRECOGNIZED, splitResponse))
199239
End Select
200240
End If
201241

202-
Dim transaction = New Transaction(Query_Msg, response, responseEnum)
203-
204-
If responseEnum = NUTResponse.OK OrElse responseEnum = NUTResponse.BEGINLIST OrElse responseEnum = NUTResponse.ENDLIST Then
205-
Return transaction
206-
End If
207-
208-
Throw New NutException(transaction)
242+
Return New Transaction(Query_Msg, response, responseEnum, splitResponse)
209243
End Function
210244

211245
Public Function Query_List_Datas(Query_Msg As String) As List(Of UPS_List_Datas)

0 commit comments

Comments
 (0)