From f98b5cf2636cb9e64c625a0c87a47d349b552970 Mon Sep 17 00:00:00 2001 From: Anna Gringauze Date: Tue, 17 Aug 2021 12:06:46 -0700 Subject: [PATCH 1/7] Fix hot restart and breakpoints races Multiple breakpoints restored ad the isolate start can cause the app to crash in chrome, serialize them using a mutex to prevent the crash. Hot restart runs main immediately, followed by createIsolate that restores breakpoints, sometimes too late, causing non- removable breakpoints and failures in the UI. Make the dwds run main after createIsolate is done, to make sure breakpoints are all set before the run. Hot restart tries to resume the exiting isolate, and checks if it resumed before that, which causes various races or chrome requiring to be in focus, depending on the way the check is done. Remove the check and just catch exception in case the isolate is already running. --- dwds/lib/src/connections/app_connection.dart | 6 ++- dwds/lib/src/debugging/debugger.dart | 35 ++++++++++++--- dwds/lib/src/debugging/remote_debugger.dart | 6 +++ dwds/lib/src/debugging/webkit_debugger.dart | 5 +++ dwds/lib/src/dwds_vm_client.dart | 38 +++++++++------- dwds/lib/src/handlers/dev_handler.dart | 7 +++ dwds/lib/src/injected/client.js | 1 - dwds/lib/src/servers/extension_debugger.dart | 5 +++ dwds/lib/src/utilities/mutex.dart | 46 ++++++++++++++++++++ dwds/test/fixtures/fakes.dart | 4 ++ dwds/web/reloader/require_restarter.dart | 2 - 11 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 dwds/lib/src/utilities/mutex.dart diff --git a/dwds/lib/src/connections/app_connection.dart b/dwds/lib/src/connections/app_connection.dart index 04669c312..90e4d6b12 100644 --- a/dwds/lib/src/connections/app_connection.dart +++ b/dwds/lib/src/connections/app_connection.dart @@ -16,7 +16,7 @@ import '../handlers/socket_connections.dart'; class AppConnection { /// The initial connection request sent from the application in the browser. final ConnectRequest request; - final _startedCompleter = Completer(); + var _startedCompleter = Completer(); final SocketConnection _connection; AppConnection(this.request, this._connection); @@ -33,4 +33,8 @@ class AppConnection { _connection.sink.add(jsonEncode(serializers.serialize(RunRequest()))); _startedCompleter.complete(); } + + void reset() { + _startedCompleter = Completer(); + } } diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart index b230d613b..654eb16d2 100644 --- a/dwds/lib/src/debugging/debugger.dart +++ b/dwds/lib/src/debugging/debugger.dart @@ -18,6 +18,7 @@ import '../services/chrome_proxy_service.dart'; import '../utilities/conversions.dart'; import '../utilities/dart_uri.dart'; import '../utilities/domain.dart'; +import '../utilities/mutex.dart'; import '../utilities/objects.dart' show Property; import '../utilities/shared.dart'; import 'dart_scope.dart'; @@ -187,6 +188,7 @@ class Debugger extends Domain { runZonedGuarded(() { _remoteDebugger?.onPaused?.listen(_pauseHandler); _remoteDebugger?.onResumed?.listen(_resumeHandler); + _remoteDebugger?.onTargetCrashed?.listen(_crashHandler); }, (e, StackTrace s) { logger.warning('Error handling Chrome event', e, s); }); @@ -580,6 +582,23 @@ class Debugger extends Domain { _streamNotify('Debug', event); } + /// Handles targetCrashed events coming from the Chrome connection. + Future _crashHandler(TargetCrashedEvent _) async { + // We can receive a resume event in the middle of a reload which will result + // in a null isolate. + var isolate = inspector?.isolate; + if (isolate == null) return; + + stackComputer = null; + var event = Event( + kind: EventKind.kIsolateExit, + timestamp: DateTime.now().millisecondsSinceEpoch, + isolate: inspector.isolateRef); + isolate.pauseEvent = event; + _streamNotify('Isolate', event); + logger.severe('Target crashed!'); + } + /// Evaluate [expression] by calling Chrome's Runtime.evaluate Future evaluate(String expression) async { try { @@ -715,6 +734,8 @@ class _Breakpoints extends Domain { return breakpoint; } + Mutex breakpointMutex = Mutex(); + /// Calls the Chrome protocol setBreakpoint and returns the remote ID. Future _setJsBreakpoint(Location location) async { // Location is 0 based according to: @@ -722,12 +743,16 @@ class _Breakpoints extends Domain { // The module can be loaded from a nested path and contain an ETAG suffix. var urlRegex = '.*${location.jsLocation.module}.*'; - var response = await remoteDebugger - .sendCommand('Debugger.setBreakpointByUrl', params: { - 'urlRegex': urlRegex, - 'lineNumber': location.jsLocation.line - 1, + // Prevent `Aww, snap!` errors when setting multiple breakpoints + // simultaneously by serializing the requests. + return breakpointMutex.runGuarded(() async { + var response = await remoteDebugger + .sendCommand('Debugger.setBreakpointByUrl', params: { + 'urlRegex': urlRegex, + 'lineNumber': location.jsLocation.line - 1, + }); + return response.result['breakpointId'] as String; }); - return response.result['breakpointId'] as String; } /// Records the internal Dart <=> JS breakpoint id mapping and adds the diff --git a/dwds/lib/src/debugging/remote_debugger.dart b/dwds/lib/src/debugging/remote_debugger.dart index 59671293f..6520c8e27 100644 --- a/dwds/lib/src/debugging/remote_debugger.dart +++ b/dwds/lib/src/debugging/remote_debugger.dart @@ -6,6 +6,10 @@ import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; +class TargetCrashedEvent extends WipEvent { + TargetCrashedEvent(Map json) : super(json); +} + /// A generic debugger used in remote debugging. abstract class RemoteDebugger { Stream get onConsoleAPICalled; @@ -20,6 +24,8 @@ abstract class RemoteDebugger { Stream get onScriptParsed; + Stream get onTargetCrashed; + Map get scripts; Stream get onClose; diff --git a/dwds/lib/src/debugging/webkit_debugger.dart b/dwds/lib/src/debugging/webkit_debugger.dart index 7e981cda7..e9b474b04 100644 --- a/dwds/lib/src/debugging/webkit_debugger.dart +++ b/dwds/lib/src/debugging/webkit_debugger.dart @@ -112,6 +112,11 @@ class WebkitDebugger implements RemoteDebugger { @override Stream get onScriptParsed => _wipDebugger.onScriptParsed; + @override + Stream get onTargetCrashed => _wipDebugger.eventStream( + 'Inspector.targetCrashed', + (WipEvent event) => TargetCrashedEvent(event.json)); + @override Map get scripts => _wipDebugger.scripts; diff --git a/dwds/lib/src/dwds_vm_client.dart b/dwds/lib/src/dwds_vm_client.dart index 558f8e90b..82b777369 100644 --- a/dwds/lib/src/dwds_vm_client.dart +++ b/dwds/lib/src/dwds_vm_client.dart @@ -194,22 +194,28 @@ Future _disableBreakpointsAndResume( if (vm.isolates.isEmpty) throw StateError('No active isolate to resume.'); var isolateRef = vm.isolates.first; - // Pause the app to prevent it from hitting a breakpoint - // during hot restart and stalling hot restart execution. - // Then wait for the app to pause or to hit a breakpoint. - var debug = chromeProxyService.onEvent('Debug').firstWhere((event) => - event.kind == EventKind.kPauseInterrupted || - event.kind == EventKind.kPauseBreakpoint); - - await client.pause(isolateRef.id); - - var isolate = await client.getIsolate(isolateRef.id); - if (isolate.pauseEvent.kind != EventKind.kPauseInterrupted && - isolate.pauseEvent.kind != EventKind.kPauseBreakpoint) { - await debug; - } - await chromeProxyService.disableBreakpoints(); - await client.resume(isolateRef.id); + try { + // Any checks for paused status result in race conditions or hangs + // at this point: + // + // - `getIsolate()` and check for status: + // the app migth still pause on existing breakpoint. + // + // - `pause()` and wait for `Debug.paused` event: + // chrome does not send the `Debug.Paused `notification + // without shifting focus to chrome. + // + // Instead, just try resuming and + // ignore failures indicating that the app is already running: + // + // WipError -32000 Can only perform operation while paused. + await client.resume(isolateRef.id); + } on RPCError catch (e, s) { + if (!e.message.contains('Can only perform operation while paused')) { + _logger.severe('Hot restart failed to resume exiting isolate', e, s); + rethrow; + } + } _logger.info('Successfully disabled breakpoints and resumed the isolate'); } diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart index 2a036ee18..8b9af0057 100644 --- a/dwds/lib/src/handlers/dev_handler.dart +++ b/dwds/lib/src/handlers/dev_handler.dart @@ -142,6 +142,9 @@ class DevHandler { tabConnection.onReceive.listen((message) { _log(' wip', '<== $message'); }); + tabConnection.onNotification.listen((message) { + _log(' wip', '<== $message'); + }); } var contextIds = tabConnection.runtime.onExecutionContextCreated .map((context) => context.id) @@ -404,9 +407,13 @@ class DevHandler { Future _handleIsolateStart( AppConnection appConnection, SocketConnection sseConnection) async { + // App is not running yet, reset `isStarted` state. + appConnection.reset(); await _servicesByAppId[appConnection.request.appId] ?.chromeProxyService ?.createIsolate(appConnection); + // Now run. + appConnection.runMain(); } void _listen() async { diff --git a/dwds/lib/src/injected/client.js b/dwds/lib/src/injected/client.js index 10bd83d76..2f5a577f0 100644 --- a/dwds/lib/src/injected/client.js +++ b/dwds/lib/src/injected/client.js @@ -24358,7 +24358,6 @@ case 8: // join self.$loadModuleConfig.call$1("dart_sdk").dart.hotRestart(); - V.runMain(); $async$returnValue = result; // goto return $async$goto = 1; diff --git a/dwds/lib/src/servers/extension_debugger.dart b/dwds/lib/src/servers/extension_debugger.dart index 7bb069e5e..941fca290 100644 --- a/dwds/lib/src/servers/extension_debugger.dart +++ b/dwds/lib/src/servers/extension_debugger.dart @@ -279,6 +279,11 @@ class ExtensionDebugger implements RemoteDebugger { 'Debugger.scriptParsed', (WipEvent event) => ScriptParsedEvent(event.json)); + @override + Stream get onTargetCrashed => eventStream( + 'Inspector.targetCrashed', + (WipEvent event) => TargetCrashedEvent(event.json)); + @override Map get scripts => UnmodifiableMapView(_scripts); diff --git a/dwds/lib/src/utilities/mutex.dart b/dwds/lib/src/utilities/mutex.dart new file mode 100644 index 000000000..b697e78e0 --- /dev/null +++ b/dwds/lib/src/utilities/mutex.dart @@ -0,0 +1,46 @@ +// 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. + +import 'dart:async'; +import 'dart:collection'; + +/// Used to protect global state accessed in blocks containing calls to +/// asynchronous methods. +class Mutex { + /// Executes a block of code containing asynchronous calls atomically. + /// + /// If no other asynchronous context is currently executing within + /// [criticalSection], it will immediately be called. Otherwise, the caller + /// will be suspended and entered into a queue to be resumed once the lock is + /// released. + Future runGuarded(FutureOr Function() criticalSection) async { + try { + await _acquireLock(); + return await criticalSection(); + } finally { + _releaseLock(); + } + } + + Future _acquireLock() async { + if (!_locked) { + _locked = true; + return; + } + final request = Completer(); + _outstandingRequests.add(request); + await request.future; + } + + void _releaseLock() { + _locked = false; + if (_outstandingRequests.isNotEmpty) { + final request = _outstandingRequests.removeFirst(); + request.complete(); + } + } + + bool _locked = false; + final _outstandingRequests = Queue>(); +} diff --git a/dwds/test/fixtures/fakes.dart b/dwds/test/fixtures/fakes.dart index cc9ab3da4..36227f6fc 100644 --- a/dwds/test/fixtures/fakes.dart +++ b/dwds/test/fixtures/fakes.dart @@ -11,6 +11,7 @@ import 'package:dwds/src/debugging/execution_context.dart'; import 'package:dwds/src/debugging/inspector.dart'; import 'package:dwds/src/debugging/instance.dart'; import 'package:dwds/src/debugging/modules.dart'; +import 'package:dwds/src/debugging/remote_debugger.dart'; import 'package:dwds/src/debugging/webkit_debugger.dart'; import 'package:dwds/src/loaders/strategy.dart'; import 'package:dwds/src/utilities/domain.dart'; @@ -174,6 +175,9 @@ class FakeWebkitDebugger implements WebkitDebugger { @override Stream get onScriptParsed => null; + @override + Stream get onTargetCrashed => null; + @override Future pause() => null; diff --git a/dwds/web/reloader/require_restarter.dart b/dwds/web/reloader/require_restarter.dart index a69b90a9e..86be8c2b0 100644 --- a/dwds/web/reloader/require_restarter.dart +++ b/dwds/web/reloader/require_restarter.dart @@ -17,7 +17,6 @@ import 'package:js/js.dart'; import 'package:js/js_util.dart'; import '../promise.dart'; -import '../run_main.dart'; import 'restarter.dart'; /// The last known digests of all the modules in the application. @@ -129,7 +128,6 @@ class RequireRestarter implements Restarter { } callMethod(getProperty(require('dart_sdk'), 'dart'), 'hotRestart', []); - runMain(); return result; } From 937dfab4794108e875f5f5174f066256d9c74671 Mon Sep 17 00:00:00 2001 From: Anna Gringauze Date: Tue, 17 Aug 2021 13:27:20 -0700 Subject: [PATCH 2/7] Use local dwds in webdev, update changelog --- dwds/CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index dea20660a..10c22c285 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,3 +1,12 @@ +## 11.2.3-dev + +- Fix race conditions: + - Intermittent hot restart failures to restore breakpoints. + - Intermittent `Aww, snap` errors on starting debugger with multiple + breakpoints in source. + - Needing chrome to be focus in order to wait for the isolate to + exit on hot restart. + ## 11.2.2 - Depend on `dds` version `2.1.1`. From 1a2aa22a1bdee24ceae52af6e100d486d6797de7 Mon Sep 17 00:00:00 2001 From: Anna Gringauze Date: Wed, 18 Aug 2021 10:15:55 -0700 Subject: [PATCH 3/7] Rebase, update version and build --- dwds/lib/src/version.dart | 2 +- dwds/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dwds/lib/src/version.dart b/dwds/lib/src/version.dart index 97e914528..0bfc9c790 100644 --- a/dwds/lib/src/version.dart +++ b/dwds/lib/src/version.dart @@ -1,2 +1,2 @@ // Generated code. Do not modify. -const packageVersion = '11.2.2'; +const packageVersion = '11.2.3-dev'; diff --git a/dwds/pubspec.yaml b/dwds/pubspec.yaml index 707931074..3c941bf49 100644 --- a/dwds/pubspec.yaml +++ b/dwds/pubspec.yaml @@ -1,6 +1,6 @@ name: dwds # Every time this changes you need to run `pub run build_runner build`. -version: 11.2.2 +version: 11.2.3-dev homepage: https://github.com/dart-lang/webdev/tree/master/dwds description: >- A service that proxies between the Chrome debug protocol and the Dart VM From 0c4d1bd72b82f7272d2d35ed741271b289cfb3c7 Mon Sep 17 00:00:00 2001 From: Anna Gringauze Date: Wed, 18 Aug 2021 11:40:38 -0700 Subject: [PATCH 4/7] Increase timeout for failing tests --- frontend_server_client/test/frontend_sever_client_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend_server_client/test/frontend_sever_client_test.dart b/frontend_server_client/test/frontend_sever_client_test.dart index 97a66a4d0..e661fa032 100644 --- a/frontend_server_client/test/frontend_sever_client_test.dart +++ b/frontend_server_client/test/frontend_sever_client_test.dart @@ -235,7 +235,7 @@ String get message => p.join('hello', 'world'); expect( utf8.decode(dartDevcClient.assetBytes('${entrypointUri.path}.lib.js')!), contains('goodbye')); - }); + }, timeout: const Timeout.factor(2)); test('can enable experiments', () async { await d.dir('a', [ From acd9c74bf9665a72724d3655d84e302f0e6eafe9 Mon Sep 17 00:00:00 2001 From: Anna Gringauze Date: Wed, 18 Aug 2021 18:00:55 -0700 Subject: [PATCH 5/7] Update frontend_sever_client_test.dart revert timeout on frontend_server_client tests, it is fixed in another PR. --- frontend_server_client/test/frontend_sever_client_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend_server_client/test/frontend_sever_client_test.dart b/frontend_server_client/test/frontend_sever_client_test.dart index e661fa032..97a66a4d0 100644 --- a/frontend_server_client/test/frontend_sever_client_test.dart +++ b/frontend_server_client/test/frontend_sever_client_test.dart @@ -235,7 +235,7 @@ String get message => p.join('hello', 'world'); expect( utf8.decode(dartDevcClient.assetBytes('${entrypointUri.path}.lib.js')!), contains('goodbye')); - }, timeout: const Timeout.factor(2)); + }); test('can enable experiments', () async { await d.dir('a', [ From 0ad0b995ebe83fa80c86f999f0e55de36f562456 Mon Sep 17 00:00:00 2001 From: Anna Gringauze Date: Thu, 19 Aug 2021 11:32:36 -0700 Subject: [PATCH 6/7] Remove no running main on hot restart --- dwds/CHANGELOG.md | 10 ++++------ dwds/lib/src/connections/app_connection.dart | 6 +----- dwds/lib/src/handlers/dev_handler.dart | 4 ---- dwds/lib/src/injected/client.js | 1 + dwds/web/reloader/require_restarter.dart | 2 ++ 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index 10c22c285..a848f9794 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,11 +1,9 @@ ## 11.2.3-dev -- Fix race conditions: - - Intermittent hot restart failures to restore breakpoints. - - Intermittent `Aww, snap` errors on starting debugger with multiple - breakpoints in source. - - Needing chrome to be focus in order to wait for the isolate to - exit on hot restart. +- Fix race causing intermittent `Aww, snap` errors on starting debugger + with multiple breakpoints in source. +- Fix needing chrome to be focus in order to wait for the isolate to + exit on hot restart. ## 11.2.2 diff --git a/dwds/lib/src/connections/app_connection.dart b/dwds/lib/src/connections/app_connection.dart index 90e4d6b12..04669c312 100644 --- a/dwds/lib/src/connections/app_connection.dart +++ b/dwds/lib/src/connections/app_connection.dart @@ -16,7 +16,7 @@ import '../handlers/socket_connections.dart'; class AppConnection { /// The initial connection request sent from the application in the browser. final ConnectRequest request; - var _startedCompleter = Completer(); + final _startedCompleter = Completer(); final SocketConnection _connection; AppConnection(this.request, this._connection); @@ -33,8 +33,4 @@ class AppConnection { _connection.sink.add(jsonEncode(serializers.serialize(RunRequest()))); _startedCompleter.complete(); } - - void reset() { - _startedCompleter = Completer(); - } } diff --git a/dwds/lib/src/handlers/dev_handler.dart b/dwds/lib/src/handlers/dev_handler.dart index 8b9af0057..9dc66c436 100644 --- a/dwds/lib/src/handlers/dev_handler.dart +++ b/dwds/lib/src/handlers/dev_handler.dart @@ -407,13 +407,9 @@ class DevHandler { Future _handleIsolateStart( AppConnection appConnection, SocketConnection sseConnection) async { - // App is not running yet, reset `isStarted` state. - appConnection.reset(); await _servicesByAppId[appConnection.request.appId] ?.chromeProxyService ?.createIsolate(appConnection); - // Now run. - appConnection.runMain(); } void _listen() async { diff --git a/dwds/lib/src/injected/client.js b/dwds/lib/src/injected/client.js index 2f5a577f0..10bd83d76 100644 --- a/dwds/lib/src/injected/client.js +++ b/dwds/lib/src/injected/client.js @@ -24358,6 +24358,7 @@ case 8: // join self.$loadModuleConfig.call$1("dart_sdk").dart.hotRestart(); + V.runMain(); $async$returnValue = result; // goto return $async$goto = 1; diff --git a/dwds/web/reloader/require_restarter.dart b/dwds/web/reloader/require_restarter.dart index 86be8c2b0..a69b90a9e 100644 --- a/dwds/web/reloader/require_restarter.dart +++ b/dwds/web/reloader/require_restarter.dart @@ -17,6 +17,7 @@ import 'package:js/js.dart'; import 'package:js/js_util.dart'; import '../promise.dart'; +import '../run_main.dart'; import 'restarter.dart'; /// The last known digests of all the modules in the application. @@ -128,6 +129,7 @@ class RequireRestarter implements Restarter { } callMethod(getProperty(require('dart_sdk'), 'dart'), 'hotRestart', []); + runMain(); return result; } From 0753003c0eefe39044f427857d555bb2b4a9a093 Mon Sep 17 00:00:00 2001 From: Anna Gringauze Date: Thu, 19 Aug 2021 15:59:47 -0700 Subject: [PATCH 7/7] Addressed CR comments --- dwds/lib/src/debugging/debugger.dart | 8 ++--- dwds/lib/src/utilities/mutex.dart | 46 ---------------------------- 2 files changed, 4 insertions(+), 50 deletions(-) delete mode 100644 dwds/lib/src/utilities/mutex.dart diff --git a/dwds/lib/src/debugging/debugger.dart b/dwds/lib/src/debugging/debugger.dart index 654eb16d2..8194f0568 100644 --- a/dwds/lib/src/debugging/debugger.dart +++ b/dwds/lib/src/debugging/debugger.dart @@ -9,6 +9,7 @@ import 'dart:math' as math; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; +import 'package:pool/pool.dart'; import 'package:vm_service/vm_service.dart'; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' hide StackTrace; @@ -18,7 +19,6 @@ import '../services/chrome_proxy_service.dart'; import '../utilities/conversions.dart'; import '../utilities/dart_uri.dart'; import '../utilities/domain.dart'; -import '../utilities/mutex.dart'; import '../utilities/objects.dart' show Property; import '../utilities/shared.dart'; import 'dart_scope.dart'; @@ -674,6 +674,8 @@ class _Breakpoints extends Domain { final _bpByDartId = >{}; + final _pool = Pool(1); + final Locations locations; final RemoteDebugger remoteDebugger; @@ -734,8 +736,6 @@ class _Breakpoints extends Domain { return breakpoint; } - Mutex breakpointMutex = Mutex(); - /// Calls the Chrome protocol setBreakpoint and returns the remote ID. Future _setJsBreakpoint(Location location) async { // Location is 0 based according to: @@ -745,7 +745,7 @@ class _Breakpoints extends Domain { var urlRegex = '.*${location.jsLocation.module}.*'; // Prevent `Aww, snap!` errors when setting multiple breakpoints // simultaneously by serializing the requests. - return breakpointMutex.runGuarded(() async { + return _pool.withResource(() async { var response = await remoteDebugger .sendCommand('Debugger.setBreakpointByUrl', params: { 'urlRegex': urlRegex, diff --git a/dwds/lib/src/utilities/mutex.dart b/dwds/lib/src/utilities/mutex.dart deleted file mode 100644 index b697e78e0..000000000 --- a/dwds/lib/src/utilities/mutex.dart +++ /dev/null @@ -1,46 +0,0 @@ -// 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. - -import 'dart:async'; -import 'dart:collection'; - -/// Used to protect global state accessed in blocks containing calls to -/// asynchronous methods. -class Mutex { - /// Executes a block of code containing asynchronous calls atomically. - /// - /// If no other asynchronous context is currently executing within - /// [criticalSection], it will immediately be called. Otherwise, the caller - /// will be suspended and entered into a queue to be resumed once the lock is - /// released. - Future runGuarded(FutureOr Function() criticalSection) async { - try { - await _acquireLock(); - return await criticalSection(); - } finally { - _releaseLock(); - } - } - - Future _acquireLock() async { - if (!_locked) { - _locked = true; - return; - } - final request = Completer(); - _outstandingRequests.add(request); - await request.future; - } - - void _releaseLock() { - _locked = false; - if (_outstandingRequests.isNotEmpty) { - final request = _outstandingRequests.removeFirst(); - request.complete(); - } - } - - bool _locked = false; - final _outstandingRequests = Queue>(); -}