Ben Konyi f023430859
Launch DDS from Dart SDK and prepare to serve DevTools from DDS (#146593)
This change is a major step towards moving away from shipping DDS via
Pub.

The first component of this PR is the move away from importing
package:dds to launch DDS. Instead, DDS is launched out of process using
the `dart development-service` command shipped with the Dart SDK. This
makes Flutter's handling of DDS consistent with the standalone Dart VM.

The second component of this PR is the initial work to prepare for the
removal of instances of DevTools being served manually by the
flutter_tool, instead relying on DDS to serve DevTools. This will be
consistent with how the standalone Dart VM serves DevTools, tying the
DevTools lifecycle to a live DDS instance. This will allow for the
removal of much of the logic needed to properly manage the lifecycle of
the DevTools server in a future PR. Also, by serving DevTools from DDS,
users will no longer need to forward a secondary port in remote
workflows as DevTools will be available on the DDS port. This code is currently 
commented out and will be enabled in a future PR.

There's two remaining circumstances that will prevent us from removing
DevtoolsRunner completely:

 - The daemon's `devtools.serve` endpoint
- `flutter drive`'s `--profile-memory` flag used for recording memory
profiles

This PR also includes some refactoring around `DebuggingOptions` to
reduce the number of debugging related arguments being passed as
parameters adjacent to a `DebuggingOptions` instance.
2024-07-15 14:08:31 -04:00

759 lines
18 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 'dart:io' as io show IOSink, ProcessSignal, Stdout, StdoutException;
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/java.dart';
import 'package:flutter_tools/src/base/bot_detector.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/time.dart';
import 'package:flutter_tools/src/base/version.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/ios/plist_parser.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/resident_runner.dart';
import 'package:flutter_tools/src/version.dart';
import 'package:test/fake.dart';
/// Environment with DYLD_LIBRARY_PATH=/path/to/libraries
class FakeDyldEnvironmentArtifact extends ArtifactSet {
FakeDyldEnvironmentArtifact() : super(DevelopmentArtifact.iOS);
@override
Map<String, String> get environment => <String, String>{
'DYLD_LIBRARY_PATH': '/path/to/libraries',
};
@override
Future<bool> isUpToDate(FileSystem fileSystem) => Future<bool>.value(true);
@override
String get name => 'fake';
@override
Future<void> update(ArtifactUpdater artifactUpdater, Logger logger, FileSystem fileSystem, OperatingSystemUtils operatingSystemUtils, {bool offline = false}) async {
}
}
/// A fake process implementation which can be provided all necessary values.
class FakeProcess implements Process {
FakeProcess({
this.pid = 1,
Future<int>? exitCode,
IOSink? stdin,
this.stdout = const Stream<List<int>>.empty(),
this.stderr = const Stream<List<int>>.empty(),
}) : exitCode = exitCode ?? Future<int>.value(0),
stdin = stdin ?? MemoryIOSink();
@override
final int pid;
@override
final Future<int> exitCode;
@override
final io.IOSink stdin;
@override
final Stream<List<int>> stdout;
@override
final Stream<List<int>> stderr;
@override
bool kill([io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
return true;
}
}
/// An IOSink that completes a future with the first line written to it.
class CompleterIOSink extends MemoryIOSink {
CompleterIOSink({
this.throwOnAdd = false,
});
final bool throwOnAdd;
final Completer<List<int>> _completer = Completer<List<int>>();
Future<List<int>> get future => _completer.future;
@override
void add(List<int> data) {
if (!_completer.isCompleted) {
// When throwOnAdd is true, complete with empty so any expected output
// doesn't appear.
_completer.complete(throwOnAdd ? <int>[] : data);
}
if (throwOnAdd) {
throw Exception('CompleterIOSink Error');
}
super.add(data);
}
}
/// An IOSink that collects whatever is written to it.
class MemoryIOSink implements IOSink {
@override
Encoding encoding = utf8;
final List<List<int>> writes = <List<int>>[];
@override
void add(List<int> data) {
writes.add(data);
}
@override
Future<void> addStream(Stream<List<int>> stream) {
final Completer<void> completer = Completer<void>();
late StreamSubscription<List<int>> sub;
sub = stream.listen(
(List<int> data) {
try {
add(data);
// Catches all exceptions to propagate them to the completer.
} catch (err, stack) { // ignore: avoid_catches_without_on_clauses
sub.cancel();
completer.completeError(err, stack);
}
},
onError: completer.completeError,
onDone: completer.complete,
cancelOnError: true,
);
return completer.future;
}
@override
void writeCharCode(int charCode) {
add(<int>[charCode]);
}
@override
void write(Object? obj) {
add(encoding.encode('$obj'));
}
@override
void writeln([ Object? obj = '' ]) {
add(encoding.encode('$obj\n'));
}
@override
void writeAll(Iterable<dynamic> objects, [ String separator = '' ]) {
bool addSeparator = false;
for (final dynamic object in objects) {
if (addSeparator) {
write(separator);
}
write(object);
addSeparator = true;
}
}
@override
void addError(dynamic error, [ StackTrace? stackTrace ]) {
throw UnimplementedError();
}
@override
Future<void> get done => close();
@override
Future<void> close() async { }
@override
Future<void> flush() async { }
void clear() {
writes.clear();
}
String getAndClear() {
final String result = utf8.decode(writes.expand((List<int> l) => l).toList());
clear();
return result;
}
}
class MemoryStdout extends MemoryIOSink implements io.Stdout {
@override
bool get hasTerminal => _hasTerminal;
set hasTerminal(bool value) {
_hasTerminal = value;
}
bool _hasTerminal = true;
@override
// ignore: override_on_non_overriding_member
String get lineTerminator => '\n';
@override
// ignore: override_on_non_overriding_member
set lineTerminator(String value) {
throw UnimplementedError('Setting the line terminator is not supported');
}
@override
io.IOSink get nonBlocking => this;
@override
bool get supportsAnsiEscapes => _supportsAnsiEscapes;
set supportsAnsiEscapes(bool value) {
_supportsAnsiEscapes = value;
}
bool _supportsAnsiEscapes = true;
@override
int get terminalColumns {
if (_terminalColumns != null) {
return _terminalColumns!;
}
throw const io.StdoutException('unspecified mock value');
}
set terminalColumns(int value) => _terminalColumns = value;
int? _terminalColumns;
@override
int get terminalLines {
if (_terminalLines != null) {
return _terminalLines!;
}
throw const io.StdoutException('unspecified mock value');
}
set terminalLines(int value) => _terminalLines = value;
int? _terminalLines;
}
/// A Stdio that collects stdout and supports simulated stdin.
class FakeStdio extends Stdio {
final MemoryStdout _stdout = MemoryStdout()..terminalColumns = 80;
final MemoryIOSink _stderr = MemoryIOSink();
final FakeStdin _stdin = FakeStdin();
@override
MemoryStdout get stdout => _stdout;
@override
MemoryIOSink get stderr => _stderr;
@override
Stream<List<int>> get stdin => _stdin;
void simulateStdin(String line) {
_stdin.controller.add(utf8.encode('$line\n'));
}
@override
bool hasTerminal = false;
List<String> get writtenToStdout => _stdout.writes.map<String>(_stdout.encoding.decode).toList();
List<String> get writtenToStderr => _stderr.writes.map<String>(_stderr.encoding.decode).toList();
}
class FakeStdin extends Fake implements Stdin {
final StreamController<List<int>> controller = StreamController<List<int>>();
void Function(bool mode)? echoModeCallback;
bool _echoMode = true;
@override
bool get echoMode => _echoMode;
@override
set echoMode(bool mode) {
_echoMode = mode;
if (echoModeCallback != null) {
echoModeCallback!(mode);
}
}
@override
bool lineMode = true;
@override
bool hasTerminal = false;
@override
Stream<S> transform<S>(StreamTransformer<List<int>, S> transformer) {
return controller.stream.transform(transformer);
}
@override
StreamSubscription<List<int>> listen(
void Function(List<int> event)? onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
return controller.stream.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
}
class FakePlistParser implements PlistParser {
FakePlistParser([Map<String, Object>? underlyingValues]):
_underlyingValues = underlyingValues ?? <String, Object>{};
final Map<String, Object> _underlyingValues;
void setProperty(String key, Object value) {
_underlyingValues[key] = value;
}
@override
String? plistXmlContent(String plistFilePath) => throw UnimplementedError();
@override
String? plistJsonContent(String filePath, {bool sorted = false}) {
throw UnimplementedError();
}
@override
Map<String, Object> parseFile(String plistFilePath) {
return _underlyingValues;
}
@override
T? getValueFromFile<T>(String plistFilePath, String key) {
return _underlyingValues[key] as T?;
}
@override
bool replaceKey(String plistFilePath, {required String key, String? value}) {
if (value == null) {
_underlyingValues.remove(key);
return true;
}
setProperty(key, value);
return true;
}
}
class FakeBotDetector implements BotDetector {
const FakeBotDetector(bool isRunningOnBot)
: _isRunningOnBot = isRunningOnBot;
@override
Future<bool> get isRunningOnBot async => _isRunningOnBot;
final bool _isRunningOnBot;
}
class FakeFlutterVersion implements FlutterVersion {
FakeFlutterVersion({
this.branch = 'master',
this.dartSdkVersion = '12',
this.devToolsVersion = '2.8.0',
this.engineRevision = 'abcdefghijklmnopqrstuvwxyz',
this.engineRevisionShort = 'abcde',
this.repositoryUrl = 'https://github.com/flutter/flutter.git',
this.frameworkVersion = '0.0.0',
this.frameworkRevision = '11111111111111111111',
this.frameworkRevisionShort = '11111',
this.frameworkAge = '0 hours ago',
this.frameworkCommitDate = '12/01/01',
this.gitTagVersion = const GitTagVersion.unknown(),
this.flutterRoot = '/path/to/flutter',
this.nextFlutterVersion,
});
final String branch;
bool get didFetchTagsAndUpdate => _didFetchTagsAndUpdate;
bool _didFetchTagsAndUpdate = false;
/// Will be returned by [fetchTagsAndGetVersion] if not null.
final FlutterVersion? nextFlutterVersion;
@override
FlutterVersion fetchTagsAndGetVersion({
SystemClock clock = const SystemClock(),
}) {
_didFetchTagsAndUpdate = true;
return nextFlutterVersion ?? this;
}
bool get didCheckFlutterVersionFreshness => _didCheckFlutterVersionFreshness;
bool _didCheckFlutterVersionFreshness = false;
@override
String get channel {
if (kOfficialChannels.contains(branch) || kObsoleteBranches.containsKey(branch)) {
return branch;
}
return kUserBranch;
}
@override
final String flutterRoot;
@override
final String devToolsVersion;
@override
final String dartSdkVersion;
@override
final String engineRevision;
@override
final String engineRevisionShort;
@override
final String? repositoryUrl;
@override
final String frameworkVersion;
@override
final String frameworkRevision;
@override
final String frameworkRevisionShort;
@override
final String frameworkAge;
@override
final String frameworkCommitDate;
@override
final GitTagVersion gitTagVersion;
@override
FileSystem get fs => throw UnimplementedError('FakeFlutterVersion.fs is not implemented');
@override
Future<void> checkFlutterVersionFreshness() async {
_didCheckFlutterVersionFreshness = true;
}
@override
Future<void> ensureVersionFile() async { }
@override
String getBranchName({bool redactUnknownBranches = false}) {
if (!redactUnknownBranches || kOfficialChannels.contains(branch) || kObsoleteBranches.containsKey(branch)) {
return branch;
}
return kUserBranch;
}
@override
String getVersionString({bool redactUnknownBranches = false}) {
return '${getBranchName(redactUnknownBranches: redactUnknownBranches)}/$frameworkRevision';
}
@override
Map<String, Object> toJson() {
return <String, Object>{};
}
}
// A test implementation of [FeatureFlags] that allows enabling without reading
// config. If not otherwise specified, all values default to false.
class TestFeatureFlags implements FeatureFlags {
TestFeatureFlags({
this.isLinuxEnabled = false,
this.isMacOSEnabled = false,
this.isWebEnabled = false,
this.isWindowsEnabled = false,
this.isAndroidEnabled = true,
this.isIOSEnabled = true,
this.isFuchsiaEnabled = false,
this.areCustomDevicesEnabled = false,
this.isCliAnimationEnabled = true,
this.isNativeAssetsEnabled = false,
this.isPreviewDeviceEnabled = false,
this.isSwiftPackageManagerEnabled = false,
});
@override
final bool isLinuxEnabled;
@override
final bool isMacOSEnabled;
@override
final bool isWebEnabled;
@override
final bool isWindowsEnabled;
@override
final bool isAndroidEnabled;
@override
final bool isIOSEnabled;
@override
final bool isFuchsiaEnabled;
@override
final bool areCustomDevicesEnabled;
@override
final bool isCliAnimationEnabled;
@override
final bool isNativeAssetsEnabled;
@override
final bool isPreviewDeviceEnabled;
@override
final bool isSwiftPackageManagerEnabled;
@override
bool isEnabled(Feature feature) {
return switch (feature) {
flutterWebFeature => isWebEnabled,
flutterLinuxDesktopFeature => isLinuxEnabled,
flutterMacOSDesktopFeature => isMacOSEnabled,
flutterWindowsDesktopFeature => isWindowsEnabled,
flutterAndroidFeature => isAndroidEnabled,
flutterIOSFeature => isIOSEnabled,
flutterFuchsiaFeature => isFuchsiaEnabled,
flutterCustomDevicesFeature => areCustomDevicesEnabled,
cliAnimation => isCliAnimationEnabled,
nativeAssets => isNativeAssetsEnabled,
_ => false,
};
}
}
class FakeOperatingSystemUtils extends Fake implements OperatingSystemUtils {
FakeOperatingSystemUtils({this.hostPlatform = HostPlatform.linux_x64});
final List<List<String>> chmods = <List<String>>[];
@override
void makeExecutable(File file) { }
@override
HostPlatform hostPlatform = HostPlatform.linux_x64;
@override
void chmod(FileSystemEntity entity, String mode) {
chmods.add(<String>[entity.path, mode]);
}
@override
File? which(String execName) => null;
@override
List<File> whichAll(String execName) => <File>[];
@override
int? getDirectorySize(Directory directory) => 10000000; // 10 MB / 9.5 MiB
@override
void unzip(File file, Directory targetDirectory) { }
@override
void unpack(File gzippedTarFile, Directory targetDirectory) { }
@override
Stream<List<int>> gzipLevel1Stream(Stream<List<int>> stream) => stream;
@override
String get name => 'fake OS name and version';
@override
String get pathVarSeparator => ';';
@override
Future<int> findFreePort({bool ipv6 = false}) async => 12345;
}
class FakeStopwatch implements Stopwatch {
@override
bool get isRunning => _isRunning;
bool _isRunning = false;
@override
void start() => _isRunning = true;
@override
void stop() => _isRunning = false;
@override
Duration elapsed = Duration.zero;
@override
int get elapsedMicroseconds => elapsed.inMicroseconds;
@override
int get elapsedMilliseconds => elapsed.inMilliseconds;
@override
int get elapsedTicks => elapsed.inMilliseconds;
@override
int get frequency => 1000;
@override
void reset() {
_isRunning = false;
elapsed = Duration.zero;
}
@override
String toString() => '$runtimeType $elapsed $isRunning';
}
class FakeStopwatchFactory implements StopwatchFactory {
FakeStopwatchFactory({
Stopwatch? stopwatch,
Map<String, Stopwatch>? stopwatches
}) : stopwatches = <String, Stopwatch>{
if (stopwatches != null) ...stopwatches,
if (stopwatch != null) '': stopwatch,
};
Map<String, Stopwatch> stopwatches;
@override
Stopwatch createStopwatch([String name = '']) {
return stopwatches[name] ?? FakeStopwatch();
}
}
class FakeFlutterProjectFactory implements FlutterProjectFactory {
@override
FlutterProject fromDirectory(Directory directory) {
return FlutterProject.fromDirectoryTest(directory);
}
@override
Map<String, FlutterProject> get projects => throw UnimplementedError();
}
class FakeAndroidSdk extends Fake implements AndroidSdk {
@override
late bool platformToolsAvailable;
@override
late bool licensesAvailable;
@override
AndroidSdkVersion? latestVersion;
}
class FakeAndroidStudio extends Fake implements AndroidStudio {
@override
String get javaPath => 'java';
}
class FakeJava extends Fake implements Java {
FakeJava({
this.javaHome = '/android-studio/jbr',
String binary = '/android-studio/jbr/bin/java',
Version? version,
bool canRun = true,
}): binaryPath = binary,
version = version ?? const Version.withText(19, 0, 2, 'openjdk 19.0.2 2023-01-17'),
_environment = <String, String>{
if (javaHome != null) Java.javaHomeEnvironmentVariable: javaHome,
'PATH': '/android-studio/jbr/bin',
},
_canRun = canRun;
@override
String? javaHome;
@override
String binaryPath;
final Map<String, String> _environment;
final bool _canRun;
@override
Map<String, String> get environment => _environment;
@override
Version? version;
@override
bool canRun() {
return _canRun;
}
}
class FakeDevtoolsLauncher extends Fake implements DevtoolsLauncher {
FakeDevtoolsLauncher({DevToolsServerAddress? serverAddress})
: _serverAddress = serverAddress;
@override
Future<void> get processStart => _processStarted.future;
final Completer<void> _processStarted = Completer<void>();
@override
Future<void> get ready => readyCompleter.future;
Completer<void> readyCompleter = Completer<void>()..complete();
@override
DevToolsServerAddress? activeDevToolsServer;
@override
Uri? devToolsUrl;
@override
Uri? dtdUri;
@override
bool printDtdUri = false;
final DevToolsServerAddress? _serverAddress;
@override
Future<DevToolsServerAddress?> serve() async => _serverAddress;
@override
Future<void> launch(Uri vmServiceUri, {List<String>? additionalArguments}) {
_processStarted.complete();
return Completer<void>().future;
}
bool closed = false;
@override
Future<void> close() async {
closed = true;
}
}
/// A fake [Logger] that throws the [Invocation] for any method call.
class FakeLogger implements Logger {
@override
dynamic noSuchMethod(Invocation invocation) => throw invocation; // ignore: only_throw_errors
}
class ClosedStdinController extends Fake implements StreamSink<List<int>> {
@override
Future<Object?> addStream(Stream<List<int>> stream) async => throw const SocketException('Bad pipe');
@override
Future<Object?> close() async {
return null;
}
}