mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Add timeouts for thread.joins to ensure it does not block indefinitely (#644)
* Add timeouts for thread.joins (this is the main change) * Format errors captured and displayed (for py27 and 3 compatibility) * Format code (this accounts for 95% of the changes, was tired of linter errors, hence formatted code using autpep8)
This commit is contained in:
parent
96cbec8522
commit
3c29eeeab7
5 changed files with 171 additions and 124 deletions
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
from ptvsd.socket import Address
|
||||
|
|
@ -15,12 +17,13 @@ class _LifecycleClient(Closeable):
|
|||
|
||||
SESSION = DebugSession
|
||||
|
||||
def __init__(self,
|
||||
addr=None,
|
||||
port=8888,
|
||||
breakpoints=None,
|
||||
connecttimeout=1.0,
|
||||
):
|
||||
def __init__(
|
||||
self,
|
||||
addr=None,
|
||||
port=8888,
|
||||
breakpoints=None,
|
||||
connecttimeout=1.0,
|
||||
):
|
||||
super(_LifecycleClient, self).__init__()
|
||||
self._addr = Address.from_raw(addr, defaultport=port)
|
||||
self._connecttimeout = connecttimeout
|
||||
|
|
@ -165,7 +168,6 @@ class DebugClient(_LifecycleClient):
|
|||
|
||||
|
||||
class EasyDebugClient(DebugClient):
|
||||
|
||||
def start_detached(self, argv):
|
||||
"""Start an adapter in a background process."""
|
||||
if self.closed:
|
||||
|
|
@ -191,8 +193,14 @@ class EasyDebugClient(DebugClient):
|
|||
assert self._session is None
|
||||
addr = ('localhost', self._addr.port)
|
||||
|
||||
self._run_server_ex = None
|
||||
|
||||
def run():
|
||||
self._session = self.SESSION.create_server(addr, **kwargs)
|
||||
try:
|
||||
self._session = self.SESSION.create_server(addr, **kwargs)
|
||||
except Exception as ex:
|
||||
self._run_server_ex = traceback.format_exc()
|
||||
|
||||
t = new_hidden_thread(
|
||||
target=run,
|
||||
name='test.client',
|
||||
|
|
@ -204,8 +212,14 @@ class EasyDebugClient(DebugClient):
|
|||
if t.is_alive():
|
||||
warnings.warn('timed out waiting for connection')
|
||||
if self._session is None:
|
||||
raise RuntimeError('unable to connect after {} secs'.format(
|
||||
self._connecttimeout))
|
||||
message = 'unable to connect after {} secs'.format( # noqa
|
||||
self._connecttimeout)
|
||||
if self._run_server_ex is None:
|
||||
raise Exception(message)
|
||||
else:
|
||||
message = message + os.linesep + self._run_server_ex # noqa
|
||||
raise Exception(message)
|
||||
|
||||
# The adapter will close when the connection does.
|
||||
|
||||
self._launch(
|
||||
|
|
|
|||
|
|
@ -132,13 +132,17 @@ class BinderBase(object):
|
|||
else:
|
||||
raise RuntimeError('timed out')
|
||||
return self._wrap_sock()
|
||||
|
||||
return connect, remote
|
||||
|
||||
def wait_until_done(self):
|
||||
def wait_until_done(self, timeout=10.0):
|
||||
"""Wait for the started debugger operation to finish."""
|
||||
if self._thread is None:
|
||||
return
|
||||
self._thread.join()
|
||||
self._thread.join(timeout)
|
||||
if self._thread.isAlive():
|
||||
raise Exception(
|
||||
'wait_until_done timed out after {} secs'.format(timeout))
|
||||
|
||||
####################
|
||||
# for subclassing
|
||||
|
|
@ -187,8 +191,10 @@ class Binder(BinderBase):
|
|||
|
||||
def __init__(self, do_debugging=None, **kwargs):
|
||||
if do_debugging is None:
|
||||
|
||||
def do_debugging(external, internal):
|
||||
time.sleep(5)
|
||||
|
||||
super(Binder, self).__init__(**kwargs)
|
||||
self._do_debugging = do_debugging
|
||||
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ class VSCLifecycle(object):
|
|||
daemon.wait_until_connected()
|
||||
return daemon
|
||||
|
||||
def _stop_daemon(self, daemon, disconnect=True):
|
||||
def _stop_daemon(self, daemon, disconnect=True, timeout=10.0):
|
||||
# We must close ptvsd directly (rather than closing the external
|
||||
# socket (i.e. "daemon"). This is because cloing ptvsd blocks,
|
||||
# keeping us from sending the disconnect request we need to send
|
||||
|
|
@ -251,7 +251,10 @@ class VSCLifecycle(object):
|
|||
t.start()
|
||||
if disconnect:
|
||||
self.disconnect()
|
||||
t.join()
|
||||
t.join(timeout)
|
||||
if t.isAlive():
|
||||
raise Exception(
|
||||
'_stop_daemon timed out after {} secs'.format(timeout))
|
||||
daemon.close()
|
||||
|
||||
def _handshake(self, command, threadnames=None, config=None, requests=None,
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import contextlib
|
|||
import os
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
import traceback
|
||||
import unittest
|
||||
|
||||
import ptvsd
|
||||
from ptvsd.wrapper import INITIALIZE_RESPONSE # noqa
|
||||
from ptvsd.wrapper import INITIALIZE_RESPONSE # noqa
|
||||
from tests.helpers._io import captured_stdio
|
||||
from tests.helpers.pydevd._live import LivePyDevd
|
||||
from tests.helpers.script import set_lock, find_line
|
||||
|
|
@ -20,7 +21,6 @@ from . import (
|
|||
|
||||
|
||||
class Fixture(VSCFixture):
|
||||
|
||||
def __init__(self, source, new_fake=None):
|
||||
self._pydevd = LivePyDevd(source)
|
||||
super(Fixture, self).__init__(
|
||||
|
|
@ -100,6 +100,7 @@ class TestBase(VSCTest):
|
|||
self.pathentry.install()
|
||||
self._filename = 'module:' + name
|
||||
|
||||
|
||||
##################################
|
||||
# lifecycle tests
|
||||
|
||||
|
|
@ -116,7 +117,7 @@ class LifecycleTests(TestBase, unittest.TestCase):
|
|||
addr = (None, 8888)
|
||||
with self.fake.start(addr):
|
||||
#with self.fix.install_sig_handler():
|
||||
yield
|
||||
yield
|
||||
|
||||
def test_launch(self):
|
||||
addr = (None, 8888)
|
||||
|
|
@ -143,33 +144,37 @@ class LifecycleTests(TestBase, unittest.TestCase):
|
|||
self.fix.binder.wait_until_done()
|
||||
received = self.vsc.received
|
||||
|
||||
self.assert_vsc_received(received, [
|
||||
self.new_event(
|
||||
'output',
|
||||
category='telemetry',
|
||||
output='ptvsd',
|
||||
data={'version': ptvsd.__version__}),
|
||||
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
|
||||
self.new_event('initialized'),
|
||||
self.new_response(req_attach),
|
||||
self.new_response(req_config),
|
||||
self.new_event('process', **dict(
|
||||
name=sys.argv[0],
|
||||
systemProcessId=os.getpid(),
|
||||
isLocalProcess=True,
|
||||
startMethod='attach',
|
||||
)),
|
||||
self.new_event('thread', reason='started', threadId=1),
|
||||
#self.new_event('exited', exitCode=0),
|
||||
#self.new_event('terminated'),
|
||||
])
|
||||
self.assert_vsc_received(
|
||||
received,
|
||||
[
|
||||
self.new_event(
|
||||
'output',
|
||||
category='telemetry',
|
||||
output='ptvsd',
|
||||
data={'version': ptvsd.__version__}),
|
||||
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
|
||||
self.new_event('initialized'),
|
||||
self.new_response(req_attach),
|
||||
self.new_response(req_config),
|
||||
self.new_event(
|
||||
'process',
|
||||
**dict(
|
||||
name=sys.argv[0],
|
||||
systemProcessId=os.getpid(),
|
||||
isLocalProcess=True,
|
||||
startMethod='attach',
|
||||
)),
|
||||
self.new_event('thread', reason='started', threadId=1),
|
||||
#self.new_event('exited', exitCode=0),
|
||||
#self.new_event('terminated'),
|
||||
])
|
||||
|
||||
|
||||
##################################
|
||||
# "normal operation" tests
|
||||
|
||||
|
||||
class VSCFlowTest(TestBase):
|
||||
|
||||
@contextlib.contextmanager
|
||||
def launched(self, port=8888, **kwargs):
|
||||
kwargs.setdefault('process', False)
|
||||
|
|
@ -177,7 +182,23 @@ class VSCFlowTest(TestBase):
|
|||
with self.lifecycle.launched(port=port, hide=True, **kwargs):
|
||||
yield
|
||||
self.fix.binder.done(close=False)
|
||||
self.fix.binder.wait_until_done()
|
||||
|
||||
try:
|
||||
self.fix.binder.wait_until_done()
|
||||
except Exception as ex:
|
||||
formatted_ex = traceback.format_exc()
|
||||
if hasattr(self, 'vsc') and hasattr(self.vsc, 'received'):
|
||||
message = """
|
||||
Session Messages:
|
||||
-----------------
|
||||
{}
|
||||
|
||||
Original Error:
|
||||
---------------
|
||||
{}""".format(os.linesep.join(self.vsc.received), formatted_ex)
|
||||
raise Exception(message)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
class BreakpointTests(VSCFlowTest, unittest.TestCase):
|
||||
|
|
@ -301,9 +322,13 @@ class BreakpointTests(VSCFlowTest, unittest.TestCase):
|
|||
self.lifecycle.requests = [] # Trigger capture.
|
||||
config = {
|
||||
'breakpoints': [{
|
||||
'source': {'path': self.filename},
|
||||
'source': {
|
||||
'path': self.filename
|
||||
},
|
||||
'breakpoints': [
|
||||
{'line': lineno},
|
||||
{
|
||||
'line': lineno
|
||||
},
|
||||
],
|
||||
}],
|
||||
'excbreakpoints': [],
|
||||
|
|
@ -317,18 +342,21 @@ class BreakpointTests(VSCFlowTest, unittest.TestCase):
|
|||
done1()
|
||||
with self.wait_for_event('stopped'):
|
||||
with self.wait_for_event('continued'):
|
||||
req_continue1 = self.send_request('continue', {
|
||||
'threadId': tid,
|
||||
})
|
||||
req_continue1 = self.send_request(
|
||||
'continue', {
|
||||
'threadId': tid,
|
||||
})
|
||||
with self.wait_for_event('stopped'):
|
||||
with self.wait_for_event('continued'):
|
||||
req_continue2 = self.send_request('continue', {
|
||||
req_continue2 = self.send_request(
|
||||
'continue', {
|
||||
'threadId': tid,
|
||||
})
|
||||
with self.wait_for_event('continued'):
|
||||
req_continue_last = self.send_request(
|
||||
'continue', {
|
||||
'threadId': tid,
|
||||
})
|
||||
with self.wait_for_event('continued'):
|
||||
req_continue_last = self.send_request('continue', {
|
||||
'threadId': tid,
|
||||
})
|
||||
|
||||
# Allow the script to run to completion.
|
||||
received = self.vsc.received
|
||||
|
|
@ -344,36 +372,33 @@ class BreakpointTests(VSCFlowTest, unittest.TestCase):
|
|||
self.assertEqual(got, config['breakpoints'])
|
||||
|
||||
self.assert_vsc_received_fixing_events(received, [
|
||||
('stopped', dict(
|
||||
reason='breakpoint',
|
||||
threadId=tid,
|
||||
text=None,
|
||||
description=None,
|
||||
)),
|
||||
('stopped',
|
||||
dict(
|
||||
reason='breakpoint',
|
||||
threadId=tid,
|
||||
text=None,
|
||||
description=None,
|
||||
)),
|
||||
req_continue1,
|
||||
('continued', dict(
|
||||
threadId=tid,
|
||||
)),
|
||||
('stopped', dict(
|
||||
reason='breakpoint',
|
||||
threadId=tid,
|
||||
text=None,
|
||||
description=None,
|
||||
)),
|
||||
('continued', dict(threadId=tid, )),
|
||||
('stopped',
|
||||
dict(
|
||||
reason='breakpoint',
|
||||
threadId=tid,
|
||||
text=None,
|
||||
description=None,
|
||||
)),
|
||||
req_continue2,
|
||||
('continued', dict(
|
||||
threadId=tid,
|
||||
)),
|
||||
('stopped', dict(
|
||||
reason='breakpoint',
|
||||
threadId=tid,
|
||||
text=None,
|
||||
description=None,
|
||||
)),
|
||||
('continued', dict(threadId=tid, )),
|
||||
('stopped',
|
||||
dict(
|
||||
reason='breakpoint',
|
||||
threadId=tid,
|
||||
text=None,
|
||||
description=None,
|
||||
)),
|
||||
req_continue_last,
|
||||
('continued', dict(
|
||||
threadId=tid,
|
||||
)),
|
||||
('continued', dict(threadId=tid, )),
|
||||
])
|
||||
self.assertIn('2 4 4', out)
|
||||
self.assertIn('ka-boom', err)
|
||||
|
|
@ -386,7 +411,9 @@ class BreakpointTests(VSCFlowTest, unittest.TestCase):
|
|||
config = {
|
||||
'breakpoints': [],
|
||||
'excbreakpoints': [
|
||||
{'filters': ['raised']},
|
||||
{
|
||||
'filters': ['raised']
|
||||
},
|
||||
],
|
||||
}
|
||||
with captured_stdio() as (stdout, _):
|
||||
|
|
@ -412,12 +439,12 @@ class BreakpointTests(VSCFlowTest, unittest.TestCase):
|
|||
else:
|
||||
description = "MyError('ka-boom',)"
|
||||
self.assert_vsc_received_fixing_events(received, [
|
||||
('stopped', dict(
|
||||
reason='exception',
|
||||
threadId=tid,
|
||||
text='MyError',
|
||||
description=description
|
||||
)),
|
||||
('stopped',
|
||||
dict(
|
||||
reason='exception',
|
||||
threadId=tid,
|
||||
text='MyError',
|
||||
description=description)),
|
||||
])
|
||||
self.assertIn('2 4 4', out)
|
||||
self.assertIn('ka-boom', out)
|
||||
|
|
@ -439,7 +466,6 @@ class LogpointTests(TestBase, unittest.TestCase):
|
|||
|
||||
@contextlib.contextmanager
|
||||
def closing(self, exit=True):
|
||||
|
||||
def handle_msg(msg, _):
|
||||
with self.wait_for_event('output'):
|
||||
self.req_disconnect = self.send_request('disconnect')
|
||||
|
|
@ -455,7 +481,7 @@ class LogpointTests(TestBase, unittest.TestCase):
|
|||
def running(self):
|
||||
addr = (None, 8888)
|
||||
with self.fake.start(addr):
|
||||
yield
|
||||
yield
|
||||
|
||||
def test_basic(self):
|
||||
with open(self.filename) as scriptfile:
|
||||
|
|
@ -473,18 +499,20 @@ class LogpointTests(TestBase, unittest.TestCase):
|
|||
req_initialize = self.send_request('initialize', {
|
||||
'adapterID': 'spam',
|
||||
})
|
||||
req_attach = self.send_request('attach', {
|
||||
'debugOptions': ['RedirectOutput']
|
||||
})
|
||||
req_breakpoints = self.send_request('setBreakpoints', {
|
||||
'source': {'path': self.filename},
|
||||
'breakpoints': [
|
||||
{
|
||||
'line': '4',
|
||||
'logMessage': '{a}+{b}=3'
|
||||
req_attach = self.send_request(
|
||||
'attach', {'debugOptions': ['RedirectOutput']})
|
||||
req_breakpoints = self.send_request(
|
||||
'setBreakpoints', {
|
||||
'source': {
|
||||
'path': self.filename
|
||||
},
|
||||
],
|
||||
})
|
||||
'breakpoints': [
|
||||
{
|
||||
'line': '4',
|
||||
'logMessage': '{a}+{b}=3'
|
||||
},
|
||||
],
|
||||
})
|
||||
with self.vsc.wait_for_event('output'): # 1+2=3
|
||||
with self.vsc.wait_for_event('thread'):
|
||||
req_config = self.send_request('configurationDone')
|
||||
|
|
@ -507,19 +535,27 @@ class LogpointTests(TestBase, unittest.TestCase):
|
|||
self.new_response(req_initialize, **INITIALIZE_RESPONSE),
|
||||
self.new_event('initialized'),
|
||||
self.new_response(req_attach),
|
||||
self.new_response(req_breakpoints, **dict(
|
||||
breakpoints=[{'id': 1, 'verified': True, 'line': '4'}]
|
||||
)),
|
||||
self.new_response(
|
||||
req_breakpoints,
|
||||
**dict(breakpoints=[{
|
||||
'id': 1,
|
||||
'verified': True,
|
||||
'line': '4'
|
||||
}])),
|
||||
self.new_response(req_config),
|
||||
self.new_event('process', **dict(
|
||||
name=sys.argv[0],
|
||||
systemProcessId=os.getpid(),
|
||||
isLocalProcess=True,
|
||||
startMethod='attach',
|
||||
)),
|
||||
self.new_event(
|
||||
'process',
|
||||
**dict(
|
||||
name=sys.argv[0],
|
||||
systemProcessId=os.getpid(),
|
||||
isLocalProcess=True,
|
||||
startMethod='attach',
|
||||
)),
|
||||
self.new_event('thread', reason='started', threadId=1),
|
||||
self.new_event('output', **dict(
|
||||
category='stdout',
|
||||
output='1+2=3' + os.linesep,
|
||||
)),
|
||||
self.new_event(
|
||||
'output',
|
||||
**dict(
|
||||
category='stdout',
|
||||
output='1+2=3' + os.linesep,
|
||||
)),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -249,28 +249,16 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase):
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
fmt = {
|
||||
"sep": os.linesep,
|
||||
"messages": os.linesep.join(messages),
|
||||
"error": ''.join(traceback.format_exception_only(exc_type, exc_value)) # noqa
|
||||
}
|
||||
message = """
|
||||
|
||||
Session Messages:
|
||||
-----------------
|
||||
%(messages)s
|
||||
{}
|
||||
|
||||
Original Error:
|
||||
---------------
|
||||
%(error)s""" % fmt
|
||||
{}""".format(os.linesep.join(messages), formatted_ex)
|
||||
|
||||
try:
|
||||
# Chain the original exception for py3.
|
||||
exec('raise Exception(message) from ex', globals(), locals())
|
||||
except SyntaxError:
|
||||
# This happens when using py27.
|
||||
message = message + os.linesep + formatted_ex
|
||||
exec("raise Exception(message)", globals(), locals())
|
||||
raise Exception(message)
|
||||
|
||||
def _handle_exception(ex, adapter, session):
|
||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue