Андрей Колесов

Одна из обычных задач, возникающих при разработке ПО, - запуск и отслеживание состояния внешних программ. Традиционно при программировании в Windows для этого приходилось использовать средства Win API. Функции, появившиеся в технологии .NET, существенно упрощают решение данной задачи. Рассмотрим эти новые возможности на примере VB.NET.

Знакомство с классом Process

В классическом Visual Basic запуск внешних приложений выполнялся с помощью функции Shell, например, так:

ReturnID = Shell ("calc.exe", vbNormalFocus)

Эта функция в .NET была несколько улучшена по сравнению с VB 6.0, и ею по-прежнему можно пользоваться, однако ее возможности весьма ограниченны, прежде всего из-за того, что вызываемое приложение запускается в асинхронном режиме.

Вместо этого для работы с внешними программами в .NET лучше использовать класс Process, находящийся в пространстве имен System.Diagnostics. В простейшем случае запуск внешней программы будет выполняться с помощью метода Start; в данном примере для обработки указанного файла будет запускаться текстовый редактор (обычно это NotePad, установленный по умолчанию):

System.Diagnostics.Process.Start _
  ("c:\MYPATH\MYFILE.TXT")

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

Dim myProcess As Process = _
  Process.Start("c:\MYPATH\MYFILE.TXT")
MsgBox(myProcess.ProcessName)

Для управления параметрами запускаемого процесса можно воспользоваться объектом ProcessStartInfo из того же пространства имен:

Dim psInfo As New _
  System.Diagnostics.ProcessStartInfo _
  ("c:\mypath\myfile.txt")
' устанавливаем стиль открываемого окна
psInfo.WindowStyle = _
  System.Diagnostics.ProcessWindowStyle.Normal
Dim myProcess As Process = _
  System.Diagnostics.Process.Start(psInfo)

Объект ProcessStartInfo можно также получить с помощью свойства Process.Start:

Dim myProcess As _
  System.Diagnostics.Process = _
  New System.Diagnostics.Process()
myProcess.StartInfo.FileName = _
  "c:\mypath\myfile.txt"
myProcess.StartInfo.WindowStyle = _
  System.Diagnostics.ProcessWindowStyle.Normal
myProcess.Start()

Предварительную установку параметров запускаемого процесса (имя файла, стиль окна и т. п.) можно выполнять и в режиме разработки (Design Time) через компонент Process, который следует добавить к форме из раздела Components панели Toolbar. В этом случае все параметры свойства StartInfo можно будет вводить в окне Properties (рис. 1).

Fig.1 Рис. 1. Управлять параметрами объекта Process можно в среде разработки.

Запуск процесса и ожидание его завершения

Самый простой способ ожидания завершения запущенного процесса - использовать метод Process.WaitForExit (однако нужно иметь в виду, что, когда вы применяете его из Windows Form, форма перестает реагировать на некоторые системные события, например, Close):

' создание нового процесса
Dim myProcess As Process = _
  System.Diagnostics.Process.Start _
  ("c:\mypath\myfile.txt")
' Ожидание его завершения
myProcess.WaitForExit()
' вывод результатов
MessageBox.Show _
  ("Notepad был закрыт в: " & _
  myProcess.ExitTime & "." & _
  System.Environment.NewLine & _
  "Код завершения: " & _
  myProcess.ExitCode)
' закрытие процесса
myProcess.Close()

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

Метод WaitForExit можно использовать для ожидания завершения запущенного процесса в течение заданного интервала времени, а метод Kill - для его аварийного завершения:

' Ожидание в течение 5 с
myProcess.WaitForExit(5000)
' Если процесс не завершился, то мы 
' аварийно завершаем его
If Not myProcess.HasExited Then
   myProcess.Kill()
   ' все же ждем его завершения
   myProcess.WaitForExit()
End If

Обратите внимание: после выполнения метода Kill мы опять ожидаем завершения процесса. Это нужно сделать, если мы хотим затем получить информацию о времени завершения и код завершения, - иначе при обращении к свойствам ExitTime или ExitCode будет выдана программная ошибка, так как запущенный процесс еще не успеет закончиться.

Запуск скрытого процесса

Довольно часто разработчику вообще не нужно, чтобы внешний процесс отражался в окне на экране монитора. Типичный случай - выполнение какой-то операции в сеансе MS-DOS, например, получение списка файлов заданного каталога. Приведенный ниже код выполняет подобную операцию без создания окна для запущенного процесса. Обратите внимание на содержание командной строки: Windows XP распознает "&&" как разделитель команд, позволяя записывать в одну строку сразу несколько команд.

Dim myProcess As Process = New Process()
' имя выходного файла
Dim outfile As String = _
  Application.StartupPath & _
  "\dirOutput.txt"

  ' адрес системного каталога
Dim sysFolder As String = _
  System.Environment.GetFolderPath _
  (Environment.SpecialFolder.System)

 ' Имя запускаемой программы 
 ' и строка аргументов
 myProcess.StartInfo.FileName = "cmd.exe"
 myProcess.StartInfo.Arguments = _
    "C cd " & sysFolder & _
    " && dir *.com >> " & Chr(34) & _
    outfile & Chr(34) & " && exit"

 ' запуск процесса в скрытом окне
 myProcess.StartInfo.WindowStyle = _
   ProcessWindowStyle.Hidden
 myProcess.StartInfo.CreateNoWindow = True
 myProcess.Start()

 myProcess.WaitForExit() 'ожидание

Определение момента завершения процесса

Как мы видели в предыдущем примере, метод WaitForExit блокирует все события приложения, которое инициировало внешний процесс. Чтобы дать возможность работать главной программе, используется конструкция, которая в цикле проверяет состояние внешнего процесса и тут же вызывает метод Application.DoEvents, обеспечивающий обработку других событий данного приложения:

Do While Not myProcess.HasExited
  Application.DoEvents
Loop

Кстати, эффект, получаемый в данном случае при опросе свойства HasExit, в VB 6.0 можно было получить, обратившись к функции Win32 API GetModuleUsage.

Однако с точки зрения минимизации загрузки процессора более эффективный по сравнению с предыдущим примером вариант - инициализация события Exited класса Process. При этом нужно установить свойство Process.EnableRaisingEvents равным True (по умолчанию оно равно False) и создать манипулятор события, включая процедуру обработки события:

Private Sub Button4_Click _
  (ByVal sender As System.Object, _
   ByVal e As System.EventArgs) _
   Handles Button4.Click
  
  Dim myProcess As Process = New Process()
  myProcess.StartInfo.FileName = _
    "c:\mypath\myfile.txt"
  ' разрешаем процессу
  ' инициализировать событие
  myProcess.EnableRaisingEvents = True
  ' Добавить манипулятор события Exited 
  AddHandler myProcess.Exited, _
    AddressOf Me.ProcessExited
  ' запуск процесса
  myProcess.Start()
End Sub

Friend Sub ProcessExited _
  (ByVal sender As Object, _
   ByVal e As System.EventArgs)
   ' процедура обработки события 
   ' (завершение процесса)
  Dim myProcess As Process = _
    DirectCast(sender, Process)
  MessageBox.Show _
   ("Процесс завершился в" & _
    myProcess.ExitTime & _
    System.Environment.NewLine & _
    "Код завершения: " & _
    myProcess.ExitCode)
  myProcess.Close()
End Sub

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

Обмен информацией с внешним процессом

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

Для вызываемых программ, которые поддерживают механизмы StdIn, StdOut и StdErr (например, консольных приложений), можно использовать объекты StreamWriter и StreamReader для записи и чтения данных. Чтобы это сделать, нужно установить свойства RedirectStandardInput, RedirectStandardOutput и RedirectStandardError объекта ProcessStartInfo равными True. Затем, после запуска внешнего процесса, нужно использовать свойства StandardInput, StandardOutput и StandardError объекта Process для привязки потока ввода-вывода к объектам StreamReader и StreamWriter.

Еще одно замечание: по умолчанию среда .NET Framework использует функцию Win32 ShellExecute для взаимодействия с внешним процессом, но когда вы переопределяете потоки ввода-вывода, перед запуском приложения нужно установить свойство ProcessStartInfo.UseShellExecute равным False.

В приведенном ниже примере создается невидимое окно, формируется список файлов заданного каталога, а результаты выводятся в окне MessageBox (рис. 2)

Fig.2 Рис. 2. Информация, полученная из вызванного процесса через объект StdOut.

' обмен данными с внешним процессом через 
' функции StdIn, StdOut и StdErr
Dim s As String
Dim myProcess As Process = New Process()
' описание и запуск процесса
myProcess.StartInfo.FileName = "cmd.exe"
myProcess.StartInfo.UseShellExecute = False
myProcess.StartInfo.CreateNoWindow = True
myProcess.StartInfo.RedirectStandardInput = True
myProcess.StartInfo.RedirectStandardOutput = True
myProcess.StartInfo.RedirectStandardError = True
myProcess.Start()

' описание объектов передачи данных 
Dim sIn As System.IO.StreamWriter = _
  myProcess.StandardInput
sIn.AutoFlush = True
Dim sOut As System.IO.StreamReader = _
  myProcess.StandardOutput
Dim sErr As System.IO.StreamReader = _
  myProcess.StandardError
' передача входных данных 
sIn.Write("dir c:\drv\*.*" & _
   System.Environment.NewLine)
sIn.Write("exit" & _
   System.Environment.NewLine)
' получаем результат выполнения команды DIR
s = sOut.ReadToEnd()
If Not myProcess.HasExited Then
  myProcess.Kill()
End If

MessageBox.Show("Окно команды 'dir' " & _
  "было закрыто в: " & _
  myProcess.ExitTime & "." & _
  System.Environment.NewLine & _
  "Код завершения: " & _
  myProcess.ExitCode)

sIn.Close()
sOut.Close()
sErr.Close()
myProcess.Close()

' смотрим результат работы процесса DIR
MessageBox.Show(s)

Если вызываемое приложение не использует StdIn, можно применить метод SendKeys для передачи данных, вводимой с клавиатуры. Например, следующий код вызывает NotePad и вводит в него некоторый текст:

Dim myProcess As Process = New Process()
myProcess.StartInfo.FileName = "notepad"
myProcess.StartInfo.WindowStyle = _
   ProcessWindowStyle.Normal
myProcess.Start()

' Ждем 1 с, чтобы NotePad был готов
' к вводу данных
myProcess.WaitForInputIdle(1000)
if myProcess.Responding Then
  System.Windows.Forms.SendKeys.SendWait( _
  "Этот текст был введен " & _
  "с помощью метода " & _
  "System.Windows.Forms.SendKeys.")
Else
  myProcess.Kill()
End If

Метод SendKeys позволяет передавать коды любых клавиш, включая Alt, Ctrl и Shift. Таким образом можно передавать комбинации клавиш для выполнения стандартных команд, например, загрузки или сохранения файлов, управления командами меню и т. п. Но нужно помнить, что этот метод посылает код только в активное окно приложения, и если нужное окно потеряет фокус, могут возникнуть проблемы. Именно поэтому мы использовали метод Process.WaitForInputIdle, чтобы проверить, готово ли приложение к получению информации. Для NotePad времени ожидания в 1 с вполне достаточно, но для других приложений его, возможно, придется увеличить.

* * *

Итак, хотя функция Shell по-прежнему работает в .NET Framework, класс System.Diagnostics.Process предоставляет гораздо больше возможностей для взаимодействия с внешними процессами. Переадресуя потоки StdIn, StdOut и StdErr, можно наладить двусторонний обмен данными с приложением, а применяя метод SendKeys, можно вводить информацию (в том числе управляющие команды меню) в программы, которые не используют StdIn.