5 января 2015 г.

Почему вам (никогда) не следует использовать ShellExecute и WinExec

В Интернете полно примеров кода, которые используют функцию ShellExecute (реже - функцию WinExec). Однако, суровая правда состоит в том, что вам никогда не нужно использовать эти функции.

Почему?

Суть проблемы

Посмотрите на такой кусок кода:
var
  FS: TFileStream;
begin
  FS := TFileStream.Create('C:\MyFile.txt', fmOpenRead or fmShareDenyWrite);
  // ...
Вопрос: что будет, если при открытии файла возникнет ошибка? Скажем, файла такого нет или он эксклюзивно открыт другой программой.

Ну, конструктор класса TFileStream возбудит исключение - что "отменит" код после этой строки и переведёт управление на обработчик ошибок. По умолчанию это будет глобальный обработчик объекта Application, который покажет сообщение об ошибке:


С таким сообщением очевидно, что именно произошло, не так ли?
P.S. А в общем случае это может быть какой-то ваш код для обработки ошибок. Или, к примеру, если вы используете трейсер исключений, то он создаст вам отчёт о этой проблеме и отправит его вам на почту.
Вроде всё хорошо и красиво, да?

Да. А теперь посмотрим на такой код:
ShellExecute(Handle, 'open', 'C:\MyFile.txt', nil, nil, SW_SHOWNORMAL);
Вопрос: что будет, если при открытии файла возникнет ошибка? Например, нет ни одной программы для открытия таких файлов. Или программа есть, но её действие называется не open, а edit? Да или та же ошибка: нет такого файла?

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

Обработка ошибок в коде Delphi и коде Windows

Почему так происходит?

Дело в том, что TFileStream - это код Delphi. ShellExecute и WinExec - это код Windows.

Как я это определил?

Есть много способов:
  1. TFileStream описан в документации Delphi. Если вы запустите поиск в справке Delphi, то найдёте его. ShellExecute и WinExec описаны в документации Windows. Если вы запустите поиск по справке Microsoft, то найдёте их.
  2. Вы можете зажать кнопку Ctrl и щёлкнуть левой кнопкой мыши по тексту идентификатора в редакторе кода Delphi. Delphi откроет вам объявление идентификатора под курсором мыши. Тогда вы увидите, что TFileStream объявлен как класс Delphi в модуле System.Classes, а ShellExecute и WinExec объявлены в модуле Winapi.ShellAPI следующим образом: "function ShellExecute; external shell32 name 'ShellExecuteW';". Откуда и следует, что TFileStream - код Delphi (т.к. вы можете видеть этот код на Паскале), а ShellExecute и WinExec - код Windows (т.к. они импортируются из системной библиотеки shell32.dll).
  3. Наведя курсор мыши на идентификатор и задержав курсор над ним, вы получите всплывающую подсказку, из которой можно увидеть, что в аргументах TFileStream.Create используются типы данных Delphi (такие как string), а в аргументах ShellExecute и WinExec используются типы данных Windows (такие как PChar).

Хорошо, мы тремя способами выяснили, что TFileStream - это код Delphi, а ShellExecute и WinExec - это код Windows. И что с того?

Дело в том, что код Delphi для сообщения об ошибках использует механизм исключений, а код Windows использует механизм кодов ошибок.

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

С кодами ошибок дело обстоит иначе, ведь это просто числа (и логические значения), возвращаемые функциями. Если вы их не сохраняете в переменные, не проверяете, то они пропадают. Таким образом, ситуация по умолчанию для кодов ошибок - это игнорирование. Иными словами, вам всегда нужно писать специальный код, чтобы реагировать на ошибки в таких функциях.
P.S. Когда на форуме кто-то задаёт вопрос "почему не работает код", в 90% случаев причина вопроса в том, что программист не расставил правильную обработку ошибок. Чаще всего он вызывает функции Windows, не удосуживаясь проверять коды ошибок.
P.P.S. Не всегда код без обработки ошибок будет выполняться далее при ошибке.

Обработка ошибок вызовов ShellExecute и WinExec

Окей, как же нам нужно проверять ошибки вызова функций ShellExecute и WinExec? Что ж, откроем их описание в документации и почитаем:
Return value
Type: HINSTANCE
If the function succeeds, it returns a value greater than 32. If the function fails, it returns an error value that indicates the cause of the failure. The return value is cast as an HINSTANCE for backward compatibility with 16-bit Windows applications. It is not a true HINSTANCE, however. It can be cast only to an int and compared to either 32 or the following error codes below.
О чём говорит нам этот текст? Хотя он действительно описывает способ обработки ошибок, мы пока подождём его использовать: выделенная мной часть говорит о том, что эта функция (ShellExecute) создана для 16-битной Windows (т.е. Windows 3.11 и младше).
Функции ShellExecute и WinExec используют семантику единого адресного пространства для всех программ. Как мы знаем, в современных 32-битных (и выше) Windows (т.е. Windows 95 и старше) каждый процесс получает свой экземпляр "памяти" (адресного пространства), который изолирован и никак не связан с другими программами. Это было не так в 16-битных Windows: в ней все программы запускались в одной "памяти" (в едином адресном пространстве).

В такой модели программа идентифицировалась по экземпляру (HINSTANCE) её модуля. Поскольку адресное пространство было едино, то каждая программа загружалась в одно и то же пространство и, следовательно, имела уникальный экземпляр своего исполняемого модуля (exe-файла). Когда же адресное пространство стало своим у каждой программы, все программы стали загружаться в свои собственные адресные пространства. И описатель и экземпляр загруженного модуля стали одинаковы для всех программ (а именно: $00400000) и, таким образом, не могли более использоваться для идентификации запущенных программ.

И раз функции ShellExecute и WinExec возвращают экземпляр загруженного модуля (HINSTANCE) как идентификатор запущенной программы, то они созданы для Windows 3.11 (и более ранних ОС) и крайне плохо приспособлены для Windows 95 (и более новых систем).
Говоря кратко: ShellExecute и WinExec - это устаревшее говно мамонта начала 90-х годов. Они созданы в действительно доисторические времена. 640 Кб. Сегменты. Ближние и дальние указатели. Нет виртуальной памяти. Кооперативная многозадачность. Эти функции устарели в 1995 году. Ни один код, написанный после 1995 года, не должен использовать эти функции.

Но почему бы их не использовать? Ведь то, что функции устарели, ещё не означает, что они отсутствуют. Они есть и работают. Дело в том, что чтобы правильно сделать обработку ошибок для вызовов этих функций, вам нужно написать вот такого монстра:
var
  ErrorCode: Integer;
begin
  ErrorCode := Integer(ShellAPI.ShellExecute(Handle, 'open', 'notepad', nil, nil, SW_SHOWNORMAL));
  if ErrorCode <= HINSTANCE_ERROR { = 32 } then
  begin
    case ErrorCode of
      0: Application.MessageBox(PChar('The operating system is out of memory or resources.'), 'Error', MB_OK or MB_ICONERROR);
      ERROR_FILE_NOT_FOUND: Application.MessageBox(PChar('The specified file was not found.'), 'Error', MB_OK or MB_ICONERROR);
      ERROR_PATH_NOT_FOUND: Application.MessageBox(PChar('The specified path was not found.'), 'Error', MB_OK or MB_ICONERROR);
      ERROR_BAD_FORMAT: Application.MessageBox(PChar('The .exe file is invalid (non-Win32 .exe or error in .exe image).'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_ACCESSDENIED: Application.MessageBox(PChar('The operating system denied access to the specified file.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_ASSOCINCOMPLETE: Application.MessageBox(PChar('The file name association is incomplete or invalid.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_DDEBUSY: Application.MessageBox(PChar('The DDE transaction could not be completed because other DDE transactions were being processed.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_DDEFAIL: Application.MessageBox(PChar('The DDE transaction failed.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_DDETIMEOUT: Application.MessageBox(PChar('The DDE transaction could not be completed because the request timed out.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_DLLNOTFOUND: Application.MessageBox(PChar('The specified DLL was not found.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_FNF: Application.MessageBox(PChar('The specified file was not found.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_NOASSOC: Application.MessageBox(PChar('There is no application associated with the given file name extension. This error will also be returned if you attempt to print a file that is not printable.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_OOM: Application.MessageBox(PChar('There was not enough memory to complete the operation.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_PNF: Application.MessageBox(PChar('The specified path was not found.'), 'Error', MB_OK or MB_ICONERROR);
      SE_ERR_SHARE: Application.MessageBox(PChar('A sharing violation occurred.'), 'Error', MB_OK or MB_ICONERROR);
    else
      Application.MessageBox(PChar(Format('Unknown Error %d', [ErrorCode])), 'Error', MB_OK or MB_ICONERROR);
    end;
    Exit;
  end;
Хорошо, но если нам нельзя использовать функции ShellExecute и WinExec, то что же нам нужно использовать?

Правильное решение

Ответ: вместо функции ShellExecute следует использовать функцию ShellExecuteEx, а вместо функции WinExec следует использовать функцию CreateProcess.

Почему же примеры кода в Интернете используют старые функции? На это есть несколько причин:
  1. Безграмотность программистов. Большинство программистов даже не читали документацию по этим функциям. В лучшем случае они видели укороченный пересказ описания функций.
  2. Примеры кода иллюстрируют идею. Они должны быть просты, ненужные детали (в частности: обработка ошибок) должны быть убраны. Автор примера кода подразумевает, что вы напишете свой код, использовав идею примера, а не будете копировать пример один-к-одному.

К счастью, теперь мы уже не совсем безграмотные программисты, и мы в курсе, что нам не нужно использовать функции ShellExecute и WinExec, а нужно использовать функции ShellExecuteEx и CreateProcess. Таким образом, если у вас на руках есть "типичный" пример кода, который использует функцию ShellExecute или WinExec, то вы можете исправить его, заменив вызовы старых функций на новые. Это можно сделать двумя способами. Во-первых, вы можете переписать код:
  1. Вставив вместо ShellExecute функцию ShellExecuteEx (и вместо WinExec - функцию CreateProcess),
  2. Добавив правильную обработку ошибок,
  3. Добавив недостающий код для вызова ShellExecuteEx и CreateProcess (эти функции мощнее по возможностям и требуют больше аргументов).
Или вы можете написать функции-переходники (что мы и сделаем ниже). Функция должна иметь то же имя и те же аргументы, что и оригинал (т.е. ShellExecute и WinExec), но вызывать она должна современные функции (т.е. ShellExecuteEx и CreateProcess). Добавив такую функцию перед любым "плохим" примером кода, мы автоматически сделаем его "хорошим".

Обработка ошибок вызовов ShellExecuteEx и CreateProcess

Чтобы правильно написать вызов ShellExecuteEx и CreateProcess - нам всё ещё нужно научиться правильно обрабатывать ошибки вызова этих функций. Функции ShellExecuteEx и CreateProcess, равно как и функции ShellExecute и WinExec, являются функциями Windows, а не Delphi. Иными словами, ShellExecuteEx и CreateProcess также используют коды ошибок (а не исключения) для сообщения о проблемах выполнения.

Как и ранее, нам нужно открыть документацию функций ShellExecuteEx и CreateProcess, чтобы прочитать, как следует обрабатывать их ошибки:
Return value
Type: BOOL
Returns TRUE if successful; otherwise, FALSE. Call GetLastError for extended error information.
Как мы видим, способ обработки ошибок одинаков для этих функций. И он, вообще-то, является стандартом для большинства классических функций Windows. Функция возвращает логическое значение: истина или ложь, сигнализирующее об успешном или не успешном выполнении. Если выполнение было не успешно, то точную причину подскажет функция GetLastError, которая вернёт одну из констант ERROR_xyz, определённых в модуле Winapi.Windows. Чисто, чтобы удовлетворить ваше любопытство: вот список некоторых стандартных кодов ошибок (это не полный список - вы можете добавлять и свои собственные коды ошибок).

Этот код ошибки (только стандартный, а не пользовательский) можно конвертировать в текстовое сообщение с помощью вспомогательной функции SysErrorMessage. Таким образом, чтобы правильно обработать результат вызова ShellExecuteEx или CreateProcess (а равно как и любой другой не устаревшей классической функции Windows), мы можем сделать следующее (решение в лоб):
if not ShellExecuteEx({ ... }) then
begin
  Application.MessageBox(PChar(SysErrorMessage(GetLastError)), 'Ошибка', MB_OK or MB_ICONERROR);
  Exit;
end;
if not CreateProcess({ ... }) then
begin
  Application.MessageBox(PChar(SysErrorMessage(GetLastError)), 'Ошибка', MB_OK or MB_ICONERROR);
  Exit;
end;
Конечно, это ужасно неудобно, не так ли? Сравните этот громоздкий код с вызовов TFileStream.Create из примера выше. Нельзя ли с этим что-то сделать?

Можно. Идея здесь заключается в том, что мы конвертируем код ошибки в исключение. Чтобы не делать это вручную, можно воспользоваться уже готовой вспомогательной функцией: RaiseLastOSError. А чтобы не писать подобный неуклюжий if, мы можем сделать вспомогательную функцию. Вообще-то, такая функция уже есть, и она называется Win32Check. Итого, наш код примет вид:
Win32Check(ShellExecuteEx({ ... }));
Win32Check(CreateProcess({ ... }));
Этот код функционально эквивалентен:
TFileStream.Create({ ... });
Т.е. если при его выполнении возникнет ошибка, то возбудится исключение, код дальше по тексту будет пропущен, а управление будет передано на ближайший обработчик ошибок, по умолчанию таковым будет обработчик от объекта Application, который и покажет сообщение об ошибке:


Как видим, при этом код вызова системных функций Windows практически не отличается от вызова кода Delphi (нужно просто не забывать добавлять вызовы Win32Check) и не требует дополнительного кода (весь вспомогательный код уже написан в самой Delphi). Клёво, да?
P.S. Вообще-то, хотя функция RaiseLastOSError - кросс-платформенна, но Win32Check - нет. Win32Check помечена как "platform-specific", хотя она не делает ничего, кроме вызова RaiseLastOSError. Это не проблема в нашем случае, поскольку мы всё равно вызываем функции Windows (т.е. функции, специфичные для одной конкретной платформы).

Кросс-платформенный же способ - это функция CheckOSError. К сожалению, из-за кросс-платформенности эта функция крайне неудобна для использования в Windows. Во-первых, с ней код становится сложнее:
ShellExecuteEx({...});
CheckOSError(GetLastError);
Во-вторых, эта функция проверяет, что код ошибки должен быть отличен от "успех" (в случае с Windows - отличен от ERROR_SUCCESS). Хотя сперва это кажется логичным - но только до тех пор, пока вы не столкнётесь с таким поведением. Поэтому гораздо лучше использовать RaiseLastOSError:
if not ShellExecuteEx({...}) then
  RaiseLastOSError;
Это и корректнее и кросс-платформенно.

А в общем и целом, если вы пишете под Windows и вызываете функции Windows - просто используйте Win32Check.

Простые обёртки к ShellExecuteEx и CreateProcess

Итак, теперь мы готовы написать функции-обёртки для исправления "плохих" примеров кода:
uses
  ActiveX, ShellApi;

procedure ShellExecute(const AWnd: HWND; const AOperation, AFileName: String; const AParameters: String = ''; const ADirectory: String = ''; const AShowCmd: Integer = SW_SHOWNORMAL);
var
  ExecInfo: TShellExecuteInfo;
  NeedUnitialize: Boolean;
begin
  Assert(AFileName <> '');

  NeedUnitialize := Succeeded(CoInitializeEx(nil, COINIT_APARTMENTTHREADED or COINIT_DISABLE_OLE1DDE));
  try
    FillChar(ExecInfo, SizeOf(ExecInfo), 0);
    ExecInfo.cbSize := SizeOf(ExecInfo);

    ExecInfo.Wnd := AWnd;
    ExecInfo.lpVerb := Pointer(AOperation);
    ExecInfo.lpFile := PChar(AFileName);
    ExecInfo.lpParameters := Pointer(AParameters);
    ExecInfo.lpDirectory := Pointer(ADirectory);
    ExecInfo.nShow := AShowCmd;
    ExecInfo.fMask := SEE_MASK_NOASYNC { = SEE_MASK_FLAG_DDEWAIT для старых версий Delphi } 
                   or SEE_MASK_FLAG_NO_UI;
    {$IFDEF UNICODE} 
    // Необязательно, см. http://www.transl-gunsmoker.ru/2015/01/what-does-SEEMASKUNICODE-flag-in-ShellExecuteEx-actually-do.html
    ExecInfo.fMask := ExecInfo.fMask or SEE_MASK_UNICODE;
    {$ENDIF}

    {$WARN SYMBOL_PLATFORM OFF}
    Win32Check(ShellExecuteEx(@ExecInfo));
    {$WARN SYMBOL_PLATFORM ON}
  finally
    if NeedUnitialize then
      CoUninitialize;
  end;
end;
procedure WinExec(const ACmdLine: String; const ACmdShow: UINT = SW_SHOWNORMAL);
var
  SI: TStartupInfo;
  PI: TProcessInformation;
  CmdLine: String;
begin
  Assert(ACmdLine <> '');

  CmdLine := ACmdLine;
  UniqueString(CmdLine);

  FillChar(SI, SizeOf(SI), 0);
  FillChar(PI, SizeOf(PI), 0);
  SI.cb := SizeOf(SI);
  SI.dwFlags := STARTF_USESHOWWINDOW;
  SI.wShowWindow := ACmdShow;

  SetLastError(ERROR_INVALID_PARAMETER);
  {$WARN SYMBOL_PLATFORM OFF}
  Win32Check(CreateProcess(nil, PChar(CmdLine), nil, nil, False, CREATE_DEFAULT_ERROR_MODE {$IFDEF UNICODE}or CREATE_UNICODE_ENVIRONMENT{$ENDIF}, nil, nil, SI, PI));
  {$WARN SYMBOL_PLATFORM ON}
  CloseHandle(PI.hThread);
  CloseHandle(PI.hProcess);
end;
В целом, этот код достаточно прямолинеен: мы просто копируем параметры от ShellExecute/WinExec к ShellExecuteEx/CreateProcess, добавляя дополнительные параметры, требуемые ShellExecuteEx/CreateProcess. Вот несколько особенностей, на которые хотелось бы обратить внимание:
  1. Обе функции сделаны в виде процедур (т.е. они не возвращают значения). Это сделано на тот случай, если кто-то по ошибке попытается применить их к примеру кода, который корректно использует ShellExecute/WinExec ("корректно" - т.е. с обработкой ошибок);
  2. Добавлен Assert для гарантии корректного вызова (наличия обязательных параметров);
  3. Код с ShellExecuteEx инициализирует COM - об этом (помимо обработки ошибок) также часто забывают. Дело в том, что в Delphi COM автоматически инициализируется в главном потоке, поэтому эта ошибка обычно не заметна. Но если вы попытаетесь вызвать подобный "плохой" код из вторичного фонового потока, то получите ошибку RPC_E_THREAD_NOT_INIT = $8001010F ("CoInitialize has not been called on the current thread."/"Обращение к CoInitialize из текущего потока не производилось.");
  4. Мы указываем флаг SEE_MASK_NOASYNC (бывший SEE_MASK_FLAG_DDEWAIT), чтобы указать ShellExecuteEx подождать завершения всех асинхронных операций перед возвратом управления нам. Это нужно в двух случаях: если наш код вызывается из вторичного фонового потока и тут же выходит из потока, либо если наш код вызывается в потоке без оконной очереди сообщений. Это также довольно частаянетривиальная) ошибка;
  5. Мы указываем флаг SEE_MASK_FLAG_NO_UI, чтобы указать ShellExecuteEx, что мы сами обрабатываем ошибки (это подавит системный диалог с ошибками);
  6. Для аргументов мы используем приведение к Pointer вместо приведения к PChar - разница в том, что пустую строку приведение к PChar преобразует в указатель на #0, а приведение к Pointer - к nil;
  7. Мы делаем локальную копию командной строки для CreateProcess, чтобы избежать проблемы модификации константы (только для Unicode-приложения);
  8. Параметры используют типы данных Delphi (а не Windows), а также имеют значения по умолчанию для необязательных параметров. Это не нужно для цели "сделать кривой пример кода корректным", но удобно для самостоятельного использования функций-обёрток. Например:
    ShellExecute(Handle, '', Edit1.Text);
    Этот код откроет файл из Edit1.Text в программе по умолчанию. Обратите внимание, что мы не стали указывать все параметры, а также нам не пришлось приводить тип строки к PChar;
  9. Директивы IFDEF UNICODE делают код корректным как для ANSI (Delphi 2007 и ниже), так и для Unicode (Delphi 2009 и выше) версий Delphi (примечание: хотя код выше строго следует документации и указывает флаг SEE_MASK_UNICODE, но фактически этот флаг ничего не делает).

Пример исправления кода

Фух, теперь мы, наконец-то, можем исправить код "плохих" примеров!

Для этого я сделал поиск по "ShellExecute Delphi" (без кавычек, конечно же) и взял несколько примеров:
// Внимание! Код ниже не корректен!

ShellExecute(Handle, 'open', 'c:\Windows\notepad.exe', nil, nil, SW_SHOWNORMAL);
ShellExecute(Handle, 'open', 'c:\windows\notepad.exe', 'c:\text.txt', nil, nil, SW_SHOWNORMAL);
ShellExecute(Form1.Handle, nil, PChar(Site), nil, nil, SW_SHOW);
ShellExecute(Form1.Handle, nil, 'mailto:semen@krovatka.net?subject=delphi', nil, nil, SW_RESTORE);
Как мы можем исправить этот код?

Чтобы исправить этот код, нам нужно, во-первых, вставить код функций-обёрток перед этими "плохими" кусками кода, а, во-вторых, заменить nil на '', а преобразования PChar/ PAnsiChar/PWideChar и вовсе убрать - поскольку вместо системных типов Windows (PChar) наш код использует обычные строки Delphi.

Итого:
ShellExecute(Handle, 'open', 'c:\Windows\notepad.exe', '', '', SW_SHOWNORMAL);
ShellExecute(Handle, 'open', 'c:\windows\notepad.exe', 'c:\text.txt', '', '', SW_SHOWNORMAL);
ShellExecute(Form1.Handle, '', Site, '', '', SW_SHOW);
ShellExecute(Form1.Handle, '', 'mailto:semen@krovatka.net?subject=delphi', '', '', SW_RESTORE);
Этот код теперь "волшебным" образом стал правильным и корректным.

И под "правильным" здесь я подразумеваю только те моменты, что мы уже обговаривали: обработка ошибок, COM, асинхронные операции. К примеру, Form1.Handle вместо Handle - как минимум идеологически неверно и некоторых случаях даже и не корректно. Жёстко зашитые пути к программам - ещё косяк. Явный запуск программ - ещё одна проблема.

Обратите внимание, что с нашими обёртками описатель окна (первый параметр ShellExecute) можно не указывать - этот параметр не будет использоваться для показа сообщений об ошибках, поскольку мы указали флаг SEE_MASK_FLAG_NO_UI. И хотя флаг SEE_MASK_FLAG_NO_UI не подавляет диалог "Открыть с помощью" (см. ниже), но описатель окна не используется при показе этого диалога. Поэтому вы можете передавать туда 0.
P.S. Если вы используете ShellExecuteEx с действием "runas" для запуска процесса с элевацией, то вам нужно передавать корректный описатель окна (HWND): он будет использоваться для идентификации вашего процесса как приложения первого плана. Если же вы его не укажете, то ваше приложение будет считаться фоновым приложением. В этом случае запрос UAC на повышение прав не будет показан на экране сразу, а появится в свёрнутом (и мигающем) виде на панели задач.
Кроме того, с нашими обёртками не обязательно указывать все параметры - ведь они сделаны опциональными.

Итого, при желании код может быть упрощён до:
ShellExecute(0, 'open', 'c:\Windows\notepad.exe');
ShellExecute(0, 'open', 'c:\windows\notepad.exe', 'c:\text.txt');
ShellExecute(0, '', Site);
ShellExecute(0, '', 'mailto:semen@krovatka.net?subject=delphi');
P.S. Обратите внимание, что этот код правилен и корректен только при условии наличия функции-обёртки выше по тексту - ведь именно там сосредоточенна обработка ошибок, COM и асинхронных операций. Без функции-обёртки этот код не будет верным.

Прочие проблемы

Отсутствие обработки ошибок, отсутствие инициализации COM и отсутствие обработки асинхронных операций - это ещё не все проблемы в типичных примерах кода для ShellExecute.

В частности, довольно часто примеры используют действие (verb) "open" для открытия файлов. Однако корректнее не указывать действие вообще - т.е. передавать во второй параметр nil (для системных функций) или '' (пустую строку) - для нашей функции-обёртки. Дело в том, что иногда действие "open" может отсутствовать, либо не быть действием по умолчанию. К примеру, достаточно часто действие по умолчанию для типов файлов документов является "edit". И если вы явно укажете действие "open" для открытия таких файлов, то либо вы откроете файл не в той программе (не в той, что назначена по умолчанию), либо получите ошибку "Указанному файлу не сопоставлено ни одно приложение для выполнения данной операции"/"No application is associated with the specified file for this operation.". Например, в некоторых версиях Windows действие 'open' для .lnk файлов не назначено. Вызов ShellExecute(0, 'open', 'my.lnk', ...) вернёт ошибку, но ShellExecute(0, nil, 'my.lnk', ...) будет успешен.

Чтобы не гадать, какое же действие назначено по умолчанию для этого типа файла - просто не указывайте его. В этом случае система сама выберет действие по умолчанию, будь это "open", "edit", "explore", "print" или что-то иное. А если действия по умолчанию не назначено, то система выполнит "open". А если его нет, то она возьмёт первое попавшееся действие. А если действий вообще нет - то покажет диалог "Открыть с помощью":


Итого: всегда передавайте nil/'' в качестве действия (verb) в ShellExecute(Ex). Явно указывайте действие (такое как "open") только в специальных случаях:
  1. Вам явно нужно специальное действие вместо открытия файла. Например, "print", "explore" или "runas";
  2. Вам нужно получить ошибку "Указанному файлу не сопоставлено ни одно приложение для выполнения данной операции" в случае отсутствия программы для открытия таких файлов. В этом случае вы можете явно указать действие "open" - и тогда поиск действия выполняться не будет, диалог "Открыть с помощью" показываться не будет, и GetLastError вернёт вам ошибку ERROR_NO_ASSOCIATION = 1155. И в этом случае, при желании, вы можете сами показать диалог "Открыть с помощью" - использовав функцию SHOpenWithDialog (действие же "openas" сработает только если для типа файла нет других действий). Если же действие (verb) не указывать, то операция всегда будет успешной (исключая ошибки типа "файл не найден") - вне зависимости от наличия ассоциаций;
  3. Вам явно нужно действие "open", но действием по умолчанию назначено что-то иное.

Заключение

Что ж, я надеюсь, что вы узнали что-то новое сегодня. Вы узнали, почему вам не следует использовать функции ShellExecute и WinExec, какие функции нужно использовать вместо них (ShellExecuteEx и CreateProcess), как нужно правильно делать обработку ошибок при вызове системных функций Windows, как упростить этот код, какие ещё есть подводные камни у ShellExecute(Ex) и CreateProcess.

Читать далее: Почему вам не следует использовать ShellExecuteEx.

См. также: проблемы с CreateProcess.

59 комментариев :

  1. Спасибо за отличный материал!

    ОтветитьУдалить
  2. Опечатка в ShellExecute:
    ExecInfo.nShow := AShowCmd;
    ExecInfo.nShow := SW_SHOWNORMAL;

    ОтветитьУдалить
  3. в D7 константа SEE_MASK_NOASYNC не определена

    ОтветитьУдалить
    Ответы
    1. Не уверен насколько это корректно в глобальном плане, но мы делаем так:

      procedure ShellExecute(...
      {$IF Not Declared (SEE_MASK_NOASYNC)} Const SEE_MASK_NOASYNC = SEE_MASK_FLAG_DDEWAIT; {$IFEND}
      var
      ...

      Удалить
  4. Ответы
    1. Опечатки в "несколько примеров:" и в "Итого:" - во вторых примерах кода - лишние предпоследние параметры, соответственно "nil" и "пустая строка".

      Удалить
    2. Не вижу, где? Там же за-nil-иваются два параметра: lpParameters и lpDirectory (указывается lpOperation и lpFile).

      Удалить
  5. Как правильно указано в статье ShellExecute возвращает хендл запущенного процесса, благодаря которому я могу найти окно этого процесса и выполнять с ним разные действия и проверки, например:

    h: THandle;

    h := ShellExecute(Application.Handle, nil, PChar('test.png'),
    PChar(''), PChar(''), SW_SHOWNORMAL);

    if h > 32 then ShowWindow(h, SW_SHOW);

    То есть я хочу, что бы запустилась любая программа, которая умеет показывать png, и если она есть (h >32), то я вывожу ее на передний план.

    Вопрос: Как быть в этом случае с ShellExecuteEx ?

    ОтветитьУдалить
  6. У вас в голове какая-то каша.

    Я только что в статье подробно расписал, что значения, возвращаемые ShellExecute - бесполезны.

    ShellExecute возвращает описатель экземпляра (HINSTANCE). Это не описатель процесса (HANDLE). Это не описатель окна (HWND).

    HINSTANCE - это устаревший способ идентификации процессов. Windows 3.x не имела PID, вместо PID использовался HINSTANCE. В современном 32-разрядном мире HINSTANCE более не идентифицируют запущенную программу, потому что это просто базовый адрес исполняемого файла. Точное значение, которое вы получаете, вообще не имеет смысла. Даже, если бы оно было осмысленным, то не дало бы вам ничего, поскольку HINSTANCE не уникальны (фактически, HINSTANCE процесса почти всегда равно $00400000, поскольку это значение по умолчанию в большинстве компоновщиках).

    Я не знаю, как это сказать иначе. Если вы не верите мне, пусть вам об этом скажет разработчик Windows.

    ОтветитьУдалить
  7. Иными словами, ваш код с ShowWindow не делает ничего, поскольку на вход ему вы подсовываете значение, не являющееся описателем окна.

    ShowWindow возвращает вам False, а GetLastError возвращает точную причину ошибки. Конечно же, вы об этом даже не знаете, поскольку совершаете ровно ту же ошибку, о которой я говорю в статье: вы бездумно смешиваете в коде вызовы функций Delphi и Windows.

    Правильный вызов ShowWindow был бы таким:

    Win32Check(ShowWindow(...));

    И тогда вы бы сразу увидели ошибку, поскольку она была бы немедленно преобразована в исключение, которое не игнорируется (по умолчанию).

    Если же ShowWindow выполняется успешно и не возбуждает ошибку, то это говорит лишь о том, что значение, возвращаемое ShellExecute, случайно совпало со значением описателя какого-то окна в системе.

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

    Далее, ShowWindow, вообще-то, показывает окно, а не выводит его на передний план. Для выноса окна на передний план есть SetForegroundWindow. Запускаемое приложение самостоятельно покажет окно в соответствии с последним параметром ShellExecute (при условии, что оно его читает, а не предпочитает своё значение). По умолчанию это окно будет показано на переднем плане.

    ОтветитьУдалить
  8. Описатель окна может потребоваться вам, если вы передаёте управление уже запущенной программе. Но тогда это вопрос не про ShellExecute, а про IPC.

    ОтветитьУдалить
  9. Если же вы всё равно хотите получить описатель окна запущенного приложения, то эту задачу невозможно выполнить с ShellExecute. Зато её можно выполнить с помощью CreateProcess (технически, это также можно сделать с ShellExecuteEx, но "можно" - не равно "нужно").

    CreateProcess (а равно и ShellExecuteEx с соответствующими флагами) вернёт PID и описатель (HANDLE) процесса (а ShellExecuteEx - только описатель процесса).

    Вы можете подождать, пока целевой процесс создаст окно (например, с помощью WaitForInputIdle).

    А потом найти все окна, принадлежащие целевому процессу (процессу, с заданным PID) - через EnumWindows.

    Далее, из всех окон, которые создал целевой процесс, вам нужно как-то выбрать главное окно и делать с ним то, что вы хотели делать.

    ОтветитьУдалить
    Ответы
    1. С помощью ShellExecuteEx (с использованием функции-оболочки) открывается приложение. Программа, которая его запускает, хранит ExecInfo.hProcess.
      Не могу найти все окна, принадлежащие целевому процессу. Пробую через EnumWindows - не находит нужное окно.

      // При нажатии на кнопку
      ShellExecute(Self.Handle, 'open', App, '', '', SW_SHOWMAXIMIZED);
      WaitForInputIdle(pidKR, INFINITE);

      // функция
      function ProcCloseEnum(hwnd:THandle;data:DWORD):BOOL;stdcall;
      var pid:DWORD;
      begin
      Result:=true;
      GetWindowThreadProcessId(hwnd,@pid);
      if pid=data then
      SendMessage(hwnd,WM_CLOSE,0,0);
      end;
      end;

      // При нажатии на другую кнопку - закрыть окно, принадлежащее процессу ExecInfo.hProcess
      EnumWindows(@ProcCloseEnum,ExecInfo.hProcess);


      Среди всех hwnd, которые ищутся в ProcCloseEnum, нет нужного.
      ExecInfo.hProcess сохраняется верно, т.к. если запустить
      TerminateProcess(ExecInfo.hProcess,0);
      , то убивается нужный процесс. Хотелось бы закрыть приложение, попросив вежливо, а не используя такой метод убийства процесса.

      Удалить
    2. Перепутали всё что можно.

      Во-первых, описатель окна - это не THandle, это HWND (хотя технически оба типа - Cardinal).
      Во-вторых, описатель процесса (Handle) - это не идентификатор процесса (PID); хотя, опять же, технически оба типа - Cardinal, но это совершенно разные вещи.

      Удалить
  10. Да, был не прав.

    Поменял код на

    if ShellExecute(Application.Handle, nil, PChar('test.png'), PChar(''), PChar(''), SW_SHOWNORMAL) <= 32 then
    MessageDlg('У Вас на компьютере нет программы для просмотра этого файла.' , mtError, [mbOK], 0);


    ОтветитьУдалить
  11. А перебор окон, что бы определить запустил ли я этот файл на просмотр я делаю так:

    h :THandle;

    // Если текст уже открыт для просмотра, то переключаемся на него
    h := IsShowTextWnd(Application.Handle, );
    if h <> 0 then begin
    ShowWindow(h, SW_RESTORE);
    ShowWindow(h, SW_SHOWMAXIMIZED);
    Exit;
    end; { if }


    {--------------------------------------------------------------------}
    { Возвращает Wnd окна, если оно с заданной частью заголовка }
    {--------------------------------------------------------------------}
    function IsShowTextWnd(hFormHandle :THandle; cFileNamePart :String) :hWnd;
    var
    Wnd :hWnd;
    buff :Array[0..127] of Char;

    begin

    Result := 0;
    Wnd := GetWindow(hFormHandle, gw_HWndFirst);

    while Wnd <> 0 do begin { Не показываем: }

    if (Wnd <> Application.Handle) and { Собственное окно }
    IsWindowVisible(Wnd) and { Невидимые окна }
    (GetWindow(Wnd, gw_Owner) = 0) and { Дочерние окна }
    (GetWindowText(Wnd, buff, SizeOf(buff)) <> 0) { Окна без заголовков } then begin

    GetWindowText(Wnd, buff, SizeOf(buff));

    if AnsiPos(cFileNamePart, StrPas(buff)) <> 0 then begin
    Result := Wnd;
    Exit;
    end; { if }

    end; { if }

    Wnd := GetWindow(Wnd, gw_hWndNext);

    end; { while }

    end; { IsShowTextWnd }

    ОтветитьУдалить
  12. Что если ошибка будет не в том, что отсутствует программа для просмотра .png файлов, а в том, что такого .png файла нет? Тогда будет показано не то сообщение об ошибке - отличный подход, да. Это особенно актуально с учётом того, что вы используете относительные имена файлов.

    Я действительно не понимаю, зачем нужно с таким упорством цепляться за эти старые функции, если новые функции делают всё то же самое, но быстрее, корректнее и проще. Это примерно то же самое, что использовать сегодня голубиную почту для доставки клиентам актов и документов. Вы не получаете никаких плюсов, только издержки.

    ОтветитьУдалить
  13. Что касается первого абзаца Вашего ответа:

    Приведенный мной код, это всего лишь упрощенный пример. Реальный код запускает файл на просмотр по полному пути и
    перед этим запуском есть другой код который проверяет успешность сохранения файла из база на диск со случайным именем.

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

    ОтветитьУдалить
  14. Старое уже 100500 раз проверено и работает. А вот API-функции, добавленные скажем после Vista часто глючат. Если старое работает так мак вам необходимо, то надо использовать именно старое, проверенное.

    Все win api возвращают коды ошибок, ну или GetLastError. Я вообще не заморачиваюсь с разбором ошибок в программе. if LastError <> 0 then raise Exception.Create('[D3ECC87B-495C-471B-9C3E-64BDA09845DB] Случилось страшное. Код ошибки Windows API ' + IntToStr(LastError)); Если апликуха для распространения, то текст ошибки просто предлагается переслать в саппорт.

    ОтветитьУдалить
    Ответы
    1. > API-функции, добавленные скажем после Vista часто глючат

      Например?

      Есть мнение, что надо сначала проверить свой код, а уж потом вините других.

      Удалить
    2. Есть мнение, что битые ссылки никак не могут ни являться ссылкой "по теме" в общем, ни пруфом чего бы то ни было в частности.
      Также есть мнение, что за SEO бьют по морде http://lurkmore.to/Фриланс

      Удалить
  15. Автор поста зазнался немного. "Почему вам (никогда) не следует использовать ShellExecute и WinExec", прямые руки иметь надо, а не писать такие позорные посты, применение той или иной функции/процедуры зависит от решения определенной задачи. Вместо того чтобы писать подобное, учи матчасть, а не говори другим как надо. Один сказал, а другие подхватили и пошли философию разводить. Лучше бы грамотные описания и примеры всех выше описанных функцмй/процедур выложили. Хоть немного помогли бы новичкам.
    В моем ShellExecute часто применяется, в написанных плагинах поддержки серверов. Так же (не так часто) ShellExecuteEx. И все прекрасно работает. Заказчики ни разу не жаловались.

    ОтветитьУдалить
    Ответы
    1. пришел знаток, и давай показывать свою крутость. Сходу обвинил автора в позорности.
      "Лучше бы грамотные описания и примеры всех выше описанных функцмй/процедур выложили. Хоть немного помогли бы новичкам."
      Новичок (или знаток? походу скорее новичок с понтами), читай MSDN, подробнее чем там описано - сложно расписать. Или тебе английский непонятен - тогда что ты делаешь в программировании вообще?
      P.S. С аккоунта гугла и ЖЖ не получается опубликовать, с чего бы это?
      "И все прекрасно работает. Заказчики ни разу не жаловались." Самый безумный аргумент, который я слышал. А потом несчастные люди, которые будут вынуждены сопровождать, или, упаси Рендом, до (или, скорее)пере-переписывать этот говнокод, будут хвататься за голову в ужасе.



      Александр, спасибо за очередной полезный пост.

      Удалить
  16. Некоторые пишут так: SizeOf(ExecInfo), а некоторые так: SizeOf(TShellExecuteInfo) - как же лучше/правильнее и почему?
    Для чего делать CoUninitialize? Может пусть и останется инициализированным, не?

    А как правильнее подойти к такой парной задаче:
    1) Надо открыть html-файл с локального диска во всех программах умеющих/ассоциированных с веб-страницами (как минимум во всех установленных браузерах).
    2) Надо добавить закладку на этот же html-файл во все браузеры на "Панель закладок" (штука которая под адресной строкой).
    Пока сделал открытие в браузере по умолчанию через ShellExecuteEx, и сообщение мол добавьте на панель вручную. Возможно есть вообще иной подход?

    ОтветитьУдалить
    Ответы
    1. > как же лучше/правильнее и почему?

      Корректно - и так, и так, ведь ExecInfo имеет тип TShellExecuteInfo, т.е. размер у них одинаковый. Но обычно лучше указывать переменную - SizeOf(ExecInfo), т.к. потенциально тип переменной может быть изменён. Тогда SizeOf(переменная) вернёт правильное значение, а SizeOf(тип) вернёт размер чего-то другого.

      > Для чего делать CoUninitialize? Может пусть и останется инициализированным, не?

      Это действие указано как обязательное в контракте COM.

      > Надо добавить закладку на этот же html-файл во все браузеры на "Панель закладок" (штука которая под адресной строкой).

      Есть мнение, что это делать не нужно.

      > Надо открыть html-файл с локального диска во всех программах умеющих/ассоциированных с веб-страницами

      Не уверен, что в системе есть для этого API. Посмотрите в сторону IQueryAssociations.

      Удалить
    2. Разумно, будем придерживаться.


      а... И сильно страшное случится если не вызвать CoUninitialize до уничтожения потока? :)
      Просто думаю маловероятно что поток в общем случае создаётся только для одного вызова ShellExecute и сразу после её завершения уничтожается.
      Создали поток - он к примеру каждые 20 секунд вызывает наш ShellExecute. Даже если поток при создании сделал CoInitializeEx и NeedUninitialize будет false - сколько лишних действий будет сделано?
      Я лишь к тому что вызов CoInitializeEx/CoUninitialize для потока должен быть в ответственности программиста, использующего нашу новую ShellExecute.


      мм... Я снова забыл обдумать ситуацию в ключе "а что если всякие разные программы станут постоянно злоупотреблять подобным функционалом?"...


      Ситуация - есть пожилой человек который не умеет добавлять в закладки. Раз двадцать объяснял/показывал - не выходит и всё тут. А доступ к целевому компу лишь по пропускам.
      И человек не понимает что html-файл это не программа (почему мне нельзя "её" просто установить?), а даже если бы был программой - закладка есть элемент другой программы (и чтоб сама мне в "интернет" кнопку установила?).


      Спасибо большое, посмотрим сиё!

      Удалить
    3. Ну как-бы никто не запрещает реализовать свою функцию и выносить инициализацию COM за скобки. Просто код-пример был написан как универсальный.

      Удалить
    4. Кстати, с NeedUninitialize - косяк был. Я исправил.

      Удалить
    5. ...кажется NeedUninitialize просто пропало из кода... :D


      А как вы относитесь к подобным.. ..назовём это "прослойкам":

      [code]procedure ShellExecute(...);
      begin
      Assert(AFileName <> '');
      CoInitializeEx(nil, COINIT_APARTMENTTHREADED or COINIT_DISABLE_OLE1DDE);
      try
      ShellExecute_NoCoInit(...);
      finally
      // A thread must call CoUninitialize once for each successful call it has made to the CoInitialize or CoInitializeEx function, including any call that returns S_FALSE.
      CoUninitialize;
      end;
      end;

      procedure ShellExecute_NoCoInit(...);
      begin
      ...
      end;[/code]

      Удалить
    6. Ну, я в целом отрицательно отношусь к преждевременной оптимизации (т.е. если профайлер не показал, что инициализация COM - узкое место в коде, то не надо её и трогать).

      А так - лучше сделать одну функцию ShellExecute + ввести свои CoInitialize(Ex)/CoUninitialize. Пусть CoInitialize(Ex)/CoUninitialize взводят/сбрасывают флаг (и вызывают реальные CoInitialize(Ex)/CoUninitialize), а ShellExecute - проверяет флаг и вызывает CoInitialize(Ex)/CoUninitialize, если флаг не взведёт. Таким образом получаем:
      1. ShellExecute всегда гарантированно будет вызвана внутри CoInitialize(Ex)/CoUninitialize.
      2. CoInitialize(Ex)/CoUninitialize лишний раз не будут вызываться.

      Само собой, флаг - в threadvar.

      Только вот есть у меня сильное подозрение, что CoInitialize(Ex)/CoUninitialize и так уже делают ровно это же самое, так что я просто не вижу смысла возиться с этим.

      Если уж делать вынесение за скобки, то сделать одну функцию, которая делает всю работу, пусть в начале она вызывает CoInitialize(Ex), затем - что-то делает - и многократно вызывает ShellExecute (уже без CoInitialize(Ex)/CoUninitialize), ну и в конце пусть функция вызывает CoUninitialize. При этом ShellExecute должна быть внутренней подфункцией, чтобы её нельзя было вызвать извне (где "извне" - мы могли бы не вызывать CoInitialize(Ex)/CoUninitialize).

      Удалить
  17. Александр, интересная статья. Есть вопрос по этим функциям, а точнее по ShellExecute. Нужно сделать перехват вывода в коносль приложения, запущенного с помощью ShellExecute из моей программы в ТМемо или СтрингЛист. Перерыл в поисковиках кучу примеров, форумов и т.д. Везде либо код от начала 2000-х, который не работает, либо компилируется, но не выполняет свои функции. Сбился уже с ног. У меня Delphi XE10. Что можете посоветовать?

    ОтветитьУдалить
    Ответы
    1. Статья посвящена тому, что не нужно использовать ShellExecute для запуска процессов. Вы спрашиваете, как использовать ShellExecute для запуска процесса. Вопрос: какой ответ вы ожидаете услышать?

      Удалить
  18. Очень хорошо написанная статья но у меня вопрос можно ли написать функцию-обёртку к ShellExecuteEx для ранних Delphi (у меня большой проект на Delphi 2 и переписывать его на более новые версии очень напряжно) там нет ActiveX
    заранее спасибо

    ОтветитьУдалить
    Ответы
    1. У меня нет Delphi 2, но никто не мешает вам сделать это самостоятельно. Модули нужны только для импорта объявлений функций и констант. Если в Delphi 2 эти вещи объявлены в другом модуле (Windows?), то его и нужно использовать. Если их вообще нет - их нужно объявить самостоятельно.

      Удалить
    2. Александр у меня проблема с печатью на матричный принтер Epson lx300 может поможете - суть проблемы:
      до недавнего времени мы использовали в качестве сервера 32-разрядные Server 2003 и печать я осуществлял с помощью команд
      err:=shellexecute(0,'open','cmd',putt,'',SW_SHOW);
      где putt=' /c copy C:\WDATA\paradox.rnk\dov_km6.txt LPT1 /b'

      все работало без проблем на десятке серверов
      сейчас перешли на новый сервер 64-разрядный Server 2003 и печать перестала работать выдает 'ошибка 42' хотя я нигде не могу найти ее описание

      пробовал вот такую процедуру - не работает

      procedure TMoi_ProcModule.ShellExecute_moe_2;
      var SEInfo: TShellExecuteInfo;
      ExitCode: DWORD;
      ExecuteFile, ParamString: string;
      begin
      ExecuteFile:='cmd';
      FillChar(SEInfo, SizeOf(SEInfo), 0) ;
      SEInfo.cbSize := SizeOf(TShellExecuteInfo) ;
      with SEInfo do begin
      fMask := SEE_MASK_NOCLOSEPROCESS;
      Wnd := Application.Handle;
      lpFile := PChar(ExecuteFile) ;
      lpParameters:=putt;
      nShow := SW_SHOWNORMAL;
      end;
      if ShellExecuteEx(@SEInfo) then begin
      repeat
      Application.ProcessMessages;
      GetExitCodeProcess(SEInfo.hProcess, ExitCode) ;
      until (ExitCode <> STILL_ACTIVE) or
      Application.Terminated;
      ShowMessage('Напечатали!') ;
      end
      else ShowMessage('Ошибка !') ;
      end;

      что не так ?

      Удалить
    3. Мдаа.... вот такой код-то в кошмарах и снится.

      Давайте начнём со слона в комнате - в этом коде нет строки вида 'ошибка ' + IntToStr(ErrorCode), что означает, что ошибку показывает какой-то другой код.

      P.S. А так-то в этом коде вы совершили практически все ошибки, которые можно было сделать. И код ошибки выбрасываете, а не анализируете, и 'open' вместо nil, и ShellExecute(Ex) вместо CreateProcess (у вас же запуск процесса)...

      Удалить
    4. код брал с инета :(
      ошибку 42 выдает вот эта строка
      err:=shellexecute(0,'open','cmd',putt,'',SW_SHOW);

      процедура со 2 примера ShellExecute_moe_2; вообще ничего не выдает и не печатает :(

      мне другое интересно почему на 32-разряд работало на многих серверах а на 64-разр нет
      хотя может и поставили его (64) некоректно не знаю на что думать

      Удалить
    5. > ошибку 42 выдает вот эта строка

      Конкретно эта строка "ошибку выдавать" не может. Она или успешно запускает, что её просили, или возвращает код ошибки.

      Я не телепат. Если это вы сами err ниже показываете - я этого не вижу. Если это сообщение показывает вызываемый процесс - я этого не вижу.

      > процедура со 2 примера ShellExecute_moe_2; вообще ничего не выдает и не печатает :(

      Поскольку во втором варианте у вас обе ветки с ShowMessage, то "ничего не выдавать" она не может. Она может только показать сообщение об успехе, ошибке, или ждать окончания вызываемого.

      > мне другое интересно почему на 32-разряд работало на многих серверах а на 64-разр нет

      Несложно сообразить, что на 64-разрядной ОС есть два cmd.exe. Кроме того, все 32-разрядные функции работают через слой эмуляции WOW64, так что детали реализации могут отличаться, и баги, которые не были заметны ранее, теперь всплывают. Равно как проблема может быть в чём угодно ещё, я, повторюсь, не телепат.

      Удалить
    6. >Если это вы сами err ниже показываете - я этого не вижу.
      да ошибку эту я сам вывожу для простоты не показывал

      >у вас обе ветки с ShowMessage
      да я в последней отладке убрал IntToStr() с кода сори

      как я вижу в 64-й немного сложней будет у меня просьба Александр не могли бы вы привести пример как по вашему мнению правильно будет послать содержимое файла C:\WDATA\paradox.rnk\dov_km6.txt на LPT1 порт принтера, сроки поджимают надо вводить в строй этот серевер фактически из-за этой печати стала загвоздка
      (если это будет с помощью CreateProcess привожу на всякий случай справку с Delphi 2 по этой команде

      The CreateProcess function creates a new process and its primary thread. The new process executes the specified executable file.

      BOOL CreateProcess(

      LPCTSTR lpApplicationName, // pointer to name of executable module
      LPTSTR lpCommandLine, // pointer to command line string
      LPSECURITY_ATTRIBUTES lpProcessAttributes, // pointer to process security attributes
      LPSECURITY_ATTRIBUTES lpThreadAttributes, // pointer to thread security attributes
      BOOL bInheritHandles, // handle inheritance flag
      DWORD dwCreationFlags, // creation flags
      LPVOID lpEnvironment, // pointer to new environment block
      LPCTSTR lpCurrentDirectory, // pointer to current directory name
      LPSTARTUPINFO lpStartupInfo, // pointer to STARTUPINFO
      LPPROCESS_INFORMATION lpProcessInformation // pointer to PROCESS_INFORMATION
      );
      )

      Удалить
  19. функция ShellExecuteEx в Delphi 2 есть

    ОтветитьУдалить
  20. В "Простые обёртки" вы не запоминаете что вернула CoInitializeEx(). Можно узнать подробнее - почему?
    Например CoInitializeEx() вернула S_FALSE - может тогда не надо делать дальнейший код и особенно CoUninitialize()?
    А если RPC_E_CHANGED_MODE вернула? Оно получится уже было инициализировано, а мы его после выполнения ShellExecuteEx() деинициализируем?

    ОтветитьУдалить
    Ответы
    1. Сделал тест (с вашими параметрами COINIT_APARTMENTTHREADED + COINIT_DISABLE_OLE1DDE).
      Делфи, Win32, WinAPI, не консольная, без окон, без потоков.
      В старой Делфи7 вызов CoInitializeEx() возвращает S_OK, а в Делфи Берлин возвращает S_FALSE, при этом GetLastError() говорит "Code 87 - Параметр задан неверно".
      Интересует - в каких случаях надо CoUninitialize() а в каких нет, и в каких случаях надо бросать исключение (или код ошибки) вместо вызова ShellExecuteEx()...

      Удалить
    2. > вы не запоминаете что вернула CoInitializeEx(). Можно узнать подробнее - почему?

      Кажется я в попыхах неверно прочитал документацию, извиняюсь. Исправил.

      > В старой Делфи7 вызов CoInitializeEx() возвращает S_OK, а в Делфи Берлин возвращает S_FALSE, при этом GetLastError() говорит "Code 87 - Параметр задан неверно".

      Во-первых, S_FALSE - это "успех" (признак "ошибки" в этом коде сброшен в 0). В отличии от RPC_E_CHANGED_MODE - "ошибки" (признак "ошибки" в этом коде взведён в 1). Возврат этого значения говорит: "COM уже инициализирован с этими же параметрами, всё в порядке".

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

      В-третьих, в главном потоке Delphi зачастую инициализирует COM за вас. Делает это она в TApplication.Create для EXE (не для DLL). Идея в том, что многие новые контролы требуют COM, поэтому его инициализация проводится заранее. Какие-то старые Delphi такое, само собой, не делают. В них не используются контролы с COM-интерфейсами.

      > Интересует - в каких случаях надо CoUninitialize() а в каких нет, и в каких случаях надо бросать исключение (или код ошибки) вместо вызова ShellExecuteEx()...

      CoInitializeEx: "Each successful call to CoInitialize or CoInitializeEx, including any call that returns S_FALSE, must be balanced by a corresponding call to CoUninitialize".

      Т.е. если CoInitialize(Ex) была вызвана успешно (даже если она вернула S_FALSE), то нужно вызывать CoUninitialize. Если CoInitialize(Ex) вернула ошибку, то CoUninitialize вызывать не нужно.

      Успешность/не успешность проверяется через Succeeded() или Failed().

      Удалить
    3. Забыл ещё:

      > и в каких случаях надо бросать исключение (или код ошибки) вместо вызова ShellExecuteEx()...

      Сама по себе ShellExecuteEx не требует COM. Это видно хотя бы по тому, что она возвращает BOOL + GetLastError вместо COM-ского HRESULT. Это означает, что даже если нам не удалось инициализировать COM (или мы вообще это не стали делать), то мы всё ещё можем вызывать ShellExecute(Ex).

      Дело тут в том, что ShellExecute(Ex) может передать управление COM. А может и не передать. Зависит от того, как зарегистрированы обработчики запрашиваемого вами действия. Поскольку наперёд вы этого не знаете - COM лучше бы инициализировать. Но если что - вызывайте ShellExecute(Ex). Если ей что-то не понравится, она сама вернёт ошибку.

      Это всё написано первыми же словами в документации по ShellExecute(Ex).

      Удалить
  21. Поясните пожалуйста, почему lpVerb и lpParameters и lpDirectory заполняется через каст к Pointer(), а lpFile заполняется через каст к PChar()?
    Не понимаю почему там так, а там не так... В чём вообще разница? Есть ли какие нюансы?
    Спасибо!

    ОтветитьУдалить
    Ответы
    1. Написано же. №6 после "Вот несколько особенностей, на которые хотелось бы обратить внимание".

      Удалить
    2. Спасибо, не обратил внимания, потому что это как бы не поясняет главный-то вопрос - почему в трёх местах к Pointer, а в двух к PChar? Почему же не во всех пяти местах к чему-то одному, к Pointer?
      мм... Так, nil это грубо говоря число ноль, а "указатель на #0" - на символ ноль?? Откуда он его возьмёт? На какую-то константу? Всегда на одну и ту же?

      Удалить
    3. > почему в трёх местах к Pointer, а в двух к PChar?

      Почитайте описание функции. В одних местах она допускает nil, в других - нет.

      Семантика проста: nil - не указано; указатель на #0 - указано, но пусто.

      К примеру, действие, параметр и каталог могут быть не заданы - и это указывается как nil (что и указывается в описании функции). Передавать указатель на #0 будет неправильным (хотя это может работать).

      А, к примеру, файл - обязателен. И если туда передать nil - функция может вылететь с EAccessViolation, когда попытается его прочитать. Но если туда передать указатель на #0 - то функция успешно прочитать имя файла (пустое) и сможет вернуть ошибку "файл не найден".

      > Откуда он его возьмёт? На какую-то константу? Всегда на одну и ту же?

      Так точно. Константа #0 зашита в коде RTL, встроенная псевдо-функция PChar возвращает указатель на неё. Всегда одинаковый.

      Удалить
    4. Огромное спасибо!

      Удалить
  22. Мне непонятна лишь одна вещь... На момент написания этого комментария подстрока UniqueString встречается на странице лишь один раз, в коде, без пояснений.
    Не могли бы вы пояснить - зачем же оно? Нет, я вижу пункт номер семь, понятно почему нужна локальная копия строки, но не нахожу по ссылке оговорок об UniqueString.
    Спасибо!

    ОтветитьУдалить
    Ответы
    1. По ссылке приведено описание функции и её аргументов, в частности для командной строки: "The Unicode version of this function, CreateProcessW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation".

      (Функция CreateProcessW может модифицировать содержимое параметра "командная строка", поэтому этот параметр не должен указывать на область памяти только для чтения)

      Иными словами, если вы вызываете CreateProcessW(nil, 'константа', ...) - вы получаете EAccessViolation, потому что CreateProcessW попытается записать в область памяти 'константа', а константы у нас расположены в области только для чтения.

      Смысл тут в том, чтобы избежать выделения памяти в куче при вызове CreateProcess. Дело в том, что CreateProcess может использоваться для вызова отладчика для отладки бага в процессе. Было бы не здорово, если при повреждении кучи CreateProcess не смог бы запустить отладчик.

      С CreateProcessA такой проблемы нет, поскольку эта функция является просто переходником: она транслирует параметры и вызывает CreateProcessW - таким образом CreateProcessW получит указатель на временный локальный буфер (доступный для чтения/записи), созданный CreateProcessA, а не на исходный параметр. Следовательно, даже если мы передадим в CreateProcessA константу - она не приведёт к вылету, поскольку не будет использоваться CreateProcessW напрямую.

      P.S. Понятно, что CreateProcess - это не настоящая функция, а только ссылка на CreateProcessA или CreateProcessW в зависимости от используемого компилятора?

      Удалить
    2. Так а конкретно UniqueString() там зачем?

      Удалить
    3. ACmdLine может быть константой. Соответственно, CmdLine тоже будет константой. Счётчик ссылок у обеих будет -1 и они будут указывать на read-only память. UniqueString делает копию в куче - в памяти для чтения/записи.

      Удалить
    4. Спасибо большое!

      Удалить
  23. Прочитав первые две страницы, решил, что Автор дебил. Но прочитав далее поменял свое решение.

    ОтветитьУдалить
  24. Функции до сих пор юзают, потому что нет простой и удобной замены типа RunApp(AppPath, Params). CreateProcess у неподготовленного человека только и может что вызвать дрожь и трепет своим набором параметров.

    ОтветитьУдалить
  25. > Кроме того, все 32-разрядные функции работают через слой эмуляции WOW64

    Подскажите, а как корректнее запустить "cmd.exe" из 32-хбитного процесса, на 64-хбитной ОС, чтоб оно нормально видела файлы в папке System32?
    Это возможно только прыгая вокруг Wow64DisableWow64FsRedirection() и Wow64EnableWow64FsRedirection() или как-то более правильно?
    А то нашёл статьи мол последние вообще никогда использовать не надо ни при каких условиях... Спасибо!

    ОтветитьУдалить
    Ответы
    1. В 64-битной системе два cmd.exe: один - 32-битный, второй - 64-битный.
      1. Запуская C:\Windows\System32\cmd.exe, вы запускаете cmd.exe той же битности, что и ваше приложение.
      2. Запуская C:\Windows\SysWOW64\cmd.exe, вы запускаете 32-битный cmd.exe.
      3. Запуская C:\Windows\SysNative\cmd.exe, вы запускаете 64-битный cmd.exe.

      2-3 надо запускать с проверками битности, поскольку:
      1. Эти алиасы не существуют в 32-битной ОС.
      2. Эти алиасы не существуют в очень старых версиях ОС (Windows XP).
      3. Эти алиасы не существуют для приложений с совпадающей битностью (т.е. C:\Windows\SysNative\ не существует для 64-битных приложений).

      Удалить
    2. Огромнейшее спасибо! Теперь всё понятно!

      Удалить

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

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

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

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

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

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