Skip to content
Closed
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
101 changes: 23 additions & 78 deletions src/Servers/Kestrel/Core/src/Internal/CertificatePathWatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Diagnostics;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
Expand All @@ -13,36 +12,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;

internal sealed partial class CertificatePathWatcher : IDisposable
{
private readonly Func<string, IFileProvider?> _fileProviderFactory;
private readonly string _contentRootDir;
private readonly IHostEnvironment _hostEnvironment;
private readonly ILogger<CertificatePathWatcher> _logger;

private readonly object _metadataLock = new();

/// <remarks>Acquire <see cref="_metadataLock"/> before accessing.</remarks>
private readonly Dictionary<string, DirectoryWatchMetadata> _metadataForDirectory = new();
/// <remarks>Acquire <see cref="_metadataLock"/> before accessing.</remarks>
private readonly Dictionary<string, FileWatchMetadata> _metadataForFile = new();

private ConfigurationReloadToken _reloadToken = new();
private bool _disposed;

public CertificatePathWatcher(IHostEnvironment hostEnvironment, ILogger<CertificatePathWatcher> logger)
: this(
hostEnvironment.ContentRootPath,
logger,
dir => Directory.Exists(dir) ? new PhysicalFileProvider(dir, ExclusionFilters.None) : null)
{
}

/// <remarks>
/// For testing.
/// </remarks>
internal CertificatePathWatcher(string contentRootPath, ILogger<CertificatePathWatcher> logger, Func<string, IFileProvider?> fileProviderFactory)
{
_contentRootDir = contentRootPath;
_hostEnvironment = hostEnvironment;
_logger = logger;
_fileProviderFactory = fileProviderFactory;
}

/// <summary>
Expand Down Expand Up @@ -104,42 +88,32 @@ internal void AddWatchUnsynchronized(CertificateConfig certificateConfig)
{
Debug.Assert(certificateConfig.IsFileCert, "AddWatch called on non-file cert");

var path = Path.Combine(_contentRootDir, certificateConfig.Path);
var dir = Path.GetDirectoryName(path)!;

if (!_metadataForDirectory.TryGetValue(dir, out var dirMetadata))
{
// If we wanted to detected deletions of this whole directory (which we don't since we ignore deletions),
// we'd probably need to watch the whole directory hierarchy

var fileProvider = _fileProviderFactory(dir);
if (fileProvider is null)
{
_logger.DirectoryDoesNotExist(dir, path);
return;
}

dirMetadata = new DirectoryWatchMetadata(fileProvider);
_metadataForDirectory.Add(dir, dirMetadata);

_logger.CreatedDirectoryWatcher(dir);
}
var contentRootPath = _hostEnvironment.ContentRootPath;
var path = Path.Combine(contentRootPath, certificateConfig.Path);
var relativePath = Path.GetRelativePath(contentRootPath, path);

if (!_metadataForFile.TryGetValue(path, out var fileMetadata))
{
// PhysicalFileProvider appears to be able to tolerate non-existent files, as long as the directory exists

var disposable = ChangeToken.OnChange(
() => dirMetadata.FileProvider.Watch(Path.GetFileName(path)),
() =>
{
var changeToken = _hostEnvironment.ContentRootFileProvider.Watch(relativePath);
if (ReferenceEquals(changeToken, NullChangeToken.Singleton))
{
_logger.NullChangeToken(path);
}
return changeToken;
},
static tuple => tuple.Item1.OnChange(tuple.Item2),
ValueTuple.Create(this, path));

fileMetadata = new FileWatchMetadata(disposable);
_metadataForFile.Add(path, fileMetadata);
dirMetadata.FileWatchCount++;

// We actually don't care if the file doesn't exist - we'll watch in case it is created
fileMetadata.LastModifiedTime = GetLastModifiedTimeOrMinimum(path, dirMetadata.FileProvider);
fileMetadata.LastModifiedTime = GetLastModifiedTimeOrMinimum(path, _hostEnvironment.ContentRootFileProvider);

_logger.CreatedFileWatcher(path);
}
Expand All @@ -153,7 +127,7 @@ internal void AddWatchUnsynchronized(CertificateConfig certificateConfig)
_logger.AddedObserver(path);

_logger.ObserverCount(path, fileMetadata.Configs.Count);
_logger.FileCount(dir, dirMetadata.FileWatchCount);
_logger.FileCount(contentRootPath, _metadataForFile.Count);
}

private DateTimeOffset GetLastModifiedTimeOrMinimum(string path, IFileProvider fileProvider)
Expand Down Expand Up @@ -181,9 +155,6 @@ private void OnChange(string path)
return;
}

// Existence implied by the fact that we're tracking the file
var dirMetadata = _metadataForDirectory[Path.GetDirectoryName(path)!];

// We ignore file changes that don't advance the last modified time.
// For example, if we lose access to the network share the file is
// stored on, we don't notify our listeners because no one wants
Expand All @@ -192,7 +163,7 @@ private void OnChange(string path)
// before a new cert is introduced with the old name.
// This also helps us in scenarios where the underlying file system
// reports more than one change for a single logical operation.
var lastModifiedTime = GetLastModifiedTimeOrMinimum(path, dirMetadata.FileProvider);
var lastModifiedTime = GetLastModifiedTimeOrMinimum(path, _hostEnvironment.ContentRootFileProvider);
if (lastModifiedTime > fileMetadata.LastModifiedTime)
{
fileMetadata.LastModifiedTime = lastModifiedTime;
Expand All @@ -219,6 +190,8 @@ private void OnChange(string path)
{
config.FileHasChanged = true;
}

_logger.FlaggedObservers(path, configs.Count);
}

// AddWatch and RemoveWatch don't affect the token, so this doesn't need to be under the semaphore.
Expand All @@ -238,8 +211,8 @@ internal void RemoveWatchUnsynchronized(CertificateConfig certificateConfig)
{
Debug.Assert(certificateConfig.IsFileCert, "RemoveWatch called on non-file cert");

var path = Path.Combine(_contentRootDir, certificateConfig.Path);
var dir = Path.GetDirectoryName(path)!;
var contentRootPath = _hostEnvironment.ContentRootPath;
var path = Path.Combine(contentRootPath, certificateConfig.Path);

if (!_metadataForFile.TryGetValue(path, out var fileMetadata))
{
Expand All @@ -257,35 +230,20 @@ internal void RemoveWatchUnsynchronized(CertificateConfig certificateConfig)

_logger.RemovedObserver(path);

// If we found fileMetadata, there must be a containing/corresponding dirMetadata
var dirMetadata = _metadataForDirectory[dir];

if (configs.Count == 0)
{
fileMetadata.Dispose();
_metadataForFile.Remove(path);
dirMetadata.FileWatchCount--;

_logger.RemovedFileWatcher(path);

if (dirMetadata.FileWatchCount == 0)
{
dirMetadata.Dispose();
_metadataForDirectory.Remove(dir);

_logger.RemovedDirectoryWatcher(dir);
}
}

_logger.ObserverCount(path, configs.Count);
_logger.FileCount(dir, dirMetadata.FileWatchCount);
_logger.FileCount(contentRootPath, _metadataForFile.Count);
}

/// <remarks>Test hook</remarks>
internal int TestGetDirectoryWatchCountUnsynchronized() => _metadataForDirectory.Count;

/// <remarks>Test hook</remarks>
internal int TestGetFileWatchCountUnsynchronized(string dir) => _metadataForDirectory.TryGetValue(dir, out var metadata) ? metadata.FileWatchCount : 0;
internal int TestGetFileWatchCountUnsynchronized() => _metadataForFile.Count;

/// <remarks>Test hook</remarks>
internal int TestGetObserverCountUnsynchronized(string path) => _metadataForFile.TryGetValue(path, out var metadata) ? metadata.Configs.Count : 0;
Expand All @@ -298,25 +256,12 @@ void IDisposable.Dispose()
}
_disposed = true;

foreach (var dirMetadata in _metadataForDirectory.Values)
{
dirMetadata.Dispose();
}

foreach (var fileMetadata in _metadataForFile.Values)
{
fileMetadata.Dispose();
}
}

private sealed class DirectoryWatchMetadata(IFileProvider fileProvider) : IDisposable
{
public readonly IFileProvider FileProvider = fileProvider;
public int FileWatchCount;

public void Dispose() => (FileProvider as IDisposable)?.Dispose();
}

private sealed class FileWatchMetadata(IDisposable disposable) : IDisposable
{
public readonly IDisposable Disposable = disposable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal;

internal static partial class CertificatePathWatcherLoggerExtensions
{
[Obsolete("No longer fired")]
[LoggerMessage(1, LogLevel.Warning, "Directory '{Directory}' does not exist so changes to the certificate '{Path}' will not be tracked.", EventName = "DirectoryDoesNotExist")]
public static partial void DirectoryDoesNotExist(this ILogger<CertificatePathWatcher> logger, string directory, string path);

Expand All @@ -16,12 +17,14 @@ internal static partial class CertificatePathWatcherLoggerExtensions
[LoggerMessage(3, LogLevel.Warning, "Attempted to remove unknown observer from path '{Path}'.", EventName = "UnknownObserver")]
public static partial void UnknownObserver(this ILogger<CertificatePathWatcher> logger, string path);

[Obsolete("No longer fired")]
[LoggerMessage(4, LogLevel.Debug, "Created directory watcher for '{Directory}'.", EventName = "CreatedDirectoryWatcher")]
public static partial void CreatedDirectoryWatcher(this ILogger<CertificatePathWatcher> logger, string directory);

[LoggerMessage(5, LogLevel.Debug, "Created file watcher for '{Path}'.", EventName = "CreatedFileWatcher")]
public static partial void CreatedFileWatcher(this ILogger<CertificatePathWatcher> logger, string path);

[Obsolete("No longer fired")]
[LoggerMessage(6, LogLevel.Debug, "Removed directory watcher for '{Directory}'.", EventName = "RemovedDirectoryWatcher")]
public static partial void RemovedDirectoryWatcher(this ILogger<CertificatePathWatcher> logger, string directory);

Expand Down Expand Up @@ -60,4 +63,7 @@ internal static partial class CertificatePathWatcherLoggerExtensions

[LoggerMessage(18, LogLevel.Trace, "Flagged {Count} observers of '{Path}' as changed.", EventName = "FlaggedObservers")]
public static partial void FlaggedObservers(this ILogger<CertificatePathWatcher> logger, string path, int count);

[LoggerMessage(18, LogLevel.Warning, "The host environment cannot watch path '{Path}' - is it within the content root?", EventName = "NullChangeToken")]
public static partial void NullChangeToken(this ILogger<CertificatePathWatcher> logger, string path);
}
Loading