1 (изменено: YMP, 2014-10-16 08:35:10)

Тема: 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 сможет завершиться.

Хэндлы каналов закроются также при завершении процесса, что опять же позволит другому процессу прекратить ожидание.

2 (изменено: YMP, 2014-10-16 08:36:56)

Re: AHK: Запуск консольного приложения с перехватом его ввода и вывода

Это вариант той же функции для современных версий AutoHotkey: x86 ANSI, x86 Unicode и x64. В "классическом" AutoHotkey работать не будет, т.к. используются отсутствующие в нём функции.


RunCon(CmdLine, Input, ByRef Output)
{
    static BufSizeChar := 1024, hParent := 0
    static Show := 0, Flags := 0x101  ; STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW
    static Buf, BufSizeByte, ProcessInfo, StartupInfo, PipeAttribs
    static piSize, siSize, paSize, flOffset, shOffset, ihOffset
    static inOffset, outOffset, errOffset, thrOffset
    If (!hParent) {
        BufSizeByte := A_IsUnicode ? BufSizeChar * 2 : BufSizeChar
        If (A_PtrSize = 8) {
            piSize := 24, siSize := 104, paSize = 24
            flOffset := 60, shOffset := 64, ihOffset := 16
            inOffset := 80, outOffset := 88, errOffset := 96
            thrOffset := 8
        }
        Else {
            piSize := 16, siSize := 68, paSize = 12
            flOffset := 44, shOffset := 48, ihOffset := 8
            inOffset := 56, outOffset := 60, errOffset := 64
            thrOffset := 4
        }
        VarSetCapacity(Buf, BufSizeByte, 0),    VarSetCapacity(ProcessInfo, piSize, 0)
        VarSetCapacity(StartupInfo, siSize, 0), VarSetCapacity(PipeAttribs, paSize, 0)
        NumPut(siSize, StartupInfo, 0, "uint"), NumPut(Flags, StartupInfo, flOffset, "uint")
        NumPut(Show, StartupInfo, shOffset, "ushort")
        NumPut(paSize, PipeAttribs, 0, "uint"), NumPut(1, PipeAttribs, ihOffset, "int")
        hParent := DllCall("GetCurrentProcess", "ptr")
    }
    DllCall("CreatePipe", "ptr *", hRead1_tmp, "ptr *", hWrite2
                        , "ptr", &PipeAttribs, "uint", 0)
    DllCall("CreatePipe", "ptr *", hRead2, "ptr *", hWrite1_tmp
                        , "ptr", &PipeAttribs, "uint", 0)

    NumPut(hRead2,  StartupInfo, inOffset, "ptr") 
    NumPut(hWrite2, StartupInfo, outOffset, "ptr")
    NumPut(hWrite2, StartupInfo, errOffset, "ptr")
    
    DllCall("DuplicateHandle", "ptr", hParent, "ptr", hRead1_tmp
                             , "ptr", hParent, "ptr *", hRead1
                             , "uint", 0, "uint", 0
                             , "uint", 2)    ; DUPLICATE_SAME_ACCESS
    DllCall("CloseHandle", "ptr", hRead1_tmp)
    DllCall("DuplicateHandle", "ptr", hParent, "ptr", hWrite1_tmp
                             , "ptr", hParent, "ptr *", hWrite1
                             , "uint", 0, "uint", 0
                             , "uint", 2)
    DllCall("CloseHandle", "ptr", hWrite1_tmp)
    
    DllCall("ExpandEnvironmentStrings", "str", CmdLine, "str", Buf, "uint", BufSizeChar)
    CmdLine := Buf
    Ret := DllCall("CreateProcess", "ptr", 0, "str", CmdLine, "ptr", 0, "ptr", 0
                                  , "uint", 1, "uint", 0, "ptr", 0, "ptr", 0
                                  , "ptr", &StartupInfo, "ptr", &ProcessInfo)
    If (!Ret) {
        MsgBox,, %A_ThisFunc%, Не удалось создать процесс.
        Output := ""
        Return 1
    }
    hChild := NumGet(ProcessInfo, 0, "ptr")
    DllCall("CloseHandle", "ptr", NumGet(ProcessInfo, thrOffset, "ptr"))
    DllCall("CloseHandle", "ptr", hRead2)
    DllCall("CloseHandle", "ptr", hWrite2)
    If (Input) {
        InLen := StrLen(Input) + 2
        VarSetCapacity(InBuf, InLen, 0)
        StrPut(Input . "`r`n", &InBuf, "cp866")
        DllCall("WriteFile", "ptr", hWrite1, "ptr", &InBuf, "uint", InLen
                           , "uint *", BytesWritten, "uint", 0)
    }
    DllCall("CloseHandle", "ptr", hWrite1)
    Output := ""
    Loop {
        If not DllCall("ReadFile", "ptr", hRead1, "ptr", &Buf, "uint", BufSizeByte
                                 , "uint *", BytesRead, "uint", 0)
            Break
        NumPut(0, Buf, BytesRead, "Char")
        Output .= StrGet(&Buf, "cp866")
    }
    DllCall("CloseHandle", "ptr", hRead1)
    DllCall("GetExitCodeProcess", "ptr", hChild, "int *", ExitCode)
    DllCall("CloseHandle", "ptr", hChild)
    Return ExitCode
}