mirror of
https://github.com/servo/servo
synced 2026-04-25 17:15:48 +02:00
Rename the `--scan` argument to `-w/--write-file` and the `--use` argument to `-r/--read-file`. This is more aligned with `tshark`'s syntax, and I think it is a more intuitive naming scheme. Remove the `--filter` and `--range` arguments. They are very easily replaced by more powerful tools like `grep` and `jq`. It seems unnecessary to have them in this script (specially when the most useful thing it does is exporting the capture as NDJSON for other tools to process). This fixes an issue with the last message not being exported. Change the default port to `6080`. This matches the current information [in the book](https://book.servo.org/hacking/using-devtools.html#connecting-to-servo) on how to run Servo with DevTools enabled. Testing: Checked with `math test-scripts` --------- Signed-off-by: eri <eri@igalia.com>
196 lines
7.7 KiB
Python
Executable File
196 lines
7.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
# This is a script designed to easily debug devtools messages
|
|
# It takes the content of a pcap wireshark capture (or creates a new
|
|
# one when using `-w`) and prints the JSON payloads.
|
|
#
|
|
# Wireshark (more specifically its cli tool tshark) needs to be installed
|
|
# for this script to work. Go to https://tshark.dev/setup/install for a
|
|
# comprehensive guide on how to install it. In short:
|
|
#
|
|
# Linux (Debian based): apt install tshark
|
|
# Linux (Arch based): pacman -Sy wireshark-cli
|
|
# MacOS (With homebrew): brew install --cask wireshark
|
|
# Windows (With chocolatey): choco install wireshark
|
|
#
|
|
# To use it, launch Servo or Firefox in devtools mode:
|
|
#
|
|
# Servo: ./mach run --devtools 6080
|
|
# Firefox: firefox --new-instance --start-debugger-server 6080 --profile PROFILE
|
|
#
|
|
# Then run this tool in capture mode, specifying the same port as before:
|
|
#
|
|
# ./devtools_parser.py -w capture.pcap -p 6080
|
|
#
|
|
# Finally, open another instance of Firefox, go to about:debugging and connect
|
|
# to localhost:6080. Messages should start popping up. The scan can be finished
|
|
# by pressing Ctrl+C. After that the messages will be printed.
|
|
#
|
|
# To review the results of the scan use the `-r` flag. It is possible to output
|
|
# newline-delimited JSON for further processing with other tools using the
|
|
# `--json` flag.
|
|
#
|
|
# ./devtools_parser.py -r capture.pcap --json > capture.json
|
|
|
|
import json
|
|
import signal
|
|
import sys
|
|
from argparse import ArgumentParser
|
|
from subprocess import Popen, PIPE
|
|
|
|
try:
|
|
from termcolor import colored
|
|
except ImportError:
|
|
|
|
def colored(text, *args, **kwargs):
|
|
return text
|
|
|
|
|
|
def tshark(args, wait=False):
|
|
"""Run a tshark command with the specified arguments.
|
|
Some custom arguments are always prepended to set the stdout format: date, port, hex-encoded data.
|
|
If `wait` is True, the main process will pause until SIGINT is triggered. This action will stop the analysis and continue execution."""
|
|
|
|
cmd = ["tshark", "-T", "fields", "-e", "frame.time", "-e", "tcp.srcport", "-e", "tcp.payload"] + args
|
|
process = Popen(cmd, stdout=PIPE, encoding="utf-8")
|
|
|
|
if wait:
|
|
signal.signal(signal.SIGINT, lambda _signal, _frame: process.send_signal(signal.SIGINT))
|
|
signal.pause()
|
|
|
|
return process.communicate()[0]
|
|
|
|
|
|
def process_data(input):
|
|
"""Transform the raw output of tshark stdout into a manageable list."""
|
|
|
|
# Split the input into lines.
|
|
# `input` = newline-terminated lines of tab-delimited tshark(1) output
|
|
lines = [line.split("\t") for line in input.split("\n")]
|
|
|
|
# Remove empty lines and empty messages, and decode hex to bytes.
|
|
# `lines` = [[date, port, hex-encoded data]], e.g.
|
|
# `["2025-11-04T16:01:38.013100950+0100", "6080", "3133"]`
|
|
# `["2025-11-04T16:01:38.013100950+0100", "6080", "393a"]`
|
|
# `["2025-11-04T16:01:38.013100950+0100", "6080", "7b..."]`
|
|
messages = []
|
|
for line in lines:
|
|
if len(line) != 3:
|
|
continue
|
|
time, port, data = line
|
|
if len(data) == 0:
|
|
continue
|
|
elif len(data) % 2 == 1:
|
|
print(f"[WARNING] Extra byte in hex-encoded data: {data[-1]}", file=sys.stderr)
|
|
data = data[:-1]
|
|
if len(messages) > 0 and messages[-1][1] == port:
|
|
messages[-1][2] += bytearray.fromhex(data)
|
|
else:
|
|
messages.append([time, port, bytearray.fromhex(data)])
|
|
|
|
# Split and merge consecutive messages with the same port, to yield exactly one record per message.
|
|
# Message records are of the form `length:{...}`, where `length` is an integer in ASCII decimal.
|
|
# Incomplete messages are deferred until they are complete.
|
|
# `sends` = [[date, port, record data]], e.g.
|
|
# `["2025-11-04T16:01:38.013100950+0100", "6080", b"13"]`
|
|
# `["2025-11-04T16:01:38.013100950+0100", "6080", b"9:"]`
|
|
# `["2025-11-04T16:01:38.013100950+0100", "6080", b"{..."]`
|
|
# `["2025-11-04T16:01:38.013100950+0100", "6080", b"...}"]`
|
|
records = []
|
|
scunge = {} # Map from port to incomplete message data
|
|
for time, port, rest in messages:
|
|
rest = scunge.pop(port, b"") + rest
|
|
while rest != b"":
|
|
try:
|
|
length, new_rest = rest.split(b":", 1) # Can raise ValueError
|
|
length = int(length)
|
|
if len(new_rest) < length:
|
|
raise ValueError("Incomplete message (for now)")
|
|
# If we found a `length:` prefix and we have enough data to satisfy it,
|
|
# cut off the prefix so `rest` is just `{...}length:{...}length:{...}`.
|
|
rest = new_rest
|
|
except ValueError:
|
|
print(f"[WARNING] Incomplete message detected (will try to reassemble): {repr(rest)}", file=sys.stderr)
|
|
scunge[port] = rest
|
|
# Wait for more data from later sends, potentially after sends with the other port.
|
|
break
|
|
# Cut off the message so `rest` is just `length:{...}length:{...}`.
|
|
message = rest[:length]
|
|
rest = rest[length:]
|
|
try:
|
|
records.append([time, message.decode()])
|
|
except UnicodeError as e:
|
|
print(f"[WARNING] Failed to decode message as UTF-8: {e}")
|
|
continue
|
|
|
|
# Return enumerated records.
|
|
# `records` = [[date, message text]], e.g.
|
|
# `["2025-11-04T16:01:38.013100950+0100", "{...}"]`
|
|
# `return` = [[date, message text, index]], e.g.
|
|
# `["2025-11-04T16:01:38.013100950+0100", "{...}", 0]`
|
|
return [(*line, i) for i, line in enumerate(records)]
|
|
|
|
|
|
def parse_message(msg, json_output=False):
|
|
"""Pretty print the JSON message, actor and timestamp.
|
|
If `json_output` is True, output the JSON message in one line instead."""
|
|
|
|
time, data, i = msg
|
|
|
|
try:
|
|
content = json.loads(data)
|
|
except json.JSONDecodeError:
|
|
print(f"Warning: Couldn't decode json\n{data}")
|
|
return
|
|
|
|
if json_output:
|
|
# Place from and to at the start so that it is easier to see which actor is involved
|
|
sorted_content = dict(
|
|
sorted(content.items(), key=lambda k: f"_{k[0]}" if k[0] == "from" or k[0] == "to" else k[0])
|
|
)
|
|
print(json.dumps(sorted_content))
|
|
return
|
|
|
|
is_server = "from" in content
|
|
colored_sender = (
|
|
colored("Server", "black", "on_yellow") if is_server else colored("Client", "on_magenta", attrs=["bold"])
|
|
)
|
|
pretty_json = json.dumps(content, sort_keys=True, indent=4)
|
|
|
|
print(f"""
|
|
{colored_sender} - {colored(i, "blue")} - {colored(time, "dark_grey")}
|
|
{pretty_json}
|
|
""")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Program arguments
|
|
parser = ArgumentParser()
|
|
parser.add_argument("-p", "--port", default="6080", help="the port where the devtools client is running")
|
|
parser.add_argument("--json", action="store_true", help="output in newline-delimited JSON (NDJSON)")
|
|
|
|
actions = parser.add_mutually_exclusive_group(required=True)
|
|
actions.add_argument(
|
|
"-w", "--write-file", help="capture messages on the specified port and write the output to a .pcap file"
|
|
)
|
|
actions.add_argument("-r", "--read-file", help="parse the captured messages from a .pcap file")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Run tshark, either to start a capture or to read an already existing pcap file
|
|
if args.write_file:
|
|
capture_args = ["-i", "lo", "-f", f"tcp port {args.port}", "-w", args.write_file]
|
|
data = tshark(capture_args, wait=True)
|
|
else:
|
|
read_args = ["-r", args.read_file]
|
|
data = tshark(read_args)
|
|
|
|
data = process_data(data)
|
|
|
|
for msg in data:
|
|
parse_message(msg, json_output=args.json)
|