[ Service ] Refactor VM service request handling
TEST=CQ
CoreLibraryReviewExempt: VM service only change
Change-Id: Ieda3a81bc10fdb7904357a742b5a3e6f67e145d2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/383760
Auto-Submit: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ben Konyi <bkonyi@google.com>
Reviewed-by: Derek Xu <derekx@google.com>
diff --git a/sdk/lib/_internal/vm/bin/vmservice_server.dart b/sdk/lib/_internal/vm/bin/vmservice_server.dart
index fadd4aa..8cdfafb 100644
--- a/sdk/lib/_internal/vm/bin/vmservice_server.dart
+++ b/sdk/lib/_internal/vm/bin/vmservice_server.dart
@@ -231,7 +231,7 @@
final bool _authCodesDisabled;
final bool _enableServicePortFallback;
final String? _serviceInfoFilename;
- HttpServer? _server;
+ HttpServer? _httpServer;
bool get running => _running;
bool _running = false;
@@ -251,7 +251,7 @@
if (_service.ddsUri != null) {
return _service.ddsUri;
}
- final server = _server;
+ final server = _httpServer;
if (server != null) {
final ip = server.address.address;
final port = server.port;
@@ -273,6 +273,139 @@
this._enableServicePortFallback)
: _authCodesDisabled = (authCodesDisabled || Platform.isFuchsia);
+ Future<void> startup() async {
+ if (running) {
+ // Already running.
+ return;
+ }
+
+ {
+ final startingCompleter = _startingCompleter;
+ if (startingCompleter != null) {
+ if (!startingCompleter.isCompleted) {
+ await startingCompleter.future;
+ }
+ return;
+ }
+ }
+
+ final startingCompleter = Completer<bool>();
+ _startingCompleter = startingCompleter;
+ // Startup HTTP server.
+ Future<bool> startServer() async {
+ try {
+ var address;
+ var addresses = await InternetAddress.lookup(_ip);
+ // Prefer IPv4 addresses.
+ for (int i = 0; i < addresses.length; i++) {
+ address = addresses[i];
+ if (address.type == InternetAddressType.IPv4) break;
+ }
+ _httpServer = await HttpServer.bind(address, _port);
+ } catch (e, st) {
+ if (_port != 0 && _enableServicePortFallback) {
+ serverPrint(
+ 'Failed to bind Dart VM service HTTP server to port $_port. '
+ 'Falling back to automatic port selection');
+ _port = 0;
+ return await startServer();
+ } else {
+ serverPrint('Could not start Dart VM service HTTP server:\n'
+ '$e\n$st');
+ _notifyServerState('');
+ onServerAddressChange(null);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ if (!(await startServer())) {
+ startingCompleter.complete(true);
+ return;
+ }
+ if (_service.isExiting) {
+ serverPrint('Dart VM service HTTP server exiting before listening as '
+ 'vm service has received exit request\n');
+ startingCompleter.complete(true);
+ await shutdown(true);
+ return;
+ }
+ final server = _httpServer!;
+ server.listen(_requestHandler, cancelOnError: true);
+
+ if (_waitForDdsToAdvertiseService) {
+ _ddsInstance = _DebuggingSession();
+ await _ddsInstance!.start(
+ serverAddress!,
+ _ddsIP,
+ _ddsPort.toString(),
+ _authCodesDisabled,
+ _serveDevtools,
+ );
+ } else {
+ await outputConnectionInformation();
+ }
+ // Server is up and running.
+ _running = true;
+ _notifyServerState(serverAddress.toString());
+ onServerAddressChange('$serverAddress');
+ startingCompleter.complete(true);
+ }
+
+ Future<void> shutdown(bool forced) async {
+ // If start is pending, wait for it to complete.
+ if (_startingCompleter != null) {
+ if (!_startingCompleter!.isCompleted) {
+ await _startingCompleter!.future;
+ }
+ }
+
+ final server = _httpServer;
+ if (server == null) {
+ // Not started.
+ return;
+ }
+
+ if (Platform.isFuchsia) {
+ _cleanupFuchsiaState(server.port);
+ }
+
+ final address = serverAddress!;
+
+ try {
+ // Shutdown HTTP server and subscription.
+ await server.close(force: forced);
+ if (!_service.isExiting) {
+ // Only print this message if the service has been toggled off, not
+ // when the VM is exiting.
+ serverPrint('Dart VM service no longer listening on $address');
+ }
+ } catch (e, st) {
+ serverPrint('Could not shutdown Dart VM service HTTP server:\n$e\n$st\n');
+ } finally {
+ _ddsInstance?.shutdown();
+ _ddsInstance = null;
+ _httpServer = null;
+ _startingCompleter = null;
+ _running = false;
+ _notifyServerState('');
+ onServerAddressChange(null);
+ }
+ }
+
+ Future<void> outputConnectionInformation() async {
+ serverPrint('The Dart VM service is listening on $serverAddress');
+ if (Platform.isFuchsia) {
+ _writeFuchsiaState(_httpServer!.port);
+ }
+ final serviceInfoFilenameLocal = _serviceInfoFilename;
+ if (serviceInfoFilenameLocal != null &&
+ serviceInfoFilenameLocal.isNotEmpty) {
+ await _dumpServiceInfoToFile(serviceInfoFilenameLocal);
+ }
+ }
+
bool _isAllowedOrigin(String origin) {
Uri uri;
try {
@@ -289,7 +422,7 @@
return true;
}
- final server = _server!;
+ final server = _httpServer!;
if ((uri.port == server.port) &&
((uri.host == server.address.address) ||
(uri.host == server.address.host))) {
@@ -358,6 +491,100 @@
: '/${requestPathSegments.sublist(1).join('/')}';
}
+ Future<void> _processDevFSRequest(HttpRequest request) async {
+ String? fsName;
+ String? fsPath;
+ Uri? fsUri;
+
+ try {
+ // Extract the fs name and fs path from the request headers.
+ fsName = request.headers['dev_fs_name']![0];
+
+ // Prefer Uri encoding first, then fallback to path encoding.
+ if (request.headers['dev_fs_uri_b64'] case [String base64Uri]) {
+ fsUri = Uri.parse(utf8.decode(base64.decode(base64Uri)));
+ } else if (request.headers['dev_fs_path_b64'] case [String base64Uri]) {
+ fsPath = utf8.decode(base64.decode(base64Uri));
+ } else if (request.headers['dev_fs_path'] case [String path]) {
+ fsPath = path;
+ }
+ } catch (_) {
+ /* ignore */
+ }
+
+ try {
+ final result = await _service.devfs.handlePutStream(
+ fsName,
+ fsPath,
+ fsUri,
+ request.cast<List<int>>().transform(gzip.decoder),
+ );
+
+ request.response.headers.contentType = HttpRequestClient.jsonContentType;
+ request.response.write(result);
+ } catch (e, st) {
+ request.response.statusCode = HttpStatus.internalServerError;
+ request.response.write(e);
+ } finally {
+ request.response.close();
+ }
+ }
+
+ void _handleWebSocketRequest(HttpRequest request) {
+ final subprotocols = request.headers['sec-websocket-protocol'];
+ if (acceptNewWebSocketConnections) {
+ WebSocketTransformer.upgrade(
+ request,
+ protocolSelector:
+ subprotocols == null ? null : (_) => 'implicit-redirect',
+ compression: CompressionOptions.compressionOff,
+ ).then((WebSocket webSocket) {
+ WebSocketClient(webSocket, _service);
+ });
+ } else {
+ // Attempt to redirect client to the DDS instance.
+ request.response.redirect(_service.ddsUri!);
+ }
+ }
+
+ Future<void> _redirectToDevTools(HttpRequest request) async {
+ final ddsUri = _service.ddsUri;
+ if (ddsUri == null) {
+ request.response.headers.contentType = ContentType.text;
+ request.response.write('This VM does not have a registered Dart '
+ 'Development Service (DDS) instance and is not currently serving '
+ 'Dart DevTools.');
+ request.response.close();
+ return;
+ }
+ // We build this path manually rather than manipulating ddsUri directly
+ // as the resulting path requires an unencoded '#'. The Uri class will
+ // always encode '#' as '%23' in paths to avoid conflicts with fragments,
+ // which will result in the redirect failing.
+ final path = StringBuffer();
+ // Add authentication code to the path.
+ if (ddsUri.pathSegments.length > 1) {
+ path.writeAll([
+ ddsUri.pathSegments
+ .sublist(0, ddsUri.pathSegments.length - 1)
+ .join('/'),
+ '/',
+ ]);
+ }
+ final queryComponent = Uri.encodeQueryComponent(
+ ddsUri.replace(scheme: 'ws', path: '${path}ws').toString(),
+ );
+ path.writeAll([
+ 'devtools/',
+ '?uri=$queryComponent',
+ ]);
+ final redirectUri = Uri.parse(
+ 'http://${ddsUri.host}:${ddsUri.port}/$path',
+ );
+ request.response.redirect(redirectUri);
+ return;
+ }
+
Future<void> _requestHandler(HttpRequest request) async {
if (!_originCheck(request)) {
// This is a cross origin attempt to connect
@@ -368,54 +595,7 @@
}
if (request.method == 'PUT') {
// PUT requests are forwarded to DevFS for processing.
-
- List<String> fsNameList;
- List<String>? fsPathList;
- List<String>? fsPathBase64List;
- List<String>? fsUriBase64List;
- Object? fsName;
- Object? fsPath;
- Uri? fsUri;
-
- try {
- // Extract the fs name and fs path from the request headers.
- fsNameList = request.headers['dev_fs_name']!;
- fsName = fsNameList[0];
-
- // Prefer Uri encoding first.
- fsUriBase64List = request.headers['dev_fs_uri_b64'];
- if ((fsUriBase64List != null) && (fsUriBase64List.length > 0)) {
- final decodedFsUri = utf8.decode(base64.decode(fsUriBase64List[0]));
- fsUri = Uri.parse(decodedFsUri);
- }
-
- // Fallback to path encoding.
- if (fsUri == null) {
- fsPathList = request.headers['dev_fs_path'];
- fsPathBase64List = request.headers['dev_fs_path_b64'];
- // If the 'dev_fs_path_b64' header field was sent, use that instead.
- if ((fsPathBase64List != null) && fsPathBase64List.isNotEmpty) {
- fsPath = utf8.decode(base64.decode(fsPathBase64List[0]));
- } else if (fsPathList != null && fsPathList.isNotEmpty) {
- fsPath = fsPathList[0];
- }
- }
- } catch (e) {/* ignore */}
-
- String result;
- try {
- result = await _service.devfs.handlePutStream(fsName, fsPath, fsUri,
- request.cast<List<int>>().transform(gzip.decoder));
- } catch (e) {
- request.response.statusCode = HttpStatus.internalServerError;
- request.response.write(e);
- request.response.close();
- return;
- }
-
- request.response.headers.contentType = HttpRequestClient.jsonContentType;
- request.response.write(result);
- request.response.close();
+ await _processDevFSRequest(request);
return;
}
if (request.method != 'GET') {
@@ -444,58 +624,13 @@
final String path = result;
if (path == WEBSOCKET_PATH) {
- final subprotocols = request.headers['sec-websocket-protocol'];
- if (acceptNewWebSocketConnections) {
- WebSocketTransformer.upgrade(request,
- protocolSelector:
- subprotocols == null ? null : (_) => 'implicit-redirect',
- compression: CompressionOptions.compressionOff)
- .then((WebSocket webSocket) {
- WebSocketClient(webSocket, _service);
- });
- } else {
- // Attempt to redirect client to the DDS instance.
- request.response.redirect(_service.ddsUri!);
- }
+ _handleWebSocketRequest(request);
return;
}
// Don't redirect HTTP VM service requests, just requests for Observatory
// assets.
- if (!_serveObservatory && path == '/index.html') {
- final ddsUri = _service.ddsUri;
- if (ddsUri == null) {
- request.response.headers.contentType = ContentType.text;
- request.response.write('This VM does not have a registered Dart '
- 'Development Service (DDS) instance and is not currently serving '
- 'Dart DevTools.');
- request.response.close();
- return;
- }
- // We build this path manually rather than manipulating ddsUri directly
- // as the resulting path requires an unencoded '#'. The Uri class will
- // always encode '#' as '%23' in paths to avoid conflicts with fragments,
- // which will result in the redirect failing.
- final path = StringBuffer();
- // Add authentication code to the path.
- if (ddsUri.pathSegments.length > 1) {
- path.writeAll([
- ddsUri.pathSegments
- .sublist(0, ddsUri.pathSegments.length - 1)
- .join('/'),
- '/',
- ]);
- }
- final queryComponent = Uri.encodeQueryComponent(
- ddsUri.replace(scheme: 'ws', path: '${path}ws').toString(),
- );
- path.writeAll([
- 'devtools/',
- '?uri=$queryComponent',
- ]);
- final redirectUri = Uri.parse(
- 'http://${ddsUri.host}:${ddsUri.port}/$path',
- );
- request.response.redirect(redirectUri);
+ if (!_serveObservatory && path == ROOT_REDIRECT_PATH) {
+ await _redirectToDevTools(request);
return;
}
if (assets == null) {
@@ -515,7 +650,9 @@
// HTTP based service request.
final client = HttpRequestClient(request, _service);
final message = Message.fromUri(
- client, Uri(path: path, queryParameters: request.uri.queryParameters));
+ client,
+ Uri(path: path, queryParameters: request.uri.queryParameters),
+ );
client.onRequest(message); // exception free, no need to try catch
}
@@ -535,100 +672,12 @@
return file.writeAsString(json.encode(serviceInfo));
}
- Future<void> startup() async {
- if (running) {
- // Already running.
- return;
- }
-
- {
- final startingCompleter = _startingCompleter;
- if (startingCompleter != null) {
- if (!startingCompleter.isCompleted) {
- await startingCompleter.future;
- }
- return;
- }
- }
-
- final startingCompleter = Completer<bool>();
- _startingCompleter = startingCompleter;
- // Startup HTTP server.
- Future<bool> startServer() async {
- try {
- var address;
- var addresses = await InternetAddress.lookup(_ip);
- // Prefer IPv4 addresses.
- for (int i = 0; i < addresses.length; i++) {
- address = addresses[i];
- if (address.type == InternetAddressType.IPv4) break;
- }
- _server = await HttpServer.bind(address, _port);
- } catch (e, st) {
- if (_port != 0 && _enableServicePortFallback) {
- serverPrint(
- 'Failed to bind Dart VM service HTTP server to port $_port. '
- 'Falling back to automatic port selection');
- _port = 0;
- return await startServer();
- } else {
- serverPrint('Could not start Dart VM service HTTP server:\n'
- '$e\n$st');
- _notifyServerState('');
- onServerAddressChange(null);
- return false;
- }
- }
- return true;
- }
-
- if (!(await startServer())) {
- startingCompleter.complete(true);
- return;
- }
- if (_service.isExiting) {
- serverPrint('Dart VM service HTTP server exiting before listening as '
- 'vm service has received exit request\n');
- startingCompleter.complete(true);
- await shutdown(true);
- return;
- }
- final server = _server!;
- server.listen(_requestHandler, cancelOnError: true);
-
- if (_waitForDdsToAdvertiseService) {
- _ddsInstance = _DebuggingSession();
- await _ddsInstance!.start(
- serverAddress!,
- _ddsIP,
- _ddsPort.toString(),
- _authCodesDisabled,
- _serveDevtools,
- );
- } else {
- await outputConnectionInformation();
- }
- // Server is up and running.
- _running = true;
- _notifyServerState(serverAddress.toString());
- onServerAddressChange('$serverAddress');
- startingCompleter.complete(true);
- }
-
- Future<void> outputConnectionInformation() async {
- serverPrint('The Dart VM service is listening on $serverAddress');
- if (Platform.isFuchsia) {
- // Create a file with the port number.
- final tmp = Directory.systemTemp.path;
- final path = '$tmp/dart.services/${_server!.port}';
- serverPrint('Creating $path');
- File(path)..createSync(recursive: true);
- }
- final serviceInfoFilenameLocal = _serviceInfoFilename;
- if (serviceInfoFilenameLocal != null &&
- serviceInfoFilenameLocal.isNotEmpty) {
- await _dumpServiceInfoToFile(serviceInfoFilenameLocal);
- }
+ void _writeFuchsiaState(int port) {
+ // Create a file with the port number.
+ final tmp = Directory.systemTemp.path;
+ final path = '$tmp/dart.services/${port}';
+ serverPrint('Creating $path');
+ File(path).createSync(recursive: true);
}
void _cleanupFuchsiaState(int port) {
@@ -638,47 +687,6 @@
serverPrint('Deleting $path');
File(path).deleteSync();
}
-
- Future<void> shutdown(bool forced) async {
- // If start is pending, wait for it to complete.
- if (_startingCompleter != null) {
- if (!_startingCompleter!.isCompleted) {
- await _startingCompleter!.future;
- }
- }
-
- final server = _server;
- if (server == null) {
- // Not started.
- return;
- }
-
- if (Platform.isFuchsia) {
- _cleanupFuchsiaState(server.port);
- }
-
- final address = serverAddress!;
-
- try {
- // Shutdown HTTP server and subscription.
- await server.close(force: forced);
- if (!_service.isExiting) {
- // Only print this message if the service has been toggled off, not
- // when the VM is exiting.
- serverPrint('Dart VM service no longer listening on $address');
- }
- } catch (e, st) {
- serverPrint('Could not shutdown Dart VM service HTTP server:\n$e\n$st\n');
- } finally {
- _ddsInstance?.shutdown();
- _ddsInstance = null;
- _server = null;
- _startingCompleter = null;
- _running = false;
- _notifyServerState('');
- onServerAddressChange(null);
- }
- }
}
@pragma("vm:external-name", "VMServiceIO_NotifyServerState")