Debugging analyzer plugins
Analyzer plugins can be used to extend the Dart analysis server used by all Dart editors. They can provide new lints and fixes for Dart or analysis support for other programming languages. Analyzer plugins are loaded and managed by the Dart analysis server, making them easy to distribute and use. This approach has a downside though: Since the analyzer is responsible to load plugins, they're rather tough to debug - we can't just run our main
method from the IDE.
Luckily, we can use a little trick to help us here. For debugging purposes, we instead use the following model:
- The actual plugin will only start an http server. As it won't be loaded by the analyzer, we can run and debug it from the IDE.
- We'll replace the plugin seen by the analysis server with a small proxy that talks to our http server.
All we need to do is implement the different PluginCommunicationChannel
- almost no code changes are needed to the actual plugin here.
First, let's write the proxy that can be loaded by the analysis server.
# tool/analyzer_plugin/pubspec.yaml
name: analyzer_load_my_plugin
version: 1.0.0
dependencies:
your_real_plugin: ^2.0.0 # remove if you don't have a published analyzer plugin yet, or use an (absolute!) path dependency
web_socket_channel: ^1.0.15
// tool/analyzer_plugin/bin/plugin.dart
import 'dart:convert';
import 'dart:isolate';
import 'package:your_real_plugin/plugin.dart' as plugin;
import 'package:web_socket_channel/io.dart';
const useDebuggingVariant = true; // set to false before publishing
void main(List<String> args, SendPort sendPort) {
if (useDebuggingVariant) {
_PluginProxy(sendPort).start();
} else {
// start the regular plugin like you normally would
plugin.start(args, sendPort);
}
}
class _PluginProxy {
final SendPort sendToAnalysisServer;
ReceivePort _receive;
IOWebSocketChannel _channel;
_PluginProxy(this.sendToAnalysisServer);
Future<void> start() async {
_channel = IOWebSocketChannel.connect('ws://localhost:9999');
_receive = ReceivePort();
sendToAnalysisServer.send(_receive.sendPort);
_receive.listen((data) {
// the server will send messages as maps, convert to json
_channel.sink.add(json.encode(data));
});
_channel.stream.listen((data) {
sendToAnalysisServer.send(json.decode(data as String));
});
}
}
Next, we need to adapt the plugin so that it opens the websocket server. For that, we'll write a drop-in replacement for ServerPluginStarter
:
class _WebSocketPluginServer implements PluginCommunicationChannel {
final dynamic address;
final int port;
HttpServer server;
WebSocket _currentClient;
final StreamController<WebSocket> _clientStream =
StreamController.broadcast();
_WebSocketPluginServer({dynamic address, this.port = 9999})
: address = address ?? InternetAddress.loopbackIPv4 {
_init();
}
Future<void> _init() async {
server = await HttpServer.bind(address, port);
print('listening on $address at port $port');
server.transform(WebSocketTransformer()).listen(_handleClientAdded);
}
void _handleClientAdded(WebSocket socket) {
if (_currentClient != null) {
print('ignoring connection attempt because an active client already '
'exists');
socket.close();
} else {
print('client connected');
_currentClient = socket;
_clientStream.add(_currentClient);
_currentClient.done.then((_) {
print('client disconnected');
_currentClient = null;
_clientStream.add(null);
});
}
}
@override
void close() {
server?.close(force: true);
}
@override
void listen(void Function(Request request) onRequest,
{Function onError, void Function() onDone}) {
final stream = _clientStream.stream;
// wait until we're connected
stream.firstWhere((socket) => socket != null).then((_) {
_currentClient.listen((data) {
print('I: $data');
onRequest(Request.fromJson(
json.decode(data as String) as Map<String, dynamic>));
});
});
stream.firstWhere((socket) => socket == null).then((_) => onDone());
}
@override
void sendNotification(Notification notification) {
print('N: ${notification.toJson()}');
_currentClient?.add(json.encode(notification.toJson()));
}
@override
void sendResponse(Response response) {
print('O: ${response.toJson()}');
_currentClient?.add(json.encode(response.toJson()));
}
}
You can then write a new Dart file where the main method looks like
void main() {
final plugin = YourAnalysisServerPlugin();
plugin.start(_WebSocketPluginServer());
}
To debug the plugin, all you need to do is
- Run the file with the
main
method from an IDE. You have full control over that process, so you can debug it. - Start another IDE with code that uses the plugin. It will start an analysis server that loads the proxy, which will then connect to your server.
The server will also print incoming and outgoing messages, which can be useful to find errors.