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:
Pavel Minaev 2019-08-04 21:57:55 -07:00 committed by Pavel Minaev
parent 7256e99b52
commit 981b1d1559
12 changed files with 932 additions and 823 deletions

4
.vscode/launch.json vendored
View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) + ".*")

View file

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

View file

@ -1,3 +1,5 @@
import sys
print('one') # @one
print('two') # @two
print('three') # @three
sys.exit(42)