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 |