¿Puedo enviar Ctrl-C (SIGINT) a una aplicación en Windows?

Resuelto Matthew Murdoch asked hace 15 años • 18 respuestas

He escrito (en el pasado) aplicaciones multiplataforma (Windows/Unix) que, cuando se iniciaban desde la línea de comandos, manejaban una combinación escrita por el usuario Ctrlde Cla misma manera (es decir, para finalizar la aplicación limpiamente).

¿Es posible en Windows enviar un Ctrl- C/SIGINT/equivalente a un proceso de otro proceso (no relacionado) para solicitar que finalice limpiamente (dándole la oportunidad de ordenar recursos, etc.)?

Matthew Murdoch avatar May 02 '09 03:05 Matthew Murdoch
Aceptado

Investigué un poco sobre este tema, que resultó ser más popular de lo que esperaba. La respuesta de KindDragon fue uno de los puntos fundamentales.

Escribí una publicación de blog más larga sobre el tema y creé un programa de demostración funcional, que demuestra el uso de este tipo de sistema para cerrar una aplicación de línea de comandos de un par de maneras agradables. Esa publicación también enumera enlaces externos que utilicé en mi investigación.

En resumen, esos programas de demostración hacen lo siguiente:

  • Inicie un programa con una ventana visible usando .Net , oculte con pinvoke, ejecute durante 6 segundos, muestre con pinvoke, detenga con .Net .
  • Inicie un programa sin ventana usando .Net , ejecútelo durante 6 segundos, deténgalo conectando la consola y emitiendoConsoleCtrlEvent

Editar: la solución modificada de @KindDragon para aquellos que estén interesados ​​en el código aquí y ahora. Si planea iniciar otros programas después de detener el primero, debe volver a habilitar el manejo CTRLde + C; de lo contrario, el siguiente proceso heredará el estado deshabilitado del padre y no responderá a CTRL+ C.

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint dwProcessId);

[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern bool FreeConsole();

[DllImport("kernel32.dll")]
static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);

delegate bool ConsoleCtrlDelegate(CtrlTypes CtrlType);

// Enumerated type for the control messages sent to the handler routine
enum CtrlTypes : uint
{
    CTRL_C_EVENT = 0,
    CTRL_BREAK_EVENT,
    CTRL_CLOSE_EVENT,
    CTRL_LOGOFF_EVENT = 5,
    CTRL_SHUTDOWN_EVENT
}

[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);

public void StopProgram(Process proc)
{
    //This does not require the console window to be visible.
    if (AttachConsole((uint)proc.Id))
    {
    // Disable Ctrl-C handling for our program
    SetConsoleCtrlHandler(null, true); 
    GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0);

    //Moved this command up on suggestion from Timothy Jannace (see comments below)
    FreeConsole();
                
    // Must wait here. If we don't and re-enable Ctrl-C
    // handling below too fast, we might terminate ourselves.
    proc.WaitForExit(2000);
                
    //Re-enable Ctrl-C handling or any subsequently started
    //programs will inherit the disabled state.
    SetConsoleCtrlHandler(null, false); 
    }
}

Además, planifique una solución de contingencia si AttachConsole()la señal enviada falla, por ejemplo, durmiendo, entonces esto:

if (!proc.HasExited)
{
    try
    {
    proc.Kill();
    }
    catch (InvalidOperationException e){}
}
Stanislav avatar Mar 07 '2013 20:03 Stanislav

Lo más cerca que he estado de una solución es la aplicación de terceros SendSignal . El autor enumera el código fuente y un ejecutable. He verificado que funciona en Windows de 64 bits (ejecutándose como un programa de 32 bits, eliminando otro programa de 32 bits), pero no he descubierto cómo incrustar el código en un programa de Windows (ya sea de 32 bits). o 64 bits).

Cómo funciona:

Después de investigar mucho en el depurador, descubrí que el punto de entrada que realmente realiza el comportamiento asociado con una señal como ctrl-break es kernel32!CtrlRoutine. La función tenía el mismo prototipo que ThreadProc, por lo que se puede utilizar directamente con CreateRemoteThread, sin tener que inyectar código. Sin embargo, ¡ese no es un símbolo exportado! Está en diferentes direcciones (e incluso tiene diferentes nombres) en diferentes versiones de Windows. ¿Qué hacer?

Aquí está la solución que finalmente se me ocurrió. Instalo un controlador Ctrl de consola para mi aplicación y luego genero una señal de interrupción Ctrl para mi aplicación. Cuando llaman a mi controlador, miro hacia la parte superior de la pila para descubrir los parámetros pasados ​​a kernel32!BaseThreadStart. Tomo el primer parámetro, que es la dirección de inicio deseada del hilo, que es la dirección de kernel32! CtrlRoutine. Luego regreso de mi controlador, indicando que he manejado la señal y que mi aplicación no debe finalizarse. De vuelta en el hilo principal, espero hasta que se recupere la dirección de kernel32! CtrlRoutine. Una vez que lo tengo, creo un hilo remoto en el proceso de destino con la dirección de inicio descubierta. ¡Esto hace que los controladores Ctrl en el proceso de destino se evalúen como si se hubiera presionado Ctrl-break!

Lo bueno es que sólo el proceso objetivo se ve afectado y cualquier proceso (incluso un proceso con ventana) puede ser objetivo. Una desventaja es que mi pequeña aplicación no se puede usar en un archivo por lotes, ya que la eliminará cuando envíe el evento ctrl-break para descubrir la dirección de kernel32!CtrlRoutine.

(Prepárelo startsi lo ejecuta en un archivo por lotes).

arolson101 avatar Jul 24 '2009 17:07 arolson101

Supongo que llego un poco tarde a esta pregunta, pero escribiré algo de todos modos para cualquiera que tenga el mismo problema. Esta es la misma respuesta que le di a esta pregunta.

Mi problema era que me gustaría que mi aplicación fuera una aplicación GUI, pero los procesos ejecutados deberían ejecutarse en segundo plano sin ninguna ventana de consola interactiva adjunta. Creo que esta solución también debería funcionar cuando el proceso principal es un proceso de consola. Sin embargo, es posible que tengas que eliminar el indicador "CREATE_NO_WINDOW".

Logré resolver esto usando GenerateConsoleCtrlEvent () con una aplicación contenedora. La parte complicada es que la documentación no es muy clara sobre cómo se puede utilizar exactamente y los peligros que conlleva.

Mi solución se basa en lo que se describe aquí . Pero eso tampoco explica todos los detalles y con un error, así que aquí están los detalles sobre cómo hacerlo funcionar.

Cree una nueva aplicación auxiliar "Helper.exe". Esta aplicación se ubicará entre su aplicación (principal) y el proceso secundario que desea poder cerrar. También creará el proceso hijo real. Debe tener este proceso de "intermediario" o GenerateConsoleCtrlEvent() fallará.

Utilice algún tipo de mecanismo IPC para comunicar desde el padre al proceso auxiliar que el asistente debe cerrar el proceso hijo. Cuando el asistente recibe este evento, llama a "GenerateConsoleCtrlEvent(CTRL_BREAK, 0)", que se cierra a sí mismo y al proceso secundario. Yo mismo utilicé un objeto de evento para esto que el padre completa cuando quiere cancelar el proceso hijo.

Para crear su Helper.exe, créelo con CREATE_NO_WINDOW y CREATE_NEW_PROCESS_GROUP. Y al crear el proceso hijo, créelo sin banderas (0), lo que significa que derivará la consola de su padre. De no hacerlo, se ignorará el evento.

Es muy importante que cada paso se haga así. He estado probando todo tipo de combinaciones, pero esta combinación es la única que funciona. No puedes enviar un evento CTRL_C. Devolverá éxito pero el proceso lo ignorará. CTRL_BREAK es el único que funciona. Realmente no importa ya que ambos llamarán a ExitProcess() al final.

Tampoco puede llamar a GenerateConsoleCtrlEvent() con una identificación de grupo de proceso de la identificación del proceso secundario, lo que permite directamente que el proceso auxiliar continúe vivo. Esto también fracasará.

Pasé un día entero intentando que esto funcionara. Esta solución funciona para mí, pero si alguien tiene algo más que agregar, hágalo. Recorrí toda la red y encontré muchas personas con problemas similares pero sin una solución definitiva al problema. El funcionamiento de GenerateConsoleCtrlEvent() también es un poco extraño, así que si alguien conoce más detalles, por favor compártalo.

Shakta avatar Mar 15 '2010 07:03 Shakta

Una solución que encontré aquí es bastante simple si tiene Python 3.x disponible en su línea de comando. Primero, guarde un archivo (ctrl_c.py) con el contenido:

import ctypes
import sys
  
kernel = ctypes.windll.kernel32
    
pid = int(sys.argv[1])
kernel.FreeConsole()
kernel.AttachConsole(pid)
kernel.SetConsoleCtrlHandler(None, 1)
kernel.GenerateConsoleCtrlEvent(0, 0)
sys.exit(0)

Luego llame:

python ctrl_c.py 12345

Si eso no funciona, recomiendo probar el proyecto windows-kill:

  • Chocó: https://github.com/ElyDotDev/windows-kill
  • Nodo: https://github.com/ElyDotDev/node-windows-kill
johnml1135 avatar Oct 14 '2020 16:10 johnml1135

De alguna manera GenerateConsoleCtrlEvent()devuelve un error si lo llama para otro proceso, pero puede adjuntarlo a otra aplicación de consola y enviar eventos a todos los procesos secundarios.

void SendControlC(int pid)
{
    AttachConsole(pid); // attach to process console
    SetConsoleCtrlHandler(NULL, TRUE); // disable Control+C handling for our app
    GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); // generate Control+C event
}
KindDragon avatar Oct 15 '2012 15:10 KindDragon