[ VM / DDS / CLI ] Add DevTools support to the standalone VM

Example output on stdout when DevTools is enabled:

Observatory listening on http://127.0.0.1:8181/CzkZzZaONW4=/
The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/devtools/#/?uri=ws%3A%2F%2F127.0.0.1%3A8181%2FCzkZzZaONW4%3D%2Fws
hello world!

vm-service: isolate(1674461414267555)  'main' has no debugger attached and is paused at exit.  Connect to Observatory at http://127.0.0.1:8181/CzkZzZaONW4=/ to debug.

TEST=pkg/dartdev/test/commands/run_test.dart

Change-Id: Icd1afda87ad4a46f228125d53094d10adf8056ec
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/188361
Commit-Queue: Ben Konyi <bkonyi@google.com>
Reviewed-by: Siva Annamalai <asiva@google.com>
This commit is contained in:
Ben Konyi 2021-05-03 18:24:09 +00:00 committed by commit-bot@chromium.org
parent ec48e8f323
commit ef0e4ea107
29 changed files with 1199 additions and 110 deletions

View file

@ -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",

View file

@ -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

10
DEPS
View file

@ -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":

View file

@ -40,7 +40,8 @@ Future<void> runDartdev(List<String> 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();
}

View file

@ -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<bool> 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<void>();
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<String, dynamic>;
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 {

View file

@ -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',

View file

@ -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<String> 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<void>();
final completer = Completer<void>();
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,
);
});
}

View file

@ -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.

View file

@ -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<void> main(List<String> args) async {
if (args.isEmpty) return;
@ -37,16 +41,37 @@ Future<void> main(List<String> 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',
}));
}
}

View file

@ -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;
}

View file

@ -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,

View file

@ -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;

View file

@ -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<String> _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<void> get done => _done.future;
Completer _done = Completer<void>();
bool _shuttingDown = false;

View file

@ -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<S> implements StreamSink<S> {
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<S> 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<String> stream = sse.stream;
StreamSink sink = sse.sink;
if (loggingEnabled) {
stream = stream.map<String>((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;
}

View file

@ -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<Handler> 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/<method> 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);
};
}

View file

@ -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();
}
}

View file

@ -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<shelf.Response> 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<shelf.Response> logScreenView(shelf.Request request) =>
notImplemented(request);
/// Return the value of the property.
FutureOr<shelf.Response> getCompleted(shelf.Request request, String value) =>
shelf.Response.ok('$value');
/// Return the value of the property after the property value has been set.
FutureOr<shelf.Response> 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<shelf.Response> 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<shelf.Response> notImplemented(shelf.Request request) =>
shelf.Response(HttpStatus.noContent);
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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()

View file

@ -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.

View file

@ -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",

View file

@ -43,6 +43,7 @@ bool _waitForDdsToAdvertiseService = false;
// HTTP server.
Server? server;
Future<Server>? 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<bool> 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<void>();
late StreamSubscription stderrSub;
stderrSub = _process!.stderr.transform(utf8.decoder).listen((event) {
final result = json.decode(event) as Map<String, dynamic>;
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,
);
});
}
}

View file

@ -26,9 +26,9 @@ class WebSocketClient extends Client {
socket.done.then((_) => close());
}
disconnect() {
Future<void> 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<void> disconnect() async {
await request.response.close();
close();
}

View file

@ -411,6 +411,16 @@ class VMService extends MessageRouter {
replyPort.send(bytes);
}
Future<void> 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();

View file

@ -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

View file

@ -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/",

View file

@ -57,6 +57,8 @@ void main(List<String> 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'),
];

View file

@ -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 = "{}"
},
]
}