From 9248fda41005ce0af2e522fabd0e9fedf2b3c863 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Thu, 3 Sep 2020 16:08:01 -0700 Subject: [PATCH] [flutter_tools] add EACCES to list of immediate exit tool conditions (#65125) Similar to the permission denied error on Windows, this is not resolvable by the tool. --- .../lib/src/base/error_handling_io.dart | 27 ++- .../base/error_handling_io_test.dart | 172 ++++++++++++++++++ 2 files changed, 190 insertions(+), 9 deletions(-) diff --git a/packages/flutter_tools/lib/src/base/error_handling_io.dart b/packages/flutter_tools/lib/src/base/error_handling_io.dart index ad1f31442a9..e4c1d8628bc 100644 --- a/packages/flutter_tools/lib/src/base/error_handling_io.dart +++ b/packages/flutter_tools/lib/src/base/error_handling_io.dart @@ -357,15 +357,15 @@ Future _run(Future Function() op, { } on FileSystemException catch (e) { if (platform.isWindows) { _handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0); - } else if (platform.isLinux) { - _handleLinuxException(e, failureMessage, e.osError?.errorCode ?? 0); + } else if (platform.isLinux || platform.isMacOS) { + _handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0); } rethrow; } on io.ProcessException catch (e) { if (platform.isWindows) { _handleWindowsException(e, failureMessage, e.errorCode ?? 0); - } else if (platform.isLinux) { - _handleLinuxException(e, failureMessage, e.errorCode ?? 0); + } else if (platform.isLinux || platform.isMacOS) { + _handlePosixException(e, failureMessage, e.errorCode ?? 0); } rethrow; } @@ -381,15 +381,15 @@ T _runSync(T Function() op, { } on FileSystemException catch (e) { if (platform.isWindows) { _handleWindowsException(e, failureMessage, e.osError?.errorCode ?? 0); - } else if (platform.isLinux) { - _handleLinuxException(e, failureMessage, e.osError?.errorCode ?? 0); + } else if (platform.isLinux || platform.isMacOS) { + _handlePosixException(e, failureMessage, e.osError?.errorCode ?? 0); } rethrow; } on io.ProcessException catch (e) { if (platform.isWindows) { _handleWindowsException(e, failureMessage, e.errorCode ?? 0); - } else if (platform.isLinux) { - _handleLinuxException(e, failureMessage, e.errorCode ?? 0); + } else if (platform.isLinux || platform.isMacOS) { + _handlePosixException(e, failureMessage, e.errorCode ?? 0); } rethrow; } @@ -490,11 +490,13 @@ class ErrorHandlingProcessManager extends ProcessManager { } } -void _handleLinuxException(Exception e, String message, int errorCode) { +void _handlePosixException(Exception e, String message, int errorCode) { // 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 enospc = 28; + const int eacces = 13; // Catch errors and bail when: switch (errorCode) { case enospc: @@ -504,6 +506,13 @@ void _handleLinuxException(Exception e, String message, int errorCode) { 'Free up space and try again.', ); break; + case eacces: + throwToolExit( + '$message. The flutter tool cannot access the file.\n' + 'Please ensure that the SDK and/or project is installed in a location ' + 'that has read/write permissions for the current user.' + ); + break; default: // Caller must rethrow the exception. break; diff --git a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart index a3de2e77e8a..9d6f1b0758b 100644 --- a/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart +++ b/packages/flutter_tools/test/general.shard/base/error_handling_io_test.dart @@ -28,6 +28,11 @@ final Platform linuxPlatform = FakePlatform( environment: {} ); +final Platform macOSPlatform = FakePlatform( + operatingSystem: 'macos', + environment: {} +); + void setupWriteMocks({ FileSystem mockFileSystem, ErrorHandlingFileSystem fs, @@ -194,6 +199,7 @@ void main() { group('throws ToolExit on Linux', () { const int enospc = 28; + const int eacces = 13; MockFileSystem mockFileSystem; ErrorHandlingFileSystem fs; @@ -206,6 +212,103 @@ void main() { when(mockFileSystem.path).thenReturn(MockPathContext()); }); + testWithoutContext('when access is denied', () async { + setupWriteMocks( + mockFileSystem: mockFileSystem, + fs: fs, + errorCode: eacces, + ); + + final File file = fs.file('file'); + + const String expectedMessage = 'The flutter tool cannot access the file'; + expect(() async => await file.writeAsBytes([0]), + throwsToolExit(message: expectedMessage)); + expect(() async => await file.writeAsString(''), + throwsToolExit(message: expectedMessage)); + expect(() => file.writeAsBytesSync([0]), + throwsToolExit(message: expectedMessage)); + expect(() => file.writeAsStringSync(''), + throwsToolExit(message: expectedMessage)); + expect(() => file.openSync(), + throwsToolExit(message: expectedMessage)); + }); + + 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([0]), + throwsToolExit(message: expectedMessage)); + expect(() async => await file.writeAsString(''), + throwsToolExit(message: expectedMessage)); + expect(() => file.writeAsBytesSync([0]), + throwsToolExit(message: expectedMessage)); + expect(() => file.writeAsStringSync(''), + throwsToolExit(message: expectedMessage)); + }); + + testWithoutContext('when creating a temporary dir on a full device', () async { + setupDirectoryMocks( + 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)); + }); + }); + + + group('throws ToolExit on macOS', () { + const int enospc = 28; + const int eacces = 13; + MockFileSystem mockFileSystem; + ErrorHandlingFileSystem fs; + + setUp(() { + mockFileSystem = MockFileSystem(); + fs = ErrorHandlingFileSystem( + delegate: mockFileSystem, + platform: macOSPlatform, + ); + when(mockFileSystem.path).thenReturn(MockPathContext()); + }); + + testWithoutContext('when access is denied', () async { + setupWriteMocks( + mockFileSystem: mockFileSystem, + fs: fs, + errorCode: eacces, + ); + + final File file = fs.file('file'); + + const String expectedMessage = 'The flutter tool cannot access the file'; + expect(() async => await file.writeAsBytes([0]), + throwsToolExit(message: expectedMessage)); + expect(() async => await file.writeAsString(''), + throwsToolExit(message: expectedMessage)); + expect(() => file.writeAsBytesSync([0]), + throwsToolExit(message: expectedMessage)); + expect(() => file.writeAsStringSync(''), + throwsToolExit(message: expectedMessage)); + expect(() => file.openSync(), + throwsToolExit(message: expectedMessage)); + }); + testWithoutContext('when writing to a full device', () async { setupWriteMocks( mockFileSystem: mockFileSystem, @@ -365,6 +468,7 @@ void main() { group('ProcessManager on linux throws tool exit', () { const int enospc = 28; + const int eacces = 13; test('when writing to a full device', () { final MockProcessManager mockProcessManager = MockProcessManager(); @@ -386,6 +490,74 @@ void main() { expect(() => processManager.runSync(['foo']), throwsToolExit(message: expectedMessage)); }); + + test('when permissions are denied', () { + final MockProcessManager mockProcessManager = MockProcessManager(); + final ProcessManager processManager = ErrorHandlingProcessManager( + delegate: mockProcessManager, + platform: linuxPlatform, + ); + setupProcessManagerMocks(mockProcessManager, eacces); + + const String expectedMessage = 'The flutter tool cannot access the file'; + expect(() => processManager.canRun('foo'), + throwsToolExit(message: expectedMessage)); + expect(() => processManager.killPid(1), + throwsToolExit(message: expectedMessage)); + expect(() async => await processManager.start(['foo']), + throwsToolExit(message: expectedMessage)); + expect(() async => await processManager.run(['foo']), + throwsToolExit(message: expectedMessage)); + expect(() => processManager.runSync(['foo']), + throwsToolExit(message: expectedMessage)); + }); + }); + + group('ProcessManager on macOS throws tool exit', () { + const int enospc = 28; + const int eacces = 13; + + test('when writing to a full device', () { + final MockProcessManager mockProcessManager = MockProcessManager(); + final ProcessManager processManager = ErrorHandlingProcessManager( + delegate: mockProcessManager, + platform: macOSPlatform, + ); + setupProcessManagerMocks(mockProcessManager, enospc); + + const String expectedMessage = 'The target device is full'; + expect(() => processManager.canRun('foo'), + throwsToolExit(message: expectedMessage)); + expect(() => processManager.killPid(1), + throwsToolExit(message: expectedMessage)); + expect(() async => await processManager.start(['foo']), + throwsToolExit(message: expectedMessage)); + expect(() async => await processManager.run(['foo']), + throwsToolExit(message: expectedMessage)); + expect(() => processManager.runSync(['foo']), + throwsToolExit(message: expectedMessage)); + }); + + test('when permissions are denied', () { + final MockProcessManager mockProcessManager = MockProcessManager(); + final ProcessManager processManager = ErrorHandlingProcessManager( + delegate: mockProcessManager, + platform: linuxPlatform, + ); + setupProcessManagerMocks(mockProcessManager, eacces); + + const String expectedMessage = 'The flutter tool cannot access the file'; + expect(() => processManager.canRun('foo'), + throwsToolExit(message: expectedMessage)); + expect(() => processManager.killPid(1), + throwsToolExit(message: expectedMessage)); + expect(() async => await processManager.start(['foo']), + throwsToolExit(message: expectedMessage)); + expect(() async => await processManager.run(['foo']), + throwsToolExit(message: expectedMessage)); + expect(() => processManager.runSync(['foo']), + throwsToolExit(message: expectedMessage)); + }); }); }