diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index cd82a15330f4..a0dd8d0a9c34 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -6,6 +6,7 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Diagnostics; +using System.Runtime.InteropServices; using Microsoft.DotNet.Cli.CommandFactory; using Microsoft.DotNet.Cli.CommandFactory.CommandResolution; using Microsoft.DotNet.Cli.Commands.Run; @@ -29,6 +30,10 @@ public class Program public static ITelemetry TelemetryClient; public static int Main(string[] args) { + // Register a handler for SIGTERM to allow graceful shutdown of the application on Unix. + // See https://github.com/dotnet/docs/issues/46226. + using var termSignalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => Environment.Exit(0)); + using AutomaticEncodingRestorer _ = new(); // Setting output encoding is not available on those platforms diff --git a/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs b/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs index f1d2a3b77bbd..7171e0a19260 100644 --- a/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs +++ b/test/TestAssets/TestProjects/WatchHotReloadApp/Program.cs @@ -8,6 +8,7 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using System.Runtime.InteropServices; // diff --git a/test/dotnet-watch.Tests/CommandLine/LaunchSettingsTests.cs b/test/dotnet-watch.Tests/CommandLine/LaunchSettingsTests.cs index 24ee2076885b..95bbd1d79657 100644 --- a/test/dotnet-watch.Tests/CommandLine/LaunchSettingsTests.cs +++ b/test/dotnet-watch.Tests/CommandLine/LaunchSettingsTests.cs @@ -92,22 +92,15 @@ public async Task RunsWithIterationEnvVariable() App.Start(testAsset, []); - await App.AssertStarted(); + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting); - var source = Path.Combine(testAsset.Path, "Program.cs"); - var contents = File.ReadAllText(source); - const string messagePrefix = "DOTNET_WATCH_ITERATION = "; + App.AssertOutputContains("DOTNET_WATCH_ITERATION = 1"); + App.Process.ClearOutput(); - var value = await App.AssertOutputLineStartsWith(messagePrefix); - Assert.Equal(1, int.Parse(value, CultureInfo.InvariantCulture)); + UpdateSourceFile(Path.Combine(testAsset.Path, "Program.cs")); await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForFileChangeBeforeRestarting); - - UpdateSourceFile(source); - await App.AssertStarted(); - - value = await App.AssertOutputLineStartsWith(messagePrefix); - Assert.Equal(2, int.Parse(value, CultureInfo.InvariantCulture)); + App.AssertOutputContains("DOTNET_WATCH_ITERATION = 2"); } [Fact] diff --git a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs index be83d241b5a5..4068b5642871 100644 --- a/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs +++ b/test/dotnet-watch.Tests/HotReload/ApplyDeltaTests.cs @@ -429,7 +429,7 @@ public async Task AutoRestartOnRudeEdit(bool nonInteractive) await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(38,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); + App.AssertOutputContains($"⌚ [auto-restart] {programPath}(39,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); App.Process.ClearOutput(); @@ -459,7 +459,7 @@ public async Task AutoRestartOnRudeEditAfterRestartPrompt() await App.AssertOutputLineStartsWith(" ❔ Do you want to restart your app? Yes (y) / No (n) / Always (a) / Never (v)", failure: _ => false); App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"❌ {programPath}(38,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); + App.AssertOutputContains($"❌ {programPath}(39,11): error ENC0023: Adding an abstract method or overriding an inherited method requires restarting the application."); App.Process.ClearOutput(); App.SendKey('a'); @@ -476,7 +476,7 @@ public async Task AutoRestartOnRudeEditAfterRestartPrompt() await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(38,1): error ENC0033: Deleting method 'F()' requires restarting the application."); + App.AssertOutputContains($"⌚ [auto-restart] {programPath}(39,1): error ENC0033: Deleting method 'F()' requires restarting the application."); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); } @@ -514,7 +514,7 @@ public async Task AutoRestartOnNoEffectEdit(bool nonInteractive) await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); App.AssertOutputContains(MessageDescriptor.RestartNeededToApplyChanges); - App.AssertOutputContains($"⌚ [auto-restart] {programPath}(16,19): warning ENC0118: Changing 'top-level code' might not have any effect until the application is restarted."); + App.AssertOutputContains($"⌚ [auto-restart] {programPath}(17,19): warning ENC0118: Changing 'top-level code' might not have any effect until the application is restarted."); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Exited"); App.AssertOutputContains($"[WatchHotReloadApp ({ToolsetInfo.CurrentTargetFramework})] Launched"); App.AssertOutputContains(""); @@ -721,6 +721,8 @@ class AppUpdateHandler [PlatformSpecificFact(TestPlatforms.Windows)] public async Task GracefulTermination_Windows() { + var tfm = ToolsetInfo.CurrentTargetFramework; + var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp") .WithSource(); @@ -739,7 +741,7 @@ public async Task GracefulTermination_Windows() await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); - await App.WaitUntilOutputContains(new Regex(@"dotnet watch 🕵️ \[.*\] Windows Ctrl\+C handling enabled.")); + await App.WaitUntilOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Windows Ctrl+C handling enabled."); await App.WaitUntilOutputContains("Started"); @@ -749,6 +751,37 @@ public async Task GracefulTermination_Windows() await App.WaitUntilOutputContains("exited with exit code 0."); } + [PlatformSpecificFact(TestPlatforms.AnyUnix)] + public async Task GracefulTermination_Unix() + { + var tfm = ToolsetInfo.CurrentTargetFramework; + + var testAsset = TestAssets.CopyTestAsset("WatchHotReloadApp") + .WithSource(); + + var programPath = Path.Combine(testAsset.Path, "Program.cs"); + + UpdateSourceFile(programPath, src => src.Replace("// ", """ + using var termSignalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => + { + Console.WriteLine("SIGTERM detected! Performing cleanup..."); + }); + """)); + + App.Start(testAsset, [], testFlags: TestFlags.ReadKeyFromStdin); + + await App.WaitForOutputLineContaining(MessageDescriptor.WaitingForChanges); + + await App.WaitUntilOutputContains($"dotnet watch 🕵️ [WatchHotReloadApp ({tfm})] Posix signal handlers registered."); + + await App.WaitUntilOutputContains("Started"); + + App.SendControlC(); + + await App.WaitForOutputLineContaining("SIGTERM detected! Performing cleanup..."); + await App.WaitUntilOutputContains("exited with exit code 0."); + } + [PlatformSpecificTheory(TestPlatforms.Windows, Skip = "https://github.com/dotnet/sdk/issues/49928")] // https://github.com/dotnet/aspnetcore/issues/63759 [CombinatorialData] public async Task BlazorWasm(bool projectSpecifiesCapabilities)