Cleanup analysis_server_client API

This reduces the client functionality and renames some
of the classes and methods in the API.

Specifically:
- rename Server to Client
- rename ServerConnectionHandler to ConnectionHandler
- rename Client.start to Client.startServer
- rename Client.stop to Client.stopServer
- rename Client.kill to Client.killServer
- extract behavior from Client into Listeners
- move some of the listeners into dartfix
- make several Client fields private
Change-Id: Ie71b0ac55b489099a848764251e8369c27f6ea2d
Reviewed-on: https://dart-review.googlesource.com/c/84460
Commit-Queue: Dan Rubel <danrubel@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/tool/spec/codegen_dart_notification_handler.dart b/pkg/analysis_server/tool/spec/codegen_dart_notification_handler.dart
index 1fa6600..45c8f2e 100644
--- a/pkg/analysis_server/tool/spec/codegen_dart_notification_handler.dart
+++ b/pkg/analysis_server/tool/spec/codegen_dart_notification_handler.dart
@@ -44,10 +44,13 @@
 /// and dispatches those notifications to different methods based upon
 /// the type of notification. Clients may override
 /// any of the "on<EventName>" methods that are of interest.
+///
+/// Clients may mix-in this class, but may not implement it.
 mixin NotificationHandler {
-  void handleEvent(String event, params) {
+  void handleEvent(Notification notification) {
+    Map<String, Object> params = notification.params;
     ResponseDecoder decoder = new ResponseDecoder(null);
-    switch (event) {
+    switch (notification.event) {
 ''');
     for (_Notification notification in notifications) {
       writeln('      case ${notification.constName}:');
@@ -57,7 +60,7 @@
       writeln('        break;');
     }
     writeln('      default:');
-    writeln('        onUnknownNotification(event, params);');
+    writeln('        onUnknownNotification(notification.event, params);');
     writeln('        break;');
     writeln('    }');
     writeln('  }');
diff --git a/pkg/analysis_server_client/example/example.dart b/pkg/analysis_server_client/example/example.dart
index 91a68e8..21573b4 100644
--- a/pkg/analysis_server_client/example/example.dart
+++ b/pkg/analysis_server_client/example/example.dart
@@ -6,7 +6,7 @@
 import 'dart:io' show Directory, Platform, ProcessSignal, exit;
 
 import 'package:analysis_server_client/handler/notification_handler.dart';
-import 'package:analysis_server_client/handler/server_connection_handler.dart';
+import 'package:analysis_server_client/handler/connection_handler.dart';
 import 'package:analysis_server_client/protocol.dart';
 import 'package:analysis_server_client/server.dart';
 import 'package:path/path.dart' as path;
@@ -43,31 +43,13 @@
   });
 }
 
-class _Handler with NotificationHandler, ServerConnectionHandler {
+class _Handler with NotificationHandler, ConnectionHandler {
   final Server server;
   int errorCount = 0;
 
   _Handler(this.server);
 
   @override
-  void handleFailedToConnect() {
-    print('Failed to connect to server');
-  }
-
-  @override
-  void handleProtocolNotSupported(Version version) {
-    print('Expected protocol version $PROTOCOL_VERSION, but found $version');
-  }
-
-  @override
-  void handleServerError(String error, String trace) {
-    print('Server Error: $error');
-    if (trace != null) {
-      print(trace);
-    }
-  }
-
-  @override
   void onAnalysisErrors(AnalysisErrorsParams params) {
     List<AnalysisError> errors = params.errors;
     bool first = true;
@@ -87,6 +69,29 @@
   }
 
   @override
+  void onFailedToConnect() {
+    print('Failed to connect to server');
+  }
+
+  @override
+  void onProtocolNotSupported(Version version) {
+    print('Expected protocol version $PROTOCOL_VERSION, but found $version');
+  }
+
+  @override
+  void onServerError(ServerErrorParams params) {
+    if (params.isFatal) {
+      print('Fatal Server Error: ${params.message}');
+    } else {
+      print('Server Error: ${params.message}');
+    }
+    if (params.stackTrace != null) {
+      print(params.stackTrace);
+    }
+    super.onServerError(params);
+  }
+
+  @override
   void onServerStatus(ServerStatusParams params) {
     if (!params.analysis.isAnalyzing) {
       // Whenever the server stops analyzing,
diff --git a/pkg/analysis_server_client/lib/handler/server_connection_handler.dart b/pkg/analysis_server_client/lib/handler/connection_handler.dart
similarity index 71%
rename from pkg/analysis_server_client/lib/handler/server_connection_handler.dart
rename to pkg/analysis_server_client/lib/handler/connection_handler.dart
index 5ff36db..800f75d 100644
--- a/pkg/analysis_server_client/lib/handler/server_connection_handler.dart
+++ b/pkg/analysis_server_client/lib/handler/connection_handler.dart
@@ -13,12 +13,14 @@
 import 'package:analysis_server_client/server.dart';
 import 'package:pub_semver/pub_semver.dart';
 
-/// [ServerConnectionHandler] listens to analysis server notifications
+/// [ConnectionHandler] listens to analysis server notifications
 /// and detects when a connection has been established with the server.
 ///
-/// Clients may override [handleFailedToConnect], [handleProtocolNotSupported],
-/// and [handleServerError] to display connection failure information.
-mixin ServerConnectionHandler on NotificationHandler {
+/// Clients may override [onFailedToConnect], [onProtocolNotSupported],
+/// and [onServerError] to display connection failure information.
+///
+/// Clients may mix-in this class, but may not extend or implement it.
+mixin ConnectionHandler on NotificationHandler {
   Completer<bool> _connected = new Completer();
 
   /// Clients should implement this method to return the server being managed.
@@ -26,11 +28,9 @@
   /// established or if a server error occurs after connecting.
   Server get server;
 
-  void handleFailedToConnect() {}
+  void onFailedToConnect() {}
 
-  void handleProtocolNotSupported(Version version) {}
-
-  void handleServerError(String error, String trace) {}
+  void onProtocolNotSupported(Version version) {}
 
   @override
   void onServerConnected(ServerConnectedParams params) {
@@ -40,7 +40,7 @@
     if (minVersion <= version && version < maxVersion) {
       _connected.complete(true);
     } else {
-      handleProtocolNotSupported(version);
+      onProtocolNotSupported(version);
       _connected.complete(false);
       server.stop();
     }
@@ -48,7 +48,6 @@
 
   @override
   void onServerError(ServerErrorParams params) {
-    handleServerError(params.message, params.stackTrace);
     server.stop();
   }
 
@@ -57,14 +56,11 @@
   Future<bool> serverConnected({Duration timeLimit}) {
     Future<bool> future = _connected.future;
     if (timeLimit != null) {
-      future = future.timeout(
-        timeLimit ?? const Duration(seconds: 15),
-        onTimeout: () {
-          handleFailedToConnect();
-          server.stop();
-          return false;
-        },
-      );
+      future = future.timeout(timeLimit, onTimeout: () {
+        onFailedToConnect();
+        server.stop();
+        return false;
+      });
     }
     return future;
   }
diff --git a/pkg/analysis_server_client/lib/handler/notification_handler.dart b/pkg/analysis_server_client/lib/handler/notification_handler.dart
index b115169..8bdbf14 100644
--- a/pkg/analysis_server_client/lib/handler/notification_handler.dart
+++ b/pkg/analysis_server_client/lib/handler/notification_handler.dart
@@ -12,10 +12,13 @@
 /// and dispatches those notifications to different methods based upon
 /// the type of notification. Clients may override
 /// any of the "on<EventName>" methods that are of interest.
+///
+/// Clients may mix-in this class, but may not implement it.
 mixin NotificationHandler {
-  void handleEvent(String event, params) {
+  void handleEvent(Notification notification) {
+    Map<String, Object> params = notification.params;
     ResponseDecoder decoder = new ResponseDecoder(null);
-    switch (event) {
+    switch (notification.event) {
       case ANALYSIS_NOTIFICATION_ANALYZED_FILES:
         onAnalysisAnalyzedFiles(new AnalysisAnalyzedFilesParams.fromJson(
             decoder, 'params', params));
@@ -93,7 +96,7 @@
             new ServerStatusParams.fromJson(decoder, 'params', params));
         break;
       default:
-        onUnknownNotification(event, params);
+        onUnknownNotification(notification.event, params);
         break;
     }
   }
diff --git a/pkg/analysis_server_client/lib/listener/client_listener.dart b/pkg/analysis_server_client/lib/listener/client_listener.dart
new file mode 100644
index 0000000..93272c0
--- /dev/null
+++ b/pkg/analysis_server_client/lib/listener/client_listener.dart
@@ -0,0 +1,61 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Instances of the class [ClientListener] receive information from [Client]
+/// about interactions with the server.
+///
+/// Clients may mix-in this class, but may not implement it.
+mixin ClientListener {
+  /// Called when the [Client] could not decode a message.
+  void badMessage(String trimmedLine, exception) {
+    log('JSON decode failure', '$exception');
+  }
+
+  /// Called when the [Client] receives a line on stderr.
+  void errorMessage(String line) {
+    log('ERR:', line);
+  }
+
+  /// Called when the [Client] is terminating the server process
+  /// rather than requesting that the server stop itself.
+  void killingServerProcess(String reason) {
+    log('FORCIBLY TERMINATING SERVER: ', reason);
+  }
+
+  /// Log a message about interaction with the server.
+  void log(String prefix, String details);
+
+  /// Called when the [Client] received a response or notification.
+  void messageReceived(String json) {
+    log('<== ', json);
+  }
+
+  /// Called when the [Client] sends a request.
+  void requestSent(String json) {
+    log('==> ', json);
+  }
+
+  /// Called when the [Client] starts the server process.
+  void startingServer(String dartBinary, List<String> arguments) {
+    log('Starting analysis server:', '$dartBinary ${arguments.join(' ')}');
+  }
+
+  /// Called when the [Client] receives an unexpected message
+  /// which is not a notification or response.
+  void unexpectedMessage(Map<String, dynamic> message) {
+    log('Unexpected message from server:', '$message');
+  }
+
+  /// Called when the [Client] recieved an unexpected response
+  /// where the [id] does not match the [id] of an outstanding request.
+  void unexpectedResponse(Map<String, dynamic> message, id) {
+    log('Unexpected response from server', 'id=$id');
+  }
+
+  /// Called when the server process unexpectedly exits
+  /// with a non-zero exit code.
+  void unexpectedStop(int exitCode) {
+    log('Server terminated with exit code', '$exitCode');
+  }
+}
diff --git a/pkg/analysis_server_client/lib/recording_server.dart b/pkg/analysis_server_client/lib/recording_server.dart
deleted file mode 100644
index 5fc8347..0000000
--- a/pkg/analysis_server_client/lib/recording_server.dart
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:async';
-
-import 'package:analysis_server_client/server.dart';
-
-/// A subclass of [Server] that caches all messages exchanged with the server.
-/// This is primarily used when testing and debugging the analysis server.
-/// Most clients will want to use [Server] rather than this class.
-class RecordingServer extends Server {
-  /// True if we are currently printing out messages exchanged with the server.
-  bool _echoMessages = false;
-
-  /// Messages which have been exchanged with the server; we buffer these
-  /// up until the test finishes, so that they can be examined in the debugger
-  /// or printed out in response to a call to [echoMessages].
-  final _messages = <String>[];
-
-  /// Print out any messages exchanged with the server.  If some messages have
-  /// already been exchanged with the server, they are printed out immediately.
-  void echoMessages() {
-    if (_echoMessages) {
-      return;
-    }
-    _echoMessages = true;
-    for (String line in _messages) {
-      print(line);
-    }
-  }
-
-  @override
-  Future<int> kill([String reason = 'none']) {
-    echoMessages();
-    return super.kill(reason);
-  }
-
-  @override
-  void logBadDataFromServer(String details, {bool silent: false}) {
-    echoMessages();
-    super.logBadDataFromServer(details, silent: silent);
-  }
-
-  /// Record a message that was exchanged with the server,
-  /// and print it out if [echoMessages] has been called.
-  @override
-  void logMessage(String prefix, String details) {
-    String line = '$currentElapseTime: $prefix $details';
-    if (_echoMessages) {
-      print(line);
-    }
-    _messages.add(line);
-  }
-}
diff --git a/pkg/analysis_server_client/lib/server.dart b/pkg/analysis_server_client/lib/server.dart
index b43090f..dc485ce 100644
--- a/pkg/analysis_server_client/lib/server.dart
+++ b/pkg/analysis_server_client/lib/server.dart
@@ -6,15 +6,22 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:analysis_server_client/listener/client_listener.dart';
 import 'package:analysis_server_client/protocol.dart';
 import 'package:path/path.dart';
 
 /// Type of callbacks used to process notifications.
-typedef void NotificationProcessor(String event, Map<String, dynamic> params);
+typedef void NotificationProcessor(Notification notification);
 
 /// Instances of the class [Server] manage a server process,
 /// and facilitate communication to and from the server.
+///
+/// Clients may not extend, implement or mix-in this class.
 class Server {
+  /// If not `null`, [_listener] will be sent information
+  /// about interactions with the server.
+  ClientListener _listener;
+
   /// Server process object, or `null` if server hasn't been started yet
   /// or if the server has already been stopped.
   Process _process;
@@ -28,39 +35,21 @@
   /// to send in the next command sent to the server.
   int _nextId = 0;
 
-  /// True if we've received bad data from the server.
-  bool _receivedBadDataFromServer = false;
-
   /// The stderr subscription or `null` if either
   /// [listenToOutput] has not been called or [stop] has been called.
-  StreamSubscription<String> stderrSubscription;
+  StreamSubscription<String> _stderrSubscription;
 
   /// The stdout subscription or `null` if either
   /// [listenToOutput] has not been called or [stop] has been called.
-  StreamSubscription<String> stdoutSubscription;
+  StreamSubscription<String> _stdoutSubscription;
 
-  /// Stopwatch that we use to generate timing information for debug output.
-  Stopwatch _time = new Stopwatch();
-
-  /// The [currentElapseTime] at which the last communication was received from
-  /// the server or `null` if no communication has been received.
-  double lastCommunicationTime;
-
-  Server([Process process]) : this._process = process;
-
-  /// The current elapse time (seconds) since the server was started.
-  double get currentElapseTime => _time.elapsedTicks / _time.frequency;
-
-  /// Future that completes when the server process exits.
-  Future<int> get exitCode => _process.exitCode;
-
-  /// Return a future that will complete when all commands that have been sent
-  /// to the server so far have been flushed to the OS buffer.
-  Future<void> flushCommands() => _process.stdin.flush();
+  Server({ClientListener listener, Process process})
+      : this._listener = listener,
+        this._process = process;
 
   /// Force kill the server. Returns exit code future.
-  Future<int> kill([String reason = 'none']) {
-    logMessage('FORCIBLY TERMINATING SERVER: ', reason);
+  Future<int> kill({String reason = 'none'}) {
+    _listener?.killingServerProcess(reason);
     final process = _process;
     _process = null;
     process.kill();
@@ -70,11 +59,10 @@
   /// Start listening to output from the server,
   /// and deliver notifications to [notificationProcessor].
   void listenToOutput({NotificationProcessor notificationProcessor}) {
-    stdoutSubscription = _process.stdout
+    _stdoutSubscription = _process.stdout
         .transform(utf8.decoder)
         .transform(new LineSplitter())
         .listen((String line) {
-      lastCommunicationTime = currentElapseTime;
       String trimmedLine = line.trim();
 
       // Guard against lines like:
@@ -89,72 +77,50 @@
         return;
       }
 
-      logMessage('<== ', trimmedLine);
+      _listener?.messageReceived(trimmedLine);
       Map<String, dynamic> message;
       try {
         message = json.decoder.convert(trimmedLine);
       } catch (exception) {
-        logBadDataFromServer('JSON decode failure: $exception');
+        _listener?.badMessage(trimmedLine, exception);
         return;
       }
 
-      final id = message['id'];
+      final id = message[Response.ID];
       if (id != null) {
         // Handle response
         final completer = _pendingCommands.remove(id);
         if (completer == null) {
-          throw 'Unexpected response from server: id=$id';
+          _listener?.unexpectedResponse(message, id);
         }
-        if (message.containsKey('error')) {
-          completer.completeError(new ServerErrorMessage(message));
+        if (message.containsKey(Response.ERROR)) {
+          completer.completeError(new RequestError.fromJson(
+              new ResponseDecoder(null), '.error', message[Response.ERROR]));
         } else {
-          completer.complete(message['result']);
+          completer.complete(message[Response.RESULT]);
         }
       } else {
         // Handle notification
-        final String event = message['event'];
+        final String event = message[Notification.EVENT];
         if (event != null) {
           if (notificationProcessor != null) {
-            notificationProcessor(event, message['params']);
+            notificationProcessor(
+                new Notification(event, message[Notification.PARAMS]));
           }
         } else {
-          logBadDataFromServer('Unexpected message from server');
+          _listener?.unexpectedMessage(message);
         }
       }
     });
-    stderrSubscription = _process.stderr
+    _stderrSubscription = _process.stderr
         .transform(utf8.decoder)
         .transform(new LineSplitter())
         .listen((String line) {
       String trimmedLine = line.trim();
-      logMessage('ERR: ', trimmedLine);
-      logBadDataFromServer('Message received on stderr', silent: true);
+      _listener?.errorMessage(trimmedLine);
     });
   }
 
-  /// Deal with bad data received from the server.
-  void logBadDataFromServer(String details, {bool silent: false}) {
-    if (!silent) {
-      logMessage('BAD DATA FROM SERVER: ', details);
-    }
-    if (_receivedBadDataFromServer) {
-      // We're already dealing with it.
-      return;
-    }
-    _receivedBadDataFromServer = true;
-    // Give the server 1 second to continue outputting bad data
-    // such as outputting a stacktrace.
-    new Future.delayed(new Duration(seconds: 1), () {
-      throw 'Bad data received from server: $details';
-    });
-  }
-
-  /// Log a message that was exchanged with the server.
-  /// Subclasses may override as needed.
-  void logMessage(String prefix, String details) {
-    // no-op
-  }
-
   /// Send a command to the server. An 'id' will be automatically assigned.
   /// The returned [Future] will be completed when the server acknowledges
   /// the command with a response.
@@ -166,16 +132,16 @@
       String method, Map<String, dynamic> params) {
     String id = '${_nextId++}';
     Map<String, dynamic> command = <String, dynamic>{
-      'id': id,
-      'method': method
+      Request.ID: id,
+      Request.METHOD: method
     };
     if (params != null) {
-      command['params'] = params;
+      command[Request.PARAMS] = params;
     }
     final completer = new Completer<Map<String, dynamic>>();
     _pendingCommands[id] = completer;
     String line = json.encode(command);
-    logMessage('==> ', line);
+    _listener?.requestSent(line);
     _process.stdin.add(utf8.encoder.convert("$line\n"));
     return completer.future;
   }
@@ -203,7 +169,6 @@
     if (_process != null) {
       throw new Exception('Process already started');
     }
-    _time.start();
     String dartBinary = Platform.executable;
 
     // The integration tests run 3x faster when run from snapshots
@@ -270,13 +235,12 @@
     if (useAnalysisHighlight2) {
       arguments.add('--useAnalysisHighlight2');
     }
-    logMessage(
-        'Starting analysis server: ', '$dartBinary ${arguments.join(' ')}');
+    _listener?.startingServer(dartBinary, arguments);
     _process = await Process.start(dartBinary, arguments);
     _process.exitCode.then((int code) {
       if (code != 0 && _process != null) {
         // Report an error if server abruptly terminated
-        logBadDataFromServer('server terminated with exit code $code');
+        _listener?.unexpectedStop(code);
       }
     });
   }
@@ -297,32 +261,18 @@
         .timeout(timeLimit, onTimeout: () {
       return null;
     }).whenComplete(() async {
-      await stderrSubscription?.cancel();
-      stderrSubscription = null;
-      await stdoutSubscription?.cancel();
-      stdoutSubscription = null;
+      await _stderrSubscription?.cancel();
+      _stderrSubscription = null;
+      await _stdoutSubscription?.cancel();
+      _stdoutSubscription = null;
     });
-    return await process.exitCode.timeout(
+    return process.exitCode.timeout(
       timeLimit,
       onTimeout: () {
-        logMessage('FORCIBLY TERMINATING SERVER: ', 'server failed to exit');
+        _listener?.killingServerProcess('server failed to exit');
         process.kill();
         return process.exitCode;
       },
     );
   }
 }
-
-/// An error result from a server request.
-class ServerErrorMessage {
-  final Map<String, dynamic> message;
-
-  ServerErrorMessage(this.message);
-
-  Map<String, dynamic> get error => message['error'];
-  get errorCode => error['code'];
-  get errorMessage => error['message'];
-  get stackTrace => error['stackTrace'];
-
-  String toString() => message.toString();
-}
diff --git a/pkg/analysis_server_client/test/live_test.dart b/pkg/analysis_server_client/test/live_test.dart
index 15b0431..4156bcc 100644
--- a/pkg/analysis_server_client/test/live_test.dart
+++ b/pkg/analysis_server_client/test/live_test.dart
@@ -2,8 +2,9 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:analysis_server_client/listener/client_listener.dart';
 import 'package:analysis_server_client/handler/notification_handler.dart';
-import 'package:analysis_server_client/handler/server_connection_handler.dart';
+import 'package:analysis_server_client/handler/connection_handler.dart';
 import 'package:analysis_server_client/protocol.dart';
 import 'package:analysis_server_client/server.dart';
 import 'package:test/test.dart';
@@ -12,7 +13,7 @@
 
 void main() {
   test('live', () async {
-    final server = _debug ? new TestServer() : new Server();
+    final server = new Server(listener: _debug ? new TestListener() : null);
     await server.start(clientId: 'test', suppressAnalytics: true);
 
     TestHandler handler = new TestHandler(server);
@@ -32,15 +33,15 @@
   });
 }
 
-class TestHandler with NotificationHandler, ServerConnectionHandler {
+class TestHandler with NotificationHandler, ConnectionHandler {
   final Server server;
 
   TestHandler(this.server);
 }
 
-class TestServer extends Server {
+class TestListener with ClientListener {
   @override
-  void logMessage(String prefix, String details) {
+  void log(String prefix, String details) {
     print('$prefix $details');
   }
 }
diff --git a/pkg/analysis_server_client/test/server_test.dart b/pkg/analysis_server_client/test/server_test.dart
index 82eaad7..9f3d643 100644
--- a/pkg/analysis_server_client/test/server_test.dart
+++ b/pkg/analysis_server_client/test/server_test.dart
@@ -7,6 +7,7 @@
 import 'dart:io';
 
 import 'package:analysis_server_client/server.dart';
+import 'package:analysis_server_client/protocol.dart';
 import 'package:test/test.dart';
 
 void main() {
@@ -15,7 +16,7 @@
 
   setUp(() async {
     process = new MockProcess();
-    server = new Server(process);
+    server = new Server(process: process);
   });
 
   group('listenToOutput', () {
@@ -36,10 +37,10 @@
 
       final future = server.send('blahMethod', null);
       future.catchError((e) {
-        expect(e, const TypeMatcher<ServerErrorMessage>());
-        final error = e as ServerErrorMessage;
-        expect(error.errorCode, 'someErrorCode');
-        expect(error.errorMessage, 'something went wrong');
+        expect(e, const TypeMatcher<RequestError>());
+        final error = e as RequestError;
+        expect(error.code, RequestErrorCode.UNKNOWN_REQUEST);
+        expect(error.message, 'something went wrong');
         expect(error.stackTrace, 'some long stack trace');
       });
       server.listenToOutput();
@@ -50,11 +51,11 @@
       process.stderr = _noMessage();
 
       final completer = new Completer();
-      void eventHandler(String event, Map<String, Object> params) {
-        expect(event, 'fooEvent');
-        expect(params.length, 2);
-        expect(params['foo'] as String, 'bar');
-        expect(params['baz'] as String, 'bang');
+      void eventHandler(Notification notification) {
+        expect(notification.event, 'fooEvent');
+        expect(notification.params.length, 2);
+        expect(notification.params['foo'] as String, 'bar');
+        expect(notification.params['baz'] as String, 'bang');
         completer.complete();
       }
 
@@ -93,17 +94,17 @@
       final mockout = new StreamController<List<int>>();
       process.stdout = mockout.stream;
       process.stderr = _noMessage();
-      process.exitCode = new Future.delayed(const Duration(milliseconds: 20));
+      process.exitCode = new Future.delayed(const Duration(seconds: 1));
 
       server.listenToOutput();
-      await server.stop(timeLimit: const Duration(milliseconds: 1));
+      await server.stop(timeLimit: const Duration(milliseconds: 10));
       expect(process.killed, isTrue);
     });
   });
 }
 
 final _badErrorMessage = {
-  'code': 'someErrorCode',
+  'code': 'UNKNOWN_REQUEST',
   'message': 'something went wrong',
   'stackTrace': 'some long stack trace'
 };
diff --git a/pkg/dartfix/lib/listener/bad_message_listener.dart b/pkg/dartfix/lib/listener/bad_message_listener.dart
new file mode 100644
index 0000000..ed919fb
--- /dev/null
+++ b/pkg/dartfix/lib/listener/bad_message_listener.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:analysis_server_client/listener/client_listener.dart';
+
+/// [BadMessageListener] throws an exception if the [Client] receives bad data.
+mixin BadMessageListener on ClientListener {
+  /// True if we've received bad data from the server.
+  bool _receivedBadDataFromServer = false;
+
+  void throwDelayedException(String prefix, String details) {
+    if (!_receivedBadDataFromServer) {
+      _receivedBadDataFromServer = true;
+      // Give the server 1 second to continue outputting bad data
+      // such as outputting a stacktrace.
+      new Future.delayed(new Duration(seconds: 1), () {
+        throw '$prefix $details';
+      });
+    }
+  }
+
+  @override
+  void badMessage(String trimmedLine, exception) {
+    super.badMessage(trimmedLine, exception);
+    throwDelayedException('JSON decode failure', '$exception');
+  }
+
+  @override
+  void errorMessage(String line) {
+    super.errorMessage(line);
+    throwDelayedException('ERR:', line);
+  }
+
+  @override
+  void unexpectedMessage(Map<String, dynamic> message) {
+    super.unexpectedMessage(message);
+    throwDelayedException(
+        'BAD DATA FROM SERVER:', 'Unexpected message from server');
+  }
+
+  @override
+  void unexpectedResponse(Map<String, dynamic> message, id) {
+    super.unexpectedResponse(message, id);
+    throw 'Unexpected response from server: id=$id';
+  }
+
+  @override
+  void unexpectedStop(int exitCode) {
+    super.unexpectedStop(exitCode);
+    throwDelayedException('Server terminated with exit code', '$exitCode');
+  }
+}
diff --git a/pkg/dartfix/lib/listener/recording_listener.dart b/pkg/dartfix/lib/listener/recording_listener.dart
new file mode 100644
index 0000000..c04477d
--- /dev/null
+++ b/pkg/dartfix/lib/listener/recording_listener.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server_client/server.dart';
+import 'package:analysis_server_client/listener/client_listener.dart';
+import 'package:dartfix/listener/bad_message_listener.dart';
+import 'package:dartfix/listener/timed_listener.dart';
+
+/// [RecordingListener] caches all messages exchanged with the server
+/// and print them if a problem occurs.
+///
+/// This is primarily used when testing and debugging the analysis server.
+class RecordingListener with ClientListener, BadMessageListener, TimedListener {
+  /// True if we are currently printing out messages exchanged with the server.
+  bool _echoMessages = false;
+
+  /// Messages which have been exchanged with the server; we buffer these
+  /// up until the test finishes, so that they can be examined in the debugger
+  /// or printed out in response to a call to [echoMessages].
+  final _messages = <String>[];
+
+  /// Print out any messages exchanged with the server.  If some messages have
+  /// already been exchanged with the server, they are printed out immediately.
+  void echoMessages() {
+    if (_echoMessages) {
+      return;
+    }
+    _echoMessages = true;
+    for (String line in _messages) {
+      print(line);
+    }
+  }
+
+  /// Called when the [Server] is terminating the server process
+  /// rather than requesting that the server stop itself.
+  void killingServerProcess(String reason) {
+    echoMessages();
+    super.killingServerProcess(reason);
+  }
+
+  /// Log a timed message about interaction with the server.
+  void logTimed(double elapseTime, String prefix, String details) {
+    String line = '$elapseTime: $prefix $details';
+    if (_echoMessages) {
+      print(line);
+    }
+    _messages.add(line);
+  }
+
+  @override
+  void throwDelayedException(String prefix, String details) {
+    echoMessages();
+    super.throwDelayedException(prefix, details);
+  }
+}
diff --git a/pkg/dartfix/lib/listener/timed_listener.dart b/pkg/dartfix/lib/listener/timed_listener.dart
new file mode 100644
index 0000000..f4116ed
--- /dev/null
+++ b/pkg/dartfix/lib/listener/timed_listener.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:analysis_server_client/listener/client_listener.dart';
+
+/// [TimedListener] appends a timestamp (seconds since server startup)
+/// to each logged interaction with the server.
+mixin TimedListener on ClientListener {
+  /// Stopwatch that we use to generate timing information for debug output.
+  Stopwatch _time = new Stopwatch();
+
+  /// The [currentElapseTime] at which the last communication was received from
+  /// the server or `null` if no communication has been received.
+  double lastCommunicationTime;
+
+  /// The current elapse time (seconds) since the server was started.
+  double get currentElapseTime => _time.elapsedTicks / _time.frequency;
+
+  @override
+  void log(String prefix, String details) {
+    logTimed(currentElapseTime, prefix, details);
+  }
+
+  /// Log a timed message about interaction with the server.
+  void logTimed(double elapseTime, String prefix, String details);
+
+  @override
+  void messageReceived(String json) {
+    lastCommunicationTime = currentElapseTime;
+    super.messageReceived(json);
+  }
+
+  @override
+  void startingServer(String dartBinary, List<String> arguments) {
+    _time.start();
+    super.startingServer(dartBinary, arguments);
+  }
+}
diff --git a/pkg/dartfix/lib/src/driver.dart b/pkg/dartfix/lib/src/driver.dart
index 8902799..55950db 100644
--- a/pkg/dartfix/lib/src/driver.dart
+++ b/pkg/dartfix/lib/src/driver.dart
@@ -5,16 +5,17 @@
 import 'dart:async';
 import 'dart:io' show File, Platform;
 
-import 'package:analysis_server_client/handler/notification_handler.dart';
-import 'package:analysis_server_client/handler/server_connection_handler.dart';
-import 'package:analysis_server_client/protocol.dart';
 import 'package:analysis_server_client/server.dart';
+import 'package:analysis_server_client/handler/connection_handler.dart';
+import 'package:analysis_server_client/handler/notification_handler.dart';
+import 'package:analysis_server_client/listener/client_listener.dart';
+import 'package:analysis_server_client/protocol.dart';
 import 'package:cli_util/cli_logging.dart';
 import 'package:dartfix/handler/analysis_complete_handler.dart';
+import 'package:dartfix/listener/bad_message_listener.dart';
 import 'package:dartfix/src/context.dart';
 import 'package:dartfix/src/options.dart';
 import 'package:dartfix/src/util.dart';
-import 'package:dartfix/src/verbose_server.dart';
 import 'package:pub_semver/pub_semver.dart';
 
 class Driver {
@@ -37,7 +38,7 @@
     targets = options.targets;
     context = options.context;
     logger = options.logger;
-    server = logger.isVerbose ? new VerboseServer(logger) : new Server();
+    server = new Server(listener: new _Listener(logger));
     handler = new _Handler(this);
 
     if (!await startServer(options)) {
@@ -184,8 +185,22 @@
   }
 }
 
+class _Listener with ClientListener, BadMessageListener {
+  final Logger logger;
+  final bool verbose;
+
+  _Listener(this.logger) : verbose = logger.isVerbose;
+
+  @override
+  void log(String prefix, String details) {
+    if (verbose) {
+      logger.trace('$prefix $details');
+    }
+  }
+}
+
 class _Handler
-    with NotificationHandler, ServerConnectionHandler, AnalysisCompleteHandler {
+    with NotificationHandler, ConnectionHandler, AnalysisCompleteHandler {
   final Driver driver;
   final Logger logger;
   final Server server;
@@ -195,22 +210,27 @@
         server = driver.server;
 
   @override
-  void handleFailedToConnect() {
+  void onFailedToConnect() {
     logger.stderr('Failed to connect to server');
   }
 
   @override
-  void handleProtocolNotSupported(Version version) {
+  void onProtocolNotSupported(Version version) {
     logger.stderr('Expected protocol version $PROTOCOL_VERSION,'
         ' but found $version');
   }
 
   @override
-  void handleServerError(String error, String trace) {
-    logger.stderr('Server Error: $error');
-    if (trace != null) {
-      logger.stderr(trace);
+  void onServerError(ServerErrorParams params) {
+    if (params.isFatal) {
+      logger.stderr('Fatal Server Error: ${params.message}');
+    } else {
+      logger.stderr('Server Error: ${params.message}');
     }
+    if (params.stackTrace != null) {
+      logger.stderr(params.stackTrace);
+    }
+    super.onServerError(params);
   }
 
   @override
diff --git a/pkg/dartfix/lib/src/verbose_server.dart b/pkg/dartfix/lib/src/verbose_server.dart
deleted file mode 100644
index a96f426..0000000
--- a/pkg/dartfix/lib/src/verbose_server.dart
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:analysis_server_client/server.dart';
-import 'package:cli_util/cli_logging.dart';
-
-class VerboseServer extends Server {
-  final Logger logger;
-
-  VerboseServer(this.logger);
-
-  @override
-  void logMessage(String prefix, String details) {
-    logger.trace('$prefix $details');
-  }
-}