blob: f3f9bde8a8fd7d19ff9ea4d19ff8b6aed5e4a3e7 [file] [log] [blame]
// 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 'dart:math';
import 'package:appengine/appengine.dart' as ae;
import 'package:logging/logging.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'src/common.dart';
import 'src/common_server_api.dart';
import 'src/common_server_impl.dart';
import 'src/flutter_web.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 4 hours, +- 1 hour.
final DateTime _serveUntil = DateTime.now()
.add(Duration(hours: 3))
.add(Duration(minutes: Random().nextInt(120)));
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;
CommonServerImpl commonServerImpl;
CommonServerApi commonServerApi;
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'],
),
);
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.handler);
} 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;
}