OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, 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 dart2js_incremental.server; | |
6 | |
7 import 'dart:io'; | |
8 | |
9 import 'dart:async' show | |
10 Completer, | |
11 Future, | |
12 Stream, | |
13 StreamController, | |
14 StreamSubscription; | |
15 | |
16 import 'dart:convert' show | |
17 HtmlEscape, | |
18 JSON, | |
19 UTF8; | |
20 | |
21 import 'src/options.dart'; | |
22 | |
23 import 'compiler.dart' show | |
24 CompilerEvent, | |
25 IncrementalKind, | |
26 compile; | |
27 | |
28 class Conversation { | |
29 HttpRequest request; | |
30 HttpResponse response; | |
31 | |
32 static const String PACKAGES_PATH = '/packages'; | |
33 | |
34 static const String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE; | |
35 | |
36 static Uri documentRoot = Uri.base; | |
37 | |
38 static Uri packageRoot = Uri.base.resolve('packages/'); | |
39 | |
40 static Map<Uri, Future<String>> generatedFiles = | |
41 new Map<Uri, Future<String>>(); | |
42 | |
43 static Map<Uri, StreamController<String>> updateControllers = | |
44 new Map<Uri, StreamController<String>>(); | |
45 | |
46 Conversation(this.request, this.response); | |
47 | |
48 onClosed(_) { | |
49 if (response.statusCode == HttpStatus.OK) return; | |
50 print('Request for ${request.uri} ${response.statusCode}'); | |
51 } | |
52 | |
53 Future notFound(Uri uri) { | |
54 response | |
55 ..headers.set(CONTENT_TYPE, 'text/html') | |
56 ..statusCode = HttpStatus.NOT_FOUND | |
57 ..write(htmlInfo("Not Found", "The file '$uri' could not be found.")); | |
58 return response.close(); | |
59 } | |
60 | |
61 Future badRequest(String problem) { | |
62 response | |
63 ..headers.set(CONTENT_TYPE, 'text/html') | |
64 ..statusCode = HttpStatus.BAD_REQUEST | |
65 ..write( | |
66 htmlInfo("Bad request", "Bad request '${request.uri}': $problem")); | |
67 return response.close(); | |
68 } | |
69 | |
70 Future handleSocket() async { | |
71 StreamController<String> controller = updateControllers[request.uri]; | |
72 if (controller != null) { | |
73 WebSocket socket = await WebSocketTransformer.upgrade(request); | |
74 print( | |
75 "Patches to ${request.uri} will be pushed to " | |
76 "${request.connectionInfo.remoteAddress.host}:" | |
77 "${request.connectionInfo.remotePort}."); | |
78 controller.stream.pipe(socket); | |
79 } else { | |
80 response.done | |
81 .then(onClosed) | |
82 .catchError(onError); | |
83 return await notFound(request.uri); | |
84 } | |
85 } | |
86 | |
87 Future handle() { | |
88 response.done | |
89 .then(onClosed) | |
90 .catchError(onError); | |
91 | |
92 Uri uri = request.uri; | |
93 if (uri.path.endsWith('/')) { | |
94 uri = uri.resolve('index.html'); | |
95 } | |
96 if (uri.path.contains('..') || uri.path.contains('%')) { | |
97 return notFound(uri); | |
98 } | |
99 String path = uri.path; | |
100 Uri root = documentRoot; | |
101 if (path.startsWith('${PACKAGES_PATH}/')) { | |
102 root = packageRoot; | |
103 path = path.substring(PACKAGES_PATH.length); | |
104 } | |
105 | |
106 Uri resolvedRequest = root.resolve('.$path'); | |
107 switch (request.method) { | |
108 case 'GET': | |
109 return handleGet(resolvedRequest); | |
110 default: | |
111 String method = const HtmlEscape().convert(request.method); | |
112 return badRequest("Unsupported method: '$method'"); | |
113 } | |
114 } | |
115 | |
116 Future handleGet(Uri uri) async { | |
117 String path = uri.path; | |
118 var f = new File.fromUri(uri); | |
119 if (!await f.exists()) { | |
120 return await handleNonExistingFile(uri); | |
121 } else { | |
122 setContentType(path); | |
123 } | |
124 return await f.openRead().pipe(response); | |
125 } | |
126 | |
127 void setContentType(String path) { | |
128 if (path.endsWith('.html')) { | |
129 response.headers.set(CONTENT_TYPE, 'text/html'); | |
130 } else if (path.endsWith('.dart')) { | |
131 response.headers.set(CONTENT_TYPE, 'application/dart'); | |
132 } else if (path.endsWith('.js')) { | |
133 response.headers.set(CONTENT_TYPE, 'application/javascript'); | |
134 } else if (path.endsWith('.ico')) { | |
135 response.headers.set(CONTENT_TYPE, 'image/x-icon'); | |
136 } else if (path.endsWith('.appcache')) { | |
137 response.headers.set(CONTENT_TYPE, 'text/cache-manifest'); | |
138 } else if (path.endsWith('.css')) { | |
139 response.headers.set(CONTENT_TYPE, 'text/css'); | |
140 } else if (path.endsWith('.png')) { | |
141 response.headers.set(CONTENT_TYPE, 'image/png'); | |
142 } | |
143 } | |
144 | |
145 Future handleNonExistingFile(Uri uri) async { | |
146 String path = uri.path; | |
147 String generated = await generatedFiles[request.uri]; | |
148 if (generated != null) { | |
149 print("Serving ${request.uri} from memory."); | |
150 setContentType(path); | |
151 response.write(generated); | |
152 return await response.close(); | |
153 } | |
154 if (path.endsWith('.dart.js')) { | |
155 Uri dartScript = uri.resolve(path.substring(0, path.length - 3)); | |
156 if (await new File.fromUri(dartScript).exists()) { | |
157 return await compileToJavaScript(dartScript); | |
158 } | |
159 } | |
160 return await notFound(request.uri); | |
161 } | |
162 | |
163 compileToJavaScript(Uri dartScript) { | |
164 Uri outputUri = request.uri; | |
165 Completer<String> completer = new Completer<String>(); | |
166 generatedFiles[outputUri] = completer.future; | |
167 StreamController controller = updateControllers[outputUri]; | |
168 if (controller != null) { | |
169 controller.close(); | |
170 } | |
171 updateControllers[outputUri] = new StreamController<String>.broadcast(); | |
172 print("Compiling $dartScript to $outputUri."); | |
173 StreamSubscription<CompilerEvent> subscription; | |
174 subscription = compile(dartScript).listen((CompilerEvent event) { | |
175 subscription.onData( | |
176 (CompilerEvent event) => onCompilerEvent(completer, event)); | |
177 if (event.kind != IncrementalKind.FULL) { | |
178 notFound(request.uri); | |
179 // TODO(ahe): Do something about this situation. | |
180 } else { | |
181 print("Done compiling $dartScript to $outputUri."); | |
182 completer.complete(event['.js']); | |
183 setContentType(outputUri.path); | |
184 response.write(event['.js']); | |
185 response.close(); | |
186 } | |
187 }); | |
188 } | |
189 | |
190 onCompilerEvent(Completer completer, CompilerEvent event) { | |
191 Uri outputUri = request.uri; | |
192 print("Got ${event.kind} for $outputUri"); | |
193 | |
194 switch (event.kind) { | |
195 case IncrementalKind.FULL: | |
196 generatedFiles[outputUri] = new Future.value(event['.js']); | |
197 break; | |
198 | |
199 case IncrementalKind.INCREMENTAL: | |
200 generatedFiles[outputUri] = completer.future.then( | |
201 (String full) => '$full\n\n${event.compiler.allUpdates()}'); | |
202 pushUpdates(event.updates); | |
203 break; | |
204 | |
205 case IncrementalKind.ERROR: | |
206 generatedFiles.removeKey(outputUri); | |
207 break; | |
208 } | |
209 } | |
210 | |
211 void pushUpdates(String updates) { | |
212 if (updates == null) return; | |
213 StreamController<String> controller = updateControllers[request.uri]; | |
214 if (controller == null) return; | |
215 print("Adding updates to controller"); | |
216 controller.add(updates); | |
217 } | |
218 | |
219 Future dispatch() async { | |
220 try { | |
221 return await WebSocketTransformer.isUpgradeRequest(request) | |
222 ? handleSocket() | |
223 : handle(); | |
224 } catch (e, s) { | |
225 onError(e, s); | |
226 } | |
227 } | |
228 | |
229 static Future onRequest(HttpRequest request) async { | |
230 HttpResponse response = request.response; | |
231 try { | |
232 return await new Conversation(request, response).dispatch(); | |
233 } catch (e, s) { | |
234 try { | |
235 onStaticError(e, s); | |
236 return await response.close(); | |
237 } catch (e, s) { | |
238 onStaticError(e, s); | |
239 } | |
240 } | |
241 } | |
242 | |
243 Future onError(error, [stack]) async { | |
244 try { | |
245 onStaticError(error, stack); | |
246 return await response.close(); | |
247 } catch (e, s) { | |
248 onStaticError(e, s); | |
249 } | |
250 } | |
251 | |
252 static void onStaticError(error, [stack]) { | |
253 if (error is HttpException) { | |
254 print('Error: ${error.message}'); | |
255 } else { | |
256 print('Error: ${error}'); | |
257 } | |
258 if (stack != null) { | |
259 print(stack); | |
260 } | |
261 } | |
262 | |
263 String htmlInfo(String title, String text) { | |
264 // No script injection, please. | |
265 title = const HtmlEscape().convert(title); | |
266 text = const HtmlEscape().convert(text); | |
267 return """ | |
268 <!DOCTYPE html> | |
269 <html lang='en'> | |
270 <head> | |
271 <title>$title</title> | |
272 </head> | |
273 <body> | |
274 <h1>$title</h1> | |
275 <p style='white-space:pre'>$text</p> | |
276 </body> | |
277 </html> | |
278 """; | |
279 } | |
280 } | |
281 | |
282 main(List<String> arguments) async { | |
283 Options options = Options.parse(arguments); | |
284 if (options == null) { | |
285 exit(1); | |
286 } | |
287 if (!options.arguments.isEmpty) { | |
288 Conversation.documentRoot = Uri.base.resolve(options.arguments.single); | |
289 } | |
290 Conversation.packageRoot = options.packageRoot; | |
291 String host = options.host; | |
292 int port = options.port; | |
293 try { | |
294 HttpServer server = await HttpServer.bind(host, port); | |
295 print('HTTP server started on http://$host:${server.port}/'); | |
296 server.listen(Conversation.onRequest, onError: Conversation.onStaticError); | |
297 } catch (e) { | |
298 print("HttpServer.bind error: $e"); | |
299 exit(1); | |
300 }; | |
301 } | |
OLD | NEW |