Posso inviare un ctrl-C (SIGINT) a un'applicazione su Windows?


91

Ho (in passato) scritto applicazioni cross-piattaforma (Windows / Unix) che, una volta avviato dalla riga di comando, gestite un utente digitato Ctrl- Ccombinazione nello stesso modo (cioè per terminare l'applicazione in modo pulito).

È possibile su Windows inviare un Ctrl- C/ SIGINT / equivalente a un processo da un altro processo (non correlato) per richiedere che venga terminato in modo pulito (dandogli l'opportunità di riordinare le risorse, ecc.)?


1
Ero interessato a inviare Ctrl-C ai processi del servizio Java solo per ottenere i threaddump. Sembra che jstackpossa essere utilizzato in modo affidabile invece per questo argomento specifico: stackoverflow.com/a/47723393/603516
Vadzim

Risposte:


31

La soluzione più vicina a cui sono arrivato è l' app di terze parti SendSignal . L'autore elenca il codice sorgente e un eseguibile. Ho verificato che funziona con Windows a 64 bit (in esecuzione come un programma a 32 bit, uccidendo un altro programma a 32 bit), ma non ho capito come incorporare il codice in un programma Windows (sia o 64 bit).

Come funziona:

Dopo molte ricerche nel debugger, ho scoperto che il punto di ingresso che esegue effettivamente il comportamento associato a un segnale come ctrl-break è kernel32! CtrlRoutine. La funzione aveva lo stesso prototipo di ThreadProc, quindi può essere utilizzata direttamente con CreateRemoteThread, senza dover iniettare codice. Tuttavia, questo non è un simbolo esportato! È a indirizzi diversi (e ha anche nomi diversi) su diverse versioni di Windows. Cosa fare?

Ecco la soluzione che ho finalmente trovato. Installa un gestore ctrl della console per la mia app, quindi genera un segnale di interruzione del controllo per la mia app. Quando il mio gestore viene chiamato, guardo indietro all'inizio dello stack per scoprire i parametri passati a kernel32! BaseThreadStart. Prendo il primo parametro, che è l'indirizzo iniziale desiderato del thread, che è l'indirizzo di kernel32! CtrlRoutine. Quindi ritorno dal mio gestore, indicando che ho gestito il segnale e la mia app non dovrebbe essere terminata. Di nuovo nel thread principale, aspetto che l'indirizzo di kernel32! CtrlRoutine sia stato recuperato. Una volta ottenuto, creo un thread remoto nel processo di destinazione con l'indirizzo iniziale scoperto. Ciò fa sì che i gestori ctrl nel processo di destinazione vengano valutati come se fosse stato premuto ctrl-break!

La cosa bella è che solo il processo di destinazione è interessato e qualsiasi processo (anche un processo in finestra) può essere mirato. Uno svantaggio è che la mia piccola app non può essere utilizzata in un file batch, poiché la ucciderà quando invia l'evento ctrl-break per scoprire l'indirizzo di kernel32! CtrlRoutine.

(Anteponilo a startse lo stai eseguendo in un file batch.)


2
+1 - Il codice collegato utilizza Ctrl-Break invece di Ctrl-C ma a prima vista (guardando la documentazione per GenerateConsoleCtrlEvent) potrebbe essere adattato per Ctrl-C.
Matthew Murdoch,

8
In realtà c'è una funzione per farlo per te, lol, "GenerateConsoleCtrlEvent". Vedi: msdn.microsoft.com/en-us/library/ms683155(v=vs.85).aspx Vedi anche la mia risposta qui: serverfault.com/a/501045/10023
Triynko

1
Link Github relativo alla risposta sopra: github.com/walware/statet/tree/master/…
meawoppl

3
È sbalordito il fatto che Windows sia vecchio quanto lo è eppure Microsoft non ha fornito un meccanismo per l'applicazione console A per ascoltare un segnale dall'applicazione console B in modo che l'applicazione console B possa dire all'applicazione console A di spegnersi in modo pulito. In ogni caso, MS-bashing a parte, ho provato SendSignal e ho ottenuto "CreateRemoteThread fallito con 0x00000005". Qualche altra idea su come codificare l'applicazione A per ascoltare un segnale (qualsiasi segnale conveniente) e come segnalarlo?
David I. McIntosh

3
@ DavidI.McIntosh sembra che tu voglia creare un evento con nome in entrambi i processi; saresti quindi in grado di segnalare l'uno dall'altro. Controlla la documentazione per
CreateEvent

58

Ho svolto alcune ricerche su questo argomento, che si è rivelato più popolare di quanto mi aspettassi. La risposta di KindDragon è stata uno dei punti cardine.

Ho scritto un post sul blog più lungo sull'argomento e ho creato un programma demo funzionante, che dimostra l'utilizzo di questo tipo di sistema per chiudere un'applicazione a riga di comando in un paio di bei modi. Quel post elenca anche i link esterni che ho usato nella mia ricerca.

In breve, quei programmi demo fanno quanto segue:

  • Avvia un programma con una finestra visibile usando .Net, nascondi con pinvoke, corri per 6 secondi, mostra con pinvoke, interrompi con .Net.
  • Avvia un programma senza una finestra utilizzando .Net, esegui per 6 secondi, interrompi collegando la console ed emettendo ConsoleCtrlEvent

Modifica: la soluzione modificata di KindDragon per coloro che sono interessati al codice qui e ora. Se prevedi di avviare altri programmi dopo aver interrotto il primo, dovresti riattivare la gestione Ctrl-C, altrimenti il ​​processo successivo erediterà lo stato disabilitato del genitore e non risponderà 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); 
  }
}

Inoltre, pianifica una soluzione di emergenza se AttachConsole()o il segnale inviato dovesse fallire, ad esempio dormendo allora questo:

if (!proc.HasExited)
{
  try
  {
    proc.Kill();
  }
  catch (InvalidOperationException e){}
}

4
Ispirato da questo, ho creato un'app cpp "standalone" che lo fa: gist.github.com/rdp/f51fb274d69c5c31b6be nel caso fosse utile. E sì, questo metodo può inviare ctrl + c o ctrl + break a "qualsiasi" pid, apparentemente.
Rogerdpack

La mancanza di una delle DLL importa "SetConsoleCtrlHandler", ma funziona.
Derf Skren

ottimo post. ottimo blog. Suggerirei di terminare la funzione StopProgramWithInvisibleWindowUsingPinvoke nel programma dell'esperimento. Ho quasi abbandonato l'idea pensando che non avessi trovato una soluzione
Wilmer Saint

Per evitare il wait()ho rimosso il Riattiva Ctrl-C ( SetConsoleCtrlHandler(null, false);). Ho fatto alcuni test (più chiamate, anche su processi già terminati) e non ho riscontrato alcun effetto collaterale (ancora?).
56ka

FreeConsole dovrebbe essere chiamato immediatamente dopo aver inviato GenerateConsoleCtrlEvent. Fino a quando non viene chiamato, la tua applicazione verrà allegata all'altra console. Se l'altra console viene interrotta prima che termini la chiusura, anche l'applicazione verrà terminata!
Timothy Jannace

17

Immagino di essere un po 'in ritardo su questa domanda, ma scriverò comunque qualcosa per chiunque abbia lo stesso problema. Questa è la stessa risposta che ho dato a questa domanda.

Il mio problema era che vorrei che la mia applicazione fosse un'applicazione GUI, ma i processi eseguiti dovrebbero essere eseguiti in background senza alcuna finestra della console interattiva collegata. Penso che questa soluzione dovrebbe funzionare anche quando il processo genitore è un processo della console. Tuttavia, potresti dover rimuovere il flag "CREATE_NO_WINDOW".

Sono riuscito a risolverlo utilizzando GenerateConsoleCtrlEvent () con un'app wrapper. La parte difficile è solo che la documentazione non è molto chiara su come può essere utilizzata e sulle insidie.

La mia soluzione si basa su quanto descritto qui . Ma anche questo non ha spiegato tutti i dettagli e con un errore, quindi ecco i dettagli su come farlo funzionare.

Crea una nuova applicazione di supporto "Helper.exe". Questa applicazione si troverà tra la tua applicazione (genitore) e il processo figlio che vuoi poter chiudere. Creerà anche il processo figlio effettivo. È necessario disporre di questo processo "intermediario" altrimenti GenerateConsoleCtrlEvent () fallirà.

Usa un qualche tipo di meccanismo IPC per comunicare dal processo genitore al processo di supporto che l'helper dovrebbe chiudere il processo figlio. Quando l'helper ottiene questo evento, chiama "GenerateConsoleCtrlEvent (CTRL_BREAK, 0)" che chiude se stesso e il processo figlio. Ho usato un oggetto evento per questo io stesso che il genitore completa quando vuole annullare il processo figlio.

Per creare il tuo Helper.exe, crealo con CREATE_NO_WINDOW e CREATE_NEW_PROCESS_GROUP. E quando si crea il processo figlio, crearlo senza flag (0), il che significa che deriverà la console dal suo genitore. In caso contrario, ignorerà l'evento.

È molto importante che ogni passaggio venga eseguito in questo modo. Ho provato tutti i diversi tipi di combinazioni, ma questa è l'unica che funziona. Non puoi inviare un evento CTRL_C. Restituirà il successo ma verrà ignorato dal processo. CTRL_BREAK è l'unico che funziona. Non ha molta importanza dal momento che entrambi chiameranno ExitProcess () alla fine.

Inoltre, non puoi chiamare GenerateConsoleCtrlEvent () con un process groupd id dell'id del processo figlio, consentendo direttamente al processo di supporto di continuare a vivere. Anche questo fallirà.

Ho passato un'intera giornata a cercare di farlo funzionare. Questa soluzione funziona per me, ma se qualcuno ha qualcos'altro da aggiungere, fallo. Sono andato in tutta la rete trovando molte persone con problemi simili ma nessuna soluzione definitiva al problema. Anche il modo in cui funziona GenerateConsoleCtrlEvent () è un po 'strano, quindi se qualcuno conosce maggiori dettagli su di esso, condividilo.


6
Grazie per la soluzione! Questo è ancora un altro esempio dell'API di Windows che è un disastro così terribile.
Vitaut

8

Modificare:

Per un'app GUI, il modo "normale" per gestirlo nello sviluppo di Windows sarebbe inviare un messaggio WM_CLOSE alla finestra principale del processo.

Per un'app console, è necessario utilizzare SetConsoleCtrlHandler per aggiungere un file CTRL_C_EVENT.

Se l'applicazione non lo rispetta, puoi chiamare TerminateProcess .


Voglio farlo in un'app della riga di comando (senza finestre). TerminateProcess è un meccanismo di chiusura forzata (simile a SIGKILL), quindi non offre all'applicazione di terminazione alcuna possibilità di eseguire la pulizia.
Matthew Murdoch

@ Matthew Murdoch: aggiornato per includere informazioni su come gestirlo in un'app per console. Puoi anche catturare altri eventi, tra cui l'arresto di Windows o la chiusura della console, ecc. Tramite lo stesso meccanismo. Vedere MSDN per i dettagli completi.
Reed Copsey

@Reed Copsey: Ma mi piacerebbe iniziare questo processo da un processo separato. Ho dato un'occhiata a GenerateConsoleCtrlEvent (referenziato da SetConsoleCtrlHandler) ma sembra funzionare solo se il processo terminato è nello stesso "gruppo di processi" del processo che sta terminando ... Qualche idea?
Matthew Murdoch

1
Matthew: L'unica idea che avrei sarebbe quella di trovare il processo, trovare il suo genitore (la console), ottenere l'handle della finestra principale del genitore (la console) e usare SendKeys per inviare un Ctrl + C direttamente ad esso. Ciò dovrebbe far sì che il processo della console riceva un CTRL_C_EVENT. Non l'ho provato, però, non sono sicuro di come avrebbe funzionato.
Reed Copsey

Ecco un c ++ equivalente a SendKeys stackoverflow.com/questions/6838363/… vedi anche stackoverflow.com/questions/10407769/… anche se apparentemente non è garantito che funzioni anche allora ...
rogerdpack

7

In qualche modo GenerateConsoleCtrlEvent()restituisce l'errore se lo chiami per un altro processo, ma puoi collegarti a un'altra applicazione console e inviare l'evento a tutti i processi figli.

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
}

e come restituire il controllo? e poi mando control-c, programma stopar, ma non riesco a fare niente con le mani.
Yaroslav L.

Dove restituire il controllo?
KindDragon

Penso che chiami SetConsoleCtrlHandler (NULL, FALSE) per rimuovere il tuo gestore, vedi le varie altre risposte.
rogerdpack

6

Ecco il codice che uso nella mia app C ++.

Punti positivi:

  • Funziona dall'app della console
  • Funziona dal servizio Windows
  • Nessun ritardo richiesto
  • Non chiude l'app corrente

Punti negativi:

  • La console principale viene persa e ne viene creata una nuova (vedi FreeConsole )
  • Il cambio di console dà risultati strani ...

// 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;
}

Esempio di utilizzo:

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

Dovrebbe essere chiarissimo perché al momento non lo è. Esiste una versione modificata e compilata di SendSignal per inviare Ctrl-C (per impostazione predefinita invia solo Ctrl + Break). Ecco alcuni binari:

(2014-3-7): ho realizzato sia la versione a 32 bit che quella a 64 bit con Ctrl-C, si chiama SendSignalCtrlC.exe e puoi scaricarlo su: https://dl.dropboxusercontent.com/u/49065779/ sendignalctrlc / x86 / SendSignalCtrlC.exe https://dl.dropboxusercontent.com/u/49065779/sendsignalctrlc/x86_64/SendSignalCtrlC.exe - Juraj Michalak

Ho anche rispecchiato quei file nel caso:
versione a 32 bit: https://www.dropbox.com/s/r96jxglhkm4sjz2/SendSignalCtrlC.exe?dl=0 versione a
64 bit: https://www.dropbox.com /s/hhe0io7mcgcle1c/SendSignalCtrlC64.exe?dl=0

Disclaimer: non ho creato quei file. Nessuna modifica è stata apportata ai file originali compilati. L'unica piattaforma testata è Windows 7 a 64 bit. Si consiglia di adattare il sorgente disponibile su http://www.latenighthacking.com/projects/2003/sendSignal/ e di compilarlo da soli.


2

In Java, utilizzando JNA con la libreria Kernel32.dll, simile a una soluzione C ++. Esegue il metodo principale CtrlCSender come un processo che ottiene solo la console del processo a cui inviare l'evento Ctrl + C e genera l'evento. Poiché viene eseguito separatamente senza una console, non è necessario disabilitare e abilitare nuovamente l'evento Ctrl + C.

CtrlCSender.java - Basato sulle risposte di Nemo1024 e KindDragon .

Dato un ID processo noto, questa applicazione consoless collegherà la console del processo mirato e genererà un evento CTRL + C su di essa.

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);
    }
}

Applicazione principale : esegue CtrlCSender come processo separato senza console

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, se lo eseguo su un processo PowerShell il processo rimane attivo. D'altra parte, è interessante notare che l'esecuzione in-process sulla JVM fa effettivamente arrestare la VM! Hai idea del perché un processo PowerShell non risponderebbe a questa tecnica?
Groostav


1

Una soluzione che ho trovato da qui è piuttosto semplice se hai python 3.x disponibile nella tua riga di comando. Innanzitutto, salva un file (ctrl_c.py) con il contenuto:

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)

Quindi chiama:

python ctrl_c.py 12345

Se non funziona, ti consiglio di provare il progetto Windows-kill: https://github.com/alirdn/windows-kill


0

Un mio amico ha suggerito un modo completamente diverso di risolvere il problema e per me ha funzionato. Usa un vbscript come di seguito. Si avvia e l'applicazione, lascialo funzionare per 7 secondi e chiudilo usando ctrl + c .

'Esempio VBScript

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

WshShell.Run "notepad.exe"

WshShell.AppActivate "notepad"

WScript.Sleep 7000

WshShell.SendKeys "^C"

Funziona se l'applicazione ha una finestra puoi AppActive (portare in primo piano).
rogerdpack

0

Ho trovato tutto questo troppo complicato e utilizzato SendKeys per inviare un CTRL- Csequenza di tasti alla finestra riga di comando (finestra di IE cmd.exe) come una soluzione.


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 può essere inviato al programma utilizzando windows-kill , per sintassi windows-kill -SIGINT PID, dove PIDpuò essere ottenuto da pslist di Microsoft .

Per quanto riguarda la cattura di SIGINT, se il tuo programma è in Python, puoi implementare l'elaborazione / cattura SIGINT come in questa soluzione .


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.