| Index: site/try/project_server.dart
|
| diff --git a/site/try/project_server.dart b/site/try/project_server.dart
|
| deleted file mode 100644
|
| index 08c5d565425f206d777c404842ecc524397d19d4..0000000000000000000000000000000000000000
|
| --- a/site/try/project_server.dart
|
| +++ /dev/null
|
| @@ -1,455 +0,0 @@
|
| -// 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 trydart.projectServer;
|
| -
|
| -import 'dart:io';
|
| -
|
| -import 'dart:async' show
|
| - Future,
|
| - Stream;
|
| -
|
| -import 'dart:convert' show
|
| - HtmlEscape,
|
| - JSON,
|
| - UTF8;
|
| -
|
| -class WatchHandler {
|
| - final WebSocket socket;
|
| -
|
| - final Set<String> watchedFiles;
|
| -
|
| - static final Set<WatchHandler> handlers = new Set<WatchHandler>();
|
| -
|
| - static const Map<int, String> fsEventNames = const <int, String>{
|
| - FileSystemEvent.CREATE: 'create',
|
| - FileSystemEvent.DELETE: 'delete',
|
| - FileSystemEvent.MODIFY: 'modify',
|
| - FileSystemEvent.MOVE: 'move',
|
| - };
|
| -
|
| - WatchHandler(this.socket, Iterable<String> watchedFiles)
|
| - : this.watchedFiles = watchedFiles.toSet();
|
| -
|
| - handleFileSystemEvent(FileSystemEvent event) {
|
| - if (event.isDirectory) return;
|
| - String type = fsEventNames[event.type];
|
| - if (type == null) type = 'unknown';
|
| - String path = new Uri.file(event.path).pathSegments.last;
|
| - shouldIgnore(type, path).then((bool ignored) {
|
| - if (ignored) return;
|
| - socket.add(JSON.encode({type: [path]}));
|
| - });
|
| - }
|
| -
|
| - Future<bool> shouldIgnore(String type, String path) {
|
| - switch (type) {
|
| - case 'create':
|
| - return new Future<bool>.value(!watchedFiles.contains(path));
|
| - case 'delete':
|
| - return Conversation.listProjectFiles().then((List<String> files) {
|
| - watchedFiles
|
| - ..retainAll(files)
|
| - ..addAll(files);
|
| - return watchedFiles.contains(path);
|
| - });
|
| - case 'modify':
|
| - return new Future<bool>.value(false);
|
| - default:
|
| - print('Unhandled fs-event for $path ($type).');
|
| - return new Future<bool>.value(true);
|
| - }
|
| - }
|
| -
|
| - onData(_) {
|
| - // TODO(ahe): Move POST code here?
|
| - }
|
| -
|
| - onDone() {
|
| - handlers.remove(this);
|
| - }
|
| -
|
| - static handleWebSocket(WebSocket socket) {
|
| - Conversation.ensureProjectWatcher();
|
| - Conversation.listProjectFiles().then((List<String> files) {
|
| - socket.add(JSON.encode({'create': files}));
|
| - WatchHandler handler = new WatchHandler(socket, files);
|
| - handlers.add(handler);
|
| - socket.listen(
|
| - handler.onData, cancelOnError: true, onDone: handler.onDone);
|
| - });
|
| - }
|
| -
|
| - static onFileSystemEvent(FileSystemEvent event) {
|
| - for (WatchHandler handler in handlers) {
|
| - handler.handleFileSystemEvent(event);
|
| - }
|
| - }
|
| -}
|
| -
|
| -/// Represents a "project" command. These commands are accessed from the URL
|
| -/// "/project?name".
|
| -class ProjectCommand {
|
| - final String name;
|
| -
|
| - /// For each query parameter, this map describes rules for validating them.
|
| - final Map<String, String> rules;
|
| -
|
| - final Function handle;
|
| -
|
| - const ProjectCommand(this.name, this.rules, this.handle);
|
| -}
|
| -
|
| -class Conversation {
|
| - HttpRequest request;
|
| - HttpResponse response;
|
| -
|
| - static const String PROJECT_PATH = '/project';
|
| -
|
| - static const String PACKAGES_PATH = '/packages';
|
| -
|
| - static const String CONTENT_TYPE = HttpHeaders.CONTENT_TYPE;
|
| -
|
| - static const String GIT_TAG = 'try_dart_backup';
|
| -
|
| - static const String COMMIT_MESSAGE = """
|
| -Automated backup.
|
| -
|
| -It is safe to delete tag '$GIT_TAG' if you don't need the backup.""";
|
| -
|
| - static Uri documentRoot = Uri.base;
|
| -
|
| - static Uri projectRoot = Uri.base.resolve('site/try/src/');
|
| -
|
| - static Uri packageRoot = Uri.base.resolve('sdk/lib/_internal/');
|
| -
|
| - static const List<ProjectCommand> COMMANDS = const <ProjectCommand>[
|
| - const ProjectCommand('list', const {'list': null}, handleProjectList),
|
| - ];
|
| -
|
| - static Stream<FileSystemEvent> projectChanges;
|
| -
|
| - static final Map<String, String> gitEnv = computeGitEnv();
|
| -
|
| - Conversation(this.request, this.response);
|
| -
|
| - onClosed(_) {
|
| - if (response.statusCode == HttpStatus.OK) return;
|
| - print('Request for ${request.uri} ${response.statusCode}');
|
| - }
|
| -
|
| - notFound(path) {
|
| - response.statusCode = HttpStatus.NOT_FOUND;
|
| - response.write(htmlInfo('Not Found',
|
| - 'The file "$path" could not be found.'));
|
| - response.close();
|
| - }
|
| -
|
| - badRequest(String problem) {
|
| - response.statusCode = HttpStatus.BAD_REQUEST;
|
| - response.write(htmlInfo("Bad request",
|
| - "Bad request '${request.uri}': $problem"));
|
| - response.close();
|
| - }
|
| -
|
| - internalError(error, stack) {
|
| - print(error);
|
| - if (stack != null) print(stack);
|
| - response.statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
| - response.write(htmlInfo("Internal Server Error",
|
| - "Internal Server Error: $error\n$stack"));
|
| - response.close();
|
| - }
|
| -
|
| - bool validate(Map<String, String> parameters, Map<String, String> rules) {
|
| - Iterable<String> problems = rules.keys
|
| - .where((name) => !parameters.containsKey(name))
|
| - .map((name) => "Missing parameter: '$name'.");
|
| - if (!problems.isEmpty) {
|
| - badRequest(problems.first);
|
| - return false;
|
| - }
|
| - Set extra = new Set.from(parameters.keys)..removeAll(rules.keys);
|
| - if (extra.isEmpty) return true;
|
| - String extraString = (extra.toList()..sort()).join("', '");
|
| - badRequest("Extra parameters: '$extraString'.");
|
| - return false;
|
| - }
|
| -
|
| - static Future<List<String>> listProjectFiles() {
|
| - String nativeDir = projectRoot.toFilePath();
|
| - Directory dir = new Directory(nativeDir);
|
| - var future = dir.list(recursive: true, followLinks: false).toList();
|
| - return future.then((List<FileSystemEntity> entries) {
|
| - return entries
|
| - .map((e) => e.path)
|
| - .where((p) => p.endsWith('.dart') && p.startsWith(nativeDir))
|
| - .map((p) => p.substring(nativeDir.length))
|
| - .map((p) => new Uri.file(p).path).toList();
|
| - });
|
| - }
|
| -
|
| - static handleProjectList(Conversation self) {
|
| - listProjectFiles().then((List<String> files) {
|
| - self.response
|
| - ..write(JSON.encode(files))
|
| - ..close();
|
| - });
|
| - }
|
| -
|
| - handleProjectRequest() {
|
| - Map<String, String> parameters = request.uri.queryParameters;
|
| - for (ProjectCommand command in COMMANDS) {
|
| - if (parameters.containsKey(command.name)) {
|
| - if (validate(parameters, command.rules)) {
|
| - (command.handle)(this);
|
| - }
|
| - return;
|
| - }
|
| - }
|
| - String commands = COMMANDS.map((c) => c.name).join("', '");
|
| - badRequest("Valid commands are: '$commands'");
|
| - }
|
| -
|
| - handleSocket() {
|
| - if (request.uri.path == '/ws/watch') {
|
| - WebSocketTransformer.upgrade(request).then(WatchHandler.handleWebSocket);
|
| - } else {
|
| - response.done
|
| - .then(onClosed)
|
| - .catchError(onError);
|
| - notFound(request.uri.path);
|
| - }
|
| - }
|
| -
|
| - handle() {
|
| - response.done
|
| - .then(onClosed)
|
| - .catchError(onError);
|
| -
|
| - Uri uri = request.uri;
|
| - if (uri.path == PROJECT_PATH) {
|
| - return handleProjectRequest();
|
| - }
|
| - if (uri.path.endsWith('/')) {
|
| - uri = uri.resolve('index.html');
|
| - }
|
| - if (uri.path == '/css/fonts/fontawesome-webfont.woff') {
|
| - uri = uri.resolve('/fontawesome-webfont.woff');
|
| - }
|
| - if (uri.path.contains('..') || uri.path.contains('%')) {
|
| - return notFound(uri.path);
|
| - }
|
| - String path = uri.path;
|
| - Uri root = documentRoot;
|
| - String dartType = 'application/dart';
|
| - if (path.startsWith('/project/packages/')) {
|
| - root = packageRoot;
|
| - path = path.substring('/project/packages'.length);
|
| - } else if (path.startsWith('${PROJECT_PATH}/')) {
|
| - root = projectRoot;
|
| - path = path.substring(PROJECT_PATH.length);
|
| - dartType = 'text/plain';
|
| - } else if (path.startsWith('${PACKAGES_PATH}/')) {
|
| - root = packageRoot;
|
| - path = path.substring(PACKAGES_PATH.length);
|
| - }
|
| -
|
| - String filePath = root.resolve('.$path').toFilePath();
|
| - switch (request.method) {
|
| - case 'GET':
|
| - return handleGet(filePath, dartType);
|
| - case 'POST':
|
| - return handlePost(filePath);
|
| - default:
|
| - String method = const HtmlEscape().convert(request.method);
|
| - return badRequest("Unsupported method: '$method'");
|
| - }
|
| - }
|
| -
|
| - void handleGet(String path, String dartType) {
|
| - var f = new File(path);
|
| - f.exists().then((bool exists) {
|
| - if (!exists) return notFound(request.uri);
|
| - if (path.endsWith('.html')) {
|
| - response.headers.set(CONTENT_TYPE, 'text/html');
|
| - } else if (path.endsWith('.dart')) {
|
| - response.headers.set(CONTENT_TYPE, dartType);
|
| - } 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');
|
| - }
|
| - f.openRead().pipe(response).catchError(onError);
|
| - });
|
| - }
|
| -
|
| - handlePost(String path) {
|
| - // The data is sent using a dart:html HttpRequest (aka XMLHttpRequest).
|
| - // According to http://xhr.spec.whatwg.org/, strings are always encoded as
|
| - // UTF-8.
|
| - request.transform(UTF8.decoder).join().then((String data) {
|
| - // The rest of this method is synchronous. This guarantees that we don't
|
| - // make conflicting git changes in response to multiple POST requests.
|
| - try {
|
| - backup(path);
|
| - } catch (e, stack) {
|
| - return internalError(e, stack);
|
| - }
|
| -
|
| - new File(path).writeAsStringSync(data);
|
| -
|
| - response
|
| - ..statusCode = HttpStatus.OK
|
| - ..close();
|
| - });
|
| - }
|
| -
|
| - // Back up the file [path] using git.
|
| - static void backup(String path) {
|
| - // Reset the index.
|
| - git('read-tree', ['HEAD']);
|
| -
|
| - // Save modifications in index.
|
| - git('update-index', ['--add', path]);
|
| -
|
| - // If the file isn't modified, don't back it up.
|
| - if (checkGit('diff', ['--cached', '--quiet'])) return;
|
| -
|
| - String localModifications = git('write-tree');
|
| -
|
| - String tag = 'refs/tags/$GIT_TAG';
|
| - var arguments = ['-m', COMMIT_MESSAGE, localModifications];
|
| -
|
| - if (checkGit('rev-parse', ['-q', '--verify', tag])) {
|
| - // The tag already exists.
|
| -
|
| - if (checkGit('diff-tree', ['--quiet', localModifications, tag])) {
|
| - // localModifications are identical to the last backup.
|
| - return;
|
| - }
|
| -
|
| - // Use the tag as a parent.
|
| - arguments = ['-p', tag]..addAll(arguments);
|
| -
|
| - String headCommit = git('rev-parse', ['HEAD']);
|
| - String mergeBase = git('merge-base', [tag, 'HEAD']);
|
| - if (headCommit != mergeBase) {
|
| - arguments = ['-p', 'HEAD']..addAll(arguments);
|
| - }
|
| - } else {
|
| - arguments = ['-p', 'HEAD']..addAll(arguments);
|
| - }
|
| -
|
| - // Commit the local modifcations.
|
| - String commit = git('commit-tree', arguments);
|
| -
|
| - // Create or update the tag.
|
| - git('tag', ['-f', GIT_TAG, commit]);
|
| - }
|
| -
|
| - static String git(String command,
|
| - [List<String> arguments = const <String> []]) {
|
| - ProcessResult result =
|
| - run('git', <String>[command]..addAll(arguments), gitEnv);
|
| - if (result.exitCode != 0) {
|
| - throw 'git error: ${result.stdout}\n${result.stderr}';
|
| - }
|
| - return result.stdout.trim();
|
| - }
|
| -
|
| - static bool checkGit(String command,
|
| - [List<String> arguments = const <String> []]) {
|
| - return
|
| - run('git', <String>[command]..addAll(arguments), gitEnv).exitCode == 0;
|
| - }
|
| -
|
| - static Map<String, String> computeGitEnv() {
|
| - ProcessResult result = run('git', ['rev-parse', '--git-dir'], null);
|
| - if (result.exitCode != 0) {
|
| - throw 'git error: ${result.stdout}\n${result.stderr}';
|
| - }
|
| - String gitDir = result.stdout.trim();
|
| - return <String, String>{ 'GIT_INDEX_FILE': '$gitDir/try_dart_backup' };
|
| - }
|
| -
|
| - static ProcessResult run(String executable,
|
| - List<String> arguments,
|
| - Map<String, String> environment) {
|
| - // print('Running $executable ${arguments.join(" ")}');
|
| - return Process.runSync(executable, arguments, environment: environment);
|
| - }
|
| -
|
| - static onRequest(HttpRequest request) {
|
| - Conversation conversation = new Conversation(request, request.response);
|
| - if (WebSocketTransformer.isUpgradeRequest(request)) {
|
| - conversation.handleSocket();
|
| - } else {
|
| - conversation.handle();
|
| - }
|
| - }
|
| -
|
| - static ensureProjectWatcher() {
|
| - if (projectChanges != null) return;
|
| - String nativeDir = projectRoot.toFilePath();
|
| - Directory dir = new Directory(nativeDir);
|
| - projectChanges = dir.watch();
|
| - projectChanges.listen(WatchHandler.onFileSystemEvent);
|
| - }
|
| -
|
| - static onError(error) {
|
| - if (error is HttpException) {
|
| - print('Error: ${error.message}');
|
| - } else {
|
| - print('Error: ${error}');
|
| - }
|
| - }
|
| -
|
| - 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) {
|
| - if (arguments.length > 0) {
|
| - Conversation.documentRoot = Uri.base.resolve(arguments[0]);
|
| - }
|
| - var host = '127.0.0.1';
|
| - if (arguments.length > 1) {
|
| - host = arguments[1];
|
| - }
|
| - int port = 0;
|
| - if (arguments.length > 2) {
|
| - port = int.parse(arguments[2]);
|
| - }
|
| - if (arguments.length > 3) {
|
| - Conversation.projectRoot = Uri.base.resolve(arguments[3]);
|
| - }
|
| - if (arguments.length > 4) {
|
| - Conversation.packageRoot = Uri.base.resolve(arguments[4]);
|
| - }
|
| - HttpServer.bind(host, port).then((HttpServer server) {
|
| - print('HTTP server started on http://$host:${server.port}/');
|
| - server.listen(Conversation.onRequest, onError: Conversation.onError);
|
| - }).catchError((e) {
|
| - print("HttpServer.bind error: $e");
|
| - exit(1);
|
| - });
|
| -}
|
|
|