Expose a WebSocket debug controller.

Still needs tests.
diff --git a/doc/json_reporter.md b/doc/json_reporter.md
index a3a557b..bb9a001 100644
--- a/doc/json_reporter.md
+++ b/doc/json_reporter.md
@@ -453,11 +453,15 @@
 runner to tell it things like "all the breakpoints are set and the test should
 begin running". This is done through a [JSON-RPC 2.0][] API over a WebSocket
 connection. The WebSocket URL is available in
-[`StartEvent.controllerUrl`](#StartEvent). The following RPCs are available:
+[`StartEvent.controllerUrl`](#StartEvent).
 
 [JSON-RPC 2.0][]: http://www.jsonrpc.org/specification
 
-### `null resume()`
+Each RPC will return a success response with a null result once the request has
+been handled. If there's no test suite currently being debugged, they'll return
+an error response with error code 1. The following RPCs are available:
+
+### `resume()`
 
 Calling `resume()` when the test runner is paused causes it to resume running
 tests. If the test runner is not paused, it won't do anything. When
@@ -469,9 +473,9 @@
 with `--pause-after-load`, connect to the controller protocol using
 [`StartEvent.controllerUrl`](#StartEvent), set breakpoints, then call `resume()`
 in when they're finished.
+L
+#### `restartTest()`
 
-#### `restartCurrent()`
-
-Calling `restartCurrent()` when the test runner is running a test causes it to
+Calling `restartTest()` when the test runner is running a test causes it to
 re-run that test once it completes its current run. It's intended to be called
 when the browser is paused, as at a breakpoint.
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 20f4cdb..8678741 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -157,7 +157,7 @@
     return;
   }
 
-  var runner = new Runner(configuration);
+  var runner = await Runner.start(configuration);
 
   var signalSubscription;
   close() async {
diff --git a/lib/src/runner.dart b/lib/src/runner.dart
index 4dd0faf..29ced0c 100644
--- a/lib/src/runner.dart
+++ b/lib/src/runner.dart
@@ -46,6 +46,10 @@
   /// The reporter that's emitting the test runner's results.
   final Reporter _reporter;
 
+  /// The controller that controls suite debugging, or `null` if we aren't in
+  /// debug mode or we aren't using the JSON reporter.
+  final DebugController _debugController;
+
   /// The subscription to the stream returned by [_loadSuites].
   StreamSubscription _suiteSubscription;
 
@@ -66,10 +70,12 @@
   bool get _closed => _closeMemo.hasRun;
 
   /// Creates a new runner based on [configuration].
-  factory Runner(Configuration config) => config.asCurrent(() {
+  static Future<Runner> start(Configuration config) =>
+      config.asCurrent(() async {
     var engine = new Engine(concurrency: config.concurrency);
 
-    var reporter;
+    Reporter reporter;
+    DebugController controller;
     switch (config.reporter) {
       case "expanded":
         reporter = ExpandedReporter.watch(
@@ -85,14 +91,15 @@
         break;
 
       case "json":
-        reporter = JsonReporter.watch(engine);
+        if (config.pauseAfterLoad) controller = await DebugController.start();
+        reporter = JsonReporter.watch(engine, controller?.url);
         break;
     }
 
-    return new Runner._(engine, reporter);
+    return new Runner._(engine, reporter, controller);
   });
 
-  Runner._(this._engine, this._reporter);
+  Runner._(this._engine, this._reporter, this._debugController);
 
   /// Starts the runner.
   ///
@@ -207,9 +214,11 @@
       });
     }
 
-    if (_debugOperation != null) await _debugOperation.cancel();
+    print("in close");
+    await _debugOperation?.cancel();
+    await _debugController?.close();
 
-    if (_suiteSubscription != null) _suiteSubscription.cancel();
+    _suiteSubscription?.cancel();
     _suiteSubscription = null;
 
     // Make sure we close the engine *before* the loader. Otherwise,
@@ -372,7 +381,7 @@
   /// that support debugging.
   Future<bool> _loadThenPause(Stream<LoadSuite> suites) async {
     _suiteSubscription = suites.asyncMap((loadSuite) async {
-      _debugOperation = debug(_engine, _reporter, loadSuite);
+      _debugOperation = debug(_engine, _reporter, loadSuite, _debugController);
       await _debugOperation.valueOrCancellation();
     }).listen(null);
 
diff --git a/lib/src/runner/browser/browser_manager.dart b/lib/src/runner/browser/browser_manager.dart
index 0b89e7d..9216be2 100644
--- a/lib/src/runner/browser/browser_manager.dart
+++ b/lib/src/runner/browser/browser_manager.dart
@@ -305,7 +305,7 @@
 
   final Uri remoteDebuggerUrl;
 
-  final String isolateID => null;
+  final String isolateID = null;
 
   final Stream onRestart;
 
diff --git a/lib/src/runner/debugger.dart b/lib/src/runner/debugger.dart
index 11b424f..90bae04 100644
--- a/lib/src/runner/debugger.dart
+++ b/lib/src/runner/debugger.dart
@@ -3,8 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:io';
 
 import 'package:async/async.dart';
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:json_rpc_2/json_rpc_2.dart' as rpc;
+import 'package:shelf_web_socket/shelf_web_socket.dart';
+import 'package:shelf/shelf_io.dart' as io;
 
 import '../backend/test_platform.dart';
 import '../util/io.dart';
@@ -22,11 +27,14 @@
 /// watching [engine], and the [config] should contain the user configuration
 /// for the test runner.
 ///
+/// If [controller] is passed, the debugger will hook into it to allow debugging
+/// to be controlled via WebSocket.
+///
 /// Returns a [CancelableOperation] that will complete once the suite has
 /// finished running. If the operation is canceled, the debugger will clean up
 /// any resources it allocated.
 CancelableOperation debug(Engine engine, Reporter reporter,
-    LoadSuite loadSuite) {
+    LoadSuite loadSuite, [DebugController controller]) {
   var debugger;
   var canceled = false;
   return new CancelableOperation.fromFuture(() async {
@@ -41,7 +49,9 @@
     if (canceled || suite == null) return;
 
     debugger = new _Debugger(engine, reporter, suite);
+    controller?._debugger = debugger;
     await debugger.run();
+    controller?._debugger = null;
   }(), onCancel: () {
     canceled = true;
     // Make sure the load test finishes so the engine can close.
@@ -73,8 +83,7 @@
   /// overlap with the reporter's reporting.
   final Console _console;
 
-  /// A completer that's used to manually unpause the test if the debugger is
-  /// closed.
+  /// A completer that's used to manually unpause the test.
   final _pauseCompleter = new CancelableCompleter();
 
   /// The subscription to [_suite.onDebugging].
@@ -221,10 +230,48 @@
 
   /// Closes the debugger and releases its resources.
   void close() {
-    _pauseCompleter.complete();
+    if (!_pauseCompleter.isCompleted) _pauseCompleter.complete();
     _closed = true;
     _onDebuggingSubscription?.cancel();
     _onRestartSubscription.cancel();
     _console.stop();
   }
 }
+
+/// A singleton WebSocket server with a JSON-RPC 2.0 protocol that services RPCs
+/// that allow IDEs using the JSON reporter to control the debugger.
+class DebugController {
+  final HttpServer _server;
+
+  _Debugger _debugger;
+
+  Uri get url => Uri.parse("ws://localhost:${_server.port}");
+
+  static Future<DebugController> start() async =>
+      new DebugController._(await HttpMultiServer.loopback(0));
+
+  DebugController._(this._server) {
+    io.serveRequests(_server, webSocketHandler((webSocket) {
+      var server = new rpc.Server(webSocket);
+      server.registerMethod("resume", () {
+        _assertDebugger();
+        _debugger._pauseCompleter.complete();
+      });
+
+      server.registerMethod("restartTest", () {
+        _assertDebugger();
+        _debugger._restartTest();
+      });
+    }));
+  }
+
+  void _assertDebugger() {
+    if (_debugger != null) return;
+    throw new rpc.RpcException(1, "No suite is being debugged.");
+  }
+
+  Future close() {
+    print("closing debug controller");
+    return _server.close();
+  }
+}
diff --git a/lib/src/runner/reporter/json.dart b/lib/src/runner/reporter/json.dart
index bfa4b75..a37389b 100644
--- a/lib/src/runner/reporter/json.dart
+++ b/lib/src/runner/reporter/json.dart
@@ -4,7 +4,6 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:developer';
 
 import '../../backend/group.dart';
 import '../../backend/group_entry.dart';
@@ -59,9 +58,14 @@
   var _nextID = 0;
 
   /// Watches the tests run by [engine] and prints their results as JSON.
-  static JsonReporter watch(Engine engine) => new JsonReporter._(engine);
+  ///
+  /// If [controllerUrl] is passed, it's emitted as the URL for IDE clients to
+  /// use to control the debugger.
+  static JsonReporter watch(Engine engine, [Uri controllerUrl]) =>
+      new JsonReporter._(engine, controllerUrl);
 
-  JsonReporter._(this._engine) : _config = Configuration.current {
+  JsonReporter._(this._engine, Uri controllerUrl)
+      : _config = Configuration.current {
     _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
 
     /// Convert the future to a stream so that the subscription can be paused or
@@ -76,7 +80,8 @@
 
     _emit("start", {
       "protocolVersion": "0.1.0",
-      "runnerVersion": testVersion
+      "runnerVersion": testVersion,
+      "controllerUrl": controllerUrl?.toString()
     });
   }