Expose plugin debug server as cli command

This commit is contained in:
Simon Binder 2019-12-15 21:38:46 +01:00
parent 49a1e8253e
commit ae91b5d526
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
6 changed files with 140 additions and 96 deletions

View File

@ -66,13 +66,19 @@ used to construct common queries.
## Workflows
### Debugging the analyzer plugin
The analyzer plugin (branded as "SQL IDE" to users) can be run in isolation, which makes
debugging easier. Note: Port 9999 has to be free for this to work, but you can change the
We have an analyzer plugin to support IDE features like auto-complete, navigation, syntax
highlighting, outline and folding to users. Normally, analyzer plugins are discovered and
loaded by the analysis server, which makes them very annoying to debug.
However, we found a way to run the plugin in isolation, which makes debugging much easier.
Note: Port 9999 has to be free for this to work, but you can change the
port defined in the two files below.
To debug the plugin, do the following:
1. In `moor/tools/analyzer_plugin/bin/plugin.dart`, set `useDebuggingVariant` to true.
2. Run `moor_generator/lib/plugin.dart` as a regular Dart VM app (this can be debugged).
2. Run `moor_generator/bin/moor_generator.dart debug-plugin` as a regular Dart VM app
(this can be debugged when started from an IDE).
3. (optional) Make sure the analysis server picks up the updated version of the analysis
plugin by deleting the `~/.dartServer/.plugin_manager` folder.
4. Open a project that uses the plugin, for instance via `code extras/plugin_example`.

View File

@ -28,14 +28,15 @@ Note: If you only want to _use_ the plugin and don't care about debugging it, fo
from the [user documentation](https://moor.simonbinder.eu/docs/using-sql/sql_ide/).
After you completed the setup, these steps will open an editor instance that runs the plugin.
1. chdir into `moor_generator` and run `lib/plugin.dart`. You can run that file from an IDE if
you need debugging capabilities, but starting it from the command line is fine. Keep that
script running.
2. Open this folder in the code instance
3. Wait ~15s, you should start to see some log entries in the output of step 1.
1. chdir into `moor_generator` and run `dart bin/moor_generator.dart debug-plugin`.
You can run that script from an IDE if you need debugging capabilities, but starting
it from the command line is fine. Keep that script running.
3. Uncomment the `plugin` lines in `analysis_options.yaml`
3. Open this folder in the code instance
4. Wait ~15s, you should start to see some log entries in the output of step 1.
As soon as they appear, the plugin is ready to go.
_Note_: `lib/plugin.dart` doesn't support multiple clients. Whenever you close or reload the
_Note_: `bin/moor_generator.dart` doesn't support multiple clients. Whenever you close or reload the
editor, that script needs to be restarted as well. That script should also be running before
starting the analysis server.
@ -43,7 +44,7 @@ starting the analysis server.
If the plugin doesn't start properly, you can
1. make sure it was picked up by the analysis server: You set the `dart.analyzerDiagnosticsPort`
1. make sure it was picked up by the analysis server: Set the `dart.analyzerDiagnosticsPort`
to any port and see some basic information under the "plugins" tab of the website started.
2. When setting `dart.analyzerInstrumentationLogFile`, the analysis server will write the
exception that caused the plugin to stop

View File

@ -1,92 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer_plugin/channel/channel.dart';
import 'package:analyzer_plugin/protocol/protocol.dart';
import 'package:analyzer_plugin/starter.dart';
import 'package:moor_generator/src/backends/plugin/plugin.dart';
void start(List<String> args, SendPort sendPort) {
ServerPluginStarter(MoorPlugin(PhysicalResourceProvider.INSTANCE))
.start(sendPort);
}
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()));
}
}
/// Starts the plugin over a websocket service.
void main() {
MoorPlugin(PhysicalResourceProvider.INSTANCE).start(WebSocketPluginServer());
ServerPluginStarter(MoorPlugin.forProduction()).start(sendPort);
}

View File

@ -1,4 +1,5 @@
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/context/builder.dart'; // ignore: implementation_imports
import 'package:analyzer/src/context/context_root.dart'; // ignore: implementation_imports
// ignore: implementation_imports
@ -47,6 +48,10 @@ class MoorPlugin extends ServerPlugin
dartScheduler.start();
}
factory MoorPlugin.forProduction() {
return MoorPlugin(PhysicalResourceProvider.INSTANCE);
}
@override
final List<String> fileGlobsToAnalyze = const ['*.moor'];
@override

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:args/command_runner.dart';
import 'package:moor_generator/src/backends/standalone.dart';
import 'package:moor_generator/src/cli/commands/debug_plugin.dart';
import 'commands/identify_databases.dart';
@ -27,7 +28,9 @@ class MoorCli {
_runner = CommandRunner(
'pub run moor_generator',
'CLI utilities for the moor package, currently in an experimental state.',
)..addCommand(IdentifyDatabases(this));
)
..addCommand(IdentifyDatabases(this))
..addCommand(DebugPluginCommand(this));
_analyzerReadyCompleter.complete(_analyzer.init());
}

View File

@ -0,0 +1,113 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:analyzer_plugin/channel/channel.dart';
import 'package:analyzer_plugin/protocol/protocol.dart';
import 'package:moor_generator/src/backends/plugin/plugin.dart';
import '../cli.dart';
class DebugPluginCommand extends MoorCommand {
DebugPluginCommand(MoorCli cli) : super(cli) {
argParser.addOption(
'port',
abbr: 'p',
help: 'The port to use when starting the websocket server',
defaultsTo: '9999',
);
}
@override
String get name => 'debug-plugin';
@override
String get description => 'Start the analyzer plugin on a websocket server';
@override
bool get hidden => true;
@override
void run() {
final port = int.tryParse(argResults['port'] as String);
if (port == null) {
print('Port must be an int');
printUsage();
return;
}
MoorPlugin.forProduction().start(_WebSocketPluginServer(port: port));
}
}
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()));
}
}