Итак, необходимо перехватить в реальном времени вывод консольной программы, которая запущена собственным приложением в фоновом режиме, например, как это делает GUI Openvpn для Windows.
На моделировании формы приложения особо останавливаться не буду, сделаем там элемент управления для вывода данных, организуем возможность выбора консольного приложения или BAT/CMD-файла, вывод которого будем перехватывать, кнопки для запуска, останова внешнего приложения, и кнопку для очистки лога.
Замечу только, что для вывода перехваченных сообщений использовал немного модифицированный ListView
(копия)
Главное окно основной программы
Теперь надо подготовить несколько приложений для теста, одно будет консольное приложение на C#, которое генерирует случайное число, выводит его на консоль, ожидает 250 мс, и так повторяет в бесконечном цикле:
Бинарник testapp.exe
Исходный код
И несколько вариантов BAT-файлов, реализующий тот же функционал, только время ожидания составляет 1 секунду.
Правда с BAT-файлами есть пока нерешенный вопрос, некоторые, а именно в которых содержатся команды timeout
и choice
, при перехвате работают нестабильно. Вариант с choice
зависает, начиная нагружать процессор, timeout
— не выполняет свои функции, т.е. никакого таймаута, а потом зависает. Пока не понял, с чем это связано, но включил эти файлы отдельно, в комплект к тестовым
1. В более современных версиях .NET можно использовать оператор async
, но я решил обойтись без него, т.к. все возможности для перехвата данных с консоли были еще в .NET Framework 2.0. были, а слова такого не было. Как в анекдоте про Вовочку.
2. Запускать вызываемую консольную программу следует в отдельном потоке, с которым мы будем общаться как обычно — с помощью событий. Иначе чуда не получится — все будет висеть и не работать.
Т.е алгоритм примерно понятен — надо запустить вызываемую консольную программу в отдельном потоке, а потом рулить основной программой, перехватывая данные при наличии события (т.е вывода запущенной программой новых данных).
Создадим новый класс, например Runner
:
public class Runner
Выше класса добавляем публичный делегат (он нам потом для события понадобится)
public delegate void LogMessage(string Data);
Подключаем нужные пространства имен:
using System.Threading;
using System.Diagnostics;
Внутри класса заводим публичное свойство, определяющее путь к процессу:
public string ProcessPath { get; set; }
Заводим событие, которое будем генерировать при перехвате сообщения с консоли.
public event LogMessage LogSend;
И приватные переменные типов ProcessStartInfo
, Process
, для управления вызываемым процессом и типа Thread
, для управления потоком, в котором будет запущен дочерний процесс.
private ProcessStartInfo Info = null;
private Process Proc = null;
private Thread t = null;
В конструкторе инициализируем переменную Info
:
public Runner(string processpath) { ProcessPath = processpath; //устанавливаем значение свойства ProcessPath Info = new ProcessStartInfo(ProcessPath); Info.RedirectStandardError = true; //перехват STDERR Info.RedirectStandardOutput = true; //перехват STDOUT Info.UseShellExecute = false; //иначе перехват работать не будет, см. MSDN Info.CreateNoWindow = true; //не запускать процесс в новом окне //это скроет консоль запускаемой программы }
Примечание: Присвоение свойству ShellExecute
значения false
позволяет перенаправлять потоки ввода, вывода и ошибки.
В функции StartProcess()
непосредственно выполняем запуск процесса и перехват.
try { Proc = Process.Start(Info); } catch (Exception ex) { LogSend("INTERNAL ERROR: "+ex.Message); return; }
Запускаем процесс, если произошла ошибка, вызываем событие, ответственное за отправку сообщения и выходим.
Для перехвата создаем цикл, в котором вызываем Process.StandardOutput.ReadLine()
, пока результат не будет равен null
.
StandardOutput.ReadLine()
будет ждать, пока программа не выведет на консоль строку, и тогда вернет ее, или же вернет null
, когда программа завершится.
string ConOut = ""; do { ConOut = Proc.StandardOutput.ReadLine(); if (ConOut != null) { LogSend(ConOut); } } while (ConOut != null);
Поскольку запуск этой функции вызовет «зависание», если запустить ее в основном потоке, то вызывать ее нужно в отдельном:
public void Start() { t = new Thread(StartProcess); t.Start(); }
Ну и функция для остановки вызванного процесса:
public void Stop() { if (Proc != null) { Proc.Kill(); Proc = null; } if (t != null) { t.Abort(); t = null; } }
В коде основной формы создаем объект Runner
, регистрируем обработчик событий, запускаем перехват, не забывая про останов по нажатию нужной кнопки:
Runner runner = null; //... private void btnStart_Click(object sender, EventArgs e) { runner = new Runner(txtPath.Text); runner.LogSend += new LogMessage(runner_LogSend); runner.Start(); } private void btnStop_Click(object sender, EventArgs e) { if (runner != null) { runner.Stop(); } }
В обработчике события отправляем данные в нужный элемент управления, не забывая про Invoke
:
void runner_LogSend(string Data) { Invoke((MethodInvoker)delegate { lvConsole.Items.Add(Data); lvConsole.EnsureVisible(lvConsole.Items.Count - 1); }); }