@@ -14,6 +14,7 @@ import '../utils/auto_dispose.dart';
1414import 'connected_app.dart' ;
1515import 'constants.dart' ;
1616import 'isolate_manager.dart' ;
17+ import 'rpc_error_extension.dart' ;
1718import 'service_extensions.dart' as extensions;
1819import '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