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


91

He (en el pasado) escrita aplicaciones multiplataforma (Windows / Unix) que, cuando se inicia desde la línea de comandos, manejan un escrito de usuario Ctrl- Cla combinación de la misma manera (es decir, para terminar la aplicación limpia).

¿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 los recursos, etc.)?


1
Estaba interesado en enviar Ctrl-C a los procesos de servicio de Java solo para obtener threaddumps. Parece que se jstackpuede usar de manera confiable en su lugar para este asunto específico: stackoverflow.com/a/47723393/603516
Vadzim

Respuestas:


31

Lo más cerca que he llegado a 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, matando a 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 usar con CreateRemoteThread directamente, 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, luego genero una señal ctrl-break para mi aplicación. Cuando se llama a mi controlador, miro hacia atrás en la parte superior de la pila para descubrir los parámetros pasados ​​a kernel32! BaseThreadStart. Agarro el primer parámetro, que es la dirección de inicio deseada del hilo, que es la dirección de kernel32! CtrlRoutine. Luego vuelvo de mi controlador, lo que indica que he manejado la señal y mi aplicación no debería cerrarse. 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 solo el proceso de destino se ve afectado y cualquier proceso (incluso un proceso en ventana) puede ser dirigido. Una desventaja es que mi pequeña aplicación no se puede usar en un archivo por lotes, ya que la matará cuando envíe el evento ctrl-break para descubrir la dirección de kernel32! CtrlRoutine.

(Antepóngalo startsi lo ejecuta en un archivo por lotes).


2
+1: el código vinculado usa Ctrl-Break en lugar de Ctrl-C, pero a primera vista (mirando la documentación de GenerateConsoleCtrlEvent) podría adaptarse para Ctrl-C.
Matthew Murdoch

8
En realidad, hay una función para hacer esto por ti, lol, "GenerateConsoleCtrlEvent". Ver: msdn.microsoft.com/en-us/library/ms683155(v=vs.85).aspx Ver también mi respuesta aquí: serverfault.com/a/501045/10023
Triynko

1
Enlace de Github relacionado con la respuesta anterior: github.com/walware/statet/tree/master/…
meawoppl

3
Asombra la mente de que Windows es tan antiguo como lo es y, sin embargo, Microsoft no ha proporcionado un mecanismo para que la aplicación de consola A escuche una señal de la aplicación de consola B para que la aplicación de consola B pueda decirle a la aplicación de consola A que se apague de forma limpia. En cualquier caso, aparte de MS-bashing, probé SendSignal y obtuve "CreateRemoteThread falló con 0x00000005". ¿Alguna otra idea de cómo codificar la aplicación A para escuchar una señal (cualquier señal que sea conveniente), y cómo luego señalizarla?
David I. McIntosh

3
@ DavidI.McIntosh parece que desea crear un evento con nombre en ambos procesos; entonces podrá señalar uno del otro. Consulte la documentación de CreateEvent
arolson101

58

He realizado algunas investigaciones 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 en un par de formas agradables. Esa publicación también enumera los 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, ocúltese con pinvoke, ejecútelo durante 6 segundos, muestre con pinvoke, deténgase con .Net.
  • Inicie un programa sin una ventana utilizando .Net, ejecútelo durante 6 segundos, deténgase conectando la consola y emitiendo ConsoleCtrlEvent

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 de Ctrl-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){}
}

4
Inspirado por esto, creé una aplicación cpp "independiente" que lo hace: gist.github.com/rdp/f51fb274d69c5c31b6be en caso de que sea útil. Y sí, este método puede enviar ctrl + co ctrl + break a "cualquier" pid, aparentemente.
rogerdpack

Falta una de las importaciones DLL "SetConsoleCtrlHandler", pero esto funciona.
Derf Skren

excelente post. excelente blog. Sugeriría terminar la función StopProgramWithInvisibleWindowUsingPinvoke en su programa de experimento. Casi abandoné la idea pensando que no habías encontrado una solución
Wilmer Saint

Para evitar el wait(), eliminé la opción Volver a habilitar Ctrl-C ( SetConsoleCtrlHandler(null, false);). Hice algunas pruebas (múltiples llamadas, también en procesos ya terminados) y no encontré ningún efecto secundario (¿todavía?).
56ka

FreeConsole debe llamarse inmediatamente después de enviar GenerateConsoleCtrlEvent. Hasta que se llame, su aplicación se adjuntará a la otra consola. Si la otra consola se mata antes de que termine de cerrarse, su aplicación también se cerrará.
Timothy Jannace

17

Supongo que llego un poco tarde en esta pregunta, pero de todos modos escribiré algo 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 deba eliminar la bandera "CREATE_NO_WINDOW".

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

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

Cree una nueva aplicación de ayuda "Helper.exe". Esta aplicación se ubicará entre su aplicación (padre) y el proceso hijo 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 de IPC para comunicar del padre al proceso auxiliar que el ayudante debe cerrar el proceso hijo. Cuando el asistente obtiene este evento, llama a "GenerateConsoleCtrlEvent (CTRL_BREAK, 0)", que se cierra a sí mismo y al proceso hijo. Usé un objeto de evento para esto yo mismo que el padre completa cuando quiere cancelar el proceso secundario.

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 marcas (0), lo que significa que derivará la consola de su padre. Si no lo hace, se ignorará el evento.

Es muy importante que cada paso se haga así. He estado probando diferentes tipos de combinaciones, pero esta combinación es la única que funciona. No puede enviar un evento CTRL_C. Devolverá el é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 que el proceso auxiliar continúe viviendo. Esto también fallará.

Pasé un día entero tratando de que esto funcionara. Esta solución funciona para mí, pero si alguien tiene algo más que agregar, hágalo. Busqué por toda la red 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 al respecto, compártelo.


6
¡Gracias por la solución! Este es otro ejemplo de que la API de Windows es un desastre tan terrible.
vitaut

8

Editar:

Para una aplicación GUI, la forma "normal" de manejar esto en el desarrollo de Windows sería enviar un mensaje WM_CLOSE a la ventana principal del proceso.

Para una aplicación de consola, debe usar SetConsoleCtrlHandler para agregar unCTRL_C_EVENT .

Si la aplicación no cumple con eso, puede llamar a TerminateProcess .


Quiero hacer esto en una aplicación de línea de comandos (sin Windows). TerminateProcess es un mecanismo de eliminación forzada (similar a SIGKILL), por lo que no le da a la aplicación de terminación ninguna oportunidad de limpiar.
Matthew Murdoch

@Matthew Murdoch: actualizado para incluir información sobre cómo manejar esto en una aplicación de consola. También puede detectar otros eventos, incluido el cierre de Windows o el cierre de la consola, etc., a través del mismo mecanismo. Consulte MSDN para obtener todos los detalles.
Reed Copsey

@Reed Copsey: Pero me gustaría iniciar esto desde un proceso separado. He echado un vistazo a GenerateConsoleCtrlEvent (referenciado desde SetConsoleCtrlHandler) pero esto parece funcionar solo si el proceso que se termina está en el mismo 'grupo de procesos' que el proceso que realiza la terminación ... ¿Alguna idea?
Matthew Murdoch

1
Matthew: La única idea que tendría sería encontrar el proceso, encontrar su padre (la consola), obtener el identificador de la ventana principal del padre (la consola) y usar SendKeys para enviarle Ctrl + C directamente. Eso debería hacer que el proceso de su consola reciba un CTRL_C_EVENT. Sin embargo, no lo he probado, no estoy seguro de qué tan bien funcionaría.
Reed Copsey

Aquí hay un equivalente en c ++ a SendKeys stackoverflow.com/questions/6838363/… vea también stackoverflow.com/questions/10407769/… aunque aparentemente no está garantizado que funcione incluso entonces ...
rogerdpack

7

De alguna manera GenerateConsoleCtrlEvent()devuelve el error si lo llama para otro proceso, pero puede adjuntarlo a otra aplicación de consola y enviar un evento 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
}

y como devolver el control? y luego mando control-c, programa stopar, pero no puedo hacer nada con sus manos.
Yaroslav L.

¿Dónde devolver el control?
KindDragon

Creo que llamas a SetConsoleCtrlHandler (NULL, FALSE) para eliminar tu controlador, mira las otras respuestas.
rogerdpack

6

Aquí está el código que uso en mi aplicación C ++.

Puntos positivos :

  • Funciona desde la aplicación de consola
  • Funciona desde el servicio de Windows
  • No se requiere demora
  • No cierra la aplicación actual

Puntos negativos :

  • La consola principal se pierde y se crea una nueva (ver FreeConsole )
  • El cambio de consola da resultados extraños ...

// Inspired from http://stackoverflow.com/a/15281070/1529139
// and http://stackoverflow.com/q/40059902/1529139
bool signalCtrl(DWORD dwProcessId, DWORD dwCtrlEvent)
{
    bool success = false;
    DWORD thisConsoleId = GetCurrentProcessId();
    // Leave current console if it exists
    // (otherwise AttachConsole will return ERROR_ACCESS_DENIED)
    bool consoleDetached = (FreeConsole() != FALSE);

    if (AttachConsole(dwProcessId) != FALSE)
    {
        // Add a fake Ctrl-C handler for avoid instant kill is this console
        // WARNING: do not revert it or current program will be also killed
        SetConsoleCtrlHandler(nullptr, true);
        success = (GenerateConsoleCtrlEvent(dwCtrlEvent, 0) != FALSE);
        FreeConsole();
    }

    if (consoleDetached)
    {
        // Create a new console if previous was deleted by OS
        if (AttachConsole(thisConsoleId) == FALSE)
        {
            int errorCode = GetLastError();
            if (errorCode == 31) // 31=ERROR_GEN_FAILURE
            {
                AllocConsole();
            }
        }
    }
    return success;
}

Ejemplo de uso:

DWORD dwProcessId = ...;
if (signalCtrl(dwProcessId, CTRL_C_EVENT))
{
    cout << "Signal sent" << endl;
}

4
        void SendSIGINT( HANDLE hProcess )
        {
            DWORD pid = GetProcessId(hProcess);
            FreeConsole();
            if (AttachConsole(pid))
            {
                // Disable Ctrl-C handling for our program
                SetConsoleCtrlHandler(NULL, true);

                GenerateConsoleCtrlEvent(CTRL_C_EVENT, 0); // SIGINT

                //Re-enable Ctrl-C handling or any subsequently started
                //programs will inherit the disabled state.
                SetConsoleCtrlHandler(NULL, false);

                WaitForSingleObject(hProcess, 10000);
            }
        }

2

Debería quedar muy claro porque por el momento no lo es. Existe una versión modificada y compilada de SendSignal para enviar Ctrl-C (por defecto solo envía Ctrl + Break). Aquí hay algunos binarios:

(2014-3-7): Creé las versiones de 32 y 64 bits con Ctrl-C, se llama SendSignalCtrlC.exe y puede descargarlo en: https://dl.dropboxusercontent.com/u/49065779/ sendignalctrlc / x86 / SendSignalCtrlC.exe https://dl.dropboxusercontent.com/u/49065779/sendsignalctrlc/x86_64/SendSignalCtrlC.exe - Juraj Michalak

También he reflejado esos archivos por si acaso:
versión de 32 bits: https://www.dropbox.com/s/r96jxglhkm4sjz2/SendSignalCtrlC.exe?dl=0 versión de
64 bits: https://www.dropbox.com /s/hhe0io7mcgcle1c/SendSignalCtrlC64.exe?dl=0

Descargo de responsabilidad: yo no construí esos archivos. No se realizaron modificaciones a los archivos originales compilados. La única plataforma probada es Windows 7 de 64 bits. Se recomienda adaptar la fuente disponible en http://www.latenighthacking.com/projects/2003/sendSignal/ y compilarla usted mismo.


2

En Java, se usa JNA con la biblioteca Kernel32.dll, similar a una solución C ++. Ejecuta el método principal CtrlCSender como un proceso que solo obtiene la consola del proceso para enviar el evento Ctrl + C y genera el evento. Como se ejecuta por separado sin una consola, el evento Ctrl + C no necesita deshabilitarse y habilitarse nuevamente.

CtrlCSender.java - Sobre la base de Nemo1024 de y de KindDragon respuestas.

Dado un ID de proceso conocido, esta aplicación sin consola adjuntará la consola del proceso objetivo y generará un evento CTRL + C en él.

import com.sun.jna.platform.win32.Kernel32;    

public class CtrlCSender {

    public static void main(String args[]) {
        int processId = Integer.parseInt(args[0]);
        Kernel32.INSTANCE.AttachConsole(processId);
        Kernel32.INSTANCE.GenerateConsoleCtrlEvent(Kernel32.CTRL_C_EVENT, 0);
    }
}

Aplicación principal : ejecuta CtrlCSender como un proceso independiente sin consola

ProcessBuilder pb = new ProcessBuilder();
pb.command("javaw", "-cp", System.getProperty("java.class.path", "."), CtrlCSender.class.getName(), processId);
pb.redirectErrorStream();
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
Process ctrlCProcess = pb.start();
ctrlCProcess.waitFor();

hmm, si ejecuto esto contra un proceso de PowerShell, el proceso permanece vivo. Por otro lado, curiosamente, la ejecución en proceso en la JVM hace que la máquina virtual se caiga. ¿Tiene alguna idea de por qué un proceso de PowerShell no respondería a esta técnica?
Groostav


1

Una solución que he encontrado desde 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: https://github.com/alirdn/windows-kill


0

Un amigo mío sugirió una forma completamente diferente de resolver el problema y funcionó para mí. Utilice un vbscript como el siguiente. Se inicia y aplica, déjalo correr por 7 segundos y ciérralo usando ctrl + c .

'Ejemplo de VBScript

Set WshShell = WScript.CreateObject("WScript.Shell")

WshShell.Run "notepad.exe"

WshShell.AppActivate "notepad"

WScript.Sleep 7000

WshShell.SendKeys "^C"

Esto funciona si la aplicación tiene una ventana que puede AppActive (traer al primer plano).
rogerdpack

0

He encontrado todo esto demasiado complicado y se utiliza SendKeys para enviar un CTRL- C(es decir, la ventana cmd.exe) de pulsaciones de teclas a la ventana de línea de comandos como una solución.


0
// Send [CTRL-C] to interrupt a batch file running in a Command Prompt window, even if the Command Prompt window is not visible,
// without bringing the Command Prompt window into focus.
// [CTRL-C] will have an effect on the batch file, but not on the Command Prompt  window itself -- in other words,
// [CTRL-C] will not have the same visible effect on a Command Prompt window that isn't running a batch file at the moment
// as bringing a Command Prompt window that isn't running a batch file into focus and pressing [CTRL-C] on the keyboard.
ulong ulProcessId = 0UL;
// hwC = Find Command Prompt window HWND
GetWindowThreadProcessId (hwC, (LPDWORD) &ulProcessId);
AttachConsole ((DWORD) ulProcessId);
SetConsoleCtrlHandler (NULL, TRUE);
GenerateConsoleCtrlEvent (CTRL_C_EVENT, 0UL);
SetConsoleCtrlHandler (NULL, FALSE);
FreeConsole ();

0

SIGINT se puede enviar al programa usando windows-kill , por sintaxis windows-kill -SIGINT PID, donde PIDse puede obtener mediante pslist de Microsoft .

Con respecto a la captura de SIGINT, si su programa está en Python, puede implementar el procesamiento / captura de SIGINT como en esta solución .


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.