Unverified Commit 943b41bd authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] allow device classes to provide platform-specific interface for devFS Sync (#66266)

parent 1b60988f
......@@ -776,20 +776,21 @@ class WebDevFS implements DevFS {
@override
Future<UpdateFSReport> update({
Uri mainUri,
@required Uri mainUri,
@required ResidentCompiler generator,
@required bool trackWidgetCreation,
@required String pathToReload,
@required List<Uri> invalidatedFiles,
@required PackageConfig packageConfig,
DevFSWriter devFSWriter,
String target,
AssetBundle bundle,
DateTime firstBuildTime,
bool bundleFirstUpload = false,
@required ResidentCompiler generator,
String dillOutputPath,
@required bool trackWidgetCreation,
bool fullRestart = false,
String projectRootPath,
String pathToReload,
List<Uri> invalidatedFiles,
bool skipAssets = false,
@required PackageConfig packageConfig,
}) async {
assert(trackWidgetCreation != null);
assert(generator != null);
......
......@@ -696,6 +696,7 @@ class _ResidentWebRunner extends ResidentWebRunner {
invalidatedFiles: invalidationResult.uris,
packageConfig: invalidationResult.packageConfig,
trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation,
devFSWriter: null,
);
devFSStatus.stop();
globals.printTrace('Synced ${getSizeAsMB(report.syncedBytes)}.');
......
......@@ -9,10 +9,12 @@ import 'package:process/process.dart';
import 'application_package.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'base/io.dart';
import 'base/logger.dart';
import 'build_info.dart';
import 'convert.dart';
import 'devfs.dart';
import 'device.dart';
import 'globals.dart' as globals;
import 'protocol_discovery.dart';
......@@ -25,8 +27,10 @@ abstract class DesktopDevice extends Device {
@required bool ephemeral,
Logger logger,
ProcessManager processManager,
FileSystem fileSystem,
}) : _logger = logger ?? globals.logger, // TODO(jonahwilliams): remove after updating google3
_processManager = processManager ?? globals.processManager,
_fileSystem = fileSystem ?? globals.fs,
super(
identifier,
category: Category.desktop,
......@@ -36,9 +40,13 @@ abstract class DesktopDevice extends Device {
final Logger _logger;
final ProcessManager _processManager;
final FileSystem _fileSystem;
final Set<Process> _runningProcesses = <Process>{};
final DesktopLogReader _deviceLogReader = DesktopLogReader();
DevFSWriter get devFSWriter => _desktopDevFSWriter ??= LocalDevFSWriter(fileSystem: _fileSystem);
LocalDevFSWriter _desktopDevFSWriter;
// Since the host and target devices are the same, no work needs to be done
// to install the application.
@override
......
......@@ -209,7 +209,17 @@ class DevFSException implements Exception {
final StackTrace stackTrace;
}
class _DevFSHttpWriter {
/// Interface responsible for syncing asset files to a development device.
abstract class DevFSWriter {
/// Write the assets in [entries] to the target device.
///
/// The keys of the map are relative from the [baseUri].
///
/// Throws a [DevFSException] if the process fails to complete.
Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, DevFSWriter parent);
}
class _DevFSHttpWriter implements DevFSWriter {
_DevFSHttpWriter(
this.fsName,
vm_service.VmService serviceProtocol, {
......@@ -235,12 +245,21 @@ class _DevFSHttpWriter {
Map<Uri, DevFSContent> _outstanding;
Completer<void> _completer;
Future<void> write(Map<Uri, DevFSContent> entries) async {
_client.maxConnectionsPerHost = kMaxInFlight;
_completer = Completer<void>();
_outstanding = Map<Uri, DevFSContent>.of(entries);
_scheduleWrites();
await _completer.future;
@override
Future<void> write(Map<Uri, DevFSContent> entries, Uri devFSBase, [DevFSWriter parent]) async {
try {
_client.maxConnectionsPerHost = kMaxInFlight;
_completer = Completer<void>();
_outstanding = Map<Uri, DevFSContent>.of(entries);
_scheduleWrites();
await _completer.future;
} on SocketException catch (socketException, stackTrace) {
_logger.printTrace('DevFS sync failed. Lost connection to device: $socketException');
throw DevFSException('Lost connection to device.', socketException, stackTrace);
} on Exception catch (exception, stackTrace) {
_logger.printError('Could not update files on device: $exception');
throw DevFSException('Sync failed', exception, stackTrace);
}
}
void _scheduleWrites() {
......@@ -425,6 +444,7 @@ class DevFS {
@required String pathToReload,
@required List<Uri> invalidatedFiles,
@required PackageConfig packageConfig,
DevFSWriter devFSWriter,
String target,
AssetBundle bundle,
DateTime firstBuildTime,
......@@ -445,7 +465,6 @@ class DevFS {
int syncedBytes = 0;
if (bundle != null && !skipAssets) {
_logger.printTrace('Scanning asset files');
final String assetBuildDirPrefix = _asUriPath(getAssetBuildDirectory());
// We write the assets into the AssetBundle working dir so that they
// are in the same location in DevFS and the iOS simulator.
......@@ -503,15 +522,7 @@ class DevFS {
}
_logger.printTrace('Updating files');
if (dirtyEntries.isNotEmpty) {
try {
await _httpWriter.write(dirtyEntries);
} on SocketException catch (socketException, stackTrace) {
_logger.printTrace('DevFS sync failed. Lost connection to device: $socketException');
throw DevFSException('Lost connection to device.', socketException, stackTrace);
} on Exception catch (exception, stackTrace) {
_logger.printError('Could not update files on device: $exception');
throw DevFSException('Sync failed', exception, stackTrace);
}
await (devFSWriter ?? _httpWriter).write(dirtyEntries, _baseUri, _httpWriter);
}
_logger.printTrace('DevFS: Sync finished');
return UpdateFSReport(
......@@ -525,3 +536,40 @@ class DevFS {
/// Converts a platform-specific file path to a platform-independent URL path.
String _asUriPath(String filePath) => _fileSystem.path.toUri(filePath).path + '/';
}
/// An implementation of a devFS writer which copies physical files for devices
/// running on the same host.
///
/// DevFS entries which correspond to physical files are copied using [File.copySync],
/// while entries that correspond to arbitrary string/byte values are written from
/// memory.
///
/// Requires that the file system is the same for both the tool and application.
class LocalDevFSWriter implements DevFSWriter {
LocalDevFSWriter({
@required FileSystem fileSystem,
}) : _fileSystem = fileSystem;
final FileSystem _fileSystem;
@override
Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, [DevFSWriter parent]) async {
try {
for (final Uri uri in entries.keys) {
final DevFSContent devFSContent = entries[uri];
final File destination = _fileSystem.file(baseUri.resolveUri(uri));
if (!destination.parent.existsSync()) {
destination.parent.createSync(recursive: true);
}
if (devFSContent is DevFSFileContent) {
final File content = devFSContent.file as File;
content.copySync(destination.path);
continue;
}
destination.writeAsBytesSync(await devFSContent.contentsAsBytes());
}
} on FileSystemException catch (err) {
throw DevFSException(err.toString());
}
}
}
......@@ -368,12 +368,14 @@ class FlutterDeviceManager extends DeviceManager {
macOSWorkflow: macOSWorkflow,
logger: logger,
platform: platform,
fileSystem: fileSystem,
),
LinuxDevices(
platform: platform,
featureFlags: featureFlags,
processManager: processManager,
logger: logger,
fileSystem: fileSystem,
),
WindowsDevices(),
WebDevices(
......
......@@ -17,6 +17,7 @@ import '../base/process.dart';
import '../base/utils.dart';
import '../build_info.dart';
import '../convert.dart';
import '../devfs.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../macos/xcode.dart';
......@@ -295,6 +296,9 @@ class IOSSimulator extends Device {
final SimControl _simControl;
final Xcode _xcode;
DevFSWriter get devFSWriter => _desktopDevFSWriter ??= LocalDevFSWriter(fileSystem: globals.fs);
LocalDevFSWriter _desktopDevFSWriter;
@override
Future<bool> get isLocalEmulator async => true;
......
......@@ -5,6 +5,7 @@
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../base/file_system.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../build_info.dart';
......@@ -22,12 +23,14 @@ class LinuxDevice extends DesktopDevice {
LinuxDevice({
@required ProcessManager processManager,
@required Logger logger,
@required FileSystem fileSystem,
}) : super(
'linux',
platformType: PlatformType.linux,
ephemeral: false,
logger: logger,
processManager: processManager,
fileSystem: fileSystem,
);
@override
......@@ -67,6 +70,7 @@ class LinuxDevices extends PollingDeviceDiscovery {
LinuxDevices({
@required Platform platform,
@required FeatureFlags featureFlags,
FileSystem fileSystem,
ProcessManager processManager,
Logger logger,
}) : _platform = platform ?? globals.platform, // TODO(jonahwilliams): remove after google3 roll
......@@ -74,6 +78,7 @@ class LinuxDevices extends PollingDeviceDiscovery {
platform: platform,
featureFlags: featureFlags,
),
_fileSystem = fileSystem ?? globals.fs,
_logger = logger,
_processManager = processManager ?? globals.processManager,
super('linux devices');
......@@ -82,6 +87,7 @@ class LinuxDevices extends PollingDeviceDiscovery {
final LinuxWorkflow _linuxWorkflow;
final ProcessManager _processManager;
final Logger _logger;
final FileSystem _fileSystem;
@override
bool get supportsPlatform => _platform.isLinux;
......@@ -98,6 +104,7 @@ class LinuxDevices extends PollingDeviceDiscovery {
LinuxDevice(
logger: _logger,
processManager: _processManager,
fileSystem: _fileSystem,
),
];
}
......
......@@ -5,12 +5,14 @@
import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../build_info.dart';
import '../desktop_device.dart';
import '../device.dart';
import '../globals.dart' as globals;
import '../macos/application_package.dart';
import '../project.dart';
import 'build_macos.dart';
......@@ -21,6 +23,7 @@ class MacOSDevice extends DesktopDevice {
MacOSDevice({
@required ProcessManager processManager,
@required Logger logger,
FileSystem fileSystem,
}) : _processManager = processManager,
_logger = logger,
super(
......@@ -29,6 +32,7 @@ class MacOSDevice extends DesktopDevice {
ephemeral: false,
processManager: processManager,
logger: logger,
fileSystem: fileSystem ?? globals.fs,
);
final ProcessManager _processManager;
......@@ -89,16 +93,19 @@ class MacOSDevices extends PollingDeviceDiscovery {
@required MacOSWorkflow macOSWorkflow,
@required ProcessManager processManager,
@required Logger logger,
FileSystem fileSystem,
}) : _logger = logger,
_platform = platform,
_macOSWorkflow = macOSWorkflow,
_processManager = processManager,
_fileSystem = fileSystem ?? globals.fs,
super('macOS devices');
final MacOSWorkflow _macOSWorkflow;
final Platform _platform;
final ProcessManager _processManager;
final Logger _logger;
final FileSystem _fileSystem;
@override
bool get supportsPlatform => _platform.isMacOS;
......@@ -112,7 +119,11 @@ class MacOSDevices extends PollingDeviceDiscovery {
return const <Device>[];
}
return <Device>[
MacOSDevice(processManager: _processManager, logger: _logger),
MacOSDevice(
processManager: _processManager,
logger: _logger,
fileSystem: _fileSystem,
),
];
}
......
......@@ -697,6 +697,7 @@ class FlutterDevice {
pathToReload: pathToReload,
invalidatedFiles: invalidatedFiles,
packageConfig: packageConfig,
devFSWriter: null,
);
} on DevFSException {
devFSStatus.cancel();
......
......@@ -6,6 +6,7 @@ import 'dart:convert';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:file_testing/file_testing.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';
......@@ -256,6 +257,81 @@ void main() {
expect(report.success, true);
expect(devFS.lastCompiled, isNot(previousCompile));
});
testWithoutContext('DevFS uses provided DevFSWriter instead of default HTTP writer', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final FakeDevFSWriter writer = FakeDevFSWriter();
final FakeVmServiceHost fakeVmServiceHost = FakeVmServiceHost(
requests: <VmServiceExpectation>[createDevFSRequest],
);
final DevFS devFS = DevFS(
fakeVmServiceHost.vmService,
'test',
fileSystem.currentDirectory,
fileSystem: fileSystem,
logger: BufferLogger.test(),
osUtils: FakeOperatingSystemUtils(),
httpClient: MockHttpClient(),
);
await devFS.create();
final MockResidentCompiler residentCompiler = MockResidentCompiler();
when(residentCompiler.recompile(
any,
any,
outputPath: anyNamed('outputPath'),
packageConfig: anyNamed('packageConfig'),
)).thenAnswer((Invocation invocation) async {
fileSystem.file('example').createSync();
return const CompilerOutput('lib/foo.txt.dill', 0, <Uri>[]);
});
expect(writer.written, false);
final UpdateFSReport report = await devFS.update(
mainUri: Uri.parse('lib/main.dart'),
generator: residentCompiler,
dillOutputPath: 'lib/foo.dill',
pathToReload: 'lib/foo.txt.dill',
trackWidgetCreation: false,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
devFSWriter: writer,
);
expect(report.success, true);
expect(writer.written, true);
});
testWithoutContext('Local DevFSwriter can copy and write files', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final File file = fileSystem.file('foo_bar')
..writeAsStringSync('goodbye');
final LocalDevFSWriter writer = LocalDevFSWriter(fileSystem: fileSystem);
await writer.write(<Uri, DevFSContent>{
Uri.parse('hello'): DevFSStringContent('hello'),
Uri.parse('goodbye'): DevFSFileContent(file),
}, Uri.parse('/foo/bar/devfs/'));
expect(fileSystem.file('/foo/bar/devfs/hello'), exists);
expect(fileSystem.file('/foo/bar/devfs/hello').readAsStringSync(), 'hello');
expect(fileSystem.file('/foo/bar/devfs/goodbye'), exists);
expect(fileSystem.file('/foo/bar/devfs/goodbye').readAsStringSync(), 'goodbye');
});
testWithoutContext('Local DevFSwriter turns FileSystemException into DevFSException', () async {
final FileSystem fileSystem = MemoryFileSystem.test();
final LocalDevFSWriter writer = LocalDevFSWriter(fileSystem: fileSystem);
final File file = MockFile();
when(file.copySync(any)).thenThrow(const FileSystemException('foo'));
await expectLater(() async => await writer.write(<Uri, DevFSContent>{
Uri.parse('goodbye'): DevFSFileContent(file),
}, Uri.parse('/foo/bar/devfs/')), throwsA(isA<DevFSException>()));
});
}
class MockHttpClientRequest extends Mock implements HttpClientRequest {}
......@@ -263,3 +339,12 @@ class MockHttpHeaders extends Mock implements HttpHeaders {}
class MockHttpClientResponse extends Mock implements HttpClientResponse {}
class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {}
class MockResidentCompiler extends Mock implements ResidentCompiler {}
class MockFile extends Mock implements File {}
class FakeDevFSWriter implements DevFSWriter {
bool written = false;
@override
Future<void> write(Map<Uri, DevFSContent> entries, Uri baseUri, DevFSWriter parent) async {
written = true;
}
}
......@@ -31,6 +31,7 @@ void main() {
final LinuxDevice device = LinuxDevice(
processManager: FakeProcessManager.any(),
logger: BufferLogger.test(),
fileSystem: MemoryFileSystem.test(),
);
final PrebuiltLinuxApp linuxApp = PrebuiltLinuxApp(executable: 'foo');
......@@ -51,6 +52,7 @@ void main() {
testWithoutContext('LinuxDevice: no devices listed if platform unsupported', () async {
expect(await LinuxDevices(
fileSystem: MemoryFileSystem.test(),
platform: windows,
featureFlags: TestFeatureFlags(isLinuxEnabled: true),
logger: BufferLogger.test(),
......@@ -60,6 +62,7 @@ void main() {
testWithoutContext('LinuxDevice: no devices listed if Linux feature flag disabled', () async {
expect(await LinuxDevices(
fileSystem: MemoryFileSystem.test(),
platform: linux,
featureFlags: TestFeatureFlags(isLinuxEnabled: false),
logger: BufferLogger.test(),
......@@ -69,6 +72,7 @@ void main() {
testWithoutContext('LinuxDevice: devices', () async {
expect(await LinuxDevices(
fileSystem: MemoryFileSystem.test(),
platform: linux,
featureFlags: TestFeatureFlags(isLinuxEnabled: true),
logger: BufferLogger.test(),
......@@ -79,6 +83,7 @@ void main() {
testWithoutContext('LinuxDevice: discoverDevices', () async {
// Timeout ignored.
final List<Device> devices = await LinuxDevices(
fileSystem: MemoryFileSystem.test(),
platform: linux,
featureFlags: TestFeatureFlags(isLinuxEnabled: true),
logger: BufferLogger.test(),
......@@ -96,6 +101,7 @@ void main() {
expect(LinuxDevice(
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
fileSystem: MemoryFileSystem.test(),
).isSupportedForProject(flutterProject), true);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
......@@ -110,6 +116,7 @@ void main() {
expect(LinuxDevice(
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
fileSystem: MemoryFileSystem.test(),
).isSupportedForProject(flutterProject), false);
}, overrides: <Type, Generator>{
FileSystem: () => MemoryFileSystem(),
......@@ -121,6 +128,7 @@ void main() {
final LinuxDevice device = LinuxDevice(
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
fileSystem: MemoryFileSystem.test(),
);
const String debugPath = 'debug/executable';
const String profilePath = 'profile/executable';
......
......@@ -36,6 +36,7 @@ void main() {
final MacOSDevice device = MacOSDevice(
processManager: FakeProcessManager.any(),
logger: BufferLogger.test(),
fileSystem: MemoryFileSystem.test(),
);
final MockMacOSApp mockMacOSApp = MockMacOSApp();
......@@ -56,6 +57,7 @@ void main() {
testUsingContext('Attaches to log reader when running in release mode', () async {
final Completer<void> completer = Completer<void>();
final MacOSDevice device = MacOSDevice(
fileSystem: MemoryFileSystem.test(),
processManager: FakeProcessManager.list(<FakeCommand>[
FakeCommand(
command: const <String>['Example.app'],
......@@ -88,6 +90,7 @@ void main() {
testWithoutContext('No devices listed if platform is unsupported', () async {
expect(await MacOSDevices(
fileSystem: MemoryFileSystem.test(),
processManager: FakeProcessManager.any(),
logger: BufferLogger.test(),
platform: linux,
......@@ -100,6 +103,7 @@ void main() {
testWithoutContext('No devices listed if platform is supported and feature is disabled', () async {
final MacOSDevices macOSDevices = MacOSDevices(
fileSystem: MemoryFileSystem.test(),
processManager: FakeProcessManager.any(),
logger: BufferLogger.test(),
platform: macOS,
......@@ -114,6 +118,7 @@ void main() {
testWithoutContext('devices listed if platform is supported and feature is enabled', () async {
final MacOSDevices macOSDevices = MacOSDevices(
fileSystem: MemoryFileSystem.test(),
processManager: FakeProcessManager.any(),
logger: BufferLogger.test(),
platform: macOS,
......@@ -128,6 +133,7 @@ void main() {
testWithoutContext('can discover devices with a provided timeout', () async {
final MacOSDevices macOSDevices = MacOSDevices(
fileSystem: MemoryFileSystem.test(),
processManager: FakeProcessManager.any(),
logger: BufferLogger.test(),
platform: macOS,
......@@ -145,6 +151,7 @@ void main() {
testUsingContext('isSupportedForProject is true with editable host app', () async {
final MacOSDevice device = MacOSDevice(
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
);
......@@ -162,6 +169,7 @@ void main() {
testUsingContext('isSupportedForProject is false with no host app', () async {
final MacOSDevice device = MacOSDevice(
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
);
......@@ -178,6 +186,7 @@ void main() {
testUsingContext('executablePathForDevice uses the correct package executable', () async {
final MockMacOSApp mockApp = MockMacOSApp();
final MacOSDevice device = MacOSDevice(
fileSystem: MemoryFileSystem.test(),
logger: BufferLogger.test(),
processManager: FakeProcessManager.any(),
);
......
......@@ -513,6 +513,7 @@ void main() {
bundleFirstUpload: true,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
pathToReload: '',
);
expect(webDevFS.webAssetServer.getFile('require.js'), isNotNull);
......@@ -627,6 +628,7 @@ void main() {
bundleFirstUpload: true,
invalidatedFiles: <Uri>[],
packageConfig: PackageConfig.empty,
pathToReload: '',
);
expect(webDevFS.webAssetServer.getFile('require.js'), isNotNull);
......
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