blob: c879a622b4d95eedc82b56c3d588e214f669a1fd [file] [log] [blame]
// 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 'package:path/path.dart' as p; // ignore: package_path_import
import 'package:platform/platform.dart';
import 'common.dart' show throwToolExit;
// 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({
@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
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
// via the forbidden import tests. Otherwise, the path context's current
// working directory might get out of sync, leading to unexpected results from
// methods like `path.relative`.
@override
p.Context get path => _cachedPath ??= delegate.path;
p.Context _cachedPath;
@override
set currentDirectory(dynamic path) {
_cachedPath = null;
delegate.currentDirectory = path;
}
@override
String toString() => delegate.toString();
}
class ErrorHandlingFile
extends ForwardingFileSystemEntity<File, io.File>
with ForwardingFile {
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;
@override
final FileSystem fileSystem;
final Platform _platform;
@override
File wrapFile(io.File delegate) => ErrorHandlingFile(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Link wrapLink(io.Link delegate) => ErrorHandlingLink(
platform: _platform,
fileSystem: fileSystem,
delegate: 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,
)),
platform: _platform,
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),
platform: _platform,
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,
)),
platform: _platform,
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,
),
platform: _platform,
failureMessage: 'Flutter failed to write to a file at "${delegate.path}"',
);
}
@override
String toString() => delegate.toString();
}
class ErrorHandlingDirectory
extends ForwardingFileSystemEntity<Directory, io.Directory>
with ForwardingDirectory<Directory> {
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;
@override
final FileSystem fileSystem;
final Platform _platform;
@override
File wrapFile(io.File delegate) => ErrorHandlingFile(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
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
// 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));
@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();
}
class ErrorHandlingLink
extends ForwardingFileSystemEntity<Link, io.Link>
with ForwardingLink {
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;
@override
final FileSystem fileSystem;
final Platform _platform;
@override
File wrapFile(io.File delegate) => ErrorHandlingFile(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
Directory wrapDirectory(io.Directory delegate) => ErrorHandlingDirectory(
platform: _platform,
fileSystem: fileSystem,
delegate: delegate,
);
@override
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;
}
}