microsoft / vscode-python-debugger

Python debugger (debugpy) extension for VS Code.
https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy
MIT License
57 stars 24 forks source link

VScode debugger does not stop at the breakpoint #226

Closed rttus closed 3 months ago

rttus commented 7 months ago

Hi,

I am trying to run this multithread publisher subscriber code https://fast-dds.docs.eprosima.com/en/latest/fastdds/getting_started/simple_python_app/simple_python_app.html Below is the code for quick reference.

I am running into the issue where breakpoint in the function on_data_available in the subscriber code is not hit. Below is my vscode and extension version. Please help.

Version: 1.86.2 (Universal) Commit: 903b1e9d8990623e3d7da1df3d33db3e42d80eda Date: 2024-02-13T19:42:13.651Z Electron: 27.2.3 ElectronBuildId: 26908389 Chromium: 118.0.5993.159 Node.js: 18.17.1 V8: 11.8.172.18-electron.0 OS: Darwin arm64 23.3.0

# Copyright 2022 Proyectos y Sistemas de Mantenimiento SL (eProsima).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
HelloWorld Publisher
"""
from threading import Condition
import time
import os

import fastdds
import HelloWorld

DESCRIPTION = """HelloWorld Publisher example for Fast DDS python bindings"""
USAGE = ('python3 HelloWorldPublisher.py')

class WriterListener (fastdds.DataWriterListener) :
    def __init__(self, writer) :
        self._writer = writer
        super().__init__()

    def on_publication_matched(self, datawriter, info) :
        print('on_publication_matched: {}'.format(os.getpid()))
        if (0 < info.current_count_change) :
            print ("Publisher matched subscriber {}".format(info.last_subscription_handle))
            self._writer._cvDiscovery.acquire()
            self._writer._matched_reader += 1
            self._writer._cvDiscovery.notify()
            self._writer._cvDiscovery.release()
        else :
            print ("Publisher unmatched subscriber {}".format(info.last_subscription_handle))
            self._writer._cvDiscovery.acquire()
            self._writer._matched_reader -= 1
            self._writer._cvDiscovery.notify()
            self._writer._cvDiscovery.release()

class Writer:

    def __init__(self):
        print('init: {}'.format(os.getpid()))
        self._matched_reader = 0
        self._cvDiscovery = Condition()
        self.index = 0

        factory = fastdds.DomainParticipantFactory.get_instance()
        self.participant_qos = fastdds.DomainParticipantQos()
        factory.get_default_participant_qos(self.participant_qos)
        self.participant = factory.create_participant(0, self.participant_qos)

        self.topic_data_type = HelloWorld.HelloWorldPubSubType()
        self.topic_data_type.setName("HelloWorld")
        self.type_support = fastdds.TypeSupport(self.topic_data_type)
        self.participant.register_type(self.type_support)

        self.topic_qos = fastdds.TopicQos()
        self.participant.get_default_topic_qos(self.topic_qos)
        self.topic = self.participant.create_topic("HelloWorldTopic", self.topic_data_type.getName(), self.topic_qos)

        self.publisher_qos = fastdds.PublisherQos()
        self.participant.get_default_publisher_qos(self.publisher_qos)
        self.publisher = self.participant.create_publisher(self.publisher_qos)

        self.listener = WriterListener(self)
        self.writer_qos = fastdds.DataWriterQos()
        self.publisher.get_default_datawriter_qos(self.writer_qos)
        self.writer = self.publisher.create_datawriter(self.topic, self.writer_qos, self.listener)

    def write(self):
        print('write: {}'.format(os.getpid()))
        data = HelloWorld.HelloWorld()
        data.message("Hello World")
        data.index(self.index)
        self.writer.write(data)
        print("Sending {message} : {index}".format(message=data.message(), index=data.index()))
        self.index = self.index + 1

    def wait_discovery(self) :
        print('waid_discovery: {}'.format(os.getpid()))
        self._cvDiscovery.acquire()
        print ("Writer is waiting discovery...")
        self._cvDiscovery.wait_for(lambda : self._matched_reader != 0)
        self._cvDiscovery.release()
        print("Writer discovery finished...")

    def run(self):
        print('run: {}'.format(os.getpid()))
        self.wait_discovery()
        for x in range(10) :
            time.sleep(1)
            self.write()
        self.delete()

    def delete(self):
        print('delete: {}'.format(os.getpid()))
        factory = fastdds.DomainParticipantFactory.get_instance()
        self.participant.delete_contained_entities()
        factory.delete_participant(self.participant)

if __name__ == '__main__':
    print('main: {}'.format(os.getpid()))
    print('Starting publisher.')
    writer = Writer()
    writer.run()
    exit()
# Copyright 2022 Proyectos y Sistemas de Mantenimiento SL (eProsima).
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
HelloWorld Subscriber
"""
import os
import signal

import fastdds
import HelloWorld

DESCRIPTION = """HelloWorld Subscriber example for Fast DDS python bindings"""
USAGE = ('python3 HelloWorldSubscriber.py')

# To capture ctrl+C
def signal_handler(sig, frame):
    print('Interrupted!')

class ReaderListener(fastdds.DataReaderListener):

    def __init__(self):
        super().__init__()

    def on_subscription_matched(self, datareader, info) :
        print('on_subscription_matched: {}'.format(os.getpid()))
        if (0 < info.current_count_change) :
            print ("Subscriber matched publisher {}".format(info.last_publication_handle))
        else :
            print ("Subscriber unmatched publisher {}".format(info.last_publication_handle))

    def on_data_available(self, reader):
        print('on_data_available: {}'.format(os.getpid()))
        info = fastdds.SampleInfo()
        data = HelloWorld.HelloWorld()
        reader.take_next_sample(data, info)

        print("Received {message} : {index}".format(message=data.message(), index=data.index()))

class Reader:

    def __init__(self):
        print('init: {}'.format(os.getpid()))
        factory = fastdds.DomainParticipantFactory.get_instance()
        self.participant_qos = fastdds.DomainParticipantQos()
        factory.get_default_participant_qos(self.participant_qos)
        self.participant = factory.create_participant(0, self.participant_qos)

        self.topic_data_type = HelloWorld.HelloWorldPubSubType()
        self.topic_data_type.setName("HelloWorld")
        self.type_support = fastdds.TypeSupport(self.topic_data_type)
        self.participant.register_type(self.type_support)

        self.topic_qos = fastdds.TopicQos()
        self.participant.get_default_topic_qos(self.topic_qos)
        self.topic = self.participant.create_topic("HelloWorldTopic", self.topic_data_type.getName(), self.topic_qos)

        self.subscriber_qos = fastdds.SubscriberQos()
        self.participant.get_default_subscriber_qos(self.subscriber_qos)
        self.subscriber = self.participant.create_subscriber(self.subscriber_qos)

        self.listener = ReaderListener()
        self.reader_qos = fastdds.DataReaderQos()
        self.subscriber.get_default_datareader_qos(self.reader_qos)
        self.reader = self.subscriber.create_datareader(self.topic, self.reader_qos, self.listener)

    def delete(self):
        print('delete: {}'.format(os.getpid()))
        factory = fastdds.DomainParticipantFactory.get_instance()
        self.participant.delete_contained_entities()
        factory.delete_participant(self.participant)

    def run(self):
        print('run: {}'.format(os.getpid()))
        signal.signal(signal.SIGINT, signal_handler)
        print('Press Ctrl+C to stop')
        signal.pause()
        self.delete()

if __name__ == '__main__':
    print('main: {}'.format(os.getpid()))
    print('Creating subscriber.')
    reader = Reader()
    reader.run()
    exit()
PanzerFowst commented 7 months ago

I have similar issues trying to stop on breakpoints using examples from this Brainflow example code.

As far as I can tell, breakpoints work until the code gets paste the brainflow import statements. Breakpoints will work on any line of code up to the first brainflow import statement and then all breakpoints are essentially invisible. This makes me wonder if there is something being changed behind the scenes of the Brainflow import that messes with the Python debugger and similarly in rttus's code too?

My only workaround that I have found to work is to use the actual breakpoint() function into places in the code where I want to break after.

rttus commented 7 months ago

Thank you for sharing your workaround. If i use breakpoint() it works for me as well.

PanzerFowst commented 7 months ago

Glad the workaround works for you too!

Wish I knew what was happening behind the scenes to make it so that actual code breakpoints in the gutter would work... =(

paulacamargo25 commented 7 months ago

Thanks for your issue report, i'm not sure if the Python Debugger Extension handles that. @int19h do you have any idea what's going on?

int19h commented 7 months ago

I am not familiar with FastDDS, but if it spawns threads itself (i.e. those threads are not spawned via Python's threading module), the debugger will not be aware of them, and thus will not be able to hit breakpoints on them. The workaround is to call debugpy.trace_this_thread() as early as possible to notify the debugger.

PanzerFowst commented 7 months ago

That makes sense to me. Would libraries spawning threads stop debugpy from stopping on breakpoints on the main thread too?

In my case, I don't think the Brainflow libraries that I use spawn threads, but I'd have to look into that more. Is there other reasons that the debugger would no longer stop on breakpoints set in the gutter but would stop with the breakpoint() function?

rttus commented 6 months ago

That makes lot of sense, i presume it would be the same for multiprocessing module?

int19h commented 6 months ago

multiprocessing and the rest of stdlib should work just fine. It's specifically the problem when a third party library spawns a new thread from native code, and then tries to run Python code on it. CPython itself handles this just fine, automatically initializing the thread as needed, but the debugger is unaware that there's a new thread it needs to be tracing. It should not affect any "normal" threads in the same process, though, including the main thread.

int19h commented 6 months ago

With respect to BrainFlaw, if breakpoints suddenly stop working after it gets imported, it is quite likely that it overrides the tracing hook via sys.settrace for its own purposes - some packages do this for integrated debugging or diagnostic functionality of their own, but then that interferes with debugger operation.

Both issues are something that will go away with the new debugger architecture in debugpy v2 (but the cost of that will be requiring Python 3.12+).