понедельник, 1 декабря 2008 г.

Как бы мне передать большой объём данных процессу при его запуске?

Это перевод How do I pass a lot of data to a process when it starts up? Автор: Реймонд Чен. Примечание: в отличие от других постов, этот пост сильно отличается от оригинала. Произведено множество замен от C к Delphi.

Как мы обсуждали вчера, если вам нужно передать больше, чем 32767 символов в дочерний процесс, вам придётся использовать что-то отличное от командной строки.

Один метод заключается в том, чтобы дождаться, пока дочерний процесс закончит инициализацию, затем найти окно дочернего процесса и отправить данные с помощью сообщения WM_COPYDATA. У этого метода есть несколько проблем:
  • Вам нужен способ, чтобы узнать, что дочерний процесс полностью инициализировался и создал свои окна, прежде чем начать искать окно для передачи сообщения (возможно, вам подойдёт WaitForInputIdle).
  • Вам нужно убедиться, что окно, которое вы нашли, действительно принадлежит вашему дочернему процессу, а не просто случайно имеет то же самое имя. Или, возможно, не только случайно: если запущено более одной копии вашего дочернего процесса, вам лучше бы быть уверенным, что вы собираетесь говорить с нужной копией (здесь вам пригодится GetWindowThreadProcessId).
  • Вам нужно надеятся, что никто больше не сумел найти ваше окно и отправить ему WM_COPYDATA раньше вас (а если они сумели это сделать, это значит, что они сумели угнать ваш дочерний процесс).
  • Дочерний процесс должен предусматривать реакцию на неверные данные, отправленные через WM_COPYDATA.
Метод, который я предпочитаю - использование анонимной разделяемой памяти (shared memory). Его идея заключается в создании блока разделяемой памяти и заполнении его информацией. Затем вы делаете его дескриптор наследуемым и создаёте дочерний процесс, передавая дескриптор разделяемой памяти параметром в командной строке. Дочерний процесс анализирует параметры командной строки, находит там дескриптор, открывает блок разделяемой памяти и читает оттуда данные.

Замечания об этом методе:
  • Вам нужно аккуратно найти и проверить дескриптор разделяемой памяти на случай, если кто-то попробует запустить вас с мусором в командной строке.
  • Чтобы изменить созданную вашим процессом командную строчку, внешнему процессу понадобится разрешение PROCESS_VM_WRITE, а для изменения вашей таблицы дескрипторов - PROCESS_DUP_HANDLE. Эти разрешения вы можете защитить правильным выбором ACL (а списки ACL по-умолчанию достаточно хороши).
  • В схеме нет внешних имён или других значений, которые можно перехватить или сфальсифицировать (в предположении, что вы защитили процесс от PROCESS_VM_WRITE и PROCESS_DUP_HANDLE).
  • Поскольку вы используете разделяемую память, то между двумя процессами никаких данных реально копироваться не будет - происходит просто маппинг блоков памяти. Это более эффективная схема для больших объёмов данных.
Вот простая программа-пример для иллюстрации техники с разделяемой памятью:
type
  PStartupParams = ^TStartupParams;
  TStartupParams = packed record
    Magic: Integer;               // только одно значение
  end;
В принципе, запись TStartupParams может быть сколь угодно сложна (и необязательно быть именно записью), но для демонстрационных целей я собираюсь передавать простое число.
function CreateStartupParams(out AMapping: THandle): PStartupParams;
var
  SA: TSecurityAttributes;
begin
  FillChar(SA, SizeOf(SA), 0);
  SA.nLength := SizeOf(SA);
  SA.lpSecurityDescriptor := nil;
  SA.bInheritHandle := TRUE;
  AMapping := CreateFileMapping(INVALID_HANDLE_VALUE, @SA, PAGE_READWRITE, 0, SizeOf(TStartupParams), nil);
  if AMapping = 0 then
    RaiseLastOSError;
  Result := MapViewOfFile(AMapping, FILE_MAP_WRITE, 0, 0, 0);
  if Result = nil then
  begin
    FreeStartupParams(AMapping, Result);  // будет описана чуть позже
    RaiseLastOSError;
  end;
end;
Функция CreateStartupParams создаёт запись TStartupParams в наследуемом разделяемом блоке памяти. Сначала мы заполняем структуру TSecurityAttributes, чтобы отметить создаваемый дескриптор как наследуемый дочерним процессом. Установка lpSecurityDescriptor в nil означает, что мы хотим использовать дескриптор безопасности по-умолчанию. Затем мы создаём объект разделяемой памяти подходящего размера, проецируем его на адресное пространство и возвращаем дескриптор и указатель.
function GetStartupParams(out AMapping: THandle): PStartupParams;
var
  MBI: TMemoryBasicInformation;
begin
  AMapping := StrToInt(ParamStr(1));
  Result := MapViewOfFile(AMapping, FILE_MAP_READ, 0, 0, 0);
  if Result = nil then
    RaiseLastOSError;
  // После проецирования произведём базовую проверку
  if (
      (VirtualQuery(Result, MBI, SizeOf(MBI)) >= SizeOf(MBI)) and
      (mbi.State = MEM_COMMIT) and
      (mbi.BaseAddress = Result) and
      (mbi.RegionSize >= SizeOf(TStartupParams))
     ) then
  begin
     // Успех!
  end
  else
  begin
    // Блок памяти не подходит
    FreeStartupParams(AMapping, Result);  // будет описана чуть позже
    raise Exception.Create('Передан неверный блок памяти.');
  end;
end;
Функция GetStartupParams является обратной к CreateStartupParams. Она проверяет командную строку и пытается создать проекцию. Если в командной строке не число - StrToInt возбудит исключение. Если переданное число не является корректным дескриптором, то вызов MapViewOfFile будет неудачным, так что проверку дескриптора нам выполнять не нужно. Для проверки размера блока памяти мы используем VirtualQuery (мы не используем строгое равенство, т.к. размер возвращаемого значения округляется вверх до ближайшего размера кратного размеру страницы).
procedure FreeStartupParams(var AMapping: THandle; var AData: PStartupParams);
var
  HR: HRESULT;
begin
  HR := GetLastError;
  if AData <> nil then
  begin
    UnmapViewOfFile(AData);
    AData := nil;
  end;
  if AMapping <> 0 then
  begin
    CloseHandle(AMapping);
    AMapping := 0;
  end;
  SetLastError(HR);
end;
После того, как мы закончили работу с данными (либо в родительском, либо в дочернем процессе), нам нужно освободить ресурся для устранения утечки памяти. Вот для этого нам и нужна FreeStartupParams.
procedure PassNumberViaSharedMemory(const AMapping: THandle);
var
  SI: TStartupInfo;
  PI: TProcessInformation;
  ExeName, CmdLine: String;
begin
  ExeName := GetModuleName(0);
  CmdLine := '"' + ExeName + '" ' + IntToStr(AMapping);
  FillChar(SI, SizeOf(SI), 0);
  SI.cb := SizeOf(SI);
  if not CreateProcess(PChar(ExeName), PChar(CmdLine), nil, nil, True, 0, nil, nil, SI, PI) then
    RaiseLastOSError;
  CloseHandle(PI.hProcess);
  CloseHandle(PI.hThread);
end;
Большая часть работы здесь - это просто создание командной строки. Мы запускаем сами себя (используя трюк с GetModuleFileName(0)), передавая числовое значение дескриптора в командную строку и задавая True в CreateProcess для указания на то, что мы хотим наследовать дескрипторы. Заметьте, как мы ставим апострофы на случай, если полное имя нашей программы содержит пробелы.
program Project1;

{$APPTYPE CONSOLE}

uses
  Windows, SysUtils;

... // Вставьте сюда все наши процедуры. Первой должна идти FreeStartupParams.

function AnsiToOEM(const AMsg: AnsiString): AnsiString;
begin
  SetLength(Result, Length(AMsg));
  Windows.AnsiToOem(PAnsiChar(AMsg), PAnsiChar(Result));
end;

var
  Mapping: THandle;
  Data: PStartupParams;
begin
  try
    Mapping := 0;
    Data := nil;
    try
      if ParamCount <> 0 then
      begin
        Data := GetStartupParams(Mapping);
        WriteLn(AnsiToOEM('Переданное значение: '), Data^.Magic);
      end
      else
      begin
        Data := CreateStartupParams(Mapping);
        Data^.Magic := 42;
        PassNumberViaSharedMemory(Mapping);
      end;
    finally
      FreeStartupParams(Mapping, Data);
    end;
  except
    on E: Exception do
      WriteLn(E.Classname, ': ', AnsiToOEM(E.Message));
  end;
end.
В конце мы собираем всё в кучу. Если у нас есть параметры командной строки, тогда это значит, что мы - дочерний процесс, поэтому мы конвертируем их в TStartupParams и показываем номер, который был нам передан. Если же у нас нет аргументов командной строки, тогда это значит, что мы родительский процесс, поэтому мы создаём запись TStartupParams, записываем в него волшебные данные (42, конечно же) и передаём её дочернему процессу.

Вот, пожалуйста: безопасная передача "большого" объёма данных дочернему процессу (ну, окей, маленькому в нашем примере, но это могли быть и мегабайты данных, если бы вы захотели).

Примечание переводчика: некоторые программы, например WinRAR, используют такой способ: данные загоняются в текстовый файл, затем имя этого текстового файла передаётся в командной строке. С одной стороны, вы можете передать сколь угодно большой объём данных (нет ограничения на размер файла), а с другой стороны, его (входной файл) может сформировать и человек, а не только программа (в отличие от разделяемой памяти). Например, bat-ником прошерстить каталоги и скинуть имена файлов в этот файл. Разумеется, ни о какой безопасности передачи данных тут речь не идёт (разве что вы сумеете защитить файл подходящим ACL).

P.S. Не забывайте передавать текущий каталог вместе с командной строкой в ваших приложениях с одним экземпляром.

Комментариев нет:

Отправить комментарий

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.

Примечание. Отправлять комментарии могут только участники этого блога.