[flutter_tool] Handling of certain unrecoverable filesystem errors (#46617)
This commit is contained in:
parent
ceab1248d0
commit
895ffc80dc
@ -0,0 +1,241 @@
|
|||||||
|
// 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:convert';
|
||||||
|
import 'dart:io' as io show Directory, File, Link;
|
||||||
|
|
||||||
|
import 'package:file/file.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
import 'common.dart' show throwToolExit;
|
||||||
|
import 'platform.dart';
|
||||||
|
|
||||||
|
// The Flutter tool hits file system errors that only the end-user can address.
|
||||||
|
// We would like these errors to not hit crash logging. In these cases, we
|
||||||
|
// should exit gracefully and provide potentially useful advice. For example, if
|
||||||
|
// a write fails because the target device is full, we can explain that with a
|
||||||
|
// ToolExit and a message that is more clear than the FileSystemException by
|
||||||
|
// itself.
|
||||||
|
|
||||||
|
/// A [FileSystem] that throws a [ToolExit] on certain errors.
|
||||||
|
///
|
||||||
|
/// If a [FileSystem] error is not caused by the Flutter tool, and can only be
|
||||||
|
/// addressed by the user, it should be caught by this [FileSystem] and thrown
|
||||||
|
/// as a [ToolExit] using [throwToolExit].
|
||||||
|
///
|
||||||
|
/// Cf. If there is some hope that the tool can continue when an operation fails
|
||||||
|
/// with an error, then that error/operation should not be handled here. For
|
||||||
|
/// 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);
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
FileSystem get fileSystem => delegate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
File file(dynamic path) => ErrorHandlingFile(delegate, delegate.file(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorHandlingFile
|
||||||
|
extends ForwardingFileSystemEntity<File, io.File>
|
||||||
|
with ForwardingFile {
|
||||||
|
ErrorHandlingFile(this.fileSystem, this.delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final io.File delegate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final FileSystem fileSystem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
File wrapFile(io.File delegate) =>
|
||||||
|
ErrorHandlingFile(fileSystem, delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Directory wrapDirectory(io.Directory delegate) =>
|
||||||
|
ErrorHandlingDirectory(fileSystem, delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Link wrapLink(io.Link delegate) =>
|
||||||
|
ErrorHandlingLink(fileSystem, delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<File> writeAsBytes(
|
||||||
|
List<int> bytes, {
|
||||||
|
FileMode mode = FileMode.write,
|
||||||
|
bool flush = false,
|
||||||
|
}) async {
|
||||||
|
return _run<File>(
|
||||||
|
() async => wrap(await delegate.writeAsBytes(
|
||||||
|
bytes,
|
||||||
|
mode: mode,
|
||||||
|
flush: flush,
|
||||||
|
)),
|
||||||
|
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void writeAsBytesSync(
|
||||||
|
List<int> bytes, {
|
||||||
|
FileMode mode = FileMode.write,
|
||||||
|
bool flush = false,
|
||||||
|
}) {
|
||||||
|
_runSync<void>(
|
||||||
|
() => delegate.writeAsBytesSync(bytes, mode: mode, flush: flush),
|
||||||
|
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<File> writeAsString(
|
||||||
|
String contents, {
|
||||||
|
FileMode mode = FileMode.write,
|
||||||
|
Encoding encoding = utf8,
|
||||||
|
bool flush = false,
|
||||||
|
}) async {
|
||||||
|
return _run<File>(
|
||||||
|
() async => wrap(await delegate.writeAsString(
|
||||||
|
contents,
|
||||||
|
mode: mode,
|
||||||
|
encoding: encoding,
|
||||||
|
flush: flush,
|
||||||
|
)),
|
||||||
|
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void writeAsStringSync(
|
||||||
|
String contents, {
|
||||||
|
FileMode mode = FileMode.write,
|
||||||
|
Encoding encoding = utf8,
|
||||||
|
bool flush = false,
|
||||||
|
}) {
|
||||||
|
_runSync<void>(
|
||||||
|
() => delegate.writeAsStringSync(
|
||||||
|
contents,
|
||||||
|
mode: mode,
|
||||||
|
encoding: encoding,
|
||||||
|
flush: flush,
|
||||||
|
),
|
||||||
|
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T> _run<T>(Future<T> Function() op, { String failureMessage }) async {
|
||||||
|
try {
|
||||||
|
return await op();
|
||||||
|
} on FileSystemException catch (e) {
|
||||||
|
if (platform.isWindows) {
|
||||||
|
_handleWindowsException(e, failureMessage);
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
T _runSync<T>(T Function() op, { String failureMessage }) {
|
||||||
|
try {
|
||||||
|
return op();
|
||||||
|
} on FileSystemException catch (e) {
|
||||||
|
if (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);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final io.Directory delegate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final FileSystem fileSystem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
File wrapFile(io.File delegate) =>
|
||||||
|
ErrorHandlingFile(fileSystem, delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Directory wrapDirectory(io.Directory delegate) =>
|
||||||
|
ErrorHandlingDirectory(fileSystem, delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Link wrapLink(io.Link delegate) =>
|
||||||
|
ErrorHandlingLink(fileSystem, delegate);
|
||||||
|
|
||||||
|
// For the childEntity methods, we first obtain an instance of the entity
|
||||||
|
// from the underlying file system, then invoke childEntity() on it, then
|
||||||
|
// wrap in the ErrorHandling version.
|
||||||
|
@override
|
||||||
|
Directory childDirectory(String basename) =>
|
||||||
|
wrapDirectory(fileSystem.directory(delegate).childDirectory(basename));
|
||||||
|
|
||||||
|
@override
|
||||||
|
File childFile(String basename) =>
|
||||||
|
wrapFile(fileSystem.directory(delegate).childFile(basename));
|
||||||
|
|
||||||
|
@override
|
||||||
|
Link childLink(String basename) =>
|
||||||
|
wrapLink(fileSystem.directory(delegate).childLink(basename));
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorHandlingLink
|
||||||
|
extends ForwardingFileSystemEntity<Link, io.Link>
|
||||||
|
with ForwardingLink {
|
||||||
|
ErrorHandlingLink(this.fileSystem, this.delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final io.Link delegate;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final FileSystem fileSystem;
|
||||||
|
|
||||||
|
@override
|
||||||
|
File wrapFile(io.File delegate) =>
|
||||||
|
ErrorHandlingFile(fileSystem, delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Directory wrapDirectory(io.Directory delegate) =>
|
||||||
|
ErrorHandlingDirectory(fileSystem, delegate);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Link wrapLink(io.Link delegate) =>
|
||||||
|
ErrorHandlingLink(fileSystem, delegate);
|
||||||
|
}
|
@ -9,6 +9,7 @@ import 'package:meta/meta.dart';
|
|||||||
|
|
||||||
import 'common.dart' show throwToolExit;
|
import 'common.dart' show throwToolExit;
|
||||||
import 'context.dart';
|
import 'context.dart';
|
||||||
|
import 'error_handling_file_system.dart';
|
||||||
import 'platform.dart';
|
import 'platform.dart';
|
||||||
|
|
||||||
export 'package:file/file.dart';
|
export 'package:file/file.dart';
|
||||||
@ -20,7 +21,9 @@ const FileSystem _kLocalFs = LocalFileSystem();
|
|||||||
///
|
///
|
||||||
/// By default it uses local disk-based implementation. Override this in tests
|
/// By default it uses local disk-based implementation. Override this in tests
|
||||||
/// with [MemoryFileSystem].
|
/// with [MemoryFileSystem].
|
||||||
FileSystem get fs => context.get<FileSystem>() ?? _kLocalFs;
|
FileSystem get fs => ErrorHandlingFileSystem(
|
||||||
|
context.get<FileSystem>() ?? _kLocalFs,
|
||||||
|
);
|
||||||
|
|
||||||
/// Create the ancestor directories of a file path if they do not already exist.
|
/// Create the ancestor directories of a file path if they do not already exist.
|
||||||
void ensureDirectoryExists(String filePath) {
|
void ensureDirectoryExists(String filePath) {
|
||||||
|
@ -174,6 +174,7 @@ void main() {
|
|||||||
FlutterVersion: () => flutterVersion,
|
FlutterVersion: () => flutterVersion,
|
||||||
FeatureFlags: () => TestFeatureFlags(isWebEnabled: false),
|
FeatureFlags: () => TestFeatureFlags(isWebEnabled: false),
|
||||||
});
|
});
|
||||||
|
|
||||||
testUsingContext('precache downloads artifacts when --force is provided', () async {
|
testUsingContext('precache downloads artifacts when --force is provided', () async {
|
||||||
when(cache.isUpToDate()).thenReturn(true);
|
when(cache.isUpToDate()).thenReturn(true);
|
||||||
final PrecacheCommand command = PrecacheCommand();
|
final PrecacheCommand command = PrecacheCommand();
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
// 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 'package:file/file.dart';
|
||||||
|
import 'package:flutter_tools/src/base/error_handling_file_system.dart';
|
||||||
|
import 'package:mockito/mockito.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
|
import '../../src/common.dart';
|
||||||
|
import '../../src/context.dart';
|
||||||
|
import '../../src/testbed.dart';
|
||||||
|
|
||||||
|
class MockFile extends Mock implements File {}
|
||||||
|
class MockFileSystem extends Mock implements FileSystem {}
|
||||||
|
class MockPlatform extends Mock implements Platform {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('throws ToolExit on Windows', () {
|
||||||
|
const int kDeviceFull = 112;
|
||||||
|
const int kUserMappedSectionOpened = 1224;
|
||||||
|
Testbed testbed;
|
||||||
|
MockFileSystem mockFileSystem;
|
||||||
|
MockPlatform windowsPlatform;
|
||||||
|
ErrorHandlingFileSystem fs;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
mockFileSystem = MockFileSystem();
|
||||||
|
fs = ErrorHandlingFileSystem(mockFileSystem);
|
||||||
|
|
||||||
|
windowsPlatform = MockPlatform();
|
||||||
|
when(windowsPlatform.isWindows).thenReturn(true);
|
||||||
|
when(windowsPlatform.isLinux).thenReturn(false);
|
||||||
|
when(windowsPlatform.isMacOS).thenReturn(false);
|
||||||
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
@ -39,6 +39,7 @@ void main() {
|
|||||||
test('no unauthorized imports of dart:io', () {
|
test('no unauthorized imports of dart:io', () {
|
||||||
final List<String> whitelistedPaths = <String>[
|
final List<String> whitelistedPaths = <String>[
|
||||||
fs.path.join(flutterTools, 'lib', 'src', 'base', 'io.dart'),
|
fs.path.join(flutterTools, 'lib', 'src', 'base', 'io.dart'),
|
||||||
|
fs.path.join(flutterTools, 'lib', 'src', 'base', 'error_handling_file_system.dart'),
|
||||||
];
|
];
|
||||||
bool _isNotWhitelisted(FileSystemEntity entity) => whitelistedPaths.every((String path) => path != entity.path);
|
bool _isNotWhitelisted(FileSystemEntity entity) => whitelistedPaths.every((String path) => path != entity.path);
|
||||||
|
|
||||||
@ -83,6 +84,7 @@ void main() {
|
|||||||
test('no unauthorized imports of dart:convert', () {
|
test('no unauthorized imports of dart:convert', () {
|
||||||
final List<String> whitelistedPaths = <String>[
|
final List<String> whitelistedPaths = <String>[
|
||||||
fs.path.join(flutterTools, 'lib', 'src', 'convert.dart'),
|
fs.path.join(flutterTools, 'lib', 'src', 'convert.dart'),
|
||||||
|
fs.path.join(flutterTools, 'lib', 'src', 'base', 'error_handling_file_system.dart'),
|
||||||
];
|
];
|
||||||
bool _isNotWhitelisted(FileSystemEntity entity) => whitelistedPaths.every((String path) => path != entity.path);
|
bool _isNotWhitelisted(FileSystemEntity entity) => whitelistedPaths.every((String path) => path != entity.path);
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ import 'dart:async';
|
|||||||
import 'dart:io' as io;
|
import 'dart:io' as io;
|
||||||
|
|
||||||
import 'package:flutter_tools/src/base/common.dart';
|
import 'package:flutter_tools/src/base/common.dart';
|
||||||
|
import 'package:flutter_tools/src/base/error_handling_file_system.dart';
|
||||||
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/io.dart';
|
import 'package:flutter_tools/src/base/io.dart';
|
||||||
import 'package:flutter_tools/src/base/signals.dart';
|
import 'package:flutter_tools/src/base/signals.dart';
|
||||||
import 'package:flutter_tools/src/base/time.dart';
|
import 'package:flutter_tools/src/base/time.dart';
|
||||||
@ -65,6 +67,16 @@ void main() {
|
|||||||
Cache: () => cache,
|
Cache: () => cache,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testUsingContext('uses the error handling file system', () async {
|
||||||
|
final DummyFlutterCommand flutterCommand = DummyFlutterCommand(
|
||||||
|
commandFunction: () async {
|
||||||
|
expect(fs, isA<ErrorHandlingFileSystem>());
|
||||||
|
return const FlutterCommandResult(ExitStatus.success);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await flutterCommand.run();
|
||||||
|
});
|
||||||
|
|
||||||
void testUsingCommandContext(String testName, dynamic Function() testBody) {
|
void testUsingCommandContext(String testName, dynamic Function() testBody) {
|
||||||
testUsingContext(testName, testBody, overrides: <Type, Generator>{
|
testUsingContext(testName, testBody, overrides: <Type, Generator>{
|
||||||
ProcessInfo: () => mockProcessInfo,
|
ProcessInfo: () => mockProcessInfo,
|
||||||
|
@ -5,9 +5,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file/file.dart';
|
||||||
import 'package:file/memory.dart';
|
import 'package:file/memory.dart';
|
||||||
import 'package:flutter_tools/src/base/context.dart';
|
import 'package:flutter_tools/src/base/context.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
|
import 'package:flutter_tools/src/base/error_handling_file_system.dart';
|
||||||
|
|
||||||
import '../src/common.dart';
|
import '../src/common.dart';
|
||||||
import '../src/testbed.dart';
|
import '../src/testbed.dart';
|
||||||
@ -23,7 +25,9 @@ void main() {
|
|||||||
localFileSystem = fs;
|
localFileSystem = fs;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(localFileSystem, isA<MemoryFileSystem>());
|
expect(localFileSystem, isA<ErrorHandlingFileSystem>());
|
||||||
|
expect((localFileSystem as ErrorHandlingFileSystem).fileSystem,
|
||||||
|
isA<MemoryFileSystem>());
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can provide setup interfaces', () async {
|
test('Can provide setup interfaces', () async {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user