// 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 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; } /// Finds unused port and binds a new http server to it. /// /// Retries a few times to recover from errors due to /// another thread or process opening the same port. /// Starts by trying to bind to [port] if specified. Future startHttpServer(String hostname, {int? port}) async { HttpServer? httpServer; final retries = 5; var i = 0; var foundPort = port ?? await findUnusedPort(); while (i < retries) { i++; try { httpServer = await HttpMultiServer.bind(hostname, foundPort); } on SocketException { if (i == retries) rethrow; } if (httpServer != null || i == retries) return httpServer!; foundPort = await findUnusedPort(); await Future.delayed(const Duration(milliseconds: 100)); } return httpServer!; } /// Handles [requests] using [handler]. /// /// Captures all sync and async stack error traces and passes /// them to the [onError] handler. void serveHttpRequests( Stream 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, 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 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; }