This commit is contained in:
Tobe O 2018-10-02 12:13:47 -04:00
parent 8cbfb713bd
commit eaf78ed1a5
5 changed files with 101 additions and 38 deletions

View file

@ -1,6 +1,8 @@
# hot # hot
[![Pub](https://img.shields.io/pub/v/angel_hot.svg)](https://pub.dartlang.org/packages/angel_hot) [![Pub](https://img.shields.io/pub/v/angel_hot.svg)](https://pub.dartlang.org/packages/angel_hot)
![Screenshot of terminal](screenshots/screenshot.png)
Supports *hot reloading* of Angel servers on file changes. This is faster and Supports *hot reloading* of Angel servers on file changes. This is faster and
more reliable than merely reactively restarting a `Process`. more reliable than merely reactively restarting a `Process`.
@ -22,7 +24,9 @@ Usage is fairly simple. Pass a function that creates an `Angel` server, along wi
to watch, to the `HotReloader` constructor. The rest is history!!! to watch, to the `HotReloader` constructor. The rest is history!!!
The recommended pattern is to only use hot-reloading in your application entry point. Create your `Angel` instance The recommended pattern is to only use hot-reloading in your application entry point. Create your `Angel` instance
within a separate function, conventionally named `createServer`. Using this in production mode is pointless. within a separate function, conventionally named `createServer`.
**Using this in production mode is pointless.**
You can watch: You can watch:
* Files * Files

View file

@ -1,2 +1,3 @@
analyzer: analyzer:
strong-mode: true strong-mode:
implicit-casts: false

View file

@ -14,9 +14,7 @@ main() async {
'main.dart', 'main.dart',
Uri.parse('package:angel_hot/angel_hot.dart') Uri.parse('package:angel_hot/angel_hot.dart')
]); ]);
var server = await hot.startServer('127.0.0.1', 3000); await hot.startServer('127.0.0.1', 3000);
print(
'Hot server listening at http://${server.address.address}:${server.port}');
} }
Future<Angel> createServer() async { Future<Angel> createServer() async {

View file

@ -15,10 +15,12 @@ import 'dart:io'
Platform, Platform,
exit, exit,
stderr, stderr,
stdin,
stdout; stdout;
import 'dart:isolate'; import 'dart:isolate';
import 'package:angel_framework/angel_framework.dart'; import 'package:angel_framework/angel_framework.dart';
import 'package:angel_websocket/server.dart'; import 'package:angel_websocket/server.dart';
import 'package:charcode/ascii.dart';
import 'package:dart2_constant/convert.dart'; import 'package:dart2_constant/convert.dart';
import 'package:dart2_constant/io.dart'; import 'package:dart2_constant/io.dart';
import 'package:glob/glob.dart'; import 'package:glob/glob.dart';
@ -102,16 +104,16 @@ class HotReloader {
var response = request.response; var response = request.response;
response.statusCode = HttpStatus.badGateway; response.statusCode = HttpStatus.badGateway;
response.headers response.headers
..contentType = ContentType.HTML ..contentType = ContentType.html
..set(HttpHeaders.SERVER, 'angel_hot'); ..set(HttpHeaders.serverHeader, 'angel_hot');
if (request.headers if (request.headers
.value(HttpHeaders.ACCEPT_ENCODING) .value(HttpHeaders.acceptEncodingHeader)
?.toLowerCase() ?.toLowerCase()
?.contains('gzip') == ?.contains('gzip') ==
true) { true) {
response response
..headers.set(HttpHeaders.CONTENT_ENCODING, 'gzip') ..headers.set(HttpHeaders.contentEncodingHeader, 'gzip')
..add(gzip.encode(utf8.encode(_renderer.render(doc)))); ..add(gzip.encode(utf8.encode(_renderer.render(doc))));
} else } else
response.write(_renderer.render(doc)); response.write(_renderer.render(doc));
@ -148,44 +150,94 @@ class HotReloader {
/// Starts listening to requests and filesystem events. /// Starts listening to requests and filesystem events.
Future<HttpServer> startServer([address, int port]) async { Future<HttpServer> startServer([address, int port]) async {
var isHot = true;
_server = await _generateServer(); _server = await _generateServer();
if (_paths?.isNotEmpty != true) if (_paths?.isNotEmpty != true)
print( print(yellow.wrap(
'WARNING: You have instantiated a HotReloader without providing any filesystem paths to watch.'); 'WARNING: You have instantiated a HotReloader without providing any filesystem paths to watch.'));
if (!Platform.executableArguments.contains('--observe') && if (!Platform.executableArguments.contains('--observe') &&
!Platform.executableArguments.contains('--enable-vm-service')) { !Platform.executableArguments.contains('--enable-vm-service')) {
stderr.writeln( 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.'); '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 { } else {
_client = await vm.vmServiceConnect( _client = await vm.vmServiceConnect(
vmServiceHost ?? 'localhost', vmServicePort ?? 8181); vmServiceHost ?? 'localhost', vmServicePort ?? 8181);
_vmachine ??= await _client.getVM(); _vmachine ??= await _client.getVM();
_mainIsolate ??= _vmachine.isolates.first; _mainIsolate ??= _vmachine.isolates.first;
await _client.setExceptionPauseMode(_mainIsolate.id, 'None'); await _client.setExceptionPauseMode(_mainIsolate.id, 'None');
_onChange.stream
.transform(new _Debounce(new Duration(seconds: 1)))
.listen(_handleWatchEvent);
await _listenToFilesystem(); await _listenToFilesystem();
} }
_onChange.stream
//.transform(new _Debounce(new Duration(seconds: 1)))
.listen(_handleWatchEvent);
while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst()); while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst());
var server = await HttpServer.bind(address ?? '127.0.0.1', port ?? 0); var server = await HttpServer.bind(address ?? '127.0.0.1', port ?? 0);
server.listen(handleRequest); server.listen(handleRequest);
// Print a Flutter-like prompt... // Print a Flutter-like prompt...
if (enableHotkeys) { 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 host = vmServiceHost == 'localhost' ? '127.0.0.1' : vmServiceHost;
var observatoryUri = var observatoryUri =
new Uri(scheme: 'http', host: host, port: vmServicePort); new Uri(scheme: 'http', host: host, port: vmServicePort);
print(styleBold.wrap( print(styleBold.wrap(
'🔥 To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".')); '\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( stdout.write(
'An Observatory debugger and profiler on iPhone XS Max is available at: '); 'An Observatory debugger and profiler on ${Platform.operatingSystem} is available at: ');
print(wrapWith('$observatoryUri', [cyan, styleUnderlined])); print(wrapWith('$observatoryUri', [cyan, styleUnderlined]));
print('To quit, press "q".'); 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
stdin.lineMode = stdin.echoMode = false;
StreamSubscription<int> sub;
sub = stdin.expand((l) => l).listen((ch) async {
var ch = stdin.readByteSync();
if (ch == $r) {
_handleWatchEvent(
new WatchEvent(ChangeType.MODIFY, '[manual-reload]'), isHot);
}
if (ch == $R) {
//print('Manually restarting server...\n');
_handleWatchEvent(
new WatchEvent(ChangeType.MODIFY, '[manual-restart]'), false);
} 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();
}
});
} }
return server; return server;
@ -240,7 +292,7 @@ class HotReloader {
stderr.writeln('Could not listen to file changes at ${path}: $e'); stderr.writeln('Could not listen to file changes at ${path}: $e');
}); });
print('Listening for file changes at ${path}...'); // print('Listening for file changes at ${path}...');
return true; return true;
} catch (e) { } catch (e) {
if (e is! FileSystemException) rethrow; if (e is! FileSystemException) rethrow;
@ -255,37 +307,43 @@ class HotReloader {
} }
} }
_handleWatchEvent(WatchEvent e) async { _handleWatchEvent(WatchEvent e, [bool hot = true]) async {
print('${e.path} changed. Reloading server...'); print('${e.path} changed. Reloading server...\n');
var old = _server; var old = _server;
if (old != null) { if (old != null) {
// Do this asynchronously, because we really don't care about the old server anymore. // Do this asynchronously, because we really don't care about the old server anymore.
new Future(() async { new Future(() async {
// TODO: Instead of disconnecting, just forward websockets to the next server.
// Disconnect active WebSockets // Disconnect active WebSockets
var ws = old.app.container.make(AngelWebSocket) as AngelWebSocket; try {
var ws = old.app.container.make<AngelWebSocket>();
for (var client in ws.clients) { for (var client in ws.clients) {
try { try {
await client.close(WebSocketStatus.goingAway); await client.close(WebSocketStatus.goingAway);
} catch (e) { } catch (e) {
stderr.writeln( stderr.writeln(
'Couldn\'t close WebSocket from session #${client.request.session.id}: $e'); 'Couldn\'t close WebSocket from session #${client.request.session.id}: $e');
}
} }
}
Future.forEach(old.app.shutdownHooks, old.app.configure); await Future.forEach(old.app.shutdownHooks, old.app.configure);
} catch (_) {
// Fail silently...
}
}); });
} }
_server = null; _server = null;
var report = await _client.reloadSources(_mainIsolate.id);
if (!report.success) { if (hot) {
stderr.writeln('Hot reload failed!!!'); var report = await _client.reloadSources(_mainIsolate.id);
stderr.writeln(report.toString());
exit(1); if (!report.success) {
stderr.writeln('Hot reload failed!!!');
stderr.writeln(report.toString());
exit(1);
}
} }
var s = await _generateServer(); var s = await _generateServer();
@ -294,6 +352,7 @@ class HotReloader {
} }
} }
/*
class _Debounce<S> extends StreamTransformerBase<S, S> { class _Debounce<S> extends StreamTransformerBase<S, S> {
final Duration _delay; final Duration _delay;
@ -313,3 +372,4 @@ class _Debounce<S> extends StreamTransformerBase<S, S> {
}); });
} }
} }
*/

BIN
screenshots/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB