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 |