| // Copyright (c) 2022, 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 'dart:convert'; |
| import 'dart:io'; |
| |
| Future<void> main(List<String> args) async { |
| print(''' |
| ================================================================================ |
| Stress test tool for language-server protocol. |
| |
| Example run: |
| out/ReleaseX64/dart-sdk/bin/dart \\ |
| pkg/analysis_server/tool/lspTestWithParameters.dart \\ |
| --root=pkg/analysis_server \\ |
| --sdk=out/ReleaseX64/dart-sdk/ \\ |
| --click=pkg/analyzer/lib/src/dart/analysis/driver.dart \\ |
| --line=506 \\ |
| --column=20 |
| |
| Additional options: |
| -v / --verbose Be more verbose. Specify several times for more verbosity. |
| --verbosity=<int> Set verbosity directly. Defaults to 0. A higher number is |
| more verbose. |
| --every=<int> Set how often - in ms - to fire an event. Defaults to 100. |
| |
| ================================================================================ |
| '''); |
| { |
| Uri exe = Uri.base.resolve(Platform.resolvedExecutable); |
| Uri librariesDart = |
| exe.resolve('../lib/_internal/sdk_library_metadata/lib/libraries.dart'); |
| if (!File.fromUri(librariesDart).existsSync()) { |
| throw 'Execute with a dart that has ' |
| "'../lib/_internal/sdk_library_metadata/lib/libraries.dart' " |
| 'available (e.g. out/ReleaseX64/dart-sdk/bin/dart)'; |
| } |
| } |
| Uri? rootUri; |
| Uri? sdkUri; |
| Uri? clickOnUri; |
| int? clickLine; |
| int? clickColumn; |
| int everyMs = 100; |
| for (String arg in args) { |
| if (arg.startsWith('--root=')) { |
| rootUri = Uri.base.resolve(arg.substring('--root='.length).trim()); |
| } else if (arg.startsWith('--sdk=')) { |
| sdkUri = Uri.base.resolve(arg.substring('--sdk='.length).trim()); |
| } else if (arg.startsWith('--click=')) { |
| clickOnUri = Uri.base.resolve(arg.substring('--click='.length).trim()); |
| } else if (arg.startsWith('--line=')) { |
| clickLine = int.parse(arg.substring('--line='.length).trim()); |
| } else if (arg.startsWith('--column=')) { |
| clickColumn = int.parse(arg.substring('--column='.length).trim()); |
| } else if (arg.startsWith('--every=')) { |
| everyMs = int.parse(arg.substring('--every='.length).trim()); |
| } else if (arg == '--verbose' || arg == '-v') { |
| verbosity++; |
| } else if (arg.startsWith('--verbosity=')) { |
| verbosity = int.parse(arg.substring('--verbosity='.length).trim()); |
| } else { |
| throw 'Unknown argument: $arg'; |
| } |
| } |
| |
| if (rootUri == null) { |
| throw "Didn't get a root uri. Specify with --root="; |
| } |
| if (!Directory.fromUri(rootUri).existsSync()) { |
| throw "Directory $rootUri doesn't exist. " |
| 'Specify existing directory with --root='; |
| } |
| |
| if (sdkUri == null) { |
| throw "Didn't get a sdk path. Specify with --sdk="; |
| } |
| if (!Directory.fromUri(sdkUri).existsSync()) { |
| throw "Directory $sdkUri doesn't exist. " |
| 'Specify existing directory with --sdk='; |
| } |
| |
| if (clickOnUri == null) { |
| throw "Didn't get a sdk path. Specify with --click="; |
| } |
| if (!File.fromUri(clickOnUri).existsSync()) { |
| throw "File $clickOnUri doesn't exist. " |
| 'Specify existing file with --click='; |
| } |
| |
| if (clickLine == null) { |
| throw "Didn't get a line to click on. Specify with --line="; |
| } |
| if (clickColumn == null) { |
| throw "Didn't get a column to click on. Specify with --column="; |
| } |
| |
| Process p = await Process.start(Platform.resolvedExecutable, [ |
| 'language-server', |
| ]); |
| |
| p.stdout.listen(listenToStdout); |
| Timer.periodic(const Duration(seconds: 1), (timer) { |
| bool reportedSomething = false; |
| for (MapEntry<int, Stopwatch> waitingFor |
| in outstandingRequestsWithId.entries) { |
| if (waitingFor.value.elapsed > const Duration(seconds: 1)) { |
| if (!reportedSomething) { |
| print('----'); |
| reportedSomething = true; |
| } |
| print('==> Has been waiting for ${waitingFor.key} for ' |
| '${waitingFor.value.elapsed}'); |
| } |
| } |
| if (reportedSomething) { |
| print('----'); |
| } else { |
| print(' -- not waiting for anything -- '); |
| } |
| }); |
| |
| await send(p, initMessage(pid, rootUri)); |
| await receivedCompleter.future; |
| await send(p, initNotification); |
| await receivedCompleter.future; |
| await send(p, initMore(sdkUri)); |
| await receivedCompleter.future; |
| |
| // Try to let it get done... |
| await Future.delayed(const Duration(seconds: 2)); |
| |
| Duration everyDuration = Duration(milliseconds: everyMs); |
| while (true) { |
| await send( |
| p, |
| gotoDef( |
| largestIdSeen + 1, |
| clickOnUri, |
| clickLine, |
| clickColumn, |
| )); |
| await Future.delayed(everyDuration); |
| } |
| } |
| |
| final buffer = <int>[]; |
| |
| int? headerContentLength; |
| |
| Map<String, dynamic> initNotification = { |
| 'jsonrpc': '2.0', |
| 'method': 'initialized', |
| 'params': {} |
| }; |
| |
| /// There's something weird about getting (several) id 3's that wasn't |
| /// requested... |
| int largestIdSeen = 3; |
| Map<int, Stopwatch> outstandingRequestsWithId = {}; |
| Completer<Map<String, dynamic>> receivedCompleter = Completer(); |
| |
| int verbosity = 0; |
| Map<String, dynamic> gotoDef(int id, Uri uri, int line, int char) { |
| return { |
| 'jsonrpc': '2.0', |
| 'id': id, |
| 'method': 'textDocument/definition', |
| 'params': { |
| 'textDocument': {'uri': '$uri'}, |
| 'position': {'line': line, 'character': char} |
| } |
| }; |
| } |
| |
| // Messages taken from what VSCode sent. |
| |
| Map<String, dynamic> initMessage(int processId, Uri rootUri) { |
| String rootPath = rootUri.toFilePath(); |
| String name = rootUri.pathSegments.last; |
| if (name.isEmpty) { |
| name = rootUri.pathSegments[rootUri.pathSegments.length - 2]; |
| } |
| return { |
| 'id': 0, |
| 'jsonrpc': '2.0', |
| 'method': 'initialize', |
| 'params': { |
| 'processId': processId, |
| 'clientInfo': {'name': 'lspTestScript', 'version': '0.0.1'}, |
| 'locale': 'en', |
| 'rootPath': rootPath, |
| 'rootUri': '$rootUri', |
| 'capabilities': {}, |
| 'initializationOptions': {}, |
| 'workspaceFolders': [ |
| {'uri': '$rootUri', 'name': rootUri.pathSegments.last} |
| ] |
| } |
| }; |
| } |
| |
| Map<String, dynamic> initMore(Uri sdkUri) { |
| String sdkPath = sdkUri.toFilePath(); |
| return { |
| 'id': 1, |
| 'jsonrpc': '2.0', |
| 'result': [ |
| { |
| 'useLsp': true, |
| 'sdkPath': sdkPath, |
| 'allowAnalytics': false, |
| } |
| ] |
| }; |
| } |
| |
| void listenToStdout(List<int> event) { |
| // General idea taken from |
| // pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart |
| for (int element in event) { |
| buffer.add(element); |
| if (verbosity > 3 && buffer.length % 1000 == 999) { |
| print('DEBUG MESSAGE: Stdout buffer with length ${buffer.length} so far: ' |
| '${utf8.decode(buffer)}'); |
| } |
| if (headerContentLength == null && _endsWithCrLfCrLf()) { |
| String headerRaw = utf8.decode(buffer); |
| buffer.clear(); |
| List<String> headers = headerRaw.split('\r\n'); |
| for (String header in headers) { |
| if (header.startsWith('Content-Length:')) { |
| String contentLength = |
| header.substring('Content-Length:'.length).trim(); |
| headerContentLength = int.parse(contentLength); |
| break; |
| } |
| } |
| } else if (headerContentLength != null && |
| buffer.length == headerContentLength!) { |
| String messageString = utf8.decode(buffer); |
| buffer.clear(); |
| headerContentLength = null; |
| Map<String, dynamic> message = |
| json.decode(messageString) as Map<String, dynamic>; |
| dynamic possibleId = message['id']; |
| if (possibleId is int) { |
| if (possibleId > largestIdSeen) { |
| largestIdSeen = possibleId; |
| } |
| |
| if (verbosity > 0) { |
| if (messageString.length > 100) { |
| print('Got message ${messageString.substring(0, 100)}...'); |
| } else { |
| print('Got message $messageString'); |
| } |
| } |
| |
| Stopwatch? stopwatch = outstandingRequestsWithId.remove(possibleId); |
| if (stopwatch != null) { |
| stopwatch.stop(); |
| if (verbosity > 2) { |
| print(' => Got response for $possibleId in ${stopwatch.elapsed}'); |
| } |
| } |
| } else if (verbosity > 1) { |
| if (messageString.length > 100) { |
| print('Got message ${messageString.substring(0, 100)}...'); |
| } else { |
| print('Got message $messageString'); |
| } |
| } |
| receivedCompleter.complete(message); |
| receivedCompleter = Completer(); |
| } |
| } |
| } |
| |
| Future<void> send(Process p, Map<String, dynamic> json) async { |
| // Mostly copied from |
| // pkg/analysis_server/lib/src/lsp/channel/lsp_byte_stream_channel.dart |
| var jsonEncodedBody = jsonEncode(json); |
| var utf8EncodedBody = utf8.encode(jsonEncodedBody); |
| var header = 'Content-Length: ${utf8EncodedBody.length}\r\n' |
| 'Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n'; |
| var asciiEncodedHeader = ascii.encode(header); |
| |
| dynamic possibleId = json['id']; |
| if (possibleId is int && possibleId > largestIdSeen) { |
| largestIdSeen = possibleId; |
| outstandingRequestsWithId[possibleId] = Stopwatch()..start(); |
| if (verbosity > 2) { |
| print('Sending message with id $possibleId'); |
| } |
| } |
| |
| // Header is always ascii, body is always utf8! |
| p.stdin.add(asciiEncodedHeader); |
| p.stdin.add(utf8EncodedBody); |
| await p.stdin.flush(); |
| if (verbosity > 2) { |
| print('\n\nMessage sent...\n\n'); |
| } |
| } |
| |
| /// Copied from pkg/analysis_server/lib/src/lsp/lsp_packet_transformer.dart. |
| bool _endsWithCrLfCrLf() { |
| var l = buffer.length; |
| return l > 4 && |
| buffer[l - 1] == 10 && |
| buffer[l - 2] == 13 && |
| buffer[l - 3] == 10 && |
| buffer[l - 4] == 13; |
| } |