Skip to content
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

Detecting the user or the system closing .NET Core console application #7120

Closed
nicolasr75 opened this issue Dec 9, 2016 · 65 comments
Closed
Labels
area-System.Runtime question Answer questions and provide assistance, not an issue with source code or documentation.
Milestone

Comments

@nicolasr75
Copy link

Is there any way to detect whether a user or the system closes a console application? I support Ctrl-C for closing by a user but since the system provides the ability to simply close the console window I can not clean up my application which is important since it performs regular tasks that should be terminated cleanly. The same goes for system shutdown.

I tried

System.Runtime.Loader.AssemblyLoadContext.Default.Unloading

but that does not fire when the window is closed.

@alikalfaoglu
Copy link

Try using IApplicationLifetime..
Process shuts down before stopping event executes sometimes but I guess it is fixed in 1.0.3

http://shazwazza.com/post/aspnet-core-application-shutdown-events/

@nicolasr75
Copy link
Author

This seems to be an ASP.NET Core feature. Are you sure this can be used in a .NET Core console application?

@alikalfaoglu
Copy link

Sorry, my mistake, missed the ".NET Core Console only" part of your question. Isn't closing console window directly same as killing the process since console windows do not have message loops? Maybe, you may create an Kestrel server instance without injecting MVC and manage your requirements (the way I suggested above) through startup class..

@gkhanna79
Copy link
Member

CC @AlexGhiondea

@AlexGhiondea
Copy link
Contributor

@nicolasr75
Copy link
Author

@AlexGhiondea yes I considered that but according to
https://blogs.msdn.microsoft.com/dotnet/2016/02/10/porting-to-net-core/
AppDomains aren't part of .NET Core. The blog post recommends using AssemblyLoadContext but that doesn't seem to work.

@AlexGhiondea
Copy link
Contributor

@nicolasr75 we have brought back quite a few APIs. You can check http://apisof.net for a recent list of API availability.

With the recent set of APIs we have brought back,ProcessExit exists on AppDomain.

Can you try that?

@nicolasr75
Copy link
Author

Is this available in .NET Core 1.1?
https://apisof.net/catalog/System.AppDomain
says System.Runtime.Extensions, Version=4.2.0.0 is needed.
Nuget gets me 4.3.0.0 but I can't find it in namespace System.

@AlexGhiondea
Copy link
Contributor

@nicolasr75 it is not in .NET Core 1.1... It is in .NET Core 1.2 though.

@nicolasr75
Copy link
Author

Ok, I will set up a project with the .NET Core dev feed on a test PC after Christmas.

@nicolasr75
Copy link
Author

@AlexGhiondea
Unfortunately I don't know how to correctly set it up.
I downloaded and installed .NET Core Runtime from here:

https://github.com/dotnet/core-setup/blob/master/README.md

Trying to run dotnet new I was asked to install .NET SDK and was pointed to
https://www.microsoft.com/net/core#windowscmd

Programs and features now shows two things installed
.NET Core 1.1.0 - SDK 1.0.0 Preview 2.1-003177
.NET Core 1.2.0 - Runtime

This is my project.json for a simple command line application:

{
  "version": "1.0.0-*",
  "buildOptions": {
	"debugType": "portable",
	"emitEntryPoint": true
  },
  "dependencies": {},
  "frameworks": {
	"netcoreapp1.2": {
	  "dependencies": {
		"Microsoft.NETCore.App": {
		  "type": "platform",
		  "version": "1.1.0"
		},
		"System.Runtime.Extensions": "4.3.0"
	  },
	  "imports": "dnxcore50"
	}
  }
}

My code:

using System;
using System.Threading;

namespace ConsoleApplication
{
	public class Program
	{
		public static void Main(string[] args)
		{
			
			System.AppDomain.ProcessExit += (s, e) => Console.WriteLine("Process exiting");
			
			Console.WriteLine("Hello World!");
			
			
			while(true)
			{
				string s = Console.ReadLine();
				
				if(s == "q")
					break;
				
				System.Threading.Thread.Sleep(100);
			}
			
		}
	}
}

Running dotnet restore works fine, dotnet build gives me

The type or namespace name 'AppDomain' does not exist in the namespace 'System' (are you missing an assembly reference?)

Any idea what I'm doing wrong?

@AlexGhiondea
Copy link
Contributor

@nicolasr75 I think you are using .NET 1.1. If you want to use the newer APIs you need to update the version of `System.Runtime.Extensions' to a newer one (i.e. 4.4.0-beta-24903-02) that we publish on the dotnet.myget.org feed.

@jnm2
Copy link
Contributor

jnm2 commented Jan 3, 2017

Talk about a throwback. Cut my teeth on .NET 1.1 way back in 2003.

@AlexGhiondea
Copy link
Contributor

I meant .NET Core 1.1 :).

@nicolasr75
Copy link
Author

@AlexGhiondea Sorry to bother you again but I still cannot get this to run. My current project.json is like this:

{
  "name:": "ConsoleTest",
  "version": "1.0.0-*",
  "buildOptions": {
	"debugType": "portable",
	"emitEntryPoint": true
  },
  "dependencies": {},
  "frameworks": {
	"netcoreapp1.2": {
	  "dependencies": {
		"Microsoft.NETCore.App": {
		  "type": "platform",
		  "version": "1.1.0"
		}
		,
		"System.Runtime.Extensions": "4.4.0-*"
	  }
	}
  }
}

.NET Core installations are still as shown in my post above. In my code I changed a single line:

System.AppDomain.CurrentDomain.ProcessExit += (s, e) => Console.WriteLine("Process exiting");

I was missing the fact that we need to use the static CurrentDomain property. This restores and builds successfully.

To have the dev feed I use a local nuget.config like this:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
	<clear/>
	<add key="myget.org dotnet-buildtools" value="https://dotnet.myget.org/F/dotnet-buildtools/api/v3/index.json" />
	<add key="myget.org dotnet-core" value="https://dotnet.myget.org/F/dotnet-core/api/v3/index.json" />
	<add key="nuget.org" value="https://www.nuget.org/api/v2/" />
  </packageSources>
</configuration>

Furthermore notice that I use netcoreapp1.2 in project.json but Microsoft.NETCore.App 1.1.0. I'm not sure whether this is correct.

Executing dotnet run fails with

Unhandled Exception: System.IO.FileLoadException: Could not load file or assembly '
System.Runtime, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'. 
The located assembly's manifest definition does not match the assembly reference. 
(Exception from HRESULT: 0x80131040)

I have no idea where this dependency comes from. Any further idea?

@AlexGhiondea
Copy link
Contributor

@joperezr do you have any tips on how to figure out where that reference is coming from?

@weshaggard
Copy link
Member

@nicolasr75 unfortunately you will not be able to use the new APIs while referencing Microsoft.NETCore.App 1.1.0. You will need to reference a Microsoft.NETCore.App 1.2.0-beta in order to use the newer APIs. Also to reference that newer versions you will need to have a newer set of CLI tools at least preview 4 version of the tools.

@terrajobst is working on writing up how to consume our newest prerelease packages.

@nicolasr75
Copy link
Author

@weshaggard thanks for these informations. I updated everything as you say and also switched from project.json to msbuild. Finally it runs. Here is the final code:

using System;
using System.Threading;

namespace ConsoleApplication
{
	public class Program
	{
		public static void Log(string s)
		{
			using(var sw = System.IO.File.AppendText("log.txt"))
			{
				sw.WriteLine(s);
			}
		}
		
		public static void Main(string[] args)
		{				
			System.AppDomain.CurrentDomain.ProcessExit += (s, e) => 
			{
				Log("Process exiting");				
			};				
			
			Log("Hello World!");				
			
			while(true)
			{
				string s = Console.ReadLine();					
				if(s == "q")
					break;					
				System.Threading.Thread.Sleep(100);
			}			
		}
	}
}

The result is:
ProcessExit is not called when I close the console window :-(

I read that for Windows applications there exists a SetConsoleCtrlHandler API in kernel32. I have not yet tried that myself but from what others report this should do what I need. I had hoped that I don't need to pinvoke and that there were a cross-platform way.

@weshaggard
Copy link
Member

Good to hear that the code is running now. As for the behavior of ProcessExit I'm not sure. @rahku do you know what the expected behavior is for this case?

@rahku
Copy link
Contributor

rahku commented Jan 10, 2017

I don't it will be invoked in case of rude termination of process.

@nicolasr75
Copy link
Author

It looks like Linux does by default warn the user if there is still an active process running in a terminal. This is a great feature! Unfortunately Windows does not seem to support this :-( Maybe I could handle this by pinvoking SetConsoleCtrlHandler but in my case I will switch to a service application on Windows anyway which should give me better control. So from my side this issue could be closed. Thanks to all!

@Workshop2
Copy link

@danmosemsft ProcessExit still isn't invoked when close a console window. How else can we detect the closing of a dotnet core console app?

@danmoseley
Copy link
Member

@Workshop2 I did this

using System;
using System.Diagnostics;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            AppDomain.CurrentDomain.ProcessExit += (s, ev) =>
            {
                Console.WriteLine("process exit");
                Console.ReadLine();
            };

            Console.CancelKeyPress += (s, ev) =>
            {
                Console.WriteLine("Ctrl+C pressed");
                Console.ReadLine();
                ev.Cancel = true;
            };

            Console.WriteLine("hit a key");
            Console.ReadLine();
        }
    }
}

And I get the same behavior on .NET Core 2.0 as on Desktop. This whether I hit Ctrl-C or I hit enter at the prompt.

Can you give me a repro that behaves differently on Core?

@danmoseley danmoseley reopened this Aug 18, 2017
@danmoseley
Copy link
Member

That behavior being, that output occurs thus:

hit a key
process exit
Ctrl+C pressed

@Workshop2
Copy link

Workshop2 commented Aug 19, 2017

Hello @danmosemsft, I am able to hook into CTRL+C ok, but closing the Window (using the close button) doesn't cause the ProcessExit to hit.

https://github.com/noobot/noobot/blob/dotnet-standard-port/src/Noobot.Console/Program.cs#L21

@nicolasr75
Copy link
Author

@danmosemsft @Workshop2 I can confirm this. I just tried my test code from above (Jan 9th) with .NET Core 2.0 and it still behaves the same, no ProcessExit when closing the window.

@adamtuliper
Copy link

On version 2.0.2, I don't get the ctrl-c pressed message, does this fail for anyone else?

@angelsix
Copy link

angelsix commented Nov 26, 2017

I have same issue. .Net Core 2.0 and 2.0.3 app doing AppDomain.CurrentDomain.ProcessExit += ... will never fire on closing window by the X

Console.CancelKeyPress works fine on both

@danmoseley
Copy link
Member

@jeffschw do you guys own this part?

@msftgits msftgits transferred this issue from dotnet/coreclr Jan 31, 2020
@msftgits msftgits added this to the Future milestone Jan 31, 2020
@ricardoboss
Copy link

ricardoboss commented Feb 23, 2020

I just want to execute some cleanup before my program exits. I hope this issue gets resolved before .NET 5.

@maryamariyan maryamariyan added the untriaged New issue has not been triaged by the area owner label Feb 26, 2020
@joperezr joperezr modified the milestones: Future, 5.0.0 Jul 1, 2020
@joperezr joperezr removed the untriaged New issue has not been triaged by the area owner label Jul 1, 2020
@joperezr
Copy link
Member

joperezr commented Jul 1, 2020

Moving to 5.0 milestone just to do a quick check to see if there is anything that we want/need to do in here for 5.0 as from the discussion above it is not clear yet what works and what doesn't.

@ericstj
Copy link
Member

ericstj commented Aug 5, 2020

Seems related to #36089

@ststeiger
Copy link

ststeiger commented Aug 7, 2020

@ricardoboss: Yea, me too.

@joperezr;
In a nutshell: I have a command line SQL-Server profiler in .NET Core based on ExpressProfiler.
And it needs to stop the profiling (=call a stored procedure) when the command-line-program is closed (via quit or CTRL+C or sigkill or system shutdown/ssh logout or exception or threadexception).
Otherwise, the profiling keeps continuing ad infinitum, and slows down production machines...
This was not possible with 2.1 & 2.,2, haven't tried in 3.1.

@ygoe
Copy link

ygoe commented Aug 7, 2020

I'm still using my above code (4 Nov 2018) with .NET Core 3.1. And after digging through lots of WebHost and other host code from the framework, I know that it's still used there as well. So there might not be a ready-to-use API for this, but the code shown above solves the problem completely.

On Windows you have about 3 seconds before the OS kills the process. No way around that. On Linux with Systemd, you can configure this and it defaults to 90 seconds. When using the application shutdown behaviour that's built into the hosts (like WebHost), you can use additional configuration to prolong the internal default of 5 seconds before hosted services get aborted. This is another thing though and unrelated to what the OS does.

@danmoseley danmoseley modified the milestones: 5.0.0, Future Aug 12, 2020
@sbomer
Copy link
Member

sbomer commented Aug 19, 2020

From my testing, it looks like ProcessExit is correctly invoked when a terminal window is closed on Windows (which sends the CTRL_CLOSE_EVENT) on 5.0. A few things to watch out for (repeating some things @ygoe has already pointed out):

  • The console window will be closed while ProcessExit runs - so don't expect to see output from Console.WriteLine after closing the window.
  • There's a timeout, so any cleanup code should finish quickly.
  • If you kill the application via Ctrl+C, the ProcessExit handler will not run. You can get around this like ASP.NET does, by installing a CancelKeyPress handler that sets Cancel = true (preventing the OS from tearing down the process immediately after the handler runs) and initiates an asynchronous shutdown path.
    One way to do this is to use the ASP.NET generic host, like in AppDomain.ProcessExit is not invoked on docker stop #36089 (comment). Just replace the WriteLine with the shutdown logic that should run on Ctrl+C or window close.

Shutdown is not handled correctly - that's tracked by #36089.

On Unix, it looks like closing a terminal window sends SIGHUP which we do not handle at all.

A few questions:

  • Do we want the ProcessExit handler to be called if a process is shut down by an unhandled Ctrl+C? (This would be a breaking change for folks who intentionally have independent shutdown paths for the two events, so I think we want to keep the current behavior).
  • Should we call the ProcessExit handler on Unix when receiving a SIGHUP?

@danmoseley
Copy link
Member

One datapoint might be what Mono does.

@ygoe
Copy link

ygoe commented Aug 19, 2020

Wasn't SIGHUP the signal that Systemd sends when a service should be reloaded? I have no experience with Linux desktop though, so never really use a terminal window, only things like PuTTY.

@ststeiger
Copy link

ststeiger commented Aug 20, 2020

@ygoe:
That's only a convention, and it is by no means a general rule. The primary and documented purpose of SIGHUP is still to signal the fact that the terminal connection has been severed. For that reason, the default action for any program upon receipt of the SIGHUP signal is still to terminate, even if the process is a daemon.

Since SIGHUP is meaningless for a daemon anyway, why not just reuse that?
Right, so that's what happened, and as a result, the convention today is indeed for daemons to re-read their configuration file when they receive SIGHUP.

So, while many services will trigger a reload when receiving a SIGHUP, some might as well ignore the signal or terminate.

(Source: https://unix.stackexchange.com/questions/239599/does-a-service-restart-send-a-hup)

Actually, systemd will look into the ExecReload= option in the [Service] section in the .service unit file.
(e.g. located at /usr/lib/systemd/system/nginx.service on my system):

From nginx(8):

-s signal      Send a signal to the master process. The argument signal
               can be one of: stop, quit, reopen, reload. The following
               table shows the corresponding system signals:

               stop    SIGTERM
               quit    SIGQUIT
               reopen  SIGUSR1
               reload  SIGHUP

(Source: https://superuser.com/questions/710986/how-to-reload-nginx-systemctl-or-nginx-s/953901#953901)

SIGTERM - politely ask a process to terminate. It shall terminate gracefully, cleaning up all resources (files, sockets, child processes, etc.), deleting temporary files and so on.

SIGQUIT - a more forceful request

  • Ctrl+C - SIGINT
  • Ctrl+\ - SIGQUIT
  • Ctrl+Z - SIGTSTP

Also about

I have no experience with Linux desktop though,

Use KDE Neon to get a nice Ubuntu 20.04 based Linux Desktop with KDE Plasma . ;)

@sbomer
Copy link
Member

sbomer commented Aug 20, 2020

It looks to me like mono doesn't handle SIGHUP either (or SIGTERM for that matter, which is what 'docker stop' sends to linux containers).

@VILLAN3LL3
Copy link

VILLAN3LL3 commented Feb 25, 2021

await shutdownCts.Token.WaitAsync();

I tried this in .NET 5.0 but there's no WaitAsync() method on CancellationToken. Any advice how to get this running in .NET 5.0?

@jnm2
Copy link
Contributor

jnm2 commented Feb 25, 2021

@VILLAN3LL3 I've used C#'s async Main feature and await Task.Delay(Timeout.Infinite, shutdownCancellationToken); when there was not already an async call in progress keeping the main method from returning that I could pass the cancellation token to.

@ygoe
Copy link

ygoe commented Feb 25, 2021

@VILLAN3LL3 What are you trying to wait asynchronously for? There's nothing in the quoted code. Remember, you must not leave the ProcessExit event handler, not even by await, or the process will end immediately. The point is to block that method until the event has been set.

@VILLAN3LL3
Copy link

@jnm2 Thank you very much for your advice. I had to additionally catch TaskCanceledExceptions to make this work because shutting down the app throws this exception if I use your code snippet. But with the Catch it works - almost. Stopping the app by clicking on the Stop Debugging Button in Visual Studio does obviously not trigger one of the events.

@ygoe Sorry I quoted the wrong part of your code. It's corrected now.

@ygoe
Copy link

ygoe commented Feb 25, 2021

@VILLAN3LL3 That WaitAsync may come from the AsyncEx NuGet package. But the solution provided by @jnm2 looks good, too. Sometimes you have to be creative when the BCL isn't comprehensive.

Yes, that use of a CancellationToken throws an exception when cancelled. Also note that TaskCanceledException is derived from OperationCanceledException and I never know which of the two is actually thrown, so I always catch the OCE as the more generic one to be safe. If somebody has found a pattern behind these two, please let me know.

@jnm2
Copy link
Contributor

jnm2 commented Feb 25, 2021

If somebody has found a pattern behind these two, please let me know.

@ygoe Roughly, CancellationToken.ThrowIfCancellationRequested() creates the base exception type and Task/Task<T> creates TaskCanceledException if it's in the TaskStatus.Canceled state.

@ygoe
Copy link

ygoe commented Feb 25, 2021

Yes, but the caller of a method often doesn't know what implementation is in there, or doesn't want to rely on that detail.

@jnm2
Copy link
Contributor

jnm2 commented Feb 25, 2021

All a caller should care about is the base exception type, as far as I know.

@eerhardt
Copy link
Member

I believe the original issue has been addressed. Using .NET 6, you can subscribe to https://docs.microsoft.com/dotnet/api/system.appdomain.processexit, and it will be invoked even when the terminal window is closed.

For handling CTRL+C and signals, a new option added in .NET 6 is https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.posixsignalregistration which was added to correctly handle POSIX signals. Just an FYI, you can also use this API on Windows, and it will fire SIGINT for CTRL+C. See #50527 for scenarios on when this is useful.

I'm going to close this issue has I believe the original problem has been addressed. If you have a different problem, please log a new issue.

@ghost ghost locked as resolved and limited conversation to collaborators Feb 14, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Runtime question Answer questions and provide assistance, not an issue with source code or documentation.
Projects
None yet
Development

No branches or pull requests