| // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| library services_gae; |
| |
| import 'dart:io' as io; |
| import 'dart:math'; |
| |
| import 'package:appengine/appengine.dart' as ae; |
| import 'package:args/args.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:shelf/shelf_io.dart' as shelf_io; |
| |
| import 'src/common_server_api.dart'; |
| import 'src/common_server_impl.dart'; |
| import 'src/sdk_manager.dart'; |
| import 'src/server_cache.dart'; |
| |
| const String _API_PREFIX = '/api/dartservices/'; |
| const String _livenessCheck = '/liveness_check'; |
| const String _readinessCheck = '/readiness_check'; |
| // Serve content for 1.5 hours, +- 30 minutes. |
| final DateTime _serveUntil = DateTime.now() |
| .add(Duration(hours: 1)) |
| .add(Duration(minutes: Random().nextInt(60))); |
| |
| final Logger _logger = Logger('gae_server'); |
| |
| void main(List<String> args) { |
| final parser = ArgParser() |
| ..addFlag('dark-launch', help: 'Dark launch proxied compilation requests') |
| ..addOption('proxy-target', |
| help: 'URL base to proxy compilation requests to') |
| ..addOption('port', |
| abbr: 'p', defaultsTo: '8080', help: 'Port to attach to'); |
| final results = parser.parse(args); |
| |
| final gaePort = int.tryParse(results['port'] as String ?? '') ?? 8080; |
| |
| if (SdkManager.sdk.sdkPath == null) { |
| throw 'No Dart SDK is available; set the DART_SDK env var.'; |
| } |
| |
| // Log to stdout/stderr. AppEngine's logging package is disabled in 0.6.0 |
| // and AppEngine copies stdout/stderr to the dashboards. |
| _logger.onRecord.listen((LogRecord rec) { |
| final out = ('${rec.level.name}: ${rec.time}: ${rec.message}\n'); |
| if (rec.level > Level.INFO) { |
| io.stderr.write(out); |
| } else { |
| io.stdout.write(out); |
| } |
| }); |
| log.info('''Initializing dart-services: |
| --port: $gaePort |
| --dark-launch: ${results['dark-launch']} |
| --proxy-target: ${results['proxy-target']} |
| sdkPath: ${SdkManager.sdk?.sdkPath} |
| \$REDIS_SERVER_URI: ${io.Platform.environment['REDIS_SERVER_URI']} |
| \$GAE_VERSION: ${io.Platform.environment['GAE_VERSION']} |
| '''); |
| |
| final server = GaeServer( |
| io.Platform.environment['REDIS_SERVER_URI'], |
| results['dark-launch'].toString().toLowerCase() == 'true', |
| results['proxy-target'].toString()); |
| server.start(gaePort); |
| } |
| |
| class GaeServer { |
| final String redisServerUri; |
| |
| CommonServerImpl commonServerImpl; |
| CommonServerApi commonServerApi; |
| |
| GaeServer(this.redisServerUri, bool darkLaunch, String proxyTarget) { |
| hierarchicalLoggingEnabled = true; |
| recordStackTraceAtLevel = Level.SEVERE; |
| |
| _logger.level = Level.ALL; |
| |
| commonServerImpl = CommonServerImpl( |
| GaeServerContainer(), |
| redisServerUri == null |
| ? InMemoryCache() |
| : RedisCache( |
| redisServerUri, |
| io.Platform.environment['GAE_VERSION'], |
| ), |
| ); |
| if (proxyTarget != null && proxyTarget.isNotEmpty) { |
| commonServerImpl = |
| CommonServerImplProxy(commonServerImpl, darkLaunch, proxyTarget); |
| } |
| commonServerApi = CommonServerApi(commonServerImpl); |
| } |
| |
| Future<dynamic> start([int gaePort = 8080]) async { |
| await commonServerImpl.init(); |
| return ae.runAppEngine(requestHandler, port: gaePort); |
| } |
| |
| Future<void> requestHandler(io.HttpRequest request) async { |
| request.response.headers |
| ..add('Access-Control-Allow-Origin', '*') |
| ..add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') |
| ..add('Access-Control-Allow-Headers', |
| 'Origin, X-Requested-With, Content-Type, Accept'); |
| |
| if (request.method == 'OPTIONS') { |
| await _processOptionsRequest(request); |
| } else if (request.uri.path == _readinessCheck) { |
| await _processReadinessRequest(request); |
| } else if (request.uri.path == _livenessCheck) { |
| await _processLivenessRequest(request); |
| } else if (request.uri.path.startsWith(_API_PREFIX)) { |
| await shelf_io.handleRequest(request, commonServerApi.router); |
| } else { |
| await _processDefaultRequest(request); |
| } |
| } |
| |
| Future<void> _processOptionsRequest(io.HttpRequest request) async { |
| final statusCode = io.HttpStatus.ok; |
| request.response.statusCode = statusCode; |
| await request.response.close(); |
| } |
| |
| Future _processReadinessRequest(io.HttpRequest request) async { |
| _logger.info('Processing readiness check'); |
| if (!commonServerImpl.isRestarting && |
| DateTime.now().isBefore(_serveUntil)) { |
| request.response.statusCode = io.HttpStatus.ok; |
| } else { |
| request.response.statusCode = io.HttpStatus.serviceUnavailable; |
| _logger.severe('CommonServer not running - failing readiness check.'); |
| } |
| |
| await request.response.close(); |
| } |
| |
| Future _processLivenessRequest(io.HttpRequest request) async { |
| _logger.info('Processing liveness check'); |
| if (!commonServerImpl.isHealthy || DateTime.now().isAfter(_serveUntil)) { |
| _logger.severe('CommonServer is no longer healthy.' |
| ' Intentionally failing health check.'); |
| request.response.statusCode = io.HttpStatus.serviceUnavailable; |
| } else { |
| try { |
| final tempDir = await io.Directory.systemTemp.createTemp('healthz'); |
| try { |
| final file = await io.File('${tempDir.path}/livecheck.txt'); |
| await file.writeAsString('testing123\n' * 1000, flush: true); |
| final stat = await file.stat(); |
| if (stat.size > 10000) { |
| _logger.info('CommonServer healthy and file system working.' |
| ' Passing health check.'); |
| request.response.statusCode = io.HttpStatus.ok; |
| } else { |
| _logger.severe('CommonServer healthy, but filesystem is not.' |
| ' Intentionally failing health check.'); |
| request.response.statusCode = io.HttpStatus.serviceUnavailable; |
| } |
| } finally { |
| await tempDir.delete(recursive: true); |
| } |
| } catch (e) { |
| _logger.severe('CommonServer healthy, but failed to create temporary' |
| ' file: $e'); |
| request.response.statusCode = io.HttpStatus.serviceUnavailable; |
| } |
| } |
| |
| await request.response.close(); |
| } |
| |
| Future _processDefaultRequest(io.HttpRequest request) async { |
| request.response.statusCode = io.HttpStatus.notFound; |
| await request.response.close(); |
| } |
| } |
| |
| class GaeServerContainer implements ServerContainer { |
| @override |
| String get version => io.Platform.version; |
| } |