Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ private static void AddLifetime(IServiceCollection services)
{
if (!OperatingSystem.IsAndroid() && !OperatingSystem.IsBrowser() && !OperatingSystem.IsWasi() && !OperatingSystem.IsIOS() && !OperatingSystem.IsTvOS())
{
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
HostingHostBuilderExtensions.AddConsoleLifetime(services);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public partial class HostBuilder
{
private static void AddLifetime(IServiceCollection services)
{
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
HostingHostBuilderExtensions.AddConsoleLifetime(services);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.Hosting
{
Expand Down Expand Up @@ -341,6 +342,26 @@ internal static ServiceProviderOptions CreateDefaultServiceProviderOptions(HostB
};
}

// Factory helper for registering ConsoleLifetime. Uses the internal ctor so the diagnostic
// log message in WaitForStartAsync can inspect the application's IConfiguration to detect
// a likely-misconfigured content root. Going through a factory (rather than typed
// registration) lets us pass IConfiguration without adding a new public ctor to the
// pubternal ConsoleLifetime type.
[UnsupportedOSPlatform("android")]
[UnsupportedOSPlatform("browser")]
[UnsupportedOSPlatform("ios")]
[UnsupportedOSPlatform("tvos")]
internal static void AddConsoleLifetime(IServiceCollection collection)
{
collection.AddSingleton<IHostLifetime>(static sp => new ConsoleLifetime(
sp.GetRequiredService<IOptions<ConsoleLifetimeOptions>>(),
sp.GetRequiredService<IHostEnvironment>(),
sp.GetRequiredService<IHostApplicationLifetime>(),
sp.GetRequiredService<IOptions<HostOptions>>(),
sp.GetRequiredService<ILoggerFactory>(),
sp.GetService<IConfiguration>()));
}

/// <summary>
/// Listens for Ctrl+C or SIGTERM and calls <see cref="IHostApplicationLifetime.StopApplication"/> to start the shutdown process.
/// This will unblock extensions like RunAsync and WaitForShutdownAsync.
Expand All @@ -353,7 +374,7 @@ internal static ServiceProviderOptions CreateDefaultServiceProviderOptions(HostB
[UnsupportedOSPlatform("tvos")]
public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices(collection => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());
return hostBuilder.ConfigureServices(AddConsoleLifetime);
}

/// <summary>
Expand All @@ -371,7 +392,7 @@ public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder, Act
{
return hostBuilder.ConfigureServices(collection =>
{
collection.AddSingleton<IHostLifetime, ConsoleLifetime>();
AddConsoleLifetime(collection);
collection.Configure(configureOptions);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Runtime.Versioning;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -44,6 +48,13 @@ public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironmen
/// <param name="loggerFactory">An object to configure the logging system and create instances of <see cref="ILogger"/> from the registered <see cref="ILoggerProvider"/>.</param>
/// <exception cref="ArgumentNullException"><paramref name="options"/> or <paramref name="environment"/> or <paramref name="applicationLifetime"/> or <paramref name="hostOptions"/> or <paramref name="loggerFactory"/> is <see langword="null"/>.</exception>
public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions, ILoggerFactory loggerFactory)
: this(options, environment, applicationLifetime, hostOptions, loggerFactory, configuration: null) { }

// Internal ctor accepting IConfiguration for diagnostic logging. Kept internal to avoid
// adding a new public API surface to a pubternal class; ConsoleLifetime is registered via
// a factory (see HostingHostBuilderExtensions.AddConsoleLifetime) so DI doesn't need
// to pick this ctor automatically.
internal ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironment environment, IHostApplicationLifetime applicationLifetime, IOptions<HostOptions> hostOptions, ILoggerFactory loggerFactory, IConfiguration? configuration)
{
ArgumentNullException.ThrowIfNull(options?.Value, nameof(options));
ArgumentNullException.ThrowIfNull(applicationLifetime);
Expand All @@ -55,6 +66,7 @@ public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironmen
Environment = environment;
ApplicationLifetime = applicationLifetime;
HostOptions = hostOptions.Value;
Configuration = configuration;
Logger = loggerFactory.CreateLogger("Microsoft.Hosting.Lifetime");
}

Expand All @@ -66,6 +78,8 @@ public ConsoleLifetime(IOptions<ConsoleLifetimeOptions> options, IHostEnvironmen

private HostOptions HostOptions { get; }

private IConfiguration? Configuration { get; }

private ILogger Logger { get; }

/// <summary>
Expand All @@ -77,6 +91,16 @@ public Task WaitForStartAsync(CancellationToken cancellationToken)
{
if (!Options.SuppressStatusMessages)
{
// Log a diagnostic when the content root is the current working directory and looks
// suspicious. Logging here (rather than in OnApplicationStarted) ensures the message
// is still emitted when the host fails to start (e.g. a hosted service can't find
// appsettings.json because the working directory is unintentionally "/" in a
// container without WORKDIR or when launched by systemd without WorkingDirectory).
if (Logger.IsEnabled(LogLevel.Information) && ShouldWarnAboutContentRoot())
{
Logger.LogInformation("Content root path is the current working directory ({ContentRoot}). To override, set the content root explicitly.", Environment.ContentRootPath);
}

Comment on lines 91 to +103
_applicationStartedRegistration = ApplicationLifetime.ApplicationStarted.Register(state =>
{
((ConsoleLifetime)state!).OnApplicationStarted();
Expand All @@ -95,6 +119,106 @@ public Task WaitForStartAsync(CancellationToken cancellationToken)
return Task.CompletedTask;
}

private bool ShouldWarnAboutContentRoot()
{
try
{
string contentRootPath = Environment.ContentRootPath;

// Hosting does not normalize ContentRootPath, so an exact match
// indicates the working directory is being used as the content root.
// A user-specified content root that happens to expand to the same directory
// but as a different string (e.g. with a trailing separator, or via "./")
// is assumed to be intentional.
if (!string.Equals(contentRootPath, Directory.GetCurrentDirectory(), StringComparison.Ordinal))
{
return false;
}

// Case 1: the content root is a filesystem root (e.g. "/" or "C:\").
// Almost certainly not where the user intended their app to be rooted - log
// regardless of how it got set.
if (string.Equals(Path.GetPathRoot(contentRootPath), contentRootPath, StringComparison.Ordinal))
{
return true;
}

// Case 2: at least one file-based configuration source is rooted at the content
// root but none of those files exist on disk. This typically means appsettings.json
// was expected (hosting defaults registered it) but the working directory doesn't
// actually contain it - a sign the working directory is not the intended app
// directory.
return AllContentRootFileSourcesAreMissing(contentRootPath);
}
Comment on lines +146 to +152

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking for appsettings.json is the goal of this check, ignoring optional sources would defeat the purpose.

catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException or ArgumentException)
{
// This diagnostic is a heuristic. I/O and security failures from
// querying the current directory or probing file-based configuration providers
// should simply skip the diagnostic.
return false;
}
Comment thread
svick marked this conversation as resolved.
}

private bool AllContentRootFileSourcesAreMissing(string contentRootPath)
{
if (Configuration is not IConfigurationRoot configRoot)
{
return false;
}

bool sawContentRootSource = false;

foreach (IConfigurationProvider provider in configRoot.Providers)
{
if (provider is not FileConfigurationProvider fileProvider)
{
continue;
}

FileConfigurationSource source = fileProvider.Source;
if (source.FileProvider is not PhysicalFileProvider physicalProvider)
{
// We can only compare paths against the content root for physical providers.
continue;
}

if (!TrimTrailingDirectorySeparator(physicalProvider.Root).Equals(contentRootPath.AsSpan(), StringComparison.Ordinal))
{
continue;
}

if (source.Path is not string sourcePath)
{
continue;
}

sawContentRootSource = true;

if (source.FileProvider.GetFileInfo(sourcePath).Exists)
{
return false;
}
}

return sawContentRootSource;
}

private static ReadOnlySpan<char> TrimTrailingDirectorySeparator(string path)
{
if (path.Length <= 1)
{
return path;
}

char last = path[path.Length - 1];
if (last == Path.DirectorySeparatorChar || last == Path.AltDirectorySeparatorChar)
{
return path.AsSpan(0, path.Length - 1);
}

Comment thread
svick marked this conversation as resolved.
return path;
}

private partial void RegisterShutdownHandlers();

private void OnApplicationStarted()
Expand Down
Loading
Loading