Using .NET to interact with the Chrome V8 debugger (running in ClearScript)
Exploring the Google Chrome debugger protocol by trying to talk to ClearScript running in V8.
An enterprise app I develop & maintain makes use of ClearScript as an extensibility point: end-users (semi-technical) write financial algorithms in JavaScript and those algorithms are evaluated by the system during batch processing (certain key C# types are exposed that the algorithms can make use of). ClearScript is set up to run the JavaScript against the Chrome “V8” JavaScript engine (the same as NodeJS and, well, Chrome).
Some of these algorithms are quite complex and time consuming to write - the question arose “Can we provide a way for end-users to debug the algorithms”?
As a minimum-viable-product I decided users would need the ability to
- Set breakpoints
- Inspect the value of variables
- Step over/into code
- Modify the value of variables in-place (nice-to-have)
As noted my StackOverflow question, whilst I’m (as a developer) able to connect to the V8 engine w/ Chrome DevTools, that’s not ideal for end-users for a couple of reasons:
- Tricky to set up / not integrated with the rest of the web app
- Probably won’t play nicely with corporate firewall (every simultaneous instance of the debugger will have to listen on a unique port; That’s another port we’d need to let through the firewall).
I was therefore wondering - is there a .NET interface to the Chrome V8 debugger? I didn’t find one so set out about building one.
SIDE NOTE: After building mine, I found chrome-dev-tools-runtime - it looks pretty similar to what I implemented (but more/less elegant in places). Note that as well as a concrete implementation, the same author (BaristaLabs) offers a generator for creating DTOs for the various messages/events for the protocol (the protocol is constantly evolving as Chrome evolves). That should be less of an issue when targetting ClearScript though.
Even after reading what I could find in terms of official documtation here & here I still didn’t really understand how the protocol works. With a bit of Fiddler + Wireshark and general prodding I managed to talk to it. Here’s what I found:
- It’s primarily a websocket based protocol (asynchronous JSON messages). That said, the initial setup of the session needs to be done through HTTP (I’m not referring to the websocket protocol escalation).
- Client code can interact with the debugger by sending commands & (asyncronously) waiting for a response (commands have an
id
property which is used a sequence number to correlate requests and responses). - The browser/debugger also publishes events to the same websocket - that slightly complicates processing of messages (since any given message could either be a response to a command or an event)
In the case of ClearScript hosted V8, the initial setup was as simple as making a GET
to http://{hostname}:{port}/json/list
- that returns a list of open “tabs”. In a ClearScript scenario (as opposed to real Chrome) I think we’ll only ever have one tab. The tab DTO has an id
property - we need the id
to setup the WebSocket:
await _socket.ConnectAsync(new Uri($"ws://{_hostname}:{_port}/{tabInfo.Id}"), cancellationToken);
Issuing a command is as simple as serializing the command to JSON and sending it to the websocket. The websocket should be read periodically for command responses & events (also JSON).
Other miscellaneous information:
- When ClearScript evaluates a script, a
debugger
statement won’t do anything unless there’s a debugger attached (just like real Chrome needs DevTools open). Use the flagsEnableDebugging
andAwaitDebuggerAndPauseOnStart
V8ScriptEngineFlags flags = V8ScriptEngineFlags.EnableDebugging |
V8ScriptEngineFlags.AwaitDebuggerAndPauseOnStart;
new V8ScriptEngine(flags);
- A client connecting to the debugger (via websocket) is not enough to satisfy
AwaitDebuggerAndPauseOnStart
- the client must issue the commandsDebugger.enable
&Runtime.runIfWaitingForDebugger
- after that point, even if the client disconnects the script will continue to completion. - There is an experimental feature in Chrome Devtools whereby you can get DevTools to show the debugger protocol commands it is executing (see screenshot below). Extremely handy to know about!
Some useful commands:
Runtime.enable
Debugger.enable
Runtime.runIfWaitingForDebugger
Debugger.getScriptSource
Debugger.resume
Debugger.evaluateOnCallFrame
Runtime.getProperties
I’ve built a Communicator
class to abstract the V8 debugger interactions. The interface looks as follows:
public interface ICommunicator
{
Task Connect(CancellationToken cancellationToken);
Task<string> SendCommand(string method, object parameters = null);
Task<TEvent> WaitForEventAsync<TEvent>(CancellationToken token) where TEvent : IV8EventParameters;
}
Consuming could would instantiate the communicator (in a using
block), Connect
to it, invoke SendCommand
several times, possibly wait on the DebuggerPaused
/ ScriptParsed
events and finally Disconnect
.
The implementation of Communicator
makes use of MessagePump
(a task running on a separate thread to poll & read messages (both events and responses to commands) from the websocket. The MessagePump
raises events which the Communicator
handles.
Things get a little bit fruity in the Communicator
class - it creates three channels (one for each of the event types + another for command responses) and adds event handlers that write to the appropriate channel. The implementation of WaitForDebuggerPausedEventAsync
and WaitForScriptParsedEventAsync
is trivial - we just read asyncronously from the appropriate channel. Obviously, this approach wouldn’t scale if we were interested in multiple types of events. Calling could would probably like a generic WaitForEventAsync<TEvent>
method anyway. I’m not sure how you’d achieve that.
UPDATE: 3rd September 2019: I’ve refactored things to remove the channels from the Communicator
class - instead, the code now maintains a dictionary of buffered events as well as a dictionary of task completion sources. When an event is fired or awaited we will pair of the first matching task completion source with the (possibly buffered) event.
A sample solution (showing a ClearScript target script being debugged by another process) is up on Github if you’re interested.
Conclusion
Whilst I was able to achieve the goals I set out to (set breakpoint, step over/into, examine/modify variables) there’s clearly a lot of moving parts - integrating it into the production app would require careful management of threads to ensure things don’t get stuck / resources exhausted.
Hope you enjoyed the post!