Версия для печати


StdIn, StdOut и StdErr. Перенаправление, чтение и запись.
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=736

Сергей Горбань
дата публикации 23-12-2002 18:26

StdIn, StdOut и StdErr. Перенаправление, чтение и запись.

Вообще - я программист молодой, стаж - всего 2 года. И я никак не ожидал, что в век GDI мне придется возится с консолью... Ан нет, пришлось.

Начал писать "движок" для собственного сайта. А именно - "Apache 1.x shared module" (dll - линкуется к Апачу и обрабатывает определенные адреса).

Написал. Всего три сотни строк. НО умеет кучу всяких полезностей, типа вставлять на страницы данные из файлов (файл в файл), строки и, главное, данные из БД. Все это прекрасно. НО не умеет вставлять результаты работы других файлов (типа как CGI). Ну, думаю, надо сделать.
Ага, а как? Вот тут то все и началось...

Итак,
ЗАДАЧА:
запустить процесс (некий файл), передать ему команды и получить от него результаты работы. Вставить полученные результаты на страницу сайта.
Причем в целях совместимости механизмы передачи данных ДОЛЖНЫ быть стандартными - StdIn, StdOut, StdErr.
Поискал на КД. Нашел вот такую штуку: Как переназначить StdOut в файл для консольной программы запускаемой по CreateProcess

Хорошая статья, но мне-то НЕ в ФАЙЛ, а в ПРОГРАММУ надо!
Автор (Спасибо ему!!!) предусмотрительно указал ссылки на полезные раздел справки - "Creating a Child Process with Redirected Input and Output".
Лезем туда. Ууууух... Круто. В общем, каких-то два дня ковыряния и вуаля!

Работает!

Получился небольшой такой класс... За кривизну некоторых мест, типа отсутствия проверок - НЕ бить! (по крайней мере ногами :-)). Кому надо - тот сам вставит. (Вот так и рождается "кривой" код. Типа сейчас лень, потом добавлю... Ага... Через час уже забудешь!!!)

В общем - перехожу таки к технике дела.

Для передачи данных используются "безымянные" (Anonymus) "каналы" (Pipes). Чтобы заставить программу писать в (читать из) канал (а) - просто подменяем соответствующие Std(In, Out, Err). Программа и знать не будет, что ее данные уходят в "трубу" а не на реальную консоль.

При создании каналов есть одна ВАЖНАЯ особенность. Создаем-то мы их в своем процессе (Parent) а использовать будем и в дочернем. (Учтите! дочерний процесс НЕ будет знать, что использует КАНАЛ! НО будет его использовать...). Так, вот, чтобы дочерний процесс мог нормально работать - хэндлы канала должны быть НАСЛЕДУЕМЫМИ.
Чтобы это обеспечить - надо правильно заполнить структуру SECURITY_ATTRIBUTES используемую при вызове CreatePipe:

    New(FsaAttr);
    FsaAttr.nLength:=SizeOf(SECURITY_ATTRIBUTES);
    FsaAttr.bInheritHandle:=True;
    FsaAttr.lpSecurityDescriptor:=Nil;

Заполнили? Молодцы! Теперь создаем каналы (я делаю только два, StdErr мне не нужен):

If not CreatePipe(FChildStdoutRd, FChildStdoutWr, FsaAttr, 0) Then 
//Создаем "читальный" Pipe
      raise ECreatePipeErr.CreateRes(@sCreatePipeMsg)
    Else
      If not CreatePipe(FChildStdinRd, FChildStdinWr, FsaAttr, 0) Then 
//Создаем "писальный" Pipe
        raise ECreatePipeErr.CreateRes(@sCreatePipeMsg) 

Создали? Если нет - то дальше ловить нечего, поэтому генерим Exception'ы...
Есть еще одна тонкость. У нас Все созданные хэндлы наследуемые! А дочернему процессу понадобятся только два... Поэтому:

//Делаем НЕ наследуемые дубликаты
//Это нужно, чтобы не тащить лишние хэндлы в дочерний процесс...
          If not DuplicateHandle(GetCurrentProcess(), FChildStdoutRd,
GetCurrentProcess(), @Tmp1, 0, False, DUPLICATE_SAME_ACCESS) Then
            raise EDuplicateHandleErr.CreateRes(@sDuplicateHandleMsg)
          Else
            If not DuplicateHandle(GetCurrentProcess(), FChildStdinWr,
GetCurrentProcess(), @Tmp2, 0, False, DUPLICATE_SAME_ACCESS) Then
              raise EDuplicateHandleErr.CreateRes(@sDuplicateHandleMsg)

Дубликаты у нас в Tmp1 и Tmp2, теперь:

CloseHandle(FChildStdoutRd);//Закроем наследуемый вариант "Читального" хэндла
CloseHandle(FChildStdinWr); //Закроем наследуемый вариант "Писального" хэндла
FChildStdoutRd:=Tmp1;       //И воткнем их места НЕ наследуемые дубликаты
FChildStdinWr:=Tmp2;        //И воткнем их места НЕ наследуемые дубликаты

Ура! Теперь можем создавать дочерний процесс!

    If not CreateChildProcess(ExeName, CommadLine, FChildStdinRd,
FChildStdoutWr) Then  //Наконец-то! Создаем дочерний процесс!
      raise ECreateChildProcessErr.CreateRes(@sCreateChildProcessMsg)

Причем CreateChildProcess - это не API - это моя функция! Вот она:

//*************************************************************************
function TChildProc.CreateChildProcess(ExeName, CommadLine: String; StdIn,
  StdOut: THandle): Boolean;
Var
  piProcInfo: TProcessInformation;
  siStartInfo: TStartupInfo;
begin
// Set up members of STARTUPINFO structure.
  ZeroMemory(@siStartInfo, SizeOf(TStartupInfo));
  siStartInfo.cb:=SizeOf(TStartupInfo);
  siStartInfo.hStdInput:=StdIn;
  siStartInfo.hStdOutput:=StdOut;
  siStartInfo.dwFlags:=STARTF_USESTDHANDLES;
// Create the child process.
   Result:=CreateProcess(Nil,
      PChar(ExeName+' '+CommadLine),       // command line
      Nil,          // process security attributes
      Nil,          // primary thread security attributes
      TRUE,          // handles are inherited
      0,             // creation flags
      Nil,          // use parent's environment
      Nil,          // use parent's current directory
      siStartInfo,  // STARTUPINFO pointer
      piProcInfo);  // receives PROCESS_INFORMATION
end;    
//*************************************************************************

Здесь важное значение имеют вот эти строчки:
      siStartInfo.hStdInput:=StdIn;
      siStartInfo.hStdOutput:=StdOut;
      siStartInfo.dwFlags:=STARTF_USESTDHANDLES;
Первые две - понятно. А третья - читайте Хелп! Там все написано...

Самые умные (то есть те, кто ухитрился дочитать до этого места :-))) спросят:

- Ну, создали мы процесс и что дальше?

А дальше - мы можем с ентим процессом общаться! Например вот так:

//*************************************************************************
function TChildProc.WriteToChild(Data: String; Timeout: Integer=1000):
Boolean;
Var
        dwWritten, BufSize: DWORD;
  chBuf: PChar;
begin
//Обратите внимание на Chr($0D)+Chr($0A)!!! Без них - будет работать с ошибками
//На досуге - подумайте почему...
//Для тех, кому думать лень - подскажу - это пара символов конца строки.
//(вообще-то можно обойтись одним, но так надежнее, программы-то бывают разные)
  chBuf:=PChar(Data+Chr($0D)+Chr($0A));
  BufSize:=Length(chBuf);
  Result:=WriteFile(FChildStdinWr, chBuf^, BufSize, dwWritten, Nil);
  Result:=Result and (BufSize = dwWritten);
end;    
//*************************************************************************

Это мы посылаем данные на StdIn процесса.

Читать - несколько сложнее. Нам же не надо вешать всю нашу программу только потому, что процесс не желает нам ничего сообщать??? А ReadFile - функция синхронная и висит - пока не прочитает! Если заранее известно, чего и сколько ДОЛЖЕН выдать процесс, то еще ничего... А если нет?

А если нет - делаем хитрый финт ушами :-) Есть у Мелко-Мягких такая ф-ия PeekNamedPipe. Не покупайтесь, на то, что она "Named" - фигня! Она прекрасно работает а анонимными пайпами! (кто не верит - можете почитать хелп)

Поэтому делаем так:

//*************************************************************************
function TChildProc.ReadStrFromChild(Timeout: Integer): String;
Var
  i: Integer;
  dwRead, BufSize, DesBufSize: DWORD;
  chBuf: PChar;
  Res: Boolean;
begin
  Try
    BufSize:=0;
    New(chBuf);
    Repeat
      For i:=0 to 9 do
        begin
          Res:=PeekNamedPipe(FChildStdoutRd, nil, 0, nil, @DesBufSize, nil);
          Res:=Res and (DesBufSize > 0);
          If Res Then
            Break;
          Sleep(Round(Timeout/10));
        end;
      If Res Then
        begin
          If DesBufSize > BufSize Then
            begin
              FreeMem(chBuf);
              GetMem(chBuf, DesBufSize);
              BufSize:=DesBufSize;
            end;
          Res:=ReadFile(FChildStdoutRd, chBuf^, BufSize, dwRead, Nil);
          Result:=Result+LeftStr(chBuf, dwRead);
        end;
    Until not Res;
  Except
    Result:='Read Err';
  End;
end;
//*************************************************************************

Ну, вот, как я и говорил - работает. Даже слишком хорошо. Как я и говорил - эта вся бодяга для Web сервера. Ну, беру я в качестве файла - format.exe.... Ндаааа....

Если честно - с format'ом я не прверял - а вот help c парметрами и "net use" прошли на ура! Так что пришлось резко думать, как ограничить список разрешенных для запуска программ....

В общем, кому лень разбираться - вот вам исходники модуля с готовым классом. А вот пример его использования:

/*************************************************************************
  With TChildProc.Create(ReadIni(TagParams.Values['file'], FPage),
TagParams.Values['cmd']) do
    Try
      WriteToChild(TagParams.Text);
      ReplaceText:=ReadStrFromChild;
    Finally
      Free;
    End;
//*************************************************************************

Не правда ли просто?

Горбань С.В.



К материалу прилагаются файлы: