mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

**Original Description:** > Service extensions are unable to handle requests when the isolate they were registered on is paused. The DevTools launcher logic was waiting for some service extension invocations to complete before advertising the already active DevTools instance, but when --start-paused was provided these requests would never complete, preventing users from using DevTools to resume the paused isolate. > > Fixes https://github.com/flutter/flutter/issues/126691 **Additional changes in this PR:** The failures listed in https://github.com/flutter/flutter/pull/128117 appear to be related to a shutdown race. It's possible for the test to complete while the tool is in the process of starting and advertising DevTools, so we need to perform a check of `_shutdown` in `FlutterResidentDevtoolsHandler` before advertising DevTools. Before the original fix, this check was being performed immediately after invoking the service extensions, which creates an asynchronous gap in execution. With #126698, the callsite of the service extensions was moved and the `_shutdown` check wasn't, allowing for the tool to attempt to advertise DevTools after the DevTools server had been cleaned up. --------- Co-authored-by: Zachary Anderson <zanderso@users.noreply.github.com>
505 lines
16 KiB
Dart
505 lines
16 KiB
Dart
// Copyright 2014 The Flutter Authors. 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 'package:flutter_tools/src/base/dds.dart';
|
|
import 'package:flutter_tools/src/base/logger.dart';
|
|
import 'package:flutter_tools/src/build_info.dart';
|
|
import 'package:flutter_tools/src/cache.dart';
|
|
import 'package:flutter_tools/src/device.dart';
|
|
import 'package:flutter_tools/src/devtools_launcher.dart';
|
|
import 'package:flutter_tools/src/resident_devtools_handler.dart';
|
|
import 'package:flutter_tools/src/resident_runner.dart';
|
|
import 'package:flutter_tools/src/vmservice.dart';
|
|
import 'package:test/fake.dart';
|
|
import 'package:vm_service/vm_service.dart' as vm_service;
|
|
|
|
import '../src/common.dart';
|
|
import '../src/fake_process_manager.dart';
|
|
import '../src/fake_vm_services.dart';
|
|
import '../src/fakes.dart';
|
|
|
|
final vm_service.Isolate isolate = vm_service.Isolate(
|
|
id: '1',
|
|
pauseEvent: vm_service.Event(
|
|
kind: vm_service.EventKind.kResume,
|
|
timestamp: 0
|
|
),
|
|
breakpoints: <vm_service.Breakpoint>[],
|
|
libraries: <vm_service.LibraryRef>[
|
|
vm_service.LibraryRef(
|
|
id: '1',
|
|
uri: 'file:///hello_world/main.dart',
|
|
name: '',
|
|
),
|
|
],
|
|
livePorts: 0,
|
|
name: 'test',
|
|
number: '1',
|
|
pauseOnExit: false,
|
|
runnable: true,
|
|
startTime: 0,
|
|
isSystemIsolate: false,
|
|
isolateFlags: <vm_service.IsolateFlag>[],
|
|
extensionRPCs: <String>['ext.flutter.connectedVmServiceUri'],
|
|
);
|
|
|
|
final FakeVmServiceRequest listViews = FakeVmServiceRequest(
|
|
method: kListViewsMethod,
|
|
jsonResponse: <String, Object>{
|
|
'views': <Object>[
|
|
FlutterView(
|
|
id: 'a',
|
|
uiIsolate: isolate,
|
|
).toJson(),
|
|
],
|
|
},
|
|
);
|
|
|
|
void main() {
|
|
Cache.flutterRoot = '';
|
|
|
|
testWithoutContext('Does not serve devtools if launcher is null', () async {
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
null,
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
await handler.serveAndAnnounceDevTools(flutterDevices: <FlutterDevice>[]);
|
|
|
|
expect(handler.activeDevToolsServer, null);
|
|
});
|
|
|
|
testWithoutContext('Does not serve devtools if ResidentRunner does not support the service protocol', () async {
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher(),
|
|
FakeResidentRunner()..supportsServiceProtocol = false,
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
await handler.serveAndAnnounceDevTools(flutterDevices: <FlutterDevice>[]);
|
|
|
|
expect(handler.activeDevToolsServer, null);
|
|
});
|
|
|
|
testWithoutContext('Can use devtools with existing devtools URI', () async {
|
|
final DevtoolsServerLauncher launcher = DevtoolsServerLauncher(
|
|
processManager: FakeProcessManager.empty(),
|
|
dartExecutable: 'dart',
|
|
logger: BufferLogger.test(),
|
|
botDetector: const FakeBotDetector(false),
|
|
);
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
// Uses real devtools instance which should be a no-op if
|
|
// URI is already set.
|
|
launcher,
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
await handler.serveAndAnnounceDevTools(
|
|
devToolsServerAddress: Uri.parse('http://localhost:8181'),
|
|
flutterDevices: <FlutterDevice>[],
|
|
);
|
|
|
|
expect(handler.activeDevToolsServer!.host, 'localhost');
|
|
expect(handler.activeDevToolsServer!.port, 8181);
|
|
});
|
|
|
|
testWithoutContext('serveAndAnnounceDevTools with attached device does not fail on null vm service', () async {
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher()
|
|
..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
|
|
..devToolsUrl = Uri.parse('http://localhost:8080'),
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
// VM Service is intentionally null
|
|
final FakeFlutterDevice device = FakeFlutterDevice();
|
|
|
|
await handler.serveAndAnnounceDevTools(
|
|
flutterDevices: <FlutterDevice>[device],
|
|
);
|
|
});
|
|
|
|
testWithoutContext('serveAndAnnounceDevTools with invokes devtools and vm_service setter', () async {
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher()
|
|
..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
|
|
..devToolsUrl = Uri.parse('http://localhost:8080'),
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
|
|
const FakeVmServiceRequest(
|
|
method: 'streamListen',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
}
|
|
),
|
|
listViews,
|
|
FakeVmServiceRequest(
|
|
method: 'getIsolate',
|
|
jsonResponse: isolate.toJson(),
|
|
args: <String, Object>{
|
|
'isolateId': '1',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'streamCancel',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
},
|
|
),
|
|
listViews,
|
|
listViews,
|
|
const FakeVmServiceRequest(
|
|
method: 'ext.flutter.activeDevToolsServerAddress',
|
|
args: <String, Object>{
|
|
'isolateId': '1',
|
|
'value': 'http://localhost:8080',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'ext.flutter.connectedVmServiceUri',
|
|
args: <String, Object>{
|
|
'isolateId': '1',
|
|
'value': 'http://localhost:1234',
|
|
},
|
|
),
|
|
], httpAddress: Uri.parse('http://localhost:1234'));
|
|
|
|
final FakeFlutterDevice device = FakeFlutterDevice()
|
|
..vmService = fakeVmServiceHost.vmService;
|
|
|
|
await handler.serveAndAnnounceDevTools(
|
|
flutterDevices: <FlutterDevice>[device],
|
|
);
|
|
});
|
|
|
|
testWithoutContext('serveAndAnnounceDevTools will bail if launching devtools fails', () async {
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher()..activeDevToolsServer = null,
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[], httpAddress: Uri.parse('http://localhost:1234'));
|
|
|
|
final FakeFlutterDevice device = FakeFlutterDevice()
|
|
..vmService = fakeVmServiceHost.vmService;
|
|
|
|
await handler.serveAndAnnounceDevTools(
|
|
flutterDevices: <FlutterDevice>[device],
|
|
);
|
|
});
|
|
|
|
testWithoutContext('serveAndAnnounceDevTools with web device', () async {
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher()
|
|
..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
|
|
..devToolsUrl = Uri.parse('http://localhost:8080'),
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
|
|
const FakeVmServiceRequest(
|
|
method: 'streamListen',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
}
|
|
),
|
|
listViews,
|
|
FakeVmServiceRequest(
|
|
method: 'getIsolate',
|
|
jsonResponse: isolate.toJson(),
|
|
args: <String, Object>{
|
|
'isolateId': '1',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'streamCancel',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'ext.flutter.activeDevToolsServerAddress',
|
|
args: <String, Object>{
|
|
'value': 'http://localhost:8080',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'ext.flutter.connectedVmServiceUri',
|
|
args: <String, Object>{
|
|
'value': 'http://localhost:1234',
|
|
},
|
|
),
|
|
], httpAddress: Uri.parse('http://localhost:1234'));
|
|
|
|
final FakeFlutterDevice device = FakeFlutterDevice()
|
|
..vmService = fakeVmServiceHost.vmService
|
|
..targetPlatform = TargetPlatform.web_javascript;
|
|
|
|
await handler.serveAndAnnounceDevTools(
|
|
flutterDevices: <FlutterDevice>[device],
|
|
);
|
|
});
|
|
|
|
testWithoutContext('serveAndAnnounceDevTools with skips calling service extensions when VM service disappears', () async {
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher()..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
|
|
const FakeVmServiceRequest(
|
|
method: 'streamListen',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: kListViewsMethod,
|
|
errorCode: RPCErrorCodes.kServiceDisappeared,
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'streamCancel',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
},
|
|
errorCode: RPCErrorCodes.kServiceDisappeared,
|
|
),
|
|
], httpAddress: Uri.parse('http://localhost:1234'));
|
|
|
|
final FakeFlutterDevice device = FakeFlutterDevice()
|
|
..vmService = fakeVmServiceHost.vmService;
|
|
|
|
await handler.serveAndAnnounceDevTools(
|
|
flutterDevices: <FlutterDevice>[device],
|
|
);
|
|
});
|
|
|
|
testWithoutContext('serveAndAnnounceDevTools with multiple devices and VM service disappears on one', () async {
|
|
final ResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher()
|
|
..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
|
|
..devToolsUrl = Uri.parse('http://localhost:8080'),
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
final FakeVmServiceHost vmServiceHost = FakeVmServiceHost(requests: <VmServiceExpectation>[
|
|
const FakeVmServiceRequest(
|
|
method: 'streamListen',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
},
|
|
),
|
|
listViews,
|
|
FakeVmServiceRequest(
|
|
method: 'getIsolate',
|
|
jsonResponse: isolate.toJson(),
|
|
args: <String, Object>{
|
|
'isolateId': '1',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'streamCancel',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
},
|
|
),
|
|
listViews,
|
|
listViews,
|
|
const FakeVmServiceRequest(
|
|
method: 'ext.flutter.activeDevToolsServerAddress',
|
|
args: <String, Object>{
|
|
'isolateId': '1',
|
|
'value': 'http://localhost:8080',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'ext.flutter.connectedVmServiceUri',
|
|
args: <String, Object>{
|
|
'isolateId': '1',
|
|
'value': 'http://localhost:1234',
|
|
},
|
|
),
|
|
], httpAddress: Uri.parse('http://localhost:1234'));
|
|
|
|
final FakeVmServiceHost vmServiceHostThatDisappears = FakeVmServiceHost(requests: <VmServiceExpectation>[
|
|
const FakeVmServiceRequest(
|
|
method: 'streamListen',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
},
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: kListViewsMethod,
|
|
errorCode: RPCErrorCodes.kServiceDisappeared,
|
|
),
|
|
const FakeVmServiceRequest(
|
|
method: 'streamCancel',
|
|
args: <String, Object>{
|
|
'streamId': 'Isolate',
|
|
},
|
|
errorCode: RPCErrorCodes.kServiceDisappeared,
|
|
),
|
|
], httpAddress: Uri.parse('http://localhost:5678'));
|
|
|
|
await handler.serveAndAnnounceDevTools(
|
|
flutterDevices: <FlutterDevice>[
|
|
FakeFlutterDevice()
|
|
..vmService = vmServiceHostThatDisappears.vmService,
|
|
FakeFlutterDevice()
|
|
..vmService = vmServiceHost.vmService,
|
|
],
|
|
);
|
|
});
|
|
|
|
testWithoutContext('Does not launch devtools in browser if launcher is null', () async {
|
|
final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
null,
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]);
|
|
expect(handler.launchedInBrowser, isFalse);
|
|
expect(handler.activeDevToolsServer, null);
|
|
});
|
|
|
|
testWithoutContext('Does not launch devtools in browser if ResidentRunner does not support the service protocol', () async {
|
|
final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher(),
|
|
FakeResidentRunner()..supportsServiceProtocol = false,
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]);
|
|
expect(handler.launchedInBrowser, isFalse);
|
|
expect(handler.activeDevToolsServer, null);
|
|
});
|
|
|
|
testWithoutContext('launchDevToolsInBrowser launches after _devToolsLauncher.ready completes', () async {
|
|
final Completer<void> completer = Completer<void>();
|
|
final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher()
|
|
..devToolsUrl = null
|
|
// We need to set [activeDevToolsServer] to simulate the state we would
|
|
// be in after serving devtools completes.
|
|
..activeDevToolsServer = DevToolsServerAddress('localhost', 8080)
|
|
..readyCompleter = completer,
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
expect(handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]), isTrue);
|
|
expect(handler.launchedInBrowser, isFalse);
|
|
|
|
completer.complete();
|
|
// Await a short delay to give DevTools time to launch.
|
|
await Future<void>.delayed(const Duration(microseconds: 100));
|
|
|
|
expect(handler.launchedInBrowser, isTrue);
|
|
});
|
|
|
|
testWithoutContext('launchDevToolsInBrowser launches successfully', () async {
|
|
final FlutterResidentDevtoolsHandler handler = FlutterResidentDevtoolsHandler(
|
|
FakeDevtoolsLauncher()
|
|
..devToolsUrl = Uri(host: 'localhost', port: 8080)
|
|
..activeDevToolsServer = DevToolsServerAddress('localhost', 8080),
|
|
FakeResidentRunner(),
|
|
BufferLogger.test(),
|
|
);
|
|
|
|
expect(handler.launchDevToolsInBrowser(flutterDevices: <FlutterDevice>[]), isTrue);
|
|
expect(handler.launchedInBrowser, isTrue);
|
|
});
|
|
|
|
testWithoutContext('Converts a VM Service URI with a query parameter to a pretty display string', () {
|
|
const String value = 'http://127.0.0.1:9100?uri=http%3A%2F%2F127.0.0.1%3A57922%2F_MXpzytpH20%3D%2F';
|
|
final Uri uri = Uri.parse(value);
|
|
|
|
expect(urlToDisplayString(uri), 'http://127.0.0.1:9100?uri=http://127.0.0.1:57922/_MXpzytpH20=/');
|
|
});
|
|
}
|
|
|
|
class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
|
|
@override
|
|
DevToolsServerAddress? activeDevToolsServer;
|
|
|
|
@override
|
|
Uri? devToolsUrl;
|
|
|
|
@override
|
|
Future<DevToolsServerAddress?> serve() async => null;
|
|
|
|
@override
|
|
Future<void> get ready => readyCompleter.future;
|
|
|
|
Completer<void> readyCompleter = Completer<void>()..complete();
|
|
}
|
|
|
|
class FakeResidentRunner extends Fake implements ResidentRunner {
|
|
@override
|
|
bool supportsServiceProtocol = true;
|
|
|
|
@override
|
|
bool reportedDebuggers = false;
|
|
|
|
@override
|
|
DebuggingOptions debuggingOptions = DebuggingOptions.disabled(BuildInfo.debug);
|
|
}
|
|
|
|
class FakeFlutterDevice extends Fake implements FlutterDevice {
|
|
@override
|
|
final Device device = FakeDevice();
|
|
|
|
@override
|
|
FlutterVmService? vmService;
|
|
|
|
@override
|
|
TargetPlatform targetPlatform = TargetPlatform.android_arm;
|
|
}
|
|
|
|
// Unfortunately Device, despite not being immutable, has an `operator ==`.
|
|
// Until we fix that, we have to also ignore related lints here.
|
|
// ignore: avoid_implementing_value_types
|
|
class FakeDevice extends Fake implements Device {
|
|
@override
|
|
DartDevelopmentService get dds => FakeDartDevelopmentService();
|
|
}
|
|
|
|
class FakeDartDevelopmentService extends Fake implements DartDevelopmentService {
|
|
bool started = false;
|
|
bool disposed = false;
|
|
|
|
@override
|
|
final Uri uri = Uri.parse('http://127.0.0.1:1234/');
|
|
|
|
@override
|
|
Future<void> startDartDevelopmentService(
|
|
Uri observatoryUri, {
|
|
required Logger logger,
|
|
int? hostPort,
|
|
bool? ipv6,
|
|
bool? disableServiceAuthCodes,
|
|
bool cacheStartupProfile = false,
|
|
}) async {
|
|
started = true;
|
|
}
|
|
|
|
@override
|
|
Future<void> shutdown() async {
|
|
disposed = true;
|
|
}
|
|
|
|
@override
|
|
void setExternalDevToolsUri(Uri uri) {}
|
|
}
|