blob: 866086409c5a66c06bf17b2cf27c324ab5fa764f [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, Process, ProcessException, ProcessResult, ProcessSignal, ProcessStartMode, systemEncoding;
import 'dart:typed_data';
import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p; // flutter_ignore: package_path_import
import 'package:process/process.dart';
import 'common.dart' show throwToolExit;
import 'platform.dart';
// The Flutter tool hits file system and process 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.
/// On windows this is error code 2: ERROR_FILE_NOT_FOUND, and on
/// macOS/Linux it is error code 2/ENOENT: No such file or directory.
const int kSystemCannotFindFile = 2;
/// 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 generally 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;
/// Allow any file system operations executed within the closure to fail with any
/// operating system error, rethrowing an [Exception] instead of a [ToolExit].
///
/// This should not be used with async file system operation.
///
/// This can be used to bypass the [ErrorHandlingFileSystem] permission exit
/// checks for situations where failure is acceptable, such as the flutter
/// persistent settings cache.
static void noExitOnFailure(void Function() operation) {
final bool previousValue = ErrorHandlingFileSystem._noExitOnFailure;
try {
ErrorHandlingFileSystem._noExitOnFailure = true;
operation();
} finally {
ErrorHandlingFileSystem._noExitOnFailure = previousValue;
}
}
/// Delete the file or directory and return true if it exists, take no
/// action and return false if it does not.
///
/// This method should be preferred to checking if it exists and
/// then deleting, because it handles the edge case where the file or directory
/// is deleted by a different program between the two calls.
static bool deleteIfExists(FileSystemEntity file, {bool recursive = false}) {
if (!file.existsSync()) {
return false;
}
try {
file.deleteSync(recursive: recursive);
} on FileSystemException catch (err) {
// Certain error codes indicate the file could not be found. It could have
// been deleted by a different program while the tool was running.
// if it still exists, the file likely exists on a read-only volume.
if (err.osError?.errorCode != kSystemCannotFindFile || _noExitOnFailure) {
rethrow;
}
if (file.existsSync()) {
throwToolExit(
'The Flutter tool tried to delete the file or directory ${file.path} but was '
"unable to. This may be due to the file and/or project's location on a read-only "
'volume. Consider relocating the project and trying again',
);
}
}
return true;
}
static bool _noExitOnFailure = false;
@override
Directory get currentDirectory {
try {
return _runSync(() => directory(delegate.currentDirectory), platform: _platform);
} on FileSystemException catch (err) {
// Special handling for OS error 2 for current directory only.
if (err.osError?.errorCode == kSystemCannotFindFile) {
throwToolExit(
'Unable to read current working directory. This can happen if the directory the '
'Flutter tool was run from was moved or deleted.'
);
}
rethrow;
}
}
@override
File file(dynamic path) => ErrorHandlingFile(
platform: _platform,
fileSystem: this,
delegate: delegate.file(path),
);
@override
Directory directory(dynamic path) => ErrorHandlingDirectory(
platform: _platform,
fileSystem: this,
delegate: delegate.directory(path),
);
@override
Link link(dynamic path) => ErrorHandlingLink(
platform: _platform,
fileSystem: this,
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 ErrorHandlingFileSystem 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}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
);
}
@override
String readAsStringSync({Encoding encoding = utf8}) {
return _runSync<String>(
() => delegate.readAsStringSync(),
platform: _platform,
failureMessage: 'Flutter failed to read a file at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[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}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[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}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[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}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
);
}
// TODO(aam): Pass `exclusive` through after dartbug.com/49647 lands.
@override
void createSync({bool recursive = false, bool exclusive = false}) {
_runSync<void>(
() => delegate.createSync(
recursive: recursive,
),
platform: _platform,
failureMessage: 'Flutter failed to create file at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(<String>[delegate.parent.path]),
);
}
@override
RandomAccessFile openSync({FileMode mode = FileMode.read}) {
return _runSync<RandomAccessFile>(
() => delegate.openSync(
mode: mode,
),
platform: _platform,
failureMessage: 'Flutter failed to open a file at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[delegate.path]),
);
}
/// This copy method attempts to handle file system errors from both reading
/// and writing the copied file.
@override
File copySync(String newPath) {
final File resultFile = fileSystem.file(newPath);
// First check if the source file can be read. If not, bail through error
// handling.
_runSync<void>(
() => delegate.openSync().closeSync(),
platform: _platform,
failureMessage: 'Flutter failed to copy $path to $newPath due to source location error',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[path]),
);
// Next check if the destination file can be written. If not, bail through
// error handling.
_runSync<void>(
() => resultFile.createSync(recursive: true),
platform: _platform,
failureMessage: 'Flutter failed to copy $path to $newPath due to destination location error'
);
// If both of the above checks passed, attempt to copy the file and catch
// any thrown errors.
try {
return wrapFile(delegate.copySync(newPath));
} on FileSystemException {
// Proceed below
}
// If the copy failed but both of the above checks passed, copy the bytes
// directly.
_runSync(() {
RandomAccessFile? source;
RandomAccessFile? sink;
try {
source = delegate.openSync();
sink = resultFile.openSync(mode: FileMode.writeOnly);
// 64k is the same sized buffer used by dart:io for `File.openRead`.
final Uint8List buffer = Uint8List(64 * 1024);
final int totalBytes = source.lengthSync();
int bytes = 0;
while (bytes < totalBytes) {
final int chunkLength = source.readIntoSync(buffer);
sink.writeFromSync(buffer, 0, chunkLength);
bytes += chunkLength;
}
} catch (err) { // ignore: avoid_catches_without_on_clauses, rethrows
ErrorHandlingFileSystem.deleteIfExists(resultFile, recursive: true);
rethrow;
} finally {
source?.closeSync();
sink?.closeSync();
}
}, platform: _platform,
failureMessage: 'Flutter failed to copy $path to $newPath due to unknown error',
posixPermissionSuggestion: _posixPermissionSuggestion(<String>[path, resultFile.parent.path]),
);
// The original copy failed, but the manual copy worked.
return wrapFile(resultFile);
}
String _posixPermissionSuggestion(List<String> paths) => 'Try running:\n'
' sudo chown -R \$(whoami) ${paths.map(fileSystem.path.absolute).join(' ')}';
@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 ErrorHandlingFileSystem 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
Directory childDirectory(String basename) {
return fileSystem.directory(fileSystem.path.join(path, basename));
}
@override
File childFile(String basename) {
return fileSystem.file(fileSystem.path.join(path, basename));
}
@override
Link childLink(String basename) {
return fileSystem.link(fileSystem.path.join(path, basename));
}
@override
void createSync({bool recursive = false}) {
return _runSync<void>(
() => delegate.createSync(recursive: recursive),
platform: _platform,
failureMessage:
'Flutter failed to create a directory at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.parent.path),
);
}
@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
Future<Directory> create({bool recursive = false}) {
return _run<Directory>(
() async => wrap(await delegate.create(recursive: recursive)),
platform: _platform,
failureMessage:
'Flutter failed to create a directory at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.parent.path),
);
}
@override
Future<Directory> delete({bool recursive = false}) {
return _run<Directory>(
() async => wrap(fileSystem.directory((await delegate.delete(recursive: recursive)).path)),
platform: _platform,
failureMessage:
'Flutter failed to delete a directory at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.path),
);
}
@override
void deleteSync({bool recursive = false}) {
return _runSync<void>(
() => delegate.deleteSync(recursive: recursive),
platform: _platform,
failureMessage:
'Flutter failed to delete a directory at "${delegate.path}"',
posixPermissionSuggestion: recursive ? null : _posixPermissionSuggestion(delegate.path),
);
}
@override
bool existsSync() {
return _runSync<bool>(
() => delegate.existsSync(),
platform: _platform,
failureMessage:
'Flutter failed to check for directory existence at "${delegate.path}"',
posixPermissionSuggestion: _posixPermissionSuggestion(delegate.parent.path),
);
}
String _posixPermissionSuggestion(String path) => 'Try running:\n'
' sudo chown -R \$(whoami) ${fileSystem.path.absolute(path)}';
@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 ErrorHandlingFileSystem 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();
}
const String _kNoExecutableFound = 'The Flutter tool could not locate an executable with suitable permissions';
Future<T> _run<T>(Future<T> Function() op, {
required Platform platform,
String? failureMessage,
String? posixPermissionSuggestion,
}) async {
assert(platform != null);
try {
return await op();
} on ProcessPackageExecutableNotFoundException catch (e) {
if (e.candidates.isNotEmpty) {
throwToolExit('$_kNoExecutableFound: $e');
}
rethrow;
} on FileSystemException catch (e) {
if (platform.isWindows) {
_handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0);
} else if (platform.isLinux || platform.isMacOS) {
_handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0, posixPermissionSuggestion);
}
rethrow;
} on io.ProcessException catch (e) {
if (platform.isWindows) {
_handleWindowsException(e, failureMessage, e.errorCode);
} else if (platform.isLinux || platform.isMacOS) {
_handlePosixException(e, failureMessage, e.errorCode, posixPermissionSuggestion);
}
rethrow;
}
}
T _runSync<T>(T Function() op, {
required Platform platform,
String? failureMessage,
String? posixPermissionSuggestion,
}) {
assert(platform != null);
try {
return op();
} on ProcessPackageExecutableNotFoundException catch (e) {
if (e.candidates.isNotEmpty) {
throwToolExit('$_kNoExecutableFound: $e');
}
rethrow;
} on FileSystemException catch (e) {
if (platform.isWindows) {
_handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0);
} else if (platform.isLinux || platform.isMacOS) {
_handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0, posixPermissionSuggestion);
}
rethrow;
} on io.ProcessException catch (e) {
if (platform.isWindows) {
_handleWindowsException(e, failureMessage, e.errorCode);
} else if (platform.isLinux || platform.isMacOS) {
_handlePosixException(e, failureMessage, e.errorCode, posixPermissionSuggestion);
}
rethrow;
}
}
/// A [ProcessManager] that throws a [ToolExit] on certain errors.
///
/// If a [ProcessException] is not caused by the Flutter tool, and can only be
/// addressed by the user, it should be caught by this [ProcessManager] and thrown
/// as a [ToolExit] using [throwToolExit].
///
/// See also:
/// * [ErrorHandlingFileSystem], for a similar file system strategy.
class ErrorHandlingProcessManager extends ProcessManager {
ErrorHandlingProcessManager({
required ProcessManager delegate,
required Platform platform,
}) : _delegate = delegate,
_platform = platform;
final ProcessManager _delegate;
final Platform _platform;
@override
bool canRun(dynamic executable, {String? workingDirectory}) {
return _runSync(
() => _delegate.canRun(executable, workingDirectory: workingDirectory),
platform: _platform,
failureMessage: 'Flutter failed to run "$executable"',
posixPermissionSuggestion: 'Try running:\n'
' sudo chown -R \$(whoami) $executable && chmod u+rx $executable',
);
}
@override
bool killPid(int pid, [io.ProcessSignal signal = io.ProcessSignal.sigterm]) {
return _runSync(
() => _delegate.killPid(pid, signal),
platform: _platform,
);
}
@override
Future<io.ProcessResult> run(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding? stdoutEncoding = io.systemEncoding,
Encoding? stderrEncoding = io.systemEncoding,
}) {
return _run(() {
return _delegate.run(
command,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell,
stdoutEncoding: stdoutEncoding,
stderrEncoding: stderrEncoding,
);
}, platform: _platform);
}
@override
Future<io.Process> start(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
io.ProcessStartMode mode = io.ProcessStartMode.normal,
}) {
return _run(() {
return _delegate.start(
command,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell,
);
}, platform: _platform);
}
@override
io.ProcessResult runSync(
List<Object> command, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
Encoding? stdoutEncoding = io.systemEncoding,
Encoding? stderrEncoding = io.systemEncoding,
}) {
return _runSync(() {
return _delegate.runSync(
command,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell,
stdoutEncoding: stdoutEncoding,
stderrEncoding: stderrEncoding,
);
}, platform: _platform);
}
}
void _handlePosixException(Exception e, String? message, int errorCode, String? posixPermissionSuggestion) {
// 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
// https://github.com/apple/darwin-xnu/blob/master/bsd/dev/dtrace/scripts/errno.d
const int eperm = 1;
const int enospc = 28;
const int eacces = 13;
// Catch errors and bail when:
String? errorMessage;
switch (errorCode) {
case enospc:
errorMessage =
'$message. The target device is full.'
'\n$e\n'
'Free up space and try again.';
break;
case eperm:
case eacces:
final StringBuffer errorBuffer = StringBuffer();
if (message != null && message.isNotEmpty) {
errorBuffer.writeln('$message.');
} else {
errorBuffer.writeln('The flutter tool cannot access the file or directory.');
}
errorBuffer.writeln('Please ensure that the SDK and/or project is installed in a location '
'that has read/write permissions for the current user.');
if (posixPermissionSuggestion != null && posixPermissionSuggestion.isNotEmpty) {
errorBuffer.writeln(posixPermissionSuggestion);
}
errorMessage = errorBuffer.toString();
break;
default:
// Caller must rethrow the exception.
break;
}
_throwFileSystemException(errorMessage);
}
void _handleWindowsException(Exception e, String? message, int errorCode) {
// From:
// https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes
const int kDeviceFull = 112;
const int kUserMappedSectionOpened = 1224;
const int kAccessDenied = 5;
const int kFatalDeviceHardwareError = 483;
const int kDeviceDoesNotExist = 433;
// Catch errors and bail when:
String? errorMessage;
switch (errorCode) {
case kAccessDenied:
errorMessage =
'$message. The flutter tool cannot access the file or directory.\n'
'Please ensure that the SDK and/or project is installed in a location '
'that has read/write permissions for the current user.';
break;
case kDeviceFull:
errorMessage =
'$message. The target device is full.'
'\n$e\n'
'Free up space and try again.';
break;
case kUserMappedSectionOpened:
errorMessage =
'$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;
case kFatalDeviceHardwareError:
errorMessage =
'$message. There is a problem with the device driver '
'that this file or directory is stored on.';
break;
case kDeviceDoesNotExist:
errorMessage =
'$message. The device was not found.'
'\n$e\n'
'Verify the device is mounted and try again.';
break;
default:
// Caller must rethrow the exception.
break;
}
_throwFileSystemException(errorMessage);
}
void _throwFileSystemException(String? errorMessage) {
if (errorMessage == null) {
return;
}
if (ErrorHandlingFileSystem._noExitOnFailure) {
throw Exception(errorMessage);
}
throwToolExit(errorMessage);
}