Необходимо отправить CTRL+C (SIGINT) в объект Process из основного приложения C# WPF

У меня есть приложение, в котором я запускаю несколько объектов Process, перенаправляя вывод на запуск событий. Эти процессы должны иметь возможность работать бесконечно долго, но я также хочу иметь возможность подать им сигнал о корректном завершении (например, завершить какое-либо дело, которое у них есть, а затем завершиться). В целях тестирования я использую tracert. Вот как я создаю и запускаю процессы:

//Create
this.process = new Process();
this.process.StartInfo.FileName = "tracert.exe";
this.process.StartInfo.Arguments = "google.com";
this.process.StartInfo.UseShellExecute = false;
this.process.StartInfo.CreateNoWindow = true;
this.process.StartInfo.RedirectStandardOutput = true;
this.process.StartInfo.RedirectStandardError = true;
this.process.StartInfo.RedirectStandardInput = true;
this.process.EnableRaisingEvents = true;
this.process.OutputDataReceived += Process_OutputDataReceived;
this.process.Exited += Process_Exited;
...
//Start
new Thread(() =>
{
  Thread.CurrentThread.IsBackground = true;
  this.process.Refresh();
  this.process.Start();
  this.process.BeginOutputReadLine();
  this.process.WaitForExit();
}).Start();

Основываясь на том, что я прочитал, я понимаю, что способ отправки сигнала ctrl+c следующий:

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GenerateConsoleCtrlEvent(ConsoleCtrlEvent sigevent, int dwProcessGroupId);

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

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

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

delegate Boolean ConsoleCtrlDelegate(uint CtrlType);

public enum ConsoleCtrlEvent
{
  CTRL_C = 0,
  CTRL_BREAK = 1,
  CTRL_CLOSE = 2,
  CTRL_LOGOFF = 5,
  CTRL_SHUTDOWN = 6
}

private void StopProcess {
  if (AttachConsole((uint)this.process.Id))
  {
    SetConsoleCtrlHandler(null, true);
    GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
    FreeConsole();
    SetConsoleCtrlHandler(null, false);
  }
}

Но это, кажется, не работает. Кажется, что он немедленно запускает событие Exited процесса (с отрицательным кодом выхода из tracert), но вывод tracert продолжает отображаться до завершения, а затем событие Exited процесса запускается во ВТОРОЙ раз, на этот раз с нулевым кодом выхода. Если я вызову функцию StopProcess один раз, а затем вызову ее второй раз, пока tracert продолжает выполнять свою работу, все приложение закроется.

Я создаю основное приложение с помощью WPF, ориентированного на платформу .NET 5.0. Любая помощь будет принята с благодарностью!


person Razz    schedule 15.02.2021    source источник


Ответы (1)


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

После расследования мне кажется, что это вызвано состоянием гонки между вызовом GenerateConsoleCtrlEvent() и последующим вызовом SetConsoleCtrlHandler(). Кажется, что если эти вызовы происходят слишком быстро, Ctrl+C, отправленный GenerateConsoleCtrlEvent(), остается видимым для обработки по умолчанию в приложении WPF, что приводит к завершению процесса с код STATUS_CONTROL_C_EXIT (т.е. нормальный результат нажатия Ctrl+C, но для неправильного процесса).

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

Если изменить код таким образом, чтобы обработка сигнала восстанавливалась до освобождения подключенной консоли (т. е. так, как обычно можно написать код), то проблема с хост-процессом, получающим сигнал и закрывающимся, воспроизводит первый время вызова метода. т.е. кажется, что единственная причина, по которой он даже работает в первый раз, заключается в том, что при первом вызове хост-процессом функции FreeConsole() происходит некоторая задержка, которой достаточно, чтобы сигнал остался незамеченным. Во второй раз задержки больше нет (возможно, что-то закешировалось в слое p/invoke… Я не удосужился исследовать эту часть).

Однако после этого он работает так же, как если бы вы восстановили состояние в ожидаемом порядке.

В любом случае…

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

Я рекомендую вам исправить код аналогичным образом. Обратите внимание, что вы не можете вызвать WaitForExit() для самого Process, если только вы не сделаете это из какого-либо потока, отличного от потока пользовательского интерфейса, потому что класс Process использует поток пользовательского интерфейса для создания события Exited, и поэтому блокировка потока пользовательского интерфейса вызовом WaitForExit() приведет к вызвать тупик. Вы могли бы избежать этого, поместив весь вызов StopProcess() в другой поток, но мне это кажется излишним, особенно когда есть более элегантный способ реализовать все это.

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

В классе окна (обратите внимание, полностью не работает для WPF, поскольку здесь вообще нет MVVM… это было просто для того, чтобы получить базовый минимальный, полный пример):

private Process _process;
private TaskCompletionSource _processTask;

private async void startButton_Click(object sender, RoutedEventArgs e)
{
    startButton.IsEnabled = false;
    stopButton.IsEnabled = true;

    try
    {
        _process = new Process();
        _processTask = new TaskCompletionSource();

        _process.StartInfo.FileName = "tracert.exe";
        _process.StartInfo.Arguments = "google.com";
        _process.StartInfo.UseShellExecute = false;
        _process.StartInfo.CreateNoWindow = true;
        _process.StartInfo.RedirectStandardOutput = true;
        _process.StartInfo.RedirectStandardError = true;
        _process.StartInfo.RedirectStandardInput = true;
        _process.EnableRaisingEvents = true;
        _process.OutputDataReceived += (_, e) => _WriteLine($"stdout: \"{e.Data}\"");
        _process.ErrorDataReceived += (_, e) => _WriteLine($"stderr: \"{e.Data}\"");
        _process.Exited += (_, _) =>
        {
            _WriteLine($"Process exited. Exit code: {_process.ExitCode}");
            _processTask.SetResult();
        };

        _process.Start();
        _process.BeginOutputReadLine();
        _process.BeginErrorReadLine();

        await _processTask.Task;
    }
    finally
    {
        _process?.Dispose();
        _process = null;
        _processTask = null;
        startButton.IsEnabled = true;
        stopButton.IsEnabled = false;
    }
}

private async void stopButton_Click(object sender, RoutedEventArgs e)
{
    try
    {
        await Win32Process.StopProcess(_process, _processTask.Task);
    }
    catch (InvalidOperationException exception)
    {
        _WriteLine(exception.Message);
    }
}

private void _WriteLine(string text)
{
    Dispatcher.Invoke(() => consoleOutput.Text += $"{text}{Environment.NewLine}");
}

Вот обновленная версия метода StopProcess() (который я поместил в собственный вспомогательный класс):

public static async Task StopProcess(Process process, Task processTask)
{
    if (AttachConsole((uint)process.Id))
    {
        // NOTE: each of these functions could fail. Error-handling omitted
        // for clarity. A real-world program should check the result of each
        // call and handle errors appropriately.
        SetConsoleCtrlHandler(null, true);
        GenerateConsoleCtrlEvent(ConsoleCtrlEvent.CTRL_C, 0);
        await processTask;
        SetConsoleCtrlHandler(null, false);
        FreeConsole();
    }
    else
    {
        int hresult = Marshal.GetLastWin32Error();
        Exception e = Marshal.GetExceptionForHR(hresult);

        throw new InvalidOperationException(
            $"ERROR: failed to attach console to process {process.Id}: {e?.Message ?? hresult.ToString()}");
    }
}

Вероятно, вы можете догадаться, что такое XAML — всего пара кнопок и TextBlock для отображения сообщений — но для полноты картины все равно вот:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>

    <Button x:Name="startButton" Grid.Row="0" Grid.Column="0" Content="Start" Click="startButton_Click"/>
    <Button x:Name="stopButton" Grid.Row="0" Grid.Column="1" Content="Ctrl-C" Click="stopButton_Click" IsEnabled="False"/>
    <ScrollViewer Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3">
      <TextBlock x:Name="consoleOutput"/>
    </ScrollViewer>
  </Grid>
</Window>
person Peter Duniho    schedule 15.02.2021