Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ class ExtensionService extends DisposableController
serviceConnection.serviceManager.connectedState.value.connected &&
serviceConnection.serviceManager.isolateManager.mainIsolate.value !=
null) {
_appRoot = await _connectedAppRoot();
_appRoot = await serviceConnection.connectedAppPackageRoot();
}

// TODO(kenz): gracefully handle app connections / disconnects when there
Expand Down Expand Up @@ -358,13 +358,6 @@ class ExtensionService extends DisposableController
}
}

Future<Uri?> _connectedAppRoot() async {
final packageUriString =
await serviceConnection.rootPackageDirectoryForMainIsolate();
if (packageUriString == null) return null;
return Uri.parse(packageUriString);
}

/// Compares the versions of extension configurations [a] and [b] and returns
/// the extension configuration with the latest version, following semantic
/// versioning rules.
Expand Down
97 changes: 79 additions & 18 deletions packages/devtools_app/lib/src/service/service_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:devtools_app_shared/service.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_app_shared/service.dart' hide SentinelException;
import 'package:devtools_shared/devtools_shared.dart';
import 'package:logging/logging.dart';
import 'package:vm_service/vm_service.dart' hide Error;
Expand Down Expand Up @@ -36,10 +35,6 @@ const debugLogServiceProtocolEvents = false;

const defaultRefreshRate = 60.0;

/// The amount of time we will wait for the main isolate to become non-null when
/// calling [ServiceConnectionManager.rootLibraryForMainIsolate].
const _waitForRootLibraryTimeout = Duration(seconds: 3);

class ServiceConnectionManager {
ServiceConnectionManager() {
serviceManager = ServiceManager()
Expand Down Expand Up @@ -180,22 +175,88 @@ class ServiceConnectionManager {
await serviceManager.isolateManager.init(isolates);
}

/// Returns the package root URI for the connected app.
///
/// This should be the directory up the tree from the debugging target that
/// contains the .dart_tool/package_config.json file.
///
/// This method contains special logic for detecting the package root for
/// test targets (i.e., a VM service connections spawned from `dart test` or
/// `flutter test`). This is because the main isolate for test targets is
/// running the test runner, and not the test library itself, so we have to do
/// some extra work to find the package root of the test target.
Future<Uri?> connectedAppPackageRoot() async {
var packageRootUriString = await rootPackageDirectoryForMainIsolate();
_log.fine(
'[connectedAppPackageRoot] root package directory for main isolate: '
'$packageRootUriString',
);

// If a Dart library URI was returned, this may be a test target (i.e. a
// VM service connection spawned from `dart test` or `flutter test`).
if (packageRootUriString?.endsWith('.dart') ?? false) {
final rootLibrary = await _mainIsolateRootLibrary();
final testTargetFileUriString =
(rootLibrary?.dependencies ?? <LibraryDependency>[])
.firstWhereOrNull((dep) => dep.prefix == 'test')
?.target
?.uri;
if (testTargetFileUriString != null) {
_log.fine(
'[connectedAppPackageRoot] detected test library from root library '
'imports: $testTargetFileUriString',
);
packageRootUriString = await packageRootFromFileUriString(
testTargetFileUriString,
dtd: dtdManager.connection.value,
);
_log.fine(
'[connectedAppPackageRoot] package root for test target: '
'$packageRootUriString',
);
}
}

return packageRootUriString == null
? null
: Uri.parse(packageRootUriString);
}

Future<Library?> _mainIsolateRootLibrary() async {
final ref = (await serviceManager.isolateManager.waitForMainIsolateState())
?.isolateNow
?.rootLib;
if (ref == null) return null;
try {
final library = await serviceManager.service!.getObject(
serviceManager.isolateManager.mainIsolate.value!.id!,
ref.id!,
);
assert(library is Library);
return library as Library;
} on SentinelException catch (_) {
// Fail gracefully if the request to get the Library object fails.
return null;
}
}

// TODO(kenz): consider caching this value for the duration of the VM service
// connection.
Future<String?> rootLibraryForMainIsolate() async {
final mainIsolateRef = await whenValueNonNull(
serviceManager.isolateManager.mainIsolate,
timeout: _waitForRootLibraryTimeout,
);
if (mainIsolateRef == null) return null;
/// Returns a file URI String for the root library of the connected app's main
/// isolate.
///
/// If a non-null value is returned, the value will be a file URI String and
/// it will NOT have a trailing slash.
Future<String?> mainIsolateRootLibraryUriAsString() async {
final mainIsolateState =
await serviceManager.isolateManager.waitForMainIsolateState();
if (mainIsolateState == null) return null;

final isolateState =
serviceManager.isolateManager.isolateState(mainIsolateRef);
await isolateState.waitForIsolateLoad();
final rootLib = isolateState.rootInfo?.library;
final rootLib = mainIsolateState.rootInfo?.library;
if (rootLib == null) return null;

final selectedIsolateRefId = mainIsolateRef.id!;
final selectedIsolateRefId =
serviceManager.isolateManager.mainIsolate.value!.id!;
await serviceManager.resolvedUriManager
.fetchFileUris(selectedIsolateRefId, [rootLib]);
final fileUriString = serviceManager.resolvedUriManager.lookupFileUri(
Expand All @@ -213,7 +274,7 @@ class ServiceConnectionManager {
/// If a non-null value is returned, the value will be a file URI String and
/// it will NOT have a trailing slash.
Future<String?> rootPackageDirectoryForMainIsolate() async {
final fileUriString = await serviceConnection.rootLibraryForMainIsolate();
final fileUriString = await mainIsolateRootLibraryUriAsString();
final packageUriString = fileUriString != null
? await packageRootFromFileUriString(
fileUriString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ class InspectorPreferencesController extends DisposableController
/// directories are for the current project so we make a best guess based on
/// the root library for the main isolate.
Future<String?> _inferPubRootDirectory() async {
final path = await serviceConnection.rootLibraryForMainIsolate();
if (path == null) {
final fileUriString =
await serviceConnection.mainIsolateRootLibraryUriAsString();
if (fileUriString == null) {
return null;
}
// TODO(jacobr): Once https://github.com/flutter/flutter/issues/26615 is
Expand All @@ -222,15 +223,15 @@ class InspectorPreferencesController extends DisposableController
// TODO(jacobr): use the list of loaded scripts to determine the appropriate
// package root directory given that the root script of this project is in
// this directory rather than guessing based on url structure.
final parts = path.split('/');
final parts = fileUriString.split('/');
String? pubRootDirectory;
// For google3, we grab the top-level directory in the google3 directory
// (e.g. /education), or the top-level directory in third_party (e.g.
// /third_party/dart):
if (isGoogle3Path(parts)) {
pubRootDirectory = _pubRootDirectoryForGoogle3(parts);
} else {
final parts = path.split('/');
final parts = fileUriString.split('/');

for (int i = parts.length - 1; i >= 0; i--) {
final part = parts[i];
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools_app/lib/src/shared/server/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ void logWarning(Response? response, String apiType) {
final respText = response?.body;
_log.warning(
'HttpRequest $apiType failed status = ${response?.statusCode}'
'${respText != null ? ', responseText = $respText' : ''}',
'${respText.isNullOrEmpty ? '' : ', responseText = $respText'}',
);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ TODO: Remove this section if there are not any general updates.

## DevTools Extension updates

* Fix an issue with detecting extensions for Dart or Flutter
tests. - [#7717](https://github.com/flutter/devtools/pull/7717)
* Fix an issue with detecting extensions for nested Dart or Flutter
projects. - [#7742](https://github.com/flutter/devtools/pull/7742)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ void main() {
isTrue,
);
final rootLibrary =
await serviceConnection.rootLibraryForMainIsolate();
await serviceConnection.mainIsolateRootLibraryUriAsString();
await inspectorServiceLocal.addPubRootDirectories([rootLibrary!]);
final List<String> rootDirectories =
await inspectorServiceLocal.getPubRootDirectories() ?? [];
Expand Down
1 change: 1 addition & 0 deletions packages/devtools_app_shared/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Fix a race condition during service manager disconnect.
* Add `IdeThemeQueryParams` extension type for parsing query params.
* Add `EmbedMode` to enumerate the possible DevTools embedded states.
* Add `IsolateManager.waitForMainIsolateState` method.

## 0.1.1
* Update `package:dtd` to `^2.1.0`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:vm_service/vm_service.dart' hide Error;

import '../utils/auto_dispose.dart';
import '../utils/list.dart';
import '../utils/utils.dart';
import 'isolate_state.dart';
import 'service_extensions.dart' as extensions;

Expand All @@ -24,6 +25,10 @@ base mixin TestIsolateManager implements IsolateManager {}
final class IsolateManager with DisposerMixin {
final _isolateStates = <IsolateRef, IsolateState>{};

/// The amount of time we will wait for the main isolate to become non-null
/// when calling [waitForMainIsolateState].
static const _waitForMainIsolateStateTimeout = Duration(seconds: 3);

/// Signifies whether the main isolate should be selected if it is started.
///
/// This is used to make sure the the main isolate remains selected after
Expand Down Expand Up @@ -65,6 +70,17 @@ final class IsolateManager with DisposerMixin {
: null;
}

Future<IsolateState?> waitForMainIsolateState() async {
final mainIsolateRef = await whenValueNonNull<IsolateRef?>(
mainIsolate,
timeout: _waitForMainIsolateStateTimeout,
);
if (mainIsolateRef == null) return null;
final state = mainIsolateState;
await state?.waitForIsolateLoad();
return state;
}

/// Return a unique, monotonically increasing number for this Isolate.
int? isolateIndex(IsolateRef isolateRef) {
if (!_isolateIndexMap.containsKey(isolateRef.id)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/devtools_extensions/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* Bump `vm_service` dependency to `^14.2.1`.
* Dispose `DTDManager` when the `DevToolsExtension` widget state is disposed.
* Add an example of a standalone extension.
* Add examples of Dart and Flutter tests that can be ran and connected to
available DevTools extensions.

## 0.1.1
* Update the simulated environment help dialogs with information about the
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2024 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.

import 'package:app_that_uses_foo/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:foo/foo.dart';

// This test can be run to verify that the DevTools extensions available for
// package:app_that_uses_foo load properly when debugging a Flutter test target
// with DevTools.
//
// To test this, run the following command and copy the VM service URI to
// connect to DevTools:
//
// flutter test test/flutter_test_1.dart --start-paused
//
// To test this test as part of a suite, use this command instead:
//
// flutter test test/ --start-paused


void main() {
testWidgets('Builds $MyAppThatUsesFoo', (tester) async {
await tester.pumpWidget(const MyAppThatUsesFoo());
await tester.pumpAndSettle();

expect(find.byType(MyAppThatUsesFoo), findsOneWidget);
expect(find.byType(FooWidget), findsOneWidget);
});
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Copyright 2024 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.

import 'package:app_that_uses_foo/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:foo/foo.dart';

// This test can be run to verify that the `package:foo` DevTools extension
// loads properly when debugging a test target with DevTools.
// This test can be run to verify that the DevTools extensions available for
// package:app_that_uses_foo load properly when debugging a Flutter test target
// with DevTools.
//
// To test this, run the following command and copy the VM service URI to
// connect to DevTools:
//
// flutter run test/app_that_uses_foo_test.dart --start-paused -d flutter-tester
// flutter test test/flutter_test_2.dart --start-paused
//
// To test this test as part of a suite, use this command instead:
//
// flutter test test/ --start-paused

void main() {
testWidgets('Builds $MyAppThatUsesFoo', (tester) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@
import 'package:dart_foo/dart_foo.dart';
import 'package:test/test.dart';

// This test can be run to verify that the `package:foo` DevTools extension
// loads properly when debugging a Dart test target with DevTools.
// This test can be run to verify that the DevTools extensions available for
// package:app_that_uses_foo load properly when debugging a Dart test target
// with DevTools.
//
// This doubles as a test to make sure the DevTools extension loads properly
// when the test target is in a subdirectory of 'test/'.
//
// To test this, run the following command and copy the VM service URI to
// connect to DevTools:
//
// dart run test/dart_test/example_dart_test.dart --start-paused
// dart test test/nested/dart_test_1.dart --pause-after-load
//
// To test this test as part of a suite, use this command instead:
//
// dart test test/nested/ --pause-after-load

void main() {
test('a simple dart test', () {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2024 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.

import 'package:dart_foo/dart_foo.dart';
import 'package:test/test.dart';

// This test can be run to verify that the DevTools extensions available for
// package:app_that_uses_foo load properly when debugging a Dart test target
// with DevTools.
//
// This doubles as a test to make sure the DevTools extension loads properly
// when the test target is in a subdirectory of 'test/'.
//
// To test this, run the following command and copy the VM service URI to
// connect to DevTools:
//
// dart test test/nested/dart_test_2.dart --pause-after-load
//
// To test this test as part of a suite, use this command instead:
//
// dart test test/nested/ --pause-after-load

void main() {
test('a simple dart test', () {
final dartFoo = DartFoo();
dartFoo.foo();
expect(1 + 1, 2);
expect(1 + 2, 3);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class FakeServiceConnectionManager extends Fake
}

@override
Future<String?> rootLibraryForMainIsolate() {
Future<String?> mainIsolateRootLibraryUriAsString() {
final fakeIsolateManager =
_serviceManager.isolateManager as FakeIsolateManager;
return Future.value(fakeIsolateManager.rootLibrary);
Expand Down