Системные функции UNIX-подобных систем в PHP
Доброго времени суток!
Сегодня я расскажу про системные функции UNIX-подобных систем в PHP.
Для начала, под UNIX-подобными системами подразумеваются различные Linux'ы, BSD и т.п. Всё описанное здесь НЕ БУДЕТ работать в ОС Windows. Хотя, находились и извращенцы, которые PHP компилировали в cygwin, но это уже отдельный вопрос :-)
Итак, начнём.
С помощью данных функций можно управлять процессами - ветвить, посылать им различные сигналы. Причём, посылать сигналы процессу можно и из другого процесса, зная только его PID. Дла проверок нам понадобится:
- Сервер под управлением UNIX-подобной с
истемы с установленным PHP и модулями pcntl и posix
- SSH доступ к серверу и SSH клиент (или по старинке - сервер под ногами, монитор и клавиатура на столе)
Однако, SSH или физический доступ к серверу нужен только для наглядности примеров.
Сразу оговорюсь, что крайне не желательно использовать функции для управления процессами в скриптах, запускаемых через вебсервер. В некоторых случаях, эти функции и не доступны, в некоторых случаях, они будут работать не так, как это ожидается, а в некоторых случаях, возможен даже крах вебсервера. Если же нужно запускать скрипт через вебсервер - используйте функцию shell_exec() для запуска скрипта.
Для начала, приведу полный список сигналов. Для чего - далее поймёте.
Название Действие по умолчанию Описание Тип SIGABRT Завершение с дампом памяти Сигнал посылаемый функцией abort() Управление SIGALRM Завершение Сигнал истечения времени заданного alarm() Уведомление SIGBUS Завершение с дампом памяти Неправильное обращение в физическую память Исключение SIGCHLD Игнорируется Дочерний процесс завершен или остановлен Уведомление SIGCONT Продолжить выполнение Продолжить выполнение ранее остановленного процесса Управление SIGFPE Завершение с дампом памяти Ошибочная арифметическая операция Исключение SIGHUP Завершение Закрытие терминала Уведомление SIGILL Завершение с дампом памяти Недопустимая инструкция процессора Исключение SIGINT Завершение Сигнал прерывания (Ctrl-C) с терминала Управление SIGKILL Завершение Безусловное завершение Управление SIGPIPE Завершение Запись в разорванное соедиение (пайп, сокет) Уведомление SIGQUIT Завершение с дампом памяти Сигнал «Quit» с терминала Управление SIGSEGV Завершение с дампом памяти Нарушение при обращении в память Исключение SIGSTOP Остановка процесса Остановка выполнения процесса Управление SIGTERM Завершение Сигнал завершения (сигнал по умолчанию для утилиты kill) Управление SIGTSTP Остановка процесса Сигнал остановки с терминала (Ctrl-Z) Управление SIGTTIN Остановка процесса Попытка чтения с терминала фоновым процессом Управление SIGTTOU Остановка процесса Попытка записи на терминал фоновым процессом Управление SIGUSR1 Завершение Пользовательский сигнал № 1 Пользовательский SIGUSR2 Завершение Пользовательский сигнал № 2 Пользовательский SIGPOLL Завершение Событие отслеживаемое poll() Уведомление SIGPROF Завершение Истечение таймера профилирования Отладка SIGSYS Завершение с дампом памяти Неправильный системный вызов Исключение SIGTRAP Завершение с дампом памяти Ловушка трассировки или брейкпоинт Отладка SIGURG Игнорируется На сокете получены срочные данные Уведомление SIGVTALRM Завершение Истечение «виртуального таймера» Уведомление SIGXCPU Завершение с дампом памяти Процесс превысил лимит процессорного времени Исключение SIGXFSZ Завершение с дампом памяти Процесс превысил допустимый размер файла Исключение
В PHP определены константы для каждого из сигналов.
Описание функций я начну с простых POSIX функций, не служащих для управления процессами:
int posix_get_last_error() - Функция возвращает номер ошибки, который вернула предыдущая posix-функция. Параметры не требуются.
str posix_strerror(int errno) - Функция возвращает текстовое описание ошибки по её номеру.
Примеров к этим двум функциям не привожу.
Оговорюсь, что приведенные примеры не являются пособием по использованию функций, а только лишь наглядно показывают их работу. Следует не забывать, что любой системный вызов может завершиться ошибкой, к которой ваш скрипт должен быть готов (хотя, в нормальных условиях, эти ошибки не должны возникать).
arr posix_uname() - Функция возвращает ассоциативный массив с информацией о системе.
Пример:
<?php print_r(posix_uname()); ?> Результат выполнения: Array ( [sysname] => FreeBSD [nodename] => tem.dp.ua [release] => 6.3-RELEASE [version] => FreeBSD 6.3-RELEASE #4: Thu Mar 12 22:29:03 EET 2009 [email protected]:/src/sys/i386/compile/TEM [machine] => i386 )
sysname - название системы
nodename - имя системы
release - сборка системы
version - версия системы
machine - архитектура системы
domainname - DNS имя машины
Так же, возможно появление параметра domainname (доменное имя машины), однако, это доступно только в GNU системах или при использовании GNU libc.
str posix_getcwd() - Функция возвращает текущий рабочий каталог. Аналогом функции является функция getcwd(), доступная во всех ОС, в отличии от данной.
Пример: <?php echo 'Сurrent working directory is '.posix_getcwd(); ?> Результат выполнения: Сurrent working directory is /root
int posix_getuid() - Функция возвращает UID пользователя, который запустил процесс. Значение может на совпадать с т.н. "эффективным" UID. Аналог функции - getmyuid().
Пример:
<?php echo 'My UID: '.posix_getuid(); ?> Результаты: My UID: 0 - от имени root My UID: 84 - от имени eugen
int posix_geteuid() - Функция возвращает UID пользователя, от имени которого и с правами процесс реально работает. Чаще всего, EUID совпадает с UID.
Пример не привожу, т.к. он аналогичен предыдущему.
int posix_getgid() - Функция возвращает GID группы пользователя, запустившего процесс. Значение может не совпадать с т.н. "эффективным" GID. Аналог функции - getmygid().
Пример не привожу.
int posix_getegid() - Функция возвращает GID группы, с правами которой реально работает процесс. Чаще всего, значение EGID будет совпадать с GID.
Пример не привожу.
bool posix_setgid(int gid) - Функция устанавливает GID для текущего процесса. Однако, следует заметить, что использовать эту функцию, как правило, можно только с правами суперпользователя. В случае, если скрипт работает с правами суперпользователя, функция так же, изменяет и EGID.
Пример:
<?php echo "GID/EGID: ".posix_getgid()."/".posix_getegid()."\n"; posix_setgid(80); echo "GID/EGID: ".posix_getgid()."/".posix_getegid()."\n"; ?> Результат: GID/EGID: 0/0 GID/EGID: 80/80
bool posix_setegid(int gid) - Функция устанавливает EGID для текущего процесса. Однако, следует заметить, что использовать эту функцию можно только с правами суперпользователя. В случае, если скрипт работает с правами суперпользователя, функция НЕ изменяет и GID процесса. Чтобы изменить GID и EGID, необходимо сначала выполнить posix_setgid, а потом posix_setegid.
Пример:
<?php echo "GID/EGID: ".posix_getgid()."/".posix_getegid()."\n"; posix_setegid(80); echo "GID/EGID: ".posix_getgid()."/".posix_getegid()."\n"; ?> Результат: GID/EGID: 0/0 GID/EGID: 0/80
bool posix_setuid(int uid) - Функция устанавливает UID для текущего процесса. Для выполнения, как правило, требует привилегий суперпользователя. Аналогично posix_setgid, изменяет и EUID, в случае работы с правами суперпользователя.
Пример:
<?php echo "UID/EUID: ".posix_getuid()."/".posix_geteuid()."\n"; posix_setuid(80); echo "UID/EUID: ".posix_getuid()."/".posix_geteuid()."\n"; ?> Результат: UID/EUID: 0/0 UID/EUID: 80/80
bool posix_seteuid(int uid) - Функция устанавливает EUID для текущего процесса. Для выполнения, как правило, требует привилегий суперпользователя. Аналогично posix_setegid, НЕ изменяет EUID, в случае работы с правами суперпользователя.
Пример:
<?php echo "UID/EUID: ".posix_getuid()."/".posix_geteuid()."\n"; posix_seteuid(80); echo "UID/EUID: ".posix_getuid()."/".posix_geteuid()."\n"; ?> Результат: UID/EUID: 0/0 UID/EUID: 0/80
Примечание:
Если необходимо изменить группу и владельца процесса, функции нужно запускать в таком порядке: posix_setgid, posix_setegid, posix_setuid, posix_seteuid. В противном случае, теряются права суперпользователя и некоторые функции могут не сработать.
str posix_getlogin() - Функция возвращает имя пользователя, который запустил процесс, независимо от последующих вызовов posix_setuid и posix_seteuid.
Пример:
<?php echo "Login: ".posix_getlogin()."\n"; echo "UID/EUID: ".posix_getuid()."/".posix_geteuid()."\n"; posix_setuid(80); posix_seteuid(80); echo "UID/EUID: ".posix_getuid()."/".posix_geteuid()."\n"; echo "Login: ".posix_getlogin()."\n"; ?> Результат: Login: root UID/EUID: 0/0 UID/EUID: 80/80 Login: root
arr posix_getgrgid(int gid) - Функция возвращает массив с информацией о группе.
Пример:
<?php print_r(posix_getgrgid(0)); ?> Результат: Array ( [name] => wheel [passwd] => * [members] => Array ( [0] => root ) [gid] => 0 )
arr posix_getgrnam(str group) - Функция возвращает массив с информацией о группе. Только, в отличии от предыдущей функции, нужно знать имя группы, вместо GID.
Пример:
<?php print_r(posix_getgrnam('wheel')); ?>
Результат выполнения будет таким же, как и у предыдущей функции.
arr posix_getpwuid(int uid) - Функция возвращает массив с информацией о пользователе.
Пример:
<?php print_r(posix_getpwuid(84)); ?> Результат: Array ( [name] => eugen [passwd] => $1$************************ [uid] => 84 [gid] => 80 [gecos] => Just a Eugen [dir] => /home/eugen [shell] => /bin/sh )
Примечание:
name - имя пользователя
passwd - хэш пароля пользователя при запуске с правами суперпользователя, либо звёздочка (или другой знак) при запуске с правами обычного пользователя
uid - UID пользователя
gid - GID пользователя
gecos - текстовое описание пользователя
dir - домашний каталог пользователя
shell - Shell-интерпретатор пользователя
arr posix_getpwnam() - Функция возвращает массив с информацией о пользователе. Но, в отличии от предыдущей функции, нужно указать имя пользователя, вместо UID.
Пример:
<?php print_r(posix_getpwnam('eugen')); ?>
Результат выполнения будет таким же, как и у предыдущей функции.
arr posix_getrlimit() - Функция возвращает различные системные лимиты (мягкие и жесткие).
core Максимальный размер core-файлов. 0 - файлы не создаются. Если файл будет больше указанного размера, он обрезается до указанного. totalmem Максимальный объём памяти, выделяемый под процесс. virtualmem Максимальный размер виртуальной памяти (SWAP), выделяемой под процесс. data Максимальный размер сегмента данных процесса. stack Максимальный размер стека процесса. rss Максимальное ограничение (в страницах) для rss (числа виртуальных страниц в памяти). maxproc Максимальное количество процессов, которые может запустить владелец данного процесса. memlock Максимальное количество байт, которые могут быть заблокированы в памяти. cpu Ограничение процессорного времени. filesize Максимальный размер файла для текущей файловой системы. openfiles Максимальное количество открытых файловых дескрипторов.
Пример:
<?php print_r(posix_getrlimit()); ?> Результат: Array ( [soft core] => unlimited [hard core] => unlimited [soft data] => 536870912 [hard data] => 536870912 [soft stack] => 67108864 [hard stack] => 67108864 [soft virtualmem] => unlimited [hard virtualmem] => unlimited [soft totalmem] => unlimited [hard totalmem] => unlimited [soft rss] => unlimited [hard rss] => unlimited [soft maxproc] => 5547 [hard maxproc] => 5547 [soft memlock] => unlimited [hard memlock] => unlimited [soft cpu] => unlimited [hard cpu] => unlimited [soft filesize] => unlimited [hard filesize] => unlimited [soft openfiles] => 11095 [hard openfiles] => 11095 )
Далее опишу некоторые полезные функции для управления процессами. Данные примеры советую запускать из командной строки...
int posix_getpid() - Функция определяет PID (идентификатор) текущего процесса. Аналог функции - getmypid().
Позже, можно будет посылать сигналы процессу, зная его PID.
Пример:
<?php echo 'My PID is '.posix_getpid(); ?> Результат: My PID is 28088
int posix_getppid() - Функция возвращает PID родительского процесса. Для чего это нужно, поймёте позже...
Пример не привожу, т.к. он аналогичен предыдущему.
bool posix_kill(int pid, int sig) - Функция посылает сигнал процессу. Константы сигналов можно найти в таблице в начале статьи.
Пример:
<?php $pid = posix_getpid(); echo "My PID is ".$pid."\n"; echo "Sending SIGTERM to myself...\n"; posix_kill($pid,SIGTERM); echo "Bla-bla-bla.\n"; ?> Результат: My PID is 28144 Sending SIGTERM to myself...
Как видим, скрипт завершился и последний вызов echo не сработал. Выполнение скрипта прервалось. Естественно, зная PID, можно обмениваться сигналами с любыми принадлежащими вам процессами (либо со всеми процессами, если скрипт запускается с правами суперпользователя)
С модулем POSIX, надеюсь, более-менее познакомились. Далее, я расскажу о функциях модуля pcntl и покажу несколько хороших примеров.
Поведение функций PCNTL изменилось, начиная с PHP 5.3.0. На мой взгляд, старое поведение этих функций гораздо удобнее, но в статье я опишу оба варианта использования функций.
bool pcntl_signal(int signo, callback handler [, bool restart_syscalls=true]) - Функция связывает заданный сигнал с функцией, которая будет запускаться при поступлении заданного сигнала. В качестве единственного аргумента, в функцию будет передаваться идентификатор сигнала. Таким образом, можно делать одну общую функцию-обработчик для всех сигналов. Параметр restart_syscalls определяет, должен ли использоваться перезапуск системных вызовов; он нам не понадобится. Для работы, необходимо задать ticks: declare(ticks = 1); либо, для PHP>=5.3.0, вызывать функцию pcntl_signal_dispatch(). Причём, до вызова pcntl_signal_dispatch() поступившие сигналы не обработаются. Поэтому, можно вызывать эту функцию в цикле, а можно использовать ticks для запуска:
declare(ticks = 1); register_tick_function('pcntl_signal_dispatch');
Собственно, в качестве примера, приведу универсальный вариант:
<?php function signal_handler($sig) { // Как и в любой функции, здесь можно использовать глобальные переменные. switch($sig) { case SIGTERM: echo "SIGTERM received. Exitting...\n"; // Здесь можно выполнять различные действия перед завершением скрипта. Например, записать соотв. строку в лог, и т.п. exit(1); // Завершаемся с кодом, большим 0 break; case SIGINT: echo "SIGINT received. Exitting...\n"; // Здесь можно выполнять различные действия перед завершением скрипта (в случае прерывания по Ctrl+C). exit(2); // Завершение с другим кодом break; } } declare(ticks = 1); if(function_exists('pcntl_signal_dispatch')) // Проверяем наличие функции pcntl_signal_dispatch register_tick_function('pcntl_signal_dispatch'); // И регистрируем её как tick-функцию для запуска при выполнении каждой команды pcntl_signal(SIGTERM,'signal_handler'); // Устанавливаем обработчик для сигнала SIGTERM. pcntl_signal(SIGINT,'signal_handler'); // Устанавливаем обработчик для сигнала SIGINT. while(true) // Вводим скрипт в бесконечный цикл usleep(10); ?>
Запускаем скрипт в командной строке и прерываем его по Ctrl+C и видим примерно такое:
tem# php 1.php ^CSIGINT received. Exitting... tem#
Видим, что скрипт поймал сигнал SIGINT. Точно так же он себя поведёт при SIGTERM.
Забыл сказать, что сигнал безусловного завершения SIGKILL отловить невозможно. На то оно и безусловное завершение.
Функции pcntl_getpriority и pcntl_setpriority служат для определения и изменения приоритета процессов, соответственно. В данной статье я не буду описывать эти функции, т.к., как правило, эти функции не используются.
int pcntl_alarm(int time) - Функция посылает текущему процессу сигнал SIGALRM через заданное количество секунд. При необходимости, можно вызывать функцию повторно внутри обработчика (для циклического выполнения каких-либо действий раз в N секунд).
Пример:
<?php function signal_handler($sig) { switch($sig) { case SIGTERM: echo "SIGTERM received. Exitting...\n"; exit(1); break; case SIGINT: echo "SIGINT received. Exitting...\n"; exit(2); break; case SIGALRM: echo "ALARM! Time: ".date('H:i:s')."\n"; pcntl_alarm(1); break; } } declare(ticks = 1); if(function_exists('pcntl_signal_dispatch')) register_tick_function('pcntl_signal_dispatch'); pcntl_signal(SIGTERM,'signal_handler'); pcntl_signal(SIGINT,'signal_handler'); pcntl_signal(SIGALRM,'signal_handler'); pcntl_alarm(1); while(true) usleep(10); ?>
После запуска скрипта, видим, что скрипт каждую секунду выводит сообщение о срабатывании "будильника". Чтобы прервать скрипт достаточно нажать Ctrl+C (послать скрипту SIGINT):
tem# php 1.php ALARM! Time: 11:47:09 ALARM! Time: 11:47:10 ALARM! Time: 11:47:11 ALARM! Time: 11:47:12 ALARM! Time: 11:47:13 ALARM! Time: 11:47:14 ^CSIGINT received. Exitting... tem#
Следует отметить, что если вызвать pcntl_alarm в то время, пока предыдущий ждёт своего запуска, предыдущий alarm отменится, а функция вернёт количество секунд, оставшееся предыдущему alarm'у до запуска.
void pcntl_exec(str path [, arr args [, arr envs]]) - Функция запускает другую команду внутри памяти данного процесса. На самом деле, я не хотел описывать эту функцию в данной статье, т.к. она используется крайне редко.
Но, раз уже описываю функцию, то приведу пример:
<?php $args = array('-v'); $envs['MYVAR'] = 'test'; pcntl_exec('/usr/bin/env', $args, $envs); ?>
Результат выполнения будет таким:
MYVAR=test
Примечание:
Массив с аргументами - простой нумерованный массив. Массив с параметрами среды - ассоциативный, ключи - названия параметров, значения - их значения.
int pcntl_fork() - Отсюда всё только начинается... Функция ветвит текущий процесс, полностью копируя память в новый. После ветвления, два процесса работают независимо друг от друга, но могут обмениваться сигналами. Процесс, от которого происходит ветвление, называется родительским (parent), а ответвлённый процесс - дочерним (child). Существует ряд функций (которые будут описаны позже) для контроля дочернего процесса родительским. Однако, для контроля родительского процесса внутри дочернего можно использовать только лишь одну функцию - posix_getppid, описанную выше. Функция получает PID родительского процесса, после чего, можно обмениваться с ним сигналами. PID дочернего процесса внутри родительского уже известен, т.к. его возвращает данная функция. Использовать ветвление можно в разных целях - от запуска нескольких совершенно разных контроллируемых скриптов до реализации многопоточности в PHP (это ответ на многие споры по поводу многопоточности в PHP). При вызове, функция возвращает 0 в дочерний процесс и PID дочернего процесса в родительский. В случае неудачи, возвращает -1 и ветвление не происходит.
Примеров использования можно приводить много, но постараюсь уместить всё в одном:
<?php echo "Main PID: ".posix_getpid()."\n"; $pid = pcntl_fork(); // Fork you! :-D // Совсем без юмора нельзя :-) if($pid == -1) exit("Unable to fork\n"); // Хотя реально мне ниразу не попадалась эта ошибка. // В теории, это возможно, если // - превышен лимит на максимальное количество процессов // - при нехватки памяти // - при перегрузке процессора if(!$pid) { // Мы в дочернем процессе. // $pid равен нулю, можете сами в этом убедиться. // Процессы работают одновременно и независимо друг от друга. // Все выше объявленные переменные и функции родительского процесса будут доступны и здесь. sleep(5); echo "My (child) PID: ".posix_getpid()."\n"; echo "Parent PID: ".posix_getppid()."\n"; exit(0); // Обязательно нужно завершать дочерний процесс выходом с кодом 0 (или >0 при ошибках). // В противном случае, может произойти зацикливание либо "висящий" дочерний процесс. } echo "Child PID: ".$pid."\n"; // Это выведется раньше, чем 2 строки выше. sleep(10); // Подождём немного... // А здесь вовсе не обязательно использовать exit. ?>
При выполнении получим примерно такой результат:
tem# php 1.php Main PID: 30537 Child PID: 30538 My (child) PID: 30538 Parent PID: 30537 tem#
При этом никто не запрещает ветвление в цикле для реализации многопоточности или многозадачности. Однако, советую изучить и ниже описанные функции.
Родительский процесс может ветвиться несколько раз, так же, как и его дочерние процессы (т.к. они являются полноценными процессами).
int pcntl_wait(int &status [, int options=0]) - Функция просто ждёт завершения любого дочернего процесса. К сожалению, доступна только в PHP 5 (любой версии), но имеет аналог в виде pcntl_waitpid, который будет описан далее. status - переменная, в которую функция записывает статус выхода дочернего процесса, для использования в далее описанных функциях. options - необязательный параметр, про который я напишу в описании pcntl_waitpid. Здесь его использовать не рекомендую, т.к. он работает далеко не во всех системах. Возвращает функция PID завершившегося дочернего процесса, -1 при ошибке и 0, если ниодин дочерний процесс не завершился (при опции WNOHANG).
Пример приведу такой:
<?php $pid = pcntl_fork(); if($pid == -1) exit("Unable to fork\n"); if(!$pid) { sleep(5); echo "My (child) PID: ".posix_getpid()."\n"; echo "Parent PID: ".posix_getppid()."\n"; sleep(5); exit(0); } echo "Child ".$pid." started.\n"; echo "Child PID: ".pcntl_wait($status)." exited.\n"; ?>
Результат будет таким:
tem# php 1.php Child 30908 started. My (child) PID: 30908 Parent PID: 30907 Child PID: 30908 exited. tem#
При этом, последняя строка выведется только после завершения дочернего процесса.
int pcntl_waitpid(int pid, int &status [, int $options=0]) - Функция служит для тех же целей, что и предыдущая. Однако, во-первых, доступна и в PHP 4, а во-вторых, можно указать PID конкретного дочернего процесса, чтобы ждать только его завершения.
Опции:
WNOHANG - возвращать 0 и не ждать завершения процесса, если ниодин дочерний процесс не завершился
WUNTRACED - возвращать PID даже для дочерних процессов, статус завершения которых не удалось отследить
Для использования обеих опций следует делать так: WNOHANG | WUNTRACED
В качестве PID можно указывать PID конкретного дочернего процесса, а можно указать -1, тогда поведение функции будет таким же, как у pcntl_wait. Существуют и другие варианты запуска, однако, я их не описываю здесь, т.к. они практически не используются.
Пример использования не привожу, т.к. он аналогичен предыдущему.
На последок, расскажу про несколько функций для отслеживания кода и причины завершения дочерних процессов. Примеры для этих функций я приводить не буду, т.к. они покажутся вам очень простыми :-)
bool pcntl_wifexited(int status) - Функция возвращает true, в случае, если дочерний процесс завершился сам (вызов exit). В противном случае, возвращает false.
В случае, если функция вернёт true, можно получить код завершения дочернего процесса функцией pcntl_wexitstatus. В качестве параметра необходимо указать статус выхода, полученный от одной из двух предыдущих функций.
int pcntl_wexitstatus(int status) - Функция возвращает код завершения дочернего процесса. Функция может использоваться только если предыдущая функция вернула true.
bool pcntl_wifsignaled(int status) - Функция возвращает true, в случае, если дочерний процесс завершился из-за не отловленного сигнала. В противном случае, возвращает false.
В случае, если функция вернёт true, сам сигнал можно определить функцией pcntl_wstopsig. В качестве параметра необходимо указать статус выхода, полученный от одной из двух предыдущих функций.
int pcntl_wtermsig(int status) - Функция возвращает сигнал, который привёл к завершению дочернего процесса.
bool pcntl_wifstopped(int status) - Функция возвращает true, если дочерний процесс приостановлен. Это возможно, только если pcntl_waitpid (или pcntl_wait) вызван с опцией WUNTRACED. В противном случае, функция вернёт false.
int pcntl_wstopsig(int status) - Функция возвращает сигнал, который привёл к приостановке дочернего процесса.
На этом, пожалуй, я закончу статью.
Автор статьи Eugen.
Всем спасибо за внимание и удачных разработок!