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

Системные PHP скрипты - comments

En
скрипты php система (php)