blob: fdfa67a1cbe24945df23705a7af3c0e09215575f [file] [log] [blame]
// Copyright (c) 2016, 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.
import 'dart:async';
import 'package:path/path.dart' as p;
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:test/test.dart' hide fail;
import 'package:pub/src/utils.dart';
import 'descriptor.dart' as d;
/// The global [DescriptorServer] that's used by default.
///
/// `null` if there's no global server in use. This can be set to replace the
/// existing global server.
DescriptorServer get globalServer => _globalServer;
set globalServer(DescriptorServer value) {
if (_globalServer == null) {
addTearDown(() {
_globalServer = null;
});
} else {
expect(_globalServer.close(), completes);
}
_globalServer = value;
}
DescriptorServer _globalServer;
/// Creates a global [DescriptorServer] to serve [contents] as static files.
///
/// This server will exist only for the duration of the pub run. It's accessible
/// via [server]. Subsequent calls to [serve] replace the previous server.
Future serve([List<d.Descriptor> contents]) async {
globalServer = (await DescriptorServer.start())..contents.addAll(contents);
}
/// Like [serve], but reports an error if a request ever comes in to the server.
Future serveErrors() async {
globalServer = await DescriptorServer.errors();
}
class DescriptorServer {
/// The underlying server.
final shelf.Server _server;
/// A future that will complete to the port used for the server.
int get port => _server.url.port;
/// The list of paths that have been requested from this server.
final requestedPaths = <String>[];
/// The base directory descriptor of the directories served by [this].
final d.DirectoryDescriptor _baseDir;
/// The descriptors served by this server.
///
/// This can safely be modified between requests.
List<d.Descriptor> get contents => _baseDir.contents;
/// Handlers for requests not easily described as files.
final Map<Pattern, shelf.Handler> extraHandlers = {};
/// Creates an HTTP server to serve [contents] as static files.
///
/// This server exists only for the duration of the pub run. Subsequent calls
/// to [serve] replace the previous server.
static Future<DescriptorServer> start() async =>
DescriptorServer._(await shelf_io.IOServer.bind('localhost', 0));
/// Creates a server that reports an error if a request is ever received.
static Future<DescriptorServer> errors() async =>
DescriptorServer._(await shelf_io.IOServer.bind('localhost', 0))
..extraHandlers[RegExp('.*')] = (request) {
fail('The HTTP server received an unexpected request:\n'
'${request.method} ${request.requestedUri}');
};
DescriptorServer._(this._server) : _baseDir = d.dir('serve-dir', []) {
_server.mount((request) async {
final pathWithInitialSlash = '/${request.url.path}';
final key = extraHandlers.keys.firstWhere((pattern) {
final match = pattern.matchAsPrefix(pathWithInitialSlash);
return match != null && match.end == pathWithInitialSlash.length;
}, orElse: () => null);
if (key != null) return extraHandlers[key](request);
var path = p.posix.fromUri(request.url.path);
requestedPaths.add(path);
try {
var stream = await _validateStream(_baseDir.load(path));
return shelf.Response.ok(stream);
} catch (_) {
return shelf.Response.notFound('File "$path" not found.');
}
});
addTearDown(_server.close);
}
/// Closes this server.
Future close() => _server.close();
}
/// Ensures that [stream] can emit at least one value successfully (or close
/// without any values).
///
/// For example, reading asynchronously from a non-existent file will return a
/// stream that fails on the first chunk. In order to handle that more
/// gracefully, you may want to check that the stream looks like it's working
/// before you pipe the stream to something else.
///
/// This lets you do that. It returns a [Future] that completes to a [Stream]
/// emitting the same values and errors as [stream], but only if at least one
/// value can be read successfully. If an error occurs before any values are
/// emitted, the returned Future completes to that error.
Future<Stream<T>> _validateStream<T>(Stream<T> stream) {
var completer = Completer<Stream<T>>();
var controller = StreamController<T>(sync: true);
StreamSubscription subscription;
subscription = stream.listen((value) {
// We got a value, so the stream is valid.
if (!completer.isCompleted) completer.complete(controller.stream);
controller.add(value);
}, onError: (error, [StackTrace stackTrace]) {
// If the error came after values, it's OK.
if (completer.isCompleted) {
controller.addError(error, stackTrace);
return;
}
// Otherwise, the error came first and the stream is invalid.
completer.completeError(error, stackTrace);
// We won't be returning the stream at all in this case, so unsubscribe
// and swallow the error.
subscription.cancel();
}, onDone: () {
// It closed with no errors, so the stream is valid.
if (!completer.isCompleted) completer.complete(controller.stream);
controller.close();
});
return completer.future;
}