platform/lib/angel_hot.dart

376 lines
12 KiB
Dart
Raw Normal View History

2017-06-06 12:07:59 +00:00
import 'dart:async';
import 'dart:collection';
2018-06-07 16:11:03 +00:00
import 'dart:io'
show
ContentType,
Directory,
File,
FileStat,
FileSystemEntity,
FileSystemException,
HttpHeaders,
HttpRequest,
HttpServer,
Link,
Platform,
exit,
2018-10-02 15:20:11 +00:00
stderr,
2018-10-02 16:13:47 +00:00
stdin,
2018-10-02 15:20:11 +00:00
stdout;
2017-06-06 12:07:59 +00:00
import 'dart:isolate';
import 'package:angel_framework/angel_framework.dart';
2017-06-06 13:03:24 +00:00
import 'package:angel_websocket/server.dart';
2018-10-02 16:13:47 +00:00
import 'package:charcode/ascii.dart';
2018-06-07 16:11:03 +00:00
import 'package:dart2_constant/convert.dart';
import 'package:dart2_constant/io.dart';
2017-06-06 12:07:59 +00:00
import 'package:glob/glob.dart';
import 'package:html_builder/elements.dart';
import 'package:html_builder/html_builder.dart';
2018-10-02 15:20:11 +00:00
import 'package:io/ansi.dart';
2017-12-07 06:40:05 +00:00
import 'package:vm_service_lib/vm_service_lib.dart' as vm;
import 'package:vm_service_lib/vm_service_lib_io.dart' as vm;
2017-06-06 12:07:59 +00:00
import 'package:watcher/watcher.dart';
2017-10-19 18:32:41 +00:00
/// A utility class that watches the filesystem for changes, and starts new instances of an Angel server.
2017-06-06 12:07:59 +00:00
class HotReloader {
2017-12-07 06:40:05 +00:00
vm.VmService _client;
2018-06-07 16:11:03 +00:00
vm.IsolateRef _mainIsolate;
2017-06-12 19:26:45 +00:00
final StreamController<WatchEvent> _onChange =
new StreamController<WatchEvent>.broadcast();
2017-06-06 12:07:59 +00:00
final List _paths = [];
final StringRenderer _renderer = new StringRenderer(pretty: false);
final Queue<HttpRequest> _requestQueue = new Queue<HttpRequest>();
2018-06-07 16:11:03 +00:00
AngelHttp _server;
2017-06-06 12:07:59 +00:00
Duration _timeout;
2018-06-07 16:11:03 +00:00
vm.VM _vmachine;
2017-06-06 12:07:59 +00:00
2018-10-02 15:20:11 +00:00
/// 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;
2017-06-06 12:07:59 +00:00
/// Invoked to load a new instance of [Angel] on file changes.
2017-10-19 18:32:41 +00:00
final FutureOr<Angel> Function() generator;
2017-06-06 12:07:59 +00:00
2017-06-12 19:26:45 +00:00
/// Fires whenever a file change. You might consider using this to trigger
/// page reloads in a client.
Stream<WatchEvent> get onChange => _onChange.stream;
2017-06-06 12:07:59 +00:00
/// 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;
2017-12-07 06:40:05 +00:00
/// The Dart VM service host.
2017-06-06 12:07:59 +00:00
///
2017-12-07 06:40:05 +00:00
/// Default: `localhost`.
final String vmServiceHost;
/// The port to connect to the Dart VM service.
///
/// Default: `8181`.
final int vmServicePort;
2017-06-06 12:07:59 +00:00
/// 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,
2017-12-07 06:40:05 +00:00
{Duration timeout,
this.vmServiceHost: 'localhost',
2018-10-02 15:20:11 +00:00
this.vmServicePort: 8181,
this.enableHotkeys: true}) {
2017-06-06 12:07:59 +00:00
_timeout = timeout ?? new Duration(seconds: 5);
_paths.addAll(paths ?? []);
}
2017-06-12 19:26:45 +00:00
Future close() async {
_onChange.close();
}
2018-06-07 16:11:03 +00:00
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
2018-10-02 16:13:47 +00:00
..contentType = ContentType.html
..set(HttpHeaders.serverHeader, 'angel_hot');
2018-06-07 16:11:03 +00:00
if (request.headers
2018-10-02 16:13:47 +00:00
.value(HttpHeaders.acceptEncodingHeader)
2018-06-07 16:11:03 +00:00
?.toLowerCase()
?.contains('gzip') ==
true) {
response
2018-10-02 16:13:47 +00:00
..headers.set(HttpHeaders.contentEncodingHeader, 'gzip')
2018-06-07 16:11:03 +00:00
..add(gzip.encode(utf8.encode(_renderer.render(doc))));
} else
response.write(_renderer.render(doc));
response.close();
}
Future _handle(HttpRequest request) {
return _server.handleRequest(request);
}
2017-06-06 12:07:59 +00:00
Future handleRequest(HttpRequest request) async {
if (_server != null)
2018-06-07 16:11:03 +00:00
return await _handle(request);
2017-06-06 12:07:59 +00:00
else if (timeout == null)
_requestQueue.add(request);
else {
_requestQueue.add(request);
new Timer(timeout, () {
if (_requestQueue.remove(request)) {
// Send 502 response
2018-06-07 16:11:03 +00:00
sendError(request, HttpStatus.badGateway, '502 Bad Gateway',
'Request timed out after ${timeout.inMilliseconds}ms.');
2017-06-06 12:07:59 +00:00
}
});
}
}
2018-06-07 16:11:03 +00:00
Future<AngelHttp> _generateServer() async {
var s = await generator();
2017-10-19 18:32:41 +00:00
await Future.forEach(s.startupHooks, s.configure);
2017-06-06 12:07:59 +00:00
s.optimizeForProduction();
2018-06-07 16:11:03 +00:00
return new AngelHttp(s);
2017-06-06 12:07:59 +00:00
}
/// Starts listening to requests and filesystem events.
Future<HttpServer> startServer([address, int port]) async {
2018-10-02 16:13:47 +00:00
var isHot = true;
2018-06-08 17:45:01 +00:00
_server = await _generateServer();
2017-06-06 12:07:59 +00:00
if (_paths?.isNotEmpty != true)
2018-10-02 16:13:47 +00:00
print(yellow.wrap(
'WARNING: You have instantiated a HotReloader without providing any filesystem paths to watch.'));
2017-06-06 12:07:59 +00:00
2018-06-08 17:45:01 +00:00
if (!Platform.executableArguments.contains('--observe') &&
!Platform.executableArguments.contains('--enable-vm-service')) {
2018-10-02 16:13:47 +00:00
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;
2018-06-08 17:45:01 +00:00
} 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();
}
2018-06-07 16:11:03 +00:00
2018-10-02 16:13:47 +00:00
_onChange.stream
//.transform(new _Debounce(new Duration(seconds: 1)))
.listen(_handleWatchEvent);
2018-06-07 16:11:03 +00:00
while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst());
var server = await HttpServer.bind(address ?? '127.0.0.1', port ?? 0);
2017-06-06 12:07:59 +00:00
server.listen(handleRequest);
2018-10-02 15:20:11 +00:00
// Print a Flutter-like prompt...
if (enableHotkeys) {
2018-10-02 16:13:47 +00:00
var serverUri = new Uri(
scheme: 'http', host: server.address.address, port: server.port);
2018-10-02 15:20:11 +00:00
var host = vmServiceHost == 'localhost' ? '127.0.0.1' : vmServiceHost;
var observatoryUri =
new Uri(scheme: 'http', host: host, port: vmServicePort);
2018-10-02 16:13:47 +00:00
2018-10-02 15:20:11 +00:00
print(styleBold.wrap(
2018-10-02 16:13:47 +00:00
'\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]));
2018-10-02 15:20:11 +00:00
stdout.write(
2018-10-02 16:13:47 +00:00
'An Observatory debugger and profiler on ${Platform.operatingSystem} is available at: ');
2018-10-02 15:20:11 +00:00
print(wrapWith('$observatoryUri', [cyan, styleUnderlined]));
2018-10-02 16:13:47 +00:00
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();
}
});
2018-10-02 15:20:11 +00:00
}
2017-06-06 12:07:59 +00:00
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);
2017-06-12 19:26:45 +00:00
if (uri != null)
await _listenToStat(uri.toFilePath());
else
await _listenToStat(path.toFilePath());
2017-06-06 12:07:59 +00:00
} 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);
2018-06-07 16:11:03 +00:00
if (stat.type == FileSystemEntityType.link) {
2017-06-06 12:07:59 +00:00
var lnk = new Link(path);
var p = await lnk.resolveSymbolicLinks();
return await _listenToStat(p);
2018-06-07 16:11:03 +00:00
} else if (stat.type == FileSystemEntityType.file) {
2017-06-06 12:07:59 +00:00
var file = new File(path);
if (!await file.exists()) return null;
2018-06-07 16:11:03 +00:00
} else if (stat.type == FileSystemEntityType.directory) {
2017-06-06 12:07:59 +00:00
var dir = new Directory(path);
if (!await dir.exists()) return null;
} else
return null;
var watcher = new Watcher(path);
2017-06-12 19:26:45 +00:00
watcher.events.listen(_onChange.add, onError: (e) {
stderr.writeln('Could not listen to file changes at ${path}: $e');
});
2018-10-02 16:13:47 +00:00
// print('Listening for file changes at ${path}...');
2017-06-06 12:07:59 +00:00
return true;
} catch (e) {
if (e is! FileSystemException) rethrow;
}
}
var r = await _listen();
if (r == null) {
print(
2018-10-02 15:08:23 +00:00
'WARNING: Unable to watch path "$path" from working directory "${Directory.current.path}". Please ensure that it exists.');
2017-06-06 12:07:59 +00:00
}
}
2018-10-02 16:13:47 +00:00
_handleWatchEvent(WatchEvent e, [bool hot = true]) async {
print('${e.path} changed. Reloading server...\n');
2017-06-06 12:07:59 +00:00
var old = _server;
2017-06-06 13:03:24 +00:00
if (old != null) {
// Do this asynchronously, because we really don't care about the old server anymore.
new Future(() async {
// Disconnect active WebSockets
2018-10-02 16:13:47 +00:00
try {
var ws = old.app.container.make<AngelWebSocket>();
for (var client in ws.clients) {
try {
await client.close(WebSocketStatus.goingAway);
} catch (e) {
stderr.writeln(
'Couldn\'t close WebSocket from session #${client.request.session.id}: $e');
}
2017-06-06 13:03:24 +00:00
}
2018-10-02 16:13:47 +00:00
await Future.forEach(old.app.shutdownHooks, old.app.configure);
} catch (_) {
// Fail silently...
}
2017-06-06 13:03:24 +00:00
});
}
_server = null;
2017-12-07 06:40:05 +00:00
2018-10-02 16:13:47 +00:00
if (hot) {
var report = await _client.reloadSources(_mainIsolate.id);
if (!report.success) {
stderr.writeln('Hot reload failed!!!');
stderr.writeln(report.toString());
exit(1);
}
2017-06-06 12:07:59 +00:00
}
var s = await _generateServer();
_server = s;
2018-06-07 16:11:03 +00:00
while (!_requestQueue.isEmpty) await _handle(_requestQueue.removeFirst());
2017-06-06 12:07:59 +00:00
}
}
2017-06-12 19:26:45 +00:00
2018-10-02 16:13:47 +00:00
/*
2018-06-07 16:11:03 +00:00
class _Debounce<S> extends StreamTransformerBase<S, S> {
2017-06-12 19:26:45 +00:00
final Duration _delay;
const _Debounce(this._delay);
Stream<S> bind(Stream<S> 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;
}
});
}
}
2018-10-02 16:13:47 +00:00
*/