-
Notifications
You must be signed in to change notification settings - Fork 318
StreamInterface: prevent socket/reader-thread leak on handshake failure #918
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,6 @@ | ||||||||||||||||||
| """Stream Interface base class | ||||||||||||||||||
| """ | ||||||||||||||||||
| import contextlib | ||||||||||||||||||
| import io | ||||||||||||||||||
| import logging | ||||||||||||||||||
| import threading | ||||||||||||||||||
|
|
@@ -61,9 +62,20 @@ def __init__( # pylint: disable=R0917 | |||||||||||||||||
|
|
||||||||||||||||||
| # Start the reader thread after superclass constructor completes init | ||||||||||||||||||
| if connectNow: | ||||||||||||||||||
| self.connect() | ||||||||||||||||||
| if not noProto: | ||||||||||||||||||
| self.waitForConfig() | ||||||||||||||||||
| try: | ||||||||||||||||||
| self.connect() | ||||||||||||||||||
| if not noProto: | ||||||||||||||||||
| self.waitForConfig() | ||||||||||||||||||
| except Exception: | ||||||||||||||||||
| # Handshake failed (timeout, config error, bad stream). The caller | ||||||||||||||||||
| # never receives a reference to this half-initialized instance, so | ||||||||||||||||||
| # they cannot call close() themselves. If we don't clean up here, | ||||||||||||||||||
| # the reader thread (already started by connect()) keeps running | ||||||||||||||||||
| # and the underlying stream/socket leaks — the leak compounds on | ||||||||||||||||||
| # every retry from the caller's reconnect loop. | ||||||||||||||||||
| with contextlib.suppress(Exception): | ||||||||||||||||||
| self.close() | ||||||||||||||||||
| raise | ||||||||||||||||||
|
|
||||||||||||||||||
| def connect(self) -> None: | ||||||||||||||||||
| """Connect to our radio | ||||||||||||||||||
|
|
@@ -136,7 +148,13 @@ def close(self) -> None: | |||||||||||||||||
| # reader thread to close things for us | ||||||||||||||||||
| self._wantExit = True | ||||||||||||||||||
| if self._rxThread != threading.current_thread(): | ||||||||||||||||||
| self._rxThread.join() # wait for it to exit | ||||||||||||||||||
| try: | ||||||||||||||||||
| self._rxThread.join() # wait for it to exit | ||||||||||||||||||
| except RuntimeError: | ||||||||||||||||||
|
Comment on lines
150
to
+153
|
||||||||||||||||||
| # Thread was never started — happens when close() is invoked | ||||||||||||||||||
| # from a failed __init__ before connect() could spawn it. | ||||||||||||||||||
| # Nothing to join; safe to ignore. | ||||||||||||||||||
| pass | ||||||||||||||||||
|
Comment on lines
+156
to
+157
|
||||||||||||||||||
| # Nothing to join; safe to ignore. | |
| pass | |
| # In that case the reader thread cannot run _disconnected(), | |
| # so close the stream here to avoid leaking the resource. | |
| if self.stream is not None: | |
| with contextlib.suppress(Exception): | |
| self.stream.close() | |
| self.stream = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New failure-path behavior (calling
close()from__init__whenconnect()/waitForConfig()raises) is important for leak prevention but currently isn’t covered by unit tests. Since there are existing tests forstream_interface.py, consider adding a test that forcesconnect()orwaitForConfig()to raise and asserts cleanup occurs (e.g.,close()is invoked and doesn’t raise when the thread wasn’t started).