Skip to content

Commit eec30b7

Browse files
committed
Adding logic to handle the new HA 2026.8 http config moving.
1 parent e0d33e7 commit eec30b7

9 files changed

Lines changed: 462 additions & 350 deletions

File tree

homeway/homeway/WebStream/webstreamhttphelper.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .headerimpl import HeaderHelper
1313
from .headerimpl import BaseProtocol
1414

15-
from ..interfaces import IWebStream
15+
from ..interfaces import IWebStream, IWebStreamHelper
1616
from ..buffer import Buffer, BufferOrNone
1717
from ..httprequest import HttpRequest
1818
from ..streammsgbuilder import StreamMsgBuilder
@@ -54,7 +54,7 @@ def CreateBuilder(self, knownBodySizeBytes = 0):
5454
# The helper can close the stream by calling close directly on the WebStream object
5555
# or by returning true from `IncomingServerMessage`
5656
#
57-
class WebStreamHttpHelper:
57+
class WebStreamHttpHelper(IWebStreamHelper):
5858

5959
# Called by the main socket thread so this should be quick!
6060
def __init__(self, streamId:int, logger:logging.Logger, webStream:IWebStream, webStreamOpenMsg:WebStreamMsg.WebStreamMsg, openedTime:float) -> None:
@@ -116,11 +116,10 @@ def Close(self) -> None:
116116
if self.HttpStreamAccumulationReader is not None:
117117
self.HttpStreamAccumulationReader.CloseAsync()
118118

119-
# Ensure the upload body is cleaned up.
120-
self.UploadBody.Cleanup()
121-
122119

123120
# Called when a new message has arrived for this stream from the server.
121+
# This is called from the dedicated thread for this stream, so it can be blocked.
122+
#
124123
# This function should throw on critical errors, that will reset the connection.
125124
# Returning true will case the websocket to close on return.
126125
def IncomingServerMessage(self, webStreamMsg:WebStreamMsg.WebStreamMsg) -> bool:
@@ -152,6 +151,13 @@ def IncomingServerMessage(self, webStreamMsg:WebStreamMsg.WebStreamMsg) -> bool:
152151
return False
153152

154153

154+
# Called from the dedicated web stream thread, after it's done and just before it's exciting.
155+
# This allows anything that should only be accessed by the web stream thread to be cleaned up before the thread exits.
156+
def OnWebStreamThreadExit(self) -> None:
157+
# Since UploadBody is not thread safe and used by the web stream thread, clean it up here.
158+
self.UploadBody.Cleanup()
159+
160+
155161
# This function either needs to throw (which will restart the entire connection)
156162
# or return a WebStreamMsg, or close the web stream. Otherwise the server will be waiting for it
157163
# for until it hits a timeout.

homeway/homeway/WebStream/webstreamimpl.py

Lines changed: 105 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ..buffer import Buffer
88
from ..sentry import Sentry
99
from ..streammsgbuilder import StreamMsgBuilder
10-
from ..interfaces import ISession, IWebStream
10+
from ..interfaces import ISession, IWebStream, IWebStreamHelper
1111
from ..debugprofiler import DebugProfiler, DebugProfilerFeatures
1212

1313
from ..Proto import WebStreamMsg
@@ -36,9 +36,7 @@ def __init__(self, group:Any=None, target:Any=None, name:Any=None, args:Any=(),
3636
self.HasSentCloseMessage = False
3737
self.StateLock = threading.Lock()
3838
self.MsgQueue:queue.Queue[Optional[WebStreamMsg.WebStreamMsg]] = queue.Queue()
39-
self.HttpHelper:Optional[WebStreamHttpHelper] = None
40-
self.WsHelper:Optional[WebStreamWsHelper] = None
41-
self.IsHelperClosed = False
39+
self.WebStreamHelper:Optional[IWebStreamHelper] = None
4240
self.OpenedTime = time.time()
4341
self.ClosedDueToRequestConnectionError = False
4442

@@ -95,31 +93,21 @@ def OnIncomingServerMessage(self, webStreamMsg:WebStreamMsg.WebStreamMsg) -> Non
9593
# This is called from the main socket receive thread, so it should
9694
# execute as quickly as possible.
9795
def Close(self) -> None:
98-
# Check the state and set the flag. Only allow this code to run
99-
# once.
100-
localHttpHelper:Optional[WebStreamHttpHelper] = None
101-
localWsHelper:Optional[WebStreamWsHelper] = None
96+
streamHelper:Optional[IWebStreamHelper] = None
10297

98+
# Check the state and set the flag. Only allow this code to run once.
10399
with self.StateLock:
104100
# If we are already closed, there's nothing to do.
105101
if self.IsClosed is True:
106102
return
107-
# We will close now, so set the flag.
108103
self.IsClosed = True
109104

110-
# While under lock, exists, and if so, has it been closed.
111-
# Note it's possible that this helper is being crated on a different
112-
# thread and will be set just after we exit the lock. In that case
113-
# the creator logic will notice that the stream is closed and call close on it.
114-
# So if the http helper doesn't exist yet, we can't set the isClosed flag to false.
115-
if self.HttpHelper is not None or self.WsHelper is not None:
116-
if self.IsHelperClosed is False:
117-
self.IsHelperClosed = True
118-
localHttpHelper = self.HttpHelper
119-
localWsHelper = self.WsHelper
120-
# Important! Ensure these are set to None so we don't have a circular ref.
121-
self.HttpHelper = None
122-
self.WsHelper = None
105+
# While under lock, after the self.IsClosed flag has been set, check if there's a helper.
106+
# The helper will only be set if self.IsClosed is false, once it's set, it's this function's responsibility to close.
107+
if self.WebStreamHelper is not None:
108+
# Grab a local ref and clear the member var, which indicates we are going to close it.
109+
streamHelper = self.WebStreamHelper
110+
self.WebStreamHelper = None
123111

124112
# Remove ourselves from the session map
125113
self.Session.WebStreamClosed(self.Id)
@@ -137,10 +125,8 @@ def Close(self) -> None:
137125
# If we got a ref to the helper, we need to call close on it.
138126
# NOTE - It's very important that these don't block or they will block the entire main websocket connection.
139127
try:
140-
if localHttpHelper is not None:
141-
localHttpHelper.Close()
142-
if localWsHelper is not None:
143-
localWsHelper.Close()
128+
if streamHelper is not None:
129+
streamHelper.Close()
144130
except Exception as e:
145131
Sentry.OnException("Web stream "+str(self.Id)+" helper threw an exception during close", e)
146132

@@ -161,77 +147,84 @@ def run(self) -> None:
161147

162148

163149
def mainThread(self) -> None:
164-
# Loop until we are closed.
165-
# Check under lock to avoid race condition with Close()
166-
while True:
167-
with self.StateLock:
168-
if self.IsClosed:
169-
return
150+
# Once created, we need to hold a ref so we can use it for cleanup.
151+
streamHelper:Optional[IWebStreamHelper] = None
152+
try:
153+
# Loop until we are closed.
154+
# Check under lock to avoid race condition with Close()
155+
while True:
156+
with self.StateLock:
157+
if self.IsClosed:
158+
return
170159

171-
# Wait on incoming messages
172-
# Timeout after 60 seconds just to check that we aren't closed.
173-
# It's important to set this value to None, otherwise on loops it will hold it's old value
174-
# which can accidentally re-process old messages.
175-
webStreamMsg:Optional[WebStreamMsg.WebStreamMsg] = None
176-
try:
177-
webStreamMsg = self.MsgQueue.get(timeout=60)
178-
except Exception as _:
179-
# We get this exception on the timeout.
180-
pass
160+
# Wait on incoming messages
161+
# Timeout after 60 seconds just to check that we aren't closed.
162+
# It's important to set this value to None, otherwise on loops it will hold it's old value
163+
# which can accidentally re-process old messages.
164+
webStreamMsg:Optional[WebStreamMsg.WebStreamMsg] = None
165+
try:
166+
webStreamMsg = self.MsgQueue.get(timeout=60)
167+
except Exception as _:
168+
# We get this exception on the timeout.
169+
pass
170+
171+
# Check that we aren't closed (under lock for thread safety)
172+
with self.StateLock:
173+
if self.IsClosed:
174+
return
181175

182-
# Check that we aren't closed (under lock for thread safety)
183-
with self.StateLock:
184-
if self.IsClosed:
185-
return
176+
# Check that we got a message and this wasn't just a timeout
177+
if webStreamMsg is None:
178+
continue
186179

187-
# Check that we got a message and this wasn't just a timeout
188-
if webStreamMsg is None:
189-
continue
190-
191-
# Handle the message.
192-
if webStreamMsg.IsOpenMsg():
193-
self.initFromOpenMessage(webStreamMsg)
194-
195-
# Ensure we have an open message.
196-
if self.OpenWebStreamMsg is None:
197-
# Throw so we reset the connection.
198-
raise Exception("Web stream ["+str(self.Id)+"] got a non open message before it's open message.")
199-
200-
# Don't pass it to the helper if there's nothing more.
201-
if webStreamMsg.IsControlFlagsOnly():
202-
continue
203-
204-
# Allow the helper to process the message
205-
# We should only ever have one, but just for safety, check both.
206-
# We need to take a local reference, since they are cleared under lock on close.
207-
returnValue = True
208-
httpHelper = self.HttpHelper
209-
wsHelper = self.WsHelper
210-
if httpHelper is not None:
211-
returnValue = httpHelper.IncomingServerMessage(webStreamMsg)
212-
if wsHelper is not None:
213-
returnValue = wsHelper.IncomingServerMessage(webStreamMsg)
214-
215-
# If process server message returns true, we should close the stream.
216-
if returnValue is True:
217-
self.Close()
218-
return
180+
# Handle the message.
181+
if webStreamMsg.IsOpenMsg():
182+
# This will return the webstream helper if it was created.
183+
# The only way it wouldn't be created is if the stream was closed before it could be created.
184+
streamHelper = self.initFromOpenMessage(webStreamMsg)
219185

220-
# When the http helper sends messages, it can indicate that the close flag has been set.
221-
# In such a case, self.HasSentCloseMessage will be true. We don't want to rely on the client
222-
# returning the correct returnValue, so if we see that we will call close to make sure things
223-
# are going down. Since Close() is guarded against multiple entries, this is totally fine.
224-
# Check under lock for thread safety.
225-
with self.StateLock:
226-
shouldClose = self.HasSentCloseMessage is True and self.IsClosed is False
227-
if shouldClose:
228-
self.Logger.warning("Web stream "+str(self.Id)+" processed a message and has sent a close message, but didn't call close on the web stream. Closing now.")
229-
self.Close()
230-
return
186+
# Ensure we have an open message.
187+
if self.OpenWebStreamMsg is None:
188+
# Throw so we reset the connection.
189+
raise Exception("Web stream ["+str(self.Id)+"] got a non open message before it's open message.")
231190

191+
# Don't pass it to the helper if there's nothing more.
192+
if webStreamMsg.IsControlFlagsOnly():
193+
continue
232194

233-
def initFromOpenMessage(self, webStreamMsg:WebStreamMsg.WebStreamMsg) -> None:
234-
# Sanity check.
195+
# As long as we aren't closed and we have a helper, process the message.
196+
closeRequested = True
197+
if self.IsClosed is False and streamHelper is not None:
198+
closeRequested = streamHelper.IncomingServerMessage(webStreamMsg)
199+
200+
# If process server message returns true, we should close the stream.
201+
if closeRequested is True:
202+
self.Close()
203+
return
204+
205+
# When the http helper sends messages, it can indicate that the close flag has been set.
206+
# In such a case, self.HasSentCloseMessage will be true. We don't want to rely on the client
207+
# returning the correct returnValue, so if we see that we will call close to make sure things
208+
# are going down. Since Close() is guarded against multiple entries, this is totally fine.
209+
# Check under lock for thread safety.
210+
with self.StateLock:
211+
shouldClose = self.HasSentCloseMessage is True and self.IsClosed is False
212+
if shouldClose:
213+
self.Logger.warning("Web stream "+str(self.Id)+" processed a message and has sent a close message, but didn't call close on the web stream. Closing now.")
214+
self.Close()
215+
return
216+
finally:
217+
# Be sure to call this cleanup function before the thread exists.
218+
try:
219+
if streamHelper is not None:
220+
streamHelper.OnWebStreamThreadExit()
221+
except Exception as e:
222+
self.Logger.error("Exception in OnWebStreamThreadExit: "+str(e))
223+
224+
225+
# If the web stream helper was created AND SET, it must be returned here.
226+
def initFromOpenMessage(self, webStreamMsg:WebStreamMsg.WebStreamMsg) -> Optional[IWebStreamHelper]:
227+
# Ensure we haven't already received an open message.
235228
if self.OpenWebStreamMsg is not None:
236229
# Throw so we reset the connection.
237230
raise Exception("Web stream ["+str(self.Id)+"] already have an open message and we got another.")
@@ -248,36 +241,35 @@ def initFromOpenMessage(self, webStreamMsg:WebStreamMsg.WebStreamMsg) -> None:
248241
# Create the helper out of lock and then set it.
249242
# WE MUST ALWAYS SET THE HTTP HELPER OBJECT since down stream logic depends on it existing.
250243
# But, if the stream has closed since we created this object, we must call close on it.
251-
httpHelper = None
252-
wsHelper = None
244+
webStreamHelper:Optional[IWebStreamHelper] = None
253245
if webStreamMsg.IsWebsocketStream():
254-
wsHelper = WebStreamWsHelper(self.Id, self.Logger, self, self.OpenWebStreamMsg, self.OpenedTime)
246+
webStreamHelper = WebStreamWsHelper(self.Id, self.Logger, self, self.OpenWebStreamMsg, self.OpenedTime)
255247
else:
256-
httpHelper = WebStreamHttpHelper(self.Id, self.Logger, self, self.OpenWebStreamMsg, self.OpenedTime)
248+
webStreamHelper = WebStreamHttpHelper(self.Id, self.Logger, self, self.OpenWebStreamMsg, self.OpenedTime)
257249

258250
needsToCallCloseOnHelper = False
259251
with self.StateLock:
260-
# Set the helper, which ever we made.
261-
self.HttpHelper = httpHelper
262-
self.WsHelper = wsHelper
263-
264-
# If the stream is now closed...
265-
if self.IsClosed is True:
266-
# and the http helper didn't get closed called yet...
267-
if self.IsHelperClosed is False:
268-
# We need to call it now.
269-
self.IsHelperClosed = True
270-
needsToCallCloseOnHelper = True
271-
# Important! Ensure these are set to None so we don't have a circular ref.
272-
self.HttpHelper = None
273-
self.WsHelper = None
252+
# Ensure we haven't already done the init.
253+
if self.WebStreamHelper is not None:
254+
raise Exception("Web stream ["+str(self.Id)+"] already has a helper created but we got another init message. This means we got two init messages.")
255+
256+
if self.IsClosed is False:
257+
# If we are open, set the webstream helper now.
258+
# Once it's set, the Close function must call Close on it.
259+
self.WebStreamHelper = webStreamHelper
260+
else:
261+
# If the stream is now closed, DO NOT set the helper and close it now.
262+
needsToCallCloseOnHelper = True
274263

275264
# Outside of lock, if we need to close this helper, do it.
276265
if needsToCallCloseOnHelper is True:
277-
if httpHelper is not None:
278-
httpHelper.Close()
279-
if wsHelper is not None:
280-
wsHelper.Close()
266+
webStreamHelper.Close()
267+
webStreamHelper.OnWebStreamThreadExit()
268+
# If we closed it, we should NOT return it.
269+
return None
270+
else:
271+
# This is set and we opened it, so we must return it.
272+
return webStreamHelper
281273

282274

283275
# Called by the helpers to send messages to the server.

homeway/homeway/WebStream/webstreamwshelper.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from typing import Optional
77

88
from ..buffer import Buffer
9-
from ..interfaces import IWebSocketClient, IWebStream, WebSocketOpCode
9+
from ..interfaces import IWebSocketClient, IWebStream, WebSocketOpCode, IWebStreamHelper
1010
from .headerimpl import HeaderHelper
1111
from ..sentry import Sentry
1212
from ..httprequest import HttpRequest
@@ -28,7 +28,7 @@
2828
# The helper can close the stream by calling close directly on the WebStream object
2929
# or by returning true from `IncomingServerMessage`
3030
#
31-
class WebStreamWsHelper:
31+
class WebStreamWsHelper(IWebStreamHelper):
3232

3333
# If binary compression doesn't save at least this much, treat it as inefficient.
3434
c_BinaryCompressionMinSavingsRatio = 0.05
@@ -365,6 +365,13 @@ def IncomingServerMessage(self, webStreamMsg:WebStreamMsg.WebStreamMsg) -> bool:
365365
return False
366366

367367

368+
# Called from the dedicated web stream thread, after it's done and just before it's exciting.
369+
# This allows anything that should only be accessed by the web stream thread to be cleaned up before the thread exits.
370+
def OnWebStreamThreadExit(self) -> None:
371+
# There's nothing to do in this class for this call.
372+
pass
373+
374+
368375
def onWsData(self, ws:IWebSocketClient, buffer:Buffer, msgType:WebSocketOpCode) -> None:
369376
# Only handle callbacks for the current websocket.
370377
if self.Ws is not None and self.Ws != ws:

0 commit comments

Comments
 (0)