diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart index be4cf52a20..e0e6681816 100644 --- a/packages/flutter_tools/lib/src/base/file_system.dart +++ b/packages/flutter_tools/lib/src/base/file_system.dart @@ -198,6 +198,11 @@ class LocalFileSystem extends local_fs.LocalFileSystem { final ShutdownHooks shutdownHooks; + // Indicates that `dispose()` has been invoked or some shutdown hook has executed, + // resulting in the underlying temporary directory being cleaned up. + bool get disposed => _disposed; + bool _disposed = false; + Future dispose() async { _tryToDeleteTemp(); for (final MapEntry signalToken in _signalTokens.entries) { @@ -210,6 +215,7 @@ class LocalFileSystem extends local_fs.LocalFileSystem { final List _fatalSignals; void _tryToDeleteTemp() { + _disposed = true; try { if (_systemTemp?.existsSync() ?? false) { _systemTemp?.deleteSync(recursive: true); diff --git a/packages/flutter_tools/lib/src/ios/core_devices.dart b/packages/flutter_tools/lib/src/ios/core_devices.dart index e7eff51e28..2ccbb21a6a 100644 --- a/packages/flutter_tools/lib/src/ios/core_devices.dart +++ b/packages/flutter_tools/lib/src/ios/core_devices.dart @@ -79,14 +79,24 @@ class IOSCoreDeviceControl { try { final RunResult result = await _processUtils.run(command, throwOnError: true); + final bool isToolPossiblyShutdown = _fileSystem is LocalFileSystem && _fileSystem.disposed; - if (!output.existsSync()) { + // It's possible that the tool is in the process of shutting down, which + // could result in the temp directory being deleted after the shutdown hooks run + // before we check if `output` exists. If this happens, we shouldn't crash + // but just carry on as if no devices were found as the tool will exit on + // its own. + // + // See https://github.com/flutter/flutter/issues/141892 for details. + if (!isToolPossiblyShutdown && !output.existsSync()) { _logger.printError('After running the command ${command.join(' ')} the file'); _logger.printError('${output.path} was expected to exist, but it did not.'); _logger.printError('The process exited with code ${result.exitCode} and'); _logger.printError('Stdout:\n\n${result.stdout.trim()}\n'); _logger.printError('Stderr:\n\n${result.stderr.trim()}'); throw StateError('Expected the file ${output.path} to exist but it did not'); + } else if (isToolPossiblyShutdown) { + return []; } final String stringOutput = output.readAsStringSync(); _logger.printTrace(stringOutput); diff --git a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart index 860947c0ca..1b54f2fa51 100644 --- a/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart +++ b/packages/flutter_tools/test/general.shard/ios/core_devices_test.dart @@ -7,6 +7,7 @@ import 'package:file_testing/file_testing.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:flutter_tools/src/ios/core_devices.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; @@ -15,6 +16,15 @@ import 'package:flutter_tools/src/macos/xcode.dart'; import '../../src/common.dart'; import '../../src/fake_process_manager.dart'; +class LocalFileSystemFake extends LocalFileSystem { + LocalFileSystemFake.test({required super.signals}) : super.test(); + + MemoryFileSystem memoryFileSystem = MemoryFileSystem.test(); + + @override + Directory get systemTempDirectory => memoryFileSystem.systemTempDirectory; +} + void main() { late MemoryFileSystem fileSystem; @@ -1400,6 +1410,53 @@ invalid JSON expect(fakeProcessManager, hasNoRemainingExpectations); }); + testWithoutContext('Handles file system disposal', () async { + final LocalFileSystem localFs = LocalFileSystemFake.test(signals: Signals.test()); + deviceControl = IOSCoreDeviceControl( + logger: logger, + processManager: fakeProcessManager, + xcode: xcode, + fileSystem: localFs, + ); + final Directory tempDir = localFs.systemTempDirectory + .childDirectory('core_devices.rand0'); + final File tempFile = tempDir.childFile('core_device_list.json'); + final List args = [ + 'xcrun', + 'devicectl', + 'list', + 'devices', + '--timeout', + '5', + '--json-output', + tempFile.path, + ]; + fakeProcessManager.addCommand(FakeCommand( + command: args, + onRun: (_) { + // Simulate that the tool started shutting down and disposed the + // file system, causing the temp directory to be deleted before + // this program invocation returns a result. + localFs.dispose(); + expect(localFs.disposed, true); + }, + )); + + final List coreDevices = await deviceControl.getCoreDevices(); + expect(coreDevices, isEmpty); + expect( + logger.errorText, + isNot( + contains('After running the command xcrun devicectl list devices ' + '--timeout 5 --json-output ${tempFile.path} the file\n' + '${tempFile.path} was expected to exist, but it did not', + ), + ), + ); + expect(fakeProcessManager, hasNoRemainingExpectations); + }); + + testWithoutContext('No devices', () async { const String deviceControlOutput = ''' {