Как мы обсуждали вчера, если вам нужно передать больше, чем 32767 символов в дочерний процесс, вам придётся использовать что-то отличное от командной строки.
Один метод заключается в том, чтобы дождаться, пока дочерний процесс закончит инициализацию, затем найти окно дочернего процесса и отправить данные с помощью сообщения
WM_COPYDATA
. У этого метода есть несколько проблем:
- Вам нужен способ, чтобы узнать, что дочерний процесс полностью инициализировался и создал свои окна, прежде чем начать искать окно для передачи сообщения (возможно, вам подойдёт
WaitForInputIdle
). - Вам нужно убедиться, что окно, которое вы нашли, действительно принадлежит вашему дочернему процессу, а не просто случайно имеет то же самое имя. Или, возможно, не только случайно: если запущено более одной копии вашего дочернего процесса, вам лучше бы быть уверенным, что вы собираетесь говорить с нужной копией (здесь вам пригодится
GetWindowThreadProcessId
). - Вам нужно надеятся, что никто больше не сумел найти ваше окно и отправить ему
WM_COPYDATA
раньше вас (а если они сумели это сделать, это значит, что они сумели угнать ваш дочерний процесс). - Дочерний процесс должен предусматривать реакцию на неверные данные, отправленные через WM_COPYDATA.
Замечания об этом методе:
- Вам нужно аккуратно найти и проверить дескриптор разделяемой памяти на случай, если кто-то попробует запустить вас с мусором в командной строке.
- Чтобы изменить созданную вашим процессом командную строчку, внешнему процессу понадобится разрешение 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 можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.