Add 'packages/production/' from commit '5ac875c504cd98a5075e0316da9c153b313fd3f9'
git-subtree-dir: packages/production git-subtree-mainline:c97363d290
git-subtree-split:5ac875c504
This commit is contained in:
commit
cf2ad35d2b
14 changed files with 680 additions and 0 deletions
13
packages/production/.gitignore
vendored
Normal file
13
packages/production/.gitignore
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
# See https://www.dartlang.org/guides/libraries/private-files
|
||||
|
||||
# Files and directories created by pub
|
||||
.dart_tool/
|
||||
.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/
|
9
packages/production/CHANGELOG.md
Normal file
9
packages/production/CHANGELOG.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# 1.0.0
|
||||
* Support SSL/HTTP2.
|
||||
* Support muting the logger via `--quiet`.
|
||||
|
||||
# 1.0.0-alpha.1
|
||||
* Import `framework/http`.
|
||||
|
||||
# 1.0.0-alpha
|
||||
* Initial version.
|
21
packages/production/LICENSE
Normal file
21
packages/production/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2018 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.
|
116
packages/production/README.md
Normal file
116
packages/production/README.md
Normal file
|
@ -0,0 +1,116 @@
|
|||
# production
|
||||
[![Pub](https://img.shields.io/pub/v/angel_production.svg)](https://pub.dartlang.org/packages/angel_production)
|
||||
|
||||
Helpers for concurrency, message-passing, rotating loggers, and other production functionality in Angel.
|
||||
|
||||
![Screenshot](screenshot.png)
|
||||
|
||||
This will become the de-facto way to run Angel applications in deployed environments, as it
|
||||
takes care of inter-isolate communication, respawning dead processes, and other housekeeping for you automatically.
|
||||
|
||||
Most users will want to use the `Runner` class.
|
||||
|
||||
## `Runner`
|
||||
`Runner` is a utility, powered by `package:args`, that is intended to be the entry point of your application.
|
||||
|
||||
Instantiate it as follows, and your file will become a command-line executable that spawns multiple instances of your
|
||||
application:
|
||||
|
||||
```dart
|
||||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_production/angel_production.dart';
|
||||
|
||||
main(List<String> args) => Runner('example', configureServer).run(args);
|
||||
|
||||
Future configureServer(Angel app) async {
|
||||
app.get('/', (req, res) => 'Hello, production world!');
|
||||
|
||||
app.get('/crash', (req, res) {
|
||||
// We'll crash this instance deliberately, but the Runner will auto-respawn for us.
|
||||
Timer(const Duration(seconds: 3), Isolate.current.kill);
|
||||
return 'Crashing in 3s...';
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`Runner` will automatically re-spawn crashed instances, unless `--no-respawn` is passed. This can prevent
|
||||
your server from entirely going down at the first error, and adds a layer of fault tolerance to your
|
||||
infrastructure.
|
||||
|
||||
When combined with `systemd`, deploying Angel applications on Linux can be very simple.
|
||||
|
||||
## Message Passing
|
||||
The `Runner` class uses [`package:pub_sub`](https://github.com/thosakwe/pub_sub) to coordinate
|
||||
message passing between isolates.
|
||||
|
||||
When one isolate sends a message, all other isolates will
|
||||
receive the same message, except for the isolate that sent it.
|
||||
|
||||
It is injected into your application's `Container` as
|
||||
`pub_sub.Client`, so you can use it as follows:
|
||||
|
||||
```dart
|
||||
// Use the injected `pub_sub.Client` to send messages.
|
||||
var client = app.container.make<pub_sub.Client>();
|
||||
|
||||
// We can listen for an event to perform some behavior.
|
||||
//
|
||||
// Here, we use message passing to synchronize some common state.
|
||||
var onGreetingChanged = await client.subscribe('user_upgraded');
|
||||
onGreetingChanged
|
||||
.cast<User>()
|
||||
.listen((user) {
|
||||
// Do something...
|
||||
});
|
||||
```
|
||||
|
||||
## Run-time Metadata
|
||||
At run-time, you may want to know information about the currently-running instance,
|
||||
for example, which number instance. For this, the `InstanceInfo` class is injected
|
||||
into each instance:
|
||||
|
||||
```dart
|
||||
var instanceInfo = app.container.make<InstanceInfo>();
|
||||
print('This is instance #${instanceInfo.id}');
|
||||
```
|
||||
|
||||
## Command-line Options
|
||||
The `Runner` class supplies options like the following:
|
||||
|
||||
```
|
||||
Tobes-MacBook-Air:production thosakwe$ dart example/main.dart --help
|
||||
____________ ________________________
|
||||
___ |__ | / /_ ____/__ ____/__ /
|
||||
__ /| |_ |/ /_ / __ __ __/ __ /
|
||||
_ ___ | /| / / /_/ / _ /___ _ /___
|
||||
/_/ |_/_/ |_/ ____/ /_____/ /_____/
|
||||
|
||||
A batteries-included, full-featured, full-stack framework in Dart.
|
||||
|
||||
https://angel-dart.github.io
|
||||
|
||||
Options:
|
||||
-h, --help Print this help information.
|
||||
--[no-]respawn Automatically respawn crashed application instances.
|
||||
(defaults to on)
|
||||
|
||||
--use-zone Create a new Zone for each request.
|
||||
--quiet Completely mute logging.
|
||||
--ssl Listen for HTTPS instead of HTTP.
|
||||
--http2 Listen for HTTP/2 instead of HTTP/1.1.
|
||||
-a, --address The address to listen on.
|
||||
(defaults to "127.0.0.1")
|
||||
|
||||
-j, --concurrency The number of isolates to spawn.
|
||||
(defaults to "4")
|
||||
|
||||
-p, --port The port to listen on.
|
||||
(defaults to "3000")
|
||||
|
||||
--certificate-file The PEM certificate file to read.
|
||||
--certificate-password The PEM certificate file password.
|
||||
--key-file The PEM key file to read.
|
||||
--key-password The PEM key file password.
|
||||
```
|
4
packages/production/analysis_options.yaml
Normal file
4
packages/production/analysis_options.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
include: package:pedantic/analysis_options.yaml
|
||||
analyzer:
|
||||
strong-mode:
|
||||
implicit-casts: false
|
29
packages/production/dev.key
Normal file
29
packages/production/dev.key
Normal file
|
@ -0,0 +1,29 @@
|
|||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIE5DAcBgoqhkiG9w0BDAEBMA4ECL7L6rj6uEHGAgIIAASCBMLbucyfqAkgCbhP
|
||||
xNSHYllPMAv/dsIjtnsBwepCXPGkCBCuOAw/2FaCHjN9hBqL5V7fkrKeaemhm2YE
|
||||
ycPtlHJYPDf3kEkyMjdZ9rIY6kePGfQizs2uJPcXj4YPyQ4HsfVXpOicKfQrouf5
|
||||
Mze9bGzeMN065q3iP4dYUMwHAyZYteXCsanQNHlqvsWli0W+H8St8fdsXefZhnv1
|
||||
qVatKWdNdWQ9t5MuljgNU2Vv56sHKEYXI0yLxk2QUMk8KlJfnmt8foYUsnPUXHmc
|
||||
gIjLKwwVkpdololnEHSNu0cEOUPowjgJru+uMpn7vdNl7TPEQ9jbEgdNg4JwoYzU
|
||||
0nao8WzjaSp7kzvZz0VFwKnk5AjstGvvuAWckADdq23QElbn/mF7AG1m/TBpYxzF
|
||||
gTt37UdndS/AcvVznWVVrRP5iTSIawdIwvqI4s7rqsoE0GCcak+RhchgAz2gWKkS
|
||||
oODUo0JL6pPVbJ3l4ebbaO6c99nDVc8dViPtc1EkStJEJ2O4kI4xgLSCr4Y9ahKn
|
||||
oAaoSkX7Xxq3aQm+BzqSpLjdGL8atsqR/YVOIHYIl3gThvP0NfZGx1xHyvO5mCdZ
|
||||
kHxSA7tKWxauZ3eQ2clbnzeRsl4El0WMHy/5K1ovene4v7sunmoXVtghBC8hK6eh
|
||||
zMO9orex2PNQ/VQC7HCvtytunOVx1lkSBoNo7hR70igg6rW9H7UyoAoBOwMpT1xa
|
||||
J6V62nqruTKOqFNfur7aHJGpHGtDb5/ickHeYCyPTvmGp67u4wChzKReeg02oECe
|
||||
d1E5FKAcIa8s9TVOB6Z+HvTRNQZu2PsI6TJnjQRowvY9DAHiWTlJZBBY/pko3hxX
|
||||
TsIeybpvRdEHpDWv86/iqtw1hv9CUxS/8ZTWUgBo+osShHW79FeDASr9FC4/Zn76
|
||||
ZDERTgV4YWlW/klVWcG2lFo7jix+OPXAB+ZQavLhlN1xdWBcIz1AUWjAM4hdPylW
|
||||
HCX4PB9CQIPl2E7F+Y2p6nMcMWSJVBi5UIH7E9LfaBguXSzMmTk2Fw5p1aOQ6wfN
|
||||
goVAMVwi8ppAVs741PfHdZ295xMmK/1LCxz5DeAdD/tsA/SYfT753GotioDuC7im
|
||||
EyJ5JyvTr5I6RFFBuqt3NlUb3Hp16wP3B2x9DZiB6jxr0l341/NHgsyeBXkuIy9j
|
||||
ON2mvpBPCJhS8kgWo3G0UyyKnx64tcgpGuSvZhGwPz843B6AbYyE6pMRfSWRMkMS
|
||||
YZYa+VNKhR4ixdj07ocFZEWLVjCH7kxkE8JZXKt8jKYmkWd0lS1QVjgaKlO6lRa3
|
||||
q6SPJkhW6pvqobvcqVNXwi1XuzpZeEbuh0B7OTekFTTxx5g9XeDl56M8SVQ1KEhT
|
||||
Q1t7H2Nba18WCB7cf+6PN0F0K0Jz1Kq7ZWaqEI/grX1m4RQuvNF5807sB/QKMO/Z
|
||||
Gz3NXvHg5xTJRd/567lxPGkor0cE7qD1EZfmJ2HrBYXQ91bhgA7LToBuMZo6ZRXH
|
||||
QfsanjbP4FPLMiGdQigLjj3A35L/f4sQOOVac/sRaFnm7pzcxsMvyVU/YtvGcjYE
|
||||
xaOOVnamg661Wo0wksXoDjeSz/JIyyKO3Gwp1FSm2wGLjjy/Ehmqcqy8rvHuf07w
|
||||
AUukhVtTNn4=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
57
packages/production/dev.pem
Normal file
57
packages/production/dev.pem
Normal file
|
@ -0,0 +1,57 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDKTCCAhGgAwIBAgIJAOWmjTS+OnTEMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV
|
||||
BAMMDGludGVybWVkaWF0ZTAeFw0xNTA1MTgwOTAwNDBaFw0yMzA4MDQwOTAwNDBa
|
||||
MBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||
AQoCggEBALlcwQJuzd+xH8QFgfJSn5tRlvhkldSX98cE7NiA602NBbnAVyUrkRXq
|
||||
Ni75lgt0kwjYfA9z674m8WSVbgpLPintPCla9CYky1TH0keIs8Rz6cGWHryWEHiu
|
||||
EDuljQynu2b3sAFuHu9nfWurbJwZnFakBKpdQ9m4EyOZCHC/jHYY7HacKSXg1Cki
|
||||
we2ca0BWDrcqy8kLy0dZ5oC6IZG8O8drAK8f3f44CRYw59D3sOKBrKXaabpvyEcb
|
||||
N7Wk2HDBVwHpUJo1reVwtbM8dhqQayYSD8oXnGpP3RQNu/e2rzlXRyq/BfcDY1JI
|
||||
7TbC4t/7/N4EcPSpGsTcSOC9A7FpzvECAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
|
||||
hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
|
||||
BBYEFCnwiEMMFZh7NhCr+qA8K0w4Q+AOMB8GA1UdIwQYMBaAFB0h1Evsaw2vfrmS
|
||||
YuoCTmC4EE6ZMA0GCSqGSIb3DQEBCwUAA4IBAQAcFmHMaXRxyoNaeOowQ6iQWoZd
|
||||
AUbvG7SHr7I6Pi2aqdqofsKWts7Ytm5WsS0M2nN+sW504houu0iCPeJJX8RQw2q4
|
||||
CCcNOs9IXk+2uMzlpocHpv+yYoUiD5DxgWh7eghQMLyMpf8FX3Gy4VazeuXznHOM
|
||||
4gE4L417xkDzYOzqVTp0FTyAPUv6G2euhNCD6TMru9REcRhYul+K9kocjA5tt2KG
|
||||
MH6y28LXbLyq4YJUxSUU9gY/xlnbbZS48KDqEcdYC9zjW9nQ0qS+XQuQuFIcwjJ5
|
||||
V4kAUYxDu6FoTpyQjgsrmBbZlKNxH7Nj4NDlcdJhp/zeSKHqWa5hSWjjKIxp
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAjCCAeqgAwIBAgIJAOWmjTS+OnTDMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||
WjAXMRUwEwYDVQQDDAxpbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IB
|
||||
DwAwggEKAoIBAQDSrAO1CoPvUllgLOzDm5nG0skDF7vh1DUgAIDVGz0ecD0JFbQx
|
||||
EF79pju/6MbtpTW2FYvRp11t/G7rGtX923ybOHY/1MNFQrdIvPlO1VV7IGKjoMwP
|
||||
DNeb0fIGjHoE9QxaDxR8NX8xQbItpsw+TUtRfc9SLkR+jaYJfVRoM21BOncZbSHE
|
||||
YKiZlEbpecB/+EtwVpgvl+8mPD5U07Fi4fp/lza3WXInXQPyiTVllIEJCt4PKmlu
|
||||
MocNaJOW38bysL7i0PzDpVZtOxLHOTaW68yF3FckIHNCaA7k1ABEEEegjFMmIao7
|
||||
B9w7A0jvr4jZVvNmui5Djjn+oJxwEVVgyf8LAgMBAAGjUDBOMB0GA1UdDgQWBBQd
|
||||
IdRL7GsNr365kmLqAk5guBBOmTAfBgNVHSMEGDAWgBRk81s9d0ZbiZhh44KckwPb
|
||||
oTc0XzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBZQTK0plfdB5PC
|
||||
cC5icut4EmrByJa1RbU7ayuEE70e7hla6KVmVjVdCBGltI4jBYwfhKbRItHiAJ/8
|
||||
x+XZKBG8DLPFuDb7lAa1ObhAYF7YThUFPQYaBhfzKcWrdmWDBFpvNv6E0Mm364dZ
|
||||
e7Yxmbe5S4agkYPoxEzgEYmcUk9jbjdR6eTbs8laG169ljrECXfEU9RiAcqz5iSX
|
||||
NLSewqB47hn3B9qgKcQn+PsgO2j7M+rfklhNgeGJeWmy7j6clSOuCsIjWHU0RLQ4
|
||||
0W3SB/rpEAJ7fgQbYUPTIUNALSOWi/o1tDX2mXPRjBoxqAv7I+vYk1lZPmSzkyRh
|
||||
FKvRDxsW
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDAzCCAeugAwIBAgIJAJ0MomS4Ck+8MA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV
|
||||
BAMMDXJvb3RhdXRob3JpdHkwHhcNMTUwNTE4MDkwMDQwWhcNMjMwODA0MDkwMDQw
|
||||
WjAYMRYwFAYDVQQDDA1yb290YXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOC
|
||||
AQ8AMIIBCgKCAQEAts1ijtBV92S2cOvpUMOSTp9c6A34nIGr0T5Nhz6XiqRVT+gv
|
||||
dQgmkdKJQjbvR60y6jzltYFsI2MpGVXY8h/oAL81D/k7PDB2aREgyBfTPAhBHyGw
|
||||
siR+2xYt5b/Zs99q5RdRqQNzNpLPJriIKvUsRyQWy1UiG2s7pRXQeA8qB0XtJdCj
|
||||
kFIi+G2bDsaffspGeDOCqt7t+yqvRXfSES0c/l7DIHaiMbbp4//ZNML3RNgAjPz2
|
||||
hCezZ+wOYajOIyoSPK8IgICrhYFYxvgWxwbLDBEfC5B3jOQsySe10GoRAKZz1gBV
|
||||
DmgReu81tYJmdgkc9zknnQtIFdA0ex+GvZlfWQIDAQABo1AwTjAdBgNVHQ4EFgQU
|
||||
ZPNbPXdGW4mYYeOCnJMD26E3NF8wHwYDVR0jBBgwFoAUZPNbPXdGW4mYYeOCnJMD
|
||||
26E3NF8wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEATzkZ97K777uZ
|
||||
lQcduNX3ey4IbCiEzFA2zO5Blj+ilfIwNbZXNOgm/lqNvVGDYs6J1apJJe30vL3X
|
||||
J+t2zsZWzzQzb9uIU37zYemt6m0fHrSrx/iy5lGNqt3HMfqEcOqSCOIK3PCTMz2/
|
||||
uyGe1iw33PVeWsm1JUybQ9IrU/huJjbgOHU4wab+8SJCM49ipArp68Fr6j4lcEaE
|
||||
4rfRg1ZsvxiOyUB3qPn6wyL/JB8kOJ+QCBe498376eaem8AEFk0kQRh6hDaWtq/k
|
||||
t6IIXQLjx+EBDVP/veK0UnVhKRP8YTOoV8ZiG1NcdlJmX/Uk7iAfevP7CkBfSN8W
|
||||
r6AL284qtw==
|
||||
-----END CERTIFICATE-----
|
46
packages/production/example/main.dart
Normal file
46
packages/production/example/main.dart
Normal file
|
@ -0,0 +1,46 @@
|
|||
import 'dart:async';
|
||||
import 'dart:isolate';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_production/angel_production.dart';
|
||||
import 'package:pub_sub/pub_sub.dart' as pub_sub;
|
||||
|
||||
main(List<String> args) => Runner('example', configureServer).run(args);
|
||||
|
||||
Future configureServer(Angel app) async {
|
||||
// Use the injected `pub_sub.Client` to send messages.
|
||||
var client = app.container.make<pub_sub.Client>();
|
||||
var greeting = 'Hello! This is the default greeting.';
|
||||
|
||||
// We can listen for an event to perform some behavior.
|
||||
//
|
||||
// Here, we use message passing to synchronize some common state.
|
||||
var onGreetingChanged = await client.subscribe('greeting_changed');
|
||||
onGreetingChanged
|
||||
.cast<String>()
|
||||
.listen((newGreeting) => greeting = newGreeting);
|
||||
|
||||
// Add some routes...
|
||||
app.get('/', (req, res) => 'Hello, production world!');
|
||||
|
||||
app.get('/404', (req, res) {
|
||||
res.statusCode = 404;
|
||||
return res.close();
|
||||
});
|
||||
|
||||
// Create some routes to demonstrate message passing.
|
||||
app.get('/greeting', (req, res) => greeting);
|
||||
|
||||
// This route will push a new value for `greeting`.
|
||||
app.get('/change_greeting/:newGreeting', (req, res) {
|
||||
greeting = req.params['newGreeting'] as String;
|
||||
client.publish('greeting_changed', greeting);
|
||||
return 'Changed greeting -> $greeting';
|
||||
});
|
||||
|
||||
// The `Runner` helps with fault tolerance.
|
||||
app.get('/crash', (req, res) {
|
||||
// We'll crash this instance deliberately, but the Runner will auto-respawn for us.
|
||||
Timer(const Duration(seconds: 3), Isolate.current.kill);
|
||||
return 'Crashing in 3s...';
|
||||
});
|
||||
}
|
3
packages/production/lib/angel_production.dart
Normal file
3
packages/production/lib/angel_production.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'src/instance_info.dart';
|
||||
export 'src/options.dart';
|
||||
export 'src/runner.dart';
|
6
packages/production/lib/src/instance_info.dart
Normal file
6
packages/production/lib/src/instance_info.dart
Normal file
|
@ -0,0 +1,6 @@
|
|||
/// Information about the currently-running instance.
|
||||
class InstanceInfo {
|
||||
final int id;
|
||||
|
||||
const InstanceInfo({this.id});
|
||||
}
|
79
packages/production/lib/src/options.dart
Normal file
79
packages/production/lib/src/options.dart
Normal file
|
@ -0,0 +1,79 @@
|
|||
import 'dart:io';
|
||||
import 'package:args/args.dart';
|
||||
|
||||
class RunnerOptions {
|
||||
static final ArgParser argParser = ArgParser()
|
||||
..addFlag('help',
|
||||
abbr: 'h', help: 'Print this help information.', negatable: false)
|
||||
..addFlag('respawn',
|
||||
help: 'Automatically respawn crashed application instances.',
|
||||
defaultsTo: true,
|
||||
negatable: true)
|
||||
..addFlag('use-zone',
|
||||
negatable: false, help: 'Create a new Zone for each request.')
|
||||
..addFlag('quiet', negatable: false, help: 'Completely mute logging.')
|
||||
..addFlag('ssl',
|
||||
negatable: false, help: 'Listen for HTTPS instead of HTTP.')
|
||||
..addFlag('http2',
|
||||
negatable: false, help: 'Listen for HTTP/2 instead of HTTP/1.1.')
|
||||
..addOption('address',
|
||||
abbr: 'a', defaultsTo: '127.0.0.1', help: 'The address to listen on.')
|
||||
..addOption('concurrency',
|
||||
abbr: 'j',
|
||||
defaultsTo: Platform.numberOfProcessors.toString(),
|
||||
help: 'The number of isolates to spawn.')
|
||||
..addOption('port',
|
||||
abbr: 'p', defaultsTo: '3000', help: 'The port to listen on.')
|
||||
..addOption('certificate-file', help: 'The PEM certificate file to read.')
|
||||
..addOption('certificate-password',
|
||||
help: 'The PEM certificate file password.')
|
||||
..addOption('key-file', help: 'The PEM key file to read.')
|
||||
..addOption('key-password', help: 'The PEM key file password.');
|
||||
|
||||
final String hostname,
|
||||
certificateFile,
|
||||
keyFile,
|
||||
certificatePassword,
|
||||
keyPassword;
|
||||
final int concurrency, port;
|
||||
final bool useZone, respawn, quiet, ssl, http2;
|
||||
|
||||
RunnerOptions(
|
||||
{this.hostname = '127.0.0.1',
|
||||
this.port = 3000,
|
||||
this.concurrency = 1,
|
||||
this.useZone = false,
|
||||
this.respawn = true,
|
||||
this.quiet = false,
|
||||
this.certificateFile,
|
||||
this.keyFile,
|
||||
this.ssl = false,
|
||||
this.http2 = false,
|
||||
this.certificatePassword,
|
||||
this.keyPassword});
|
||||
|
||||
factory RunnerOptions.fromArgResults(ArgResults argResults) {
|
||||
return RunnerOptions(
|
||||
hostname: argResults['address'] as String,
|
||||
port: int.parse(argResults['port'] as String),
|
||||
concurrency: int.parse(argResults['concurrency'] as String),
|
||||
useZone: argResults['use-zone'] as bool,
|
||||
respawn: argResults['respawn'] as bool,
|
||||
quiet: argResults['quiet'] as bool,
|
||||
certificateFile: argResults.wasParsed('certificate-file')
|
||||
? argResults['certificate-file'] as String
|
||||
: null,
|
||||
keyFile: argResults.wasParsed('key-file')
|
||||
? argResults['key-file'] as String
|
||||
: null,
|
||||
ssl: argResults['ssl'] as bool,
|
||||
http2: argResults['http2'] as bool,
|
||||
certificatePassword: argResults.wasParsed('certificate-password')
|
||||
? argResults['certificate-password'] as String
|
||||
: null,
|
||||
keyPassword: argResults.wasParsed('key-password')
|
||||
? argResults['key-password'] as String
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
281
packages/production/lib/src/runner.dart
Normal file
281
packages/production/lib/src/runner.dart
Normal file
|
@ -0,0 +1,281 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:angel_container/angel_container.dart';
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_framework/http.dart';
|
||||
import 'package:angel_framework/http2.dart';
|
||||
import 'package:args/args.dart';
|
||||
import 'package:io/ansi.dart';
|
||||
import 'package:io/io.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:pub_sub/isolate.dart' as pub_sub;
|
||||
import 'package:pub_sub/pub_sub.dart' as pub_sub;
|
||||
import 'instance_info.dart';
|
||||
import 'options.dart';
|
||||
|
||||
/// A command-line utility for easier running of multiple instances of an Angel application.
|
||||
///
|
||||
/// Makes it easy to do things like configure SSL, log messages, and send messages between
|
||||
/// all running instances.
|
||||
class Runner {
|
||||
final String name;
|
||||
final AngelConfigurer configureServer;
|
||||
final Reflector reflector;
|
||||
|
||||
Runner(this.name, this.configureServer,
|
||||
{this.reflector = const EmptyReflector()});
|
||||
|
||||
static const String asciiArt = '''
|
||||
____________ ________________________
|
||||
___ |__ | / /_ ____/__ ____/__ /
|
||||
__ /| |_ |/ /_ / __ __ __/ __ /
|
||||
_ ___ | /| / / /_/ / _ /___ _ /___
|
||||
/_/ |_/_/ |_/ \____/ /_____/ /_____/
|
||||
|
||||
''';
|
||||
|
||||
static void handleLogRecord(LogRecord record, RunnerOptions options) {
|
||||
if (options.quiet) return;
|
||||
var code = chooseLogColor(record.level);
|
||||
|
||||
if (record.error == null) print(code.wrap(record.toString()));
|
||||
|
||||
if (record.error != null) {
|
||||
var err = record.error;
|
||||
if (err is AngelHttpException && err.statusCode != 500) return;
|
||||
print(code.wrap(record.toString() + '\n'));
|
||||
print(code.wrap(err.toString()));
|
||||
|
||||
if (record.stackTrace != null) {
|
||||
print(code.wrap(record.stackTrace.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chooses a color based on the logger [level].
|
||||
static AnsiCode chooseLogColor(Level level) {
|
||||
if (level == Level.SHOUT)
|
||||
return backgroundRed;
|
||||
else if (level == Level.SEVERE)
|
||||
return red;
|
||||
else if (level == Level.WARNING)
|
||||
return yellow;
|
||||
else if (level == Level.INFO)
|
||||
return cyan;
|
||||
else if (level == Level.FINER || level == Level.FINEST) return lightGray;
|
||||
return resetAll;
|
||||
}
|
||||
|
||||
/// Spawns a new instance of the application in a separate isolate.
|
||||
///
|
||||
/// If the command-line arguments permit, then the instance will be respawned on crashes.
|
||||
///
|
||||
/// The returned [Future] completes when the application instance exits.
|
||||
///
|
||||
/// If respawning is enabled, the [Future] will *never* complete.
|
||||
Future spawnIsolate(int id, RunnerOptions options, SendPort pubSubSendPort) {
|
||||
return _spawnIsolate(id, Completer(), options, pubSubSendPort);
|
||||
}
|
||||
|
||||
Future _spawnIsolate(
|
||||
int id, Completer c, RunnerOptions options, SendPort pubSubSendPort) {
|
||||
var onLogRecord = ReceivePort();
|
||||
var onExit = ReceivePort();
|
||||
var onError = ReceivePort();
|
||||
var runnerArgs = _RunnerArgs(name, configureServer, options, reflector,
|
||||
onLogRecord.sendPort, pubSubSendPort);
|
||||
var argsWithId = _RunnerArgsWithId(id, runnerArgs);
|
||||
|
||||
Isolate.spawn(isolateMain, argsWithId,
|
||||
onExit: onExit.sendPort,
|
||||
onError: onError.sendPort,
|
||||
errorsAreFatal: true && false)
|
||||
.then((isolate) {})
|
||||
.catchError(c.completeError);
|
||||
|
||||
onLogRecord.listen((msg) => handleLogRecord(msg as LogRecord, options));
|
||||
|
||||
onError.listen((msg) {
|
||||
if (msg is List) {
|
||||
var e = msg[0], st = StackTrace.fromString(msg[1].toString());
|
||||
handleLogRecord(
|
||||
LogRecord(
|
||||
Level.SEVERE, 'Fatal error', runnerArgs.loggerName, e, st),
|
||||
options);
|
||||
} else {
|
||||
handleLogRecord(
|
||||
LogRecord(Level.SEVERE, 'Fatal error', runnerArgs.loggerName, msg),
|
||||
options);
|
||||
}
|
||||
});
|
||||
|
||||
onExit.listen((_) {
|
||||
if (options.respawn) {
|
||||
handleLogRecord(
|
||||
LogRecord(
|
||||
Level.WARNING,
|
||||
'Instance #$id at ${DateTime.now()} crashed. Respawning immediately...',
|
||||
runnerArgs.loggerName),
|
||||
options);
|
||||
_spawnIsolate(id, c, options, pubSubSendPort);
|
||||
} else {
|
||||
c.complete();
|
||||
}
|
||||
});
|
||||
|
||||
return c.future
|
||||
.whenComplete(onExit.close)
|
||||
.whenComplete(onError.close)
|
||||
.whenComplete(onLogRecord.close);
|
||||
}
|
||||
|
||||
/// Starts a number of isolates, running identical instances of an Angel application.
|
||||
Future run(List<String> args) async {
|
||||
pub_sub.Server server;
|
||||
|
||||
try {
|
||||
var argResults = RunnerOptions.argParser.parse(args);
|
||||
var options = RunnerOptions.fromArgResults(argResults);
|
||||
|
||||
if (options.ssl || options.http2) {
|
||||
if (options.certificateFile == null) {
|
||||
throw ArgParserException('Missing --certificate-file option.');
|
||||
} else if (options.keyFile == null) {
|
||||
throw ArgParserException('Missing --key-file option.');
|
||||
}
|
||||
}
|
||||
|
||||
print(darkGray.wrap(asciiArt.trim() +
|
||||
'\n\n' +
|
||||
"A batteries-included, full-featured, full-stack framework in Dart." +
|
||||
'\n\n' +
|
||||
'https://angel-dart.github.io\n'));
|
||||
|
||||
if (argResults['help'] == true) {
|
||||
stdout..writeln('Options:')..writeln(RunnerOptions.argParser.usage);
|
||||
return;
|
||||
}
|
||||
|
||||
print('Starting `${name}` application...');
|
||||
|
||||
var adapter = pub_sub.IsolateAdapter();
|
||||
server = pub_sub.Server([adapter]);
|
||||
|
||||
// Register clients
|
||||
for (int i = 0; i < Platform.numberOfProcessors; i++) {
|
||||
server.registerClient(pub_sub.ClientInfo('client$i'));
|
||||
}
|
||||
|
||||
server.start();
|
||||
|
||||
await Future.wait(List.generate(options.concurrency,
|
||||
(id) => spawnIsolate(id, options, adapter.receivePort.sendPort)));
|
||||
} on ArgParserException catch (e) {
|
||||
stderr
|
||||
..writeln(red.wrap(e.message))
|
||||
..writeln()
|
||||
..writeln(red.wrap('Options:'))
|
||||
..writeln(red.wrap(RunnerOptions.argParser.usage));
|
||||
exitCode = ExitCode.usage.code;
|
||||
} catch (e, st) {
|
||||
stderr
|
||||
..writeln(red.wrap('fatal error: $e'))
|
||||
..writeln(red.wrap(st.toString()));
|
||||
exitCode = 1;
|
||||
} finally {
|
||||
await server?.close();
|
||||
}
|
||||
}
|
||||
|
||||
static void isolateMain(_RunnerArgsWithId argsWithId) {
|
||||
var args = argsWithId.args;
|
||||
hierarchicalLoggingEnabled = true;
|
||||
|
||||
var zone = Zone.current.fork(specification: ZoneSpecification(
|
||||
print: (self, parent, zone, msg) {
|
||||
args.loggingSendPort.send(LogRecord(Level.INFO, msg, args.loggerName));
|
||||
},
|
||||
));
|
||||
|
||||
zone.run(() async {
|
||||
var client =
|
||||
pub_sub.IsolateClient('client${argsWithId.id}', args.pubSubSendPort);
|
||||
|
||||
var app = Angel(reflector: args.reflector)
|
||||
..container.registerSingleton<pub_sub.Client>(client)
|
||||
..container.registerSingleton(InstanceInfo(id: argsWithId.id));
|
||||
|
||||
app.shutdownHooks.add((_) => client.close());
|
||||
|
||||
await app.configure(args.configureServer);
|
||||
|
||||
if (app.logger == null) {
|
||||
app.logger = Logger(args.loggerName)
|
||||
..onRecord.listen((rec) => Runner.handleLogRecord(rec, args.options));
|
||||
}
|
||||
|
||||
AngelHttp http;
|
||||
SecurityContext securityContext;
|
||||
Uri serverUrl;
|
||||
|
||||
if (args.options.ssl || args.options.http2) {
|
||||
securityContext = SecurityContext();
|
||||
securityContext.useCertificateChain(args.options.certificateFile,
|
||||
password: args.options.certificatePassword);
|
||||
securityContext.usePrivateKey(args.options.keyFile,
|
||||
password: args.options.keyPassword);
|
||||
}
|
||||
|
||||
if (args.options.ssl) {
|
||||
http = AngelHttp.custom(app, startSharedSecure(securityContext),
|
||||
useZone: args.options.useZone);
|
||||
} else {
|
||||
http =
|
||||
AngelHttp.custom(app, startShared, useZone: args.options.useZone);
|
||||
}
|
||||
|
||||
Driver driver;
|
||||
|
||||
if (args.options.http2) {
|
||||
securityContext.setAlpnProtocols(['h2'], true);
|
||||
var http2 = AngelHttp2.custom(app, securityContext, startSharedHttp2,
|
||||
useZone: args.options.useZone);
|
||||
http2.onHttp1.listen(http.handleRequest);
|
||||
driver = http2;
|
||||
} else {
|
||||
driver = http;
|
||||
}
|
||||
|
||||
await driver.startServer(args.options.hostname, args.options.port);
|
||||
serverUrl = driver.uri;
|
||||
if (args.options.ssl || args.options.http2)
|
||||
serverUrl = serverUrl.replace(scheme: 'https');
|
||||
print('Instance #${argsWithId.id} listening at $serverUrl');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _RunnerArgsWithId {
|
||||
final int id;
|
||||
final _RunnerArgs args;
|
||||
|
||||
_RunnerArgsWithId(this.id, this.args);
|
||||
}
|
||||
|
||||
class _RunnerArgs {
|
||||
final String name;
|
||||
|
||||
final AngelConfigurer configureServer;
|
||||
|
||||
final RunnerOptions options;
|
||||
|
||||
final Reflector reflector;
|
||||
|
||||
final SendPort loggingSendPort, pubSubSendPort;
|
||||
|
||||
_RunnerArgs(this.name, this.configureServer, this.options, this.reflector,
|
||||
this.loggingSendPort, this.pubSubSendPort);
|
||||
|
||||
String get loggerName => name;
|
||||
}
|
16
packages/production/pubspec.yaml
Normal file
16
packages/production/pubspec.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: angel_production
|
||||
version: 1.0.0
|
||||
description: Helpers for concurrency, message-passing, rotating loggers, and other production functionality in Angel.
|
||||
author: Tobe O <thosakwe@gmail.com>
|
||||
homepage: https://github.com/angel-dart/production
|
||||
environment:
|
||||
sdk: ">=2.0.0-dev <3.0.0"
|
||||
dependencies:
|
||||
angel_container: ^1.0.0-alpha
|
||||
angel_framework: ^2.0.0-alpha
|
||||
args: ^1.0.0
|
||||
io: ^0.3.2
|
||||
logging: ^0.11.3
|
||||
pub_sub: ^2.0.0
|
||||
dev_dependencies:
|
||||
pedantic: ^1.0.0
|
BIN
packages/production/screenshot.png
Normal file
BIN
packages/production/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
Loading…
Reference in a new issue