blob: d2dc9a2235657671e643385bd5cf9a66c58c09f9 [file] [log] [blame]
// 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);
};
}