2

I need to be able to shutdown an console application gracefully on demand.

Using this post i was able to control exit when CTRL events are received. Yet, i can't stop the application on demand, using taskkill.

If i try the command:

taskkill /im myapp

it responds:

ERROR: The process "MyApp.exe" with PID 23296 could not be terminated. Reason: This process can only be terminated forcefully (with /F option).

MiguelSlv
  • 14,067
  • 15
  • 102
  • 169
  • 3
    `taskkill` wants to quit things gracefully by sending a `WM_QUIT` message to the main window. Unfortunately console applications have no windows or message loops (not unless they go out of their way to make them, that is) so that leaves it with no options for graceful termination -- there is no agreed-upon mechanism in Windows to signal a console application to please terminate gracefully. (I haven't tested if creating a message loop/invisible window would actually allow `taskkill` to terminate it gracefully -- you could test that, though.) – Jeroen Mostert Apr 10 '19 at 15:22
  • The console window is special-cased to have an effective owner for the `GetWindowProcessThreadId` query. It's the process that least recently attached to the console, which is usually the allocating process. If we non-forcibly kill this lead process, it sends `WM_CLOSE` (not `WM_QUIT`) to the console window, and the console in turn sends a Ctrl+Close event to *all processes* attached to it. They have 5 seconds to exit gracefully before they get forcefully terminated. – Eryk Sun Apr 10 '19 at 15:46
  • 1
    If you need this behavior for any console process, not just the lead process, then write a little program/script that calls `AttachConsole(pid)` and then broadcasts Ctrl+C or Ctrl+Break to all attached processes (process group 0) via `GenerateConsoleCtrlEvent`. First, for your own process, you'll need to call `SetConsoleCtrlHandler` to ignore Ctrl+C or to set a handler that returns `TRUE` for `CTRL_BREAK_EVENT`, depending on which event you generate. – Eryk Sun Apr 10 '19 at 15:54
  • You can even implement the forced termination behavior for all attached processes. Call `GetConsoleProcessList`. Open a handle for each process (sans yourself) with terminate access. Call `GenerateConsoleCtrlEvent`. Wait for up to 5 seconds, and then call `TerminateProcess` on any process that hasn't already exited. – Eryk Sun Apr 10 '19 at 16:02
  • @JeroenMostert, at least in Windows 10, taskkill.exe enumerates invisible message-only windows in addition to visible top-level windows to find all windows associated with a process. IIRC it didn't check message-only windows in previous versions. – Eryk Sun Apr 10 '19 at 16:07
  • Creating a program to kill is a way, yet not the way that operators would expect. I will dig more for a way that works with taskkill. Maybe changing the console application to a windows application with no window. – MiguelSlv Apr 10 '19 at 16:36
  • @ByteArtisan, if you're adding a window, I think taskkill.exe also works with invisible top-level windows, in addition to the Windows 10 support for message-only windows. Nothing stops a console app from creating a window. And, again, a console app that's run from a process without a console or via `start` has to allocate its own and is thus the effective owner of the window. In this case, taskkill.exe works without `/F` by sending `WM_CLOSE` to the window. This necessarily terminates all processes attached to the console, but they at least get a Ctrl+Close notification and 5 seconds to exit. – Eryk Sun Apr 10 '19 at 17:14
  • @eryksun, yes, i was able to receive WM_Close by adding a window. Yet, not sure if i am doing the correct way, if there is one. I run the main loop in a background task, before entering the main console work loop. Run it like this Task.Run(() => Application.Run(new MyForm(cancellationTokenSource))); Inside the form, i added a messageFilter that cancels the CancelationToken when receives the WM_Close. Running the main loop in background may have adverse effects? – MiguelSlv Apr 10 '19 at 17:36
  • You can create a top-level window using a dedicated thread in the current process. If it's hidden, taskkill.exe should still work, but the graphical Task Manager requires a visible window. A notification icon might also work. – Eryk Sun Apr 11 '19 at 04:29

1 Answers1

0

Based on the many contributions added to this post and also detecting-console-application-exit-in-c i build the following helper, to gracefully stop a console application on taskkill signal but also to stop with any other close controls signals:

public class StopController : Form, IMessageFilter
{
    //logger
    private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    static private CancellationTokenSource cancellationTokenSource;

    public StopController(CancellationTokenSource cancellationTokenSource)
    {
        StopController.cancellationTokenSource = cancellationTokenSource;
        System.Windows.Forms.Application.AddMessageFilter(this);
        SetConsoleCtrlHandler(new HandlerRoutine(ConsoleCtrlCheck), true);
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);
        this.WindowState = FormWindowState.Minimized;
        this.ShowInTaskbar = false;
    }
    public bool PreFilterMessage(ref Message m)
    {
        if (m.Msg == 16)
        {
            log.Warn("Receiveing WF_Close event. Cancel was fired.");
            cancellationTokenSource.Cancel();
        }

        return true;
    }

    public static void Activate(CancellationTokenSource cancellationTokenSource)
    {            
        Task.Run(() => Application.Run(new StopController(cancellationTokenSource)));
    }
    #region unmanaged

    //must be static.
    private static bool ConsoleCtrlCheck(CtrlTypes ctrlType)
    {
        // Put your own handler here
        switch (ctrlType)
        {
            case CtrlTypes.CTRL_C_EVENT:

                log.Warn("CTRL+C received!. Cancel was fired.");
                cancellationTokenSource.Cancel();
                break;

            case CtrlTypes.CTRL_BREAK_EVENT:

                log.Warn("CTRL+BREAK received!. Cancel was fired.");
                cancellationTokenSource.Cancel();
                break;

            case CtrlTypes.CTRL_CLOSE_EVENT:

                log.Warn("Program being closed!. Cancel was fired.");
                cancellationTokenSource.Cancel();
                break;

            case CtrlTypes.CTRL_LOGOFF_EVENT:
            case CtrlTypes.CTRL_SHUTDOWN_EVENT:
                log.Warn("User is logging off!. Cancel was fired.");
                cancellationTokenSource.Cancel();
                break;

            default:
                log.Warn($"unknow type {ctrlType}");
                break;
        }
        return true;

    }
    // Declare the SetConsoleCtrlHandler function
    // as external and receiving a delegate.

    [DllImport("Kernel32")]
    public static extern bool SetConsoleCtrlHandler(HandlerRoutine Handler, bool Add);

    // A delegate type to be used as the handler routine
    // for SetConsoleCtrlHandler.
    public delegate bool HandlerRoutine(CtrlTypes CtrlType);

    // An enumerated type for the control messages
    // sent to the handler routine.
    public enum CtrlTypes
    {
        CTRL_C_EVENT = 0,
        CTRL_BREAK_EVENT,
        CTRL_CLOSE_EVENT,
        CTRL_LOGOFF_EVENT = 5,
        CTRL_SHUTDOWN_EVENT
    }

    #endregion
}

Usage:

  StopController.Activate(cancellationTokenSource);

There is no need to change to windows application.

MiguelSlv
  • 14,067
  • 15
  • 102
  • 169