5 января 2015 г.

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

В прошлый раз мы узнали, почему вам никогда не следует использовать функцию ShellExecute.

В этот раз я расскажу вам о том, почему вам не следует использовать функцию ShellExecuteEx.

Заголовки этого и предыдущего постов выбраны крайне тщательно. Предыдущий пост говорил о том, что если вы пишете код в 1995 году или позднее, то вы не должны использовать функцию ShellExecute. Поскольку на дворе у нас 2015 год, то это означает, что ваш код вообще никогда не должен использовать ShellExecute. В заголовке же этого поста отсутствует слово "никогда", что намекает на то, что использовать ShellExecuteEx иногда можно. Кроме того, в заголовке используется двойное наименование, обозначая как функцию ShellExecute, так и функцию ShellExecuteEx - это говорит о том, что смысл не в конкретной функции, а в логических действиях, которые они выполняют.

Итак, в чём же проблема?

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

Очень часто начинающие программисты используют функцию ShellExecute для прямого запуска программ. Иными словами, они делают что-то такое:
ShellExecute(Handle, 'open', 'notepad', nil, nil, SW_SHOWNORMAL);
Хорошо, я надеюсь, они прочитали предыдущую статью и стали делать так:
ShellExecute(0, '', 'notepad');
(этот пример кода использует функцию-обёртку из предыдущей статьи; если вы не читали предыдущую статью, то считайте, что этот код эквивалентен вызову ShellExecuteEx с правильной обработкой ошибок, COM и асинхронных операций).

Да, так стало лучше, но проблема от этого не исчезла. Что же это за проблема?

Дело в том, что ShellExecute(Ex) (здесь и далее я буду употреблять комбинированное название функций для обозначения обеих функций) является функцией Оболочки Windows (Проводника/Explorer), которая предназначена для открытия файла в ассоциированной с ним программе - т.е. в той, которую назначил пользователь для данного типа файлов. Заметьте, что эта цель отлична от "запуска явно указанной программы".

Код выше работает по той простой причине, что действие по умолчанию для .exe-файлов - это их запуск.

Решение проблемы

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

Что не так с ShellExecute?

Но почему бы не использовать ShellExecute(Ex) для запуска программ? Вот несколько причин (некоторые из причин применимы не всегда):
  1. Тяжеловесность. ShellExecute(Ex) намного тяжеловеснее CreateProcess, поскольку ей нужны многократные чтения реестра для поиска и разрешений ассоциаций, проверки override-ов и т.п. (насколько всё плохо, вы можете оценить из этой статьи - см. раздел "The Nitty Gritty" и ниже). Кроме того, ShellExecute(Ex) - функция Оболочки (импортируется из shell32.dll), а CreateProcess - функция ядра (импортируется из kernel32.dll). Иными словами, в компактных и/или не визуальных программах вы тащите к себе Оболочку (shell32.dll) целиком - ради всего одной функции, без которой можно и так обойтись;
  2. Уязвимость. ShellExecute(Ex) принимает только полную командную строку, в то время как CreateProcess позволяет явно указать имя программы и отдельно - командную строку.

    Проблема здесь в том, что если вы указываете только командную строку, то при неаккуратной расстановке кавычек вы можете создать в своём коде уязвимость или баг.

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

    Конечно, функция ShellExecuteEx даёт некоторые дополнительные возможности по сравнению с ShellExecute (например, ожидание завершения запущенной программы) - но её возможности далеки от полноценной CreateProcess;
  4. Вы не можете запустить программу, если она имеет нестандартное расширение - поскольку ShellExecute(Ex) не запускает программу, а открывает её в ассоциированной программе. Ассоциация выполняется по расширению файла. Таким образом, если файл не будет иметь расширение .exe (или .com), то запуск программы через ShellExecute невозможен (эта операция возможна для ShellExecuteEx, если вы используете переопределение типа файла). В то же время CreateProcess не смотрит на ассоциации файлов, а просто запускает программу - поэтому ему без разницы, имеет ли программа расширение .exe, .dat, .MyFile или иное;
  5. Иногда пользователь может назначить другую программу для открытия .exe файлов (по ошибке, конечно же). Или за него это может сделать какая-то бяка. В этом случае ShellExecute(Ex) будут использовать переопределённые ассоциации для .exe файлов и не смогут запустить программу. CreateProcess же запустит программу в любом случае, поскольку не смотрит на ассоциации;
  6. Зависимость от COM. Функция ShellExecute(Ex) - это функция Оболочки. Откуда следует, что для работы ShellExecute(Ex) необходима инициализация COM. Хотя в главном потоке VCL приложения Delphi делает это за вас автоматически, это не будет работать в других случаях (фоновых потоках). Отсюда же следует, что ShellExecute может не работать в MTA-окружениях. Знаете ли вы вообще, что такое инициализация COM и как она правильно делается?

    Да, если вы читали предыдущую статью, то вы в курсе, как нужно делать правильно. Кроме того, по умолчанию инициализация COM не обязательна именно для .exe файлов (ну, если только вы не будете запускать их с элевацией - как раз это делается через COM). Но для этого надо хотя бы знать про такую особенность, чтобы не впадать в панику при виде сообщения "Обращение к CoInitialize из текущего потока не производилось.".

    Функция CreateProcess - это функция ядра, она не требует знания особенностей Оболочки Windows;
  7. Аналогично, ShellExecute(Ex), являясь функциями Оболочки, требуют цикла выборки сообщений, и вам нужно делать специальную обработку для случаев, когда этого цикла у вас нет.

    Как и выше, это не проблема с .exe-файлами, но идея в том, что бездумно используя ShellExecute(Ex) вместо простейшей CreateProcess, вы не подумаете про ситуацию, в которой это действительно важно;
  8. Чтобы правильно сделать обработку ошибок для ShellExecute, вам нужно написать примерно такой код:
    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;

    Насколько проще выглядит правильная обработка ошибок для CreateProcess:
    Win32Check(CreateProcess('notepad.exe', 'notepad.exe', nil, nil, False, 0, nil, nil, SI, PI));

Как правильно делать

Итак, когда же вам нужно использовать эти разные функции?
  • Никогда не используйте ShellExecute;
  • Никогда не используйте WinExec;
  • Используйте CreateProcess, если вы хотите запустить конкретную программу (имя вам известно);
  • Используйте CreateProcess, если вы хотели использовать ShellExecute(Ex) и обнаружили, что вы передаёте имя исполняемого файла в третий параметр (например: ShellExecute(Handle, nil, 'cmd.exe', nil, nil, SW_SHOW));
  • Используйте CreateProcess, если вы не знаете имя программы, но точно знаете, что это программа (например, имя программы приходит из файла конфигурации);
  • Используйте CreateProcess, если у вас есть не имя программы, а командная строка;
  • Используйте ShellExecuteEx, если вам нужно открыть файл, не являющийся программой (например, архив, документ, музыку);
  • Используйте ShellExecuteEx, если вам нужно запустить программу с элевацией (с использованием действия "runas");
  • Также используйте ShellExecuteEx с "runas", если CreateProcess вернул ERROR_ELEVATION_REQUIRED (= 740);
  • Используйте ShellExecuteEx, если вам нужно открыть файл, но вы не знаете, что это за файл (например, его имя вводится/указывается пользователем);
  • Используйте ShellExecuteEx, если вам нужно открыть гиперссылку (http или mailto);
  • Используйте ShellExecuteEx, если вы хотите подражать оболочке (например, пишете файловый менеджер);
  • Используйте ShellExecuteEx с SEE_MASK_INVOKEIDLIST для выполнения "динамических" действий, определяемых обработчиками контекстного меню.

Говоря совсем кратко: CreateProcess - для запуска программ (исключая случаи с элевацией), ShellExecuteEx - для открытия файлов и запуска программ с элевацией.

См. также: решение проблем CreateProcess.

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

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

  1. Сань, подкорректируй маленько магическую констаннту 32.
    Т.к. ShellExecute возвращает HINSTANCE, то и писать надо:
    if ErrorCode <= HINSTANCE_ERROR then

    ОтветитьУдалить
  2. Подправил, спасибо.

    32 указано в документации, упоминания HINSTANCE_ERROR там нет. Поэтому константу вставил, а литерал заключил в коммент.

    ОтветитьУдалить
  3. Здравствуйте,

    Использую ShellExecute(0, '', 'путькфайлу') для запуска ярлыков (.lnk) и ссылок (.url). В случае если ярлык ссылается на отсутствующий файл или в ссылке использован незарегистрированный в системе протокол ShellExecuteEx возвращает ошибку или показывает диалог отличный от диалога Проводника.
    Для ярлыка правильный диалог можно получить воспользовавшись IShellLink.Resolve.
    Возможно ли возложить обработку таких случаев, со всеми вытекающими диалогами, на Проводник ?

    Мне известна как минимум одна программа которая в таких случаях ведёт себя аналогично Проводнику. Вероятно она является расширением Shell тк не имеет собственного процесса или службы. Является ли это основанием для поведения аналогичного Проводнику в таких случаях как выше ?

    ОтветитьУдалить
  4. Давайте расставим все точки над i.

    Во-первых, IShellLink.Resolve - это НЕ способ показывать диалог. Это способ исправлять испорченный ярлык. Диалог - это лишь последняя ступень в алгоритме исправления данных, когда автоматические методы не дали результатов.

    Во-вторых, вы столкнулись с проблемой, о которой я говорю в статье. Вы пытаетесь использовать общее решение (ShellExecute/Ex) для решения частной проблемы. В статье под частной проблемой я использую "запуск программы", а в вашем случае частная проблема - это "исправление испорченного ярлыка". Общее решение ничего не знает про частные случаи, поэтому частные случаи вам нужно обрабатывать самостоятельно.

    Если вы встречаетесь только с частными случаями (по статье: ваш код запускает только программы; ваш случай: вы открываете только ярлыки), то вам не нужно использовать для этого общее решение (ShellExecute/Ex) - о чём и идёт речь в статье. Используйте частное решение.

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

    В частности, вам нужно определить, является ли файл ярлыком, и если да - то вызывать ваш код по обработке частного случая (IShellLink.Resolve), а если нет - то общего (ShellExecute/Ex).

    Чтобы определить, является ли файл ярлыком, вам нужно извлечь его расширение (например, '.lnk'), прочитать значение по умолчанию для HKCR\.lnk, затем проверить наличие (пустых) данных HKCR\lnkfile\IsShortcut: если IsShortcut есть, то файл - ярлык, если IsShortcut нет, то файл - не ярлык. Разумеется, вместо .lnk и lnkfile нужно подставить расширение файла и прочитанное из реестра значение типа файла.

    ОтветитьУдалить
  5. Спасибо,
    Буду действовать так.
    Однако остаётся чувство, что должен быть способ дать Проводнику команду на запуск любого файла, а дальше пускай Проводник сам решает как обрабатывать ошибки и какие диалоги показывать пользователю.

    Результат запуска, в виде числа для дальнейшей обработки, не нужен.

    ОтветитьУдалить
  6. Сам же Проводник и пользуется IShellLink.Resolve, равно как и другие файловые менеджеры (типа, Total Commander). Это можно увидеть, проанализировав стек вызовов. Например, в стеке вызовов Проводника нет никакой "типа ShellExecute, но чтоб ещё .lnk исправляла", зато есть CShellLink._InvokeCommandAsync (вероятно, аналог IContextMenu.InvokeCommand), CShellLink._Resolve (видимо, класс, который реализует IShellLink.Resolve) и CLinkResolver.Resolve.

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

    ОтветитьУдалить
  7. Согласен чудес не бывает. Но я бы хотел переложить ответственность за нативный отклик на Оболочку, а не делать код подражающий ей.
    ps. IShellLink не работает с .url. Нахожусь в поиске решения.

    ОтветитьУдалить
  8. Смотрим MSDN. ShellExecute не отмечена как устаревшая.

    Minimum supported client
    Windows XP [desktop apps only]
    Minimum supported server
    Windows 2000 Server [desktop apps only]

    Ее поддерживают и меняют реализацию, что бы соответствовать изменениям архитектуры ОС. Если не надо тонкого управления процессами, то ShellExecute то, что вам надо.

    ОтветитьУдалить
    Ответы
    1. Базара нет, ShellExecute - "cutting edge technology".

      Удалить
  9. То есть если я хочу запустить консольную команду, надо делать через CreateProcess указывая 'cmd.exe'?

    ОтветитьУдалить
    Ответы
    1. Консольные команды вне командного интерпретатора не существуют, поэтому выполнить их можно только с его помощью. А уж будете ли вы запускать его через CreateProcess, ShellExecute(Ex), командный файл или что-то иное - не столь важно.

      А статья про другое: про то, что если код запускает программы, то не надо использовать ShellExecute(Ex), ибо она предназначена маленько для другого (открытия файлов в ассоциированной программе).

      И да, поскольку cmd.exe является программой, а не документом, то не надо его запускать через ShellExecute(Ex).

      Удалить
    2. То есть сделать примерно вот так будет нормально?: CreateProcess(PChar('C:\Windows\System32\cmd.exe'), PChar('C:\Windows\System32\cmd.exe /C DEL "C:\tests\*.tmp" & PAUSE'), ... )

      Удалить
    3. Да, вполне.

      P.S. За исключением того, что файлы можно удалить и самому :)

      Удалить
    4. Ну и того что системный каталог ОС может быть в совсем другом месте. :) Это просто для примеру. :)
      Кстати с наскоку что-то не нашёл информацию можно ли в первых двух параметрах использовать переменные среды типа %WINDIR%...
      Спасибо!

      Удалить
    5. Нет, CreateProcess не разворачивает переменные окружения, но cmd.exe - разворачивает. Поэтому параметры cmd.exe будет развёрнуты, но путь в имени cmd.exe - нет.

      Так что если хотите запуск cmd.exe делать через переменные - их нужно развернуть вручную. И я бы тогда использовал переменную %ComSpec%. В противном случае - я бы использовал просто GetSystemWindowsDirectory + 'cmd.exe'.

      Удалить
    6. Большое спасибо!
      Если можно ещё такой момент, уже не относящийся в теме. Как корректнее всего получать текст из API-функций? Вот этой же GetSystemWindowsDirectory? Просто я находил несколько десятков вариаций, некоторые вроде работают, другие периодически глючат.
      Кто-то объявляет переменную String, вручную устанавливает ей длину, передаёт в API-функцию указатель на первый символ. Кто-то объявляет статический массив Char и просто указывает имя переменной (а иногда даже динамический объявляют). А полученный результат могут конвертировать в строку или через SetString (предварительно узнавая длину либо StrLen либо странным циклом с конца), или через StrPas, или через прочие, даже просто присвоением с явным приведением типа к PChar или String, иногда ничего не делая. Однажды видел нечто в виде ассемблерной вставки.
      Просто я запутался уже как лучше и не умею определить чем могут быть чреваты иные варианты. :)

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

      Что точно не надо делать - не надо использовать статический массив символов:
      1. В Delphi нет проверок переполнения буфера на стеке. Для кучи (динамической памяти) - есть (через отладочный менеджер памяти).
      2. Данные на стеке (особенно - не инициализированные данные) приводят к ложно-положительным значениям на стеке вызовов, если используется RAW-метод трассировки.
      3. Большие данные быстро исчерпают стек. Функцию не удастся вызывать при STACK_OVERFLOW.

      Все эти моменты я разбирал в серии про DLL (плагины) и память:
      http://www.gunsmoker.ru/2011/12/delphi.html
      http://www.gunsmoker.ru/2011/04/windows.html

      В итоге, для вызова конкретно GetSystemWindowsDirectory я бы использовал вот такое:

      function GetWindowsSystemPath: String;
      begin
      SetLength(Result, MAX_PATH);
      SetLength(Result, GetSystemWindowsDirectory(PChar(Result), Length(Result)));
      Result := IncludeTrailingPathDelimiter(Result);
      end;

      Для других функций, повторюсь, этот код может не подойти - надо смотреть описание функции.

      Удалить
    8. Про { 1, 2, 3 } не знал, интересно! И ссылки пойду почитаю. А некоторые из тех способов про которые я заговорил - было также и через статические массивы объявленные глобально.
      По поводу последнего кода, спасибо за пример, тока GetSystemWindowsDirectory может вернуть ноль и это обозначает ошибку которую надо получать через GetLastError, думаю надо каплю переиначить. :)

      Удалить
    9. Вижу такую цитату:
      > var A: Integer;
      > ... так как переменная «А» локальна, значит, она расположена на стеке ...
      А здесь Result уже не считается как локальная переменная? Или String всегда будет не на стеке? А записи и простые объекты как будут располагаться?
      Как-то можно точно определить на стеке переменная или нет? Рантайм-тест-проверку сделать какой-то?
      Может вдруг по значению указателя можно сделать какие-то выводы? И можно ли как-то объявить локальную "A: Integer;" но чтоб оно было наоборот не на стеке?

      Удалить
    10. Это вы точно сюда написали? Я контекст не вижу.

      > А здесь Result уже не считается как локальная переменная? Или String всегда будет не на стеке? А записи и простые объекты как будут располагаться?

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

      > Как-то можно точно определить на стеке переменная или нет? Рантайм-тест-проверку сделать какой-то?

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

      Поэтому ответ - нельзя. Но можно посмотреть в машинном отладчике как компилятор скомпилировал код.

      > Может вдруг по значению указателя можно сделать какие-то выводы?

      Взятие указателя заставит компилятор скопировать локальную переменную на стек, если там её ещё не было.

      > И можно ли как-то объявить локальную "A: Integer;" но чтоб оно было наоборот не на стеке?

      Нельзя.

      Удалить
  10. Набор очень сомнительных утверждений.
    Как уже верно отмечено выше, ShellExecuteEx не отмечена как deprecated или out of date.
    Зато она в разы проще. И, если человек, с одной стороны, слабо разбирается в процессах и потоках, а, с другой стороны, ему не нужны возможности, предоставляемые CreateProcess (их, действительно, очень много, но большая их часть в году используется половину раза, включая флаги, возвращаемые дескрипторы, переадресацию и т.п.), то неясно, зачем ему CreateProcess, тем паче, что там кода побольше.
    Аргументы про неверно сформированную командную строку или переопределенную ассоциацию бинарей - несерьезны, извините.
    А уж тяжеловесность функции - тем более. Лишний колл библиотеки, которая, все равно, скорее всего, уже подгружена в пространство процесса - ничто в сравнении с загрузкой в память бинаря из файла, особенно большого бинаря.

    ОтветитьУдалить
    Ответы
    1. Для полноты картины покажите как ошибки-то обрабатываете.

      Удалить
  11. Подскажите, а можно ли сделать запуск процесса с элевацией не через ShellExecuteEx() + 'runas', а как-то иначе? У меня с этим что-то не ладится, не тот exe запускается, полагаю наверное SysWOW64 влазит...

    ОтветитьУдалить
    Ответы
    1. Можно. Например через COM Elevation Moniker. Есть и другие варианты. Но ShellExecuteEx - простейший способ. Если вы с ним не можете разобраться, то уж не знаю, как с остальным будете.

      И как бы вопрос с перенаправлением путей - он универсальный, от способа запуска не зависит. Т.е. если вы что-то напутали с редиректами в ShellExecuteEx - то точно так же напутаете с ними и с любым другим способом.

      Удалить
    2. Да что там разбираться, вы и так всё максимально доступно разжевали - спасибо вам огромное, с самим ShellExecuteEx() проблем нету. Проблема видимо с нюансами WoW64 и правами админа.
      хм... ShellExecuteEx() + 'runas' как-то зависит от того была ли элевация у вызывающего его процесса?
      При некоторых условиях FileExists() говорит что exe-файл существует, но ShellExecuteEx() + 'runas' падает с ошибкой "Системе не удается найти указанный путь".

      Удалить
    3. Он почему-то не видит "C:\Windows\SysNative\"? Вызов как-то делегируется куда-то в 64-битный процесс (типа как бы через COM)?

      Удалить
    4. Я, как бы, не телепат, мысли читать не умею. Я понятия не имею, что вы делаете, что вы ожидаете получить, что вы получаете.

      К примеру, из MSDN (выделение моё):
      WOW64 recognizes Sysnative as a special alias used to indicate that the file system should not redirect the access. This mechanism is flexible and easy to use, therefore, it is the recommended mechanism to bypass file system redirection. Note that 64-bit applications cannot use the Sysnative alias as it is a virtual directory not a real one.

      Удалить
    5. Создаём VCL-проект. Компилируем как 32-bit. Запускаем на Win10-x64.
      Пытаемся запустить что-либо из SysNative с элевацией. Да для начала хоть cmd.exe, главное чтоб 64-bit и элевацию спрашивало.
      Во избежание всякого, возмём прям ваши "простые обёртки", без каких-либо модификаций. И напишем:

      FileName := 'C:\Windows\SysNative\cmd.exe';
      If FileExists(FileName) Then ShellExecute(Handle, 'runas', FileName)
      Else ShowMessage(Format('Не бывает "%s"...', [FileName]));

      Скомпилируем, запустим: FileExists() утверждает что файл есть, а ShellExecute() падает с "путь не найден".
      А если запустим [i]этот же[/i] exe-файл, только под админом - тогда ShellExecute() уже внезапно работает.
      Но зачем мне тогда ShellExecute(), если права админа есть? Когда они есть - CreateProcess() справляется как часы.

      Удалить
    6. А вот с "regedit" вообще какая-то отдельная головоломка... Но об этом уже попозже...
      Кстати, про "есть и другие варианты" - можно всё же, пожалуйста, их названия/обозначения? А то гугл без конкретных названий мне выдаёт кашу... Спасибо!

      Удалить
    7. Интересно. Получается следующее:

      1. Если ShellExecute(Ex) видит, что вы запущены под админом, то запускает процесс. При этом процесс запускается вашим процессом (32-битным). И в этом случае C:\Windows\SysNative\cmd.exe сработает как вы ожидаете.

      2. Если ShellExecute(Ex) видит, что вы НЕ запущены под админом, то она делает повышение привилегий. Поскольку это невозможно сделать для уже запущенного процесса (вашего), она передаёт запрос на повышение во внешний процесс (я думаю, что технически это сделано опять же через COM). Видимо, эта третья сторона - родной для системы битности (т.е. 64). Соответственно, для неё C:\Windows\SysNative\cmd.exe не существует.

      Я бы сделал так: вместо cmd.exe запустил бы runas самого себя с параметром; при старте проверял бы параметр - если указан, запустить что надо и сразу выйти (ну или ждать окончания работы процесса, если вам его нужно ждать). Тогда, поскольку запуск процесса всегда будет выполняться вашим 32-битным процессом, то вопроса с разногласием по поводу алиасов не будет.

      Удалить
    8. > Кстати, про "есть и другие варианты" - можно всё же, пожалуйста, их названия/обозначения?

      1. Хэлпер-процесс с манифестом "всегда как админ".
      2. Служба.
      3. Задание в Task Scheduler с нужными опциями.
      4. Возможно, можно сделать руками через CreateProcessAsUser/CreateProcessWithLogon, но я, как бы, не проверял.

      Удалить
    9. Ну собственно выше я примерно так и предположил, просто не знаю как это проверить точно.
      Думаю, высока ли вероятность что передаёт запрос через этот же самый COM Elevation Moniker?

      Спасибо огромное!

      Случай с "regedit" ещё запутанее, но чёткое описание страннстей пока не готово, может попозже допишу. :)

      Удалить
  12. Добрый день!
    Вызов функции ShellExecuteEx с "runas" приводит к появлению диалога ввода логина и пароля для запуска приложения с правами администратора.
    Подскажите, пожалуйста, а как можно вызвать диалог ввода логина и пароля для запуска от имени другого пользователя, но без прав администратора?
    Ну это как в проводнике нажимаешь на exe-шнике правой кнопкой мыши удерживая кнопку Shift и там кроме пункта "Запуск от имени администратора" появляется ещё пункт "Запуск от имени другого пользователя".
    В интернете никак не могу отыскать правильный ответ - везде предлагают запуск от имени другого пользователя, но с повышением прав до администратора...

    ОтветитьУдалить
    Ответы
    1. Вот же ж блин. Стоило мне только здесь задать вопрос и я почти сразу же и нашёл ответ. Хотя правда и не прямой ответ, а только подсказку, но и этого мне хватило :)
      На одном сайте было написано, что в реестре есть такой ключик: HKEY_CLASSES_ROOT\exefile\shell\runas\command и поэтому ShellExecuteEx с "runas" работает именно так.
      Для интереса я запустил редактор реестра и обнаружил там кроме ключика "runas" ещё один интересный ключик "runasuser", попробовал его в ShellExecuteEx вместо "runas" - и оно сработало так как мне и нужно было :)
      Спасибо за помощь! :)
      Впрочем если вы знаете ещё какой-то способ сделать то же самое, то прошу поделиться со мной. Потому что под WinXP запуск ShellExecuteEx с"runasuser" не работает...

      Удалить

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

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

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

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

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

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