3b1cb964ff
R=kasperl@google.com Review URL: https://codereview.chromium.org//847573003 git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@43063 260f80e4-7a28-3924-810f-c04153c831b5
302 lines
8.4 KiB
Dart
302 lines
8.4 KiB
Dart
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
library dart2js_incremental.server;
|
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:async' show
|
|
Completer,
|
|
Future,
|
|
Stream,
|
|
StreamController,
|
|
StreamSubscription;
|
|
|
|
import 'dart:convert' show
|
|
HtmlEscape,
|
|
JSON,
|
|
UTF8;
|
|
|
|
import 'src/options.dart';
|
|
|
|
import 'compiler.dart' show
|
|
CompilerEvent,
|
|
IncrementalKind,
|
|
compile;
|
|
|
|
class Conversation {
|
|
HttpRequest request;
|
|
HttpResponse response;
|
|
|
|
static const String PACKAGES_PATH = '/packages';
|
|
|
|
static const String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE;
|
|
|
|
static Uri documentRoot = Uri.base;
|
|
|
|
static Uri packageRoot = Uri.base.resolve('packages/');
|
|
|
|
static Map<Uri, Future<String>> generatedFiles =
|
|
new Map<Uri, Future<String>>();
|
|
|
|
static Map<Uri, StreamController<String>> updateControllers =
|
|
new Map<Uri, StreamController<String>>();
|
|
|
|
Conversation(this.request, this.response);
|
|
|
|
onClosed(_) {
|
|
if (response.statusCode == HttpStatus.OK) return;
|
|
print('Request for ${request.uri} ${response.statusCode}');
|
|
}
|
|
|
|
Future notFound(Uri uri) {
|
|
response
|
|
..headers.set(CONTENT_TYPE, 'text/html')
|
|
..statusCode = HttpStatus.NOT_FOUND
|
|
..write(htmlInfo("Not Found", "The file '$uri' could not be found."));
|
|
return response.close();
|
|
}
|
|
|
|
Future badRequest(String problem) {
|
|
response
|
|
..headers.set(CONTENT_TYPE, 'text/html')
|
|
..statusCode = HttpStatus.BAD_REQUEST
|
|
..write(
|
|
htmlInfo("Bad request", "Bad request '${request.uri}': $problem"));
|
|
return response.close();
|
|
}
|
|
|
|
Future handleSocket() async {
|
|
StreamController<String> controller = updateControllers[request.uri];
|
|
if (controller != null) {
|
|
WebSocket socket = await WebSocketTransformer.upgrade(request);
|
|
print(
|
|
"Patches to ${request.uri} will be pushed to "
|
|
"${request.connectionInfo.remoteAddress.host}:"
|
|
"${request.connectionInfo.remotePort}.");
|
|
controller.stream.pipe(socket);
|
|
} else {
|
|
response.done
|
|
.then(onClosed)
|
|
.catchError(onError);
|
|
return await notFound(request.uri);
|
|
}
|
|
}
|
|
|
|
Future handle() {
|
|
response.done
|
|
.then(onClosed)
|
|
.catchError(onError);
|
|
|
|
Uri uri = request.uri;
|
|
if (uri.path.endsWith('/')) {
|
|
uri = uri.resolve('index.html');
|
|
}
|
|
if (uri.path.contains('..') || uri.path.contains('%')) {
|
|
return notFound(uri);
|
|
}
|
|
String path = uri.path;
|
|
Uri root = documentRoot;
|
|
if (path.startsWith('${PACKAGES_PATH}/')) {
|
|
root = packageRoot;
|
|
path = path.substring(PACKAGES_PATH.length);
|
|
}
|
|
|
|
Uri resolvedRequest = root.resolve('.$path');
|
|
switch (request.method) {
|
|
case 'GET':
|
|
return handleGet(resolvedRequest);
|
|
default:
|
|
String method = const HtmlEscape().convert(request.method);
|
|
return badRequest("Unsupported method: '$method'");
|
|
}
|
|
}
|
|
|
|
Future handleGet(Uri uri) async {
|
|
String path = uri.path;
|
|
var f = new File.fromUri(uri);
|
|
if (!await f.exists()) {
|
|
return await handleNonExistingFile(uri);
|
|
} else {
|
|
setContentType(path);
|
|
}
|
|
return await f.openRead().pipe(response);
|
|
}
|
|
|
|
void setContentType(String path) {
|
|
if (path.endsWith('.html')) {
|
|
response.headers.set(CONTENT_TYPE, 'text/html');
|
|
} else if (path.endsWith('.dart')) {
|
|
response.headers.set(CONTENT_TYPE, 'application/dart');
|
|
} else if (path.endsWith('.js')) {
|
|
response.headers.set(CONTENT_TYPE, 'application/javascript');
|
|
} else if (path.endsWith('.ico')) {
|
|
response.headers.set(CONTENT_TYPE, 'image/x-icon');
|
|
} else if (path.endsWith('.appcache')) {
|
|
response.headers.set(CONTENT_TYPE, 'text/cache-manifest');
|
|
} else if (path.endsWith('.css')) {
|
|
response.headers.set(CONTENT_TYPE, 'text/css');
|
|
} else if (path.endsWith('.png')) {
|
|
response.headers.set(CONTENT_TYPE, 'image/png');
|
|
}
|
|
}
|
|
|
|
Future handleNonExistingFile(Uri uri) async {
|
|
String path = uri.path;
|
|
String generated = await generatedFiles[request.uri];
|
|
if (generated != null) {
|
|
print("Serving ${request.uri} from memory.");
|
|
setContentType(path);
|
|
response.write(generated);
|
|
return await response.close();
|
|
}
|
|
if (path.endsWith('.dart.js')) {
|
|
Uri dartScript = uri.resolve(path.substring(0, path.length - 3));
|
|
if (await new File.fromUri(dartScript).exists()) {
|
|
return await compileToJavaScript(dartScript);
|
|
}
|
|
}
|
|
return await notFound(request.uri);
|
|
}
|
|
|
|
compileToJavaScript(Uri dartScript) {
|
|
Uri outputUri = request.uri;
|
|
Completer<String> completer = new Completer<String>();
|
|
generatedFiles[outputUri] = completer.future;
|
|
StreamController controller = updateControllers[outputUri];
|
|
if (controller != null) {
|
|
controller.close();
|
|
}
|
|
updateControllers[outputUri] = new StreamController<String>.broadcast();
|
|
print("Compiling $dartScript to $outputUri.");
|
|
StreamSubscription<CompilerEvent> subscription;
|
|
subscription = compile(dartScript).listen((CompilerEvent event) {
|
|
subscription.onData(
|
|
(CompilerEvent event) => onCompilerEvent(completer, event));
|
|
if (event.kind != IncrementalKind.FULL) {
|
|
notFound(request.uri);
|
|
// TODO(ahe): Do something about this situation.
|
|
} else {
|
|
print("Done compiling $dartScript to $outputUri.");
|
|
completer.complete(event['.js']);
|
|
setContentType(outputUri.path);
|
|
response.write(event['.js']);
|
|
response.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
onCompilerEvent(Completer completer, CompilerEvent event) {
|
|
Uri outputUri = request.uri;
|
|
print("Got ${event.kind} for $outputUri");
|
|
|
|
switch (event.kind) {
|
|
case IncrementalKind.FULL:
|
|
generatedFiles[outputUri] = new Future.value(event['.js']);
|
|
break;
|
|
|
|
case IncrementalKind.INCREMENTAL:
|
|
generatedFiles[outputUri] = completer.future.then(
|
|
(String full) => '$full\n\n${event.compiler.allUpdates()}');
|
|
pushUpdates(event.updates);
|
|
break;
|
|
|
|
case IncrementalKind.ERROR:
|
|
generatedFiles.removeKey(outputUri);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void pushUpdates(String updates) {
|
|
if (updates == null) return;
|
|
StreamController<String> controller = updateControllers[request.uri];
|
|
if (controller == null) return;
|
|
print("Adding updates to controller");
|
|
controller.add(updates);
|
|
}
|
|
|
|
Future dispatch() async {
|
|
try {
|
|
return await WebSocketTransformer.isUpgradeRequest(request)
|
|
? handleSocket()
|
|
: handle();
|
|
} catch (e, s) {
|
|
onError(e, s);
|
|
}
|
|
}
|
|
|
|
static Future onRequest(HttpRequest request) async {
|
|
HttpResponse response = request.response;
|
|
try {
|
|
return await new Conversation(request, response).dispatch();
|
|
} catch (e, s) {
|
|
try {
|
|
onStaticError(e, s);
|
|
return await response.close();
|
|
} catch (e, s) {
|
|
onStaticError(e, s);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future onError(error, [stack]) async {
|
|
try {
|
|
onStaticError(error, stack);
|
|
return await response.close();
|
|
} catch (e, s) {
|
|
onStaticError(e, s);
|
|
}
|
|
}
|
|
|
|
static void onStaticError(error, [stack]) {
|
|
if (error is HttpException) {
|
|
print('Error: ${error.message}');
|
|
} else {
|
|
print('Error: ${error}');
|
|
}
|
|
if (stack != null) {
|
|
print(stack);
|
|
}
|
|
}
|
|
|
|
String htmlInfo(String title, String text) {
|
|
// No script injection, please.
|
|
title = const HtmlEscape().convert(title);
|
|
text = const HtmlEscape().convert(text);
|
|
return """
|
|
<!DOCTYPE html>
|
|
<html lang='en'>
|
|
<head>
|
|
<title>$title</title>
|
|
</head>
|
|
<body>
|
|
<h1>$title</h1>
|
|
<p style='white-space:pre'>$text</p>
|
|
</body>
|
|
</html>
|
|
""";
|
|
}
|
|
}
|
|
|
|
main(List<String> arguments) async {
|
|
Options options = Options.parse(arguments);
|
|
if (options == null) {
|
|
exit(1);
|
|
}
|
|
if (!options.arguments.isEmpty) {
|
|
Conversation.documentRoot = Uri.base.resolve(options.arguments.single);
|
|
}
|
|
Conversation.packageRoot = options.packageRoot;
|
|
String host = options.host;
|
|
int port = options.port;
|
|
try {
|
|
HttpServer server = await HttpServer.bind(host, port);
|
|
print('HTTP server started on http://$host:${server.port}/');
|
|
server.listen(Conversation.onRequest, onError: Conversation.onStaticError);
|
|
} catch (e) {
|
|
print("HttpServer.bind error: $e");
|
|
exit(1);
|
|
};
|
|
}
|