Initialize new isolate after restart (#1305)
* Initiaze new isolate after restart
- reset initialization completer on `destroyIsolate`
so correct signal is set after `createIsolate`
- make all supported VM API wait for isolate initizaliation
before proceeding.
Towards:https://github.com/flutter/flutter/issues/74903
* Update changelog
* Fix evaluate completing before update dependencies
* Actually fix expression evaluation before updateDependencies
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index 2db505d..6b60fb1 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -5,6 +5,9 @@
- Fix incorrect `rootLib` returned by `ChromeProxyService`.
- Fix not working breakpoints in library part files.
- Fix data race in calculating locations for a module.
+- Fix uninitialized isolate after hot restart.
+- Fix intermittent failure caused by evaluation not waiting for dependencies
+ to be updated.
## 10.0.1
diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart
index b4589a8..f2b7670 100644
--- a/dwds/lib/src/services/chrome_proxy_service.dart
+++ b/dwds/lib/src/services/chrome_proxy_service.dart
@@ -52,10 +52,14 @@
/// are dynamic and roughly map to chrome tabs.
final VM _vm;
- final _initializedCompleter = Completer<void>();
-
+ /// Signals when isolate is intialized.
+ Completer<void> _initializedCompleter = Completer<void>();
Future<void> get isInitialized => _initializedCompleter.future;
+ /// Signals when expression compiler is ready to evaluate.
+ Completer<void> _compilerCompleter = Completer<void>();
+ Future<void> get isCompilerInitialized => _compilerCompleter.future;
+
/// The root URI at which we're serving.
final String uri;
@@ -165,10 +169,10 @@
_skipLists.initialize();
// We do not need to wait for compiler dependencies to be udpated as the
// [ExpressionEvaluator] is robust to evaluation requests during updates.
- unawaited(updateCompilerDependencies(entrypoint));
+ unawaited(_updateCompilerDependencies(entrypoint));
}
- Future<void> updateCompilerDependencies(String entrypoint) async {
+ Future<void> _updateCompilerDependencies(String entrypoint) async {
var metadataProvider = globalLoadStrategy.metadataProviderFor(entrypoint);
var moduleFormat = globalLoadStrategy.moduleFormat;
var soundNullSafety = await metadataProvider.soundNullSafety;
@@ -183,6 +187,8 @@
await globalLoadStrategy.moduleInfoForEntrypoint(entrypoint);
var stopwatch = Stopwatch()..start();
await _compiler.updateDependencies(dependencies);
+ // Expression evaluation is ready after dependencies are updated.
+ if (!_compilerCompleter.isCompleted) _compilerCompleter.complete();
emitEvent(DwdsEvent('COMPILER_UPDATE_DEPENDENCIES', {
'entrypoint': entrypoint,
'elapsedMilliseconds': stopwatch.elapsedMilliseconds,
@@ -273,6 +279,8 @@
void destroyIsolate() {
var isolate = _inspector?.isolate;
if (isolate == null) return;
+ _initializedCompleter = Completer<void>();
+ _compilerCompleter = Completer<void>();
_streamNotify(
'Isolate',
Event(
@@ -299,9 +307,11 @@
@override
Future<Breakpoint> addBreakpoint(String isolateId, String scriptId, int line,
- {int column}) async =>
- (await _debugger)
- .addBreakpoint(isolateId, scriptId, line, column: column);
+ {int column}) async {
+ await isInitialized;
+ return (await _debugger)
+ .addBreakpoint(isolateId, scriptId, line, column: column);
+ }
@override
Future<Breakpoint> addBreakpointAtEntry(String isolateId, String functionId) {
@@ -312,6 +322,7 @@
Future<Breakpoint> addBreakpointWithScriptUri(
String isolateId, String scriptUri, int line,
{int column}) async {
+ await isInitialized;
var dartUri = DartUri(scriptUri, uri);
var ref = await _inspector.scriptRefFor(dartUri.serverPath);
return (await _debugger)
@@ -321,6 +332,7 @@
@override
Future<Response> callServiceExtension(String method,
{String isolateId, Map args}) async {
+ await isInitialized;
// Validate the isolate id is correct, _getIsolate throws if not.
if (isolateId != null) _getIsolate(isolateId);
args ??= <String, String>{};
@@ -413,7 +425,9 @@
dynamic error;
try {
+ await isInitialized;
if (_expressionEvaluator != null) {
+ await isCompilerInitialized;
_validateIsolateId(isolateId);
var library = await _inspector?.getLibrary(isolateId, targetId);
@@ -453,7 +467,9 @@
dynamic error;
try {
+ await isInitialized;
if (_expressionEvaluator != null) {
+ await isCompilerInitialized;
_validateIsolateId(isolateId);
var result = await _getEvaluationResult(
@@ -516,25 +532,38 @@
}
@override
- Future<Isolate> getIsolate(String isolateId) async => _getIsolate(isolateId);
+ Future<Isolate> getIsolate(String isolateId) async {
+ await isInitialized;
+ return _getIsolate(isolateId);
+ }
@override
- Future<MemoryUsage> getMemoryUsage(String isolateId) {
+ Future<MemoryUsage> getMemoryUsage(String isolateId) async {
+ await isInitialized;
return _inspector.getMemoryUsage(isolateId);
}
@override
Future<Obj> getObject(String isolateId, String objectId,
- {int offset, int count}) =>
- _inspector?.getObject(isolateId, objectId, offset: offset, count: count);
+ {int offset, int count}) async {
+ await isInitialized;
+ return _inspector?.getObject(isolateId, objectId,
+ offset: offset, count: count);
+ }
@override
- Future<ScriptList> getScripts(String isolateId) =>
- _inspector?.getScripts(isolateId);
+ Future<ScriptList> getScripts(String isolateId) async {
+ await isInitialized;
+ return _inspector?.getScripts(isolateId);
+ }
@override
Future<SourceReport> getSourceReport(String isolateId, List<String> reports,
- {String scriptId, int tokenPos, int endTokenPos, bool forceCompile}) {
+ {String scriptId,
+ int tokenPos,
+ int endTokenPos,
+ bool forceCompile}) async {
+ await isInitialized;
return _inspector?.getSourceReport(isolateId, reports,
scriptId: scriptId,
tokenPos: tokenPos,
@@ -548,8 +577,10 @@
///
/// The returned stack will contain up to [limit] frames if provided.
@override
- Future<Stack> getStack(String isolateId, {int limit}) async =>
- (await _debugger).getStack(isolateId, limit: limit);
+ Future<Stack> getStack(String isolateId, {int limit}) async {
+ await isInitialized;
+ return (await _debugger).getStack(isolateId, limit: limit);
+ }
@override
Future<VM> getVM() async {
@@ -577,6 +608,7 @@
Future<Response> invoke(
String isolateId, String targetId, String selector, List argumentIds,
{bool disableBreakpoints}) async {
+ await isInitialized;
// TODO(798) - respect disableBreakpoints.
var remote =
await _inspector?.invoke(isolateId, targetId, selector, argumentIds);
@@ -633,7 +665,10 @@
}
@override
- Future<Success> pause(String isolateId) async => (await _debugger).pause();
+ Future<Success> pause(String isolateId) async {
+ await isInitialized;
+ return (await _debugger).pause();
+ }
@override
Future<Success> registerService(String service, String alias) async {
@@ -653,6 +688,7 @@
@override
Future<Success> removeBreakpoint(
String isolateId, String breakpointId) async {
+ await isInitialized;
_disabledBreakpoints
.removeWhere((breakpoint) => breakpoint.id == breakpointId);
return (await _debugger).removeBreakpoint(isolateId, breakpointId);
@@ -664,6 +700,7 @@
if (_inspector == null) throw StateError('No running isolate.');
if (_inspector.appConnection.isStarted) {
var stopwatch = Stopwatch()..start();
+ await isInitialized;
var result = await (await _debugger)
.resume(isolateId, step: step, frameIndex: frameIndex);
emitEvent(DwdsEvent('RESUME', {
@@ -679,6 +716,7 @@
@override
Future<Success> setExceptionPauseMode(String isolateId, String mode) async {
+ await isInitialized;
return (await _debugger).setExceptionPauseMode(isolateId, mode);
}
@@ -695,6 +733,7 @@
@override
Future<Success> setName(String isolateId, String name) async {
+ await isInitialized;
var isolate = _getIsolate(isolateId);
isolate.name = name;
return Success();
diff --git a/dwds/test/reload_test.dart b/dwds/test/reload_test.dart
index 415099f..3467315 100644
--- a/dwds/test/reload_test.dart
+++ b/dwds/test/reload_test.dart
@@ -90,7 +90,7 @@
group('Injected client', () {
setUp(() async {
- await context.setUp();
+ await context.setUp(enableExpressionEvaluation: true);
});
tearDown(() async {
@@ -204,13 +204,66 @@
expect(source.contains('Hello World!'), isTrue);
expect(source.contains('Gary is awesome!'), isTrue);
- // Should not be paused.
vm = await client.getVM();
isolateId = vm.isolates.first.id;
var isolate = await client.getIsolate(isolateId);
- expect(isolate.pauseEvent.kind, EventKind.kResume);
+
// Previous breakpoint should still exist.
expect(isolate.breakpoints.isNotEmpty, isTrue);
+ var bp = isolate.breakpoints.first;
+
+ // Should pause eventually.
+ await stream
+ .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+
+ expect(await client.removeBreakpoint(isolate.id, bp.id), isA<Success>());
+ expect(await client.resume(isolate.id), isA<Success>());
+ });
+
+ test('can evaluate expressions after hot restart ', () async {
+ var client = context.debugConnection.vmService;
+ var vm = await client.getVM();
+ var isolateId = vm.isolates.first.id;
+ await client.streamListen('Debug');
+ var stream = client.onEvent('Debug');
+ var scriptList = await client.getScripts(isolateId);
+ var main = scriptList.scripts
+ .firstWhere((script) => script.uri.contains('main.dart'));
+ var bpLine =
+ await context.findBreakpointLine('printCount', isolateId, main);
+ await client.addBreakpoint(isolateId, main.id, bpLine);
+ await stream
+ .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+
+ await client.callServiceExtension('hotRestart');
+
+ vm = await client.getVM();
+ isolateId = vm.isolates.first.id;
+ var isolate = await client.getIsolate(isolateId);
+ var library = isolate.rootLib.uri;
+ var bp = isolate.breakpoints.first;
+
+ // Should pause eventually.
+ var event = await stream
+ .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint);
+
+ // Expression evaluation while paused on a breakpoint should work.
+ var result = await client.evaluateInFrame(
+ isolate.id, event.topFrame.index, 'count');
+ expect(
+ result,
+ isA<InstanceRef>().having((instance) => instance.valueAsString,
+ 'valueAsString', greaterThanOrEqualTo('0')));
+
+ await client.removeBreakpoint(isolateId, bp.id);
+ await client.resume(isolateId);
+
+ // Expression evaluation while running should work.
+ result = await client.evaluate(isolateId, library, 'true');
+ expect(
+ result,
+ isA<InstanceRef>().having(
+ (instance) => instance.valueAsString, 'valueAsString', 'true'));
});
});