Unverified Commit 6fddb7ee authored by Zachary Anderson's avatar Zachary Anderson Committed by GitHub

[flutter_tools] Handle full device when creating a temp directory (#53691)

parent 3a0d8377
......@@ -8,8 +8,8 @@ import 'dart:io' as io show Directory, File, Link;
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p; // ignore: package_path_import
import 'package:platform/platform.dart';
import '../globals.dart' as globals;
import 'common.dart' show throwToolExit;
// The Flutter tool hits file system errors that only the end-user can address.
......@@ -30,13 +30,40 @@ import 'common.dart' show throwToolExit;
/// example, the tool should gernerally be able to continue executing even if it
/// fails to delete a file.
class ErrorHandlingFileSystem extends ForwardingFileSystem {
ErrorHandlingFileSystem(FileSystem delegate) : super(delegate);
ErrorHandlingFileSystem({
@required FileSystem delegate,
@required Platform platform,
}) :
assert(delegate != null),
assert(platform != null),
_platform = platform,
super(delegate);
@visibleForTesting
FileSystem get fileSystem => delegate;
final Platform _platform;
@override
File file(dynamic path) => ErrorHandlingFile(
platform: _platform,
fileSystem: delegate,
delegate: delegate.file(path),
);
@override
Directory directory(dynamic path) => ErrorHandlingDirectory(
platform: _platform,
fileSystem: delegate,
delegate: delegate.directory(path),
);
@override
File file(dynamic path) => ErrorHandlingFile(delegate, delegate.file(path));
Link link(dynamic path) => ErrorHandlingLink(
platform: _platform,
fileSystem: delegate,
delegate: delegate.link(path),
);
// Caching the path context here and clearing when the currentDirectory setter
// is updated works since the flutter tool restricts usage of dart:io directly
......@@ -60,7 +87,15 @@ class ErrorHandlingFileSystem extends ForwardingFileSystem {
class ErrorHandlingFile
extends ForwardingFileSystemEntity<File, io.File>
with ForwardingFile {
ErrorHandlingFile(this.fileSystem, this.delegate);
ErrorHandlingFile({
@required Platform platform,
@required this.fileSystem,
@required this.delegate,
}) :
assert(platform != null),
assert(fileSystem != null),
assert(delegate != null),
_platform = platform;
@override
final io.File delegate;
......@@ -68,17 +103,28 @@ class ErrorHandlingFile
@override
final FileSystem fileSystem;
final Platform _platform;
@override
File wrapFile(io.File delegate) =>
ErrorHandlingFile(fileSystem, delegate);
File wrapFile(io.File delegate) => ErrorHandlingFile(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Directory wrapDirectory(io.Directory delegate) =>
ErrorHandlingDirectory(fileSystem, delegate);
Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Link wrapLink(io.Link delegate) =>
ErrorHandlingLink(fileSystem, delegate);
Link wrapLink(io.Link delegate) => ErrorHandlingLink(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Future<File> writeAsBytes(
......@@ -92,6 +138,7 @@ class ErrorHandlingFile
mode: mode,
flush: flush,
)),
platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
);
}
......@@ -104,6 +151,7 @@ class ErrorHandlingFile
}) {
_runSync<void>(
() => delegate.writeAsBytesSync(bytes, mode: mode, flush: flush),
platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
);
}
......@@ -122,6 +170,7 @@ class ErrorHandlingFile
encoding: encoding,
flush: flush,
)),
platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
);
}
......@@ -140,69 +189,27 @@ class ErrorHandlingFile
encoding: encoding,
flush: flush,
),
platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
);
}
@override
String toString() => delegate.toString();
Future<T> _run<T>(Future<T> Function() op, { String failureMessage }) async {
try {
return await op();
} on FileSystemException catch (e) {
if (globals.platform.isWindows) {
_handleWindowsException(e, failureMessage);
}
rethrow;
}
}
T _runSync<T>(T Function() op, { String failureMessage }) {
try {
return op();
} on FileSystemException catch (e) {
if (globals.platform.isWindows) {
_handleWindowsException(e, failureMessage);
}
rethrow;
}
}
void _handleWindowsException(FileSystemException e, String message) {
// From:
// https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes
const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224;
final int errorCode = e.osError?.errorCode ?? 0;
// Catch errors and bail when:
switch (errorCode) {
case kDeviceFull:
throwToolExit(
'$message. The target device is full.'
'\n$e\n'
'Free up space and try again.',
);
break;
case kUserMappedSectionOpened:
throwToolExit(
'$message. The file is being used by another program.'
'\n$e\n'
'Do you have an antivirus program running? '
'Try disabling your antivirus program and try again.',
);
break;
default:
// Caller must rethrow the exception.
break;
}
}
}
class ErrorHandlingDirectory
extends ForwardingFileSystemEntity<Directory, io.Directory>
with ForwardingDirectory<Directory> {
ErrorHandlingDirectory(this.fileSystem, this.delegate);
ErrorHandlingDirectory({
@required Platform platform,
@required this.fileSystem,
@required this.delegate,
}) :
assert(platform != null),
assert(fileSystem != null),
assert(delegate != null),
_platform = platform;
@override
final io.Directory delegate;
......@@ -210,17 +217,28 @@ class ErrorHandlingDirectory
@override
final FileSystem fileSystem;
final Platform _platform;
@override
File wrapFile(io.File delegate) =>
ErrorHandlingFile(fileSystem, delegate);
File wrapFile(io.File delegate) => ErrorHandlingFile(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Directory wrapDirectory(io.Directory delegate) =>
ErrorHandlingDirectory(fileSystem, delegate);
Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Link wrapLink(io.Link delegate) =>
ErrorHandlingLink(fileSystem, delegate);
Link wrapLink(io.Link delegate) => ErrorHandlingLink(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
// For the childEntity methods, we first obtain an instance of the entity
// from the underlying file system, then invoke childEntity() on it, then
......@@ -237,6 +255,26 @@ class ErrorHandlingDirectory
Link childLink(String basename) =>
wrapLink(fileSystem.directory(delegate).childLink(basename));
@override
Future<Directory> createTemp([String prefix]) {
return _run<Directory>(
() async => wrap(await delegate.createTemp(prefix)),
platform: _platform,
failureMessage:
'Flutter failed to create a temporary directory with prefix "$prefix"',
);
}
@override
Directory createTempSync([String prefix]) {
return _runSync<Directory>(
() => wrap(delegate.createTempSync(prefix)),
platform: _platform,
failureMessage:
'Flutter failed to create a temporary directory with prefix "$prefix"',
);
}
@override
String toString() => delegate.toString();
}
......@@ -244,7 +282,15 @@ class ErrorHandlingDirectory
class ErrorHandlingLink
extends ForwardingFileSystemEntity<Link, io.Link>
with ForwardingLink {
ErrorHandlingLink(this.fileSystem, this.delegate);
ErrorHandlingLink({
@required Platform platform,
@required this.fileSystem,
@required this.delegate,
}) :
assert(platform != null),
assert(fileSystem != null),
assert(delegate != null),
_platform = platform;
@override
final io.Link delegate;
......@@ -252,18 +298,113 @@ class ErrorHandlingLink
@override
final FileSystem fileSystem;
final Platform _platform;
@override
File wrapFile(io.File delegate) =>
ErrorHandlingFile(fileSystem, delegate);
File wrapFile(io.File delegate) => ErrorHandlingFile(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Directory wrapDirectory(io.Directory delegate) =>
ErrorHandlingDirectory(fileSystem, delegate);
Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Link wrapLink(io.Link delegate) =>
ErrorHandlingLink(fileSystem, delegate);
Link wrapLink(io.Link delegate) => ErrorHandlingLink(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
String toString() => delegate.toString();
}
Future<T> _run<T>(Future<T> Function() op, {
@required Platform platform,
String failureMessage,
}) async {
assert(platform != null);
try {
return await op();
} on FileSystemException catch (e) {
if (platform.isWindows) {
_handleWindowsException(e, failureMessage);
} else if (platform.isLinux) {
_handleLinuxException(e, failureMessage);
}
rethrow;
}
}
T _runSync<T>(T Function() op, {
@required Platform platform,
String failureMessage,
}) {
assert(platform != null);
try {
return op();
} on FileSystemException catch (e) {
if (platform.isWindows) {
_handleWindowsException(e, failureMessage);
} else if (platform.isLinux) {
_handleLinuxException(e, failureMessage);
}
rethrow;
}
}
void _handleLinuxException(FileSystemException e, String message) {
// From:
// https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno.h
// https://github.com/torvalds/linux/blob/master/include/uapi/asm-generic/errno-base.h
const int enospc = 28;
final int errorCode = e.osError?.errorCode ?? 0;
// Catch errors and bail when:
switch (errorCode) {
case enospc:
throwToolExit(
'$message. The target device is full.'
'\n$e\n'
'Free up space and try again.',
);
break;
default:
// Caller must rethrow the exception.
break;
}
}
void _handleWindowsException(FileSystemException e, String message) {
// From:
// https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes
const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224;
final int errorCode = e.osError?.errorCode ?? 0;
// Catch errors and bail when:
switch (errorCode) {
case kDeviceFull:
throwToolExit(
'$message. The target device is full.'
'\n$e\n'
'Free up space and try again.',
);
break;
case kUserMappedSectionOpened:
throwToolExit(
'$message. The file is being used by another program.'
'\n$e\n'
'Do you have an antivirus program running? '
'Try disabling your antivirus program and try again.',
);
break;
default:
// Caller must rethrow the exception.
break;
}
}
......@@ -52,7 +52,8 @@ const FileSystem _kLocalFs = LocalFileSystem();
/// By default it uses local disk-based implementation. Override this in tests
/// with [MemoryFileSystem].
FileSystem get fs => ErrorHandlingFileSystem(
context.get<FileSystem>() ?? _kLocalFs,
delegate: context.get<FileSystem>() ?? _kLocalFs,
platform: platform,
);
final FileSystemUtils _defaultFileSystemUtils = FileSystemUtils(
......
......@@ -10,8 +10,6 @@ import 'package:platform/platform.dart';
import 'package:path/path.dart' as path; // ignore: package_path_import
import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/testbed.dart';
class MockFile extends Mock implements File {}
class MockFileSystem extends Mock implements FileSystem {}
......@@ -23,93 +21,200 @@ final Platform windowsPlatform = FakePlatform(
environment: <String, String>{}
);
final Platform linuxPlatform = FakePlatform(
operatingSystem: 'linux',
environment: <String, String>{}
);
void setupWriteMocks({
FileSystem mockFileSystem,
ErrorHandlingFileSystem fs,
int errorCode,
}) {
final MockFile mockFile = MockFile();
when(mockFileSystem.file(any)).thenReturn(mockFile);
when(mockFile.writeAsBytes(
any,
mode: anyNamed('mode'),
flush: anyNamed('flush'),
)).thenAnswer((_) async {
throw FileSystemException('', '', OSError('', errorCode));
});
when(mockFile.writeAsString(
any,
mode: anyNamed('mode'),
encoding: anyNamed('encoding'),
flush: anyNamed('flush'),
)).thenAnswer((_) async {
throw FileSystemException('', '', OSError('', errorCode));
});
when(mockFile.writeAsBytesSync(
any,
mode: anyNamed('mode'),
flush: anyNamed('flush'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockFile.writeAsStringSync(
any,
mode: anyNamed('mode'),
encoding: anyNamed('encoding'),
flush: anyNamed('flush'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
}
void setupCreateTempMocks({
FileSystem mockFileSystem,
ErrorHandlingFileSystem fs,
int errorCode,
}) {
final MockDirectory mockDirectory = MockDirectory();
when(mockFileSystem.directory(any)).thenReturn(mockDirectory);
when(mockDirectory.createTemp(any)).thenAnswer((_) async {
throw FileSystemException('', '', OSError('', errorCode));
});
when(mockDirectory.createTempSync(any))
.thenThrow(FileSystemException('', '', OSError('', errorCode)));
}
void main() {
group('throws ToolExit on Windows', () {
const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224;
Testbed testbed;
MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs;
setUp(() {
mockFileSystem = MockFileSystem();
fs = ErrorHandlingFileSystem(mockFileSystem);
fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: windowsPlatform,
);
when(mockFileSystem.path).thenReturn(MockPathContext());
testbed = Testbed(overrides: <Type, Generator>{
Platform: () => windowsPlatform,
});
});
void writeTests({
String testName,
int errorCode,
String expectedMessage,
}) {
test(testName, () => testbed.run(() async {
final MockFile mockFile = MockFile();
when(mockFileSystem.file(any)).thenReturn(mockFile);
when(mockFile.writeAsBytes(
any,
mode: anyNamed('mode'),
flush: anyNamed('flush'),
)).thenAnswer((_) async {
throw FileSystemException('', '', OSError('', errorCode));
});
when(mockFile.writeAsString(
any,
mode: anyNamed('mode'),
encoding: anyNamed('encoding'),
flush: anyNamed('flush'),
)).thenAnswer((_) async {
throw FileSystemException('', '', OSError('', errorCode));
});
when(mockFile.writeAsBytesSync(
any,
mode: anyNamed('mode'),
flush: anyNamed('flush'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
when(mockFile.writeAsStringSync(
any,
mode: anyNamed('mode'),
encoding: anyNamed('encoding'),
flush: anyNamed('flush'),
)).thenThrow(FileSystemException('', '', OSError('', errorCode)));
final File file = fs.file('file');
expect(() async => await file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => await file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
}));
}
writeTests(
testName: 'when writing to a full device',
errorCode: kDeviceFull,
expectedMessage: 'The target device is full',
);
writeTests(
testName: 'when the file is being used by another program',
errorCode: kUserMappedSectionOpened,
expectedMessage: 'The file is being used by another program',
);
testWithoutContext('when writing to a full device', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kDeviceFull,
);
final File file = fs.file('file');
const String expectedMessage = 'The target device is full';
expect(() async => await file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => await file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when the file is being used by another program', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kUserMappedSectionOpened,
);
final File file = fs.file('file');
const String expectedMessage = 'The file is being used by another program';
expect(() async => await file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => await file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when creating a temporary dir on a full device', () async {
setupCreateTempMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: kDeviceFull,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'The target device is full';
expect(() async => await directory.createTemp('prefix'),
throwsToolExit(message: expectedMessage));
expect(() => directory.createTempSync('prefix'),
throwsToolExit(message: expectedMessage));
});
});
test('Caches path context correctly', () {
group('throws ToolExit on Linux', () {
const int enospc= 28;
MockFileSystem mockFileSystem;
ErrorHandlingFileSystem fs;
setUp(() {
mockFileSystem = MockFileSystem();
fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: linuxPlatform,
);
when(mockFileSystem.path).thenReturn(MockPathContext());
});
testWithoutContext('when writing to a full device', () async {
setupWriteMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: enospc,
);
final File file = fs.file('file');
const String expectedMessage = 'The target device is full';
expect(() async => await file.writeAsBytes(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() async => await file.writeAsString(''),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsBytesSync(<int>[0]),
throwsToolExit(message: expectedMessage));
expect(() => file.writeAsStringSync(''),
throwsToolExit(message: expectedMessage));
});
testWithoutContext('when creating a temporary dir on a full device', () async {
setupCreateTempMocks(
mockFileSystem: mockFileSystem,
fs: fs,
errorCode: enospc,
);
final Directory directory = fs.directory('directory');
const String expectedMessage = 'The target device is full';
expect(() async => await directory.createTemp('prefix'),
throwsToolExit(message: expectedMessage));
expect(() => directory.createTempSync('prefix'),
throwsToolExit(message: expectedMessage));
});
});
testWithoutContext('Caches path context correctly', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(mockFileSystem);
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
expect(identical(fs.path, fs.path), true);
});
test('Clears cache when CWD changes', () {
testWithoutContext('Clears cache when CWD changes', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(mockFileSystem);
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
final Object firstPath = fs.path;
......@@ -119,8 +224,11 @@ void main() {
expect(identical(firstPath, fs.path), false);
});
test('Throws type error if Directory type is set to curentDirectory with LocalFileSystem', () {
final FileSystem fs = ErrorHandlingFileSystem(const LocalFileSystem());
testWithoutContext('Throws type error if Directory type is set to curentDirectory with LocalFileSystem', () {
final FileSystem fs = ErrorHandlingFileSystem(
delegate: const LocalFileSystem(),
platform: const LocalPlatform(),
);
final MockDirectory directory = MockDirectory();
when(directory.path).thenReturn('path');
......@@ -128,17 +236,23 @@ void main() {
});
group('toString() gives toString() of delegate', () {
test('ErrorHandlingFileSystem', () {
testWithoutContext('ErrorHandlingFileSystem', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(mockFileSystem);
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
expect(mockFileSystem.toString(), isNotNull);
expect(fs.toString(), equals(mockFileSystem.toString()));
});
test('ErrorHandlingFile', () {
testWithoutContext('ErrorHandlingFile', () {
final MockFileSystem mockFileSystem = MockFileSystem();
final FileSystem fs = ErrorHandlingFileSystem(mockFileSystem);
final FileSystem fs = ErrorHandlingFileSystem(
delegate: mockFileSystem,
platform: const LocalPlatform(),
);
final MockFile mockFile = MockFile();
when(mockFileSystem.file(any)).thenReturn(mockFile);
......
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