// 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, ProcessException, ProcessResult, ProcessSignal, systemEncoding, Process, ProcessStartMode; 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: 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}"', 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]), ); } @override void createSync({bool recursive = 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 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 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 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(); } 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); }