Capturar salida de consola C #

92

Tengo una aplicación de consola que contiene bastantes hilos. Hay hilos que monitorean ciertas condiciones y terminan el programa si son verdaderas. Esta terminación puede ocurrir en cualquier momento.

Necesito un evento que pueda activarse cuando el programa se está cerrando para poder limpiar todos los demás subprocesos y cerrar todos los identificadores de archivos y conexiones correctamente. No estoy seguro de si ya hay uno integrado en el marco .NET, así que pregunto antes de escribir el mío.

Me preguntaba si hubo un evento como:

MyConsoleProgram.OnExit += CleanupBeforeExit;
ZeroKelvin
fuente
2
Sé que este es un comentario muy tardío, pero realmente no es necesario que lo haga si lo único que desea hacer como limpieza es "cerrar archivos y conexiones". Porque Windows ya cierra todos los identificadores asociados con un proceso durante la terminación.
Sedat Kapanoglu
6
^ Solo si esos recursos son propiedad del proceso que se termina. Esto es absolutamente necesario si, por ejemplo, está automatizando una aplicación COM oculta (por ejemplo, Word o Excel) en segundo plano, y debe asegurarse de eliminarla antes de que salga la aplicación, etc.
BrainSlugs83
1
esto tiene una respuesta breve stackoverflow.com/questions/2555292/…
barlop

Respuestas:

96

No estoy seguro de dónde encontré el código en la web, pero lo encontré ahora en uno de mis proyectos anteriores. Esto le permitirá hacer un código de limpieza en su consola, por ejemplo, cuando se cierra abruptamente o debido a un apagado ...

[DllImport("Kernel32")]
private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);

private delegate bool EventHandler(CtrlType sig);
static EventHandler _handler;

enum CtrlType
{
  CTRL_C_EVENT = 0,
  CTRL_BREAK_EVENT = 1,
  CTRL_CLOSE_EVENT = 2,
  CTRL_LOGOFF_EVENT = 5,
  CTRL_SHUTDOWN_EVENT = 6
}

private static bool Handler(CtrlType sig)
{
  switch (sig)
  {
      case CtrlType.CTRL_C_EVENT:
      case CtrlType.CTRL_LOGOFF_EVENT:
      case CtrlType.CTRL_SHUTDOWN_EVENT:
      case CtrlType.CTRL_CLOSE_EVENT:
      default:
          return false;
  }
}


static void Main(string[] args)
{
  // Some biolerplate to react to close window event
  _handler += new EventHandler(Handler);
  SetConsoleCtrlHandler(_handler, true);
  ...
}

Actualizar

Para aquellos que no revisan los comentarios, parece que esta solución en particular no funciona bien (o en absoluto) en Windows 7 . El siguiente hilo habla de esto

flq
fuente
4
¿Puedes usar esto para cancelar la salida? ¡Aparte de cuando se está apagando!
ingh.am
7
Esto funciona muy bien, solo bool Handler()debe return false;(no devuelve nada en el código) para que funcione. Si devuelve verdadero, Windows muestra el cuadro de diálogo "Terminar proceso ahora". = D
Cipi
3
Parece que esta solución no funciona con Windows 7 para eventos de apagado, consulte social.msdn.microsoft.com/Forums/en/windowscompatibility/thread/…
CharlesB
3
Tenga en cuenta que si coloca un punto de interrupción en el método 'Handler', arrojará una NullReferenceException. Registrado en VS2010, Windows 7.
Maxim
10
Esto funcionó muy bien para mí en Windows 7 (64 bits). No estoy seguro de por qué todos dicen que no. Las únicas modificaciones importantes que hice fueron deshacerme de la declaración de enum y switch, y "devolver falso" del método; hago toda mi limpieza en el cuerpo del método.
BrainSlugs83
25

Ejemplo de trabajo completo, funciona con ctrl-c, cerrando las ventanas con X y kill:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace TestTrapCtrlC {
    public class Program {
        static bool exitSystem = false;

        #region Trap application termination
        [DllImport("Kernel32")]
        private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);

        private delegate bool EventHandler(CtrlType sig);
        static EventHandler _handler;

        enum CtrlType {
            CTRL_C_EVENT = 0,
            CTRL_BREAK_EVENT = 1,
            CTRL_CLOSE_EVENT = 2,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT = 6
        }

        private static bool Handler(CtrlType sig) {
            Console.WriteLine("Exiting system due to external CTRL-C, or process kill, or shutdown");

            //do your cleanup here
            Thread.Sleep(5000); //simulate some cleanup delay

            Console.WriteLine("Cleanup complete");

            //allow main to run off
            exitSystem = true;

            //shutdown right away so there are no lingering threads
            Environment.Exit(-1);

            return true;
        }
        #endregion

        static void Main(string[] args) {
            // Some boilerplate to react to close window event, CTRL-C, kill, etc
            _handler += new EventHandler(Handler);
            SetConsoleCtrlHandler(_handler, true);

            //start your multi threaded program here
            Program p = new Program();
            p.Start();

            //hold the console so it doesn’t run off the end
            while (!exitSystem) {
                Thread.Sleep(500);
            }
        }

        public void Start() {
            // start a thread and start doing some processing
            Console.WriteLine("Thread started, processing..");
        }
    }
}
JJ_Coder4Hire
fuente
2
Probé esto en Windows 7 con todo lo comentado Handlerexcepto por el return truey un ciclo while para contar los segundos. La aplicación continúa ejecutándose en ctrl-c pero se cierra después de 5 segundos cuando se cierra con la X.
Antonios Hadjigeorgalis
Lo siento, pero al usar este código puedo obtener "Limpieza completa" solo si presiono Ctrl + C, no si cierro con el botón 'X'; en el último caso, solo obtengo "Saliendo del sistema debido a CTRL-C externo, o proceso de muerte o apagado" pero luego parece que la consola se cierra antes de ejecutar la parte restante del Handlermétodo {usando Win10, .NET Framework 4.6.1}
Giacomo Pirinoli
8

Compruebe también:

AppDomain.CurrentDomain.ProcessExit
jmservera
fuente
7
Esto solo parece detectar salidas de return o Environment.Salir, no captura CTRL + C, CTRL + Break, ni el botón de cierre real en la consola.
Kit10
Si maneja CTRL + C por separado usando Console.CancelKeyPress, el ProcessExitevento realmente se generó después de la CancelKeyPressejecución de todos los controladores de eventos.
Konard
5

Tuve un problema similar, solo la aplicación de mi consola se ejecutaría en un bucle infinito con una declaración preventiva en el medio. Aquí está mi solución:

class Program
{
    static int Main(string[] args)
    {
        // Init Code...
        Console.CancelKeyPress += Console_CancelKeyPress;  // Register the function to cancel event

        // I do my stuffs

        while ( true )
        {
            // Code ....
            SomePreemptiveCall();  // The loop stucks here wating function to return
            // Code ...
        }
        return 0;  // Never comes here, but...
    }

    static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
    {
        Console.WriteLine("Exiting");
        // Termitate what I have to terminate
        Environment.Exit(-1);
    }
}
João Portela
fuente
4

¿Parece que los hilos terminan directamente la aplicación? Quizás sería mejor tener una señal de subproceso en el subproceso principal para decir que la aplicación debe terminarse.

Al recibir esta señal, el hilo principal puede cerrar limpiamente los otros hilos y finalmente cerrarse.

Robar
fuente
3
Tengo que estar de acuerdo con esta respuesta. Forzar la salida de la aplicación y luego intentar limpiar después no es el camino a seguir. Controla tu aplicación, Noit. No dejes que te controle.
Randolpho
1
Un hilo generado por mí directamente no es necesariamente lo único que puede cerrar mi aplicación. Ctrl-C y el "botón de cierre" son otras formas en las que puede terminar. El código publicado por Frank, después de pequeñas modificaciones, encaja perfectamente.
ZeroKelvin
4

La respuesta de ZeroKelvin funciona en la aplicación de consola Windows 10 x64, .NET 4.6. Para aquellos que no necesitan lidiar con la enumeración CtrlType, aquí hay una forma realmente simple de conectarse al cierre del marco:

class Program
{
    private delegate bool ConsoleCtrlHandlerDelegate(int sig);

    [DllImport("Kernel32")]
    private static extern bool SetConsoleCtrlHandler(ConsoleCtrlHandlerDelegate handler, bool add);

    static ConsoleCtrlHandlerDelegate _consoleCtrlHandler;

    static void Main(string[] args)
    {
        _consoleCtrlHandler += s =>
        {
            //DoCustomShutdownStuff();
            return false;   
        };
        SetConsoleCtrlHandler(_consoleCtrlHandler, true);
    }
}

Devolver FALSE desde el manejador le dice al marco que no estamos "manejando" la señal de control, y se usa la siguiente función de manejador en la lista de manejadores para este proceso. Si ninguno de los controladores devuelve VERDADERO, se llama al controlador predeterminado.

Tenga en cuenta que cuando el usuario realiza un cierre de sesión o apagado, Windows no llama a la devolución de llamada, sino que finaliza inmediatamente.

BCA
fuente
3

Hay para aplicaciones WinForms;

Application.ApplicationExit += CleanupBeforeExit;

Para aplicaciones de consola, intente

AppDomain.CurrentDomain.DomainUnload += CleanupBeforeExit;

Pero no estoy seguro de en qué momento se llama o si funcionará desde el dominio actual. Sospecho que no.

Rob Prouse
fuente
Los documentos de ayuda para DomainUnload dicen "El delegado de EventHandler para este evento puede realizar cualquier actividad de terminación antes de que se descargue el dominio de la aplicación". Entonces parece que funciona dentro del dominio actual. Sin embargo, es posible que no funcione para su necesidad porque sus hilos pueden mantener el dominio activo.
Rob Parker
2
Esto solo maneja CTRL + C y CTRL + Cerrar, no detecta existe al regresar, Environment.Salir ni al hacer clic en el botón cerrar.
Kit10
No capta CTRL + C para mí con Mono en Linux.
starbeamrainbowlabs
2

Visual Studio 2015 + Windows 10

  • Permita la limpieza
  • Aplicación de instancia única
  • Algunos chapados en oro

Código:

using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;

namespace YourNamespace
{
    class Program
    {
        // if you want to allow only one instance otherwise remove the next line
        static Mutex mutex = new Mutex(false, "YOURGUID-YOURGUID-YOURGUID-YO");

        static ManualResetEvent run = new ManualResetEvent(true);

        [DllImport("Kernel32")]
        private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);                
        private delegate bool EventHandler(CtrlType sig);
        static EventHandler exitHandler;
        enum CtrlType
        {
            CTRL_C_EVENT = 0,
            CTRL_BREAK_EVENT = 1,
            CTRL_CLOSE_EVENT = 2,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT = 6
        }
        private static bool ExitHandler(CtrlType sig)
        {
            Console.WriteLine("Shutting down: " + sig.ToString());            
            run.Reset();
            Thread.Sleep(2000);
            return false; // If the function handles the control signal, it should return TRUE. If it returns FALSE, the next handler function in the list of handlers for this process is used (from MSDN).
        }


        static void Main(string[] args)
        {
            // if you want to allow only one instance otherwise remove the next 4 lines
            if (!mutex.WaitOne(TimeSpan.FromSeconds(2), false))
            {
                return; // singleton application already started
            }

            exitHandler += new EventHandler(ExitHandler);
            SetConsoleCtrlHandler(exitHandler, true);

            try
            {
                Console.BackgroundColor = ConsoleColor.Gray;
                Console.ForegroundColor = ConsoleColor.Black;
                Console.Clear();
                Console.SetBufferSize(Console.BufferWidth, 1024);

                Console.Title = "Your Console Title - XYZ";

                // start your threads here
                Thread thread1 = new Thread(new ThreadStart(ThreadFunc1));
                thread1.Start();

                Thread thread2 = new Thread(new ThreadStart(ThreadFunc2));
                thread2.IsBackground = true; // a background thread
                thread2.Start();

                while (run.WaitOne(0))
                {
                    Thread.Sleep(100);
                }

                // do thread syncs here signal them the end so they can clean up or use the manual reset event in them or abort them
                thread1.Abort();
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.Write("fail: ");
                Console.ForegroundColor = ConsoleColor.Black;
                Console.WriteLine(ex.Message);
                if (ex.InnerException != null)
                {
                    Console.WriteLine("Inner: " + ex.InnerException.Message);
                }
            }
            finally
            {                
                // do app cleanup here

                // if you want to allow only one instance otherwise remove the next line
                mutex.ReleaseMutex();

                // remove this after testing
                Console.Beep(5000, 100);
            }
        }

        public static void ThreadFunc1()
        {
            Console.Write("> ");
            while ((line = Console.ReadLine()) != null)
            {
                if (line == "command 1")
                {

                }
                else if (line == "command 1")
                {

                }
                else if (line == "?")
                {

                }

                Console.Write("> ");
            }
        }


        public static void ThreadFunc2()
        {
            while (run.WaitOne(0))
            {
                Thread.Sleep(100);
            }

           // do thread cleanup here
            Console.Beep();         
        }

    }
}
AJBauer
fuente
Es interesante que esta parece ser la respuesta más sólida. Sin embargo, tenga cuidado al cambiar el tamaño del búfer de su consola: si la altura del búfer es menor que la altura de la ventana, el programa lanzará una excepción al inicio.
John Zabroski
1

El enlace mencionado anteriormente por Charle B en un comentario a flq

En el fondo dice:

SetConsoleCtrlHandler no funcionará en Windows7 si se vincula a user32

En algún otro lugar del hilo, se sugiere crear una ventana oculta. Así que creo un winform y en la carga lo conecté a la consola y ejecuté Main original. Y luego SetConsoleCtrlHandle funciona bien (se llama a SetConsoleCtrlHandle como lo sugiere flq)

public partial class App3DummyForm : Form
{
    private readonly string[] _args;

    public App3DummyForm(string[] args)
    {
        _args = args;
        InitializeComponent();
    }

    private void App3DummyForm_Load(object sender, EventArgs e)
    {
        AllocConsole();
        App3.Program.OriginalMain(_args);
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool AllocConsole();
}
Jens
fuente
En realidad, esto no funciona. Tengo una aplicación WFP de múltiples ventanas y uso la consola ( AllocConsolecomo en su ejemplo) para mostrar información adicional. El problema es que toda la aplicación (todas las ventanas) se cierra si el usuario hace clic en la (X) en la ventana de la consola. Las SetConsoleCtrlHandlerobras, pero las paradas de aplicaciones de todos modos antes de cualquier código en el controlador ejecuta (I ver los puntos de interrupción despedidos y en ese momento se detiene la aplicación).
Mike Keskinov
Pero encontré una solución que funciona para mí: simple botón de cierre DESHABILITADO . Ver: stackoverflow.com/questions/6052992/…
Mike Keskinov
0

Para aquellos interesados ​​en VB.net. (Busqué en Internet y no pude encontrar un equivalente) Aquí está traducido a vb.net.

    <DllImport("kernel32")> _
    Private Function SetConsoleCtrlHandler(ByVal HandlerRoutine As HandlerDelegate, ByVal Add As Boolean) As Boolean
    End Function
    Private _handler As HandlerDelegate
    Private Delegate Function HandlerDelegate(ByVal dwControlType As ControlEventType) As Boolean
    Private Function ControlHandler(ByVal controlEvent As ControlEventType) As Boolean
        Select Case controlEvent
            Case ControlEventType.CtrlCEvent, ControlEventType.CtrlCloseEvent
                Console.WriteLine("Closing...")
                Return True
            Case ControlEventType.CtrlLogoffEvent, ControlEventType.CtrlBreakEvent, ControlEventType.CtrlShutdownEvent
                Console.WriteLine("Shutdown Detected")
                Return False
        End Select
    End Function
    Sub Main()
        Try
            _handler = New HandlerDelegate(AddressOf ControlHandler)
            SetConsoleCtrlHandler(_handler, True)
     .....
End Sub
dko
fuente
La solución anterior no me funciona vb.net 4.5 framework ControlEventType no se está resolviendo. Pude usar esta idea como solución stackoverflow.com/questions/15317082/…
glant