Unverified Commit 3e838da9 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] use flutter tool handler for dwds resources and precache tool...

[flutter_tools] use flutter tool handler for dwds resources and precache tool pub dependencies (#65814)

If the tool is downloaded from a precompiled snapshot, or if the backing source files in the pub cache are deleted, the dwds debugging functionality will break as the client.js file cannot be located. Instead use the PackageConfig to verify that package location, downloading if it is missing.

Override the dwds middleware to avoid Isolate.resolvePackageUri

Fixes #53644
Fixes #65475
parent b1d17c91
......@@ -7,7 +7,7 @@ import 'dart:typed_data';
import 'package:dwds/data/build_result.dart';
import 'package:dwds/dwds.dart';
import 'package:logging/logging.dart';
import 'package:logging/logging.dart' as logging;
import 'package:meta/meta.dart';
import 'package:mime/mime.dart' as mime;
import 'package:package_config/package_config.dart';
......@@ -19,6 +19,7 @@ import '../asset.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/net.dart';
import '../base/platform.dart';
import '../base/utils.dart';
......@@ -45,7 +46,7 @@ typedef DwdsLauncher = Future<Dwds> Function({
bool useSseForDebugProxy,
bool useSseForDebugBackend,
bool serveDevTools,
void Function(Level, String) logWriter,
void Function(logging.Level, String) logWriter,
bool verbose,
UrlEncoder urlEncoder,
bool useFileProvider,
......@@ -227,6 +228,21 @@ class WebAssetServer implements AssetReader {
}
return null;
}
// Ensure dwds is present and provide middleware to avoid trying to
// load the through the isolate APIs.
final Directory directory = await _loadDwdsDirectory(globals.fs, globals.logger);
final shelf.Middleware middleware = (FutureOr<shelf.Response> Function(shelf.Request) innerHandler) {
return (shelf.Request request) async {
if (request.url.path.endsWith('dwds/src/injected/client.js')) {
final Uri uri = directory.uri.resolve('src/injected/client.js');
final String result = await globals.fs.file(uri.toFilePath()).readAsString();
return shelf.Response.ok(result, headers: <String, String>{
HttpHeaders.contentTypeHeader: 'application/javascript'
});
}
return innerHandler(request);
};
};
// In debug builds, spin up DWDS and the full asset server.
final Dwds dwds = await dwdsLauncher(
......@@ -243,7 +259,7 @@ class WebAssetServer implements AssetReader {
useSseForDebugProxy: useSseForDebugProxy,
useSseForDebugBackend: useSseForDebugBackend,
serveDevTools: false,
logWriter: (Level logLevel, String message) => globals.printTrace(message),
logWriter: (logging.Level logLevel, String message) => globals.printTrace(message),
loadStrategy: RequireStrategy(
ReloadConfiguration.none,
'.lib.js',
......@@ -258,6 +274,7 @@ class WebAssetServer implements AssetReader {
);
shelf.Pipeline pipeline = const shelf.Pipeline();
if (enableDwds) {
pipeline = pipeline.addMiddleware(middleware);
pipeline = pipeline.addMiddleware(dwds.middleware);
}
final shelf.Handler dwdsHandler = pipeline.addHandler(server.handleRequest);
......@@ -946,3 +963,14 @@ class ReleaseAssetServer {
return shelf.Response.notFound('');
}
}
Future<Directory> _loadDwdsDirectory(FileSystem fileSystem, Logger logger) async {
final String toolPackagePath = fileSystem.path.join(
Cache.flutterRoot, 'packages', 'flutter_tools');
final String packageFilePath = fileSystem.path.join(toolPackagePath, kPackagesFileName);
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
fileSystem.file(packageFilePath),
logger: logger,
);
return fileSystem.directory(packageConfig['dwds'].packageUriRoot);
}
......@@ -22,9 +22,7 @@ import '../base/terminal.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../build_system/targets/web.dart';
import '../cache.dart';
import '../dart/language_version.dart';
import '../dart/pub.dart';
import '../devfs.dart';
import '../device.dart';
import '../features.dart';
......@@ -444,15 +442,6 @@ class _ResidentWebRunner extends ResidentWebRunner {
try {
return await asyncGuard(() async {
// Ensure dwds resources are cached. If the .packages file is missing then
// the client.js script cannot be located by the injected handler in dwds.
// This will result in a NoSuchMethodError thrown by injected_handler.darts
await pub.get(
context: PubContext.pubGet,
directory: globals.fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools'),
generateSyntheticPackage: false,
);
final ExpressionCompiler expressionCompiler =
debuggingOptions.webEnableExpressionEvaluation
? WebExpressionCompiler(device.generator)
......
......@@ -4,6 +4,7 @@
import 'package:archive/archive.dart';
import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'android/gradle_utils.dart';
import 'base/common.dart';
......@@ -14,8 +15,11 @@ import 'base/net.dart';
import 'base/os.dart' show OperatingSystemUtils;
import 'base/platform.dart';
import 'base/process.dart';
import 'dart/package_map.dart';
import 'dart/pub.dart';
import 'features.dart';
import 'globals.dart' as globals;
import 'runner/flutter_command.dart';
/// A tag for a set of development artifacts that need to be cached.
class DevelopmentArtifact {
......@@ -126,6 +130,14 @@ class Cache {
_artifacts.add(IosUsbArtifacts(artifactName, this));
}
_artifacts.add(FontSubsetArtifacts(this));
_artifacts.add(PubDependencies(
fileSystem: _fileSystem,
logger: _logger,
// flutter root and pub must be lazily initialized to avoid accessing
// before the version is determined.
flutterRoot: () => flutterRoot,
pub: () => pub,
));
} else {
_artifacts.addAll(artifacts);
}
......@@ -432,7 +444,14 @@ class Cache {
);
}
bool isUpToDate() => _artifacts.every((ArtifactSet artifact) => artifact.isUpToDate());
Future<bool> isUpToDate() async {
for (final ArtifactSet artifact in _artifacts) {
if (!await artifact.isUpToDate()) {
return false;
}
}
return true;
}
/// Update the cache to contain all `requiredArtifacts`.
Future<void> updateAll(Set<DevelopmentArtifact> requiredArtifacts) async {
......@@ -444,7 +463,7 @@ class Cache {
_logger.printTrace('Artifact $artifact is not required, skipping update.');
continue;
}
if (artifact.isUpToDate()) {
if (await artifact.isUpToDate()) {
continue;
}
try {
......@@ -502,7 +521,7 @@ abstract class ArtifactSet {
final DevelopmentArtifact developmentArtifact;
/// [true] if the artifact is up to date.
bool isUpToDate();
Future<bool> isUpToDate();
/// The environment variables (if any) required to consume the artifacts.
Map<String, String> get environment {
......@@ -546,7 +565,7 @@ abstract class CachedArtifact extends ArtifactSet {
}
@override
bool isUpToDate() {
Future<bool> isUpToDate() async {
if (!location.existsSync()) {
return false;
}
......@@ -592,6 +611,66 @@ abstract class CachedArtifact extends ArtifactSet {
Uri _toStorageUri(String path) => Uri.parse('${cache.storageBaseUrl}/$path');
}
/// Ensures that the source files for all of the dependencies for the
/// flutter_tool are present.
///
/// This does not handle cases wheere the source files are modified or the
/// directory contents are incomplete.
class PubDependencies extends ArtifactSet {
PubDependencies({
// Needs to be lazy to avoid reading from the cache before the root is initialized.
@required String Function() flutterRoot,
@required FileSystem fileSystem,
@required Logger logger,
@required Pub Function() pub,
}) : _logger = logger,
_fileSystem = fileSystem,
_flutterRoot = flutterRoot,
_pub = pub,
super(DevelopmentArtifact.universal);
final String Function() _flutterRoot;
final FileSystem _fileSystem;
final Logger _logger;
final Pub Function() _pub;
@override
Future<bool> isUpToDate() async {
final File toolPackageConfig = _fileSystem.file(
_fileSystem.path.join(_flutterRoot(), 'packages', 'flutter_tools', kPackagesFileName),
);
if (!toolPackageConfig.existsSync()) {
return false;
}
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
toolPackageConfig,
logger: _logger,
throwOnError: false,
);
if (packageConfig == null || packageConfig == PackageConfig.empty ) {
return false;
}
for (final Package package in packageConfig.packages) {
if (!_fileSystem.directory(package.packageUriRoot).existsSync()) {
return false;
}
}
return true;
}
@override
String get name => 'pub_dependencies';
@override
Future<void> update(ArtifactUpdater artifactUpdater) async {
await _pub().get(
context: PubContext.pubGet,
directory: _fileSystem.path.join(_flutterRoot(), 'packages', 'flutter_tools'),
generateSyntheticPackage: false,
);
}
}
/// A cached artifact containing fonts used for Material Design.
class MaterialFonts extends CachedArtifact {
MaterialFonts(Cache cache) : super(
......@@ -979,7 +1058,7 @@ class AndroidMavenArtifacts extends ArtifactSet {
}
@override
bool isUpToDate() {
Future<bool> isUpToDate() async {
// The dependencies are downloaded and cached by Gradle.
// The tool doesn't know if the dependencies are already cached at this point.
// Therefore, call Gradle to figure this out.
......
......@@ -156,7 +156,7 @@ class PrecacheCommand extends FlutterCommand {
requiredArtifacts.add(artifact);
}
}
if (!_cache.isUpToDate()) {
if (!await _cache.isUpToDate()) {
await _cache.updateAll(requiredArtifacts);
} else {
_logger.printStatus('Already up-to-date.');
......
......@@ -232,7 +232,8 @@ Future<T> runInContext<T>(
botDetector: globals.botDetector,
platform: globals.platform,
usage: globals.flutterUsage,
toolStampFile: globals.cache.getStampFileFor('flutter_tools'),
// Avoid a circular dependency by making this access lazy.
toolStampFile: () => globals.cache.getStampFileFor('flutter_tools'),
),
ShutdownHooks: () => ShutdownHooks(logger: globals.logger),
Stdio: () => Stdio(),
......
......@@ -80,7 +80,7 @@ abstract class Pub {
@required Platform platform,
@required BotDetector botDetector,
@required Usage usage,
File toolStampFile,
File Function() toolStampFile,
}) = _DefaultPub;
/// Runs `pub get`.
......@@ -141,7 +141,7 @@ class _DefaultPub implements Pub {
@required Platform platform,
@required BotDetector botDetector,
@required Usage usage,
File toolStampFile,
File Function() toolStampFile,
}) : _toolStampFile = toolStampFile,
_fileSystem = fileSystem,
_logger = logger,
......@@ -159,7 +159,7 @@ class _DefaultPub implements Pub {
final Platform _platform;
final BotDetector _botDetector;
final Usage _usage;
final File _toolStampFile;
final File Function() _toolStampFile;
@override
Future<void> get({
......@@ -399,10 +399,10 @@ class _DefaultPub implements Pub {
if (pubSpecYaml.lastModifiedSync().isAfter(dotPackagesLastModified)) {
return true;
}
if (_toolStampFile != null &&
_toolStampFile.existsSync() &&
_toolStampFile.lastModifiedSync().isAfter(dotPackagesLastModified)) {
final File toolStampFile = _toolStampFile != null ? _toolStampFile() : null;
if (toolStampFile != null &&
toolStampFile.existsSync() &&
toolStampFile.lastModifiedSync().isAfter(dotPackagesLastModified)) {
return true;
}
return false;
......
......@@ -291,36 +291,13 @@ Future<Directory> _templateImageDirectory(String name, FileSystem fileSystem, Lo
final String toolPackagePath = fileSystem.path.join(
Cache.flutterRoot, 'packages', 'flutter_tools');
final String packageFilePath = fileSystem.path.join(toolPackagePath, kPackagesFileName);
// Ensure that .packgaes is present.
if (!fileSystem.file(packageFilePath).existsSync()) {
await _ensurePackageDependencies(toolPackagePath, pub);
}
PackageConfig packageConfig = await loadPackageConfigWithLogging(
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
fileSystem.file(packageFilePath),
logger: logger,
);
Uri imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
// Ensure that the template image package is present.
if (imagePackageLibDir == null || !fileSystem.directory(imagePackageLibDir).existsSync()) {
await _ensurePackageDependencies(toolPackagePath, pub);
packageConfig = await loadPackageConfigWithLogging(
fileSystem.file(packageFilePath),
logger: logger,
);
imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
}
final Uri imagePackageLibDir = packageConfig['flutter_template_images']?.packageUriRoot;
return fileSystem.directory(imagePackageLibDir)
.parent
.childDirectory('templates')
.childDirectory(name);
}
// Runs 'pub get' for the given path to ensure that .packages is created and
// all dependencies are present.
Future<void> _ensurePackageDependencies(String packagePath, Pub pub) async {
await pub.get(
context: PubContext.pubGet,
directory: packagePath,
generateSyntheticPackage: false,
);
}
......@@ -117,7 +117,7 @@ void main() {
platform: const LocalPlatform(),
usage: globals.flutterUsage,
botDetector: globals.botDetector,
toolStampFile: globals.fs.file('test'),
toolStampFile: () => globals.fs.file('test'),
);
await pub.get(
context: PubContext.flutterTests,
......
......@@ -23,7 +23,7 @@ void main() {
// Release lock between test cases.
Cache.releaseLock();
when(cache.isUpToDate()).thenReturn(false);
when(cache.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(false));
when(cache.updateAll(any)).thenAnswer((Invocation invocation) {
artifacts = invocation.positionalArguments.first as Set<DevelopmentArtifact>;
return Future<void>.value(null);
......@@ -410,7 +410,7 @@ void main() {
});
testUsingContext('precache deletes artifact stampfiles when --force is provided', () async {
when(cache.isUpToDate()).thenReturn(true);
when(cache.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(true));
final PrecacheCommand command = PrecacheCommand(
cache: cache,
logger: BufferLogger.test(),
......
......@@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/dart/pub.dart';
import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:meta/meta.dart';
import 'package:mockito/mockito.dart';
......@@ -137,25 +138,25 @@ void main() {
ProcessManager: () => FakeProcessManager.any(),
});
testUsingContext('should not be up to date, if some cached artifact is not', () {
testUsingContext('should not be up to date, if some cached artifact is not', () async {
final CachedArtifact artifact1 = MockCachedArtifact();
final CachedArtifact artifact2 = MockCachedArtifact();
when(artifact1.isUpToDate()).thenReturn(true);
when(artifact2.isUpToDate()).thenReturn(false);
when(artifact1.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(true));
when(artifact2.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(false));
final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
expect(cache.isUpToDate(), isFalse);
expect(await cache.isUpToDate(), isFalse);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => MemoryFileSystem.test(),
});
testUsingContext('should be up to date, if all cached artifacts are', () {
testUsingContext('should be up to date, if all cached artifacts are', () async {
final CachedArtifact artifact1 = MockCachedArtifact();
final CachedArtifact artifact2 = MockCachedArtifact();
when(artifact1.isUpToDate()).thenReturn(true);
when(artifact2.isUpToDate()).thenReturn(true);
when(artifact1.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(true));
when(artifact2.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(true));
final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
expect(cache.isUpToDate(), isTrue);
expect(await cache.isUpToDate(), isTrue);
}, overrides: <Type, Generator>{
ProcessManager: () => FakeProcessManager.any(),
FileSystem: () => MemoryFileSystem.test(),
......@@ -164,8 +165,8 @@ void main() {
testUsingContext('should update cached artifacts which are not up to date', () async {
final CachedArtifact artifact1 = MockCachedArtifact();
final CachedArtifact artifact2 = MockCachedArtifact();
when(artifact1.isUpToDate()).thenReturn(true);
when(artifact2.isUpToDate()).thenReturn(false);
when(artifact1.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(true));
when(artifact2.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(false));
final Cache cache = Cache(artifacts: <CachedArtifact>[artifact1, artifact2]);
await cache.updateAll(<DevelopmentArtifact>{
null,
......@@ -206,8 +207,8 @@ void main() {
testUsingContext('failed storage.googleapis.com download shows China warning', () async {
final CachedArtifact artifact1 = MockCachedArtifact();
final CachedArtifact artifact2 = MockCachedArtifact();
when(artifact1.isUpToDate()).thenReturn(false);
when(artifact2.isUpToDate()).thenReturn(false);
when(artifact1.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(false));
when(artifact2.isUpToDate()).thenAnswer((Invocation _) => Future<bool>.value(false));
final MockInternetAddress address = MockInternetAddress();
when(address.host).thenReturn('storage.googleapis.com');
when(artifact1.update(any)).thenThrow(SocketException(
......@@ -317,7 +318,7 @@ void main() {
..createSync(recursive: true);
when(mockCache.getRoot()).thenReturn(cacheRoot);
final AndroidMavenArtifacts mavenArtifacts = AndroidMavenArtifacts(mockCache);
expect(mavenArtifacts.isUpToDate(), isFalse);
expect(await mavenArtifacts.isUpToDate(), isFalse);
final Directory gradleWrapperDir = globals.fs.systemTempDirectory.createTempSync('flutter_cache_test_gradle_wrapper.');
when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(gradleWrapperDir);
......@@ -340,7 +341,7 @@ void main() {
await mavenArtifacts.update(MockArtifactUpdater());
expect(mavenArtifacts.isUpToDate(), isFalse);
expect(await mavenArtifacts.isUpToDate(), isFalse);
}, overrides: <Type, Generator>{
Cache: () => mockCache,
FileSystem: () => memoryFileSystem,
......@@ -660,6 +661,69 @@ void main() {
expect(cache.getStampFor('foo'), 'ABC');
});
testWithoutContext('PubDependencies needs to be updated if the package config'
' file or the source directories are missing', () async {
final BufferLogger logger = BufferLogger.test();
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final PubDependencies pubDependencies = PubDependencies(
flutterRoot: () => '',
fileSystem: fileSystem,
logger: logger,
pub: () => MockPub(),
);
expect(await pubDependencies.isUpToDate(), false); // no package config
fileSystem.file('packages/flutter_tools/.packages')
..createSync(recursive: true)
..writeAsStringSync('\n');
fileSystem.file('packages/flutter_tools/.dart_tool/package_config.json')
..createSync(recursive: true)
..writeAsStringSync('''
{
"configVersion": 2,
"packages": [
{
"name": "example",
"rootUri": "file:///.pub-cache/hosted/pub.dartlang.org/example-7.0.0",
"packageUri": "lib/",
"languageVersion": "2.7"
}
],
"generated": "2020-09-15T20:29:20.691147Z",
"generator": "pub",
"generatorVersion": "2.10.0-121.0.dev"
}
''');
expect(await pubDependencies.isUpToDate(), false); // dependencies are missing.
fileSystem.file('.pub-cache/hosted/pub.dartlang.org/example-7.0.0/lib/foo.dart')
.createSync(recursive: true);
expect(await pubDependencies.isUpToDate(), true);
});
testWithoutContext('PubDependencies updates via pub get', () async {
final BufferLogger logger = BufferLogger.test();
final MemoryFileSystem fileSystem = MemoryFileSystem.test();
final MockPub pub = MockPub();
final PubDependencies pubDependencies = PubDependencies(
flutterRoot: () => '',
fileSystem: fileSystem,
logger: logger,
pub: () => pub,
);
await pubDependencies.update(MockArtifactUpdater());
verify(pub.get(
context: PubContext.pubGet,
directory: 'packages/flutter_tools',
generateSyntheticPackage: false,
)).called(1);
});
}
class FakeCachedArtifact extends EngineCachedArtifact {
......@@ -724,6 +788,7 @@ class MockInternetAddress extends Mock implements InternetAddress {}
class MockCache extends Mock implements Cache {}
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
class MockVersionedPackageResolver extends Mock implements VersionedPackageResolver {}
class MockPub extends Mock implements Pub {}
class FakeCache extends Cache {
FakeCache({
@required Logger logger,
......
......@@ -294,12 +294,6 @@ void main() {
verify(mockAppConnection.runMain()).called(1);
verify(status.stop()).called(1);
verify(pub.get(
context: PubContext.pubGet,
directory: anyNamed('directory'),
generateSyntheticPackage: false,
)).called(1);
expect(bufferLogger.statusText, contains('Debug service listening on ws://127.0.0.1/abcd/'));
expect(debugConnectionInfo.wsUri.toString(), 'ws://127.0.0.1/abcd/');
}, overrides: <Type, Generator>{
......
......@@ -931,7 +931,7 @@ class FakeCache implements Cache {
}
@override
bool isUpToDate() {
Future<bool> isUpToDate() async {
return true;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment