Skip to content

Commit

Permalink
Improve file change post-processing
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat committed Dec 6, 2024
1 parent e748a13 commit c1bcc13
Show file tree
Hide file tree
Showing 17 changed files with 516 additions and 224 deletions.
5 changes: 1 addition & 4 deletions src/BuiltInTools/dotnet-watch/FileItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@ internal readonly record struct FileItem

/// <summary>
/// List of all projects that contain this file (does not contain duplicates).
/// Empty if <see cref="Change"/> is <see cref="ChangeKind.Add"/> and the
/// item has not been assigned to a project yet.
/// Empty if the item is added but not been assigned to a project yet.
/// </summary>
public required List<string> ContainingProjectPaths { get; init; }

public string? StaticWebAssetPath { get; init; }

public ChangeKind Change { get; init; }

public bool IsStaticFile => StaticWebAssetPath != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ private async ValueTask DisplayResultsAsync(WatchHotReloadService.Updates update
switch (updates.Status)
{
case ModuleUpdateStatus.None:
_reporter.Output("No C# changes to apply.");
_reporter.Report(MessageDescriptor.NoHotReloadChangesToApply);
break;

case ModuleUpdateStatus.Ready:
Expand Down
238 changes: 168 additions & 70 deletions src/BuiltInTools/dotnet-watch/HotReloadDotNetWatcher.cs

Large diffs are not rendered by default.

45 changes: 24 additions & 21 deletions src/BuiltInTools/dotnet-watch/Internal/FileWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ internal sealed class FileWatcher(IReporter reporter) : IDisposable
private readonly Dictionary<string, IDirectoryWatcher> _watchers = [];

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

public bool SuppressEvents { get; set; }

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

private void WatcherChangedHandler(object? sender, (string changedPath, ChangeKind kind) args)
private void WatcherChangedHandler(object? sender, ChangedPath change)
{
OnFileChange?.Invoke(args.changedPath, args.kind);
if (!SuppressEvents)
{
OnFileChange?.Invoke(change);
}
}

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

public Task<ChangedFile?> WaitForFileChangeAsync(Action? startedWatching, CancellationToken cancellationToken)
=> WaitForFileChangeAsync(
changeFilter: (path, kind) => new ChangedFile(new FileItem() { FilePath = path, ContainingProjectPaths = [] }, kind),
startedWatching,
cancellationToken);

public Task<ChangedFile?> WaitForFileChangeAsync(IReadOnlyDictionary<string, FileItem> fileSet, Action? startedWatching, CancellationToken cancellationToken)
=> WaitForFileChangeAsync(
changeFilter: (path, kind) => fileSet.TryGetValue(path, out var fileItem) ? new ChangedFile(fileItem, kind) : null,
public async Task<ChangedFile?> WaitForFileChangeAsync(IReadOnlyDictionary<string, FileItem> fileSet, Action? startedWatching, CancellationToken cancellationToken)
{
var changedPath = await WaitForFileChangeAsync(
acceptChange: change => fileSet.ContainsKey(change.Path),
startedWatching,
cancellationToken);

public async Task<ChangedFile?> WaitForFileChangeAsync(Func<string, ChangeKind, ChangedFile?> changeFilter, Action? startedWatching, CancellationToken cancellationToken)
return changedPath.HasValue ? new ChangedFile(fileSet[changedPath.Value.Path], changedPath.Value.Kind) : null;
}

public async Task<ChangedPath?> WaitForFileChangeAsync(Predicate<ChangedPath> acceptChange, Action? startedWatching, CancellationToken cancellationToken)
{
var fileChangedSource = new TaskCompletionSource<ChangedFile?>(TaskCreationOptions.RunContinuationsAsynchronously);
var fileChangedSource = new TaskCompletionSource<ChangedPath?>(TaskCreationOptions.RunContinuationsAsynchronously);
cancellationToken.Register(() => fileChangedSource.TrySetResult(null));

void FileChangedCallback(string path, ChangeKind kind)
void FileChangedCallback(ChangedPath change)
{
if (changeFilter(path, kind) is { } changedFile)
if (acceptChange(change))
{
fileChangedSource.TrySetResult(changedFile);
fileChangedSource.TrySetResult(change);
}
}

ChangedFile? changedFile;
ChangedPath? change;

OnFileChange += FileChangedCallback;
try
{
startedWatching?.Invoke();
changedFile = await fileChangedSource.Task;
change = await fileChangedSource.Task;
}
finally
{
OnFileChange -= FileChangedCallback;
}

return changedFile;
return change;
}

public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter reporter, Action? startedWatching, CancellationToken cancellationToken)
Expand All @@ -146,7 +149,7 @@ public static async ValueTask WaitForFileChangeAsync(string filePath, IReporter
watcher.WatchDirectories([Path.GetDirectoryName(filePath)!]);

var fileChange = await watcher.WaitForFileChangeAsync(
changeFilter: (path, kind) => path == filePath ? new ChangedFile(new FileItem { FilePath = path, ContainingProjectPaths = [] }, kind) : null,
acceptChange: change => change.Path == filePath,
startedWatching,
cancellationToken);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ internal enum ChangeKind
Delete
}

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

internal readonly record struct ChangedPath(string Path, ChangeKind Kind);
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Microsoft.DotNet.Watch
{
internal sealed class EventBasedDirectoryWatcher : IDirectoryWatcher
{
public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
public event EventHandler<ChangedPath>? OnFileChange;

public event EventHandler<Exception>? OnError;

Expand Down Expand Up @@ -118,7 +118,7 @@ private void WatcherAddedHandler(object sender, FileSystemEventArgs e)
private void NotifyChange(string fullPath, ChangeKind kind)
{
// Only report file changes
OnFileChange?.Invoke(this, (fullPath, kind));
OnFileChange?.Invoke(this, new ChangedPath(fullPath, kind));
}

private void CreateFileSystemWatcher()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Microsoft.DotNet.Watch
{
internal interface IDirectoryWatcher : IDisposable
{
event EventHandler<(string filePath, ChangeKind kind)> OnFileChange;
event EventHandler<ChangedPath> OnFileChange;

event EventHandler<Exception> OnError;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal sealed class PollingDirectoryWatcher : IDirectoryWatcher

private volatile bool _disposed;

public event EventHandler<(string filePath, ChangeKind kind)>? OnFileChange;
public event EventHandler<ChangedPath>? OnFileChange;

#pragma warning disable CS0067 // not used
public event EventHandler<Exception>? OnError;
Expand Down Expand Up @@ -212,7 +212,7 @@ private void NotifyChanges()
break;
}

OnFileChange?.Invoke(this, (path, kind));
OnFileChange?.Invoke(this, new ChangedPath(path, kind));
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/BuiltInTools/dotnet-watch/Internal/IReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ public bool TryGetMessage(string? prefix, object?[] args, [NotNullWhen(true)] ou
public static readonly MessageDescriptor ApplyUpdate_FileContentDoesNotMatchBuiltSource = new("{0} Expected if a source file is updated that is linked to project whose build is not up-to-date.", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor ConfiguredToLaunchBrowser = new("dotnet-watch is configured to launch a browser on ASP.NET Core application startup.", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor ConfiguredToUseBrowserRefresh = new("Configuring the app to use browser-refresh middleware", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor IgnoringChangeInHiddenDirectory = new("Ignoring change in hidden directory '{0}': {1} '{2}'", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor IgnoringChangeInOutputDirectory = new("Ignoring change in output directory: {0} '{1}'", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor FileAdditionTriggeredReEvaluation = new("File addition triggered re-evaluation.", "⌚", MessageSeverity.Verbose, s_id++);
public static readonly MessageDescriptor NoHotReloadChangesToApply = new ("No C# changes to apply.", "⌚", MessageSeverity.Output, s_id++);
}

internal interface IReporter
Expand Down
45 changes: 45 additions & 0 deletions src/BuiltInTools/dotnet-watch/Utilities/PathUtilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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.Watch;

internal static class PathUtilities
{
public static readonly IEqualityComparer<string> OSSpecificPathComparer = Path.DirectorySeparatorChar == '\\' ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal;

public static bool ContainsPath(IReadOnlySet<string> directories, string fullPath)
{
fullPath = Path.TrimEndingDirectorySeparator(fullPath);

while (true)
{
if (directories.Contains(fullPath))
{
return true;
}

var containingDir = Path.GetDirectoryName(fullPath);
if (containingDir == null)
{
return false;
}

fullPath = containingDir;
}
}

public static IEnumerable<string> GetContainingDirectories(string path)
{
while (true)
{
var containingDir = Path.GetDirectoryName(path);
if (containingDir == null)
{
yield break;
}

yield return containingDir;
path = containingDir;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public static bool IsNetCoreApp(this ProjectGraphNode projectNode)
public static bool IsNetCoreApp(this ProjectGraphNode projectNode, Version minVersion)
=> IsNetCoreApp(projectNode) && IsTargetFrameworkVersionOrNewer(projectNode, minVersion);

public static string? GetOutputDirectory(this ProjectGraphNode projectNode)
=> projectNode.ProjectInstance.GetPropertyValue("TargetPath") is { Length: >0 } path ? Path.GetDirectoryName(Path.Combine(projectNode.ProjectInstance.Directory, path)) : null;

public static string? GetIntermediateOutputDirectory(this ProjectGraphNode projectNode)
=> projectNode.ProjectInstance.GetPropertyValue("IntermediateOutputPath") is { Length: >0 } path ? Path.Combine(projectNode.ProjectInstance.Directory, path) : null;

public static IEnumerable<string> GetCapabilities(this ProjectGraphNode projectNode)
=> projectNode.ProjectInstance.GetItems("ProjectCapability").Select(item => item.EvaluatedInclude);

Expand Down
8 changes: 8 additions & 0 deletions test/dotnet-watch.Tests/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the MIT license. See License.txt in the project root for full license information. -->
<Project>
<Import Project="..\Directory.Build.targets" />

<!-- Workaround for https://github.com/dotnet/msbuild/issues/9709 -->
<Target Name="IncrementalClean" />

</Project>
Loading

0 comments on commit c1bcc13

Please sign in to comment.