| // 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:async'; |
| import 'dart:io' as io; |
| |
| import 'package:appengine/appengine.dart' as ae; |
| import 'package:logging/logging.dart'; |
| import 'package:rpc/rpc.dart' as rpc; |
| import 'package:shelf/shelf_io.dart' as shelf_io; |
| |
| import 'src/common.dart'; |
| import 'src/common_server.dart'; |
| import 'src/common_server_impl.dart'; |
| import 'src/common_server_proto.dart'; |
| import 'src/flutter_web.dart'; |
| import 'src/sdk_manager.dart'; |
| import 'src/server_cache.dart'; |
| |
| const String _API = '/api'; |
| const String _API_V1_PREFIX = '/api/dartservices/v1'; |
| const String _healthCheck = '/_ah/health'; |
| const String _readynessCheck = '/_ah/ready'; |
| |
| final Logger _logger = Logger('gae_server'); |
| |
| void main(List<String> args) { |
| var gaePort = 8080; |
| if (args.isNotEmpty) gaePort = int.parse(args[0]); |
| |
| final sdk = sdkPath; |
| |
| if (sdk == 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 |
| sdkPath: $sdkPath |
| REDIS_SERVER_URI: ${io.Platform.environment['REDIS_SERVER_URI']} |
| GAE_VERSION: ${io.Platform.environment['GAE_VERSION']} |
| '''); |
| |
| final server = GaeServer(sdk, io.Platform.environment['REDIS_SERVER_URI']); |
| server.start(gaePort); |
| } |
| |
| class GaeServer { |
| final String sdkPath; |
| final String redisServerUri; |
| |
| bool discoveryEnabled; |
| rpc.ApiServer apiServer; |
| CommonServer commonServer; |
| CommonServerImpl commonServerImpl; |
| CommonServerProto commonServerProto; |
| |
| GaeServer(this.sdkPath, this.redisServerUri) { |
| hierarchicalLoggingEnabled = true; |
| recordStackTraceAtLevel = Level.SEVERE; |
| |
| _logger.level = Level.ALL; |
| |
| discoveryEnabled = false; |
| final flutterWebManager = FlutterWebManager(SdkManager.flutterSdk); |
| commonServerImpl = CommonServerImpl( |
| sdkPath, |
| flutterWebManager, |
| GaeServerContainer(), |
| redisServerUri == null |
| ? InMemoryCache() |
| : RedisCache( |
| redisServerUri, |
| io.Platform.environment['GAE_VERSION'], |
| ), |
| ); |
| commonServer = CommonServer(commonServerImpl); |
| commonServerProto = CommonServerProto(commonServerImpl); |
| // Enabled pretty printing of returned json for debuggability. |
| apiServer = rpc.ApiServer(apiPrefix: _API, prettyPrint: true) |
| ..addApi(commonServer); |
| } |
| |
| 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-Methods', 'POST, OPTIONS'); |
| request.response.headers.add('Access-Control-Allow-Headers', |
| 'Origin, X-Requested-With, Content-Type, Accept'); |
| |
| if (request.method == 'OPTIONS') { |
| await _processOptionsRequest(request); |
| } else if (request.uri.path == _readynessCheck) { |
| await _processReadynessRequest(request); |
| } else if (request.uri.path == _healthCheck) { |
| await _processHealthRequest(request); |
| } else if (request.uri.path.startsWith(_API_V1_PREFIX)) { |
| await _processApiRequest(request); |
| } else if (request.uri.path.startsWith(PROTO_API_URL_PREFIX)) { |
| await shelf_io.handleRequest(request, commonServerProto.router.handler); |
| } else { |
| await _processDefaultRequest(request); |
| } |
| } |
| |
| Future<void> _processOptionsRequest(io.HttpRequest request) async { |
| final requestedMethod = |
| request.headers.value('access-control-request-method'); |
| int statusCode; |
| if (requestedMethod != null && requestedMethod.toUpperCase() == 'POST') { |
| statusCode = io.HttpStatus.ok; |
| } else { |
| statusCode = io.HttpStatus.badRequest; |
| } |
| request.response.statusCode = statusCode; |
| await request.response.close(); |
| } |
| |
| Future _processReadynessRequest(io.HttpRequest request) async { |
| if (commonServerImpl.running) { |
| request.response.statusCode = io.HttpStatus.ok; |
| } else { |
| request.response.statusCode = io.HttpStatus.internalServerError; |
| _logger.info('CommonServer not running - failing readiness check.'); |
| } |
| |
| await request.response.close(); |
| } |
| |
| Future _processHealthRequest(io.HttpRequest request) async { |
| if (commonServerImpl.running && !commonServerImpl.analysisServersRunning) { |
| _logger.severe('CommonServer running without analysis servers. ' |
| 'Intentionally failing healthcheck.'); |
| request.response.statusCode = io.HttpStatus.internalServerError; |
| } 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) { |
| request.response.statusCode = io.HttpStatus.ok; |
| } else { |
| request.response.statusCode = io.HttpStatus.internalServerError; |
| } |
| } finally { |
| await tempDir.delete(recursive: true); |
| } |
| } catch (e) { |
| _logger.severe('Failed to create temporary file: $e'); |
| request.response.statusCode = io.HttpStatus.internalServerError; |
| } |
| } |
| |
| await request.response.close(); |
| } |
| |
| Future _processApiRequest(io.HttpRequest request) async { |
| if (!discoveryEnabled) { |
| apiServer.enableDiscoveryApi(); |
| discoveryEnabled = true; |
| } |
| // NOTE: We could read in the request body here and parse it similar to |
| // the _parseRequest method to determine content-type and dispatch to e.g. |
| // a plain text handler if we want to support that. |
| final apiRequest = rpc.HttpApiRequest.fromHttpRequest(request); |
| |
| // Dartpad sends data as plain text, we need to promote this to |
| // application/json to ensure that the rpc library processes it correctly |
| try { |
| apiRequest.headers['content-type'] = 'application/json; charset=utf-8'; |
| final apiResponse = await apiServer.handleHttpApiRequest(apiRequest); |
| await rpc.sendApiResponse(apiResponse, request.response); |
| } catch (e) { |
| // This should only happen in the case where there is a bug in the rpc |
| // package. Otherwise it always returns an HttpApiResponse. |
| _logger.warning('Failed with error: $e when trying to call ' |
| 'method at \'${request.uri.path}\'.'); |
| request.response.statusCode = io.HttpStatus.internalServerError; |
| 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; |
| } |