Могу ли я отправить Ctrl-C (SIGINT) приложению в Windows? - PullRequest
80 голосов
/ 02 мая 2009

У меня есть (в прошлом) написанные кроссплатформенные (Windows / Unix) приложения, которые при запуске из командной строки обрабатывали пользовательскую комбинацию Ctrl - C таким же образом (то есть, чтобы завершить приложение чисто).

Возможно ли в Windows отправить Ctrl - C / SIGINT / эквивалентному процессу из другого (не связанного) процесса, чтобы запросить его чистое завершение (предоставив ему возможность убирать ресурсы и т. д.)?

Ответы [ 13 ]

52 голосов
/ 08 марта 2013

Я провел некоторое исследование по этой теме, которое оказалось более популярным, чем я ожидал. Ответ KindDragon был одним из ключевых моментов.

Я написал более длинную запись в блоге по этой теме и создал работающую демонстрационную программу, которая демонстрирует использование этого типа системы для закрытия приложения командной строки в паре приятных мод. В этом посте также перечислены внешние ссылки, которые я использовал в своем исследовании.

Короче говоря, эти демонстрационные программы делают следующее:

  • Запустите программу с видимым окном с помощью .Net, спрячьте с помощью pinvoke, запустите на 6 секунд, покажите с помощью pinvoke, остановитесь с помощью .Net.
  • Запустите программу без окна, используя .Net, запустите в течение 6 секунд, остановитесь, подключив консоль и выдав ConsoleCtrlEvent

Редактировать: Исправленное решение от KindDragon для тех, кто интересуется кодом здесь и сейчас. Если вы планируете запускать другие программы после остановки первой, вам следует снова включить обработку Ctrl-C, иначе следующий процесс унаследует отключенное состояние родителя и не будет отвечать на 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); 
  }
}

Кроме того, спланируйте решение на случай непредвиденных обстоятельств, если AttachConsole() или отправленный сигнал не будет работать, например, в спящем режиме, тогда это:

if (!proc.HasExited)
{
  try
  {
    proc.Kill();
  }
  catch (InvalidOperationException e){}
}
26 голосов
/ 24 июля 2009

Самое близкое решение, которое я нашел, - это SendSignal стороннее приложение. Автор перечисляет исходный код и исполняемый файл. Я проверил, что он работает под 64-битными окнами (работает как 32-битная программа, убивает другую 32-битную программу), но я не понял, как встроить код в программу Windows (либо 32-битную или 64-разрядный).

Как это работает:

После долгих размышлений в отладчике я обнаружил, что точкой входа, которая фактически выполняет поведение, связанное с сигналом, таким как ctrl-break, является kernel32! CtrlRoutine. Функция имела тот же прототип, что и ThreadProc, поэтому ее можно использовать непосредственно с CreateRemoteThread, без необходимости вставлять код. Однако это не экспортируемый символ! Он находится по разным адресам (и даже имеет разные имена) в разных версиях Windows. Что делать?

Вот решение, которое я наконец-то нашел. Я устанавливаю консольный обработчик ctrl для своего приложения, затем генерирую сигнал ctrl-break для своего приложения. Когда вызывается мой обработчик, я оглядываюсь назад на вершину стека, чтобы узнать параметры, передаваемые в kernel32! BaseThreadStart. Я беру первый параметр, который является желаемым начальным адресом потока, который является адресом kernel32! CtrlRoutine. Затем я возвращаюсь из своего обработчика, показывая, что обработал сигнал, и мое приложение не должно быть остановлено. Вернувшись в основной поток, я жду, пока не будет получен адрес kernel32! CtrlRoutine. Получив его, я создаю удаленный поток в целевом процессе с обнаруженным начальным адресом. Это приводит к тому, что обработчики ctrl в целевом процессе оцениваются так, как если бы был нажат ctrl-break!

Приятно то, что затрагивается только целевой процесс, и любой процесс (даже оконный процесс) может быть нацелен. Недостатком является то, что мое маленькое приложение нельзя использовать в командном файле, так как оно уничтожит его, когда отправит событие ctrl-break, чтобы обнаружить адрес kernel32! CtrlRoutine.

(предшествует start при запуске в командном файле.)

17 голосов
/ 15 марта 2010

Полагаю, я немного опоздал с этим вопросом, но в любом случае напишу что-нибудь для тех, у кого такая же проблема. Это тот же ответ, который я дал на этот вопрос.

Моя проблема заключалась в том, что я хотел бы, чтобы мое приложение было приложением с графическим интерфейсом, но выполняемые процессы должны выполняться в фоновом режиме без какого-либо окна интерактивной консоли. Я думаю, что это решение также должно работать, когда родительский процесс является консольным процессом. Возможно, вам придется удалить флаг "CREATE_NO_WINDOW".

Мне удалось решить эту проблему с помощью GenerateConsoleCtrlEvent () с приложением-оболочкой. Сложность в том, что в документации не совсем ясно, как именно ее можно использовать и какие подводные камни с ней связаны.

Мое решение основано на том, что описано здесь . Но это на самом деле не объяснило все детали и с ошибкой, так что вот подробности о том, как заставить это работать.

Создайте новое вспомогательное приложение "Helper.exe". Это приложение будет находиться между вашим приложением (родительским) и дочерним процессом, который вы хотите закрыть. Это также создаст фактический дочерний процесс. У вас должен быть этот процесс "среднего человека", иначе GenerateConsoleCtrlEvent () завершится неудачей.

Используйте какой-нибудь механизм IPC для связи от родительского процесса к вспомогательному процессу о том, что помощник должен закрыть дочерний процесс. Когда помощник получает это событие, он вызывает «GenerateConsoleCtrlEvent (CTRL_BREAK, 0)», который закрывает себя и дочерний процесс. Я сам использовал для этого объект события, который родитель завершает, когда хочет отменить дочерний процесс.

Чтобы создать свой Helper.exe, создайте его с помощью CREATE_NO_WINDOW и CREATE_NEW_PROCESS_GROUP. А при создании дочернего процесса создайте его без флагов (0), что означает, что он получит консоль от своего родителя. Невыполнение этого условия приведет к игнорированию события.

Очень важно, чтобы каждый шаг выполнялся следующим образом. Я пробовал разные комбинации, но эта комбинация единственная, которая работает. Вы не можете отправить событие CTRL_C. Это вернет успех, но будет проигнорировано процессом. CTRL_BREAK - единственный, который работает. Не имеет большого значения, так как они оба в конце вызовут ExitProcess ().

Вы также не можете вызвать GenerateConsoleCtrlEvent () с идентификатором группы процессов, непосредственно идентифицирующим дочерний процесс, что позволяет вспомогательному процессу продолжать работу. Это также не удастся.

Я провел целый день, пытаясь заставить это работать. Это решение работает для меня, но если у кого-то есть что-то еще, пожалуйста, сделайте. Я ходил по сети, находя множество людей с похожими проблемами, но без определенного решения проблемы. Принцип работы GenerateConsoleCtrlEvent () также немного странный, поэтому, если кто-то знает о нем больше подробностей, пожалуйста, поделитесь.

6 голосов
/ 15 октября 2012

Почему-то GenerateConsoleCtrlEvent() возвращает ошибку, если вы вызываете ее для другого процесса, но вы можете присоединиться к другому консольному приложению и отправить событие всем дочерним процессам.

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
}
6 голосов
/ 02 мая 2009

Edit:

Для приложения с графическим интерфейсом "нормальный" способ справиться с этим в разработке для Windows - отправить сообщение WM_CLOSE в главное окно процесса.

Для консольного приложения необходимо использовать SetConsoleCtrlHandler , чтобы добавить CTRL_C_EVENT.

Если приложение не соблюдает это, вы можете вызвать TerminateProcess .

5 голосов
/ 04 января 2017

Вот код, который я использую в своем приложении C ++.

Положительные баллы:

  • Работает из консольного приложения
  • Работает из службы Windows
  • Никаких задержек не требуется
  • Не закрывает текущее приложение

Отрицательные баллы:

  • Основная консоль потеряна и создана новая (см. FreeConsole )
  • Переключение консоли дает странные результаты ...

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

Пример использования:

DWORD dwProcessId = ...;
if (signalCtrl(dwProcessId, CTRL_C_EVENT))
{
    cout << "Signal sent" << endl;
}
4 голосов
/ 25 апреля 2017
        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);
            }
        }
3 голосов
/ 16 марта 2017

В Java используется JNA с библиотекой Kernel32.dll, аналогично решению C ++. Запускает основной метод CtrlCSender как Процесс, который просто получает консоль процесса для отправки события Ctrl + C и генерирует событие. Поскольку он запускается отдельно без консоли, событие Ctrl + C не нужно отключать и включать снова.

CtrlCSender.java - На основе Nemo1024 и KindDragon's ответов.

При известном идентификаторе процесса это консольное приложение подключит консоль целевого процесса и сгенерирует на ней событие CTRL + C.

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

Основное приложение - запускает CtrlCSender как отдельный непрерывный процесс

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();
3 голосов
/ 26 марта 2015

Это должно быть сделано кристально ясно, потому что на данный момент это не так. Существует модифицированная и скомпилированная версия SendSignal для отправки Ctrl-C (по умолчанию отправляются только Ctrl + Break). Вот несколько двоичных файлов:

(2014-3-7): я создал как 32-битную, так и 64-битную версию с помощью Ctrl-C, она называется SendSignalCtrlC.exe, и вы можете скачать ее по адресу: https://dl.dropboxusercontent.com/u/49065779/sendsignalctrlc/x86/SendSignalCtrlC.exe https://dl.dropboxusercontent.com/u/49065779/sendsignalctrlc/x86_64/SendSignalCtrlC.exe - Юрай Михалак

Я также отразил эти файлы на всякий случай:
32-разрядная версия: https://www.dropbox.com/s/r96jxglhkm4sjz2/SendSignalCtrlC.exe?dl=0
64-битная версия: https://www.dropbox.com/s/hhe0io7mcgcle1c/SendSignalCtrlC64.exe?dl=0

Отказ от ответственности: я не создавал эти файлы. Не было внесено никаких изменений в скомпилированный оригинальные файлы. Единственная протестированная платформа - это 64-битная Windows 7. Рекомендуется адаптировать источник, доступный по http://www.latenighthacking.com/projects/2003/sendSignal/, и скомпилировать его самостоятельно.

1 голос
/ 02 апреля 2019

Да. Проект windows-kill делает именно то, что вы хотите:

windows-kill -SIGINT 1234
...