[PHP]pclose関数の使い方を徹底解説!外部コマンド実行の終了処理をマスター

PHP

PHPで外部コマンドやプログラムを実行する際、popen関数でプロセスを開いた後は、適切に終了処理を行う必要があります。この記事では、PHPのpclose関数について、popenとの関係や実践的な使用方法を詳しく解説していきます。

pclose関数とは?

pcloseは、popen関数で開いたプロセスへのファイルポインタを閉じるための関数です。外部コマンドを実行した後の後処理として必須の関数で、リソースの適切な解放とプロセスの終了を保証します。

基本的な構文

int pclose(resource $handle)

パラメータ:

  • $handle: popenで開いたファイルポインタ

戻り値:

  • 実行されたプロセスの終了ステータス
  • エラーの場合は -1

popenとpcloseの関係

pcloseを理解するには、まずpopenとの関係を知る必要があります。

popenとは

popenは外部コマンドをパイプで開き、そのコマンドとの入出力を可能にする関数です。

resource popen(string $command, string $mode)
  • $command: 実行するコマンド
  • $mode: 'r'(読み取り)または'w'(書き込み)

基本的な使用の流れ

<?php
// 1. プロセスを開く
$handle = popen('ls -la', 'r');

// 2. 出力を読み取る
if ($handle) {
    while (!feof($handle)) {
        echo fgets($handle);
    }
    
    // 3. プロセスを閉じる(必須!)
    $status = pclose($handle);
    echo "終了ステータス: " . $status;
}
?>

なぜpcloseが必要なのか?

1. リソースの解放

開いたファイルポインタを閉じないと、システムリソースがリークします。特に長時間実行されるスクリプトでは深刻な問題になります。

2. プロセスの適切な終了

pcloseを呼び出すことで、子プロセスが適切に終了し、ゾンビプロセスの発生を防ぎます。

3. 終了ステータスの取得

プロセスが正常に終了したかどうかを確認できます。

<?php
$handle = popen('some-command', 'r');
// ... 処理 ...
$status = pclose($handle);

if ($status == 0) {
    echo "コマンドは正常に終了しました";
} else {
    echo "コマンドはエラーで終了しました: " . $status;
}
?>

実践的な使用例

1. システムコマンドの実行と結果の取得

<?php
function executeCommand($command) {
    $output = '';
    $handle = popen($command, 'r');
    
    if (!$handle) {
        return ['success' => false, 'output' => '', 'status' => -1];
    }
    
    // 出力を読み取る
    while (!feof($handle)) {
        $output .= fgets($handle);
    }
    
    // プロセスを閉じて終了ステータスを取得
    $status = pclose($handle);
    
    return [
        'success' => ($status == 0),
        'output' => $output,
        'status' => $status
    ];
}

// 使用例: ディスク使用量の確認
$result = executeCommand('df -h');
if ($result['success']) {
    echo "ディスク使用量:\n";
    echo $result['output'];
} else {
    echo "コマンドの実行に失敗しました (ステータス: {$result['status']})";
}
?>

2. ログファイルの監視

<?php
function tailLogFile($logFile, $lines = 10) {
    $command = "tail -n {$lines} " . escapeshellarg($logFile);
    $handle = popen($command, 'r');
    
    if (!$handle) {
        return false;
    }
    
    $logs = [];
    while (!feof($handle)) {
        $line = fgets($handle);
        if ($line !== false) {
            $logs[] = trim($line);
        }
    }
    
    $status = pclose($handle);
    
    return [
        'logs' => $logs,
        'success' => ($status == 0)
    ];
}

// 使用例
$result = tailLogFile('/var/log/apache2/access.log', 20);
if ($result['success']) {
    echo "最新のログエントリ:\n";
    foreach ($result['logs'] as $log) {
        echo $log . "\n";
    }
}
?>

3. 動画ファイルの変換処理

<?php
function convertVideo($inputFile, $outputFile, $format = 'mp4') {
    // FFmpegを使用した動画変換
    $command = sprintf(
        'ffmpeg -i %s -f %s %s 2>&1',
        escapeshellarg($inputFile),
        escapeshellarg($format),
        escapeshellarg($outputFile)
    );
    
    $handle = popen($command, 'r');
    
    if (!$handle) {
        return ['success' => false, 'error' => 'コマンドの実行に失敗しました'];
    }
    
    $progress = '';
    
    // FFmpegの進行状況を読み取る
    while (!feof($handle)) {
        $line = fgets($handle);
        if ($line !== false) {
            $progress .= $line;
            
            // 進行状況をリアルタイムで表示する場合
            if (strpos($line, 'time=') !== false) {
                echo "変換中: " . trim($line) . "\n";
                flush(); // バッファをフラッシュして即座に表示
            }
        }
    }
    
    $status = pclose($handle);
    
    return [
        'success' => ($status == 0),
        'output' => $progress,
        'status' => $status
    ];
}

// 使用例
$result = convertVideo('input.avi', 'output.mp4');
if ($result['success']) {
    echo "動画の変換が完了しました\n";
} else {
    echo "変換に失敗しました\n";
    echo "エラー詳細:\n" . $result['output'];
}
?>

4. データベースのバックアップ

<?php
function backupDatabase($host, $username, $password, $database, $backupFile) {
    // mysqldumpを使用したバックアップ
    $command = sprintf(
        'mysqldump -h %s -u %s -p%s %s > %s 2>&1',
        escapeshellarg($host),
        escapeshellarg($username),
        escapeshellarg($password),
        escapeshellarg($database),
        escapeshellarg($backupFile)
    );
    
    $handle = popen($command, 'r');
    
    if (!$handle) {
        return ['success' => false, 'message' => 'バックアップコマンドの実行に失敗'];
    }
    
    $output = '';
    while (!feof($handle)) {
        $output .= fgets($handle);
    }
    
    $status = pclose($handle);
    
    if ($status == 0 && file_exists($backupFile)) {
        $size = filesize($backupFile);
        return [
            'success' => true,
            'message' => 'バックアップが完了しました',
            'file' => $backupFile,
            'size' => $size
        ];
    } else {
        return [
            'success' => false,
            'message' => 'バックアップに失敗しました',
            'output' => $output
        ];
    }
}

// 使用例
$result = backupDatabase(
    'localhost',
    'dbuser',
    'dbpass',
    'myapp_db',
    '/backups/myapp_' . date('Y-m-d') . '.sql'
);

if ($result['success']) {
    echo "{$result['message']}\n";
    echo "ファイルサイズ: " . number_format($result['size'] / 1024 / 1024, 2) . " MB\n";
} else {
    echo "エラー: {$result['message']}\n";
}
?>

5. 圧縮ファイルの作成

<?php
function createArchive($sourceDir, $archiveFile) {
    $command = sprintf(
        'tar -czf %s -C %s .',
        escapeshellarg($archiveFile),
        escapeshellarg($sourceDir)
    );
    
    $handle = popen($command, 'r');
    
    if (!$handle) {
        return ['success' => false, 'error' => 'tar コマンドの実行に失敗'];
    }
    
    // tarコマンドの出力を読み取る
    $output = '';
    while (!feof($handle)) {
        $output .= fgets($handle);
    }
    
    $status = pclose($handle);
    
    return [
        'success' => ($status == 0),
        'archive' => $archiveFile,
        'output' => $output,
        'status' => $status
    ];
}

// 使用例
$result = createArchive(
    '/var/www/html/uploads',
    '/backups/uploads_' . date('Y-m-d') . '.tar.gz'
);

if ($result['success']) {
    echo "アーカイブの作成が完了しました: {$result['archive']}\n";
} else {
    echo "アーカイブの作成に失敗しました\n";
}
?>

よくある間違いと注意点

間違い1: pcloseを呼び忘れる

<?php
// ❌ 悪い例: リソースリークが発生
function badExample() {
    $handle = popen('ls -la', 'r');
    $output = '';
    while (!feof($handle)) {
        $output .= fgets($handle);
    }
    return $output;
    // pcloseを呼んでいない!
}

// ✅ 正しい例
function goodExample() {
    $handle = popen('ls -la', 'r');
    $output = '';
    while (!feof($handle)) {
        $output .= fgets($handle);
    }
    pclose($handle); // 必ず閉じる
    return $output;
}
?>

間違い2: エラーハンドリングの不備

<?php
// ❌ 悪い例: エラーチェックなし
$handle = popen('some-command', 'r');
while (!feof($handle)) {
    echo fgets($handle);
}
pclose($handle);

// ✅ 正しい例: 適切なエラーハンドリング
$handle = popen('some-command', 'r');

if (!$handle) {
    die("コマンドの実行に失敗しました\n");
}

while (!feof($handle)) {
    $line = fgets($handle);
    if ($line !== false) {
        echo $line;
    }
}

$status = pclose($handle);

if ($status != 0) {
    error_log("コマンドがエラーで終了しました: " . $status);
}
?>

間違い3: セキュリティリスク

<?php
// ❌ 非常に危険: ユーザー入力を直接使用
$userInput = $_GET['file'];
$handle = popen("cat {$userInput}", 'r');
// コマンドインジェクションの危険性!

// ✅ 安全: 入力を適切にエスケープ
$userInput = $_GET['file'];
$safeInput = escapeshellarg($userInput);
$handle = popen("cat {$safeInput}", 'r');

if ($handle) {
    while (!feof($handle)) {
        echo fgets($handle);
    }
    pclose($handle);
}
?>

間違い4: try-finallyを使わない

<?php
// ❌ 例外が発生するとpcloseが呼ばれない可能性
function riskyFunction() {
    $handle = popen('some-command', 'r');
    
    // もしここで例外が発生すると...
    processData($handle);
    
    pclose($handle);
}

// ✅ try-finallyで確実に閉じる
function safeFunction() {
    $handle = popen('some-command', 'r');
    
    try {
        if (!$handle) {
            throw new Exception('コマンドの実行に失敗');
        }
        
        processData($handle);
        
    } finally {
        if ($handle) {
            pclose($handle);
        }
    }
}
?>

終了ステータスの解釈

pcloseが返す終了ステータスは、実行されたコマンドの成功・失敗を示します。

<?php
$handle = popen('some-command', 'r');
// ... 処理 ...
$status = pclose($handle);

// 終了ステータスの解釈
if ($status == -1) {
    echo "pcloseがエラーを返しました\n";
} elseif ($status == 0) {
    echo "コマンドは正常に終了しました\n";
} else {
    echo "コマンドはエラーコード {$status} で終了しました\n";
    
    // UNIXシステムでのシグナル確認
    if (pcntl_wifsignaled($status)) {
        $signal = pcntl_wtermsig($status);
        echo "シグナル {$signal} により終了しました\n";
    }
}
?>

popenとpcloseの代替手段

場合によっては、popen/pcloseの代わりに他の関数を使う方が適切なこともあります。

<?php
// exec(): 出力を配列で取得
exec('ls -la', $output, $returnVar);
print_r($output);
echo "終了ステータス: {$returnVar}\n";

// shell_exec(): 出力を文字列で取得
$output = shell_exec('ls -la');
echo $output;

// system(): 出力を直接表示
system('ls -la', $returnVar);

// proc_open(): より高度な制御が可能
$descriptorspec = [
    0 => ["pipe", "r"],  // stdin
    1 => ["pipe", "w"],  // stdout
    2 => ["pipe", "w"]   // stderr
];

$process = proc_open('some-command', $descriptorspec, $pipes);

if (is_resource($process)) {
    fclose($pipes[0]);
    
    $output = stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    
    $errors = stream_get_contents($pipes[2]);
    fclose($pipes[2]);
    
    $returnValue = proc_close($process);
}
?>

パフォーマンスとベストプラクティス

タイムアウトの設定

長時間実行されるコマンドには、タイムアウトを設定することを推奨します。

<?php
function executeWithTimeout($command, $timeout = 30) {
    $handle = popen($command, 'r');
    
    if (!$handle) {
        return false;
    }
    
    // ノンブロッキングモードに設定
    stream_set_blocking($handle, false);
    
    $output = '';
    $startTime = time();
    
    while (!feof($handle)) {
        if ((time() - $startTime) > $timeout) {
            pclose($handle);
            throw new Exception("コマンドがタイムアウトしました");
        }
        
        $line = fgets($handle);
        if ($line !== false) {
            $output .= $line;
        }
        
        usleep(100000); // 0.1秒待機
    }
    
    $status = pclose($handle);
    return ['output' => $output, 'status' => $status];
}
?>

まとめ

pclose関数は、PHPで外部コマンドを実行する際の重要な後処理関数です。以下のポイントを押さえておきましょう。

  • popenで開いたプロセスは必ずpcloseで閉じる
  • リソースリークとゾンビプロセスを防ぐために不可欠
  • 終了ステータスを確認してコマンドの成功・失敗を判断
  • エラーハンドリングとセキュリティ対策を忘れずに
  • 例外安全性のため、try-finallyブロックの使用を検討
  • ユーザー入力を使う場合は必ずescapeshellarg()でエスケープ
  • 長時間実行されるコマンドにはタイムアウトを設定

外部コマンドの実行は強力な機能ですが、適切に使わないとセキュリティリスクやリソースリークの原因になります。この記事で紹介したベストプラクティスに従って、安全で効率的なコードを書きましょう!

タイトルとURLをコピーしました