Skip to content

Commit 495c66d

Browse files
authored
Do not mark service extensions as 'added' so eagerly (#9271)
1 parent e0925a8 commit 495c66d

File tree

7 files changed

+96
-55
lines changed

7 files changed

+96
-55
lines changed

packages/devtools_app/lib/src/screens/network/network_service.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
44

5+
import 'package:devtools_app_shared/service.dart';
56
import 'package:vm_service/vm_service.dart';
67

7-
import '../../service/vm_service_wrapper.dart';
88
import '../../shared/globals.dart';
99
import '../../shared/primitives/utils.dart';
1010
import '../../shared/utils/utils.dart';

packages/devtools_app/lib/src/service/vm_service_wrapper.dart

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -519,24 +519,3 @@ class TrackedFuture<T> {
519519
final String name;
520520
final Future<T> future;
521521
}
522-
523-
extension RpcErrorExtension on RPCError {
524-
/// Whether this [RPCError] is some kind of "VM Service connection has gone"
525-
/// error that may occur if the VM is shut down.
526-
bool get isServiceDisposedError {
527-
if (code == RPCErrorKind.kServiceDisappeared.code ||
528-
code == RPCErrorKind.kConnectionDisposed.code) {
529-
return true;
530-
}
531-
532-
if (code == RPCErrorKind.kServerError.code) {
533-
// Always ignore "client is closed" and "closed with pending request"
534-
// errors because these can always occur during shutdown if we were
535-
// just starting to send (or had just sent) a request.
536-
return message.contains('The client is closed') ||
537-
message.contains('The client closed with pending request') ||
538-
message.contains('Service connection dispose');
539-
}
540-
return false;
541-
}
542-
}

packages/devtools_app/lib/src/shared/diagnostics/inspector_service.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import 'package:flutter/cupertino.dart';
2222
import 'package:flutter/foundation.dart';
2323
import 'package:vm_service/vm_service.dart';
2424

25-
import '../../service/vm_service_wrapper.dart';
2625
import '../console/primitives/simple_items.dart';
2726
import '../globals.dart';
2827
import '../utils/utils.dart';

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ TODO: Remove this section if there are not any general updates.
3939

4040
## Network profiler updates
4141

42-
TODO: Remove this section if there are not any general updates.
42+
* Fixed network logging after a hot restart. -
43+
[#9271](https://github.com/flutter/devtools/pull/9271).
4344

4445
## Logging updates
4546

packages/devtools_app_shared/lib/service.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export 'src/service/flutter_version.dart';
1010
export 'src/service/isolate_manager.dart' hide TestIsolateManager;
1111
export 'src/service/isolate_state.dart';
1212
export 'src/service/resolved_uri_manager.dart';
13+
export 'src/service/rpc_error_extension.dart';
1314
export 'src/service/service_extension_manager.dart'
1415
hide TestServiceExtensionManager;
1516
export 'src/service/service_manager.dart';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
import 'package:vm_service/vm_service.dart';
6+
7+
extension RpcErrorExtension on RPCError {
8+
/// Whether this [RPCError] is some kind of "VM Service connection has gone"
9+
/// error that may occur if the VM is shut down.
10+
bool get isServiceDisposedError {
11+
if (code == RPCErrorKind.kServiceDisappeared.code ||
12+
code == RPCErrorKind.kConnectionDisposed.code) {
13+
return true;
14+
}
15+
16+
if (code == RPCErrorKind.kServerError.code) {
17+
// Always ignore "client is closed" and "closed with pending request"
18+
// errors because these can always occur during shutdown if we were
19+
// just starting to send (or had just sent) a request.
20+
return message.contains('The client is closed') ||
21+
message.contains('The client closed with pending request') ||
22+
message.contains('Service connection dispose');
23+
}
24+
return false;
25+
}
26+
}

packages/devtools_app_shared/lib/src/service/service_extension_manager.dart

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import '../utils/auto_dispose.dart';
1414
import 'connected_app.dart';
1515
import 'constants.dart';
1616
import 'isolate_manager.dart';
17+
import 'rpc_error_extension.dart';
1718
import 'service_extensions.dart' as extensions;
1819
import 'service_utils.dart';
1920

@@ -245,7 +246,7 @@ final class ServiceExtensionManager with DisposerMixin {
245246
}
246247

247248
Future<void> _addServiceExtension(String name) async {
248-
if (!_serviceExtensions.add(name)) {
249+
if (_serviceExtensions.contains(name)) {
249250
// If the service extension was already added we do not need to add it
250251
// again. This can happen depending on the timing between when extension
251252
// added events were received and when we requested the list of all
@@ -254,68 +255,94 @@ final class ServiceExtensionManager with DisposerMixin {
254255
}
255256
_hasServiceExtension(name).value = true;
256257

257-
if (_enabledServiceExtensions.containsKey(name)) {
258+
final enabledServiceExtension = _enabledServiceExtensions[name];
259+
if (enabledServiceExtension != null) {
258260
// Restore any previously enabled states by calling their service
259261
// extension. This will restore extension states on the device after a hot
260262
// restart. [_enabledServiceExtensions] will be empty on page refresh or
261263
// initial start.
262264
try {
263-
return await _callServiceExtension(
265+
final called = await _callServiceExtensionIfReady(
264266
name,
265-
_enabledServiceExtensions[name]!.value,
267+
enabledServiceExtension.value,
266268
);
269+
if (called) {
270+
// Only mark `name` as an "added service extension" if it was truly
271+
// added. If it was added, then subsequent calls to
272+
// `_addServiceExtension` with `name` will return early. If it was not
273+
// really added, then subsequent calls to `_addServiceExtension` with
274+
// `name` will proceed as usual.
275+
_serviceExtensions.add(name);
276+
}
277+
return;
267278
} on SentinelException catch (_) {
268279
// Service extension stopped existing while calling, so do nothing.
269280
// This typically happens during hot restarts.
270281
}
271282
} else {
272283
// Set any extensions that are already enabled on the device. This will
273284
// enable extension states in DevTools on page refresh or initial start.
274-
return await _restoreExtensionFromDevice(name);
285+
final restored = await _restoreExtensionFromDeviceIfReady(name);
286+
if (restored) {
287+
// Only mark `name` as an "added service extension" if it was truly
288+
// restored. If it was restored, then subsequent calls to
289+
// `_addServiceExtension` with `name` will return early. If it was not
290+
// really restored, then subsequent calls to `_addServiceExtension`
291+
// with `name` will proceed as usual.
292+
_serviceExtensions.add(name);
293+
}
275294
}
276295
}
277296

278297
IsolateRef? get _mainIsolate => _isolateManager.mainIsolate.value;
279298

280-
Future<void> _restoreExtensionFromDevice(String name) async {
299+
/// Restores the service extension named [name] from the device.
300+
///
301+
/// Returns whether isolates in the connected app are prepared for the restore.
302+
Future<bool> _restoreExtensionFromDeviceIfReady(String name) async {
281303
final isolateRef = _isolateManager.mainIsolate.value;
282-
if (isolateRef == null) return;
304+
if (isolateRef == null) return false;
283305

284306
if (!extensions.serviceExtensionsAllowlist.containsKey(name)) {
285-
return;
307+
return true;
286308
}
287309
final expectedValueType =
288310
extensions.serviceExtensionsAllowlist[name]!.values.first.runtimeType;
289311

290-
Future<void> restore() async {
312+
/// Restores the service extension named [name].
313+
///
314+
/// Returns whether isolates in the connected app are prepared for the
315+
/// restore.
316+
Future<bool> restore() async {
291317
// The restore request is obsolete if the isolate has changed.
292-
if (isolateRef != _mainIsolate) return;
318+
if (isolateRef != _mainIsolate) return false;
293319
try {
294320
final response = await _service!.callServiceExtension(
295321
name,
296322
isolateId: isolateRef.id,
297323
);
298324

299-
if (isolateRef != _mainIsolate) return;
325+
if (isolateRef != _mainIsolate) return false;
300326

301327
switch (expectedValueType) {
302328
case const (bool):
303329
final enabled = response.json!['enabled'] == 'true' ? true : false;
304330
await _maybeRestoreExtension(name, enabled);
305-
return;
306331
case const (String):
307332
final String? value = response.json!['value'];
308333
await _maybeRestoreExtension(name, value);
309-
return;
310334
case const (int):
311335
case const (double):
312336
final value = num.parse(
313337
response.json![name.substring(name.lastIndexOf('.') + 1)],
314338
);
315339
await _maybeRestoreExtension(name, value);
316-
return;
317340
default:
318-
return;
341+
return true;
342+
}
343+
} on RPCError catch (e) {
344+
if (e.isServiceDisposedError) {
345+
return false;
319346
}
320347
} catch (e) {
321348
// Do not report an error if the VMService has gone away or the
@@ -325,22 +352,25 @@ final class ServiceExtensionManager with DisposerMixin {
325352
// of allowed network related exceptions rather than ignoring all
326353
// exceptions.
327354
}
355+
return true;
328356
}
329357

330-
if (isolateRef != _mainIsolate) return;
358+
if (isolateRef != _mainIsolate) return false;
331359

332360
final isolate = await _isolateManager.isolateState(isolateRef).isolate;
333-
if (isolateRef != _mainIsolate) return;
361+
if (isolateRef != _mainIsolate) return false;
334362

335363
// Do not try to restore Dart IO extensions for a paused isolate.
336364
if (extensions.isDartIoExtension(name) &&
337365
isolate?.pauseEvent?.kind?.contains('Pause') == true) {
338366
_callbacksOnIsolateResume.putIfAbsent(isolateRef, () => []).add(restore);
367+
return true;
339368
} else {
340-
await restore();
369+
return await restore();
341370
}
342371
}
343372

373+
/// Maybe restores the service extension named [name] with [value].
344374
Future<void> _maybeRestoreExtension(String name, Object? value) async {
345375
final extensionDescription = extensions.serviceExtensionsAllowlist[name];
346376
if (extensionDescription is extensions.ToggleableServiceExtension) {
@@ -362,14 +392,15 @@ final class ServiceExtensionManager with DisposerMixin {
362392
}
363393
}
364394

365-
Future<void> _callServiceExtension(String name, Object? value) async {
366-
if (_service == null) {
367-
return;
368-
}
395+
/// Calls the service extension named [name] with [value].
396+
///
397+
/// Returns whether isolates in the connected app are prepared for the call.
398+
Future<bool> _callServiceExtensionIfReady(String name, Object? value) async {
399+
if (_service == null) return false;
369400

370-
final mainIsolate = _isolateManager.mainIsolate.value;
371-
Future<void> callExtension() async {
372-
if (_isolateManager.mainIsolate.value != mainIsolate) return;
401+
final mainIsolate = _mainIsolate;
402+
Future<bool> callExtension() async {
403+
if (_mainIsolate != mainIsolate) return false;
373404

374405
assert(value != null);
375406
try {
@@ -411,25 +442,29 @@ final class ServiceExtensionManager with DisposerMixin {
411442
}
412443
} on RPCError catch (e) {
413444
if (e.code == RPCErrorKind.kServerError.code) {
414-
// Connection disappeared
415-
return;
445+
// The connection disappeared.
446+
return false;
416447
}
417448
rethrow;
418449
}
450+
451+
return true;
419452
}
420453

421-
if (mainIsolate == null) return;
454+
if (mainIsolate == null) return false;
455+
422456
final isolate = await _isolateManager.isolateState(mainIsolate).isolate;
423-
if (_isolateManager.mainIsolate.value != mainIsolate) return;
457+
if (_mainIsolate != mainIsolate) return false;
424458

425459
// Do not try to call Dart IO extensions for a paused isolate.
426460
if (extensions.isDartIoExtension(name) &&
427461
isolate?.pauseEvent?.kind?.contains('Pause') == true) {
428462
_callbacksOnIsolateResume
429463
.putIfAbsent(mainIsolate, () => [])
430464
.add(callExtension);
465+
return true;
431466
} else {
432-
await callExtension();
467+
return await callExtension();
433468
}
434469
}
435470

@@ -488,7 +523,7 @@ final class ServiceExtensionManager with DisposerMixin {
488523
bool callExtension = true,
489524
}) async {
490525
if (callExtension && _serviceExtensions.contains(name)) {
491-
await _callServiceExtension(name, value);
526+
await _callServiceExtensionIfReady(name, value);
492527
} else if (callExtension) {
493528
_log.info(
494529
'Attempted to call extension \'$name\', but no service with that name exists',

0 commit comments

Comments
 (0)