Add delay waiting for socket server to start (#594)

Add delay waiting for socket server to start and port to be free
This commit is contained in:
Don Jayamanne 2018-07-05 14:52:43 -07:00 committed by GitHub
parent aa5a4ef524
commit 1a9112262b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 57 deletions

View file

@ -89,6 +89,19 @@ def wait_for_socket_server(addr, timeout=3.0, **kwargs):
raise ConnectionRefusedError('Timeout waiting for connection')
def wait_for_port_to_free(port, timeout=3.0):
start_time = time.time()
while True:
try:
sock = socket.create_connection(('localhost', port))
sock.close()
except Exception:
return
time.sleep(0.1)
if time.time() - start_time > timeout:
raise ConnectionRefusedError('Timeout waiting for port to be free')
class DebugAdapter(Closeable):
VERBOSE = False
@ -105,38 +118,32 @@ class DebugAdapter(Closeable):
argv = list(argv)
cls._ensure_addr(argv, addr)
return Proc.start_python_module(
'ptvsd',
argv,
env=env_vars,
cwd=cwd,
**kwds
)
'ptvsd', argv, env=env_vars, cwd=cwd, **kwds)
return cls._start(new_proc, argv, **kwargs)
@classmethod
def start_wrapper_script(cls, filename, argv, env=None, cwd=None, **kwargs): # noqa
def start_wrapper_script(cls, filename, argv, env=None, cwd=None,
**kwargs): # noqa
def new_proc(argv, addr, **kwds):
env_vars = _copy_env(verbose=cls.VERBOSE, env=env)
return Proc.start_python_script(
filename,
argv,
env=env_vars,
cwd=cwd,
**kwds
)
filename, argv, env=env_vars, cwd=cwd, **kwds)
return cls._start(new_proc, argv, **kwargs)
@classmethod
def start_wrapper_module(cls, modulename, argv, env=None, cwd=None, **kwargs): # noqa
def start_wrapper_module(cls,
modulename,
argv,
env=None,
cwd=None,
**kwargs): # noqa
def new_proc(argv, addr, **kwds):
env_vars = _copy_env(verbose=cls.VERBOSE, env=env)
return Proc.start_python_module(
modulename,
argv,
env=env_vars,
cwd=cwd,
**kwds
)
modulename, argv, env=env_vars, cwd=cwd, **kwds)
return cls._start(new_proc, argv, **kwargs)
# specific factory cases
@ -169,7 +176,12 @@ class DebugAdapter(Closeable):
return adapter
@classmethod
def _start_as(cls, addr, name, kind='script', extra=None, server=False,
def _start_as(cls,
addr,
name,
kind='script',
extra=None,
server=False,
**kwargs):
argv = []
if server:
@ -192,11 +204,7 @@ class DebugAdapter(Closeable):
# TODO: Handle this case somehow?
assert 'ptvsd.enable_attach' in content
adapter = cls.start_wrapper_script(
filename,
argv=argv,
addr=addr,
**kwargs
)
filename, argv=argv, addr=addr, **kwargs)
wait_for_socket_server(addr, **kwargs)
return adapter

View file

@ -8,14 +8,15 @@ from . import Closeable
from .debugadapter import DebugAdapter, wait_for_socket_server
from .debugsession import DebugSession
# TODO: Add a helper function to start a remote debugger for testing
# remote debugging?
class _LifecycleClient(Closeable):
def __init__(self, addr=None, port=8888, breakpoints=None,
def __init__(self,
addr=None,
port=8888,
breakpoints=None,
connecttimeout=1.0):
super(_LifecycleClient, self).__init__()
self._addr = Address.from_raw(addr, defaultport=port)
@ -101,12 +102,19 @@ class _LifecycleClient(Closeable):
if self._adapter is not None:
self._adapter.close()
def _launch(self, argv, script=None, wait_for_connect=None,
detachable=True, env=None, cwd=None, **kwargs):
def _launch(self,
argv,
script=None,
wait_for_connect=None,
detachable=True,
env=None,
cwd=None,
**kwargs):
if script is not None:
def start(*args, **kwargs):
return DebugAdapter.start_wrapper_script(script,
*args, **kwargs)
return DebugAdapter.start_wrapper_script(
script, *args, **kwargs)
else:
start = DebugAdapter.start
new_addr = Address.as_server if detachable else Address.as_client
@ -138,7 +146,6 @@ class DebugClient(_LifecycleClient):
class EasyDebugClient(DebugClient):
def start_detached(self, argv):
"""Start an adapter in a background process."""
if self.closed:
@ -151,7 +158,12 @@ class EasyDebugClient(DebugClient):
self._adapter = DebugAdapter.start(argv, port=self._port)
return self._adapter
def host_local_debugger(self, argv, script=None, env=None, cwd=None, **kwargs): # noqa
def host_local_debugger(self,
argv,
script=None,
env=None,
cwd=None,
**kwargs): # noqa
if self.closed:
raise RuntimeError('debug client closed')
if self._adapter is not None:
@ -161,6 +173,7 @@ class EasyDebugClient(DebugClient):
def run():
self._session = DebugSession.create_server(addr, **kwargs)
t = new_hidden_thread(
target=run,
name='test.client',
@ -172,16 +185,17 @@ class EasyDebugClient(DebugClient):
if t.is_alive():
warnings.warn('timed out waiting for connection')
if self._session is None:
raise RuntimeError('unable to connect')
raise RuntimeError('unable to connect after {} secs'.format(
self._connecttimeout))
# The adapter will close when the connection does.
self._launch(
argv,
script=script,
wait_for_connect=wait,
detachable=False,
env=env,
cwd=cwd
)
cwd=cwd)
return self._adapter, self._session
@ -208,7 +222,8 @@ class EasyDebugClient(DebugClient):
assert self._session is None
argv = [
'-m', module,
'-m',
module,
] + list(argv)
if kwargs.pop('nodebug', False):
argv.insert(0, '--nodebug')

View file

@ -2,11 +2,12 @@ import contextlib
import os
import ptvsd
import signal
import time
import unittest
from collections import namedtuple
from ptvsd.socket import Address
from tests.helpers.debugadapter import DebugAdapter
from tests.helpers.debugadapter import DebugAdapter, wait_for_port_to_free
from tests.helpers.debugclient import EasyDebugClient as DebugClient
from tests.helpers.script import find_line
from tests.helpers.threading import get_locked_and_waiter
@ -17,7 +18,7 @@ from tests.helpers.vsc import parse_message, VSCMessages, Response, Event # noq
ROOT = os.path.dirname(os.path.dirname(ptvsd.__file__))
PORT = 9876
CONNECT_TIMEOUT = 3.0
DELAY_WAITING_FOR_SOCKETS = 1.0
DebugInfo = namedtuple('DebugInfo', 'port starttype argv filename modulename env cwd attachtype') # noqa
DebugInfo.__new__.__defaults__ = (9876, 'launch', []) + ((None, ) * (len(DebugInfo._fields) - 3)) # noqa
@ -218,6 +219,7 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase):
addr = Address('localhost', debug_info.port)
cwd = debug_info.cwd
env = debug_info.env
wait_for_port_to_free(debug_info.port)
def _kill_proc(pid):
"""If debugger does not end gracefully, then kill proc and
@ -229,6 +231,25 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase):
import time
time.sleep(1) # wait for socket connections to die out. # noqa
def _wrap_and_reraise(ex, session):
messages = []
try:
messages = [str(msg) for msg in
_strip_newline_output_events(session.received)]
except Exception:
pass
messages = os.linesep.join(messages)
try:
raise Exception(messages) from ex
except Exception:
print(messages)
raise ex
def _handle_exception(ex, adapter, session):
_kill_proc(adapter.pid)
_wrap_and_reraise(ex, session)
if debug_info.attachtype == 'import' and \
debug_info.modulename is not None:
argv = debug_info.argv
@ -238,13 +259,13 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase):
env=env,
cwd=cwd) as adapter:
with DebugClient() as editor:
time.sleep(DELAY_WAITING_FOR_SOCKETS)
session = editor.attach_socket(addr, adapter)
try:
yield Debugger(session=session, adapter=adapter)
adapter.wait()
except Exception:
_kill_proc(adapter.pid)
raise
except Exception as ex:
_handle_exception(ex, adapter, session)
elif debug_info.attachtype == 'import' and \
debug_info.starttype == 'attach' and \
debug_info.filename is not None:
@ -256,13 +277,13 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase):
env=env,
cwd=cwd) as adapter:
with DebugClient() as editor:
time.sleep(DELAY_WAITING_FOR_SOCKETS)
session = editor.attach_socket(addr, adapter)
try:
yield Debugger(session=session, adapter=adapter)
adapter.wait()
except Exception:
_kill_proc(adapter.pid)
raise
except Exception as ex:
_handle_exception(ex, adapter, session)
elif debug_info.starttype == 'attach':
if debug_info.modulename is None:
name = debug_info.filename
@ -279,13 +300,13 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase):
env=env,
cwd=cwd) as adapter:
with DebugClient() as editor:
time.sleep(DELAY_WAITING_FOR_SOCKETS)
session = editor.attach_socket(addr, adapter)
try:
yield Debugger(session=session, adapter=adapter)
adapter.wait()
except Exception:
_kill_proc(adapter.pid)
raise
except Exception as ex:
_handle_exception(ex, adapter, session)
else:
if debug_info.filename is None:
argv = ["-m", debug_info.modulename] + debug_info.argv
@ -294,14 +315,14 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase):
with DebugClient(
port=debug_info.port,
connecttimeout=CONNECT_TIMEOUT) as editor:
time.sleep(DELAY_WAITING_FOR_SOCKETS)
adapter, session = editor.host_local_debugger(
argv, cwd=cwd, env=env)
try:
yield Debugger(session=session, adapter=adapter)
adapter.wait()
except Exception:
_kill_proc(adapter.pid)
raise
except Exception as ex:
_handle_exception(ex, adapter, session)
@property
def messages(self):

View file

@ -45,6 +45,7 @@ class LaunchExceptionLifecycleTests(LifecycleTestsBase):
options = {"debugOptions": ["RedirectOutput"]}
with self.start_debugging(debug_info) as dbg:
stopped = dbg.session.get_awaiter_for_event('stopped')
(_, req_launch_attach, _, _, _, _) = lifecycle_handshake(
dbg.session,
debug_info.starttype,
@ -52,10 +53,7 @@ class LaunchExceptionLifecycleTests(LifecycleTestsBase):
options=options,
threads=True)
req_launch_attach.wait()
stopped = dbg.session.get_awaiter_for_event('stopped')
stopped.wait()
Awaitable.wait_all(req_launch_attach, stopped)
self.assertEqual(stopped.event.body["text"], "ArithmeticError")
self.assertIn("ArithmeticError('Hello'",
stopped.event.body["description"])