Add 'packages/hot/' from commit 'ed1422b4fbfdc0bfdd44dd54900f03fb9838ad9b'

git-subtree-dir: packages/hot
git-subtree-mainline: 0aab437ca1
git-subtree-split: ed1422b4fb
This commit is contained in:
Tobe O 2020-02-15 18:28:21 -05:00
commit 6890bbf53f
16 changed files with 723 additions and 0 deletions

56
packages/hot/.gitignore vendored Normal file
View file

@ -0,0 +1,56 @@
# See https://www.dartlang.org/tools/private-files.html
# Files and directories created by pub
.packages
.pub/
build/
# If you're building an application, you may want to check-in your pubspec.lock
pubspec.lock
# Directory created by dartdoc
# If you don't generate documentation locally you can remove this line.
doc/api/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.xml
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
# Gradle:
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin:
.idea/**/mongoSettings.xml
## File-based project format:
*.iws
## Plugin-specific files:
# IntelliJ
/out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/hot.iml" filepath="$PROJECT_DIR$/.idea/hot.iml" />
</modules>
</component>
</project>

View file

@ -0,0 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="VMOptions" value="--observe" />
<option name="filePath" value="$PROJECT_DIR$/example/basic/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/example/basic" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart (No VM service)" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true">
<option name="filePath" value="$PROJECT_DIR$/example/basic/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$/example/basic" />
<method />
</configuration>
</component>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

32
packages/hot/CHANGELOG.md Normal file
View file

@ -0,0 +1,32 @@
# 2.0.6
* Support `--observe=*`, `--enable-vm-service=*` (`startsWith`, instead of `==`).
# 2.0.5
* Use `dart:developer` to find the Observatory URI.
* Use the app's logger when necessary.
* Apply `package:pedantic`.
# 2.0.4
* Forcibly close app loggers on shutdown.
# 2.0.3
* Fixed up manual restart.
* Remove stutter on hotkey press.
# 2.0.2
* Fixed for compatibility with `package:angel_websocket@^2.0.0-alpha.5`.
# 2.0.1
* Add import of `package:angel_framework/http.dart`
* https://github.com/angel-dart/hot/pull/7
# 2.0.0
* Update for Dart 2 + Angel 2.
# 1.1.1+1
* Fix a bug that threw when `--observe` was not present.
# 1.1.1
* Disable the observatory from pausing the isolate
on exceptions, because Angel already handles
all exceptions by itself.

21
packages/hot/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 The Angel Framework
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

84
packages/hot/README.md Normal file
View file

@ -0,0 +1,84 @@
# 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
more reliable than merely reactively restarting a `Process`.
This package only works with the [Angel framework](https://github.com/angel-dart/angel).
# Installation
In your `pubspec.yaml`:
```yaml
dependencies:
angel_framework: ^2.0.0-alpha
angel_hot: ^2.0.0
```
# Usage
This package is dependent on the Dart VM service, so you *must* run
Dart with the `--observe` (or `--enable-vm-service`) argument!!!
Usage is fairly simple. Pass a function that creates an `Angel` server, along with a collection of paths
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
within a separate function, conventionally named `createServer`.
**Using this in production mode is not recommended, unless you are
specifically intending for a "hot code push" in production..**
You can watch:
* Files
* Directories
* Globs
* URI's
* `package:` URI's
```dart
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',
Uri.parse('package:angel_hot/angel_hot.dart')
]);
await hot.startServer('127.0.0.1', 3000);
}
Future<Angel> 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;
}
```

View file

@ -0,0 +1,8 @@
include: package:pedantic/analysis_options.yaml
analyzer:
strong-mode:
implicit-casts: false
linter:
rules:
- unnecessary_const
- unnecessary_new

View file

@ -0,0 +1,13 @@
import 'dart:io';
import 'package:angel_hot/angel_hot.dart';
import 'server.dart';
main() async {
var hot = HotReloader(createServer, [
Directory('src'),
'server.dart',
// Also allowed: Platform.script,
Uri.parse('package:angel_hot/angel_hot.dart')
]);
await hot.startServer('127.0.0.1', 3000);
}

View file

@ -0,0 +1,33 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:logging/logging.dart';
import 'src/foo.dart';
Future<Angel> createServer() async {
var app = Angel()..serializer = json.encode;
hierarchicalLoggingEnabled = true;
// Edit this line, and then refresh the page in your browser!
app.get('/', (req, res) => {'hello': 'hot world!'});
app.get('/foo', (req, res) => Foo(bar: 'baz'));
app.fallback((req, res) => throw AngelHttpException.notFound());
app.encoders.addAll({
'gzip': gzip.encoder,
'deflate': zlib.encoder,
});
app.logger = Logger.detached('angel')
..onRecord.listen((rec) {
print(rec);
if (rec.error != null) {
print(rec.error);
print(rec.stackTrace);
}
});
return app;
}

View file

@ -0,0 +1,9 @@
class Foo {
final String bar;
Foo({this.bar});
Map<String, dynamic> toJson() {
return {'bar': bar};
}
}

View file

@ -0,0 +1,400 @@
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:developer' as dev;
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:path/path.dart' as p;
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<WatchEvent> _onChange =
StreamController<WatchEvent>.broadcast();
final List _paths = [];
final StringRenderer _renderer = StringRenderer(pretty: false);
final Queue<HttpRequest> _requestQueue = Queue<HttpRequest>();
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<Angel> Function() generator;
/// Fires whenever a file change. You might consider using this to trigger
/// page reloads in a client.
Stream<WatchEvent> 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 ?? Duration(seconds: 5);
_paths.addAll(paths ?? []);
}
Future close() async {
await _io?.close(force: true);
await _onChange.close();
}
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);
Timer(timeout, () {
if (_requestQueue.remove(request)) {
// Send 502 response
sendError(request, HttpStatus.badGateway, '502 Bad Gateway',
'Request timed out after ${timeout.inMilliseconds}ms.');
}
});
}
}
Future<AngelHttp> _generateServer() async {
var s = await generator();
await Future.forEach(s.startupHooks, s.configure);
s.optimizeForProduction();
return AngelHttp.custom(s, startShared);
}
void _logWarning(String msg) {
if (_server?.app?.logger != null) {
_server.app.logger.warning(msg);
} else {
print(yellow.wrap('WARNING: $msg'));
}
}
void _logInfo(String msg) {
if (_server?.app?.logger != null) {
_server.app.logger.info(msg);
} else {
print(lightGray.wrap(msg));
}
}
/// Starts listening to requests and filesystem events.
Future<HttpServer> startServer([address, int port]) async {
var isHot = true;
_server = await _generateServer();
if (_paths?.isNotEmpty != true)
_logWarning(
'You have instantiated a HotReloader without providing any filesystem paths to watch.');
bool _sw(String s) {
return Platform.executableArguments.any((ss) => ss.startsWith(s));
}
if (!_sw('--observe') && !_sw('--enable-vm-service')) {
_logWarning(
'You have instantiated a HotReloader without passing `--enable-vm-service` or `--observe` to the Dart VM. Hot reloading will be disabled.');
isHot = false;
} else {
var info = await dev.Service.getInfo();
var uri = info.serverUri;
uri = uri.replace(path: p.join(uri.path, 'ws'));
if (uri.scheme == 'https')
uri = uri.replace(scheme: 'wss');
else
uri = uri.replace(scheme: 'ws');
_client = await vm.vmServiceConnectUri(uri.toString());
_vmachine ??= await _client.getVM();
_mainIsolate ??= _vmachine.isolates.first;
for (var isolate in _vmachine.isolates) {
await _client.setExceptionPauseMode(isolate.id, 'None');
}
await _listenToFilesystem();
}
_onChange.stream
//.transform(new _Debounce(new Duration(seconds: 1)))
.listen(_handleWatchEvent);
while (_requestQueue.isNotEmpty) 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 =
Uri(scheme: 'http', host: server.address.address, port: server.port);
var observatoryUri = await dev.Service.getInfo().then((i) => i.serverUri);
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<int> sub;
try {
sub = stdin.expand((l) => l).listen((ch) async {
if (ch == $r) {
_handleWatchEvent(
WatchEvent(ChangeType.MODIFY, '[manual-reload]'), isHot);
}
if (ch == $R) {
_logInfo('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;
await close();
await 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 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 = Link(path);
var p = await lnk.resolveSymbolicLinks();
return await _listenToStat(p);
} else if (stat.type == FileSystemEntityType.file) {
var file = File(path);
if (!await file.exists()) return null;
} else if (stat.type == FileSystemEntityType.directory) {
var dir = Directory(path);
if (!await dir.exists()) return null;
} else
return null;
var watcher = Watcher(path);
watcher.events.listen(_onChange.add, onError: (e) {
_logWarning('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) {
_logWarning(
'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.
scheduleMicrotask(() async {
// Disconnect active WebSockets
try {
var ws = _server.app.container.make<AngelWebSocket>();
for (var client in ws.clients) {
try {
await client.close();
} catch (e) {
_logWarning(
'Couldn\'t close WebSocket from session #${client.request.session.id}: $e');
}
}
// await Future.forEach(
// _server.app.shutdownHooks, _server.app.configure);
await _server.app.close();
_server.app.logger?.clearListeners();
} catch (_) {
// Fail silently...
}
});
}
}
_handleWatchEvent(WatchEvent e, [bool hot = true]) async {
_logInfo('${e.path} changed. Reloading server...\n');
await _killServer();
_server = null;
if (hot) {
var report = await _client.reloadSources(_mainIsolate.id);
if (!report.success) {
_logWarning(
'Hot reload failed - perhaps some sources have not been generated yet.');
}
}
var s = await _generateServer();
_server = s;
while (_requestQueue.isNotEmpty) await _handle(_requestQueue.removeFirst());
}
}
/*
class _Debounce<S> extends StreamTransformerBase<S, S> {
final Duration _delay;
const _Debounce(this._delay);
Stream<S> bind(Stream<S> stream) {
var initial = DateTime.now();
var next = initial.subtract(this._delay);
return stream.where((S data) {
var now = DateTime.now();
if (now.isAfter(next)) {
next = now.add(this._delay);
return true;
} else {
return false;
}
});
}
}
*/

21
packages/hot/pubspec.yaml Normal file
View file

@ -0,0 +1,21 @@
name: angel_hot
description: Supports hot reloading/hot code push of Angel servers on file changes.
version: 2.0.6
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/angel-dart/hot
environment:
sdk: ">=2.0.0 <3.0.0"
dependencies:
angel_framework: ^2.0.0-alpha
angel_websocket: ^2.0.0-alpha
charcode: ^1.0.0
glob: ^1.0.0
html_builder: ^1.0.0
io: ^0.3.2
path: ^1.0.0
vm_service_lib: ^0.3.5
watcher: ^0.9.0
dev_dependencies:
http: ^0.11.3
logging: ^0.11.0
pedantic: ^1.0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB