Skip to content
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

Tests #43324

Closed
wants to merge 11 commits into from
Closed

Tests #43324

Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion src/BuiltInTools/DotNetDeltaApplier/StartupHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public static void Initialize()
// When launching the application process dotnet-watch sets Hot Reload environment variables via CLI environment directives (dotnet [env:X=Y] run).
// Currently, the CLI parser sets the env variables to the dotnet.exe process itself, rather then to the target process.
// This may cause the dotnet.exe process to connect to the named pipe and break it for the target process.
if (Path.ChangeExtension(processPath, ".exe") != Path.ChangeExtension(s_targetProcessPath, ".exe"))
if (string.Equals(Path.ChangeExtension(processPath, ".exe"), Path.ChangeExtension(s_targetProcessPath, ".exe"),
Path.PathSeparator == '\\' ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
{
Log($"Ignoring process '{processPath}', expecting '{s_targetProcessPath}'");
return;
Expand Down
10 changes: 5 additions & 5 deletions src/BuiltInTools/dotnet-watch/DotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken)

var environmentBuilder = EnvironmentVariablesBuilder.FromCurrentEnvironment();

FileItem? changedFile = null;
ChangedFile? changedFile = null;
var buildEvaluator = new BuildEvaluator(Context, RootFileSetFactory);
await using var browserConnector = new BrowserConnector(Context);

Expand Down Expand Up @@ -86,17 +86,17 @@ public override async Task WatchAsync(CancellationToken cancellationToken)

var processTask = ProcessRunner.RunAsync(processSpec, Context.Reporter, isUserApplication: true, processExitedSource: null, combinedCancellationSource.Token);

Task<FileItem?> fileSetTask;
Task<ChangedFile?> fileSetTask;
Task finishedTask;

while (true)
{
fileSetTask = fileSetWatcher.GetChangedFileAsync(startedWatching: null, combinedCancellationSource.Token);
finishedTask = await Task.WhenAny(processTask, fileSetTask, cancelledTaskSource.Task);

if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result is FileItem fileItem)
if (staticFileHandler != null && finishedTask == fileSetTask && fileSetTask.Result.HasValue)
{
if (await staticFileHandler.HandleFileChangesAsync([fileItem], combinedCancellationSource.Token))
if (await staticFileHandler.HandleFileChangesAsync([fileSetTask.Result.Value], combinedCancellationSource.Token))
{
// We're able to handle the file change event without doing a full-rebuild.
continue;
Expand Down Expand Up @@ -131,7 +131,7 @@ public override async Task WatchAsync(CancellationToken cancellationToken)
Debug.Assert(finishedTask == fileSetTask);
changedFile = fileSetTask.Result;
Debug.Assert(changedFile != null, "ChangedFile should only be null when cancelled");
Context.Reporter.Output($"File changed: {changedFile.Value.FilePath}");
Context.Reporter.Output($"File changed: {changedFile.Value.Item.FilePath}");
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/BuiltInTools/dotnet-watch/FileItem.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.DotNet.Watcher.Internal;

namespace Microsoft.DotNet.Watcher
{
internal readonly struct FileItem
internal readonly record struct FileItem
{
public string FilePath { get; init; }

Expand All @@ -14,7 +16,7 @@ internal readonly struct FileItem

public string? StaticWebAssetPath { get; init; }

public bool IsNewFile { get; init; }
public ChangeKind Change { get; init; }

public bool IsStaticFile => StaticWebAssetPath != null;
}
Expand Down
4 changes: 2 additions & 2 deletions src/BuiltInTools/dotnet-watch/Filters/BuildEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ public IReadOnlyList<string> GetProcessArguments(int iteration)
return [context.RootProjectOptions.Command, .. context.RootProjectOptions.CommandArguments];
}

public async ValueTask<EvaluationResult> EvaluateAsync(FileItem? changedFile, CancellationToken cancellationToken)
public async ValueTask<EvaluationResult> EvaluateAsync(ChangedFile? changedFile, CancellationToken cancellationToken)
{
if (context.EnvironmentOptions.SuppressMSBuildIncrementalism)
{
RequiresRevaluation = true;
return _evaluationResult = await CreateEvaluationResult(cancellationToken);
}

if (_evaluationResult == null || RequiresMSBuildRevaluation(changedFile))
if (_evaluationResult == null || RequiresMSBuildRevaluation(changedFile?.Item))
{
RequiresRevaluation = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using Microsoft.CodeAnalysis.ExternalAccess.Watch.Api;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.Text;
using Microsoft.DotNet.Watcher.Internal;

namespace Microsoft.DotNet.Watcher.Tools;

Expand Down Expand Up @@ -106,13 +107,24 @@ ImmutableArray<DocumentInfo> MapDocuments(ProjectId mappedProjectId, IReadOnlyLi
}).ToImmutableArray();
}

public async ValueTask UpdateFileContentAsync(IEnumerable<FileItem> changedFiles, CancellationToken cancellationToken)
public async ValueTask UpdateFileContentAsync(IEnumerable<ChangedFile> changedFiles, CancellationToken cancellationToken)
{
var updatedSolution = CurrentSolution;

foreach (var changedFile in changedFiles)
var documentsToRemove = new List<DocumentId>();

foreach (var (changedFile, change) in changedFiles)
{
// when a file is added we reevaluate the project:
Debug.Assert(change != ChangeKind.Add);

var documentIds = updatedSolution.GetDocumentIdsWithFilePath(changedFile.FilePath);
if (change == ChangeKind.Delete)
{
documentsToRemove.AddRange(documentIds);
continue;
}

foreach (var documentId in documentIds)
{
var textDocument = updatedSolution.GetDocument(documentId)
Expand Down Expand Up @@ -140,9 +152,17 @@ public async ValueTask UpdateFileContentAsync(IEnumerable<FileItem> changedFiles
}
}

updatedSolution = RemoveDocuments(updatedSolution, documentsToRemove);

await ReportSolutionFilesAsync(SetCurrentSolution(updatedSolution), cancellationToken);
}

private static Solution RemoveDocuments(Solution solution, IEnumerable<DocumentId> ids)
=> solution
.RemoveDocuments(ids.Where(id => solution.GetDocument(id) != null).ToImmutableArray())
.RemoveAdditionalDocuments(ids.Where(id => solution.GetAdditionalDocument(id) != null).ToImmutableArray())
.RemoveAnalyzerConfigDocuments(ids.Where(id => solution.GetAnalyzerConfigDocument(id) != null).ToImmutableArray());

private static async ValueTask<SourceText> GetSourceTextAsync(string filePath, CancellationToken cancellationToken)
{
var zeroLengthRetryPerformed = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections;
using System.Diagnostics;
using Microsoft.Build.Graph;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Tools.Internal;

namespace Microsoft.DotNet.Watcher.Tools
Expand All @@ -13,14 +14,14 @@ internal sealed class ScopedCssFileHandler(IReporter reporter, ProjectNodeMap pr
{
private const string BuildTargetName = "GenerateComputedBuildStaticWebAssets";

public async ValueTask HandleFileChangesAsync(IReadOnlyList<FileItem> files, CancellationToken cancellationToken)
public async ValueTask HandleFileChangesAsync(IReadOnlyList<ChangedFile> files, CancellationToken cancellationToken)
{
var projectsToRefresh = new HashSet<ProjectGraphNode>();
var hasApplicableFiles = false;

for (int i = 0; i < files.Count; i++)
{
var file = files[i];
var file = files[i].Item;

if (!file.FilePath.EndsWith(".razor.css", StringComparison.Ordinal) &&
!file.FilePath.EndsWith(".cshtml.css", StringComparison.Ordinal))
Expand Down
5 changes: 3 additions & 2 deletions src/BuiltInTools/dotnet-watch/HotReload/StaticFileHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.StackTraceExplorer;
using Microsoft.DotNet.Watcher.Internal;
using Microsoft.Extensions.Tools.Internal;

namespace Microsoft.DotNet.Watcher.Tools
Expand All @@ -18,13 +19,13 @@ internal sealed class StaticFileHandler(IReporter reporter, ProjectNodeMap proje
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};

public async ValueTask<bool> HandleFileChangesAsync(IReadOnlyList<FileItem> files, CancellationToken cancellationToken)
public async ValueTask<bool> HandleFileChangesAsync(IReadOnlyList<ChangedFile> files, CancellationToken cancellationToken)
{
var allFilesHandled = true;
var refreshRequests = new Dictionary<BrowserRefreshServer, List<string>>();
for (int i = 0; i < files.Count; i++)
{
var file = files[i];
var file = files[i].Item;

if (file.StaticWebAssetPath is null)
{
Expand Down
47 changes: 33 additions & 14 deletions src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
HotReloadFileSetWatcher? fileSetWatcher = null;
EvaluationResult? evaluationResult = null;
RunningProject? rootRunningProject = null;
Task<FileItem[]?>? fileSetWatcherTask = null;
Task<ChangedFile[]?>? fileSetWatcherTask = null;

try
{
Expand Down Expand Up @@ -178,7 +178,7 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
// When a new file is added we need to run design-time build to find out
// what kind of the file it is and which project(s) does it belong to (can be linked, web asset, etc.).
// We don't need to rebuild and restart the application though.
if (changedFiles.Any(f => f.IsNewFile))
if (changedFiles.Any(f => f.Change is ChangeKind.Add))
{
Context.Reporter.Verbose("File addition triggered re-evaluation.");

Expand All @@ -195,9 +195,9 @@ public override async Task WatchAsync(CancellationToken shutdownCancellationToke
// update files in the change set with new evaluation info:
for (int i = 0; i < changedFiles.Length; i++)
{
if (evaluationResult.Files.TryGetValue(changedFiles[i].FilePath, out var evaluatedFile))
if (evaluationResult.Files.TryGetValue(changedFiles[i].Item.FilePath, out var evaluatedFile))
{
changedFiles[i] = evaluatedFile;
changedFiles[i] = changedFiles[i] with { Item = evaluatedFile };
}
}

Expand Down Expand Up @@ -336,24 +336,43 @@ await Task.WhenAll(
}
}

private void ReportFileChanges(IReadOnlyList<FileItem> fileItems)
private void ReportFileChanges(IReadOnlyList<ChangedFile> changedFiles)
{
Report(added: true);
Report(added: false);
Report(kind: ChangeKind.Add);
Report(kind: ChangeKind.Update);
Report(kind: ChangeKind.Delete);

void Report(bool added)
void Report(ChangeKind kind)
{
var items = fileItems.Where(item => item.IsNewFile == added).ToArray();
var items = changedFiles.Where(item => item.Change == kind).ToArray();
if (items is not [])
{
Context.Reporter.Output(GetMessage(items, added));
Context.Reporter.Output(GetMessage(items, kind));
}
}

string GetMessage(IReadOnlyList<FileItem> items, bool added)
=> items is [var item]
? (added ? "File added: " : "File changed: ") + GetRelativeFilePath(item.FilePath)
: (added ? "Files added: " : "Files changed: ") + string.Join(", ", items.Select(f => GetRelativeFilePath(f.FilePath)));
string GetMessage(IReadOnlyList<ChangedFile> items, ChangeKind kind)
=> items is [{Item: var item }]
? GetSingularMessage(kind) + ": " + GetRelativeFilePath(item.FilePath)
: GetPlurarMessage(kind) + ": " + string.Join(", ", items.Select(f => GetRelativeFilePath(f.Item.FilePath)));

static string GetSingularMessage(ChangeKind kind)
=> kind switch
{
ChangeKind.Update => "File updated",
ChangeKind.Add => "File added",
ChangeKind.Delete => "File deleted",
_ => throw new InvalidOperationException()
};

static string GetPlurarMessage(ChangeKind kind)
=> kind switch
{
ChangeKind.Update => "Files updated",
ChangeKind.Add => "Files added",
ChangeKind.Delete => "Files deleted",
_ => throw new InvalidOperationException()
};
}

private async ValueTask<EvaluationResult> EvaluateRootProjectAsync(CancellationToken cancellationToken)
Expand Down
16 changes: 8 additions & 8 deletions src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal sealed class FileWatcher(IReadOnlyDictionary<string, FileItem> fileSet,
private readonly Dictionary<string, IFileSystemWatcher> _watchers = [];

private bool _disposed;
public event Action<string, bool>? OnFileChange;
public event Action<string, ChangeKind>? OnFileChange;

public void Dispose()
{
Expand Down Expand Up @@ -73,9 +73,9 @@ private void WatcherErrorHandler(object? sender, Exception error)
}
}

private void WatcherChangedHandler(object? sender, (string changedPath, bool newFile) args)
private void WatcherChangedHandler(object? sender, (string changedPath, ChangeKind kind) args)
{
OnFileChange?.Invoke(args.changedPath, args.newFile);
OnFileChange?.Invoke(args.changedPath, args.kind);
}

private void DisposeWatcher(string directory)
Expand All @@ -101,22 +101,22 @@ private void EnsureNotDisposed()
private static string EnsureTrailingSlash(string path)
=> (path is [.., var last] && last != Path.DirectorySeparatorChar) ? path + Path.DirectorySeparatorChar : path;

public async Task<FileItem?> GetChangedFileAsync(Action? startedWatching, CancellationToken cancellationToken)
public async Task<ChangedFile?> GetChangedFileAsync(Action? startedWatching, CancellationToken cancellationToken)
{
StartWatching();

var fileChangedSource = new TaskCompletionSource<FileItem?>(TaskCreationOptions.RunContinuationsAsynchronously);
var fileChangedSource = new TaskCompletionSource<ChangedFile?>(TaskCreationOptions.RunContinuationsAsynchronously);
cancellationToken.Register(() => fileChangedSource.TrySetResult(null));

void FileChangedCallback(string path, bool newFile)
void FileChangedCallback(string path, ChangeKind kind)
{
if (fileSet.TryGetValue(path, out var fileItem))
{
fileChangedSource.TrySetResult(fileItem);
fileChangedSource.TrySetResult(new ChangedFile(fileItem, kind));
}
}

FileItem? changedFile;
ChangedFile? changedFile;

OnFileChange += FileChangedCallback;
try
Expand Down
13 changes: 13 additions & 0 deletions src/BuiltInTools/dotnet-watch/Internal/FileWatcher/ChangeKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.DotNet.Watcher.Internal;

internal enum ChangeKind
{
Update,
Add,
Delete
}

internal readonly record struct ChangedFile(FileItem Item, ChangeKind Change);
Loading
Loading