mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Fix #1648: Messaging does not allow reverse requests
Separate message parsing and message handling into separate threads. Remove nested message handling in request handlers via `yield`, since it is incompatible with the new split model, and replace it with NO_RESPONSE and Request.respond() to defer responses until later. Change Message.cant_handle() and Message.isnt_valid() to respond to the request and return the exception, instead of raising it, to accommodate NO_RESPONSE scenarios where a failure needs to be reported later. Fix #1678: Do not rely on "processId" being returned by "runInTerminal" request Extract debuggee PID from the "process" event sent by the debug server. Fix #1679: "exited" event sometimes reports "exitCode": null Report it as -1 if it cannot be retrieved from the debuggee process. Fix #1680: Fatal errors in message loop do not fail fast os._exit() immediately if a fatal error occurs in message parsing or message handling background threads.
This commit is contained in:
parent
7256e99b52
commit
981b1d1559
12 changed files with 932 additions and 823 deletions
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
|
@ -21,8 +21,8 @@
|
|||
"type": "python",
|
||||
"request": "launch",
|
||||
"debugServer": 8765,
|
||||
"console": "internalConsole",
|
||||
//"console": "integratedTerminal",
|
||||
//"console": "internalConsole",
|
||||
"console": "integratedTerminal",
|
||||
//"console": "externalTerminal",
|
||||
"consoleTitle": "ptvsd.server",
|
||||
//"program": "${file}",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ def main(args):
|
|||
from ptvsd.common import log, options
|
||||
from ptvsd.adapter import channels
|
||||
|
||||
if args.cls:
|
||||
if args.cls and args.debug_server is not None:
|
||||
print("\033c")
|
||||
|
||||
options.log_dir = args.log_dir
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class Capabilities(dict):
|
|||
try:
|
||||
value = validate(value)
|
||||
except Exception as exc:
|
||||
message.isnt_valid("{0!j} {1}", name, exc)
|
||||
raise message.isnt_valid("{0!j} {1}", name, exc)
|
||||
|
||||
assert value != (), fmt(
|
||||
"{0!j} must provide a default value for missing properties.", validate
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ exit_code = None
|
|||
pid = None
|
||||
"""Debuggee process ID."""
|
||||
|
||||
_got_pid = threading.Event()
|
||||
"""A threading.Event that is set when pid is set.
|
||||
"""
|
||||
|
||||
_exited = None
|
||||
"""A threading.Event that is set when the debuggee process exits.
|
||||
|
||||
|
|
@ -110,7 +114,7 @@ def _parse_request(request, address):
|
|||
assert prop_name[0].islower() and flag_name[0].isupper()
|
||||
value = request(prop_name, json.default(flag_name in debug_options))
|
||||
if value is False and flag_name in debug_options:
|
||||
request.isnt_valid(
|
||||
raise request.isnt_valid(
|
||||
'{0!r}:false and "debugOptions":[{1!r}] are mutually exclusive',
|
||||
prop_name,
|
||||
flag_name,
|
||||
|
|
@ -125,7 +129,7 @@ def _parse_request(request, address):
|
|||
)
|
||||
if console != "internalConsole":
|
||||
if not contract.ide.capabilities["supportsRunInTerminalRequest"]:
|
||||
request.cant_handle(
|
||||
raise request.cant_handle(
|
||||
'Unable to launch via "console":{0!j}, because the IDE is does not '
|
||||
'have the "supportsRunInTerminalRequest" capability',
|
||||
console,
|
||||
|
|
@ -136,7 +140,7 @@ def _parse_request(request, address):
|
|||
cmdline = []
|
||||
if property_or_debug_option("sudo", "Sudo"):
|
||||
if platform.system() == "Windows":
|
||||
request.cant_handle('"sudo":true is not supported on Windows.')
|
||||
raise request.cant_handle('"sudo":true is not supported on Windows.')
|
||||
else:
|
||||
cmdline += ["sudo"]
|
||||
|
||||
|
|
@ -145,7 +149,9 @@ def _parse_request(request, address):
|
|||
python_key = "python"
|
||||
if python_key in request:
|
||||
if "pythonPath" in request:
|
||||
request.isnt_valid('"pythonPath" is not valid if "python" is specified')
|
||||
raise request.isnt_valid(
|
||||
'"pythonPath" is not valid if "python" is specified'
|
||||
)
|
||||
elif "pythonPath" in request:
|
||||
python_key = "pythonPath"
|
||||
python = request(python_key, json.array(unicode, vectorize=True, size=(1,)))
|
||||
|
|
@ -185,9 +191,13 @@ def _parse_request(request, address):
|
|||
|
||||
num_targets = len([x for x in (program, module, code) if x != ()])
|
||||
if num_targets == 0:
|
||||
request.isnt_valid('either "program", "module", or "code" must be specified')
|
||||
raise request.isnt_valid(
|
||||
'either "program", "module", or "code" must be specified'
|
||||
)
|
||||
elif num_targets != 1:
|
||||
request.isnt_valid('"program", "module", and "code" are mutually exclusive')
|
||||
raise request.isnt_valid(
|
||||
'"program", "module", and "code" are mutually exclusive'
|
||||
)
|
||||
|
||||
cmdline += request("args", json.array(unicode))
|
||||
|
||||
|
|
@ -209,7 +219,7 @@ def _spawn_popen(request, spawn_info):
|
|||
try:
|
||||
proc = subprocess.Popen(spawn_info.cmdline, cwd=spawn_info.cwd, env=env)
|
||||
except Exception as exc:
|
||||
request.cant_handle(
|
||||
raise request.cant_handle(
|
||||
"Error launching process: {0}\n\nCommand line:{1!r}",
|
||||
exc,
|
||||
spawn_info.cmdline,
|
||||
|
|
@ -220,6 +230,7 @@ def _spawn_popen(request, spawn_info):
|
|||
global pid
|
||||
try:
|
||||
pid = proc.pid
|
||||
_got_pid.set()
|
||||
ProcessTracker().track(pid)
|
||||
except Exception:
|
||||
# If we can't track it, we won't be able to terminate it if asked; but aside
|
||||
|
|
@ -258,19 +269,51 @@ def _spawn_terminal(request, spawn_info):
|
|||
}
|
||||
|
||||
try:
|
||||
result = channels.Channels().ide().request("runInTerminal", body)
|
||||
channels.Channels().ide().request("runInTerminal", body)
|
||||
except messaging.MessageHandlingError as exc:
|
||||
exc.propagate(request)
|
||||
|
||||
# Although "runInTerminal" response has "processId", it's optional, and in practice
|
||||
# it is not used by VSCode: https://github.com/microsoft/vscode/issues/61640.
|
||||
# Thus, we can only retrieve the PID via the "process" event, and only then we can
|
||||
# start tracking it. Until then, nothing else to do.
|
||||
pass
|
||||
|
||||
|
||||
def parse_pid(process_event):
|
||||
assert process_event.is_event("process")
|
||||
|
||||
if _got_pid.is_set():
|
||||
# If we already have the PID, there's nothing to do.
|
||||
return
|
||||
|
||||
global pid
|
||||
pid = result("processId", int)
|
||||
sys_pid = process_event("systemProcessId", int)
|
||||
|
||||
def after_exit(code):
|
||||
global exit_code
|
||||
exit_code = code
|
||||
_exited.set()
|
||||
|
||||
try:
|
||||
ProcessTracker().track(pid, after_exit=lambda: _exited.set())
|
||||
pid = sys_pid
|
||||
_got_pid.set()
|
||||
ProcessTracker().track(pid, after_exit=after_exit)
|
||||
except Exception as exc:
|
||||
# If we can't track it, we won't be able to detect if it exited or retrieve
|
||||
# the exit code, so fail immediately.
|
||||
request.cant_handle("Couldn't get debuggee process handle: {0}", str(exc))
|
||||
raise process_event.cant_handle(
|
||||
"Couldn't get debuggee process handle: {0}", str(exc)
|
||||
)
|
||||
|
||||
|
||||
def wait_for_pid(timeout=None):
|
||||
"""Waits for debuggee PID to be determined.
|
||||
|
||||
Returns True if PID was determined, False if the wait timed out. If it returned
|
||||
True, then pid is guaranteed to be set.
|
||||
"""
|
||||
return _got_pid.wait(timeout)
|
||||
|
||||
|
||||
def wait_for_exit(timeout=None):
|
||||
|
|
@ -280,6 +323,13 @@ def wait_for_exit(timeout=None):
|
|||
True, then exit_code is guaranteed to be set.
|
||||
"""
|
||||
|
||||
if pid is None:
|
||||
# Debugee was launched with "runInTerminal", but the debug session fell apart
|
||||
# before we got a "process" event and found out what its PID is. It's not a
|
||||
# fatal error, but there's nothing to wait on. Debuggee process should have
|
||||
# exited (or crashed) by now in any case.
|
||||
return
|
||||
|
||||
assert _exited is not None
|
||||
timed_out = not _exited.wait(timeout)
|
||||
if not timed_out:
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ class Messages(singleton.Singleton):
|
|||
"""
|
||||
server = _channels.server()
|
||||
if server is None:
|
||||
messaging.Message.isnt_valid(
|
||||
raise messaging.Message.isnt_valid(
|
||||
"Connection to debug server is not established yet"
|
||||
)
|
||||
return server
|
||||
|
|
@ -64,12 +64,11 @@ class Messages(singleton.Singleton):
|
|||
current_state = state.current()
|
||||
if current_state in states:
|
||||
return handler(self, message)
|
||||
if isinstance(message, messaging.Request):
|
||||
message.isnt_valid(
|
||||
"Request {0!r} is not allowed in adapter state {1!r}.",
|
||||
message.command,
|
||||
current_state,
|
||||
)
|
||||
raise message.isnt_valid(
|
||||
"{0} is not allowed in adapter state {1!r}.",
|
||||
message.describe(),
|
||||
current_state,
|
||||
)
|
||||
|
||||
return handle_if_allowed
|
||||
|
||||
|
|
@ -118,6 +117,9 @@ class IDEMessages(Messages):
|
|||
# so store them here until then. After all messages are replayed, it is set to None.
|
||||
_initial_messages = []
|
||||
|
||||
# "launch" or "attach" request that started debugging.
|
||||
_start_request = None
|
||||
|
||||
# A decorator to add the message to initial_messages if needed before handling it.
|
||||
# Must be applied to the handler for every message that can be received before
|
||||
# connection to the debug server can be established while handling attach/launch,
|
||||
|
|
@ -153,7 +155,7 @@ class IDEMessages(Messages):
|
|||
|
||||
# Handles various attributes common to both "launch" and "attach".
|
||||
def _debug_config(self, request):
|
||||
assert request.command in ("launch", "attach")
|
||||
assert request.is_request("launch", "attach")
|
||||
self._shared.start_method = request.command
|
||||
_Shared.readonly_attrs.add("start_method")
|
||||
|
||||
|
|
@ -181,8 +183,8 @@ class IDEMessages(Messages):
|
|||
_Shared.readonly_attrs.add("terminate_on_disconnect")
|
||||
self._debug_config(request)
|
||||
|
||||
options.host = request.arguments.get("host", options.host)
|
||||
options.port = int(request.arguments.get("port", options.port))
|
||||
options.host = request("host", options.host)
|
||||
options.port = request("port", options.port)
|
||||
_channels.connect_to_server(address=(options.host, options.port))
|
||||
|
||||
return self._configure(request)
|
||||
|
|
@ -190,20 +192,23 @@ class IDEMessages(Messages):
|
|||
def _set_debugger_properties(self, request):
|
||||
debug_options = set(request("debugOptions", json.array(unicode)))
|
||||
client_os_type = None
|
||||
if 'WindowsClient' in debug_options or 'WINDOWS' in debug_options:
|
||||
client_os_type = 'WINDOWS'
|
||||
elif 'UnixClient' in debug_options or 'UNIX' in debug_options:
|
||||
client_os_type = 'UNIX'
|
||||
if "WindowsClient" in debug_options or "WINDOWS" in debug_options:
|
||||
client_os_type = "WINDOWS"
|
||||
elif "UnixClient" in debug_options or "UNIX" in debug_options:
|
||||
client_os_type = "UNIX"
|
||||
else:
|
||||
client_os_type = 'WINDOWS' if platform.system() == 'Windows' else 'UNIX'
|
||||
client_os_type = "WINDOWS" if platform.system() == "Windows" else "UNIX"
|
||||
|
||||
try:
|
||||
self._server.request("setDebuggerProperty", arguments={
|
||||
"skipSuspendOnBreakpointException": ("BaseException",),
|
||||
"skipPrintBreakpointException": ("NameError",),
|
||||
"multiThreadsSingleNotification": True,
|
||||
"ideOS": client_os_type,
|
||||
})
|
||||
self._server.request(
|
||||
"setDebuggerProperty",
|
||||
arguments={
|
||||
"skipSuspendOnBreakpointException": ("BaseException",),
|
||||
"skipPrintBreakpointException": ("NameError",),
|
||||
"multiThreadsSingleNotification": True,
|
||||
"ideOS": client_os_type,
|
||||
},
|
||||
)
|
||||
except messaging.MessageHandlingError as exc:
|
||||
exc.propagate(request)
|
||||
|
||||
|
|
@ -211,6 +216,7 @@ class IDEMessages(Messages):
|
|||
# the "initialized" event is sent, to when "configurationDone" is received; see
|
||||
# https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
|
||||
def _configure(self, request):
|
||||
assert request.is_request("launch", "attach")
|
||||
log.debug("Replaying previously received messages to server.")
|
||||
|
||||
assert len(self._initial_messages)
|
||||
|
|
@ -231,30 +237,45 @@ class IDEMessages(Messages):
|
|||
self._server.propagate(msg)
|
||||
|
||||
log.debug("Finished replaying messages to server.")
|
||||
self.initial_messages = None
|
||||
self._initial_messages = None
|
||||
self._start_request = request
|
||||
|
||||
if request.command == "launch":
|
||||
# Wait until we have the debuggee PID - we either know it already because we
|
||||
# have launched it directly, or we'll find out eventually from the "process"
|
||||
# server event. Either way, we need to know the PID before we can tell the
|
||||
# server to start debugging, because we need to be able to kill the debuggee
|
||||
# process if anything goes wrong.
|
||||
#
|
||||
# However, we can't block forever, because the debug server can also crash
|
||||
# before it had a chance to send the event - so wake up periodically, and
|
||||
# check whether server channel is still alive.
|
||||
while not debuggee.wait_for_pid(1):
|
||||
if _channels.server() is None:
|
||||
raise request.cant_handle("Debug server disconnected unexpectedly.")
|
||||
|
||||
self._set_debugger_properties(request)
|
||||
|
||||
# Let the IDE know that it can begin configuring the adapter.
|
||||
state.change("configuring")
|
||||
self._ide.send_event("initialized")
|
||||
|
||||
# Process further incoming messages, until we get "configurationDone".
|
||||
while state.current() == "configuring":
|
||||
yield
|
||||
return messaging.NO_RESPONSE # will respond on "configurationDone"
|
||||
|
||||
@_only_allowed_while("configuring")
|
||||
def configurationDone_request(self, request):
|
||||
ret = self._server.delegate(request)
|
||||
assert self._start_request is not None
|
||||
|
||||
result = self._server.delegate(request)
|
||||
state.change("running")
|
||||
ServerMessages().release_events()
|
||||
return ret
|
||||
request.respond(result)
|
||||
self._start_request.respond({})
|
||||
|
||||
def _disconnect_or_terminate_request(self, request):
|
||||
assert request.is_request("disconnect") or request.is_request("terminate")
|
||||
|
||||
if request("restart", json.default(False)):
|
||||
request.isnt_valid("Restart is not supported")
|
||||
raise request.isnt_valid("Restart is not supported")
|
||||
|
||||
terminate = (request.command == "terminate") or request(
|
||||
"terminateDebuggee", json.default(self._shared.terminate_on_disconnect)
|
||||
|
|
@ -388,7 +409,9 @@ class ServerMessages(Messages):
|
|||
# requests sent over that boundary, since they may contain arbitrary code
|
||||
# that the IDE will execute - e.g. "runInTerminal". The adapter must only
|
||||
# propagate requests that it knows are safe.
|
||||
request.isnt_valid("Requests from the debug server to the IDE are not allowed.")
|
||||
raise request.isnt_valid(
|
||||
"Requests from the debug server to the IDE are not allowed."
|
||||
)
|
||||
|
||||
# Generic event handler, used if there's no specific handler below.
|
||||
def event(self, event):
|
||||
|
|
@ -407,13 +430,24 @@ class ServerMessages(Messages):
|
|||
# also remove the 'initialized' event sent from IDE messages.
|
||||
pass
|
||||
|
||||
@_only_allowed_while("initializing")
|
||||
def process_event(self, event):
|
||||
if self._shared.start_method == "launch":
|
||||
try:
|
||||
debuggee.parse_pid(event)
|
||||
except Exception:
|
||||
# If we couldn't retrieve or validate PID, we can't safely continue
|
||||
# debugging, so shut everything down.
|
||||
self.disconnect()
|
||||
self._ide.propagate(event)
|
||||
|
||||
@_only_allowed_while("running")
|
||||
def ptvsd_subprocess_event(self, event):
|
||||
sub_pid = event("processId", int)
|
||||
try:
|
||||
debuggee.register_subprocess(sub_pid)
|
||||
except Exception as exc:
|
||||
event.cant_handle("{0}", exc)
|
||||
raise event.cant_handle("{0}", exc)
|
||||
self._ide.propagate(event)
|
||||
|
||||
def terminated_event(self, event):
|
||||
|
|
@ -450,7 +484,10 @@ class ServerMessages(Messages):
|
|||
# The debuggee process should exit shortly after it has disconnected, but just
|
||||
# in case it gets stuck, don't wait forever, and force-kill it if needed.
|
||||
debuggee.terminate(after=5)
|
||||
self._ide.send_event("exited", {"exitCode": debuggee.exit_code})
|
||||
exit_code = debuggee.exit_code
|
||||
self._ide.send_event(
|
||||
"exited", {"exitCode": -1 if exit_code is None else exit_code}
|
||||
)
|
||||
|
||||
self._ide.send_event("terminated")
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ def of_type(*classinfo, **kwargs):
|
|||
if (optional and value == ()) or isinstance(value, classinfo):
|
||||
return value
|
||||
else:
|
||||
if not optional and value == ():
|
||||
raise ValueError("must be specified")
|
||||
raise TypeError("must be " + " or ".join(t.__name__ for t in classinfo))
|
||||
|
||||
return validate
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -23,7 +23,7 @@ def _get_dont_trace_patterns():
|
|||
ptvsd_path = os.path.dirname(ptvsd_path)
|
||||
start_patterns = [ptvsd_path]
|
||||
end_patterns = ["ptvsd_launcher.py"]
|
||||
log.info('Dont trace patterns: {0!r}, {1!r}', (start_patterns, end_patterns))
|
||||
log.info('Dont trace patterns: {0!r}, {1!r}', start_patterns, end_patterns)
|
||||
return (start_patterns, end_patterns)
|
||||
|
||||
def wait_for_attach(timeout=None):
|
||||
|
|
|
|||
|
|
@ -801,7 +801,7 @@ class Session(object):
|
|||
'Received "terminated" event from {0}; stopping message processing.',
|
||||
self,
|
||||
)
|
||||
raise messaging.NoMoreMessages(fmt("{0} terminated", self))
|
||||
self.channel.close()
|
||||
|
||||
def _process_request(self, request):
|
||||
self.timeline.record_request(request, block=False)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from __future__ import absolute_import, print_function, unicode_literals
|
|||
"""
|
||||
|
||||
import collections
|
||||
import functools
|
||||
import json
|
||||
import io
|
||||
import pytest
|
||||
|
|
@ -44,17 +45,26 @@ class JsonMemoryStream(object):
|
|||
def close(self):
|
||||
pass
|
||||
|
||||
def _log_message(self, dir, data):
|
||||
format_string = "{0} {1} " + (
|
||||
"{2!j:indent=None}" if isinstance(data, list) else "{2!j}"
|
||||
)
|
||||
return log.debug(format_string, self.name, dir, data)
|
||||
|
||||
def read_json(self, decoder=None):
|
||||
decoder = decoder if decoder is not None else self.json_decoder_factory()
|
||||
try:
|
||||
value = next(self.input)
|
||||
except StopIteration:
|
||||
raise messaging.NoMoreMessages(stream=self)
|
||||
return decoder.decode(json.dumps(value))
|
||||
value = decoder.decode(json.dumps(value))
|
||||
self._log_message("-->", value)
|
||||
return value
|
||||
|
||||
def write_json(self, value, encoder=None):
|
||||
encoder = encoder if encoder is not None else self.json_encoder_factory()
|
||||
value = json.loads(encoder.encode(value))
|
||||
self._log_message("<--", value)
|
||||
self.output.append(value)
|
||||
|
||||
|
||||
|
|
@ -67,7 +77,9 @@ class TestJsonIOStream(object):
|
|||
def setup_class(cls):
|
||||
for seq in range(0, 3):
|
||||
message_body = cls.MESSAGE_BODY_TEMPLATE % seq
|
||||
message = json.loads(message_body, object_pairs_hook=collections.OrderedDict)
|
||||
message = json.loads(
|
||||
message_body, object_pairs_hook=collections.OrderedDict
|
||||
)
|
||||
message_body = message_body.encode("utf-8")
|
||||
cls.MESSAGES.append(message)
|
||||
message_header = "Content-Length: %d\r\n\r\n" % len(message_body)
|
||||
|
|
@ -115,6 +127,48 @@ class TestJsonMemoryStream(object):
|
|||
assert messages == self.MESSAGES
|
||||
|
||||
|
||||
class MessageHandlerRecorder(list):
|
||||
def __call__(self, handler):
|
||||
@functools.wraps(handler)
|
||||
def record_and_handle(instance, message):
|
||||
name = handler.__name__
|
||||
if isinstance(name, bytes):
|
||||
name = name.decode("utf-8")
|
||||
record = {"channel": message.channel, "handler": name}
|
||||
|
||||
if isinstance(message, messaging.Event):
|
||||
record.update(
|
||||
{"type": "event", "event": message.event, "body": message.body}
|
||||
)
|
||||
elif isinstance(message, messaging.Request):
|
||||
record.update(
|
||||
{
|
||||
"type": "request",
|
||||
"command": message.command,
|
||||
"arguments": message.arguments,
|
||||
}
|
||||
)
|
||||
|
||||
self.append(record)
|
||||
return handler(instance, message)
|
||||
|
||||
return record_and_handle
|
||||
|
||||
def expect(self, channel, inputs, handlers):
|
||||
expected_records = []
|
||||
for input, handler in zip(inputs, handlers):
|
||||
expected_record = {"channel": channel, "handler": handler}
|
||||
expected_record.update(
|
||||
{
|
||||
key: value
|
||||
for key, value in input.items()
|
||||
if key in ("type", "event", "command", "body", "arguments")
|
||||
}
|
||||
)
|
||||
expected_records.append(expected_record)
|
||||
assert expected_records == self
|
||||
|
||||
|
||||
class TestJsonMessageChannel(object):
|
||||
@staticmethod
|
||||
def iter_with_event(collection):
|
||||
|
|
@ -146,26 +200,23 @@ class TestJsonMessageChannel(object):
|
|||
},
|
||||
]
|
||||
|
||||
events_received = []
|
||||
recorder = MessageHandlerRecorder()
|
||||
|
||||
class Handlers(object):
|
||||
@recorder
|
||||
def stopped_event(self, event):
|
||||
assert event.event == "stopped"
|
||||
events_received.append((event.channel, event.body))
|
||||
|
||||
@recorder
|
||||
def event(self, event):
|
||||
events_received.append((event.channel, event.event, event.body))
|
||||
assert event.event == "unknown"
|
||||
|
||||
input, input_exhausted = self.iter_with_event(EVENTS)
|
||||
stream = JsonMemoryStream(input, [])
|
||||
stream = JsonMemoryStream(EVENTS, [])
|
||||
channel = messaging.JsonMessageChannel(stream, Handlers())
|
||||
channel.start()
|
||||
input_exhausted.wait()
|
||||
channel.wait()
|
||||
|
||||
assert events_received == [
|
||||
(channel, EVENTS[0]["body"]),
|
||||
(channel, "unknown", EVENTS[1]["body"]),
|
||||
]
|
||||
recorder.expect(channel, EVENTS, ["stopped_event", "event"])
|
||||
|
||||
def test_requests(self):
|
||||
REQUESTS = [
|
||||
|
|
@ -178,50 +229,59 @@ class TestJsonMessageChannel(object):
|
|||
{
|
||||
"seq": 2,
|
||||
"type": "request",
|
||||
"command": "launch",
|
||||
"arguments": {"program": "main.py"},
|
||||
},
|
||||
{
|
||||
"seq": 3,
|
||||
"type": "request",
|
||||
"command": "unknown",
|
||||
"arguments": {"answer": 42},
|
||||
},
|
||||
{
|
||||
"seq": 3,
|
||||
"seq": 4,
|
||||
"type": "request",
|
||||
"command": "pause",
|
||||
"arguments": {"threadId": 5},
|
||||
},
|
||||
]
|
||||
|
||||
requests_received = []
|
||||
recorder = MessageHandlerRecorder()
|
||||
|
||||
class Handlers(object):
|
||||
@recorder
|
||||
def next_request(self, request):
|
||||
assert request.command == "next"
|
||||
requests_received.append((request.channel, request.arguments))
|
||||
return {"threadId": 7}
|
||||
|
||||
def request(self, request):
|
||||
requests_received.append(
|
||||
(request.channel, request.command, request.arguments)
|
||||
)
|
||||
return {}
|
||||
@recorder
|
||||
def launch_request(self, request):
|
||||
assert request.command == "launch"
|
||||
self._launch = request
|
||||
return messaging.NO_RESPONSE
|
||||
|
||||
@recorder
|
||||
def request(self, request):
|
||||
request.respond({})
|
||||
|
||||
@recorder
|
||||
def pause_request(self, request):
|
||||
assert request.command == "pause"
|
||||
requests_received.append((request.channel, request.arguments))
|
||||
request.cant_handle("pause error")
|
||||
self._launch.respond({"processId": 9})
|
||||
raise request.cant_handle("pause error")
|
||||
|
||||
input, input_exhausted = self.iter_with_event(REQUESTS)
|
||||
output = []
|
||||
stream = JsonMemoryStream(input, output)
|
||||
stream = JsonMemoryStream(REQUESTS, [])
|
||||
channel = messaging.JsonMessageChannel(stream, Handlers())
|
||||
channel.start()
|
||||
input_exhausted.wait()
|
||||
channel.wait()
|
||||
|
||||
assert requests_received == [
|
||||
(channel, REQUESTS[0]["arguments"]),
|
||||
(channel, "unknown", REQUESTS[1]["arguments"]),
|
||||
(channel, REQUESTS[2]["arguments"]),
|
||||
]
|
||||
recorder.expect(
|
||||
channel,
|
||||
REQUESTS,
|
||||
["next_request", "launch_request", "request", "pause_request"],
|
||||
)
|
||||
|
||||
assert output == [
|
||||
assert stream.output == [
|
||||
{
|
||||
"seq": 1,
|
||||
"type": "response",
|
||||
|
|
@ -233,14 +293,22 @@ class TestJsonMessageChannel(object):
|
|||
{
|
||||
"seq": 2,
|
||||
"type": "response",
|
||||
"request_seq": 2,
|
||||
"request_seq": 3,
|
||||
"command": "unknown",
|
||||
"success": True,
|
||||
},
|
||||
{
|
||||
"seq": 3,
|
||||
"type": "response",
|
||||
"request_seq": 3,
|
||||
"request_seq": 2,
|
||||
"command": "launch",
|
||||
"success": True,
|
||||
"body": {"processId": 9},
|
||||
},
|
||||
{
|
||||
"seq": 4,
|
||||
"type": "response",
|
||||
"request_seq": 4,
|
||||
"command": "pause",
|
||||
"success": False,
|
||||
"message": "pause error",
|
||||
|
|
@ -251,6 +319,7 @@ class TestJsonMessageChannel(object):
|
|||
request1_sent = threading.Event()
|
||||
request2_sent = threading.Event()
|
||||
request3_sent = threading.Event()
|
||||
request4_sent = threading.Event()
|
||||
|
||||
def iter_responses():
|
||||
request1_sent.wait()
|
||||
|
|
@ -283,6 +352,8 @@ class TestJsonMessageChannel(object):
|
|||
"body": {"threadId": 5},
|
||||
}
|
||||
|
||||
request4_sent.wait()
|
||||
|
||||
stream = JsonMemoryStream(iter_responses(), [])
|
||||
channel = messaging.JsonMessageChannel(stream, None)
|
||||
channel.start()
|
||||
|
|
@ -290,6 +361,7 @@ class TestJsonMessageChannel(object):
|
|||
# Blocking wait.
|
||||
request1 = channel.send_request("next")
|
||||
request1_sent.set()
|
||||
log.info("Waiting for response...")
|
||||
response1_body = request1.wait_for_response()
|
||||
response1 = request1.response
|
||||
|
||||
|
|
@ -307,8 +379,11 @@ class TestJsonMessageChannel(object):
|
|||
response2.append(resp)
|
||||
response2_received.set()
|
||||
|
||||
log.info("Registering callback")
|
||||
request2.on_response(response2_handler)
|
||||
request2_sent.set()
|
||||
|
||||
log.info("Waiting for callback...")
|
||||
response2_received.wait()
|
||||
response2, = response2
|
||||
|
||||
|
|
@ -330,7 +405,10 @@ class TestJsonMessageChannel(object):
|
|||
response3.append(resp)
|
||||
response3_received.set()
|
||||
|
||||
log.info("Registering callback")
|
||||
request3.on_response(response3_handler)
|
||||
|
||||
log.info("Waiting for callback...")
|
||||
response3_received.wait()
|
||||
response3, = response3
|
||||
|
||||
|
|
@ -339,229 +417,28 @@ class TestJsonMessageChannel(object):
|
|||
assert response3 is request3.response
|
||||
assert response3.body == {"threadId": 5}
|
||||
|
||||
def test_yield(self):
|
||||
REQUESTS = [
|
||||
{
|
||||
"seq": 10,
|
||||
"type": "request",
|
||||
"command": "launch",
|
||||
"arguments": {"noDebug": False},
|
||||
},
|
||||
{
|
||||
"seq": 20,
|
||||
"type": "request",
|
||||
"command": "setBreakpoints",
|
||||
"arguments": {"main.py": 1},
|
||||
},
|
||||
{"seq": 30, "type": "event", "event": "expected"},
|
||||
{
|
||||
"seq": 40,
|
||||
"type": "request",
|
||||
"command": "launch",
|
||||
"arguments": {"noDebug": True},
|
||||
}, # test re-entrancy
|
||||
{
|
||||
"seq": 50,
|
||||
"type": "request",
|
||||
"command": "setBreakpoints",
|
||||
"arguments": {"main.py": 2},
|
||||
},
|
||||
{"seq": 60, "type": "event", "event": "unexpected"},
|
||||
{"seq": 80, "type": "request", "command": "configurationDone"},
|
||||
{
|
||||
"seq": 90,
|
||||
"type": "request",
|
||||
"command": "launch",
|
||||
}, # test handler yielding empty body
|
||||
]
|
||||
# Async callback, registered after channel is closed.
|
||||
request4 = channel.send_request("next")
|
||||
request4_sent.set()
|
||||
channel.wait()
|
||||
response4 = []
|
||||
response4_received = threading.Event()
|
||||
|
||||
class Handlers(object):
|
||||
def response4_handler(resp):
|
||||
response4.append(resp)
|
||||
response4_received.set()
|
||||
|
||||
received = {
|
||||
"launch": 0,
|
||||
"setBreakpoints": 0,
|
||||
"configurationDone": 0,
|
||||
"expected": 0,
|
||||
"unexpected": 0,
|
||||
}
|
||||
log.info("Registering callback")
|
||||
request4.on_response(response4_handler)
|
||||
|
||||
def launch_request(self, request):
|
||||
assert request.seq in (10, 40, 90)
|
||||
self.received["launch"] += 1
|
||||
log.info("Waiting for callback...")
|
||||
response4_received.wait()
|
||||
response4, = response4
|
||||
|
||||
if request.seq == 10: # launch #1
|
||||
assert self.received == {
|
||||
"launch": 1,
|
||||
"setBreakpoints": 0,
|
||||
"configurationDone": 0,
|
||||
"expected": 0,
|
||||
"unexpected": 0,
|
||||
}
|
||||
|
||||
msg = yield # setBreakpoints #1
|
||||
assert msg.seq == 20
|
||||
assert self.received == {
|
||||
"launch": 1,
|
||||
"setBreakpoints": 1,
|
||||
"configurationDone": 0,
|
||||
"expected": 0,
|
||||
"unexpected": 0,
|
||||
}
|
||||
|
||||
msg = yield # expected
|
||||
assert msg.seq == 30
|
||||
assert self.received == {
|
||||
"launch": 1,
|
||||
"setBreakpoints": 1,
|
||||
"configurationDone": 0,
|
||||
"expected": 1,
|
||||
"unexpected": 0,
|
||||
}
|
||||
|
||||
msg = yield # launch #2 + nested messages
|
||||
assert msg.seq == 40
|
||||
assert self.received == {
|
||||
"launch": 2,
|
||||
"setBreakpoints": 2,
|
||||
"configurationDone": 0,
|
||||
"expected": 1,
|
||||
"unexpected": 1,
|
||||
}
|
||||
|
||||
# We should see that it failed, but no exception bubbling up here.
|
||||
assert not msg.response.success
|
||||
assert msg.response.body == messaging.MessageHandlingError(
|
||||
"test failure", msg
|
||||
)
|
||||
|
||||
msg = yield # configurationDone
|
||||
assert msg.seq == 80
|
||||
assert self.received == {
|
||||
"launch": 2,
|
||||
"setBreakpoints": 2,
|
||||
"configurationDone": 1,
|
||||
"expected": 1,
|
||||
"unexpected": 1,
|
||||
}
|
||||
|
||||
yield {"answer": 42}
|
||||
|
||||
elif request.seq == 40: # launch #1
|
||||
assert self.received == {
|
||||
"launch": 2,
|
||||
"setBreakpoints": 1,
|
||||
"configurationDone": 0,
|
||||
"expected": 1,
|
||||
"unexpected": 0,
|
||||
}
|
||||
|
||||
msg = yield # setBreakpoints #2
|
||||
assert msg.seq == 50
|
||||
assert self.received == {
|
||||
"launch": 2,
|
||||
"setBreakpoints": 2,
|
||||
"configurationDone": 0,
|
||||
"expected": 1,
|
||||
"unexpected": 0,
|
||||
}
|
||||
|
||||
msg = yield # unexpected
|
||||
assert msg.seq == 60
|
||||
assert self.received == {
|
||||
"launch": 2,
|
||||
"setBreakpoints": 2,
|
||||
"configurationDone": 0,
|
||||
"expected": 1,
|
||||
"unexpected": 1,
|
||||
}
|
||||
|
||||
request.cant_handle("test failure")
|
||||
|
||||
elif request.seq == 90: # launch #3
|
||||
assert self.received == {
|
||||
"launch": 3,
|
||||
"setBreakpoints": 2,
|
||||
"configurationDone": 1,
|
||||
"expected": 1,
|
||||
"unexpected": 1,
|
||||
}
|
||||
# yield {}
|
||||
|
||||
def setBreakpoints_request(self, request):
|
||||
assert request.seq in (20, 50, 70)
|
||||
self.received["setBreakpoints"] += 1
|
||||
return {"which": self.received["setBreakpoints"]}
|
||||
|
||||
def request(self, request):
|
||||
assert request.seq == 80
|
||||
assert request.command == "configurationDone"
|
||||
self.received["configurationDone"] += 1
|
||||
return {}
|
||||
|
||||
def expected_event(self, event):
|
||||
assert event.seq == 30
|
||||
self.received["expected"] += 1
|
||||
|
||||
def event(self, event):
|
||||
assert event.seq == 60
|
||||
assert event.event == "unexpected"
|
||||
self.received["unexpected"] += 1
|
||||
|
||||
input, input_exhausted = self.iter_with_event(REQUESTS)
|
||||
output = []
|
||||
stream = JsonMemoryStream(input, output)
|
||||
channel = messaging.JsonMessageChannel(stream, Handlers())
|
||||
channel.start()
|
||||
input_exhausted.wait()
|
||||
|
||||
assert output == [
|
||||
{
|
||||
"seq": 1,
|
||||
"type": "response",
|
||||
"request_seq": 20,
|
||||
"command": "setBreakpoints",
|
||||
"success": True,
|
||||
"body": {"which": 1},
|
||||
},
|
||||
{
|
||||
"seq": 2,
|
||||
"type": "response",
|
||||
"request_seq": 50,
|
||||
"command": "setBreakpoints",
|
||||
"success": True,
|
||||
"body": {"which": 2},
|
||||
},
|
||||
{
|
||||
"seq": 3,
|
||||
"type": "response",
|
||||
"request_seq": 40,
|
||||
"command": "launch",
|
||||
"success": False,
|
||||
"message": "test failure",
|
||||
},
|
||||
{
|
||||
"seq": 4,
|
||||
"type": "response",
|
||||
"request_seq": 80,
|
||||
"command": "configurationDone",
|
||||
"success": True,
|
||||
},
|
||||
{
|
||||
"seq": 5,
|
||||
"type": "response",
|
||||
"request_seq": 10,
|
||||
"command": "launch",
|
||||
"success": True,
|
||||
"body": {"answer": 42},
|
||||
},
|
||||
{
|
||||
"seq": 6,
|
||||
"type": "response",
|
||||
"request_seq": 90,
|
||||
"command": "launch",
|
||||
"success": True,
|
||||
},
|
||||
]
|
||||
assert not response4.success
|
||||
assert response4.request is request4
|
||||
assert response4 is request4.response
|
||||
assert isinstance(response4.body, messaging.NoMoreMessages)
|
||||
|
||||
def test_invalid_request_handling(self):
|
||||
REQUESTS = [
|
||||
|
|
@ -587,12 +464,11 @@ class TestJsonMessageChannel(object):
|
|||
def pause_request(self, request):
|
||||
request.arguments["DDD"]
|
||||
|
||||
input, input_exhausted = self.iter_with_event(REQUESTS)
|
||||
output = []
|
||||
stream = JsonMemoryStream(input, output)
|
||||
stream = JsonMemoryStream(REQUESTS, output)
|
||||
channel = messaging.JsonMessageChannel(stream, Handlers())
|
||||
channel.start()
|
||||
input_exhausted.wait()
|
||||
channel.wait()
|
||||
|
||||
def missing_property(name):
|
||||
return some.str.matching("Invalid message:.*" + re.escape(name) + ".*")
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ def test_package_launch():
|
|||
test_py = cwd / "pkg1" / "__main__.py"
|
||||
|
||||
with debug.Session("launch") as session:
|
||||
session.expected_returncode = 42
|
||||
session.initialize(target=("module", "pkg1"), cwd=cwd)
|
||||
session.set_breakpoints(test_py, ["two"])
|
||||
session.start_debugging()
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import sys
|
||||
print('one') # @one
|
||||
print('two') # @two
|
||||
print('three') # @three
|
||||
sys.exit(42)
|
||||
Loading…
Add table
Add a link
Reference in a new issue