| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 library server.manager; | |
| 6 | |
| 7 import 'dart:async'; | |
| 8 import 'dart:convert'; | |
| 9 import 'dart:io'; | |
| 10 | |
| 11 import 'package:matcher/matcher.dart'; | |
| 12 import 'package:analysis_server/src/protocol.dart'; | |
| 13 import 'package:analysis_server/src/channel/channel.dart'; | |
| 14 import 'package:analysis_server/src/channel/byte_stream_channel.dart'; | |
| 15 | |
| 16 part 'logging_client_channel.dart'; | |
| 17 | |
| 18 /** | |
| 19 * The results returned by [ServerManager].analyze(...) once analysis | |
| 20 * has finished. | |
| 21 */ | |
| 22 class AnalysisResults { | |
| 23 Duration elapsed; | |
| 24 int errorCount = 0; | |
| 25 int hintCount = 0; | |
| 26 int warningCount = 0; | |
| 27 } | |
| 28 | |
| 29 | |
| 30 /** | |
| 31 * [CompletionResults] contains the completion results returned by the server | |
| 32 * along with the elapse time to receive those completions. | |
| 33 */ | |
| 34 class CompletionResults { | |
| 35 final Duration elapsed; | |
| 36 final CompletionResultsParams params; | |
| 37 | |
| 38 CompletionResults(this.elapsed, this.params); | |
| 39 | |
| 40 int get suggestionCount => params.results.length; | |
| 41 } | |
| 42 | |
| 43 /** | |
| 44 * [Editor] is a virtual editor for inspecting and modifying a file's content | |
| 45 * and updating the server with those modifications. | |
| 46 */ | |
| 47 class Editor { | |
| 48 final ServerManager manager; | |
| 49 final File file; | |
| 50 int offset = 0; | |
| 51 String _content = null; | |
| 52 | |
| 53 Editor(this.manager, this.file); | |
| 54 | |
| 55 /// Return a future that returns the file content | |
| 56 Future<String> get content { | |
| 57 if (_content != null) { | |
| 58 return new Future.value(_content); | |
| 59 } | |
| 60 return file.readAsString().then((String content) { | |
| 61 _content = content; | |
| 62 return _content; | |
| 63 }); | |
| 64 } | |
| 65 | |
| 66 /** | |
| 67 * Request completion suggestions from the server. | |
| 68 * Return a future that completes with the completions sent. | |
| 69 */ | |
| 70 Future<List<CompletionResults>> getSuggestions() { | |
| 71 Request request = new CompletionGetSuggestionsParams( | |
| 72 file.path, | |
| 73 offset).toRequest(manager._nextRequestId); | |
| 74 Stopwatch stopwatch = new Stopwatch()..start(); | |
| 75 return manager.channel.sendRequest(request).then((Response response) { | |
| 76 String completionId = | |
| 77 new CompletionGetSuggestionsResult.fromResponse(response).id; | |
| 78 var completer = new Completer<List<CompletionResults>>(); | |
| 79 List<CompletionResults> results = []; | |
| 80 | |
| 81 // Listen for completion suggestions | |
| 82 StreamSubscription<Notification> subscription; | |
| 83 subscription = | |
| 84 manager.channel.notificationStream.listen((Notification notification)
{ | |
| 85 if (notification.event == 'completion.results') { | |
| 86 CompletionResultsParams params = | |
| 87 new CompletionResultsParams.fromNotification(notification); | |
| 88 if (params.id == completionId) { | |
| 89 results.add(new CompletionResults(stopwatch.elapsed, params)); | |
| 90 if (params.isLast) { | |
| 91 stopwatch.stop(); | |
| 92 subscription.cancel(); | |
| 93 completer.complete(results); | |
| 94 } | |
| 95 } | |
| 96 } | |
| 97 }); | |
| 98 | |
| 99 return completer.future; | |
| 100 }); | |
| 101 } | |
| 102 | |
| 103 /** | |
| 104 * Move the virtual cursor after the given pattern in the source. | |
| 105 * Return a future that completes once the cursor has been moved. | |
| 106 */ | |
| 107 Future<Editor> moveAfter(String pattern) { | |
| 108 return content.then((String content) { | |
| 109 offset = content.indexOf(pattern); | |
| 110 return this; | |
| 111 }); | |
| 112 } | |
| 113 | |
| 114 /** | |
| 115 * Replace the specified number of characters at the current cursor location | |
| 116 * with the given text, but do not save that content to disk. | |
| 117 * Return a future that completes once the server has been notified. | |
| 118 */ | |
| 119 Future<Editor> replace(int replacementLength, String text) { | |
| 120 return content.then((String oldContent) { | |
| 121 StringBuffer sb = new StringBuffer(); | |
| 122 sb.write(oldContent.substring(0, offset)); | |
| 123 sb.write(text); | |
| 124 sb.write(oldContent.substring(offset)); | |
| 125 _content = sb.toString(); | |
| 126 SourceEdit sourceEdit = new SourceEdit(offset, replacementLength, text); | |
| 127 Request request = new AnalysisUpdateContentParams({ | |
| 128 file.path: new ChangeContentOverlay([sourceEdit]) | |
| 129 }).toRequest(manager._nextRequestId); | |
| 130 offset += text.length; | |
| 131 return manager.channel.sendRequest(request).then((Response response) { | |
| 132 return this; | |
| 133 }); | |
| 134 }); | |
| 135 } | |
| 136 } | |
| 137 | |
| 138 /** | |
| 139 * [ServerManager] is used to launch and manage an analysis server | |
| 140 * running in a separate process. | |
| 141 */ | |
| 142 class ServerManager { | |
| 143 | |
| 144 /** | |
| 145 * The analysis server process being managed or `null` if not started. | |
| 146 */ | |
| 147 Process process; | |
| 148 | |
| 149 /** | |
| 150 * The root directory containing the Dart source files to be analyzed. | |
| 151 */ | |
| 152 Directory appDir; | |
| 153 | |
| 154 /** | |
| 155 * The channel used to communicate with the analysis server. | |
| 156 */ | |
| 157 LoggingClientChannel _channel; | |
| 158 | |
| 159 /** | |
| 160 * The identifier used in the most recent request to the server. | |
| 161 * See [_nextRequestId]. | |
| 162 */ | |
| 163 int _lastRequestId = 0; | |
| 164 | |
| 165 /** | |
| 166 * `true` if a server exception was detected on stderr as opposed to an | |
| 167 * exception that the server reported via the server.error notification. | |
| 168 */ | |
| 169 bool _unreportedServerException = false; | |
| 170 | |
| 171 /** | |
| 172 * `true` if the [stop] method has been called. | |
| 173 */ | |
| 174 bool _stopRequested = false; | |
| 175 | |
| 176 /** | |
| 177 * Return the channel used to communicate with the analysis server. | |
| 178 */ | |
| 179 ClientCommunicationChannel get channel => _channel; | |
| 180 | |
| 181 /** | |
| 182 * Return `true` if a server error occurred. | |
| 183 */ | |
| 184 bool get errorOccurred => | |
| 185 _unreportedServerException || (_channel.serverErrorCount > 0); | |
| 186 | |
| 187 String get _nextRequestId => (++_lastRequestId).toString(); | |
| 188 | |
| 189 /** | |
| 190 * Direct the server to analyze all sources in the given directory, | |
| 191 * all sub directories recursively, and any source referenced sources | |
| 192 * outside this directory hierarch such as referenced packages. | |
| 193 * Return a future that completes when the analysis is finished. | |
| 194 */ | |
| 195 Future<AnalysisResults> analyze(Directory appDir) { | |
| 196 this.appDir = appDir; | |
| 197 Stopwatch stopwatch = new Stopwatch()..start(); | |
| 198 Request request = new AnalysisSetAnalysisRootsParams( | |
| 199 [appDir.path], | |
| 200 []).toRequest(_nextRequestId); | |
| 201 | |
| 202 // Request analysis | |
| 203 return channel.sendRequest(request).then((Response response) { | |
| 204 AnalysisResults results = new AnalysisResults(); | |
| 205 StreamSubscription<Notification> subscription; | |
| 206 Completer<AnalysisResults> completer = new Completer<AnalysisResults>(); | |
| 207 subscription = | |
| 208 channel.notificationStream.listen((Notification notification) { | |
| 209 | |
| 210 // Gather analysis results | |
| 211 if (notification.event == 'analysis.errors') { | |
| 212 AnalysisErrorsParams params = | |
| 213 new AnalysisErrorsParams.fromNotification(notification); | |
| 214 params.errors.forEach((AnalysisError error) { | |
| 215 AnalysisErrorSeverity severity = error.severity; | |
| 216 if (severity == AnalysisErrorSeverity.ERROR) { | |
| 217 results.errorCount += 1; | |
| 218 } else if (severity == AnalysisErrorSeverity.WARNING) { | |
| 219 results.warningCount += 1; | |
| 220 } else if (severity == AnalysisErrorSeverity.INFO) { | |
| 221 results.hintCount += 1; | |
| 222 } else { | |
| 223 print('Unknown error severity: ${severity.name}'); | |
| 224 } | |
| 225 }); | |
| 226 } | |
| 227 | |
| 228 // Stop gathering once analysis is complete | |
| 229 if (notification.event == 'server.status') { | |
| 230 ServerStatusParams status = | |
| 231 new ServerStatusParams.fromNotification(notification); | |
| 232 AnalysisStatus analysis = status.analysis; | |
| 233 if (analysis != null && !analysis.isAnalyzing) { | |
| 234 stopwatch.stop(); | |
| 235 results.elapsed = stopwatch.elapsed; | |
| 236 subscription.cancel(); | |
| 237 completer.complete(results); | |
| 238 } | |
| 239 } | |
| 240 }); | |
| 241 return completer.future; | |
| 242 }); | |
| 243 } | |
| 244 | |
| 245 /** | |
| 246 * Send a request to the server for its version information | |
| 247 * and return a future that completes with the result. | |
| 248 */ | |
| 249 Future<ServerGetVersionResult> getVersion() { | |
| 250 Request request = new ServerGetVersionParams().toRequest(_nextRequestId); | |
| 251 return channel.sendRequest(request).then((Response response) { | |
| 252 return new ServerGetVersionResult.fromResponse(response); | |
| 253 }); | |
| 254 } | |
| 255 | |
| 256 /** | |
| 257 * Notify the server that the given file will be edited. | |
| 258 * Return a virtual editor for inspecting and modifying the file's content. | |
| 259 */ | |
| 260 Future<Editor> openFileNamed(String fileName) { | |
| 261 return _findFile(fileName, appDir).then((File file) { | |
| 262 if (file == null) { | |
| 263 throw 'Failed to find file named $fileName in ${appDir.path}'; | |
| 264 } | |
| 265 file = file.absolute; | |
| 266 Request request = | |
| 267 new AnalysisSetPriorityFilesParams([file.path]).toRequest(_nextRequest
Id); | |
| 268 return channel.sendRequest(request).then((Response response) { | |
| 269 return new Editor(this, file); | |
| 270 }); | |
| 271 }); | |
| 272 } | |
| 273 | |
| 274 /** | |
| 275 * Send a request for notifications. | |
| 276 * Return when the server has acknowledged that request. | |
| 277 */ | |
| 278 Future setSubscriptions() { | |
| 279 Request request = new ServerSetSubscriptionsParams( | |
| 280 [ServerService.STATUS]).toRequest(_nextRequestId); | |
| 281 return channel.sendRequest(request); | |
| 282 } | |
| 283 | |
| 284 /** | |
| 285 * Stop the analysis server. | |
| 286 * Return a future that completes when the server is terminated. | |
| 287 */ | |
| 288 Future stop([_]) { | |
| 289 _stopRequested = true; | |
| 290 print("Requesting server shutdown"); | |
| 291 Request request = new ServerShutdownParams().toRequest(_nextRequestId); | |
| 292 Duration waitTime = new Duration(seconds: 5); | |
| 293 return channel.sendRequest(request).timeout(waitTime, onTimeout: () { | |
| 294 print('Expected shutdown response'); | |
| 295 }).then((Response response) { | |
| 296 return channel.close().then((_) => process.exitCode); | |
| 297 }).timeout(new Duration(seconds: 2), onTimeout: () { | |
| 298 print('Expected server to shutdown'); | |
| 299 process.kill(); | |
| 300 }); | |
| 301 } | |
| 302 | |
| 303 /** | |
| 304 * Locate the given file in the directory tree. | |
| 305 */ | |
| 306 Future<File> _findFile(String fileName, Directory appDir) { | |
| 307 return appDir.list(recursive: true).firstWhere((FileSystemEntity entity) { | |
| 308 return entity is File && entity.path.endsWith(fileName); | |
| 309 }); | |
| 310 } | |
| 311 | |
| 312 /** | |
| 313 * Launch an analysis server and open a connection to that server. | |
| 314 */ | |
| 315 Future<ServerManager> _launchServer(String pathToServer) { | |
| 316 List<String> serverArgs = [pathToServer]; | |
| 317 return Process.start(Platform.executable, serverArgs).catchError((error) { | |
| 318 exitCode = 21; | |
| 319 throw 'Failed to launch analysis server: $error'; | |
| 320 }).then((Process process) { | |
| 321 this.process = process; | |
| 322 _channel = new LoggingClientChannel( | |
| 323 new ByteStreamClientChannel(process.stdout, process.stdin)); | |
| 324 | |
| 325 // simple out of band exception handling | |
| 326 process.stderr.transform( | |
| 327 new Utf8Codec().decoder).transform(new LineSplitter()).listen((String
line) { | |
| 328 if (!_unreportedServerException) { | |
| 329 _unreportedServerException = true; | |
| 330 stderr.writeln('>>> Unreported server exception'); | |
| 331 } | |
| 332 stderr.writeln('server.stderr: $line'); | |
| 333 }); | |
| 334 | |
| 335 // watch for unexpected process termination and catch the exit code | |
| 336 process.exitCode.then((int code) { | |
| 337 if (!_stopRequested) { | |
| 338 fail('Unexpected server termination: $code'); | |
| 339 } | |
| 340 if (code != null && code != 0) { | |
| 341 exitCode = code; | |
| 342 } | |
| 343 print('Server stopped: $code'); | |
| 344 }); | |
| 345 | |
| 346 return channel.notificationStream.first.then((Notification notification) { | |
| 347 print('Server connection established'); | |
| 348 return setSubscriptions().then((_) { | |
| 349 return getVersion().then((ServerGetVersionResult result) { | |
| 350 print('Server version ${result.version}'); | |
| 351 return this; | |
| 352 }); | |
| 353 }); | |
| 354 }); | |
| 355 }); | |
| 356 } | |
| 357 | |
| 358 /** | |
| 359 * Launch analysis server in a separate process | |
| 360 * and return a future with a manager for that analysis server. | |
| 361 */ | |
| 362 static Future<ServerManager> start(String serverPath) { | |
| 363 return new ServerManager()._launchServer(serverPath); | |
| 364 } | |
| 365 } | |
| OLD | NEW |