Skip to content

Persistent Dummy-N entries #1862

@RadoslawDeska

Description

@RadoslawDeska

When using debugpy on PyQt5 threads I observe that VSCode's Call Stack viewer shows Dummy-N thread. This is not peculiar yet. However, after effectively quitting the thread and re-running it, the old one is considered as Running (in other words it is persistent, as if some kind of hook existed). I use latest versions of VSCode (1.97.2) and Microsoft Python Debugger (2025.1.2025022401). Python 3.11.6, PyQt5 version 5.15.10. I tried changing the properties of launch.json file "justMyCode" to true as I was suggested to, but it doesn't change anything. I don't observe any memory leaks related to closing the threads (as far as rough checks of ram usage for the process are concerned). It seems like the hook is just persistent there and PyQt5 weren't notifying the debugger about finishing the thread.

main.py

from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
from signal_generator.signals import SignalManager

class Window(QMainWindow):
    def __init__(self):
        super().__init__()

        self.signalmanager = SignalManager(parent=self)
        
        self.setWindowTitle("Python oscilloscope")

        self.setWindowTitle("Checkable Button Example")
        self.setGeometry(100, 100, 300, 200)

        # Create a button with checkable state
        self.button = QPushButton("Checkable Button", self)
        self.button.setCheckable(True)
        self.button.setGeometry(100, 80, 100, 40)

        # Connect button signal to a slot function
        self.button.clicked.connect(self.on_button_click)
        
        self.channel1_thread = None
        self.channel1_generator = None

    def on_button_click(self):
        if self.button.isChecked():
            self.button.setText("Run background task")
            self.signalmanager.start_signal_generator(1)
        else:
            self.button.setText("Stop background task")
            self.signalmanager.stop_signal_generator(1)
    
    def closeEvent(self, event):
        self.close()

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    
    # TESTING

    window.show()
    app.exec_()

signal_generator\signals.py

import time
from PyQt5.QtCore import QObject, pyqtSignal, QThread

import sys

import debugpy
has_trace = hasattr(sys, 'gettrace') and sys.gettrace() is not None
has_breakpoint = sys.breakpointhook.__module__ != "sys"
isdebug = has_trace or has_breakpoint

class SignalManager:
    def __init__(self, parent):
        self.parent = parent
    
    def start_signal_generator(self, channel: int):
        if channel == 1:
            # Ensure the previous thread is properly finished
            if hasattr(self, "channel1_thread") and self.channel1_thread is not None:
                self.stop_signal_generator(channel)
            # Define worker thread
            self.channel1_thread = QThread()
            self.channel1_generator = SignalGenerator(self.parent)
            self.channel1_generator.moveToThread(self.channel1_thread)
            self.channel1_thread.started.connect(self.channel1_generator.run)
            self.channel1_generator.finished.connect(self.channel1_thread.quit)
            self.channel1_generator.finished.connect(self.channel1_generator.deleteLater)
            self.channel1_thread.finished.connect(self.channel1_thread.deleteLater)
            self.channel1_generator.progress.connect(self._reportProgress)
            self.channel1_thread.start()
            
    def stop_signal_generator(self, channel):
        if channel == 1 and self.channel1_thread is not None and self.channel1_generator.is_running():
            self.channel1_generator.stop()
            self.channel1_thread.quit()
            self.channel1_thread.wait()
            self.channel1_thread = None
    
    def _reportProgress(self):
        """This function will be responsible for plotting updated signal."""
        pass

class SignalGenerator(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal()
    
    def __init__(self, parent, *args, **kwargs):
        super().__init__()
        self.parent = parent
        self.running = True
        
        if "noise_std_dev" in kwargs:
            self.noise_std_dev = kwargs["noise_std_dev"]
    
    def run(self):
        """Long-running task of generating/updating the signal"""
        if isdebug:
            debugpy.debug_this_thread()
        
        while self.running:
            if not self.running:
                break
            
            time.sleep(1)
            
            self.progress.emit()
        
        self.finished.emit()
        self.stop()
    
    def stop(self):
        self.running = False
    
    def is_running(self):
        return self.running

launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger: Current File in Current Workspace with Qt",
            "type": "debugpy",
            "request": "launch",
            "program": "${workspaceFolder}/__main__.py",
            "console": "integratedTerminal",
            "cwd": "${workspaceFolder}",
            "justMyCode": false
        }
}

Metadata

Metadata

Assignees

Labels

needs reproIssue has not been reproduced yet

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions