Публикации FreePascal

Запуск чужой программы из своей

21.11.2019
Вадим Исаев
  1. Аннотация
  2. Введение
  3. ООП-метод запуска. TProcess
  4. Простые функции запуска
  5. Заключение
  6. Ссылки

1. Аннотация

В этой статье рассказывается о том, как из своей собственной программы запускать другие программы и получать от них результат. Рассказано о специфике запуска из своей 32-ух битной программы системных утилит в Windows64.

2. Введение

На сегодняшний день в современном Паскале есть такое большое количество классов, процедур и функций, что казалось бы можно легко и быстро делать всё что угодно. Увы, увы, на деле часто оказывается, что не только нелегко, но и непросто. Однако зачастую есть готовые программы, которые прекрасно делают ту или иную подзадачу, которую нужно сделать и вашей программе. Можно, конечно, найти исходный код тех программ и потихому вставить его к себе, но не всегда это возможно. Во-первых, исходников может и не быть вовсе. Во-вторых, если исходники и есть, то переписывать их с языков C\D\Java или вообще какого-нибудь экзотического Common Lisp нет не только желания, но даже и возможности в силу специфики того или иного языка. Поэтому чтобы сильно не заморачиваться, нужно (желательно невидимо для глаз пользователя) запустить чужую программу, получить от неё ответ и уже в своей программе использовать этот ответ для собственных нужд. Здесь прекрасно подходят различные консольные программы, у которых есть входные параметры, а ответ они выдают в текстовом виде или, к примеру, картинку.

В своё время разработчики TurboPascal добавили процедуру "Exec()" для возможности запуска чего-либо прстороннего. Туда можно было передавать параметры, однако с ответом другой программы и вообще результатом её деятельности работать было неудобно. В Delphi ничего придумывать не стали, а просто советовали использовать функцию WinAPI ShellExecute(). С появлением Kylix дела стали печальны, т.к. WinAPI в UNIX не работает. Ну, хорошо, можно было бы использовать условную компиляцию и для одной ОС запускать одну функцию, а для другой другую. Однако и такой способ тоже привязывает возможность запуска к определённой ОС. Тем более, что сейчас вполне доступны не одна-две ОС, а чуть ли не десяток. FreePascal предлагает пару способов запуска чужой программы, которые вообще к ОС не привязаны. Конечно, где-то там глубоко внутри есть условная компиляция, но программиста она уже не касается и он над этим заморачиваться не должен.

Способы запуска:

  • ООП-метод, с помощью класса "TProcess" (для Lazarus - "TProcessUTF8"). Поскольку ООП сегодня считается самым правильным стилем то я его поставил на первое место;
  • С помощью функций, которые прячут ООП внутрь себя, несколько упрощая запуск другой программы.

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

3. ООП-метод запуска. TProcess

Кардинальное различие между классами "TProcess" для чистого FreePascal и "TProcessUTF8" для Lazarus - возможность в последнем использовать не сильно напрягаясь символы национального алфавита в названии запускаемых программ. Всё остальное полностью одинаково. Для использования класса "TProcess" нужно подключить к своей программе модуль "Process". В Lazarus на форму нужно поместить квазивизуальный компонент "TProcess" или "TProcessUTF8", которые находится на вкладке "System" (см. рис. 1), тогда нужный модуль добавится сам собой.


Рисунок 1. Панель компонентов Lazarus. TProcess и TProcessUTF8

Небольшая ложка дёгтя

Если вы пользуетесь компонентом Lazarus, то первым по счёту свойством, куда он вам предложит ввести название запускаемой программы - "ApplicationName". Это свойство уже давно объявлено устаревшим (deprecated). Следующим по счёту идёт "CommandLine", которое тоже объявлено устарешим. Когда-то давно в это свойство вводилось название программы и все запускаемые вместе с ним параметры. Сегодня для входных параметров сделан более удобный и понятный список строк - "Parameters" типа TStrings. Хотя предыдущие два свойства пока ещё остаются рабочим вариантом, но при компиляции вы увидите предупреждение.

Название запускаемой программы нужно вводить в свойство "Executable" (см. рис.2) и никуда более, если вы не хотите при очередной смене версии компилятора получить неработающую программу.


Рисунок 2. Свойство для названия запускаемой программы

Основные свойства, которые нужны для запуска чужой программы:

  • Executable - сюда вносится имя запускаемой программы. Если программа находится в пределах стандартной переменной "PATH", то можно без указания каталога, если нет - то полное имя вместе с каталогом.
  • Parameters - дополнительные входные параметры. Это набор строк типа TStrings, поэтому один параметр в одной строке. Синтаксис всех параметров должен быть точно такой, какой полагается при обычном запуске программы в командной строке.
  • Options - опции запуска. Нужные опции перечисляются в квадратных скобках через запятую. В Lazarus ставится галочка возле соответствующей опции. Наиболее популярные опции, почти для всех ОС:
    • poUsePipes - весь вывод запущеной программы заносится в специальный поток (TProcess.Output), который можно потом загрузить в какой-нибудь более удобный для разглядывания компонент, например в TStringList или в TMemo;
    • poWaitOnExit - если запускающей программе обязательно нужно дождаться окончания работы запускаемой, то этот флаг как раз приостанавливает работу основного потока запускающей программы, пока не отработает та, которая из неё запущена. К примеру, дальнейшая работа будет строится на ответе чужой программы;
    • poStderrToOutPut - этот флаг бывает черезвычайно полезен, когда чужая программа выдаёт свои ошибки, ну или вообще какие-нибудь критически выжные сообщения не на стандартный вывод, которым обычно является экран компьютера. Здесь программу принудительно заставляют выводить вообще всё именно в стандартный вывод (TProcess.Output). Но если вам нужно мухи отдельно, а котлеты отдельно, то флаг не ставится а вывод ошибок тогда идёт в отдельный поток (TProcess.StdErr);
    • poRunSuspended - после запуска чужая программа переводится в режим ожидания, т.е. она ничего не делает, а ждёт от вас какого-нибудь пинка. Так, сходу, для чего это непременно может понадобиться я придумать не могу. Но наверняка и для этого флага найдутся веские причины.
    • Это не все опции, но остальные не особо актуальны.
  • CurrentDirectory - необязательное, но полезное свойство, если нужен специальный каталог, в котором будет работать программа. В этом случае все свои естественные потребности (например, рисунок или текстовый файл) она при работе будет искать здесь или выложит именно сюда.

После того, как все нужные свойства заполнены (как минимум должно быть имя программы в свойстве Executable), новый процесс можно запустить с помощью метода Execute(). После завершения работы и если что-то пошло не так, можно проверить свойства ExitCode и ExitStatus. Во многих случаях они будут показывать одно и тоже. Различие между ними в том, что ExitStatus - это сообщение ОС о результатах работы программы, а ExitCode - это сообщение самой программы. Проверять желательно и то и другое, поскольку сама программа не всегда в состоянии сообщить о том, что с ней произошло, а ОС не всегда знает о том, что там происходило в программе. Естественно речь идёт исключительно о какой-нибудь нештатной ситуации. Если программа отработала правильно, оба этих свойства нам не особо нужны.

Насильно прервать работу запущеной программы можно с помощью метода Terminate(КодОшибки).

Если программа запущена в режиме ожидания (т.е. с флагом poRunSuspended), запустить в работу её можно с помощью метода Resume(), а опять приостановить с помощью метода Suspend().

Приоритет запуска программы можно выставить с помощью свойства Priority (относится только к Windows):

  • ppNormal - приоритет нормальный, как у всех. Это значение стоит по умолчанию;
  • ppHigh - приоритет чуть выше нормального;
  • ppIdle - процесс будет работать только когда система простаивает без дела;
  • ppRealTime - режим реального времени. Хотя для компьютеров общего назначения это некоторое преувеличение, но по крайней мере процессор этому приложению будет давать максимальную квоту времени на выполнение, что заметно прибавит скорости работы.

Давайте напишем простенькую программку, которая определяет, запущен ли в нашей системе какой-нибудь совершенно определённый процесс. Для усложнения программка будет универсальная, работающая как в 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].

Общие пояснения к программе:

  1. Для Windows не забудьте к имени искомого процесса приписывать точку и расширение, иначе ничего не будет найдено. Чтобы освежить память, можно открыть в Windows "Диспетчер задач". Там наглядно будет видно, как должны выглядеть имена процессов;
  2. А вот теперь то, что из "Диспетчера задач" копировать ни в коем случае не надо. В 64-ёх битной винде, 32-ух битные процессы будут дополнительно помечены " *32". Несмотря на то, что эта пометка сидит в колонке "Имя образа", к именам процессов она никакого отношения не имеет;
  3. Как вы заметили, в свойство TProcess.Executable я поместил и название программы и её входные параметры. Действительно, на сегодняшний день это свойство является полным аналогом свойства "CommandLine". Будет ли так и дальше - посмотрим. А пока, если входной параметр коротенький и одинокий, его вполне можно писать вместе с названием программы. Если же жераметров много или они длинные, то для ясности их лучше помещать в свойство "Parameters".

Следующий примерчик показывает помещение входных параметров запускаемой программы в отдельное свойство "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. Процедура CommandToList() и параметры типа "ключ значение"

4. Простые функции запуска

Функций запуска примерно 3 штуки. Примерно, потому что формально их больше, но часть функций из модуля "Process" объявлены как "depreceted", т.е. устаревшие, а часть функций это просто модификации и вряд ли их стоит принимать за самостоятельные.

  1. В модуле "SysUtils":
              ExecuteProcess(const exename: string;
                             const parameters: string или array of string;
                                   flags: TExecuteFlags): integer;
    	

    Эта функция - почти точная копия процедуры Exec() из TurboPascal. Основное отличие - в возвращаемом результате будет содержаться код ошибки выполнения чужой программы. Функция является простейшей надстройкой над функциями запуска программы для API той ОС, в который вы работаете. Надстройка, конечно, сильно упрощённая, однако большой проблемы в этом нет, поскольку запуск программы обычно осуществлется просто и незатейливо.

    Входные параметры:

    • exename - имя запускаемой программы. Если программа лежит где-то вне системной переменной PATH, то имя писать вместе с полным путём;
    • parameters - входные параметры (ключи) запускаемой программы. Это может быть одна строка, где ключи разделены пробелами, как это делается при запуске из консоли или скрипта. Так же все ключи можно разметить в array of string, при этом одна ячейка (строка) массива - один ключ;
    • flags - тут можно добавить всего один флаг "ExecInheritsHandles", но только в том случае, если у вас Windows или OS/2. Во всех остальных случаях флаг бесполезен. Этот флаг передаёт файловые дескрипторы запускаемой программе.

    Возвращаемое значение - код ошибки выполнения. Если всё в порядке, будет возвращён 0, а вот если нет, то на код полюбоваться уже не придётся - сработает прерывание. И если прерывание никак не обработать, то программа просто вывалится. Поэтому, если есть вероятность ошибки запуска, лучше эту функцию завернуть в Try ... Except ... End и анализировать ошибку именно там (on E : EOSError do).

  2. В модуле "Process":
              RunCommandIndir(const curdir      : string;
                              const exename     : string;
                              const parameters  : array of string;
                              out   outputstring: string; 
                              out   exitstatus  : integer; // Необязательный
                                    Options     : TProcessOptions = []): integer;
    	

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

    Входные параметры:

    • curdir - рабочий каталог, где будет выполняться запускаемая программа. Может быть пустой строкой;
    • exename - имя запускаемой программы;
    • parameters - входные параметры для запускаемой программы. Массив строк. Одна строка - один параметр.

    Выходные параметры:

    • outputstring - это текстовые результаты работы программы, которые она могла бы выдать в консоль;
    • exitstatus - необязательный параметр. Здесь будет номер ошибки, если такое предусмотрено. Если же нет, эту переменную в функцию можно не вписывать.

    Необязательный входной параметр:

    • Options - набор флагов запуска, если нужно.

    Возвращаемое функцией значение:

    • Если вызывается с двумя выходными параметрами, то она возвратит 0 если попытка запуска успешна и -1 в случае какой-то ошибки;
    • Если без параметра exitstatus, то будет типа boolean и True в случае успешности попытки запуска.

  3. Так же в модуле "Process":
              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.

5. Выводы

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

6. Ссылки

  1. Wow64DisableWow64FsRedirection function
Актуальные версии
FPC3.0.4release
Lazarus1.8release
MSE4.6release
fpGUI1.4.1release
наши спонсоры ;)