Skip to content

Reorg #49524

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

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft

Reorg #49524

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
2 changes: 1 addition & 1 deletion src/BuiltInTools/DotNetWatchTasks/DotNetWatchTasks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\dotnet-watch\Internal\MSBuildFileSetResult.cs" />
<Compile Include="..\dotnet-watch\Build\MSBuildFileSetResult.cs" />
</ItemGroup>

</Project>
32 changes: 32 additions & 0 deletions src/BuiltInTools/dotnet-watch/Build/EvaluationResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch;

internal sealed class EvaluationResult(IReadOnlyDictionary<string, FileItem> files, ProjectGraph? projectGraph)
{
public readonly IReadOnlyDictionary<string, FileItem> Files = files;
public readonly ProjectGraph? ProjectGraph = projectGraph;

public readonly FilePathExclusions ItemExclusions
= projectGraph != null ? FilePathExclusions.Create(projectGraph) : FilePathExclusions.Empty;

private readonly Lazy<IReadOnlySet<string>> _lazyBuildFiles
= new(() => projectGraph != null ? CreateBuildFileSet(projectGraph) : new HashSet<string>());

public static IReadOnlySet<string> CreateBuildFileSet(ProjectGraph projectGraph)
=> projectGraph.ProjectNodes.SelectMany(p => p.ProjectInstance.ImportPaths)
.Concat(projectGraph.ProjectNodes.Select(p => p.ProjectInstance.FullPath))
.ToHashSet(PathUtilities.OSSpecificPathComparer);

public IReadOnlySet<string> BuildFiles
=> _lazyBuildFiles.Value;

public void WatchFiles(FileWatcher fileWatcher)
{
fileWatcher.WatchContainingDirectories(Files.Keys, includeSubdirectories: true);
fileWatcher.WatchFiles(BuildFiles);
}
}
103 changes: 103 additions & 0 deletions src/BuiltInTools/dotnet-watch/Build/FilePathExclusions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Build.Graph;
using Microsoft.Build.Globbing;

namespace Microsoft.DotNet.Watch;

internal readonly struct FilePathExclusions(
IEnumerable<(MSBuildGlob glob, string value, string projectDir)> exclusionGlobs,
IReadOnlySet<string> outputDirectories)
{
public static readonly FilePathExclusions Empty = new(exclusionGlobs: [], outputDirectories: new HashSet<string>());

public static FilePathExclusions Create(ProjectGraph projectGraph)
{
var outputDirectories = new HashSet<string>(PathUtilities.OSSpecificPathComparer);
var globs = new Dictionary<(string fixedDirectoryPart, string wildcardDirectoryPart, string filenamePart), (MSBuildGlob glob, string value, string projectDir)>();

foreach (var projectNode in projectGraph.ProjectNodes)
{
if (projectNode.AreDefaultItemsEnabled())
{
var projectDir = projectNode.ProjectInstance.Directory;

foreach (var globValue in projectNode.GetDefaultItemExcludes())
{
var glob = MSBuildGlob.Parse(projectDir, globValue);
if (glob.IsLegal)
{
// The glob creates regex based on the three parts of the glob.
// Avoid adding duplicate globs that match the same files.
globs.TryAdd((glob.FixedDirectoryPart, glob.WildcardDirectoryPart, glob.FilenamePart), (glob, globValue, projectDir));
}
}
}
else
{
// If default items are not enabled exclude just the output directories.

TryAddOutputDir(projectNode.GetOutputDirectory());
TryAddOutputDir(projectNode.GetIntermediateOutputDirectory());

void TryAddOutputDir(string? dir)
{
try
{
if (dir != null)
{
// msbuild properties may use '\' as a directory separator even on Unix.
// GetFullPath does not normalize '\' to '/' on Unix.
if (Path.DirectorySeparatorChar == '/')
{
dir = dir.Replace('\\', '/');
}

outputDirectories.Add(Path.TrimEndingDirectorySeparator(Path.GetFullPath(dir)));
}
}
catch
{
// ignore
}
}
}
}

return new FilePathExclusions(globs.Values, outputDirectories);
}

public void Report(IReporter reporter)
{
foreach (var globsPerDirectory in exclusionGlobs.GroupBy(keySelector: static g => g.projectDir, elementSelector: static g => g.value))
{
reporter.Verbose($"Exclusion glob: '{string.Join(";", globsPerDirectory)}' under project '{globsPerDirectory.Key}'");
}

foreach (var dir in outputDirectories)
{
reporter.Verbose($"Excluded directory: '{dir}'");
}
}

internal bool IsExcluded(string fullPath, ChangeKind changeKind, IReporter reporter)
{
if (PathUtilities.ContainsPath(outputDirectories, fullPath))
{
reporter.Report(MessageDescriptor.IgnoringChangeInOutputDirectory, changeKind, fullPath);
return true;
}

foreach (var (glob, globValue, projectDir) in exclusionGlobs)
{
if (glob.IsMatch(fullPath))
{
reporter.Report(MessageDescriptor.IgnoringChangeInExcludedFile, fullPath, changeKind, "DefaultItemExcludes", globValue, projectDir);
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.FileSystem;
using Microsoft.Build.Graph;

namespace Microsoft.DotNet.Watch
Expand Down Expand Up @@ -64,7 +68,7 @@ internal class MSBuildFileSetFactory(
reporter.Output($"MSBuild output from target '{TargetName}':");
}

BuildUtilities.ReportBuildOutput(reporter, capturedOutput, success, projectDisplay: null);
BuildOutput.ReportBuildOutput(reporter, capturedOutput, success, projectDisplay: null);
if (!success)
{
return null;
Expand Down Expand Up @@ -121,7 +125,7 @@ void AddFile(string filePath, string? staticWebAssetPath)
ProjectGraph? projectGraph = null;
if (requireProjectGraph != null)
{
projectGraph = TryLoadProjectGraph(requireProjectGraph.Value);
projectGraph = TryLoadProjectGraph(requireProjectGraph.Value, cancellationToken);
if (projectGraph == null && requireProjectGraph == true)
{
return null;
Expand Down Expand Up @@ -194,7 +198,11 @@ private static string FindTargetsFile()
}

// internal for testing
internal ProjectGraph? TryLoadProjectGraph(bool projectGraphRequired)

/// <summary>
/// Tries to create a project graph by running the build evaluation phase on the <see cref="rootProjectFile"/>.
/// </summary>
internal ProjectGraph? TryLoadProjectGraph(bool projectGraphRequired, CancellationToken cancellationToken)
{
var globalOptions = new Dictionary<string, string>();

Expand All @@ -203,9 +211,11 @@ private static string FindTargetsFile()
globalOptions[name] = value;
}

var entryPoint = new ProjectGraphEntryPoint(rootProjectFile, globalOptions);

try
{
return new ProjectGraph(rootProjectFile, globalOptions);
return new ProjectGraph([entryPoint], ProjectCollection.GlobalProjectCollection, projectInstanceFactory: null, cancellationToken);
}
catch (Exception e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,13 @@ internal sealed class DotNetWatchContext
public required ProcessRunner ProcessRunner { get; init; }

public required ProjectOptions RootProjectOptions { get; init; }

public MSBuildFileSetFactory CreateMSBuildFileSetFactory()
=> new(
RootProjectOptions.ProjectPath,
RootProjectOptions.BuildArguments,
EnvironmentOptions,
ProcessRunner,
Reporter);
}
}
12 changes: 0 additions & 12 deletions src/BuiltInTools/dotnet-watch/EvaluationResult.cs

This file was deleted.

54 changes: 54 additions & 0 deletions src/BuiltInTools/dotnet-watch/FileWatcher/DirectoryWatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;

namespace Microsoft.DotNet.Watch;

/// <summary>
/// Watches for changes in a <see cref="WatchedDirectory"/> and its subdirectories.
/// </summary>
internal abstract class DirectoryWatcher(string watchedDirectory, ImmutableHashSet<string> watchedFileNames, bool includeSubdirectories) : IDisposable
{
public string WatchedDirectory { get; } = watchedDirectory;
public ImmutableHashSet<string> WatchedFileNames { get; set; } = watchedFileNames;
public bool IncludeSubdirectories { get; } = includeSubdirectories;

public event EventHandler<ChangedPath>? OnFileChange;
public event EventHandler<Exception>? OnError;

public abstract bool EnableRaisingEvents { get; set; }
public abstract void Dispose();

protected void NotifyChange(string fullPath, ChangeKind kind)
{
var onFileChange = OnFileChange;
if (onFileChange == null)
{
return;
}

var watchedFileNames = WatchedFileNames;
if (watchedFileNames.Count > 0 && !watchedFileNames.Contains(Path.GetFileName(fullPath)))
{
return;
}

onFileChange.Invoke(this, new ChangedPath(fullPath, kind));
}

protected void NotifyError(Exception e)
{
OnError?.Invoke(this, e);
}

public static DirectoryWatcher Create(string watchedDirectory, ImmutableHashSet<string> watchedFileNames, bool includeSubdirectories)
=> Create(watchedDirectory, watchedFileNames, EnvironmentVariables.IsPollingEnabled, includeSubdirectories);

public static DirectoryWatcher Create(string watchedDirectory, ImmutableHashSet<string> watchedFileNames, bool usePollingWatcher, bool includeSubdirectories)
{
return usePollingWatcher ?
new PollingDirectoryWatcher(watchedDirectory, watchedFileNames, includeSubdirectories) :
new EventBasedDirectoryWatcher(watchedDirectory, watchedFileNames, includeSubdirectories);
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.ComponentModel;

namespace Microsoft.DotNet.Watch
{
internal sealed class EventBasedDirectoryWatcher : IDirectoryWatcher
internal sealed class EventBasedDirectoryWatcher : DirectoryWatcher
{
public event EventHandler<ChangedPath>? OnFileChange;
public event EventHandler<Exception>? OnError;

public string WatchedDirectory { get; }
public bool IncludeSubdirectories { get; }
public Action<string>? Logger { get; set; }

private volatile bool _disposed;
private FileSystemWatcher? _fileSystemWatcher;
private readonly Lock _createLock = new();

internal EventBasedDirectoryWatcher(string watchedDirectory, bool includeSubdirectories)
internal EventBasedDirectoryWatcher(string watchedDirectory, ImmutableHashSet<string> watchedFileNames, bool includeSubdirectories)
: base(watchedDirectory, watchedFileNames, includeSubdirectories)
{
WatchedDirectory = watchedDirectory;
IncludeSubdirectories = includeSubdirectories;

CreateFileSystemWatcher();
}

public void Dispose()
public override void Dispose()
{
_disposed = true;
DisposeInnerWatcher();
Expand Down Expand Up @@ -54,7 +48,7 @@ private void WatcherErrorHandler(object sender, ErrorEventArgs e)
CreateFileSystemWatcher();
}

OnError?.Invoke(this, exception);
NotifyError(exception);
}

private void WatcherRenameHandler(object sender, RenamedEventArgs e)
Expand Down Expand Up @@ -142,12 +136,6 @@ private void WatcherAddedHandler(object sender, FileSystemEventArgs e)
NotifyChange(e.FullPath, ChangeKind.Add);
}

private void NotifyChange(string fullPath, ChangeKind kind)
{
// Only report file changes
OnFileChange?.Invoke(this, new ChangedPath(fullPath, kind));
}

private void CreateFileSystemWatcher()
{
lock (_createLock)
Expand Down Expand Up @@ -192,7 +180,7 @@ private void DisposeInnerWatcher()
}
}

public bool EnableRaisingEvents
public override bool EnableRaisingEvents
{
get => _fileSystemWatcher!.EnableRaisingEvents;
set => _fileSystemWatcher!.EnableRaisingEvents = value;
Expand Down
Loading
Loading