Я могу воспроизвести вторую проблему, о которой вы сообщаете, то есть процесс 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