Тема: AHK: Запуск консольного приложения с перехватом его ввода и вывода
Примечание: этот код был написан под "классический" AutoHotkey. Более современный вариант см. в следующем посте.
Идея взята из примера в Platform SDK от MS. Суть перехвата состоит в подмене стандартных потоков ввода и вывода запускаемого приложения на неименованные каналы (anonymous pipes), созданные в родительской программе. Теперь всё, что приложение выводит, будет поступать в канал и считываться родителем, в данном случае скриптом. А то, что скрипт пишет в другой канал, поступит на стандартный ввод дочернего приложения.
Всё это оформлено в виде функции RunCon, которая принимает три аргумента:
1) командная строка запуска программы — с аргументами, если есть;
2) ввод в программу — одна или несколько команд (разделять новой строкой);
3) имя переменной, куда будет помещён вывод программы.
Функция возвращает код выхода дочернего приложения. Если в строке запуска или вводе есть пути с пробелами, их нужно заключать в кавычки.
; Примеры вызова.
CmdLine = cmd.exe
Input = dir
Ret := RunCon(CmdLine, Input, Output)
MsgBox, %Output%
MsgBox, Код выхода: %Ret%
CmdLine = nslookup
Input = ; Ввод нескольких команд.
(
mail.ru
yandex.ru
google.com
)
Ret := RunCon(CmdLine, Input, Output)
MsgBox, %Output%
MsgBox, Код выхода: %Ret%
CmdLine = ping 127.0.0.1 ; Запуск с аргументом, ввода нет.
Input =
Ret := RunCon(CmdLine, Input, Output)
MsgBox, %Output%
MsgBox, Код выхода: %Ret%
; =========================== Функция ============================
RunCon(CmdLine, Input, ByRef Output)
{
Static Buf, BufSize, ProcessInfo, StartupInfo, PipeAttribs, hParent, Flags, Show
If (!hParent) {
Flags := 0x101 ; STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW
Show := 0 ; SW_HIDE
BufSize := 1024
VarSetCapacity(Buf, BufSize, 0), VarSetCapacity(ProcessInfo, 16, 0)
VarSetCapacity(StartupInfo, 68, 0), VarSetCapacity(PipeAttribs, 12, 0)
NumPut(68, StartupInfo, 0), NumPut(Flags, StartupInfo, 44)
NumPut(Show, StartupInfo, 48, "UShort")
NumPut(12, PipeAttribs, 0), NumPut(1, PipeAttribs, 8)
hParent := DllCall("GetCurrentProcess")
}
DllCall("CreatePipe", "uint *", hRead1_tmp, "uint *", hWrite2
, "uint", &PipeAttribs, "uint", 0)
DllCall("CreatePipe", "uint *", hRead2, "uint *", hWrite1_tmp
, "uint", &PipeAttribs, "uint", 0)
NumPut(hRead2, StartupInfo, 56)
NumPut(hWrite2, StartupInfo, 60)
NumPut(hWrite2, StartupInfo, 64)
DllCall("DuplicateHandle", "uint", hParent, "uint", hRead1_tmp
, "uint", hParent, "uint *", hRead1
, "uint", 0, "uint", 0
, "uint", 2) ; DUPLICATE_SAME_ACCESS
DllCall("CloseHandle", "uint", hRead1_tmp)
DllCall("DuplicateHandle", "uint", hParent, "uint", hWrite1_tmp
, "uint", hParent, "uint *", hWrite1
, "uint", 0, "uint", 0
, "uint", 2)
DllCall("CloseHandle", "uint", hWrite1_tmp)
DllCall("ExpandEnvironmentStrings", "str", CmdLine, "str", Buf, "uint", BufSize)
CmdLine := Buf
Ret := DllCall("CreateProcess", "uint", 0, "str", CmdLine, "uint", 0, "uint", 0
, "uint", 1, "uint", 0, "uint", 0, "uint", 0
, "uint", &StartupInfo, "uint", &ProcessInfo)
If (!Ret) {
MsgBox,, %A_ThisFunc%, Не удалось создать процесс.
Output := ""
Return 1
}
hChild := NumGet(ProcessInfo)
DllCall("CloseHandle", "uint", NumGet(ProcessInfo, 4))
DllCall("CloseHandle", "uint", hRead2)
DllCall("CloseHandle", "uint", hWrite2)
If (Input) {
DllCall("CharToOem", "str", Input, "str", Input)
Input .= "`r`n"
DllCall("WriteFile", "uint", hWrite1, "str", Input, "uint", StrLen(Input)
, "uint *", BytesWritten, "uint", 0)
}
DllCall("CloseHandle", "uint", hWrite1)
Output := ""
Loop {
If not DllCall("ReadFile", "uint", hRead1, "uint", &Buf, "uint", BufSize
, "uint *", BytesRead, "uint", 0)
Break
NumPut(0, Buf, BytesRead, "Char")
VarSetCapacity(Buf, -1)
Output .= Buf
}
DllCall("OemToChar", "str", Output, "str", Output)
DllCall("CloseHandle", "uint", hRead1)
DllCall("GetExitCodeProcess", "uint", hChild, "int *", ExitCode)
DllCall("CloseHandle", "uint", hChild)
Return ExitCode
}
Дополнительные пояснения (насколько я сам всё понял). При создании каждого канала создаются два его хэндла — один для чтения, другой для записи. Дочерний процесс создаётся с наследованием хэндлов родителя, но чтобы произошла именно подмена, нужные хэндлы также помещаются в его структуру StartupInfo — читающий хэндл одного канала (hRead2) как стандартный ввод и пишущий другого (hWrite2) как стандарный вывод и стандартный поток ошибок.
Остающиеся у родителя хэндлы hRead1 и hWrite1 перед созданием дочернего процесса дублируются, чтобы получить их ненаследуемые копии, а наследуемые оригиналы уничтожаются, чтобы дочерний процесс их не получил. Кроме того после создания процесса должны быть уничтожены переданные ему хэндлы hRead2 и hWrite2. Поскольку при наследовании происходит дублирование хэндлов, то ему это уже не повредит.
Цель всех этих манипуляций в том, чтобы у канальных хэндлов не было дублей. Смысл же заключается в особенностях работы функций ReadFile и WriteFile при чтении из канала и записи в него. ReadFile ждёт, пока в канале что-то появится. И если там ничего уже не появится, т.к. другое приложение закончило передачу, то получится вечное ожидание. Но если у канала закрыть все пишущие хэндлы, ReadFile завершится, возвратив 0. Что же произойдёт, если одна из копий пишущего хэндла попадёт в сам тот процесс, где работает ReadFile? Закрыть эту копию будет некому, а она не даст закрыться каналу, и ReadFile будет ждать.
Что касается WriteFile, то она может зависнуть, если другой процесс уже прекратил считывание из канала, а буфер канала недостаточно велик, чтобы туда поместились все данные, указанные для записи. WriteFile будет ждать, пока освободится буфер. И опять же, если закрыть все, теперь уже читающие, хэндлы канала, WriteFile сможет завершиться.
Хэндлы каналов закроются также при завершении процесса, что опять же позволит другому процессу прекратить ожидание.