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