Запуск чужой программы из своей |
21.11.2019 Вадим Исаев |
В этой статье рассказывается о том, как из своей собственной программы запускать другие программы и получать от них результат. Рассказано о специфике запуска из своей 32-ух битной программы системных утилит в Windows64.
На сегодняшний день в современном Паскале есть такое большое количество классов, процедур и функций, что казалось бы можно легко и быстро делать всё что угодно. Увы, увы, на деле часто оказывается, что не только нелегко, но и непросто. Однако зачастую есть готовые программы, которые прекрасно делают ту или иную подзадачу, которую нужно сделать и вашей программе. Можно, конечно, найти исходный код тех программ и потихому вставить его к себе, но не всегда это возможно. Во-первых, исходников может и не быть вовсе. Во-вторых, если исходники и есть, то переписывать их с языков C\D\Java или вообще какого-нибудь экзотического Common Lisp нет не только желания, но даже и возможности в силу специфики того или иного языка. Поэтому чтобы сильно не заморачиваться, нужно (желательно невидимо для глаз пользователя) запустить чужую программу, получить от неё ответ и уже в своей программе использовать этот ответ для собственных нужд. Здесь прекрасно подходят различные консольные программы, у которых есть входные параметры, а ответ они выдают в текстовом виде или, к примеру, картинку.
В своё время разработчики TurboPascal добавили процедуру "Exec()" для возможности запуска чего-либо прстороннего. Туда можно было передавать параметры, однако с ответом другой программы и вообще результатом её деятельности работать было неудобно. В Delphi ничего придумывать не стали, а просто советовали использовать функцию WinAPI ShellExecute(). С появлением Kylix дела стали печальны, т.к. WinAPI в UNIX не работает. Ну, хорошо, можно было бы использовать условную компиляцию и для одной ОС запускать одну функцию, а для другой другую. Однако и такой способ тоже привязывает возможность запуска к определённой ОС. Тем более, что сейчас вполне доступны не одна-две ОС, а чуть ли не десяток. FreePascal предлагает пару способов запуска чужой программы, которые вообще к ОС не привязаны. Конечно, где-то там глубоко внутри есть условная компиляция, но программиста она уже не касается и он над этим заморачиваться не должен.
Способы запуска:
На самом деле ООП-метод вовсе не является сложным, но если вы не очень большой фанатик ООП и привыкли только к функциям и процедурам, то запуск с помощью функций вам может больше понравиться.
Кардинальное различие между классами "TProcess" для чистого FreePascal и "TProcessUTF8" для Lazarus - возможность в последнем использовать не сильно напрягаясь символы национального алфавита в названии запускаемых программ. Всё остальное полностью одинаково. Для использования класса "TProcess" нужно подключить к своей программе модуль "Process". В Lazarus на форму нужно поместить квазивизуальный компонент "TProcess" или "TProcessUTF8", которые находится на вкладке "System" (см. рис. 1), тогда нужный модуль добавится сам собой.
Небольшая ложка дёгтя
Если вы пользуетесь компонентом Lazarus, то первым по счёту свойством, куда он вам предложит ввести название запускаемой программы - "ApplicationName". Это свойство уже давно объявлено устаревшим (deprecated). Следующим по счёту идёт "CommandLine", которое тоже объявлено устарешим. Когда-то давно в это свойство вводилось название программы и все запускаемые вместе с ним параметры. Сегодня для входных параметров сделан более удобный и понятный список строк - "Parameters" типа TStrings. Хотя предыдущие два свойства пока ещё остаются рабочим вариантом, но при компиляции вы увидите предупреждение.
Название запускаемой программы нужно вводить в свойство "Executable" (см. рис.2) и никуда более, если вы не хотите при очередной смене версии компилятора получить неработающую программу.
Рисунок 2. Свойство для названия запускаемой программы
Основные свойства, которые нужны для запуска чужой программы:
После того, как все нужные свойства заполнены (как минимум должно быть имя программы в свойстве Executable), новый процесс можно запустить с помощью метода Execute(). После завершения работы и если что-то пошло не так, можно проверить свойства ExitCode и ExitStatus. Во многих случаях они будут показывать одно и тоже. Различие между ними в том, что ExitStatus - это сообщение ОС о результатах работы программы, а ExitCode - это сообщение самой программы. Проверять желательно и то и другое, поскольку сама программа не всегда в состоянии сообщить о том, что с ней произошло, а ОС не всегда знает о том, что там происходило в программе. Естественно речь идёт исключительно о какой-нибудь нештатной ситуации. Если программа отработала правильно, оба этих свойства нам не особо нужны.
Насильно прервать работу запущеной программы можно с помощью метода Terminate(КодОшибки).
Если программа запущена в режиме ожидания (т.е. с флагом poRunSuspended), запустить в работу её можно с помощью метода Resume(), а опять приостановить с помощью метода Suspend().
Приоритет запуска программы можно выставить с помощью свойства Priority (относится только к Windows):
Давайте напишем простенькую программку, которая определяет, запущен ли в нашей системе какой-нибудь совершенно определённый процесс. Для усложнения программка будет универсальная, работающая как в Linux, так и в Windows.
{----------------------------------------------- Запуск чужой программы из своей с помощью объекта класса TProcess. Параметры запускаемой программы в одной строке с названием программы. Пример: поиск запущенного процесса. ОС: Windows и Linux. -----------------------------------------------} {$codepage UTF8} Uses Classes, SysUtils, Process; {$IFDEF WINDOWS} function Wow64DisableWow64FsRedirection(x: Pointer): longbool; stdcall; external 'Kernel32.dll' name 'Wow64DisableWow64FsRedirection'; function Wow64RevertWow64FsRedirection (x: Pointer): longbool; stdcall; external 'Kernel32.dll' name 'Wow64RevertWow64FsRedirection'; {$ENDIF} Var AProcess: TProcess; AStringList: TStringList; // Здесь будет вывод работы процесса proc: string; {$IFDEF WINDOWS} pnt: Pointer; {$ENDIF} Begin WriteLn('Какой процесс ищем?'); ReadLn(proc); AStringList := TStringList.Create; AProcess := TProcess.Create(nil); // Эти опции говорят, что нужно дождаться окончания // работы чужой программы и её вывод положить в // специальный закуток AProcess.Options := AProcess.Options + [poWaitOnExit, poUsePipes]; {$IFDEF WINDOWS} AProcess.Executable := 'tasklist /FI "IMAGENAME eq '+proc+'"'; {$ELSE} AProcess.Executable := 'ps -C '+proc; {$ENDIF} // {$IFDEF WINDOWS} // Wow64DisableWow64FsRedirection(pnt); // {$ENDIF} AProcess.Execute; // {$IFDEF WINDOWS} // Wow64RevertWow64FsRedirection(pnt); // {$ENDIF} // Вытаскиваем вывод работы из закутка процесса // в наш набор строк, так будет проще обработать // вывод AStringList.LoadFromStream(AProcess.Output); // Если строк больше 1, // значит процесс в памяти есть If AStringList.Count>1 Then WriteLn('Процесс '+proc+' в памяти сидит!') Else WriteLn('Процесса '+proc+' в памяти не найден...'); AStringList.Free; AProcess.Free; End.
На первый взгляд исходник выглядит черезвычайно устрашающим, но тут я не виноват, клянусь своей треуголкой. Если бы не Windows вообще и 64-ёх битная Windows в частности, то код выглядел бы в два раза проще. Как-то раз пытался я запустить из своей 32-ух битной прграммы консольную утилиту, которая сидит в каталоге SYSTEM32 и получил сообщение, что такая утилита не найдена. При этом из командного файла она же запускалась прекрасно. Интернет подсказал, что хитрый Win64 любит иногда из системного каталога SYSTEM32 перенаправлять в системный каталог SYSWOW64 и если такое случается, то перенаправление нужно отключить функцией "Wow64DisableWow64FsRedirection()", а потом немедленно вернуть перенаправление на место с помощью "Wow64RevertWow64FsRedirection()", пока не случилось чего-нибудь ужасного. Так что если у вас что-то не будет запускаться, то нужно расскоментировать магические заклинания, которые начинаются на "Wow64". Это не болезнь именно Паскаля, а особенность работы 32-ух битных приложений в 64-ёх битной Windows. Почитать можно здесь [1].
Общие пояснения к программе:
Следующий примерчик показывает помещение входных параметров запускаемой программы в отдельное свойство "Parameters".
{----------------------------------------------- Запуск чужой программы из своей с помощью объекта класса TProcess. Параметры запускаемой программы заносятся отдельно в специальное поле TProcess.Parameters. Пример: поиск запущенного процесса. ОС: Windows и Linux. -----------------------------------------------} {$codepage UTF8} Uses Classes, SysUtils, Process; {$IFDEF WINDOWS} function Wow64DisableWow64FsRedirection(x: Pointer): longbool; stdcall; external 'Kernel32.dll' name 'Wow64DisableWow64FsRedirection'; function Wow64RevertWow64FsRedirection (x: Pointer): longbool; stdcall; external 'Kernel32.dll' name 'Wow64RevertWow64FsRedirection'; {$ENDIF} Var AProcess: TProcess; AStringList: TStringList; // Здесь будет вывод работы процесса proc: AnsiString; {$IFDEF WINDOWS} pnt: Pointer; {$ENDIF} Begin WriteLn('Какой процесс ищем?'); ReadLn (proc); AStringList := TStringList.Create; AProcess := TProcess.Create(nil); // Эти опции говорят, что нужно дождаться окончания // работы чужой программы и её вывод положить в // специальный закуток AProcess.Options := AProcess.Options + [poWaitOnExit, poUsePipes]; {$IFDEF WINDOWS} AProcess.Executable := 'tasklist.exe'; AProcess.Parameters.Add('/FI "IMAGENAME eq '+proc+'"'); {$ELSE} AProcess.Executable := 'ps'; AProcess.Parameters.Add('-C '+proc); {$ENDIF} // На всякий случай. Запуск из нашего 32-ух битного приложения // системной утилиты в Win64. // {$IFDEF WINDOWS} // Wow64DisableWow64FsRedirection(pnt); // {$ENDIF} AProcess.Execute; // {$IFDEF WINDOWS} // Wow64RevertWow64FsRedirection(pnt); // {$ENDIF} // Вытаскиваем вывод работы из закутка процесса // в наш набор строк, так будет проще обработать // вывод AStringList.LoadFromStream(AProcess.Output); // Если строк больше 1, // значит процесс в памяти есть If AStringList.Count>1 Then WriteLn('Процесс '+proc+' в памяти сидит!') Else WriteLn('Процесс '+proc+' в памяти не найден...'); AStringList.Free; AProcess.Free; End.
Кстати говоря, если вы из вышепредставленного кода хотите сделать функцию, но не хотите передавать в неё параметры типа TStringList, то есть возможность организации параметров в виде одной строки. Допустим строка эта называется "params". Все входные параметры запускаемой программы нужно разбавлять символом перевода строки (#10), примерно так:
params := '-a param1'#10'-b param2'#10'-c param3';
а потом засунуть эту строку в свойство "Parameters" таким образом:
AProcess.Parameters.Text := params;
TStringList прекрасно воспринимает символ #10 и строки параметров будут разделены правильно.
В модуле Process есть специальная процедура CommandToList(), которая переделывает подобную строку из всех параметров в TSringList. Однако пользоваться ею надо с умом. Разделение в ней происходит по символу пробела или любым символом перевода строки. Чтобы в одну строку были занесены параметры типа "ключ значение", которые разделены между собой пробелом, ключ и значение нужно группировать с помощью двойных кавычек:
{$codepage UTF8} Uses Classes, SysUtils, Process; Var AStringList: TStringList; s: string; i: integer; Begin AStringList:=TStringList.Create; WriteLn('Первый вариант, неправильный:'); WriteLn; s:='-a param1 -b param2 -c param3'; CommandToList(s, AStringList); For i:=0 To AStringList.Count-1 Do WriteLn(AStringList[i]); WriteLn; WriteLn('Второй вариант, правильный:'); WriteLn; s:='"-a param1" "-b param2" "-c param3"'; AStringList.Clear; CommandToList(s, AStringList); For i:=0 To AStringList.Count-1 Do WriteLn(AStringList[i]); AStringList.Free; End.
Функций запуска примерно 3 штуки. Примерно, потому что формально их больше, но часть функций из модуля "Process" объявлены как "depreceted", т.е. устаревшие, а часть функций это просто модификации и вряд ли их стоит принимать за самостоятельные.
ExecuteProcess(const exename: string; const parameters: string или array of string; flags: TExecuteFlags): integer;
Эта функция - почти точная копия процедуры Exec() из TurboPascal. Основное отличие - в возвращаемом результате будет содержаться код ошибки выполнения чужой программы. Функция является простейшей надстройкой над функциями запуска программы для API той ОС, в который вы работаете. Надстройка, конечно, сильно упрощённая, однако большой проблемы в этом нет, поскольку запуск программы обычно осуществлется просто и незатейливо.
Входные параметры:
Возвращаемое значение - код ошибки выполнения. Если всё в порядке, будет возвращён 0, а вот если нет, то на код полюбоваться уже не придётся - сработает прерывание. И если прерывание никак не обработать, то программа просто вывалится. Поэтому, если есть вероятность ошибки запуска, лучше эту функцию завернуть в Try ... Except ... End и анализировать ошибку именно там (on E : EOSError do).
RunCommandIndir(const curdir : string; const exename : string; const parameters : array of string; out outputstring: string; out exitstatus : integer; // Необязательный Options : TProcessOptions = []): integer;
Эта и следующая функция являются надстройкой для класса TProcess, поэтому параметры Options для них полностью аналогичны этому же свойству TProcess.
Входные параметры:
Выходные параметры:
Необязательный входной параметр:
Возвращаемое функцией значение:
RunCommand(const exename : string; const parameters : array of string; out outputstring: string; Options : TProcessOptions = []): boolean;
Ещё более сокращённый вариант предыдущей функции. Все параметры аналогичны предыдущим.
Из специфики двух последних функций запуска стоит упомяноуть, что флаг "poUsePipes", который заведует принятием вывода отработавшей программы в специальный поток, здесь указывать не нужно, он уже стоит по умолчанию.
Для простого запуска, например браузера с адресом какого-нибудь сайта, вполне подойдёт простая функция из SysUtils:
{----------------------------------------------- Запуск чужой программы из своей с помощью функции. Пример. Открытие в браузере сайта www.freepascal.ru. ОС: Windows и Linux. ------------------------------------------------} {$codepage UTF8} Uses SysUtils; Var prog, params: string; Begin {$IFDEF WINDOWS} prog := '"c:\Program Files (x86)\Mozilla Firefox\firefox.exe"'; {$ELSE} prog := 'firefox'; {$ENDIF} params := 'www.freepascal.ru'; Try ExecuteProcess(prog, params); Except On E: EOSError Do WriteLn('Вах, какая ужасная ошибка с номером ', E.ErrorCode); End; End.
Функции из модуля Process будут интересны в плане более гибкого управления запускаемой программы с помощью параметра Options или задания специального каталога для жизнедеятельности этой программы.
{----------------------------------------------- Запуск чужой программы из своей с помощью функции. Пример. Поиск зарегистрированного в системе пользователя. ОС: Windows и Linux. -----------------------------------------------} {$codepage UTF8} Uses Classes, SysUtils, Process; {$IFDEF WINDOWS} function Wow64DisableWow64FsRedirection(x: Pointer): longbool; stdcall; external 'Kernel32.dll' name 'Wow64DisableWow64FsRedirection'; function Wow64RevertWow64FsRedirection (x: Pointer): longbool; stdcall; external 'Kernel32.dll' name 'Wow64RevertWow64FsRedirection'; {$ENDIF} Var prog, ans, user_: String; params: array of String; outstr: TStringList; {$IFDEF WINDOWS} pnt: Pointer; {$ENDIF} Begin WriteLn('Какого пользователя ищем?'); ReadLn(user_); SetLength(params, 1); outstr:=TStringList.Create; {$IFDEF WINDOWS} prog := 'query.exe'; params[0] := 'USER "'+user_+'"'; {$ELSE} prog := 'w'; params[0] := user_; {$ENDIF} // На всякий случай. Запуск из нашего 32-ух битного приложения // системной утилиты в Win64. {$IFDEF WINDOWS} Wow64DisableWow64FsRedirection(pnt); {$ENDIF} // Вариант запуска 1 - отдельно программа, // отдельно входные параметры RunCommand(prog, params, ans, []); outstr.Text:=ans; // Если строк больше 1, // значит пользователь найден If outstr.Count>1 Then WriteLn('Пользователь '+user_+' пришёл на работу.') Else WriteLn('Пользователь '+user_+' дрыхнет дома.'); // Вариант запуска 2 - входные параметры // собираются вместе прямо в функции RunCommand(prog, [params[0]], ans, []); outstr.Text:=ans; // Если строк больше 1, // значит пользователь найден If outstr.Count>1 Then WriteLn('Пользователь '+user_+' пришёл на работу.') Else WriteLn('Пользователь '+user_+' дрыхнет дома.'); // Вариант запуска 3 - и программа и // входные параметры в одной строке RunCommand(prog+' '+params[0], [], ans, []); outstr.Text:=ans; // Если строк больше 1, // значит пользователь найден If outstr.Count>1 Then WriteLn('Пользователь '+user_+' пришёл на работу.') Else WriteLn('Пользователь '+user_+' дрыхнет дома.'); {$IFDEF WINDOWS} Wow64RevertWow64FsRedirection(pnt); {$ENDIF} End.
Несмотря на то что, по сравнению с TurboPascal, запуск чужих программ усложнился, он стал более гибким и функционально законченым. Вдобавок, по сравнению с Delphi, не нужно принимать никаких хитрых мер, чтобы заполучить вывод чужой программы в свою. Это позволяет не ломать голову над собственным кодом и не лезть в дебри чужого API, чтобы получить у себя этот же функционал, который уже реализован в чужой программе.