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 @@ -377,10 +377,10 @@ class NetworkController extends DevToolsScreenController

/// Enables HTTP timeline logging and socket profiling on all isolates.
Future<void> _enableNetworkTrafficRecordingOnAllIsolates() async {
await [
await Future.wait([
http_service.toggleHttpRequestLogging(true),
networkService.toggleSocketProfiling(true),
].wait;
]);
}

Future<void> stopRecording() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,26 @@ void main() {
},
);

test(
'refreshNetworkData swallows service disappeared error during isolate iteration',
() async {
final localVmService = HotRestartNetworkVmService();
final localConnection = FakeServiceConnectionManager(
service: localVmService,
);
final localController = await initNetworkLifecycleController(
vmService: localVmService,
fakeServiceConnection: localConnection,
);

localVmService.simulateServiceDisappeared = true;
await expectLater(
localController.networkService.refreshNetworkData(),
completes,
);
},
);

test(
'updateLastHttpDataRefreshTime does not add entries for new isolates',
() async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ class HotRestartNetworkVmService extends FakeVmServiceWrapper {
return Success();
}

/// When true, [getSocketProfileWrapper] throws an [RPCError] with
/// [RPCErrorKind.kServiceDisappeared], simulating service shutdown.
bool simulateServiceDisappeared = false;

@override
Future<void> forEachIsolate(Future<void> Function(IsolateRef) callback) =>
callback(IsolateRef.parse({'id': _currentIsolateId, 'name': 'main'})!);
Expand Down Expand Up @@ -173,6 +177,13 @@ class HotRestartNetworkVmService extends FakeVmServiceWrapper {

@override
Future<SocketProfile> getSocketProfileWrapper(String isolateId) async {
if (simulateServiceDisappeared) {
throw RPCError(
'ext.dart.io.getSocketProfile',
RPCErrorKind.kServiceDisappeared.code,
'Service has disappeared',
);
}
if (!(_socketProfilingEnabled[isolateId] ?? false)) {
return SocketProfile(sockets: []);
}
Expand Down
8 changes: 6 additions & 2 deletions packages/devtools_app_shared/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
-->
## 0.5.2-wip
* Fix `forEachIsolateHelper` to rethrow isolate callback exceptions directly
using `Future.wait` instead of wrapping them in a `ParallelWaitError`.
* Fix a `RangeError` thrown by `SplitPane` when the number of children
changes between rebuilds.
* Fix garbage collection and timeout issues with the result list in `asyncEval` on both native VM and web.
* Safely handle RPC errors and unexpected exceptions when calling service extensions in `ServiceExtensionManager`.
* Fix garbage collection and timeout issues with the result list in `asyncEval`
on both native VM and web.
* Safely handle RPC errors and unexpected exceptions when calling service
extensions in `ServiceExtensionManager`.
* The minimum Dart SDK version is bumped to 3.11.0.
* The minimum Flutter SDK version is bumped to 3.41.0.
* Fix `RootInfo.package` to be `null` when the root library is not a `package:` URI,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ Future<void> forEachIsolateHelper(
) async {
final vm = await vmService.getVM();
if (vm.isolates case final vmIsolates? when vmIsolates.isNotEmpty) {
await [for (final isolate in vmIsolates) callback(isolate)].wait;
await Future.wait([for (final isolate in vmIsolates) callback(isolate)]);
}
}
110 changes: 110 additions & 0 deletions packages/devtools_app_shared/test/service/service_utils_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2026 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'package:devtools_app_shared/service.dart' hide SentinelException;
import 'package:flutter_test/flutter_test.dart';
import 'package:vm_service/vm_service.dart';

void main() {
group('forEachIsolateHelper', () {
test('calls callback for each isolate', () async {
final isolates = [
IsolateRef(id: 'isolate_1', number: '1', name: 'main_1'),
IsolateRef(id: 'isolate_2', number: '2', name: 'main_2'),
];
final fakeService = _FakeVmService(isolates: isolates);
final visited = <String>[];
await forEachIsolateHelper(fakeService, (isolate) async {
visited.add(isolate.id!);
});
expect(visited, equals(['isolate_1', 'isolate_2']));
});

test('is a no-op when isolates list is empty or null', () async {
final emptyService = _FakeVmService(isolates: []);
var called = false;
await forEachIsolateHelper(emptyService, (isolate) async {
called = true;
});
expect(called, isFalse);

final nullService = _FakeVmService(isolates: null);
await forEachIsolateHelper(nullService, (isolate) async {
called = true;
});
expect(called, isFalse);
});

test(
'rethrows RPCError directly without wrapping in ParallelWaitError',
() async {
final isolates = [
IsolateRef(id: 'isolate_1', number: '1', name: 'main_1'),
IsolateRef(id: 'isolate_2', number: '2', name: 'main_2'),
];
final fakeService = _FakeVmService(isolates: isolates);
final error = RPCError(
'ext.dart.io.getSocketProfile',
RPCErrorKind.kServiceDisappeared.code,
'Service has disappeared',
);

await expectLater(
() => forEachIsolateHelper(
fakeService,
(isolate) => Future<void>.error(error),
),
throwsA(same(error)),
);
},
);

test(
'rethrows SentinelException directly without wrapping in ParallelWaitError',
() async {
final isolates = [
IsolateRef(id: 'isolate_1', number: '1', name: 'main_1'),
];
final fakeService = _FakeVmService(isolates: isolates);
final sentinel = SentinelException.parse('getIsolate', {
'type': 'Sentinel',
'kind': 'Collected',
'valueAsString': '<collected>',
});

await expectLater(
() => forEachIsolateHelper(
fakeService,
(isolate) => Future<void>.error(sentinel),
),
throwsA(same(sentinel)),
);
},
);

test(
'VmServiceExtension.forEachIsolate delegates to forEachIsolateHelper',
() async {
final isolates = [
IsolateRef(id: 'isolate_1', number: '1', name: 'main_1'),
];
final fakeService = _FakeVmService(isolates: isolates);
final visited = <String>[];
await fakeService.forEachIsolate((isolate) async {
visited.add(isolate.id!);
});
expect(visited, equals(['isolate_1']));
},
);
});
}

class _FakeVmService extends Fake implements VmService {
_FakeVmService({required this.isolates});

final List<IsolateRef>? isolates;

@override
Future<VM> getVM() async => VM(isolates: isolates);
}
Loading