Как мы обсуждали вчера, если вам нужно передать больше, чем 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 можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.