From 7b6ccda2375283547215950edded519a0779b696 Mon Sep 17 00:00:00 2001 From: Tobe O Date: Tue, 4 Sep 2018 16:04:53 -0400 Subject: [PATCH] Create Runner --- analysis_options.yaml | 3 + example/main.dart | 19 ++++ lib/angel_production.dart | 2 + lib/src/options.dart | 43 ++++++++ lib/src/runner.dart | 200 ++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 6 ++ 6 files changed, 273 insertions(+) create mode 100644 analysis_options.yaml create mode 100644 example/main.dart create mode 100644 lib/angel_production.dart create mode 100644 lib/src/options.dart create mode 100644 lib/src/runner.dart create mode 100644 pubspec.yaml diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..eae1e42a --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,3 @@ +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/example/main.dart b/example/main.dart new file mode 100644 index 00000000..f7a89c3a --- /dev/null +++ b/example/main.dart @@ -0,0 +1,19 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'package:angel_framework/angel_framework.dart'; +import 'package:angel_production/angel_production.dart'; + +main(List args) { + var runner = new Runner('example', configureServer); + return runner.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. + new Timer(const Duration(seconds: 3), Isolate.current.kill); + return 'Crashing in 3s...'; + }); +} diff --git a/lib/angel_production.dart b/lib/angel_production.dart new file mode 100644 index 00000000..6488127f --- /dev/null +++ b/lib/angel_production.dart @@ -0,0 +1,2 @@ +export 'src/options.dart'; +export 'src/runner.dart'; \ No newline at end of file diff --git a/lib/src/options.dart b/lib/src/options.dart new file mode 100644 index 00000000..9fefe060 --- /dev/null +++ b/lib/src/options.dart @@ -0,0 +1,43 @@ +import 'dart:io'; +import 'package:args/args.dart'; + +class RunnerOptions { + static final ArgParser argParser = new 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.') + ..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.'); + + final String hostname; + final int concurrency, port; + final bool useZone, respawn; + + RunnerOptions( + {this.hostname: '127.0.0.1', + this.port: 3000, + this.concurrency: 1, + this.useZone: false, + this.respawn: true}); + + factory RunnerOptions.fromArgResults(ArgResults argResults) { + return new 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, + ); + } +} diff --git a/lib/src/runner.dart b/lib/src/runner.dart new file mode 100644 index 00000000..3212d4ca --- /dev/null +++ b/lib/src/runner.dart @@ -0,0 +1,200 @@ +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:args/args.dart'; +import 'package:logging/logging.dart'; +import 'package:io/ansi.dart'; +import 'package:io/io.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) { + 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(RunnerOptions options) { + return _spawnIsolate(new Completer(), options); + } + + Future _spawnIsolate(Completer c, RunnerOptions options) { + var onLogRecord = new ReceivePort(); + var onExit = new ReceivePort(); + var onError = new ReceivePort(); + var runnerArgs = new _RunnerArgs( + name, configureServer, options, reflector, onLogRecord.sendPort); + + Isolate.spawn(isolateMain, runnerArgs, + onExit: onExit.sendPort, + onError: onError.sendPort, + errorsAreFatal: true && false) + .then((isolate) {}) + .catchError(c.completeError); + + onLogRecord.listen((msg) => handleLogRecord(msg as LogRecord)); + + onError.listen((msg) { + if (msg is List) { + var e = msg[0], st = StackTrace.fromString(msg[1].toString()); + handleLogRecord(new LogRecord( + Level.SEVERE, 'Fatal error', runnerArgs.loggerName, e, st)); + } else { + handleLogRecord(new LogRecord( + Level.SEVERE, 'Fatal error', runnerArgs.loggerName, msg)); + } + }); + + onExit.listen((_) { + if (options.respawn) { + handleLogRecord(new LogRecord( + Level.WARNING, + 'Detected a crashed instance at ${new DateTime.now()}. Respawning immediately...', + runnerArgs.loggerName)); + _spawnIsolate(c, options); + } 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 args) async { + try { + var argResults = RunnerOptions.argParser.parse(args); + var options = new RunnerOptions.fromArgResults(argResults); + + 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...'); + print('Arguments: $args...\n'); + + await Future.wait( + new List.generate(options.concurrency, (_) => spawnIsolate(options))); + } on ArgParserException catch (e) { + stderr + ..writeln(e.message) + ..writeln() + ..writeln('Options:') + ..writeln(RunnerOptions.argParser.usage); + exitCode = ExitCode.usage.code; + } catch (e) { + stderr..writeln('fatal error: $e'); + exitCode = 1; + } + } + + static void isolateMain(_RunnerArgs args) { + hierarchicalLoggingEnabled = true; + + var zone = Zone.current.fork(specification: new ZoneSpecification( + print: (self, parent, zone, msg) { + args.loggingSendPort + .send(new LogRecord(Level.INFO, msg, args.loggerName)); + }, + )); + + zone.run(() async { + var app = new Angel(reflector: args.reflector); + await app.configure(args.configureServer); + + if (app.logger == null) { + app.logger = new Logger(args.loggerName) + ..onRecord.listen(Runner.handleLogRecord); + } + + var http = + new AngelHttp.custom(app, startShared, useZone: args.options.useZone); + var server = + await http.startServer(args.options.hostname, args.options.port); + var url = new Uri( + scheme: 'http', host: server.address.address, port: server.port); + print('Listening at $url'); + }); + } +} + +class _RunnerArgs { + final String name; + + final AngelConfigurer configureServer; + + final RunnerOptions options; + + final Reflector reflector; + + final SendPort loggingSendPort; + + _RunnerArgs(this.name, this.configureServer, this.options, this.reflector, + this.loggingSendPort); + + String get loggerName => name; +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..4d5c9494 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,6 @@ +name: angel_production +dependencies: + angel_framework: ^2.0.0-alpha + args: ^1.0.0 + io: ^0.3.2 + logging: ^0.11.3 \ No newline at end of file