| 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 |