-
Notifications
You must be signed in to change notification settings - Fork 745
Add automatic temp file tracking and cleanup to FileSystemService #13361
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
… logging - Add tracking list for allocated temporary files/directories - Implement removal from tracking list when TempFile/TempDirectory are disposed - Add IDisposable to FileSystemService for cleanup on apphost shutdown - Add environment variable ASPIRE_PRESERVE_TEMP_FILES to skip cleanup for debugging - Add verbose logging when allocating files/directories and during cleanup - Add 21 comprehensive tests covering all new functionality - Update DI registration to set logger via factory lambda Co-authored-by: davidfowl <[email protected]>
…simplify tracking with ConcurrentDictionary - Replace Environment.GetEnvironmentVariable with IConfiguration["ASPIRE_PRESERVE_TEMP_FILES"] - Pass IConfiguration to FileSystemService constructor from DistributedApplicationBuilder - Use ConcurrentDictionary<string, IDisposable> to track temp items instead of ConcurrentBag - Items automatically untrack themselves on dispose via UntrackItem - Update all tests to pass IConfiguration to FileSystemService constructor - Add necessary using statements to test files Co-authored-by: davidfowl <[email protected]>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 13361Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 13361" |
1 similar comment
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 13361Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 13361" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR enhances the FileSystemService with automatic tracking and cleanup of temporary files and directories when the AppHost shuts down, along with debugging support via configuration settings.
Key changes:
- Implements automatic tracking using
ConcurrentDictionary<string, IDisposable>to track all allocated temp files/directories - Makes
FileSystemServiceimplementIDisposablefor cleanup on AppHost shutdown - Adds
ASPIRE_PRESERVE_TEMP_FILESconfiguration option to skip cleanup for debugging scenarios - Integrates logging with debug-level logs for allocation/cleanup and info-level for preservation
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Aspire.Hosting/Utils/FileSystemService.cs | Core implementation: adds IDisposable, tracking dictionary, preserve configuration, logging integration, and cleanup logic in Dispose() method |
| src/Aspire.Hosting/DistributedApplicationBuilder.cs | Updates DI registration to pass IConfiguration to FileSystemService constructor and sets logger via factory lambda after service provider initialization |
| tests/Aspire.Hosting.Tests/FileSystemServiceTests.cs | Adds comprehensive test coverage for cleanup functionality, preserve configuration, mixed scenarios, and idempotent dispose |
| tests/Aspire.Hosting.Tests/UserSecretsParameterDefaultTests.cs | Updates test helper to pass IConfiguration to FileSystemService constructor |
| tests/Aspire.Hosting.Tests/SecretsStoreTests.cs | Updates test helper to pass IConfiguration to FileSystemService constructor |
| tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs | Updates test initialization to pass IConfiguration to FileSystemService constructor |
| tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs | Updates test initialization to pass IConfiguration to FileSystemService constructor |
| tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs | Updates test initialization to pass IConfiguration to FileSystemService constructor |
| tests/Aspire.Hosting.Tests/AspireStoreTests.cs | Updates test initialization to pass IConfiguration to FileSystemService constructor |
|
|
||
| // Clean up manually | ||
| Directory.Delete(dirPath, recursive: true); | ||
| File.Delete(filePath); |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manual cleanup order in this test could cause a failure. The test attempts to delete the file first (line 256) and then the directory (line 255), but if the file still exists in the directory when trying to delete the directory, this could fail depending on the filesystem state.
Suggested fix:
// Clean up manually - delete directory recursively (includes the file)
Directory.Delete(dirPath, recursive: true);Remove the separate File.Delete(filePath) call since deleting the directory recursively will handle the file as well.
| File.Delete(filePath); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test cleanup code is correct as-is. The tempDir (line 239) and tempFile (line 240) are two independent items - the file is NOT inside the directory. tempFile is created with CreateTempFile() (no filename parameter), which creates a standalone temp file, not a file within a subdirectory. Therefore, both need to be deleted separately.
| // Clean up manually | ||
| File.Delete(filePath); |
Copilot
AI
Dec 6, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test's manual cleanup could fail because it tries to delete the file separately instead of relying on the directory deletion. Since CreateTempFile() with a filename creates both a temp directory and a file within it, and the temp file's parent directory should be deleted when the file is disposed (when deleteParentDirectory: true), the cleanup should delete the parent directory recursively.
However, looking at the test scenario, when preserve is enabled, both the file and its parent directory are preserved. The manual cleanup should handle this correctly by getting the parent directory path and deleting it recursively.
Suggested fix:
// Clean up manually - delete the parent directory which contains the file
var parentDir = Path.GetDirectoryName(filePath);
if (parentDir is not null && Directory.Exists(parentDir))
{
Directory.Delete(parentDir, recursive: true);
}| // Clean up manually | |
| File.Delete(filePath); | |
| // Clean up manually - delete the parent directory which contains the file | |
| var parentDir = Path.GetDirectoryName(filePath); | |
| if (parentDir is not null && Directory.Exists(parentDir)) | |
| { | |
| Directory.Delete(parentDir, recursive: true); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test cleanup code is correct as-is. The tempFile is created with CreateTempFile() without a filename parameter, which creates a standalone temp file directly in the system temp directory (not in a subdirectory). Therefore, File.Delete(filePath) is the appropriate cleanup method. There is no parent directory to delete in this case.
…hods - Add _disposed flag to FileSystemService.Dispose() to prevent multiple disposals - Add XML documentation to internal methods: TrackItem, UntrackItem, ShouldPreserveTempFiles, and Logger - Add test for idempotent dispose behavior - Addresses bot feedback while keeping test cleanup code correct as-is Co-authored-by: davidfowl <[email protected]>
|
|
||
| _logger?.LogDebug("Cleaning up {Count} remaining temporary files/directories", _allocatedItems.Count); | ||
|
|
||
| foreach (var kvp in _allocatedItems) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens here if someone adds a tracked file whilst it is disposing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a snapshot, so it will miss cleanup.
@copilot lets mark that we are disposing so that any new allocations throw, so that new files can't be added while disposing. We need to make this thread safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made TrackItem thread-safe by checking the _disposed flag before adding items. If the service is disposed, it now throws ObjectDisposedException preventing new temp files/directories from being allocated during or after disposal. Added tests to verify this behavior. (87cabce)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot There's a super tight race where one thread is about to call _allocatedItems.TryAdd(path, item); (before Dispose is called) and then another thread calls Dispose is called just before we are about to enumerate the _allocatedItems. It maybe unrealstic but it is possible (thread scheduling and all that).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed the race condition by adding a _disposeLock object. Both TrackItem and the beginning of Dispose() now use lock (_disposeLock) to atomically check and set the _disposed flag. This ensures that once Dispose() sets _disposed = true, no thread can add items to the dictionary. (a0db49c)
…isposed - TrackItem now checks _disposed flag and throws ObjectDisposedException if service is disposed - Prevents new temp files/directories from being allocated during or after disposal - Add tests verifying ObjectDisposedException is thrown when creating temp files/directories after disposal - Addresses thread safety concern from @davidfowl and @mitchdenny Co-authored-by: davidfowl <[email protected]>
| /// <summary> | ||
| /// Gets whether temporary files should be preserved for debugging. | ||
| /// </summary> | ||
| internal bool ShouldPreserveTempFiles() => _preserveTempFiles; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Make this a property
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed ShouldPreserveTempFiles() from a method to a property by removing the parentheses. Updated all callers to use property syntax. (a0db49c)
| /// <summary> | ||
| /// Gets the logger for this service, if set. | ||
| /// </summary> | ||
| internal ILogger<FileSystemService>? Logger => _logger; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Type this as ILogger instead of ILogger<FileSystemService>?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed Logger property type from ILogger<FileSystemService>? to ILogger? and updated the _logger field type accordingly. (a0db49c)
- Add lock (_disposeLock) around _disposed check and TryAdd in TrackItem to prevent race condition - Add lock in Dispose() to atomically set _disposed flag - Change Logger property type from ILogger<FileSystemService>? to ILogger? for flexibility - Change _logger field type from ILogger<FileSystemService>? to ILogger? - Convert ShouldPreserveTempFiles() method to property (remove parentheses) - Update all callers of ShouldPreserveTempFiles to use property syntax - Addresses thread safety concerns from @davidfowl and feedback on property/type improvements Co-authored-by: davidfowl <[email protected]>
Description
Enhances
FileSystemServicewith automatic tracking and cleanup of temporary files/directories on AppHost shutdown, plus debugging support via configuration.This PR implements comprehensive temporary file management to prevent temp file leaks and ensure proper cleanup when the AppHost shuts down.
Changes
ConcurrentDictionary<string, IDisposable>tracks all allocated temp files/directories using path as keyFileSystemServiceimplementsIDisposableto clean up remaining temp files when AppHost disposesTempFileandTempDirectoryautomatically remove themselves from tracking on disposeASPIRE_PRESERVE_TEMP_FILESconfiguration setting skips all cleanup for debugging scenariosIConfigurationinstead of environment variables for settingsDispose()are safe and will not cause errorsTrackItemuses lock to atomically check_disposedflag and add itemsDispose()uses the same lock to atomically set_disposedflagObjectDisposedExceptionwhen attempting to allocate temp files/directories after disposalImplementation Notes
FileSystemServiceacceptsIConfigurationin constructor, passed fromDistributedApplicationBuilderFileSystemServiceis instantiated before logging initializationILogger?for flexibilityConcurrentDictionary<string, IDisposable>where items untrack themselves on dispose_disposeLock) ensures atomic operations betweenTrackItemandDispose()_disposedflag prevents multiple cleanup attemptsShouldPreserveTempFilesimplemented as a property for API consistencyChecklist
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.