Skip to content

Commit f184e4e

Browse files
committed
Add some comments describing our protocol
Ensure all sent messages conform to the protocol. Tell future maintainers what to update if the protocol changes.
1 parent 5a1755b commit f184e4e

File tree

1 file changed

+91
-8
lines changed

1 file changed

+91
-8
lines changed

Lib/pdb.py

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2539,7 +2539,48 @@ def protocol_version():
25392539
revision = 0
25402540
return int(f"{v.major:02X}{v.minor:02X}{revision:02X}F0", 16)
25412541

2542-
def _send(self, **kwargs) -> None:
2542+
def _ensure_valid_message(self, msg):
2543+
# Ensure the message conforms to our protocol.
2544+
# If anything needs to be changed here for a patch release of Python,
2545+
# the 'revision' in protocol_version() should be updated.
2546+
match msg:
2547+
case {"message": str(), "type": str()}:
2548+
# Have the client show a message. The client chooses how to
2549+
# format the message based on its type. The currently defined
2550+
# types are "info" and "error". If a message has a type the
2551+
# client doesn't recognize, it must be treated as "info".
2552+
pass
2553+
case {"help": str()}:
2554+
# Have the client show the help for a given argument.
2555+
pass
2556+
case {"prompt": str(), "state": str()}:
2557+
# Have the client display the given prompt and wait for a reply
2558+
# from the user. If the client recognizes the state it may
2559+
# enable mode-specific features like multi-line editing.
2560+
# If it doesn't recognize the state it must prompt for a single
2561+
# line only and send it directly to the server. A server won't
2562+
# progress until it gets a "reply" or "signal" message, but can
2563+
# process "complete" requests while waiting for the reply.
2564+
pass
2565+
case {
2566+
"completions": list(completions)
2567+
} if all(isinstance(c, str) for c in completions):
2568+
# Return valid completions for a client's "complete" request.
2569+
pass
2570+
case {
2571+
"command_list": list(command_list)
2572+
} if all(isinstance(c, str) for c in command_list):
2573+
# Report the list of legal PDB commands to the client.
2574+
# Due to aliases this list is not static, but the client
2575+
# needs to know it for multi-line editing.
2576+
pass
2577+
case _:
2578+
raise AssertionError(
2579+
f"PDB message doesn't follow the schema! {msg}"
2580+
)
2581+
2582+
def _send(self, **kwargs):
2583+
self._ensure_valid_message(kwargs)
25432584
json_payload = json.dumps(kwargs)
25442585
try:
25452586
self._sockfile.write(json_payload.encode() + b"\n")
@@ -2774,6 +2815,51 @@ def __init__(self, pid, sockfile, interrupt_script):
27742815
self.pdb_commands = set()
27752816
self.completion_matches = []
27762817
self.state = "dumb"
2818+
self.write_failed = False
2819+
2820+
def _ensure_valid_message(self, msg):
2821+
# Ensure the message conforms to our protocol.
2822+
# If anything needs to be changed here for a patch release of Python,
2823+
# the 'revision' in protocol_version() should be updated.
2824+
match msg:
2825+
case {"reply": str()}:
2826+
# Send input typed by a user at a prompt to the remote PDB.
2827+
pass
2828+
case {"signal": "EOF"}:
2829+
# Tell the remote PDB that the user pressed ^D at a prompt.
2830+
pass
2831+
case {"signal": "INT"}:
2832+
# Tell the remote PDB that the user pressed ^C at a prompt.
2833+
pass
2834+
case {
2835+
"complete": {
2836+
"text": str(),
2837+
"line": str(),
2838+
"begidx": int(),
2839+
"endidx": int(),
2840+
}
2841+
}:
2842+
# Ask the remote PDB what completions are valid for the given
2843+
# parameters, using readline's completion protocol.
2844+
pass
2845+
case _:
2846+
raise AssertionError(
2847+
f"PDB message doesn't follow the schema! {msg}"
2848+
)
2849+
2850+
def _send(self, **kwargs):
2851+
self._ensure_valid_message(kwargs)
2852+
json_payload = json.dumps(kwargs)
2853+
try:
2854+
self.sockfile.write(json_payload.encode() + b"\n")
2855+
self.sockfile.flush()
2856+
except OSError:
2857+
# This means that the client has abruptly disconnected, but we'll
2858+
# handle that the next time we try to read from the client instead
2859+
# of trying to handle it from everywhere _send() may be called.
2860+
# Track this with a flag rather than assuming readline() will ever
2861+
# return an empty string because the socket may be half-closed.
2862+
self.write_failed = True
27772863

27782864
def read_command(self, prompt):
27792865
reply = input(prompt)
@@ -2829,7 +2915,7 @@ def readline_completion(self, completer):
28292915

28302916
def cmdloop(self):
28312917
with self.readline_completion(self.complete):
2832-
while True:
2918+
while not self.write_failed:
28332919
try:
28342920
if not (payload_bytes := self.sockfile.readline()):
28352921
break
@@ -2889,8 +2975,7 @@ def prompt_for_reply(self, prompt):
28892975
print("***", msg, flush=True)
28902976
continue
28912977

2892-
self.sockfile.write((json.dumps(payload) + "\n").encode())
2893-
self.sockfile.flush()
2978+
self._send(**payload)
28942979
return
28952980

28962981
def complete(self, text, state):
@@ -2916,10 +3001,8 @@ def complete(self, text, state):
29163001
}
29173002
}
29183003

2919-
try:
2920-
self.sockfile.write((json.dumps(msg) + "\n").encode())
2921-
self.sockfile.flush()
2922-
except OSError:
3004+
self._send(**msg)
3005+
if self.write_failed:
29233006
return None
29243007

29253008
payload = self.sockfile.readline()

0 commit comments

Comments
 (0)