diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json index 99dc94b64a7..6787873aab2 100644 --- a/.dart_tool/package_config.json +++ b/.dart_tool/package_config.json @@ -11,7 +11,7 @@ "constraint, update this by running tools/generate_package_config.dart." ], "configVersion": 2, - "generated": "2021-04-30T16:02:33.294454", + "generated": "2021-05-03T09:47:39.938400", "generator": "tools/generate_package_config.dart", "packages": [ { @@ -252,6 +252,18 @@ "packageUri": "lib/", "languageVersion": "2.3" }, + { + "name": "devtools_server", + "rootUri": "../third_party/devtools/devtools_server", + "packageUri": "lib/", + "languageVersion": "2.6" + }, + { + "name": "devtools_shared", + "rootUri": "../third_party/devtools/devtools_shared", + "packageUri": "lib/", + "languageVersion": "2.3" + }, { "name": "diagnostic", "rootUri": "../pkg/diagnostic", @@ -727,6 +739,12 @@ "packageUri": "lib/", "languageVersion": "2.12" }, + { + "name": "uuid", + "rootUri": "../third_party/pkg/uuid", + "packageUri": "lib/", + "languageVersion": "2.0" + }, { "name": "vector_math", "rootUri": "../third_party/pkg/vector_math", diff --git a/.packages b/.packages index af0722733b0..124a2a3cdc7 100644 --- a/.packages +++ b/.packages @@ -22,6 +22,7 @@ benchmark_harness:third_party/pkg/benchmark_harness/lib boolean_selector:third_party/pkg/boolean_selector/lib browser_launcher:third_party/pkg/browser_launcher/lib build_integration:pkg/build_integration/lib +browser_launcher:third_party/pkg/browser_launcher/lib charcode:third_party/pkg/charcode/lib cli_util:third_party/pkg/cli_util/lib collection:third_party/pkg/collection/lib @@ -38,6 +39,7 @@ dartdev:pkg/dartdev/lib dartdoc:third_party/pkg/dartdoc/lib dds:pkg/dds/lib dev_compiler:pkg/dev_compiler/lib +devtools_shared:third_party/devtools/devtools_shared/lib diagnostic:pkg/diagnostic/lib expect:pkg/expect/lib ffi:third_party/pkg/ffi/lib diff --git a/DEPS b/DEPS index 2c75fde6cbc..83e3cd08680 100644 --- a/DEPS +++ b/DEPS @@ -80,6 +80,7 @@ vars = { "boringssl_gen_rev": "7322fc15cc065d8d2957fccce6b62a509dc4d641", "boringssl_rev" : "1607f54fed72c6589d560254626909a64124f091", "browser-compat-data_tag": "v1.0.22", + "browser_launcher_rev": "12ab9f351a44ac803de9bc17bb2180bb312a9dd7", "charcode_rev": "bcd8a12c315b7a83390e4865ad847ecd9344cba2", "chrome_rev" : "19997", "cli_util_rev" : "fd1b716e8a350a454e01ae56df540293d31ff6c8", @@ -105,7 +106,6 @@ vars = { "dart_style_rev": "f17c23e0eea9a870601c19d904e2a9c1a7c81470", "chromedriver_tag": "83.0.4103.39", - "browser_launcher_rev": "12ab9f351a44ac803de9bc17bb2180bb312a9dd7", "dartdoc_rev" : "505f163f7cb48e917503e4a23fbff1227e08b263", "jsshell_tag": "version:88.0", "ffi_rev": "f3346299c55669cc0db48afae85b8110088bf8da", @@ -246,7 +246,7 @@ deps = { Var("dart_root") + "/third_party/devtools": { "packages": [{ "package": "dart/third_party/flutter/devtools", - "version": "revision:6729ec62c3548839018c32fa711756202431ccf7", + "version": "git_revision:12ad5341ae0a275042c84a4e7be9a6c98db65612", }], "dep_type": "cipd", }, @@ -319,6 +319,9 @@ deps = { Var('chromium_git') + '/external/github.com/mdn/browser-compat-data' + "@" + Var("browser-compat-data_tag"), + Var("dart_root") + "/third_party/pkg/browser_launcher": + Var("dart_git") + "browser_launcher.git" + "@" + Var("browser_launcher_rev"), + Var("dart_root") + "/third_party/tcmalloc/gperftools": Var('chromium_git') + '/external/github.com/gperftools/gperftools.git' + "@" + Var("gperftools_revision"), @@ -335,9 +338,6 @@ deps = { Var("dart_root") + "/third_party/pkg/boolean_selector": Var("dart_git") + "boolean_selector.git" + "@" + Var("boolean_selector_rev"), - Var("dart_root") + "/third_party/pkg/browser_launcher": - Var("dart_git") + "browser_launcher.git" + - "@" + Var("browser_launcher_rev"), Var("dart_root") + "/third_party/pkg/charcode": Var("dart_git") + "charcode.git" + "@" + Var("charcode_rev"), Var("dart_root") + "/third_party/pkg/cli_util": diff --git a/pkg/dartdev/lib/dartdev.dart b/pkg/dartdev/lib/dartdev.dart index b72fbba8c1f..5e581c5fda5 100644 --- a/pkg/dartdev/lib/dartdev.dart +++ b/pkg/dartdev/lib/dartdev.dart @@ -40,7 +40,8 @@ Future runDartdev(List args, SendPort port) async { args = args .where( (element) => !(element.contains('--observe') || - element.contains('--enable-vm-service')), + element.contains('--enable-vm-service') || + element.contains('--devtools')), ) .toList(); } diff --git a/pkg/dartdev/lib/src/commands/run.dart b/pkg/dartdev/lib/src/commands/run.dart index db32bfed057..d996c3c8075 100644 --- a/pkg/dartdev/lib/src/commands/run.dart +++ b/pkg/dartdev/lib/src/commands/run.dart @@ -158,6 +158,10 @@ class RunCommand extends DartdevCommand { hide: !verbose, negatable: false, help: 'Enables tracing of library and script loading.', + ) + ..addFlag( + 'debug-dds', + hide: true, ); addExperimentalFlags(argParser, verbose); } @@ -179,13 +183,18 @@ class RunCommand extends DartdevCommand { String launchDdsArg = argResults['launch-dds']; String ddsHost = ''; String ddsPort = ''; + + // TODO(bkonyi): allow for users to choose not to launch DevTools + // See https://github.com/dart-lang/sdk/issues/45867. + const bool launchDevTools = true; bool launchDds = false; if (launchDdsArg != null) { launchDds = true; - final ddsUrl = launchDdsArg.split(':'); + final ddsUrl = launchDdsArg.split('\\:'); ddsHost = ddsUrl[0]; ddsPort = ddsUrl[1]; } + final bool debugDds = argResults['debug-dds']; bool disableServiceAuthCodes = argResults['disable-service-auth-codes']; @@ -198,7 +207,12 @@ class RunCommand extends DartdevCommand { if (launchDds) { debugSession = _DebuggingSession(); if (!await debugSession.start( - ddsHost, ddsPort, disableServiceAuthCodes)) { + ddsHost, + ddsPort, + disableServiceAuthCodes, + launchDevTools, + debugDds, + )) { return errorExitCode; } } @@ -242,10 +256,19 @@ String maybeUriToFilename(String maybeUri) { class _DebuggingSession { Future start( - String host, String port, bool disableServiceAuthCodes) async { - final ddsSnapshot = (dirname(sdk.dart).endsWith('bin')) + String host, + String port, + bool disableServiceAuthCodes, + bool enableDevTools, + bool debugDds, + ) async { + final sdkDir = dirname(sdk.dart); + final fullSdk = sdkDir.endsWith('bin'); + final ddsSnapshot = fullSdk ? sdk.ddsSnapshot - : absolute(dirname(sdk.dart), 'gen', 'dds.dart.snapshot'); + : absolute(sdkDir, 'gen', 'dds.dart.snapshot'); + final devToolsBinaries = + fullSdk ? sdk.devToolsBinaries : absolute(sdkDir, 'devtools'); if (!Sdk.checkArtifactExists(ddsSnapshot)) { return false; } @@ -256,30 +279,51 @@ class _DebuggingSession { serviceInfo = await Service.getInfo(); } final process = await Process.start( - sdk.dart, - [ - if (dirname(sdk.dart).endsWith('bin')) - sdk.ddsSnapshot - else - absolute(dirname(sdk.dart), 'gen', 'dds.dart.snapshot'), - serviceInfo.serverUri.toString(), - host, - port, - disableServiceAuthCodes.toString(), - ], - mode: ProcessStartMode.detachedWithStdio); + sdk.dart, + [ + if (debugDds) '--enable-vm-service=0', + ddsSnapshot, + serviceInfo.serverUri.toString(), + host, + port, + disableServiceAuthCodes.toString(), + enableDevTools.toString(), + devToolsBinaries, + debugDds.toString(), + ], + mode: ProcessStartMode.detachedWithStdio, + ); final completer = Completer(); - StreamSubscription sub; - sub = process.stderr.transform(utf8.decoder).listen((event) { - if (event == 'DDS started') { - sub.cancel(); + const devToolsMessagePrefix = + 'The Dart DevTools debugger and profiler is available at:'; + if (debugDds) { + StreamSubscription stdoutSub; + stdoutSub = process.stdout.transform(utf8.decoder).listen((event) { + if (event.startsWith(devToolsMessagePrefix)) { + final ddsDebuggingUri = event.split(' ').last; + print( + 'A DevTools debugger for DDS is available at: $ddsDebuggingUri', + ); + stdoutSub.cancel(); + } + }); + } + StreamSubscription stderrSub; + stderrSub = process.stderr.transform(utf8.decoder).listen((event) { + final result = json.decode(event) as Map; + final state = result['state']; + if (state == 'started') { + if (result.containsKey('devToolsUri')) { + final devToolsUri = result['devToolsUri']; + print('$devToolsMessagePrefix $devToolsUri'); + } + stderrSub.cancel(); completer.complete(); - } else if (event.contains('Failed to start DDS')) { - sub.cancel(); - completer.completeError(event.replaceAll( - 'Failed to start DDS', + } else { + stderrSub.cancel(); + completer.completeError( 'Could not start Observatory HTTP server', - )); + ); } }); try { diff --git a/pkg/dartdev/lib/src/sdk.dart b/pkg/dartdev/lib/src/sdk.dart index 3e8a12a8b2b..426f8106fbd 100644 --- a/pkg/dartdev/lib/src/sdk.dart +++ b/pkg/dartdev/lib/src/sdk.dart @@ -68,6 +68,13 @@ class Sdk { 'dds.dart.snapshot', ); + String get devToolsBinaries => path.absolute( + sdkPath, + 'bin', + 'resources', + 'devtools', + ); + String get pubSnapshot => path.absolute( sdkPath, 'bin', diff --git a/pkg/dartdev/test/commands/run_test.dart b/pkg/dartdev/test/commands/run_test.dart index b8cc5e8da8c..58a4d56fe2d 100644 --- a/pkg/dartdev/test/commands/run_test.dart +++ b/pkg/dartdev/test/commands/run_test.dart @@ -2,6 +2,8 @@ // 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 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; @@ -300,4 +302,61 @@ void main(List args) => print("$b $args"); expect(result.stderr, isEmpty); expect(result.exitCode, 0); }); + + group('DevTools', () { + const devToolsMessagePrefix = + 'The Dart DevTools debugger and profiler is available at: http://127.0.0.1:'; + + test('spawn simple', () async { + p = project(mainSrc: "void main() { print('Hello World'); }"); + ProcessResult result = p.runSync([ + 'run', + '--enable-vm-service', + p.relativeFilePath, + ]); + expect(result.stdout, contains(devToolsMessagePrefix)); + }); + + test('implicit spawn', () async { + p = project(mainSrc: "void main() { print('Hello World'); }"); + ProcessResult result = p.runSync([ + '--enable-vm-service', + p.relativeFilePath, + ]); + expect(result.stdout, contains(devToolsMessagePrefix)); + }); + + test( + 'spawn via SIGQUIT', + () async { + p = project( + mainSrc: + 'void main() { print("ready"); int i = 0; while(true) { i++; } }', + ); + Process process = await p.start([ + p.relativeFilePath, + ]); + + final readyCompleter = Completer(); + final completer = Completer(); + + StreamSubscription sub; + sub = process.stdout.transform(utf8.decoder).listen((event) async { + if (event.contains('ready')) { + readyCompleter.complete(); + } else if (event.contains(devToolsMessagePrefix)) { + await sub.cancel(); + completer.complete(); + } + }); + // Wait for process to start. + await readyCompleter.future; + process.kill(ProcessSignal.sigquit); + await completer.future; + process.kill(); + }, + // No support for SIGQUIT on Windows. + skip: Platform.isWindows, + ); + }); } diff --git a/pkg/dds/CHANGELOG.md b/pkg/dds/CHANGELOG.md index 3db7549e55e..b39156ed0c5 100644 --- a/pkg/dds/CHANGELOG.md +++ b/pkg/dds/CHANGELOG.md @@ -1,4 +1,5 @@ -# 1.7.7-dev +# 1.8.0-dev +- Add support for launching DevTools from DDS. - Fixed issue where two clients subscribing to the same stream in close succession could result in DDS sending multiple `streamListen` requests to the VM service. diff --git a/pkg/dds/bin/dds.dart b/pkg/dds/bin/dds.dart index 9937d26ad0b..a8c72eef1b9 100644 --- a/pkg/dds/bin/dds.dart +++ b/pkg/dds/bin/dds.dart @@ -4,6 +4,7 @@ // @dart=2.10 +import 'dart:convert'; import 'dart:io'; import 'package:dds/dds.dart'; @@ -16,6 +17,9 @@ import 'package:dds/dds.dart'; /// - DDS bind address /// - DDS port /// - Disable service authentication codes +/// - Start DevTools +/// - DevTools build directory +/// - Enable logging Future main(List args) async { if (args.isEmpty) return; @@ -37,16 +41,37 @@ Future main(List args) async { port: int.parse(args[2]), ); final disableServiceAuthCodes = args[3] == 'true'; + + final startDevTools = args[4] == 'true'; + Uri devToolsBuildDirectory; + if (args[5].isNotEmpty) { + devToolsBuildDirectory = Uri.parse(args[5]); + } + final logRequests = args[6] == 'true'; try { // TODO(bkonyi): add retry logic similar to that in vmservice_server.dart // See https://github.com/dart-lang/sdk/issues/43192. - await DartDevelopmentService.startDartDevelopmentService( + final dds = await DartDevelopmentService.startDartDevelopmentService( remoteVmServiceUri, serviceUri: serviceUri, enableAuthCodes: !disableServiceAuthCodes, + devToolsConfiguration: startDevTools + ? DevToolsConfiguration( + enable: startDevTools, + customBuildDirectoryPath: devToolsBuildDirectory, + ) + : null, + logRequests: logRequests, ); - stderr.write('DDS started'); - } catch (e) { - stderr.writeln('Failed to start DDS:\n$e'); + stderr.write(json.encode({ + 'state': 'started', + if (dds.devToolsUri != null) 'devToolsUri': dds.devToolsUri.toString(), + })); + } catch (e, st) { + stderr.write(json.encode({ + 'state': 'error', + 'error': '$e', + 'stacktrace': '$st', + })); } } diff --git a/pkg/dds/lib/dds.dart b/pkg/dds/lib/dds.dart index f7c7a05594e..f0e913bf62f 100644 --- a/pkg/dds/lib/dds.dart +++ b/pkg/dds/lib/dds.dart @@ -44,6 +44,8 @@ abstract class DartDevelopmentService { Uri serviceUri, bool enableAuthCodes = true, bool ipv6 = false, + DevToolsConfiguration devToolsConfiguration = const DevToolsConfiguration(), + bool logRequests = false, }) async { if (remoteVmServiceUri == null) { throw ArgumentError.notNull('remoteVmServiceUri'); @@ -80,6 +82,8 @@ abstract class DartDevelopmentService { serviceUri, enableAuthCodes, ipv6, + devToolsConfiguration, + logRequests, ); await service.startService(); return service; @@ -125,6 +129,11 @@ abstract class DartDevelopmentService { /// Returns `null` if the service is not running. Uri get wsUri; + /// The HTTP [Uri] of the hosted DevTools instance. + /// + /// Returns `null` if DevTools is not running. + Uri get devToolsUri; + /// Set to `true` if this instance of [DartDevelopmentService] is accepting /// requests. bool get isRunning; @@ -168,3 +177,13 @@ class DartDevelopmentServiceException implements Exception { final int errorCode; final String message; } + +class DevToolsConfiguration { + const DevToolsConfiguration({ + this.enable = false, + this.customBuildDirectoryPath, + }); + + final bool enable; + final Uri customBuildDirectoryPath; +} diff --git a/pkg/dds/lib/src/client.dart b/pkg/dds/lib/src/client.dart index f81bd25b400..9ceb1628f03 100644 --- a/pkg/dds/lib/src/client.dart +++ b/pkg/dds/lib/src/client.dart @@ -21,27 +21,25 @@ import 'stream_manager.dart'; /// Representation of a single DDS client which manages the connection and /// DDS request intercepting / forwarding. class DartDevelopmentServiceClient { - factory DartDevelopmentServiceClient.fromWebSocket( + DartDevelopmentServiceClient.fromWebSocket( DartDevelopmentService dds, WebSocketChannel ws, json_rpc.Peer vmServicePeer, - ) => - DartDevelopmentServiceClient._( - dds, - ws, - vmServicePeer, - ); + ) : this._( + dds, + ws, + vmServicePeer, + ); - factory DartDevelopmentServiceClient.fromSSEConnection( + DartDevelopmentServiceClient.fromSSEConnection( DartDevelopmentService dds, SseConnection sse, json_rpc.Peer vmServicePeer, - ) => - DartDevelopmentServiceClient._( - dds, - sse, - vmServicePeer, - ); + ) : this._( + dds, + sse, + vmServicePeer, + ); DartDevelopmentServiceClient._( this.dds, diff --git a/pkg/dds/lib/src/constants.dart b/pkg/dds/lib/src/constants.dart index 2466390d62a..b69ae7c9085 100644 --- a/pkg/dds/lib/src/constants.dart +++ b/pkg/dds/lib/src/constants.dart @@ -16,6 +16,10 @@ abstract class RPCResponses { }; } +// Give connections time to reestablish before considering them closed. +// Required to reestablish connections killed by UberProxy. +const sseKeepAlive = Duration(seconds: 30); + abstract class PauseTypeMasks { static const pauseOnStartMask = 1 << 0; static const pauseOnReloadMask = 1 << 1; diff --git a/pkg/dds/lib/src/dds_impl.dart b/pkg/dds/lib/src/dds_impl.dart index db355fbad4e..85be647102d 100644 --- a/pkg/dds/lib/src/dds_impl.dart +++ b/pkg/dds/lib/src/dds_impl.dart @@ -24,6 +24,8 @@ import '../dds.dart'; import 'binary_compatible_peer.dart'; import 'client.dart'; import 'client_manager.dart'; +import 'constants.dart'; +import 'devtools/devtools_handler.dart'; import 'expression_evaluator.dart'; import 'isolate_manager.dart'; import 'stream_manager.dart'; @@ -51,7 +53,13 @@ WebSocketChannel _defaultWebSocketBuilder(Uri uri) { class DartDevelopmentServiceImpl implements DartDevelopmentService { DartDevelopmentServiceImpl( - this._remoteVmServiceUri, this._uri, this._authCodesEnabled, this._ipv6) { + this._remoteVmServiceUri, + this._uri, + this._authCodesEnabled, + this._ipv6, + this._devToolsConfiguration, + this.shouldLogRequests, + ) { _clientManager = ClientManager(this); _expressionEvaluator = ExpressionEvaluator(this); _isolateManager = IsolateManager(this); @@ -113,20 +121,26 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService { (_ipv6 ? InternetAddress.loopbackIPv6 : InternetAddress.loopbackIPv4) .host; final port = uri?.port ?? 0; - + final pipeline = const Pipeline(); + if (shouldLogRequests) { + pipeline.addMiddleware( + logRequests( + logger: (String message, bool isError) { + print('Log: $message'); + }, + ), + ); + } + pipeline.addMiddleware(_authCodeMiddleware); + final handler = pipeline.addHandler(_handlers().handler); // Start the DDS server. - _server = await io.serve( - const Pipeline() - .addMiddleware(_authCodeMiddleware) - .addHandler(_handlers().handler), - host, - port); + _server = await io.serve(handler, host, port); final tmpUri = Uri( scheme: 'http', host: host, port: _server.port, - path: '$_authCode/', + path: '$authCode/', ); // Notify the VM service that this client is DDS and that it should close @@ -157,7 +171,7 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService { return; } _shuttingDown = true; - // Don't accept anymore HTTP requests. + // Don't accept any more HTTP requests. await _server?.close(); // Close connections to clients. @@ -197,7 +211,7 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService { return forbidden; } final authToken = pathSegments[0]; - if (authToken != _authCode) { + if (authToken != authCode) { return forbidden; } // Creates a new request with the authentication code stripped from @@ -233,18 +247,12 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService { }); Handler _sseHandler() { - // Give connections time to reestablish before considering them closed. - // Required to reestablish connections killed by UberProxy. - const keepAlive = Duration(seconds: 30); - final handler = authCodesEnabled - ? SseHandler( - Uri.parse('/$_authCode/$_kSseHandlerPath'), - keepAlive: keepAlive, - ) - : SseHandler( - Uri.parse('/$_kSseHandlerPath'), - keepAlive: keepAlive, - ); + final handler = SseHandler( + authCodesEnabled + ? Uri.parse('/$authCode/$_kSseHandlerPath') + : Uri.parse('/$_kSseHandlerPath'), + keepAlive: sseKeepAlive, + ); handler.connections.rest.listen((sseConnection) { final client = DartDevelopmentServiceClient.fromSSEConnection( @@ -259,10 +267,18 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService { } Handler _httpHandler() { - // DDS doesn't support any HTTP requests itself, so we just forward all of - // them to the VM service. - final cascade = Cascade().add(proxyHandler(remoteVmServiceUri)); - return cascade.handler; + if (_devToolsConfiguration != null && _devToolsConfiguration.enable) { + // Install the DevTools handlers and forward any unhandled HTTP requests to + // the VM service. + final buildDir = + _devToolsConfiguration.customBuildDirectoryPath?.toFilePath(); + return devtoolsHandler( + dds: this, + buildDir: buildDir, + notFoundHandler: proxyHandler(remoteVmServiceUri), + ); + } + return proxyHandler(remoteVmServiceUri); } List _cleanupPathSegments(Uri uri) { @@ -296,14 +312,43 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService { return uri.replace(scheme: 'sse', pathSegments: pathSegments); } + Uri _toDevTools(Uri uri) { + // The DevTools URI is a bit strange as the query parameters appear after + // the fragment. There's no nice way to encode the query parameters + // properly, so we create another Uri just to grab the formatted query. + // The result will need to have '/?' prepended when being used as the + // fragment to get the correct format. + final query = Uri( + queryParameters: { + 'uri': wsUri.toString(), + }, + ).query; + return Uri( + scheme: 'http', + host: uri.host, + port: uri.port, + pathSegments: [ + ...uri.pathSegments.where( + (e) => e.isNotEmpty, + ), + 'devtools', + '', + ], + fragment: '/?$query', + ); + } + String getNamespace(DartDevelopmentServiceClient client) => clientManager.clients.keyOf(client); @override bool get authCodesEnabled => _authCodesEnabled; final bool _authCodesEnabled; + String get authCode => _authCode; String _authCode; + final bool shouldLogRequests; + @override Uri get remoteVmServiceUri => _remoteVmServiceUri; @@ -313,17 +358,21 @@ class DartDevelopmentServiceImpl implements DartDevelopmentService { @override Uri get uri => _uri; + Uri _uri; @override Uri get sseUri => _toSse(_uri); Uri get wsUri => _toWebSocket(_uri); - Uri _uri; + + Uri get devToolsUri => _toDevTools(_uri); final bool _ipv6; bool get isRunning => _uri != null; + final DevToolsConfiguration _devToolsConfiguration; + Future get done => _done.future; Completer _done = Completer(); bool _shuttingDown = false; diff --git a/pkg/dds/lib/src/devtools/devtools_client.dart b/pkg/dds/lib/src/devtools/devtools_client.dart new file mode 100644 index 00000000000..5f8670ec8e3 --- /dev/null +++ b/pkg/dds/lib/src/devtools/devtools_client.dart @@ -0,0 +1,96 @@ +// Copyright (c) 2021, 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. + +// @dart=2.9 + +import 'dart:async'; + +import 'package:json_rpc_2/src/server.dart' as json_rpc; +import 'package:sse/src/server/sse_handler.dart'; +import 'package:stream_channel/stream_channel.dart'; + +import 'server_api.dart'; + +class LoggingMiddlewareSink implements StreamSink { + LoggingMiddlewareSink(this.sink); + + @override + void add(S event) { + print('DevTools SSE response: $event'); + sink.add(event); + } + + @override + void addError(Object error, [StackTrace stackTrace]) { + print('DevTools SSE error response: $error'); + sink.addError(error); + } + + @override + Future addStream(Stream stream) { + return sink.addStream(stream); + } + + @override + Future close() => sink.close(); + + @override + Future get done => sink.done; + + final StreamSink sink; +} + +/// Represents a DevTools client connection to the DevTools server API. +class DevToolsClient { + DevToolsClient.fromSSEConnection( + SseConnection sse, + bool loggingEnabled, + ) { + Stream stream = sse.stream; + StreamSink sink = sse.sink; + + if (loggingEnabled) { + stream = stream.map((String e) { + print('DevTools SSE request: $e'); + return e; + }); + sink = LoggingMiddlewareSink(sink); + } + + _server = json_rpc.Server( + StreamChannel(stream, sink), + strictProtocolChecks: false, + ); + _registerJsonRpcMethods(); + _server.listen(); + } + + void _registerJsonRpcMethods() { + _server.registerMethod('connected', (parameters) { + // Nothing to do here. + }); + + _server.registerMethod('currentPage', (parameters) { + // Nothing to do here. + }); + + _server.registerMethod('disconnected', (parameters) { + // Nothing to do here. + }); + + _server.registerMethod('getPreferenceValue', (parameters) { + final key = parameters['key'].asString; + final value = ServerApi.devToolsPreferences.properties[key]; + return value; + }); + + _server.registerMethod('setPreferenceValue', (parameters) { + final key = parameters['key'].asString; + final value = parameters['value'].value; + ServerApi.devToolsPreferences.properties[key] = value; + }); + } + + json_rpc.Server _server; +} diff --git a/pkg/dds/lib/src/devtools/devtools_handler.dart b/pkg/dds/lib/src/devtools/devtools_handler.dart new file mode 100644 index 00000000000..a0a4bf12e85 --- /dev/null +++ b/pkg/dds/lib/src/devtools/devtools_handler.dart @@ -0,0 +1,87 @@ +// Copyright (c) 2021, 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. + +// @dart=2.9 + +import 'dart:async'; + +import 'package:dds/src/constants.dart'; +import 'package:meta/meta.dart'; +import 'package:shelf/shelf.dart'; +import 'package:shelf_static/shelf_static.dart'; +import 'package:sse/server/sse_handler.dart'; + +import '../dds_impl.dart'; +import 'devtools_client.dart'; +import 'server_api.dart'; + +/// Returns a [Handler] which handles serving DevTools and the DevTools server +/// API under $DDS_URI/devtools/. +/// +/// [buildDir] is the path to the pre-compiled DevTools instance to be served. +/// +/// [notFoundHandler] is a [Handler] to which requests that could not be handled +/// by the DevTools handler are forwarded (e.g., a proxy to the VM service). +FutureOr devtoolsHandler({ + @required DartDevelopmentServiceImpl dds, + @required String buildDir, + @required Handler notFoundHandler, +}) { + // Serves the web assets for DevTools. + final devtoolsAssetHandler = createStaticHandler( + buildDir, + defaultDocument: 'index.html', + ); + + // Support DevTools client-server interface via SSE. + // Note: the handler path needs to match the full *original* path, not the + // current request URL (we remove '/devtools' in the initial router but we + // need to include it here). + const devToolsSseHandlerPath = '/devtools/api/sse'; + final devToolsApiHandler = SseHandler( + dds.authCodesEnabled + ? Uri.parse('/${dds.authCode}$devToolsSseHandlerPath') + : Uri.parse(devToolsSseHandlerPath), + keepAlive: sseKeepAlive, + ); + + devToolsApiHandler.connections.rest.listen( + (sseConnection) => DevToolsClient.fromSSEConnection( + sseConnection, + dds.shouldLogRequests, + ), + ); + + final devtoolsHandler = (Request request) { + // If the request isn't of the form api/ assume it's a request for + // DevTools assets. + if (request.url.pathSegments.length < 2 || + request.url.pathSegments.first != 'api') { + return devtoolsAssetHandler(request); + } + final method = request.url.pathSegments[1]; + if (method == 'ping') { + // Note: we have an 'OK' body response, otherwise the response has an + // incorrect status code (204 instead of 200). + return Response.ok('OK'); + } + if (method == 'sse') { + return devToolsApiHandler.handler(request); + } + if (!ServerApi.canHandle(request)) { + return Response.notFound('$method is not a valid API'); + } + return ServerApi.handle(request); + }; + + return (request) { + final pathSegments = request.url.pathSegments; + if (pathSegments.isEmpty || pathSegments.first != 'devtools') { + return notFoundHandler(request); + } + // Forward all requests to /devtools/* to the DevTools handler. + request = request.change(path: 'devtools'); + return devtoolsHandler(request); + }; +} diff --git a/pkg/dds/lib/src/devtools/file_system.dart b/pkg/dds/lib/src/devtools/file_system.dart new file mode 100644 index 00000000000..9a05de70971 --- /dev/null +++ b/pkg/dds/lib/src/devtools/file_system.dart @@ -0,0 +1,84 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart=2.9 + +// TODO(bkonyi): remove once package:devtools_server_api is available +// See https://github.com/flutter/devtools/issues/2958. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'usage.dart'; + +class LocalFileSystem { + static String _userHomeDir() { + final String envKey = + Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME'; + final String value = Platform.environment[envKey]; + return value == null ? '.' : value; + } + + /// Returns the path to the DevTools storage directory. + static String devToolsDir() { + return path.join(_userHomeDir(), '.flutter-devtools'); + } + + /// Moves the .devtools file to ~/.flutter-devtools/.devtools if the .devtools file + /// exists in the user's home directory. + static void maybeMoveLegacyDevToolsStore() { + final file = File(path.join(_userHomeDir(), DevToolsUsage.storeName)); + if (file.existsSync()) { + ensureDevToolsDirectory(); + file.copySync(path.join(devToolsDir(), DevToolsUsage.storeName)); + file.deleteSync(); + } + } + + /// Creates the ~/.flutter-devtools directory if it does not already exist. + static void ensureDevToolsDirectory() { + Directory('${LocalFileSystem.devToolsDir()}').createSync(); + } + + /// Returns a DevTools file from the given path. + /// + /// Only files within ~/.flutter-devtools/ can be accessed. + static File devToolsFileFromPath(String pathFromDevToolsDir) { + if (pathFromDevToolsDir.contains('..')) { + // The passed in path should not be able to walk up the directory tree + // outside of the ~/.flutter-devtools/ directory. + return null; + } + ensureDevToolsDirectory(); + final file = File(path.join(devToolsDir(), pathFromDevToolsDir)); + if (!file.existsSync()) { + return null; + } + return file; + } + + /// Returns a DevTools file from the given path as encoded json. + /// + /// Only files within ~/.flutter-devtools/ can be accessed. + static String devToolsFileAsJson(String pathFromDevToolsDir) { + final file = devToolsFileFromPath(pathFromDevToolsDir); + if (file == null) return null; + + final fileName = path.basename(file.path); + if (!fileName.endsWith('.json')) return null; + + final content = file.readAsStringSync(); + final json = jsonDecode(content); + json['lastModifiedTime'] = file.lastModifiedSync().toString(); + return jsonEncode(json); + } + + /// Whether the flutter store file exists. + static bool flutterStoreExists() { + final flutterStore = File('${_userHomeDir()}/.flutter'); + return flutterStore.existsSync(); + } +} diff --git a/pkg/dds/lib/src/devtools/server_api.dart b/pkg/dds/lib/src/devtools/server_api.dart new file mode 100644 index 00000000000..b866f4448d3 --- /dev/null +++ b/pkg/dds/lib/src/devtools/server_api.dart @@ -0,0 +1,230 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart=2.9 + +// TODO(bkonyi): remove once package:devtools_server_api is available +// See https://github.com/flutter/devtools/issues/2958. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:devtools_shared/devtools_shared.dart'; +import 'package:shelf/shelf.dart' as shelf; + +import 'file_system.dart'; +import 'usage.dart'; + +/// The DevTools server API. +/// +/// This defines endpoints that serve all requests that come in over api/. +class ServerApi { + static const errorNoActiveSurvey = 'ERROR: setActiveSurvey not called.'; + + /// Determines whether or not [request] is an API call. + static bool canHandle(shelf.Request request) { + return request.url.path.startsWith(apiPrefix); + } + + /// Handles all requests. + /// + /// To override an API call, pass in a subclass of [ServerApi]. + static FutureOr handle( + shelf.Request request, [ + ServerApi api, + ]) { + api ??= ServerApi(); + switch (request.url.path) { + // ----- Flutter Tool GA store. ----- + case apiGetFlutterGAEnabled: + // Is Analytics collection enabled? + return api.getCompleted( + request, + json.encode(FlutterUsage.doesStoreExist ? _usage.enabled : null), + ); + case apiGetFlutterGAClientId: + // Flutter Tool GA clientId - ONLY get Flutter's clientId if enabled is + // true. + return (FlutterUsage.doesStoreExist) + ? api.getCompleted( + request, + json.encode(_usage.enabled ? _usage.clientId : null), + ) + : api.getCompleted( + request, + json.encode(null), + ); + + // ----- DevTools GA store. ----- + + case apiResetDevTools: + _devToolsUsage.reset(); + return api.getCompleted(request, json.encode(true)); + case apiGetDevToolsFirstRun: + // Has DevTools been run first time? To bring up welcome screen. + return api.getCompleted( + request, + json.encode(_devToolsUsage.isFirstRun), + ); + case apiGetDevToolsEnabled: + // Is DevTools Analytics collection enabled? + return api.getCompleted(request, json.encode(_devToolsUsage.enabled)); + case apiSetDevToolsEnabled: + // Enable or disable DevTools analytics collection. + final queryParams = request.requestedUri.queryParameters; + if (queryParams.containsKey(devToolsEnabledPropertyName)) { + _devToolsUsage.enabled = + json.decode(queryParams[devToolsEnabledPropertyName]); + } + return api.setCompleted(request, json.encode(_devToolsUsage.enabled)); + + // ----- DevTools survey store. ----- + + case apiSetActiveSurvey: + // Assume failure. + bool result = false; + + // Set the active survey used to store subsequent apiGetSurveyActionTaken, + // apiSetSurveyActionTaken, apiGetSurveyShownCount, and + // apiIncrementSurveyShownCount calls. + final queryParams = request.requestedUri.queryParameters; + if (queryParams.keys.length == 1 && + queryParams.containsKey(activeSurveyName)) { + final String theSurveyName = queryParams[activeSurveyName]; + + // Set the current activeSurvey. + _devToolsUsage.activeSurvey = theSurveyName; + result = true; + } + + return api.getCompleted(request, json.encode(result)); + case apiGetSurveyActionTaken: + // Request setActiveSurvey has not been requested. + if (_devToolsUsage.activeSurvey == null) { + return api.badRequest('$errorNoActiveSurvey ' + '- $apiGetSurveyActionTaken'); + } + // SurveyActionTaken has the survey been acted upon (taken or dismissed) + return api.getCompleted( + request, + json.encode(_devToolsUsage.surveyActionTaken), + ); + // TODO(terry): remove the query param logic for this request. + // setSurveyActionTaken should only be called with the value of true, so + // we can remove the extra complexity. + case apiSetSurveyActionTaken: + // Request setActiveSurvey has not been requested. + if (_devToolsUsage.activeSurvey == null) { + return api.badRequest('$errorNoActiveSurvey ' + '- $apiSetSurveyActionTaken'); + } + // Set the SurveyActionTaken. + // Has the survey been taken or dismissed.. + final queryParams = request.requestedUri.queryParameters; + if (queryParams.containsKey(surveyActionTakenPropertyName)) { + _devToolsUsage.surveyActionTaken = + json.decode(queryParams[surveyActionTakenPropertyName]); + } + return api.setCompleted( + request, + json.encode(_devToolsUsage.surveyActionTaken), + ); + case apiGetSurveyShownCount: + // Request setActiveSurvey has not been requested. + if (_devToolsUsage.activeSurvey == null) { + return api.badRequest('$errorNoActiveSurvey ' + '- $apiGetSurveyShownCount'); + } + // SurveyShownCount how many times have we asked to take survey. + return api.getCompleted( + request, + json.encode(_devToolsUsage.surveyShownCount), + ); + case apiIncrementSurveyShownCount: + // Request setActiveSurvey has not been requested. + if (_devToolsUsage.activeSurvey == null) { + return api.badRequest('$errorNoActiveSurvey ' + '- $apiIncrementSurveyShownCount'); + } + // Increment the SurveyShownCount, we've asked about the survey. + _devToolsUsage.incrementSurveyShownCount(); + return api.getCompleted( + request, + json.encode(_devToolsUsage.surveyShownCount), + ); + case apiGetBaseAppSizeFile: + final queryParams = request.requestedUri.queryParameters; + if (queryParams.containsKey(baseAppSizeFilePropertyName)) { + final filePath = queryParams[baseAppSizeFilePropertyName]; + final fileJson = LocalFileSystem.devToolsFileAsJson(filePath); + if (fileJson == null) { + return api.badRequest('No JSON file available at $filePath.'); + } + return api.getCompleted(request, fileJson); + } + return api.badRequest('Request for base app size file does not ' + 'contain a query parameter with the expected key: ' + '$baseAppSizeFilePropertyName'); + case apiGetTestAppSizeFile: + final queryParams = request.requestedUri.queryParameters; + if (queryParams.containsKey(testAppSizeFilePropertyName)) { + final filePath = queryParams[testAppSizeFilePropertyName]; + final fileJson = LocalFileSystem.devToolsFileAsJson(filePath); + if (fileJson == null) { + return api.badRequest('No JSON file available at $filePath.'); + } + return api.getCompleted(request, fileJson); + } + return api.badRequest('Request for test app size file does not ' + 'contain a query parameter with the expected key: ' + '$testAppSizeFilePropertyName'); + default: + return api.notImplemented(request); + } + } + + // Accessing Flutter usage file e.g., ~/.flutter. + // NOTE: Only access the file if it exists otherwise Flutter Tool hasn't yet + // been run. + static final FlutterUsage _usage = + FlutterUsage.doesStoreExist ? FlutterUsage() : null; + + // Accessing DevTools usage file e.g., ~/.devtools + static final DevToolsUsage _devToolsUsage = DevToolsUsage(); + + static DevToolsUsage get devToolsPreferences => _devToolsUsage; + + /// Logs a page view in the DevTools server. + /// + /// In the open-source version of DevTools, Google Analytics handles this + /// without any need to involve the server. + FutureOr logScreenView(shelf.Request request) => + notImplemented(request); + + /// Return the value of the property. + FutureOr getCompleted(shelf.Request request, String value) => + shelf.Response.ok('$value'); + + /// Return the value of the property after the property value has been set. + FutureOr setCompleted(shelf.Request request, String value) => + shelf.Response.ok('$value'); + + /// A [shelf.Response] for API calls that encountered a request problem e.g., + /// setActiveSurvey not called. + /// + /// This is a 400 Bad Request response. + FutureOr badRequest([String logError]) { + if (logError != null) print(logError); + return shelf.Response(HttpStatus.badRequest); + } + + /// A [shelf.Response] for API calls that have not been implemented in this + /// server. + /// + /// This is a no-op 204 No Content response because returning 404 Not Found + /// creates unnecessary noise in the console. + FutureOr notImplemented(shelf.Request request) => + shelf.Response(HttpStatus.noContent); +} diff --git a/pkg/dds/lib/src/devtools/usage.dart b/pkg/dds/lib/src/devtools/usage.dart new file mode 100644 index 00000000000..afa35d72805 --- /dev/null +++ b/pkg/dds/lib/src/devtools/usage.dart @@ -0,0 +1,236 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart=2.9 + +// TODO(bkonyi): remove once package:devtools_server_api is available +// See https://github.com/flutter/devtools/issues/2958. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:usage/usage_io.dart'; + +import 'file_system.dart'; + +/// Access the file '~/.flutter'. +class FlutterUsage { + /// Create a new Usage instance; [versionOverride] and [configDirOverride] are + /// used for testing. + FlutterUsage({ + String settingsName = 'flutter', + String versionOverride, + String configDirOverride, + }) { + _analytics = AnalyticsIO('', settingsName, ''); + } + + Analytics _analytics; + + /// Does the .flutter store exist? + static bool get doesStoreExist { + return LocalFileSystem.flutterStoreExists(); + } + + bool get isFirstRun => _analytics.firstRun; + + bool get enabled => _analytics.enabled; + + set enabled(bool value) => _analytics.enabled = value; + + String get clientId => _analytics.clientId; +} + +// Access the DevTools on disk store (~/.devtools/.devtools). +class DevToolsUsage { + /// Create a new Usage instance; [versionOverride] and [configDirOverride] are + /// used for testing. + DevToolsUsage({ + String versionOverride, + String configDirOverride, + }) { + LocalFileSystem.maybeMoveLegacyDevToolsStore(); + properties = IOPersistentProperties( + storeName, + documentDirPath: LocalFileSystem.devToolsDir(), + ); + } + + static const storeName = '.devtools'; + + /// The activeSurvey is the property name of a top-level property + /// existing or created in the file ~/.devtools + /// If the property doesn't exist it is created with default survey values: + /// + /// properties[activeSurvey]['surveyActionTaken'] = false; + /// properties[activeSurvey]['surveyShownCount'] = 0; + /// + /// It is a requirement that the API apiSetActiveSurvey must be called before + /// calling any survey method on DevToolsUsage (addSurvey, rewriteActiveSurvey, + /// surveyShownCount, incrementSurveyShownCount, or surveyActionTaken). + String _activeSurvey; + + IOPersistentProperties properties; + + static const _surveyActionTaken = 'surveyActionTaken'; + static const _surveyShownCount = 'surveyShownCount'; + + void reset() { + properties.remove('firstRun'); + properties['enabled'] = false; + } + + bool get isFirstRun { + properties['firstRun'] = properties['firstRun'] == null; + return properties['firstRun']; + } + + bool get enabled { + if (properties['enabled'] == null) { + properties['enabled'] = false; + } + + return properties['enabled']; + } + + set enabled(bool value) { + properties['enabled'] = value; + return properties['enabled']; + } + + bool surveyNameExists(String surveyName) => properties[surveyName] != null; + + void _addSurvey(String surveyName) { + assert(activeSurvey != null); + assert(activeSurvey == surveyName); + rewriteActiveSurvey(false, 0); + } + + String get activeSurvey => _activeSurvey; + + set activeSurvey(String surveyName) { + assert(surveyName != null); + _activeSurvey = surveyName; + + if (!surveyNameExists(activeSurvey)) { + // Create the survey if property is non-existent in ~/.devtools + _addSurvey(activeSurvey); + } + } + + /// Need to rewrite the entire survey structure for property to be persisted. + void rewriteActiveSurvey(bool actionTaken, int shownCount) { + assert(activeSurvey != null); + properties[activeSurvey] = { + _surveyActionTaken: actionTaken, + _surveyShownCount: shownCount, + }; + } + + int get surveyShownCount { + assert(activeSurvey != null); + final prop = properties[activeSurvey]; + if (prop[_surveyShownCount] == null) { + rewriteActiveSurvey(prop[_surveyActionTaken], 0); + } + return properties[activeSurvey][_surveyShownCount]; + } + + void incrementSurveyShownCount() { + assert(activeSurvey != null); + surveyShownCount; // Ensure surveyShownCount has been initialized. + final prop = properties[activeSurvey]; + rewriteActiveSurvey(prop[_surveyActionTaken], prop[_surveyShownCount] + 1); + } + + bool get surveyActionTaken { + assert(activeSurvey != null); + return properties[activeSurvey][_surveyActionTaken] == true; + } + + set surveyActionTaken(bool value) { + assert(activeSurvey != null); + final prop = properties[activeSurvey]; + rewriteActiveSurvey(value, prop[_surveyShownCount]); + } +} + +abstract class PersistentProperties { + PersistentProperties(this.name); + + final String name; + + dynamic operator [](String key); + + void operator []=(String key, dynamic value); + + /// Re-read settings from the backing store. + /// + /// May be a no-op on some platforms. + void syncSettings(); +} + +const JsonEncoder _jsonEncoder = JsonEncoder.withIndent(' '); + +class IOPersistentProperties extends PersistentProperties { + IOPersistentProperties( + String name, { + String documentDirPath, + }) : super(name) { + final String fileName = name.replaceAll(' ', '_'); + documentDirPath ??= LocalFileSystem.devToolsDir(); + _file = File(path.join(documentDirPath, fileName)); + if (!_file.existsSync()) { + _file.createSync(recursive: true); + } + syncSettings(); + } + + IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) { + _file = file; + if (!_file.existsSync()) { + _file.createSync(recursive: true); + } + syncSettings(); + } + + File _file; + + Map _map; + + @override + dynamic operator [](String key) => _map[key]; + + @override + void operator []=(String key, dynamic value) { + if (value == null && !_map.containsKey(key)) return; + if (_map[key] == value) return; + + if (value == null) { + _map.remove(key); + } else { + _map[key] = value; + } + + try { + _file.writeAsStringSync(_jsonEncoder.convert(_map) + '\n'); + } catch (_) {} + } + + @override + void syncSettings() { + try { + String contents = _file.readAsStringSync(); + if (contents.isEmpty) contents = '{}'; + _map = jsonDecode(contents); + } catch (_) { + _map = {}; + } + } + + void remove(String propertyName) { + _map.remove(propertyName); + } +} diff --git a/pkg/dds/pubspec.yaml b/pkg/dds/pubspec.yaml index 221e3df8821..a69236cd945 100644 --- a/pkg/dds/pubspec.yaml +++ b/pkg/dds/pubspec.yaml @@ -3,7 +3,7 @@ description: >- A library used to spawn the Dart Developer Service, used to communicate with a Dart VM Service instance. -version: 1.7.6 +version: 1.8.0-dev homepage: https://github.com/dart-lang/sdk/tree/master/pkg/dds @@ -12,18 +12,21 @@ environment: dependencies: async: ^2.4.1 + devtools_shared: ^2.0.0 json_rpc_2: ^2.2.0 meta: ^1.1.8 + path: ^1.8.0 pedantic: ^1.7.0 shelf: ^1.0.0 shelf_proxy: ^1.0.0 + shelf_static: ^1.0.0-dev shelf_web_socket: ^1.0.0 sse: ^3.7.0 stream_channel: ^2.0.0 + usage: ^4.0.0 vm_service: ^6.0.1-nullsafety.0 web_socket_channel: ^2.0.0 dev_dependencies: - shelf_static: ^1.0.0 test: ^1.0.0 webdriver: ^3.0.0 diff --git a/runtime/bin/main.cc b/runtime/bin/main.cc index 67d9c2d2771..95c7166d300 100644 --- a/runtime/bin/main.cc +++ b/runtime/bin/main.cc @@ -553,13 +553,12 @@ static Dart_Isolate CreateAndSetupServiceIsolate(const char* script_uri, vm_service_server_port = 0; } - // We do not want to wait for DDS to advertise availability of VM service in the - // following scenarios: - // - When the VM service is disabled (can be started at a later time via SIGQUIT). - // - The DartDev CLI is disabled (CLI isolate starts DDS) and VM service is enabled. - bool wait_for_dds_to_advertise_service = - !Options::disable_dart_dev() && Options::enable_vm_service(); - + // We do not want to wait for DDS to advertise availability of VM service in + // the following scenarios: + // - The DartDev CLI is disabled (CLI isolate starts DDS) and VM service is + // enabled. + // TODO(bkonyi): do we want to tie DevTools / DDS to the CLI in the long run? + bool wait_for_dds_to_advertise_service = !Options::disable_dart_dev(); // Load embedder specific bits and return. if (!VmService::Setup( Options::disable_dart_dev() ? Options::vm_service_server_ip() diff --git a/runtime/bin/main_options.cc b/runtime/bin/main_options.cc index e60f6c82ae7..77b794c8e03 100644 --- a/runtime/bin/main_options.cc +++ b/runtime/bin/main_options.cc @@ -583,7 +583,7 @@ bool Options::ParseArguments(int argc, run_command = true; } if (!Options::disable_dart_dev() && enable_vm_service_ && run_command) { - const char* dds_format_str = "--launch-dds=%s:%d"; + const char* dds_format_str = "--launch-dds=%s\\:%d"; size_t size = snprintf(nullptr, 0, dds_format_str, vm_service_server_ip(), vm_service_server_port()); @@ -603,6 +603,7 @@ bool Options::ParseArguments(int argc, first_option = false; } } + // Verify consistency of arguments. // snapshot_depfile is an alias for depfile. Passing them both is an error. diff --git a/sdk/BUILD.gn b/sdk/BUILD.gn index 15a1abfd32c..13900927b88 100644 --- a/sdk/BUILD.gn +++ b/sdk/BUILD.gn @@ -252,6 +252,18 @@ copy_tree_specs += [ }, ] +# This rule copies the pre-built DevTools application to +# bin/resources/devtools/ +copy_tree_specs += [ + { + target = "copy_prebuilt_devtools" + visibility = [ ":create_common_sdk" ] + source = "../third_party/devtools/web" + dest = "$root_out_dir/dart-sdk/bin/resources/devtools" + ignore_patterns = "{}" + }, +] + # This loop generates rules to copy libraries to lib/ foreach(library, _full_sdk_libraries) { copy_tree_specs += [ @@ -811,6 +823,7 @@ group("create_common_sdk") { ":copy_libraries_dart", ":copy_libraries_specification", ":copy_license", + ":copy_prebuilt_devtools", ":copy_readme", ":copy_vm_dill_files", ":write_dartdoc_options", diff --git a/sdk/lib/_internal/vm/bin/vmservice_io.dart b/sdk/lib/_internal/vm/bin/vmservice_io.dart index d1f8a5d3423..833e5a223e3 100644 --- a/sdk/lib/_internal/vm/bin/vmservice_io.dart +++ b/sdk/lib/_internal/vm/bin/vmservice_io.dart @@ -43,6 +43,7 @@ bool _waitForDdsToAdvertiseService = false; // HTTP server. Server? server; Future? serverFuture; +_DebuggingSession? ddsInstance; Server _lazyServerBoot() { var localServer = server; @@ -58,6 +59,90 @@ Server _lazyServerBoot() { return localServer; } +/// Responsible for launching a DevTools instance when the service is started +/// via SIGQUIT. +class _DebuggingSession { + Future start( + String host, + String port, + bool disableServiceAuthCodes, + bool enableDevTools, + ) async { + final dartPath = Uri.parse(Platform.resolvedExecutable); + final dartDir = [ + '', // Include leading '/' + ...dartPath.pathSegments.sublist( + 0, + dartPath.pathSegments.length - 1, + ), + ].join('/'); + + final fullSdk = dartDir.endsWith('bin'); + + final ddsSnapshot = [ + dartDir, + fullSdk ? 'snapshots' : 'gen', + 'dds.dart.snapshot', + ].join('/'); + + final devToolsBinaries = [ + dartDir, + if (fullSdk) 'resources', + 'devtools', + ].join('/'); + + const enableLogging = false; + _process = await Process.start( + dartPath.toString(), + [ + ddsSnapshot, + server!.serverAddress!.toString(), + host, + port, + disableServiceAuthCodes.toString(), + enableDevTools.toString(), + devToolsBinaries, + enableLogging.toString(), + ], + mode: ProcessStartMode.detachedWithStdio, + ); + final completer = Completer(); + late StreamSubscription stderrSub; + stderrSub = _process!.stderr.transform(utf8.decoder).listen((event) { + final result = json.decode(event) as Map; + final state = result['state']; + if (state == 'started') { + if (result.containsKey('devToolsUri')) { + // NOTE: update pkg/dartdev/lib/src/commands/run.dart if this message + // is changed to ensure consistency. + const devToolsMessagePrefix = + 'The Dart DevTools debugger and profiler is available at:'; + final devToolsUri = result['devToolsUri']; + print('$devToolsMessagePrefix $devToolsUri'); + } + stderrSub.cancel(); + completer.complete(); + } else { + stderrSub.cancel(); + completer.completeError( + 'Could not start Observatory HTTP server', + ); + } + }); + try { + await completer.future; + return true; + } catch (e) { + stderr.write(e); + return false; + } + } + + void shutdown() => _process!.kill(); + + Process? _process; +} + Future cleanupCallback() async { // Cancel the sigquit subscription. if (_signalSubscription != null) { @@ -221,10 +306,6 @@ void webServerAcceptNewWebSocketConnections(bool enable) { _server.acceptNewWebSocketConnections = enable; } -void _clearFuture(_) { - serverFuture = null; -} - _onSignal(ProcessSignal signal) { if (serverFuture != null) { // Still waiting. @@ -233,9 +314,21 @@ _onSignal(ProcessSignal signal) { final _server = _lazyServerBoot(); // Toggle HTTP server. if (_server.running) { - _server.shutdown(true).then(_clearFuture); + _server.shutdown(true).then((_) async { + ddsInstance?.shutdown(); + await VMService().clearState(); + serverFuture = null; + }); } else { - _server.startup().then(_clearFuture); + _server.startup().then((_) { + ddsInstance = _DebuggingSession() + ..start( + _server._ip, + _server._port.toString(), + false, + true, + ); + }); } } diff --git a/sdk/lib/_internal/vm/bin/vmservice_server.dart b/sdk/lib/_internal/vm/bin/vmservice_server.dart index 69aa7f8a9d1..f5742edd2e6 100644 --- a/sdk/lib/_internal/vm/bin/vmservice_server.dart +++ b/sdk/lib/_internal/vm/bin/vmservice_server.dart @@ -26,9 +26,9 @@ class WebSocketClient extends Client { socket.done.then((_) => close()); } - disconnect() { + Future disconnect() async { if (socket != null) { - socket.close(); + await socket.close(); } } @@ -102,8 +102,8 @@ class HttpRequestClient extends Client { HttpRequestClient(this.request, VMService service) : super(service, sendEvents: false); - disconnect() { - request.response.close(); + Future disconnect() async { + await request.response.close(); close(); } diff --git a/sdk/lib/vmservice/vmservice.dart b/sdk/lib/vmservice/vmservice.dart index a3f7e8b0e4d..d492dcfa5c3 100644 --- a/sdk/lib/vmservice/vmservice.dart +++ b/sdk/lib/vmservice/vmservice.dart @@ -411,6 +411,16 @@ class VMService extends MessageRouter { replyPort.send(bytes); } + Future clearState() async { + // Create a copy of the set as a list because client.disconnect() will + // alter the connected clients set. + final clientsList = clients.toList(); + for (final client in clientsList) { + await client.disconnect(); + } + devfs.cleanup(); + } + Future _exit() async { isExiting = true; @@ -423,14 +433,7 @@ class VMService extends MessageRouter { // Close receive ports. isolateControlPort.close(); scriptLoadPort.close(); - - // Create a copy of the set as a list because client.disconnect() will - // alter the connected clients set. - final clientsList = clients.toList(); - for (final client in clientsList) { - client.disconnect(); - } - devfs.cleanup(); + await clearState(); final cleanup = VMServiceEmbedderHooks.cleanup; if (cleanup != null) { await cleanup(); diff --git a/third_party/devtools/update.sh b/third_party/devtools/update.sh index a69f6ee421a..077d758c333 100755 --- a/third_party/devtools/update.sh +++ b/third_party/devtools/update.sh @@ -30,12 +30,11 @@ git checkout -b cipd_release $1 # to serve from DDS. mkdir cipd_package cp -R packages/devtools/build/ cipd_package/web -cp -r packages/devtools_server cipd_package cp -r packages/devtools_shared cipd_package cipd create \ -name dart/third_party/flutter/devtools \ -in cipd_package \ -install-mode copy \ - -tag revision:$1 + -tag git_revision:$1 diff --git a/tools/bots/test_matrix.json b/tools/bots/test_matrix.json index be5f821034a..bfa020ba583 100644 --- a/tools/bots/test_matrix.json +++ b/tools/bots/test_matrix.json @@ -322,6 +322,7 @@ "xcodebuild/ReleaseSIMARM64C/", "xcodebuild/ReleaseX64/", "xcodebuild/ReleaseX64C/", + "pkg/", "samples/", "samples_2/", "samples-dev/", @@ -329,6 +330,7 @@ "third_party/android_tools/sdk/platform-tools/adb", "third_party/android_tools/ndk/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip", "third_party/android_tools/ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin/arm-linux-androideabi-strip", + "third_party/devtools/", "third_party/webdriver/", "third_party/pkg/", "third_party/pkg_tested/", diff --git a/tools/generate_package_config.dart b/tools/generate_package_config.dart index babc89dc10f..4cf47e04c27 100644 --- a/tools/generate_package_config.dart +++ b/tools/generate_package_config.dart @@ -57,6 +57,8 @@ void main(List args) { packageDirectory( 'runtime/observatory_2/tests/service_2/observatory_test_package_2'), packageDirectory('sdk/lib/_internal/sdk_library_metadata'), + packageDirectory('third_party/devtools/devtools_server'), + packageDirectory('third_party/devtools/devtools_shared'), packageDirectory('third_party/pkg/protobuf/protobuf'), packageDirectory('tools/package_deps'), ]; diff --git a/utils/dartdev/BUILD.gn b/utils/dartdev/BUILD.gn index 7f33d990301..3ce3bc38a15 100644 --- a/utils/dartdev/BUILD.gn +++ b/utils/dartdev/BUILD.gn @@ -2,12 +2,14 @@ # 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("../../build/dart/copy_tree.gni") import("../application_snapshot.gni") group("dartdev") { public_deps = [ ":copy_dartdev_kernel", ":copy_dartdev_snapshot", + ":copy_prebuilt_devtools", ] } @@ -39,3 +41,15 @@ application_snapshot("generate_dartdev_snapshot") { deps = [ "../dds:dds" ] output = "$root_gen_dir/dartdev.dart.snapshot" } + +copy_trees("copy_prebuilt_devtools") { + sources = [ + { + target = "copy_prebuilt_devtools" + visibility = [ ":dartdev" ] + source = "../../third_party/devtools/web" + dest = "$root_out_dir/devtools" + ignore_patterns = "{}" + }, + ] +}