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:
Don Jayamanne 2018-07-12 01:01:25 -07:00 committed by GitHub
parent 96cbec8522
commit 3c29eeeab7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 171 additions and 124 deletions

View file

@ -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(

View file

@ -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

View file

@ -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,

View file

@ -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,
)),
])

View file

@ -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()