From 67feaecef2a571f7d7d41b7b9eeb7726ba85c59d Mon Sep 17 00:00:00 2001 From: Thor Whalen <1906276+thorwhalen@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:02:02 +0000 Subject: [PATCH] fix: open() after a legacy start() left the legacy run producing A reader started with start() runs in self and has no _worker, so open()'s _stop_previous_worker saw nothing to stop, then cleared the stop flag -- the legacy run never observed close() and kept feeding the queue next to the new worker. open() now also stops and waits for a run in self. Also make test_stop_does_not_shadow_thread_internals version-robust: Thread._stop no longer exists on Python 3.13, so assert that no Event shadows it instead of asserting the method is there. Co-Authored-By: Claude Opus 5 --- pchealthstream2py/pchealth.py | 5 +++++ pchealthstream2py/tests/simple_test.py | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pchealthstream2py/pchealth.py b/pchealthstream2py/pchealth.py index e96ab55..14dfa83 100644 --- a/pchealthstream2py/pchealth.py +++ b/pchealthstream2py/pchealth.py @@ -280,6 +280,11 @@ def _stop_previous_worker(self): stop, leaving two threads appending to the same queue. """ worker = self._worker + if worker is None and threading.Thread.is_alive(self): + # Started the legacy way (`start()`, so running in `self`) and then + # `open()`ed: that run must stop too, or it would miss the stop flag + # `open()` clears below and keep feeding the queue next to the new one. + worker = self if worker is None or not worker.is_alive(): return diff --git a/pchealthstream2py/tests/simple_test.py b/pchealthstream2py/tests/simple_test.py index 59ea77b..6b311f9 100644 --- a/pchealthstream2py/tests/simple_test.py +++ b/pchealthstream2py/tests/simple_test.py @@ -1,6 +1,7 @@ """Simple tests""" from pchealthstream2py.pchealth import StatusInfoReader +import threading import time from pprint import pprint @@ -43,7 +44,8 @@ def test_stop_does_not_shadow_thread_internals(): stuck on True after the worker had finished. """ reader = StatusInfoReader(read_interval_ms=50) - assert callable(reader._stop) + # (`Thread._stop` exists up to Python 3.12; 3.13 removed it.) + assert not isinstance(getattr(reader, '_stop', None), threading.Event) reader.open() try: @@ -127,3 +129,20 @@ def test_thread_api_follows_the_current_worker_after_reopen(): reader.join(timeout=10) assert not reader.is_alive() assert not reader._worker.is_alive() + + +def test_open_after_a_legacy_start_leaves_a_single_producer(): + """A reader started with `start()` (running in `self`), closed, then `open()`ed + must not keep the legacy run going: `open()` clears the stop flag, so a run it + didn't wait for would never see the stop and would keep feeding the queue.""" + reader = StatusInfoReader(read_interval_ms=50) + reader.start() + time.sleep(0.1) + reader.close() + reader.open() + try: + assert not threading.Thread.is_alive(reader) # the legacy run has stopped + assert reader.is_alive() # and the new worker is reading + finally: + reader.close() + reader.join(timeout=10)