blob: 2e2f76077714e7a2e9227bee4640c7122d64b9f8 [file]
// Copyright (c) 2019, 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:io';
import 'package:dwds/src/services/chrome/chrome_debug_exception.dart';
import 'package:http_multi_server/http_multi_server.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
as wip;
/// Returns a port that is probably, but not definitely, not in use.
///
/// This has a built-in race condition: another process may bind this port at
/// any time after this call has returned.
Future<int> findUnusedPort() async {
int port;
ServerSocket socket;
try {
socket = await ServerSocket.bind(
InternetAddress.loopbackIPv6,
0,
v6Only: true,
);
} on SocketException {
socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
}
port = socket.port;
await socket.close();
return port;
}
/// Binds a new http server to [hostname].
///
/// Tries [port] first if specified, then falls back to port 0 so the OS picks a
/// port that is free for this exact bind. A port from [findUnusedPort] is only
/// known to be free on `[::1]`. It can still be held over IPv4, which makes the
/// dual-stack `[::]` bind used for `'any'` fail with "Address already in use".
///
/// Retries a few times to recover from transient bind errors.
Future<HttpServer> startHttpServer(String hostname, {int? port}) async {
const retries = 5;
var requestedPort = port ?? 0;
for (var attempt = 1; ; attempt++) {
try {
return await HttpMultiServer.bind(hostname, requestedPort);
} on SocketException {
if (attempt == retries) rethrow;
}
requestedPort = 0;
await Future<void>.delayed(const Duration(milliseconds: 100));
}
}
/// Handles [requests] using [handler].
///
/// Captures all sync and async stack error traces and passes
/// them to the [onError] handler.
void serveHttpRequests(
Stream<HttpRequest> requests,
Handler handler,
void Function(Object, StackTrace) onError,
) {
return Chain.capture(() {
serveRequests(requests, handler);
}, onError: onError);
}
/// Throws an [wip.ExceptionDetails] object if `exceptionDetails` is present on
/// the result.
void handleErrorIfPresent(wip.WipResponse? response, {String? evalContents}) {
final result = response?.result;
if (result == null) return;
if (result.containsKey('exceptionDetails')) {
throw ChromeDebugException(
result['exceptionDetails'] as Map<String, dynamic>,
evalContents: evalContents,
);
}
}
/// Returns result contained in the response.
/// Throws an [wip.ExceptionDetails] object if `exceptionDetails` is present on
/// the result or the result is null.
Map<String, dynamic> getResultOrHandleError(
wip.WipResponse? response, {
String? evalContents,
}) {
handleErrorIfPresent(response, evalContents: evalContents);
final result = response?.result?['result'];
if (result == null) {
throw ChromeDebugException({
'text': 'null result from Chrome Devtools',
}, evalContents: evalContents);
}
return result as Map<String, dynamic>;
}