2018-09-04 20:04:53 +00:00
|
|
|
import 'dart:async';
|
|
|
|
import 'dart:io';
|
|
|
|
import 'dart:isolate';
|
|
|
|
import 'package:angel_container/angel_container.dart';
|
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
2018-11-06 20:42:59 +00:00
|
|
|
import 'package:angel_framework/http.dart';
|
2019-04-28 17:51:08 +00:00
|
|
|
import 'package:angel_framework/http2.dart';
|
2018-09-04 20:04:53 +00:00
|
|
|
import 'package:args/args.dart';
|
|
|
|
import 'package:io/ansi.dart';
|
|
|
|
import 'package:io/io.dart';
|
2018-09-04 22:00:53 +00:00
|
|
|
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';
|
2018-09-04 20:04:53 +00:00
|
|
|
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,
|
2019-04-28 17:09:44 +00:00
|
|
|
{this.reflector = const EmptyReflector()});
|
2018-09-04 20:04:53 +00:00
|
|
|
|
|
|
|
static const String asciiArt = '''
|
|
|
|
____________ ________________________
|
|
|
|
___ |__ | / /_ ____/__ ____/__ /
|
|
|
|
__ /| |_ |/ /_ / __ __ __/ __ /
|
|
|
|
_ ___ | /| / / /_/ / _ /___ _ /___
|
|
|
|
/_/ |_/_/ |_/ \____/ /_____/ /_____/
|
|
|
|
|
|
|
|
''';
|
|
|
|
|
2019-04-28 17:55:37 +00:00
|
|
|
static void handleLogRecord(LogRecord record, RunnerOptions options) {
|
|
|
|
if (options.quiet) return;
|
2018-09-04 20:04:53 +00:00
|
|
|
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.
|
2018-09-04 22:00:53 +00:00
|
|
|
Future spawnIsolate(int id, RunnerOptions options, SendPort pubSubSendPort) {
|
2019-04-28 17:51:08 +00:00
|
|
|
return _spawnIsolate(id, Completer(), options, pubSubSendPort);
|
2018-09-04 20:04:53 +00:00
|
|
|
}
|
|
|
|
|
2018-09-04 22:00:53 +00:00
|
|
|
Future _spawnIsolate(
|
|
|
|
int id, Completer c, RunnerOptions options, SendPort pubSubSendPort) {
|
2019-04-28 17:51:08 +00:00
|
|
|
var onLogRecord = ReceivePort();
|
|
|
|
var onExit = ReceivePort();
|
|
|
|
var onError = ReceivePort();
|
|
|
|
var runnerArgs = _RunnerArgs(name, configureServer, options, reflector,
|
2018-09-04 22:00:53 +00:00
|
|
|
onLogRecord.sendPort, pubSubSendPort);
|
2019-04-28 17:51:08 +00:00
|
|
|
var argsWithId = _RunnerArgsWithId(id, runnerArgs);
|
2018-09-04 20:04:53 +00:00
|
|
|
|
2018-09-04 22:00:53 +00:00
|
|
|
Isolate.spawn(isolateMain, argsWithId,
|
2018-09-04 20:04:53 +00:00
|
|
|
onExit: onExit.sendPort,
|
|
|
|
onError: onError.sendPort,
|
|
|
|
errorsAreFatal: true && false)
|
|
|
|
.then((isolate) {})
|
|
|
|
.catchError(c.completeError);
|
|
|
|
|
2019-04-28 17:55:37 +00:00
|
|
|
onLogRecord.listen((msg) => handleLogRecord(msg as LogRecord, options));
|
2018-09-04 20:04:53 +00:00
|
|
|
|
|
|
|
onError.listen((msg) {
|
|
|
|
if (msg is List) {
|
|
|
|
var e = msg[0], st = StackTrace.fromString(msg[1].toString());
|
2019-04-28 17:55:37 +00:00
|
|
|
handleLogRecord(
|
|
|
|
LogRecord(
|
|
|
|
Level.SEVERE, 'Fatal error', runnerArgs.loggerName, e, st),
|
|
|
|
options);
|
2018-09-04 20:04:53 +00:00
|
|
|
} else {
|
2019-04-28 17:51:08 +00:00
|
|
|
handleLogRecord(
|
2019-04-28 17:55:37 +00:00
|
|
|
LogRecord(Level.SEVERE, 'Fatal error', runnerArgs.loggerName, msg),
|
|
|
|
options);
|
2018-09-04 20:04:53 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
onExit.listen((_) {
|
|
|
|
if (options.respawn) {
|
2019-04-28 17:55:37 +00:00
|
|
|
handleLogRecord(
|
|
|
|
LogRecord(
|
|
|
|
Level.WARNING,
|
|
|
|
'Instance #$id at ${DateTime.now()} crashed. Respawning immediately...',
|
|
|
|
runnerArgs.loggerName),
|
|
|
|
options);
|
2018-09-04 22:00:53 +00:00
|
|
|
_spawnIsolate(id, c, options, pubSubSendPort);
|
2018-09-04 20:04:53 +00:00
|
|
|
} 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 {
|
2018-09-04 22:00:53 +00:00
|
|
|
pub_sub.Server server;
|
|
|
|
|
2018-09-04 20:04:53 +00:00
|
|
|
try {
|
|
|
|
var argResults = RunnerOptions.argParser.parse(args);
|
2019-04-28 17:51:08 +00:00
|
|
|
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.');
|
|
|
|
}
|
|
|
|
}
|
2018-09-04 20:04:53 +00:00
|
|
|
|
|
|
|
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...');
|
|
|
|
|
2019-04-28 17:51:08 +00:00
|
|
|
var adapter = pub_sub.IsolateAdapter();
|
|
|
|
server = pub_sub.Server([adapter]);
|
2018-09-04 22:00:53 +00:00
|
|
|
|
|
|
|
// Register clients
|
|
|
|
for (int i = 0; i < Platform.numberOfProcessors; i++) {
|
2019-04-28 17:51:08 +00:00
|
|
|
server.registerClient(pub_sub.ClientInfo('client$i'));
|
2018-09-04 22:00:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
server.start();
|
|
|
|
|
2019-04-28 17:51:08 +00:00
|
|
|
await Future.wait(List.generate(options.concurrency,
|
2018-09-04 22:00:53 +00:00
|
|
|
(id) => spawnIsolate(id, options, adapter.receivePort.sendPort)));
|
2018-09-04 20:04:53 +00:00
|
|
|
} on ArgParserException catch (e) {
|
|
|
|
stderr
|
2019-04-28 17:51:08 +00:00
|
|
|
..writeln(red.wrap(e.message))
|
2018-09-04 20:04:53 +00:00
|
|
|
..writeln()
|
2019-04-28 17:51:08 +00:00
|
|
|
..writeln(red.wrap('Options:'))
|
|
|
|
..writeln(red.wrap(RunnerOptions.argParser.usage));
|
2018-09-04 20:04:53 +00:00
|
|
|
exitCode = ExitCode.usage.code;
|
2019-04-28 17:51:08 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
stderr
|
|
|
|
..writeln(red.wrap('fatal error: $e'))
|
|
|
|
..writeln(red.wrap(st.toString()));
|
2018-09-04 20:04:53 +00:00
|
|
|
exitCode = 1;
|
2018-09-04 22:00:53 +00:00
|
|
|
} finally {
|
2019-04-28 17:09:44 +00:00
|
|
|
await server?.close();
|
2018-09-04 20:04:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-04 22:00:53 +00:00
|
|
|
static void isolateMain(_RunnerArgsWithId argsWithId) {
|
|
|
|
var args = argsWithId.args;
|
2018-09-04 20:04:53 +00:00
|
|
|
hierarchicalLoggingEnabled = true;
|
|
|
|
|
2019-04-28 17:51:08 +00:00
|
|
|
var zone = Zone.current.fork(specification: ZoneSpecification(
|
2018-09-04 20:04:53 +00:00
|
|
|
print: (self, parent, zone, msg) {
|
2019-04-28 17:51:08 +00:00
|
|
|
args.loggingSendPort.send(LogRecord(Level.INFO, msg, args.loggerName));
|
2018-09-04 20:04:53 +00:00
|
|
|
},
|
|
|
|
));
|
|
|
|
|
|
|
|
zone.run(() async {
|
2019-04-28 17:51:08 +00:00
|
|
|
var client =
|
|
|
|
pub_sub.IsolateClient('client${argsWithId.id}', args.pubSubSendPort);
|
2018-09-04 22:00:53 +00:00
|
|
|
|
2019-04-28 17:51:08 +00:00
|
|
|
var app = Angel(reflector: args.reflector)
|
2018-09-04 22:00:53 +00:00
|
|
|
..container.registerSingleton<pub_sub.Client>(client)
|
2019-04-28 17:51:08 +00:00
|
|
|
..container.registerSingleton(InstanceInfo(id: argsWithId.id));
|
2018-09-04 22:00:53 +00:00
|
|
|
|
|
|
|
app.shutdownHooks.add((_) => client.close());
|
|
|
|
|
2018-09-04 20:04:53 +00:00
|
|
|
await app.configure(args.configureServer);
|
|
|
|
|
|
|
|
if (app.logger == null) {
|
2019-04-28 17:51:08 +00:00
|
|
|
app.logger = Logger(args.loggerName)
|
2019-04-28 17:55:37 +00:00
|
|
|
..onRecord.listen((rec) => Runner.handleLogRecord(rec, args.options));
|
2018-09-04 20:04:53 +00:00
|
|
|
}
|
|
|
|
|
2019-04-28 17:51:08 +00:00
|
|
|
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');
|
2018-09-04 20:04:53 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-04 22:00:53 +00:00
|
|
|
class _RunnerArgsWithId {
|
|
|
|
final int id;
|
|
|
|
final _RunnerArgs args;
|
|
|
|
|
|
|
|
_RunnerArgsWithId(this.id, this.args);
|
|
|
|
}
|
|
|
|
|
2018-09-04 20:04:53 +00:00
|
|
|
class _RunnerArgs {
|
|
|
|
final String name;
|
|
|
|
|
|
|
|
final AngelConfigurer configureServer;
|
|
|
|
|
|
|
|
final RunnerOptions options;
|
|
|
|
|
|
|
|
final Reflector reflector;
|
|
|
|
|
2018-09-04 22:00:53 +00:00
|
|
|
final SendPort loggingSendPort, pubSubSendPort;
|
2018-09-04 20:04:53 +00:00
|
|
|
|
|
|
|
_RunnerArgs(this.name, this.configureServer, this.options, this.reflector,
|
2018-09-04 22:00:53 +00:00
|
|
|
this.loggingSendPort, this.pubSubSendPort);
|
2018-09-04 20:04:53 +00:00
|
|
|
|
|
|
|
String get loggerName => name;
|
|
|
|
}
|