| File: Commands\Clean\FileBasedAppArtifacts\CleanFileBasedAppArtifactsCommand.cs | Web Access |
| Project: src\sdk\src\Cli\dotnet\dotnet.csproj (dotnet) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; using System.Diagnostics; using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.FileBasedPrograms; namespace Microsoft.DotNet.Cli.Commands.Clean.FileBasedAppArtifacts; internal sealed class CleanFileBasedAppArtifactsCommand(ParseResult parseResult) : CommandBase<CleanFileBasedAppArtifactsCommandDefinition>(parseResult) { public override int Execute() { bool dryRun = _parseResult.GetValue(Definition.DryRunOption); using var metadataFileStream = OpenMetadataFile(); bool anyErrors = false; int count = 0; foreach (var folder in GetFoldersToRemove()) { if (dryRun) { Reporter.Verbose.WriteLine($"Would remove folder: {folder.FullName}"); count++; } else { try { folder.Delete(recursive: true); Reporter.Verbose.WriteLine($"Removed folder: {folder.FullName}"); count++; } catch (Exception ex) { Reporter.Error.WriteLine(string.Format(CliCommandStrings.CleanFileBasedAppArtifactsErrorRemovingFolder, folder, ex.Message).Red()); anyErrors = true; } } } Reporter.Output.WriteLine( dryRun ? CliCommandStrings.CleanFileBasedAppArtifactsWouldRemoveFolders : CliCommandStrings.CleanFileBasedAppArtifactsTotalFoldersRemoved, count); if (!dryRun) { UpdateMetadata(metadataFileStream); } return anyErrors ? 1 : 0; } private IEnumerable<DirectoryInfo> GetFoldersToRemove() { var directory = new DirectoryInfo(VirtualProjectBuilder.GetTempSubdirectory()); if (!directory.Exists) { Reporter.Error.WriteLine(string.Format(CliCommandStrings.CleanFileBasedAppArtifactsDirectoryNotFound, directory.FullName).Yellow()); yield break; } Reporter.Output.WriteLine(CliCommandStrings.CleanFileBasedAppArtifactsScanning, directory.FullName); var days = _parseResult.GetValue(Definition.DaysOption); var cutoff = DateTime.UtcNow.AddDays(-days); foreach (var subdir in directory.GetDirectories()) { if (subdir.LastWriteTimeUtc < cutoff) { yield return subdir; } } } private static FileInfo GetMetadataFile() { return new FileInfo(VirtualProjectBuilder.GetTempSubpath(RunFileArtifactsMetadata.FilePath)); } private FileStream? OpenMetadataFile() { if (!_parseResult.GetValue(Definition.AutomaticOption)) { return null; } // Open and lock the metadata file to ensure we are the only automatic cleanup process. return GetMetadataFile().Open(FileMode.Create, FileAccess.ReadWrite, FileShare.None); } private static void UpdateMetadata(FileStream? metadataFileStream) { if (metadataFileStream is null) { return; } var metadata = new RunFileArtifactsMetadata { LastAutomaticCleanupUtc = DateTime.UtcNow, }; JsonSerializer.Serialize(metadataFileStream, metadata, RunFileJsonSerializerContext.Default.RunFileArtifactsMetadata); } /// <summary> /// Starts a background process to clean up file-based app artifacts if needed. /// </summary> public static void StartAutomaticCleanupIfNeeded() { if (ShouldStartAutomaticCleanup()) { Reporter.Verbose.WriteLine("Starting automatic cleanup of file-based app artifacts."); var startInfo = new ProcessStartInfo { FileName = new Muxer().MuxerPath, Arguments = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart( [ CleanCommandDefinition.Name, CleanFileBasedAppArtifactsCommandDefinition.Name, CleanFileBasedAppArtifactsCommandDefinition.AutomaticOptionName, ]), UseShellExecute = false, CreateNoWindow = true, }; _ = Process.StartAndForget(startInfo); } } private static bool ShouldStartAutomaticCleanup() { if (Env.GetEnvironmentVariableAsBool("DOTNET_CLI_DISABLE_FILE_BASED_APP_ARTIFACTS_AUTOMATIC_CLEANUP", defaultValue: false)) { return false; } FileInfo? metadataFile = null; try { metadataFile = GetMetadataFile(); if (!metadataFile.Exists) { return true; } using var stream = metadataFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read); var metadata = JsonSerializer.Deserialize(stream, RunFileJsonSerializerContext.Default.RunFileArtifactsMetadata); if (metadata?.LastAutomaticCleanupUtc is not { } timestamp) { return true; } // Start automatic cleanup every two days. return timestamp.AddDays(2) < DateTime.UtcNow; } catch (Exception ex) { Reporter.Verbose.WriteLine($"Cannot access artifacts metadata file '{metadataFile?.FullName}': {ex}"); // If the file cannot be accessed, automatic cleanup might already be running. return false; } } } /// <summary> /// Metadata stored at the root level of the file-based app artifacts directory. /// </summary> internal sealed class RunFileArtifactsMetadata { public const string FilePath = "dotnet-run-file-artifacts-metadata.json"; public DateTime? LastAutomaticCleanupUtc { get; init; } }