Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public class DistributedApplicationBuilder : IDistributedApplicationBuilder
private readonly DistributedApplicationOptions _options;
private readonly HostApplicationBuilder _innerBuilder;
private readonly IUserSecretsManager _userSecretsManager;
private readonly IFileSystemService _directoryService;
private readonly FileSystemService _directoryService;

/// <inheritdoc />
public IHostEnvironment Environment => _innerBuilder.Environment;
Expand Down Expand Up @@ -304,7 +304,11 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
// Core things
// Create and register the directory service (first, so it can be used by other services)
_directoryService = new FileSystemService();
_innerBuilder.Services.AddSingleton<IFileSystemService>(_directoryService);
_innerBuilder.Services.AddSingleton<IFileSystemService>(sp =>
{
_directoryService.SetLogger(sp.GetRequiredService<ILogger<FileSystemService>>());
return _directoryService;
});

// Create and register the user secrets manager
var userSecretsFactory = new UserSecretsManagerFactory(_directoryService);
Expand Down
154 changes: 147 additions & 7 deletions src/Aspire.Hosting/Utils/FileSystemService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,132 @@

#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only

using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting;

/// <summary>
/// Default implementation of <see cref="IFileSystemService"/>.
/// </summary>
internal sealed class FileSystemService : IFileSystemService
internal sealed class FileSystemService : IFileSystemService, IDisposable
{
private readonly TempFileSystemService _tempDirectory = new();
private readonly TempFileSystemService _tempDirectory;
private ILogger<FileSystemService>? _logger;
private readonly bool _preserveTempFiles;

public FileSystemService()
{
// Check environment variable to preserve temp files for debugging
_preserveTempFiles = Environment.GetEnvironmentVariable("ASPIRE_PRESERVE_TEMP_FILES") is not null;

_tempDirectory = new TempFileSystemService(this);
}

/// <summary>
/// Sets the logger for this service. Called after service provider is built.
/// </summary>
/// <remarks>
/// The logger cannot be injected via constructor because the FileSystemService
/// is allocated before logging is fully initialized in the DistributedApplicationBuilder.
/// </remarks>
internal void SetLogger(ILogger<FileSystemService> logger)
{
_logger = logger;
}

/// <inheritdoc/>
public ITempFileSystemService TempDirectory => _tempDirectory;

// Track allocated temp files and directories
private readonly ConcurrentDictionary<string, bool> _allocatedPaths = new();

internal void TrackAllocatedPath(string path, bool isDirectory)
{
_allocatedPaths.TryAdd(path, isDirectory);

if (_logger?.IsEnabled(LogLevel.Debug) == true)
{
_logger.LogDebug("Allocated temporary {Type}: {Path}", isDirectory ? "directory" : "file", path);
}
}

internal void UntrackPath(string path)
{
_allocatedPaths.TryRemove(path, out _);
}

internal bool ShouldPreserveTempFiles() => _preserveTempFiles;

/// <summary>
/// Cleans up any remaining temporary files and directories.
/// </summary>
public void Dispose()
{
if (_preserveTempFiles)
{
_logger?.LogInformation("Skipping cleanup of {Count} temporary files/directories due to ASPIRE_PRESERVE_TEMP_FILES environment variable", _allocatedPaths.Count);
return;
}

if (_allocatedPaths.IsEmpty)
{
return;
}

_logger?.LogDebug("Cleaning up {Count} remaining temporary files/directories", _allocatedPaths.Count);

foreach (var kvp in _allocatedPaths)
{
var path = kvp.Key;
var isDirectory = kvp.Value;

try
{
if (isDirectory)
{
if (Directory.Exists(path))
{
Directory.Delete(path, recursive: true);
_logger?.LogDebug("Cleaned up temporary directory: {Path}", path);
}
}
else
{
if (File.Exists(path))
{
File.Delete(path);
_logger?.LogDebug("Cleaned up temporary file: {Path}", path);
}
}
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "Failed to clean up temporary {Type}: {Path}", isDirectory ? "directory" : "file", path);
}
}

_allocatedPaths.Clear();
}

/// <summary>
/// Implementation of <see cref="ITempFileSystemService"/>.
/// </summary>
private sealed class TempFileSystemService : ITempFileSystemService
{
private readonly FileSystemService _parent;

public TempFileSystemService(FileSystemService parent)
{
_parent = parent;
}

/// <inheritdoc/>
public TempDirectory CreateTempSubdirectory(string? prefix = null)
{
var path = Directory.CreateTempSubdirectory(prefix ?? "aspire").FullName;
return new DefaultTempDirectory(path);
_parent.TrackAllocatedPath(path, isDirectory: true);
return new DefaultTempDirectory(path, _parent);
}

/// <inheritdoc/>
Expand All @@ -33,14 +137,16 @@ public TempFile CreateTempFile(string? fileName = null)
if (fileName is null)
{
var tempFile = Path.GetTempFileName();
return new DefaultTempFile(tempFile, deleteParentDirectory: false);
_parent.TrackAllocatedPath(tempFile, isDirectory: false);
return new DefaultTempFile(tempFile, deleteParentDirectory: false, _parent);
}

// Create a temp subdirectory and place the named file inside it
var tempDir = Directory.CreateTempSubdirectory("aspire").FullName;
var filePath = Path.Combine(tempDir, fileName);
File.Create(filePath).Dispose();
return new DefaultTempFile(filePath, deleteParentDirectory: true);
_parent.TrackAllocatedPath(filePath, isDirectory: false);
return new DefaultTempFile(filePath, deleteParentDirectory: true, _parent);
}
}

Expand All @@ -50,16 +156,33 @@ public TempFile CreateTempFile(string? fileName = null)
private sealed class DefaultTempDirectory : TempDirectory
{
private readonly string _path;
private readonly FileSystemService _parent;
private bool _disposed;

public DefaultTempDirectory(string path)
public DefaultTempDirectory(string path, FileSystemService parent)
{
_path = path;
_parent = parent;
}

public override string Path => _path;

public override void Dispose()
{
if (_disposed)
{
return;
}

_disposed = true;
_parent.UntrackPath(_path);

// Skip deletion if preserve flag is set
if (_parent.ShouldPreserveTempFiles())
{
return;
}

try
{
if (Directory.Exists(_path))
Expand All @@ -81,17 +204,34 @@ private sealed class DefaultTempFile : TempFile
{
private readonly string _path;
private readonly bool _deleteParentDirectory;
private readonly FileSystemService _parent;
private bool _disposed;

public DefaultTempFile(string path, bool deleteParentDirectory)
public DefaultTempFile(string path, bool deleteParentDirectory, FileSystemService parent)
{
_path = path;
_deleteParentDirectory = deleteParentDirectory;
_parent = parent;
}

public override string Path => _path;

public override void Dispose()
{
if (_disposed)
{
return;
}

_disposed = true;
_parent.UntrackPath(_path);

// Skip deletion if preserve flag is set
if (_parent.ShouldPreserveTempFiles())
{
return;
}

try
{
if (File.Exists(_path))
Expand Down
Loading