From c9c103d4517095a9cfaff7277477de1b08a06242 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Wed, 6 Feb 2019 16:32:40 -0500 Subject: [PATCH] Begin 3.0.0-alpha --- example/dev.dart | 45 +++++ example/main.dart | 51 ++--- lib/angel_hot.dart | 374 +------------------------------------ lib/src/client.dart | 52 ++++++ lib/src/reloader.dart | 0 lib/src/reloader.dart.old | 378 ++++++++++++++++++++++++++++++++++++++ lib/src/rpc.dart | 0 lib/src/run_hot.dart | 18 ++ lib/src/server.dart | 5 + pubspec.yaml | 8 +- 10 files changed, 523 insertions(+), 408 deletions(-) create mode 100644 example/dev.dart create mode 100644 lib/src/client.dart create mode 100644 lib/src/reloader.dart create mode 100644 lib/src/reloader.dart.old create mode 100644 lib/src/rpc.dart create mode 100644 lib/src/run_hot.dart create mode 100644 lib/src/server.dart diff --git a/example/dev.dart b/example/dev.dart new file mode 100644 index 00000000..f83a863c --- /dev/null +++ b/example/dev.dart @@ -0,0 +1,45 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_hot/angel_hot.dart'; +import 'package:logging/logging.dart'; +import 'src/foo.dart'; + +main() async { + var hot = new HotReloader(createServer, [ + new Directory('src'), + new Directory('src'), + 'main.dart', + Platform.script, + Uri.parse('package:angel_hot/angel_hot.dart') + ]); + await hot.startServer('127.0.0.1', 3000); +} + +Future createServer() async { + var app = new Angel()..serializer = json.encode; + + + // Edit this line, and then refresh the page in your browser! + app.get('/', (req, res) => {'hello': 'hot world!'}); + app.get('/foo', (req, res) => new Foo(bar: 'baz')); + + app.fallback((req, res) => throw new AngelHttpException.notFound()); + + app.encoders.addAll({ + 'gzip': gzip.encoder, + 'deflate': zlib.encoder, + }); + + app.logger = new Logger('angel') + ..onRecord.listen((rec) { + print(rec); + if (rec.error != null) { + print(rec.error); + print(rec.stackTrace); + } + }); + + return app; +} diff --git a/example/main.dart b/example/main.dart index 20cd8d5b..70798da1 100644 --- a/example/main.dart +++ b/example/main.dart @@ -1,44 +1,23 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; +import 'dart:isolate'; import 'package:angel_framework/angel_framework.dart'; import 'package:angel_hot/angel_hot.dart'; -import 'package:logging/logging.dart'; -import 'src/foo.dart'; -main() async { - var hot = new HotReloader(createServer, [ - new Directory('src'), - new Directory('src'), - 'main.dart', - Platform.script, - Uri.parse('package:angel_hot/angel_hot.dart') - ]); - await hot.startServer('127.0.0.1', 3000); -} +main(_, [SendPort sendPort]) { + return runHot(sendPort, (client) { + var app = Angel(); -Future createServer() async { - var app = new Angel()..serializer = json.encode; + if (client != null) { + // Specify which paths to listen to. + client.watchPaths([ + 'src', + 'main.dart', + 'package:angel_hot/angel_hot.dart', + ]); - // Edit this line, and then refresh the page in your browser! - app.get('/', (req, res) => {'hello': 'hot world!'}); - app.get('/foo', (req, res) => new Foo(bar: 'baz')); + // When the top-level triggers a hot reload, shut down the existing server. + client.onReload.listen((reload) { - app.fallback((req, res) => throw new AngelHttpException.notFound()); - - app.encoders.addAll({ - 'gzip': gzip.encoder, - 'deflate': zlib.encoder, + }); + } }); - - app.logger = new Logger('angel') - ..onRecord.listen((rec) { - print(rec); - if (rec.error != null) { - print(rec.error); - print(rec.stackTrace); - } - }); - - return app; } diff --git a/lib/angel_hot.dart b/lib/angel_hot.dart index 4175b4a9..7584e1c9 100644 --- a/lib/angel_hot.dart +++ b/lib/angel_hot.dart @@ -1,369 +1,5 @@ -import 'dart:async'; -import 'dart:collection'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:isolate'; -import 'package:angel_framework/angel_framework.dart'; -import 'package:angel_framework/http.dart'; -import 'package:angel_websocket/server.dart'; -import 'package:charcode/ascii.dart'; -import 'package:glob/glob.dart'; -import 'package:html_builder/elements.dart'; -import 'package:html_builder/html_builder.dart'; -import 'package:io/ansi.dart'; -import 'package:vm_service_lib/vm_service_lib.dart' as vm; -import 'package:vm_service_lib/vm_service_lib_io.dart' as vm; -import 'package:watcher/watcher.dart'; - -/// A utility class that watches the filesystem for changes, and starts new instances of an Angel server. -class HotReloader { - vm.VmService _client; - vm.IsolateRef _mainIsolate; - final StreamController _onChange = - new StreamController.broadcast(); - final List _paths = []; - final StringRenderer _renderer = new StringRenderer(pretty: false); - final Queue _requestQueue = new Queue(); - HttpServer _io; - AngelHttp _server; - Duration _timeout; - vm.VM _vmachine; - - /// If `true` (default), then developers can `press 'r' to reload` the application on-the-fly. - /// - /// This option triggers printing a Flutter-like output to the terminal. - final bool enableHotkeys; - - /// Invoked to load a new instance of [Angel] on file changes. - final FutureOr Function() generator; - - /// Fires whenever a file change. You might consider using this to trigger - /// page reloads in a client. - Stream get onChange => _onChange.stream; - - /// The maximum amount of time to queue incoming requests for if there is no [server] available. - /// - /// If the timeout expires, then the request will be immediately terminated with a `502 Bad Gateway` error. - /// Default: `5s` - Duration get timeout => _timeout; - - /// The Dart VM service host. - /// - /// Default: `localhost`. - final String vmServiceHost; - - /// The port to connect to the Dart VM service. - /// - /// Default: `8181`. - final int vmServicePort; - - /// Initializes a hot reloader that proxies the server created by [generator]. - /// - /// [paths] can contain [FileSystemEntity], [Uri], [String] and [Glob] only. - /// URI's can be `package:` URI's as well. - HotReloader(this.generator, Iterable paths, - {Duration timeout, - this.vmServiceHost: 'localhost', - this.vmServicePort: 8181, - this.enableHotkeys: true}) { - _timeout = timeout ?? new Duration(seconds: 5); - _paths.addAll(paths ?? []); - } - - Future close() async { - _onChange.close(); - await _io?.close(force: true); - } - - void sendError(HttpRequest request, int status, String title_, e) { - var doc = html(lang: 'en', c: [ - head(c: [ - meta(name: 'viewport', content: 'width=device-width, initial-scale=1'), - title(c: [text(title_)]) - ]), - body(c: [ - h1(c: [text(title_)]), - i(c: [text(e.toString())]) - ]) - ]); - - var response = request.response; - response.statusCode = HttpStatus.badGateway; - response.headers - ..contentType = ContentType.html - ..set(HttpHeaders.serverHeader, 'angel_hot'); - - if (request.headers - .value(HttpHeaders.acceptEncodingHeader) - ?.toLowerCase() - ?.contains('gzip') == - true) { - response - ..headers.set(HttpHeaders.contentEncodingHeader, 'gzip') - ..add(gzip.encode(utf8.encode(_renderer.render(doc)))); - } else - response.write(_renderer.render(doc)); - response.close(); - } - - Future _handle(HttpRequest request) { - return _server.handleRequest(request); - } - - Future handleRequest(HttpRequest request) async { - if (_server != null) - return await _handle(request); - else if (timeout == null) - _requestQueue.add(request); - else { - _requestQueue.add(request); - new Timer(timeout, () { - if (_requestQueue.remove(request)) { - // Send 502 response - sendError(request, HttpStatus.badGateway, '502 Bad Gateway', - 'Request timed out after ${timeout.inMilliseconds}ms.'); - } - }); - } - } - - Future _generateServer() async { - var s = await generator(); - await Future.forEach(s.startupHooks, s.configure); - s.optimizeForProduction(); - return new AngelHttp.custom(s, startShared); - } - - /// Starts listening to requests and filesystem events. - Future startServer([address, int port]) async { - var isHot = true; - _server = await _generateServer(); - - if (_paths?.isNotEmpty != true) - print(yellow.wrap( - 'WARNING: You have instantiated a HotReloader without providing any filesystem paths to watch.')); - - if (!Platform.executableArguments.contains('--observe') && - !Platform.executableArguments.contains('--enable-vm-service')) { - stderr.writeln(yellow.wrap( - 'WARNING: You have instantiated a HotReloader without passing `--enable-vm-service` or `--observe` to the Dart VM. Hot reloading will be disabled.')); - isHot = false; - } else { - _client = await vm.vmServiceConnect( - vmServiceHost ?? 'localhost', vmServicePort ?? 8181); - _vmachine ??= await _client.getVM(); - _mainIsolate ??= _vmachine.isolates.first; - await _client.setExceptionPauseMode(_mainIsolate.id, 'None'); - await _listenToFilesystem(); - } - - _onChange.stream - //.transform(new _Debounce(new Duration(seconds: 1))) - .listen(_handleWatchEvent); - - while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst()); - var server = _io = await HttpServer.bind(address ?? '127.0.0.1', port ?? 0); - server.listen(handleRequest); - - // Print a Flutter-like prompt... - if (enableHotkeys) { - var serverUri = new Uri( - scheme: 'http', host: server.address.address, port: server.port); - var host = vmServiceHost == 'localhost' ? '127.0.0.1' : vmServiceHost; - var observatoryUri = - new Uri(scheme: 'http', host: host, port: vmServicePort); - - print(styleBold.wrap( - '\n🔥 To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".')); - stdout.write('Your Angel server is listening at: '); - print(wrapWith('$serverUri', [cyan, styleUnderlined])); - stdout.write( - 'An Observatory debugger and profiler on ${Platform.operatingSystem} is available at: '); - print(wrapWith('$observatoryUri', [cyan, styleUnderlined])); - print( - 'For a more detailed help message, press "h". To quit, press "q".\n'); - - if (_paths.isNotEmpty) { - print(darkGray.wrap( - 'Changes to the following path(s) will also trigger a hot reload:')); - - for (var p in _paths) { - print(darkGray.wrap(' * $p')); - } - - stdout.writeln(); - } - - // Listen for hotkeys - try { - stdin.lineMode = stdin.echoMode = false; - } catch (_) {} - - StreamSubscription sub; - - try { - sub = stdin.expand((l) => l).listen((ch) async { - if (ch == $r) { - _handleWatchEvent( - new WatchEvent(ChangeType.MODIFY, '[manual-reload]'), isHot); - } - if (ch == $R) { - print('Manually restarting server...\n'); - await _killServer(); - await _server.close(); - var addr = _io.address.address; - var port = _io.port; - await _io?.close(force: true); - await startServer(addr, port); - } else if (ch == $q) { - stdin.echoMode = stdin.lineMode = true; - close(); - sub.cancel(); - exit(0); - } else if (ch == $h) { - print( - 'Press "r" to hot reload the Dart VM, and restart the active server.'); - print( - 'Press "R" to restart the server, WITHOUT a hot reload of the VM.'); - print('Press "q" to quit the server.'); - print('Press "h" to display this help information.'); - stdout.writeln(); - } - }); - } catch (_) {} - } - - return server; - } - - _listenToFilesystem() async { - for (var path in _paths) { - if (path is String) { - await _listenToStat(path); - } else if (path is Glob) { - await for (var entity in path.list()) { - await _listenToStat(entity.path); - } - } else if (path is FileSystemEntity) { - await _listenToStat(path.path); - } else if (path is Uri) { - if (path.scheme == 'package') { - var uri = await Isolate.resolvePackageUri(path); - if (uri != null) - await _listenToStat(uri.toFilePath()); - else - await _listenToStat(path.toFilePath()); - } else - await _listenToStat(path.toFilePath()); - } else { - throw new ArgumentError( - 'Hot reload paths must be a FileSystemEntity, a Uri, a String or a Glob. You provided: $path'); - } - } - } - - _listenToStat(String path) async { - _listen() async { - try { - var stat = await FileStat.stat(path); - if (stat.type == FileSystemEntityType.link) { - var lnk = new Link(path); - var p = await lnk.resolveSymbolicLinks(); - return await _listenToStat(p); - } else if (stat.type == FileSystemEntityType.file) { - var file = new File(path); - if (!await file.exists()) return null; - } else if (stat.type == FileSystemEntityType.directory) { - var dir = new Directory(path); - if (!await dir.exists()) return null; - } else - return null; - - var watcher = new Watcher(path); - - watcher.events.listen(_onChange.add, onError: (e) { - stderr.writeln('Could not listen to file changes at ${path}: $e'); - }); - - // print('Listening for file changes at ${path}...'); - return true; - } catch (e) { - if (e is! FileSystemException) rethrow; - } - } - - var r = await _listen(); - - if (r == null) { - print(yellow.wrap( - 'WARNING: Unable to watch path "$path" from working directory "${Directory.current.path}". Please ensure that it exists.')); - } - } - - Future _killServer() async { - if (_server != null) { - // Do this asynchronously, because we really don't care about the old server anymore. - new Future(() async { - // Disconnect active WebSockets - try { - var ws = _server.app.container.make(); - - for (var client in ws.clients) { - try { - await client.close(); - } catch (e) { - stderr.writeln( - 'Couldn\'t close WebSocket from session #${client.request.session.id}: $e'); - } - } - - await Future.forEach( - _server.app.shutdownHooks, _server.app.configure); - } catch (_) { - // Fail silently... - } - }); - } - } - - _handleWatchEvent(WatchEvent e, [bool hot = true]) async { - print('${e.path} changed. Reloading server...\n'); - await _killServer(); - _server = null; - - if (hot) { - var report = await _client.reloadSources(_mainIsolate.id); - - if (!report.success) { - stderr.writeln( - 'Hot reload failed - perhaps some sources have not been generated yet.'); - } - } - - var s = await _generateServer(); - _server = s; - while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst()); - } -} - -/* -class _Debounce extends StreamTransformerBase { - final Duration _delay; - - const _Debounce(this._delay); - - Stream bind(Stream stream) { - var initial = new DateTime.now(); - var next = initial.subtract(this._delay); - return stream.where((S data) { - var now = new DateTime.now(); - if (now.isAfter(next)) { - next = now.add(this._delay); - return true; - } else { - return false; - } - }); - } -} -*/ +export 'src/client.dart'; +export 'src/reloader.dart'; +export 'src/rpc.dart'; +export 'src/run_hot.dart'; +export 'src/server.dart'; \ No newline at end of file diff --git a/lib/src/client.dart b/lib/src/client.dart new file mode 100644 index 00000000..98018d6e --- /dev/null +++ b/lib/src/client.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2; + +/// The client side of the Hot Reload RPC. +class HotReloadClient { + final StreamController _onReload = StreamController(); + + /// The underlying transport. + final json_rpc_2.Peer peer; + + HotReloadClient(this.peer); + + /// Returns whether the underlying [peer] has been closed. + bool get isClosed => peer.isClosed; + + /// Fires whenever the sources within the current Isolate are about to be reloaded. + Stream get onReload => _onReload.stream; + + /// Tell the server to establish file watchers at the given [paths], + /// which may be files, directories, globs, or even `package:` URI's. + void watchPaths(Iterable paths) { + // TODO: Watch paths + } + + /// Notifies the server that an error has been encountered. + void reportError(Object error, [StackTrace stackTrace]) { + // TODO: Error reporting + } + + void close() { + peer.close(); + _onReload.close(); + } +} + +/// A request from the server to reload the current Isolate. +class HotReload { + final HotReloadClient _client; + final List modifiedPaths; + + HotReload._(this._client, this.modifiedPaths); + + /// Successfully terminate this attempt at a hot reload. + void accept() { + // TODO: accept + } + + /// Reject the hot reload attempt + void reject(Object error, [StackTrace stackTrace]) { + // TODO: reject + } +} diff --git a/lib/src/reloader.dart b/lib/src/reloader.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/src/reloader.dart.old b/lib/src/reloader.dart.old new file mode 100644 index 00000000..eee90451 --- /dev/null +++ b/lib/src/reloader.dart.old @@ -0,0 +1,378 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_framework/http.dart'; +import 'package:angel_websocket/server.dart'; +import 'package:charcode/ascii.dart'; +import 'package:glob/glob.dart'; +import 'package:html_builder/elements.dart'; +import 'package:html_builder/html_builder.dart'; +import 'package:io/ansi.dart'; +import 'package:vm_service_lib/vm_service_lib.dart' as vm; +import 'package:vm_service_lib/vm_service_lib_io.dart' as vm; +import 'package:watcher/watcher.dart'; + +/// A utility class that watches the filesystem for changes, and starts new instances of an Angel server. +class HotReloader { + vm.VmService _client; + vm.IsolateRef _mainIsolate; + final StreamController _onChange = + new StreamController.broadcast(); + final List _paths = []; + final StringRenderer _renderer = new StringRenderer(pretty: false); + final Queue _requestQueue = new Queue(); + HttpServer _io; + AngelHttp _server; + Duration _timeout; + vm.VM _vmachine; + + /// If `true` (default), then developers can `press 'r' to reload` the application on-the-fly. + /// + /// This option triggers printing a Flutter-like output to the terminal. + final bool enableHotkeys; + + /// Invoked to load a new instance of [Angel] on file changes. + final FutureOr Function() generator; + + /// Fires whenever a file change. You might consider using this to trigger + /// page reloads in a client. + Stream get onChange => _onChange.stream; + + /// The maximum amount of time to queue incoming requests for if there is no [server] available. + /// + /// If the timeout expires, then the request will be immediately terminated with a `502 Bad Gateway` error. + /// Default: `5s` + Duration get timeout => _timeout; + + /// The Dart VM service host. + /// + /// Default: `localhost`. + final String vmServiceHost; + + /// The port to connect to the Dart VM service. + /// + /// Default: `8181`. + final int vmServicePort; + + /// Initializes a hot reloader that proxies the server created by [generator]. + /// + /// [paths] can contain [FileSystemEntity], [Uri], [String] and [Glob] only. + /// URI's can be `package:` URI's as well. + HotReloader(this.generator, Iterable paths, + {Duration timeout, + this.vmServiceHost: 'localhost', + this.vmServicePort: 8181, + this.enableHotkeys: true}) { + _timeout = timeout ?? new Duration(seconds: 5); + _paths.addAll(paths ?? []); + } + + Future close() async { + _onChange.close(); + await _io?.close(force: true); + } + + void sendError(HttpRequest request, int status, String title_, e) { + var doc = html(lang: 'en', c: [ + head(c: [ + meta(name: 'viewport', content: 'width=device-width, initial-scale=1'), + title(c: [text(title_)]) + ]), + body(c: [ + h1(c: [text(title_)]), + i(c: [text(e.toString())]) + ]) + ]); + + var response = request.response; + response.statusCode = HttpStatus.badGateway; + response.headers + ..contentType = ContentType.html + ..set(HttpHeaders.serverHeader, 'angel_hot'); + + if (request.headers + .value(HttpHeaders.acceptEncodingHeader) + ?.toLowerCase() + ?.contains('gzip') == + true) { + response + ..headers.set(HttpHeaders.contentEncodingHeader, 'gzip') + ..add(gzip.encode(utf8.encode(_renderer.render(doc)))); + } else + response.write(_renderer.render(doc)); + response.close(); + } + + Future _handle(HttpRequest request) { + return _server.handleRequest(request); + } + + Future handleRequest(HttpRequest request) async { + if (_server != null) + return await _handle(request); + else if (timeout == null) + _requestQueue.add(request); + else { + _requestQueue.add(request); + new Timer(timeout, () { + if (_requestQueue.remove(request)) { + // Send 502 response + sendError(request, HttpStatus.badGateway, '502 Bad Gateway', + 'Request timed out after ${timeout.inMilliseconds}ms.'); + } + }); + } + } + + Future _generateServer() async { + var s = await generator(); + await Future.forEach(s.startupHooks, s.configure); + s.optimizeForProduction(); + return new AngelHttp.custom(s, startShared); + } + + void handleErrorEvent(vm.Event e) { + if (e.exception != null) { + print('Uncaught error in application:'); + print(e.exception.valueAsString); + } + } + + /// Starts listening to requests and filesystem events. + Future startServer([address, int port]) async { + var isHot = true; + _server = await _generateServer(); + + if (_paths?.isNotEmpty != true) + print(yellow.wrap( + 'WARNING: You have instantiated a HotReloader without providing any filesystem paths to watch.')); + + if (!Platform.executableArguments.contains('--observe') && + !Platform.executableArguments.contains('--enable-vm-service')) { + stderr.writeln(yellow.wrap( + 'WARNING: You have instantiated a HotReloader without passing `--enable-vm-service` or `--observe` to the Dart VM. Hot reloading will be disabled.')); + isHot = false; + } else { + _client = await vm.vmServiceConnect( + vmServiceHost ?? 'localhost', vmServicePort ?? 8181); + _vmachine ??= await _client.getVM(); + _mainIsolate ??= _vmachine.isolates.first; + await _client.setExceptionPauseMode(_mainIsolate.id, 'None'); + _client.onDebugEvent.listen(handleErrorEvent); + _client.onIsolateEvent.listen(handleErrorEvent); + await _listenToFilesystem(); + } + + _onChange.stream + //.transform(new _Debounce(new Duration(seconds: 1))) + .listen(_handleWatchEvent); + + while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst()); + var server = _io = await HttpServer.bind(address ?? '127.0.0.1', port ?? 0); + server.listen(handleRequest); + + // Print a Flutter-like prompt... + if (enableHotkeys) { + var serverUri = new Uri( + scheme: 'http', host: server.address.address, port: server.port); + var host = vmServiceHost == 'localhost' ? '127.0.0.1' : vmServiceHost; + var observatoryUri = + new Uri(scheme: 'http', host: host, port: vmServicePort); + + print(styleBold.wrap( + '\n🔥 To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".')); + stdout.write('Your Angel server is listening at: '); + print(wrapWith('$serverUri', [cyan, styleUnderlined])); + stdout.write( + 'An Observatory debugger and profiler on ${Platform.operatingSystem} is available at: '); + print(wrapWith('$observatoryUri', [cyan, styleUnderlined])); + print( + 'For a more detailed help message, press "h". To quit, press "q".\n'); + + if (_paths.isNotEmpty) { + print(darkGray.wrap( + 'Changes to the following path(s) will also trigger a hot reload:')); + + for (var p in _paths) { + print(darkGray.wrap(' * $p')); + } + + stdout.writeln(); + } + + // Listen for hotkeys + try { + stdin.lineMode = stdin.echoMode = false; + } catch (_) {} + + StreamSubscription sub; + + try { + sub = stdin.expand((l) => l).listen((ch) async { + if (ch == $r) { + _handleWatchEvent( + new WatchEvent(ChangeType.MODIFY, '[manual-reload]'), isHot); + } + if (ch == $R) { + print('Manually restarting server...\n'); + await _killServer(); + await _server.close(); + var addr = _io.address.address; + var port = _io.port; + await _io?.close(force: true); + await startServer(addr, port); + } else if (ch == $q) { + stdin.echoMode = stdin.lineMode = true; + close(); + sub.cancel(); + exit(0); + } else if (ch == $h) { + print( + 'Press "r" to hot reload the Dart VM, and restart the active server.'); + print( + 'Press "R" to restart the server, WITHOUT a hot reload of the VM.'); + print('Press "q" to quit the server.'); + print('Press "h" to display this help information.'); + stdout.writeln(); + } + }); + } catch (_) {} + } + + return server; + } + + _listenToFilesystem() async { + for (var path in _paths) { + if (path is String) { + await _listenToStat(path); + } else if (path is Glob) { + await for (var entity in path.list()) { + await _listenToStat(entity.path); + } + } else if (path is FileSystemEntity) { + await _listenToStat(path.path); + } else if (path is Uri) { + if (path.scheme == 'package') { + var uri = await Isolate.resolvePackageUri(path); + if (uri != null) + await _listenToStat(uri.toFilePath()); + else + await _listenToStat(path.toFilePath()); + } else + await _listenToStat(path.toFilePath()); + } else { + throw new ArgumentError( + 'Hot reload paths must be a FileSystemEntity, a Uri, a String or a Glob. You provided: $path'); + } + } + } + + _listenToStat(String path) async { + _listen() async { + try { + var stat = await FileStat.stat(path); + if (stat.type == FileSystemEntityType.link) { + var lnk = new Link(path); + var p = await lnk.resolveSymbolicLinks(); + return await _listenToStat(p); + } else if (stat.type == FileSystemEntityType.file) { + var file = new File(path); + if (!await file.exists()) return null; + } else if (stat.type == FileSystemEntityType.directory) { + var dir = new Directory(path); + if (!await dir.exists()) return null; + } else + return null; + + var watcher = new Watcher(path); + + watcher.events.listen(_onChange.add, onError: (e) { + stderr.writeln('Could not listen to file changes at ${path}: $e'); + }); + + // print('Listening for file changes at ${path}...'); + return true; + } catch (e) { + if (e is! FileSystemException) rethrow; + } + } + + var r = await _listen(); + + if (r == null) { + print(yellow.wrap( + 'WARNING: Unable to watch path "$path" from working directory "${Directory.current.path}". Please ensure that it exists.')); + } + } + + Future _killServer() async { + if (_server != null) { + // Do this asynchronously, because we really don't care about the old server anymore. + new Future(() async { + // Disconnect active WebSockets + try { + var ws = _server.app.container.make(); + + for (var client in ws.clients) { + try { + await client.close(); + } catch (e) { + stderr.writeln( + 'Couldn\'t close WebSocket from session #${client.request.session.id}: $e'); + } + } + + await Future.forEach( + _server.app.shutdownHooks, _server.app.configure); + } catch (_) { + // Fail silently... + } + }); + } + } + + _handleWatchEvent(WatchEvent e, [bool hot = true]) async { + print('${e.path} changed. Reloading server...\n'); + await _killServer(); + _server = null; + + if (hot) { + var report = await _client.reloadSources(_mainIsolate.id); + + if (!report.success) { + stderr.writeln( + 'Hot reload failed - perhaps some sources have not been generated yet.'); + } + } + + var s = await _generateServer(); + _server = s; + while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst()); + } +} + +/* +class _Debounce extends StreamTransformerBase { + final Duration _delay; + + const _Debounce(this._delay); + + Stream bind(Stream stream) { + var initial = new DateTime.now(); + var next = initial.subtract(this._delay); + return stream.where((S data) { + var now = new DateTime.now(); + if (now.isAfter(next)) { + next = now.add(this._delay); + return true; + } else { + return false; + } + }); + } +} +*/ diff --git a/lib/src/rpc.dart b/lib/src/rpc.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/src/run_hot.dart b/lib/src/run_hot.dart new file mode 100644 index 00000000..32484dc7 --- /dev/null +++ b/lib/src/run_hot.dart @@ -0,0 +1,18 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2; +import 'package:stream_channel/src/isolate_channel.dart'; +import 'client.dart'; + +/// Runs the [f] after connecting to a hot reload server. Runs the [f] as-is if no [sendPort] is given. +Future runHot( + SendPort sendPort, FutureOr Function(HotReloadClient) f) { + if (sendPort == null) { + return Future.sync(() => f(null)); + } else { + var channel = IsolateChannel.connectSend(sendPort); + var peer = json_rpc_2.Peer.withoutJson(channel); + var client = HotReloadClient(peer); + return Future.sync(() => f(client)).whenComplete(client.close); + } +} diff --git a/lib/src/server.dart b/lib/src/server.dart new file mode 100644 index 00000000..de240233 --- /dev/null +++ b/lib/src/server.dart @@ -0,0 +1,5 @@ +import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc_2; + +class HotReloadServer { + +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 5abbbbe6..29bb449f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: angel_hot description: Supports hot reloading/hot code push of Angel servers on file changes. -version: 2.0.3 +version: 3.0.0-alpha author: Tobe O homepage: https://github.com/angel-dart/hot environment: @@ -12,8 +12,10 @@ dependencies: glob: ^1.0.0 html_builder: ^1.0.0 io: ^0.3.2 + json_rpc_2: ^2.0.0 + stream_channel: ^1.0.0 vm_service_lib: ^0.3.5 watcher: ^0.9.0 dev_dependencies: - http: ^0.11.3 - logging: ^0.11.0 + http: ^0.11.3 + logging: ^0.11.0