PHP скрипты в системном администрировании: от простых exec до демонов
Основные подходы к созданию системных PHP скриптов
Как обеспечить безопасное выполнение системных команд с полным контролем потоков и ошибок?
Решение: пользовательский класс на основе proc_open
Наиболее эффективный способ - использование функций proc_open и proc_close, которые позволяют управлять потоками ввода-вывода (stdin, stdout, stderr) и получать код возврата. Это предотвращает смешивание выводов и даёт возможность обрабатыть ошибки отдельно.
<?php
class SystemCommand {
private string $command;
private array $descriptorspec = [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
2 => ["pipe", "w"] // stderr
];
private ?resource $process = null;
private array $pipes = [];
public function __construct(string $command) {
$this->command = $command;
}
public function execute(): array {
$this->process = proc_open($this->command, $this->descriptorspec, $this->pipes);
if (!is_resource($this->process)) {
throw new \RuntimeException('Не удалось запустить процесс.');
}
// Закрываем stdin, так как не передаём данные
fclose($this->pipes[0]);
$stdout = stream_get_contents($this->pipes[1]);
fclose($this->pipes[1]);
$stderr = stream_get_contents($this->pipes[2]);
fclose($this->pipes[2]);
$returnCode = proc_close($this->process);
return [
'stdout' => $stdout,
'stderr' => $stderr,
'return_code' => $returnCode
];
}
}
try {
$cmd = new SystemCommand('ls -la /tmp');
$result = $cmd->execute();
echo "Код возврата: " . $result['return_code'] . "\n";
echo "STDOUT:\n" . $result['stdout'];
if ($result['stderr']) {
echo "STDERR:\n" . $result['stderr'];
}
} catch (\Exception $e) {
echo "Ошибка: " . $e->getMessage();
}
?>
скрипты php система (системные php скрипты)
Типичные ошибки и их решение:
- Блокировка при большом объёме вывода. Если команда выдаёт много данных, чтение из pipes может заблокироваться. Решение - читать потоки асинхронно или установить неблокирующий режим (stream_set_blocking).
- Забытые открытые pipes. Всегда закрывайте pipes после чтения, иначе процесс зависнет. Используйте finally или деструктор.
- Некорректные пути или экранирование. Для команды используйте escapeshellcmd или escapeshellarg для каждого аргумента.
Как выполнить команду оболочки и получить только стандартный вывод?
Для простых задач, когда не требуется разделение stdout/stderr, применяется функция exec. Она возвращает только последнюю строку вывода, но можно получить массив всех строк, передав второй аргумент.
<?php
$output = [];
$returnCode = 0;
exec('df -h', $output, $returnCode);
if ($returnCode === 0) {
echo implode("\n", $output);
} else {
echo "Команда завершилась с ошибкой";
}
?>
Index php pid (pid процессов)
Проблема: функция exec не даёт доступа к stderr. При ошибке вывод может быть пустым, а код возврата ненулевым. Для диагностики потребуется перенаправление stderr в stdout (например, 'df -h 2>&1').
Также при использовании exec без должного экранирования возникает риск инъекции команд.
Как получить полный вывод команды, включая stderr, одной строкой?
Функция shell_exec возвращает stdout команды как строку, но не предоставляет код возврата. Для включения stderr используйте перенаправление.
<?php
// Перенаправление stderr в stdout
$output = shell_exec('ls /nonexistent 2>&1');
echo htmlspecialchars($output);
?>
Недостаток:
- Отсутствует код возврата - невозможно определить успешность выполнения.
- При большом выводе может исчерпаться память.
- Нет возможности передать данные на stdin.
Как вывести результат команды непосредственно в браузер без буферизации?
Для потокового вывода (например, при просмотре логов или длительных процессах) используется passthru. Она передаёт stdout команды напрямую в выходной поток PHP.
<?php
ob_implicit_flush(true);
ob_end_flush();
$cmd = 'tail -f /var/log/syslog';
$handle = popen($cmd, 'r');
while (!feof($handle)) {
echo fgets($handle);
ob_flush();
flush();
}
pclose($handle);
?>
Проблема: popen открывает однонаправленный канал. Если команда требует взаимодействия (например, запрос пароля), придётся использовать proc_open. Также необходимо учитывать таймауты - длительный процесс может повесить скрипт.
Для предотвращения зависания устанавливайте время ожидания через stream_set_timeout.
Как запустить параллельные системные задачи для повышения производительности?
Для выполнения нескольких команд одновременно применяется расширение pcntl (только Unix). Используйте pcntl_fork для создания дочерних процессов. Каждый дочерний процесс выполняет свою команду, а родитель ожидает завершения.
<?php
$commands = [
'ping -c 1 8.8.8.8',
'ping -c 1 1.1.1.1',
'ping -c 1 192.168.1.1'
];
$pids = [];
foreach ($commands as $cmd) {
$pid = pcntl_fork();
if ($pid == -1) {
die("Не удалось создать процесс");
} elseif ($pid) {
// Родительский процесс сохраняет PID
$pids[] = $pid;
} else {
// Дочерний процесс выполняет команду
pcntl_exec('/bin/sh', ['-c', $cmd]);
exit(); // Если pcntl_exec не сработает
}
}
// Ожидание завершения всех дочерних процессов
foreach ($pids as $pid) {
pcntl_waitpid($pid, $status);
}
echo "Все команды выполнены\n";
?>
Важные замечания:
- Расширение pcntl не доступно в Windows.
- Необходимо быть осторожным с разделяемыми ресурсами (файлы, базы данных) - каждый дочерний процесс имеет копию памяти на момент fork.
- Дочерним процессам может потребоваться пересоединиться к БД или закрыть дескрипторы.
- Не забывайте вызывать pcntl_wait, иначе зомби-процессы заполнят таблицу процессов.
Как организовать циклический мониторинг с помощью PHP-скрипта, работающего как демон?
Для создания долгоживущих процессов (демонов) используйте комбинацию pcntl_fork, setsid и обработки сигналов. Альтернатива - использовать готовые библиотеки, например, ReactPHP или Swoole. Ниже пример простого демона, который каждые 10 секунд проверяет загрузку CPU.
<?php
// Создание демона
$pid = pcntl_fork();
if ($pid == -1) {
die("Не удалось форкнуть");
} elseif ($pid) {
// Родитель завершается
exit();
}
// Дочерний процесс становится лидером сессии
posix_setsid();
// Закрываем стандартные потоки
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
// Бесконечный цикл мониторинга
while (true) {
$load = sys_getloadavg();
$load1min = $load[0];
if ($load1min > 1.0) {
// Отправка предупреждения (например, в лог)
error_log("Высокая загрузка CPU: $load1min", 3, '/var/log/alert.log');
}
sleep(10);
}
?>
Проблемы при работе демонов:
- Утечки памяти - каждый цикл может накапливать неиспользуемые объекты. Используйте unset или перезапускайте процесс.
- Ошибки в логах - перенаправьте stderr в файл или syslog.
- Необходимость корректной остановки - обрабатывайте сигналы SIGTERM и SIGINT.
- Для production используйте systemd unit-файл для управления жизненным циклом демона.
Расширенные примеры и сценарии использования
<?php
/* Пример 1: Мониторинг дискового пространства и отправка уведомлений */
$threshold = 90; // процент заполнения
$diskUsage = shell_exec("df -h / | tail -1 | awk '{print $5}' | sed 's/%//'");
$diskUsage = (int)trim($diskUsage);
if ($diskUsage > $threshold) {
$message = "Внимание! Диск / заполнен на {$diskUsage}%. Необходимо очистить.";
mail('admin@example.com', 'Предупреждение о диске', $message);
echo $message;
} else {
echo "Дисковое пространство в норме: {$diskUsage}%";
}
?>
Вывод (при нормальном состоянии): Дисковое пространство в норме: 45%
<?php
/* Пример 2: Парсинг логов с помощью команды grep и обработка результатов */
$logFile = '/var/log/nginx/access.log';
$pattern = '404';
$command = "grep '{$pattern}' {$logFile} | tail -n 10";
$output = shell_exec($command);
$lines = explode("\n", trim($output));
foreach ($lines as $line) {
if (preg_match('/^(\S+) - - \[(.*?)\] "(.*?)" (\d+) (\d+) "(.*?)" "(.*?)"$/', $line, $matches)) {
echo "IP: {$matches[1]}, Дата: {$matches[2]}, Запрос: {$matches[3]}\n";
}
}
?>
Пример вывода: IP: 192.168.1.100, Дата: 28/May/2024:12:34:56 +0300, Запрос: GET /notfound HTTP/1.1 IP: 10.0.0.5, Дата: 28/May/2024:12:35:01 +0300, Запрос: GET /missing.jpg HTTP/1.1
<?php
/* Пример 3: Ротация логов с использованием PHP и системных утилит */
$logDir = '/var/log/myapp';
$maxSize = 10485760; // 10 MB
$currentLog = $logDir . '/app.log';
if (file_exists($currentLog) && filesize($currentLog) > $maxSize) {
$timestamp = date('Y-m-d_H-i-s');
$archive = $logDir . "/app_{$timestamp}.log.gz";
// Сжатие и перемещение
exec("gzip -c {$currentLog} > {$archive}");
file_put_contents($currentLog, ''); // Очистка текущего лога
echo "Лог файл ротирован: {$archive}\n";
} else {
echo "Ротация не требуется. Размер: " . filesize($currentLog) . " байт\n";
}
?>
Вывод при достижении лимита: Лог файл ротирован: /var/log/myapp/app_2024-05-28_12-30-00.log.gz
<?php
/* Пример 4: Параллельное выполнение нескольких команд с использованием proc_open и socket-соединений */
// Запуск трёх утилит мониторинга параллельно
$commands = [
'top -b -n 1 | head -5',
'df -h',
'free -m'
];
$processes = [];
foreach ($commands as $idx => $cmd) {
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
$process = proc_open($cmd, $descriptorspec, $pipes);
if (is_resource($process)) {
fclose($pipes[0]);
$processes[$idx] = [
'process' => $process,
'stdout' => $pipes[1],
'stderr' => $pipes[2]
];
}
}
// Сбор результатов
$results = [];
foreach ($processes as $idx => $pData) {
$stdout = stream_get_contents($pData['stdout']);
$stderr = stream_get_contents($pData['stderr']);
fclose($pData['stdout']);
fclose($pData['stderr']);
$returnCode = proc_close($pData['process']);
$results[$idx] = [
'stdout' => $stdout,
'stderr' => $stderr,
'return_code' => $returnCode
];
}
// Вывод результатов
foreach ($results as $idx => $r) {
echo "=== Команда $idx ===\n";
echo $r['stdout'];
if ($r['stderr']) {
echo "STDERR: " . $r['stderr'] . "\n";
}
echo "\n";
}
?>
Пример вывода (сокращён):
=== Команда 0 ===
top - 12:34:56 up 10 days, 1:23, 1 user, load average: 0.15, 0.20, 0.10
...
=== Команда 1 ===
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 50G 20G 28G 42% /
...
=== Команда 2 ===
total used free shared buff/cache available
Mem: 7985 1823 3128 245 3033 5821
<?php
/* Пример 5: Взаимодействие с процессом через stdin (например, для интерактивной утилиты) */
$descriptorspec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w']
];
$process = proc_open('sort', $descriptorspec, $pipes);
if (is_resource($process)) {
// Передаём данные на stdin
fwrite($pipes[0], "banana\napple\ncherry\n");
fclose($pipes[0]);
// Читаем отсортированный вывод
$sorted = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[2]);
proc_close($process);
echo "Отсортированные данные:\n$sorted";
if ($errors) {
echo "Ошибки:\n$errors";
}
}
?>
Отсортированные данные: apple banana cherry