[PHP]proc_close関数の使い方を徹底解説!プロセス管理の終了処理をマスター

PHP

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

proc_close関数とは?

proc_closeは、proc_openで開いたプロセスを閉じて、その終了ステータスを取得する関数です。プロセスの適切な終了とリソースの解放を保証するために必須の関数です。

基本的な構文

int proc_close(resource $process)

パラメータ:

  • $process: proc_openで開いたプロセスリソース

戻り値:

  • プロセスの終了ステータスコード
  • エラーの場合は -1

重要: proc_closeを呼ぶ前に、開いているパイプ(ファイルポインタ)をすべて閉じる必要があります。

proc_openとproc_closeの関係

proc_openとは

proc_openは、外部コマンドを実行し、標準入力・出力・エラー出力を制御できる高度な関数です。

resource proc_open(
    string $command,
    array $descriptorspec,
    array &$pipes,
    string $cwd = null,
    array $env = null,
    array $options = null
)

基本的な使用の流れ

<?php
// 1. ディスクリプタの定義
$descriptorspec = [
    0 => ["pipe", "r"],  // stdin(標準入力)
    1 => ["pipe", "w"],  // stdout(標準出力)
    2 => ["pipe", "w"]   // stderr(標準エラー出力)
];

// 2. プロセスを開く
$process = proc_open('ls -la', $descriptorspec, $pipes);

if (is_resource($process)) {
    // 3. パイプを使って入出力
    fclose($pipes[0]); // stdinは使わないので閉じる
    
    $output = stream_get_contents($pipes[1]);
    fclose($pipes[1]);
    
    $errors = stream_get_contents($pipes[2]);
    fclose($pipes[2]);
    
    // 4. プロセスを閉じる(必須!)
    $returnValue = proc_close($process);
    
    echo "終了ステータス: " . $returnValue . "\n";
    echo "出力:\n" . $output . "\n";
    
    if ($errors) {
        echo "エラー:\n" . $errors . "\n";
    }
}
?>

なぜproc_closeが必要なのか?

1. リソースの解放

開いたプロセスリソースとパイプを適切に解放し、メモリリークを防ぎます。

2. ゾンビプロセスの防止

proc_closeを呼ばないと、子プロセスがゾンビプロセスとして残り続ける可能性があります。

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

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

<?php
$returnValue = proc_close($process);

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

popenやpcloseとの違い

機能比較

<?php
// popen: 単方向通信(読み取りOR書き込み)
$handle = popen('ls -la', 'r');
$output = stream_get_contents($handle);
pclose($handle);

// proc_open: 双方向通信可能(入力、出力、エラー出力を個別に制御)
$descriptorspec = [
    0 => ["pipe", "r"],
    1 => ["pipe", "w"],
    2 => ["pipe", "w"]
];
$process = proc_open('command', $descriptorspec, $pipes);
// ... 処理 ...
proc_close($process);
?>

使い分けのポイント

  • popen/pclose: 単純な読み取りまたは書き込みのみ
  • proc_open/proc_close: 入力と出力を同時に制御したい、エラー出力を分けたい

実践的な使用例

1. 外部コマンド実行ラッパー

<?php
/**
 * 外部コマンド実行クラス
 */
class CommandExecutor {
    
    /**
     * コマンドを実行して結果を取得
     * @param string $command 実行するコマンド
     * @param string $input 標準入力に送るデータ
     * @return array 実行結果
     */
    public static function execute($command, $input = null) {
        $descriptorspec = [
            0 => ["pipe", "r"],  // stdin
            1 => ["pipe", "w"],  // stdout
            2 => ["pipe", "w"]   // stderr
        ];
        
        $process = proc_open($command, $descriptorspec, $pipes);
        
        if (!is_resource($process)) {
            throw new Exception('プロセスの起動に失敗しました');
        }
        
        // 入力データがあれば送信
        if ($input !== null) {
            fwrite($pipes[0], $input);
        }
        fclose($pipes[0]);
        
        // 出力とエラー出力を取得
        $stdout = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[2]);
        
        // プロセスを閉じて終了ステータスを取得
        $returnValue = proc_close($process);
        
        return [
            'success' => ($returnValue === 0),
            'exit_code' => $returnValue,
            'stdout' => $stdout,
            'stderr' => $stderr
        ];
    }
    
    /**
     * タイムアウト付きでコマンドを実行
     */
    public static function executeWithTimeout($command, $timeout = 30) {
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $process = proc_open($command, $descriptorspec, $pipes);
        
        if (!is_resource($process)) {
            throw new Exception('プロセスの起動に失敗しました');
        }
        
        fclose($pipes[0]);
        
        // ノンブロッキングモードに設定
        stream_set_blocking($pipes[1], false);
        stream_set_blocking($pipes[2], false);
        
        $stdout = '';
        $stderr = '';
        $startTime = time();
        
        while (true) {
            // タイムアウトチェック
            if ((time() - $startTime) > $timeout) {
                // プロセスを強制終了
                proc_terminate($process);
                fclose($pipes[1]);
                fclose($pipes[2]);
                proc_close($process);
                
                throw new Exception("コマンドがタイムアウトしました({$timeout}秒)");
            }
            
            // プロセスのステータスを確認
            $status = proc_get_status($process);
            
            // 出力を読み取る
            $stdout .= stream_get_contents($pipes[1]);
            $stderr .= stream_get_contents($pipes[2]);
            
            // プロセスが終了していれば抜ける
            if (!$status['running']) {
                break;
            }
            
            usleep(100000); // 0.1秒待機
        }
        
        fclose($pipes[1]);
        fclose($pipes[2]);
        
        $returnValue = proc_close($process);
        
        return [
            'success' => ($returnValue === 0),
            'exit_code' => $returnValue,
            'stdout' => $stdout,
            'stderr' => $stderr
        ];
    }
}

// 使用例
try {
    // シンプルな実行
    $result = CommandExecutor::execute('ls -la /tmp');
    
    if ($result['success']) {
        echo "=== コマンド実行成功 ===\n";
        echo $result['stdout'];
    } else {
        echo "=== エラー発生 ===\n";
        echo "終了コード: {$result['exit_code']}\n";
        echo $result['stderr'];
    }
    
    // 標準入力を使用
    $result2 = CommandExecutor::execute('grep test', "test line\nother line\ntest again");
    echo "\n=== grep結果 ===\n";
    echo $result2['stdout'];
    
    // タイムアウト付き実行
    $result3 = CommandExecutor::executeWithTimeout('sleep 5', 10);
    echo "タイムアウトテスト完了\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

2. Git操作ラッパー

<?php
/**
 * Gitコマンドラッパークラス
 */
class GitWrapper {
    private $repoPath;
    
    public function __construct($repoPath) {
        $this->repoPath = $repoPath;
    }
    
    /**
     * Gitコマンドを実行
     */
    private function executeGitCommand($command) {
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $fullCommand = "git -C " . escapeshellarg($this->repoPath) . " " . $command;
        
        $process = proc_open($fullCommand, $descriptorspec, $pipes);
        
        if (!is_resource($process)) {
            throw new Exception('Gitコマンドの実行に失敗しました');
        }
        
        fclose($pipes[0]);
        
        $stdout = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[2]);
        
        $returnValue = proc_close($process);
        
        if ($returnValue !== 0) {
            throw new Exception("Gitコマンドエラー: {$stderr}");
        }
        
        return trim($stdout);
    }
    
    /**
     * 現在のブランチ名を取得
     */
    public function getCurrentBranch() {
        return $this->executeGitCommand('branch --show-current');
    }
    
    /**
     * コミット履歴を取得
     */
    public function getLog($limit = 10) {
        $output = $this->executeGitCommand(
            "log --pretty=format:'%h|%an|%ae|%ad|%s' -n {$limit}"
        );
        
        $commits = [];
        foreach (explode("\n", $output) as $line) {
            if (empty($line)) continue;
            
            list($hash, $author, $email, $date, $message) = explode('|', $line);
            $commits[] = [
                'hash' => $hash,
                'author' => $author,
                'email' => $email,
                'date' => $date,
                'message' => $message
            ];
        }
        
        return $commits;
    }
    
    /**
     * ステータスを取得
     */
    public function getStatus() {
        return $this->executeGitCommand('status --short');
    }
    
    /**
     * ファイルを追加
     */
    public function add($files) {
        if (is_array($files)) {
            $files = implode(' ', array_map('escapeshellarg', $files));
        } else {
            $files = escapeshellarg($files);
        }
        
        return $this->executeGitCommand("add {$files}");
    }
    
    /**
     * コミット
     */
    public function commit($message) {
        $escapedMessage = escapeshellarg($message);
        return $this->executeGitCommand("commit -m {$escapedMessage}");
    }
    
    /**
     * プッシュ
     */
    public function push($remote = 'origin', $branch = null) {
        if ($branch === null) {
            $branch = $this->getCurrentBranch();
        }
        
        return $this->executeGitCommand("push {$remote} {$branch}");
    }
}

// 使用例
try {
    $git = new GitWrapper('/path/to/your/repo');
    
    echo "現在のブランチ: " . $git->getCurrentBranch() . "\n\n";
    
    echo "=== ステータス ===\n";
    echo $git->getStatus() . "\n\n";
    
    echo "=== 最近のコミット ===\n";
    $commits = $git->getLog(5);
    foreach ($commits as $commit) {
        echo "{$commit['hash']} - {$commit['message']} ({$commit['author']})\n";
    }
    
    // ファイルを追加してコミット
    // $git->add('file.txt');
    // $git->commit('Update file.txt');
    // $git->push();
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

3. FFmpeg動画処理

<?php
/**
 * FFmpeg動画処理クラス
 */
class VideoProcessor {
    
    /**
     * 動画情報を取得
     */
    public static function getVideoInfo($videoFile) {
        if (!file_exists($videoFile)) {
            throw new Exception('動画ファイルが見つかりません');
        }
        
        $command = sprintf(
            'ffprobe -v quiet -print_format json -show_format -show_streams %s',
            escapeshellarg($videoFile)
        );
        
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $process = proc_open($command, $descriptorspec, $pipes);
        
        if (!is_resource($process)) {
            throw new Exception('FFprobeの実行に失敗しました');
        }
        
        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);
        
        if ($returnValue !== 0) {
            throw new Exception("FFprobeエラー: {$errors}");
        }
        
        return json_decode($output, true);
    }
    
    /**
     * 動画を変換(進行状況コールバック付き)
     */
    public static function convert($inputFile, $outputFile, $callback = null) {
        if (!file_exists($inputFile)) {
            throw new Exception('入力ファイルが見つかりません');
        }
        
        $command = sprintf(
            'ffmpeg -i %s -c:v libx264 -crf 23 -c:a aac -b:a 128k %s -y 2>&1',
            escapeshellarg($inputFile),
            escapeshellarg($outputFile)
        );
        
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $process = proc_open($command, $descriptorspec, $pipes);
        
        if (!is_resource($process)) {
            throw new Exception('FFmpegの実行に失敗しました');
        }
        
        fclose($pipes[0]);
        
        // ノンブロッキングモードに設定
        stream_set_blocking($pipes[1], false);
        
        $duration = null;
        $output = '';
        
        while (!feof($pipes[1])) {
            $line = fgets($pipes[1]);
            
            if ($line === false) {
                usleep(100000);
                continue;
            }
            
            $output .= $line;
            
            // 動画の長さを取得
            if (preg_match('/Duration: (\d{2}):(\d{2}):(\d{2})/', $line, $matches)) {
                $duration = ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3];
            }
            
            // 進行状況を取得
            if (preg_match('/time=(\d{2}):(\d{2}):(\d{2})/', $line, $matches)) {
                $currentTime = ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3];
                
                if ($duration > 0 && $callback !== null) {
                    $progress = ($currentTime / $duration) * 100;
                    call_user_func($callback, $progress, $currentTime, $duration);
                }
            }
        }
        
        fclose($pipes[1]);
        fclose($pipes[2]);
        
        $returnValue = proc_close($process);
        
        if ($returnValue !== 0) {
            throw new Exception('動画変換に失敗しました');
        }
        
        return true;
    }
    
    /**
     * サムネイルを生成
     */
    public static function generateThumbnail($videoFile, $outputFile, $timeInSeconds = 5) {
        $command = sprintf(
            'ffmpeg -i %s -ss %d -vframes 1 %s -y 2>&1',
            escapeshellarg($videoFile),
            $timeInSeconds,
            escapeshellarg($outputFile)
        );
        
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $process = proc_open($command, $descriptorspec, $pipes);
        
        if (!is_resource($process)) {
            throw new Exception('FFmpegの実行に失敗しました');
        }
        
        fclose($pipes[0]);
        stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        stream_get_contents($pipes[2]);
        fclose($pipes[2]);
        
        $returnValue = proc_close($process);
        
        if ($returnValue !== 0) {
            throw new Exception('サムネイル生成に失敗しました');
        }
        
        return true;
    }
}

// 使用例
try {
    // 動画情報を取得
    $info = VideoProcessor::getVideoInfo('input.mp4');
    echo "動画時間: " . $info['format']['duration'] . "秒\n";
    echo "サイズ: " . round($info['format']['size'] / 1024 / 1024, 2) . " MB\n\n";
    
    // 動画を変換(進行状況表示付き)
    echo "動画変換中...\n";
    VideoProcessor::convert(
        'input.mp4',
        'output.mp4',
        function($progress, $current, $total) {
            printf("\r進行状況: %.1f%% (%d/%d秒)", $progress, $current, $total);
            flush();
        }
    );
    echo "\n変換完了!\n";
    
    // サムネイルを生成
    VideoProcessor::generateThumbnail('input.mp4', 'thumbnail.jpg', 10);
    echo "サムネイル生成完了!\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

4. SSH接続とリモートコマンド実行

<?php
/**
 * SSH接続クラス
 */
class SSHClient {
    private $host;
    private $username;
    private $keyFile;
    
    public function __construct($host, $username, $keyFile = null) {
        $this->host = $host;
        $this->username = $username;
        $this->keyFile = $keyFile;
    }
    
    /**
     * リモートでコマンドを実行
     */
    public function executeCommand($command) {
        $sshCommand = sprintf(
            'ssh %s %s@%s %s',
            $this->keyFile ? '-i ' . escapeshellarg($this->keyFile) : '',
            escapeshellarg($this->username),
            escapeshellarg($this->host),
            escapeshellarg($command)
        );
        
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $process = proc_open($sshCommand, $descriptorspec, $pipes);
        
        if (!is_resource($process)) {
            throw new Exception('SSH接続に失敗しました');
        }
        
        fclose($pipes[0]);
        
        $stdout = stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[2]);
        
        $returnValue = proc_close($process);
        
        return [
            'success' => ($returnValue === 0),
            'exit_code' => $returnValue,
            'output' => $stdout,
            'error' => $stderr
        ];
    }
    
    /**
     * ファイルをアップロード(SCP)
     */
    public function uploadFile($localFile, $remotePath) {
        $command = sprintf(
            'scp %s %s %s@%s:%s',
            $this->keyFile ? '-i ' . escapeshellarg($this->keyFile) : '',
            escapeshellarg($localFile),
            escapeshellarg($this->username),
            escapeshellarg($this->host),
            escapeshellarg($remotePath)
        );
        
        $descriptorspec = [
            0 => ["pipe", "r"],
            1 => ["pipe", "w"],
            2 => ["pipe", "w"]
        ];
        
        $process = proc_open($command, $descriptorspec, $pipes);
        
        if (!is_resource($process)) {
            throw new Exception('SCPの実行に失敗しました');
        }
        
        fclose($pipes[0]);
        stream_get_contents($pipes[1]);
        fclose($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[2]);
        
        $returnValue = proc_close($process);
        
        if ($returnValue !== 0) {
            throw new Exception("ファイルアップロード失敗: {$stderr}");
        }
        
        return true;
    }
}

// 使用例
try {
    $ssh = new SSHClient('example.com', 'username', '/path/to/key.pem');
    
    // リモートコマンドを実行
    $result = $ssh->executeCommand('df -h');
    
    if ($result['success']) {
        echo "=== ディスク使用状況 ===\n";
        echo $result['output'];
    } else {
        echo "エラー: " . $result['error'] . "\n";
    }
    
    // ファイルをアップロード
    // $ssh->uploadFile('local_file.txt', '/remote/path/file.txt');
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

よくある間違いと注意点

間違い1: パイプを閉じずにproc_closeを呼ぶ

<?php
// ❌ 悪い例: パイプを閉じていない
$process = proc_open($command, $descriptorspec, $pipes);
$returnValue = proc_close($process); // デッドロックの可能性

// ✅ 正しい例: すべてのパイプを閉じてから
$process = proc_open($command, $descriptorspec, $pipes);
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
$returnValue = proc_close($process);
?>

間違い2: proc_closeを呼び忘れる

<?php
// ❌ リソースリーク
function badExample() {
    $process = proc_open($command, $descriptorspec, $pipes);
    // ... 処理 ...
    return $output;
    // proc_closeを呼んでいない!
}

// ✅ try-finallyで確実に閉じる
function goodExample() {
    $process = proc_open($command, $descriptorspec, $pipes);
    
    try {
        // 処理
        $output = stream_get_contents($pipes[1]);
    } finally {
        fclose($pipes[0]);
        fclose($pipes[1]);
        fclose($pipes[2]);
        proc_close($process);
    }
    
    return $output;
}
?>

間違い3: 終了ステータスの確認を怠る

<?php
// ❌ エラーをチェックしない
$returnValue = proc_close($process);
// エラーが発生しても気づかない

// ✅ 終了ステータスを確認
$returnValue = proc_close($process);

if ($returnValue !== 0) {
    error_log("コマンドがエラーで終了: {$returnValue}");
    throw new Exception('コマンド実行エラー');
}
?>

まとめ

proc_close関数は、PHPで外部プロセスを管理する際の重要な終了処理関数です。以下のポイントを押さえておきましょう。

  • proc_openで開いたプロセスは必ずproc_closeで閉じる
  • すべてのパイプを先に閉じてからproc_closeを呼ぶ
  • 終了ステータスを確認してエラーを検出
  • リソースリークとゾンビプロセスを防ぐために不可欠
  • try-finallyでエラー時も確実に閉じる
  • popen/pcloseより高度な制御が必要な場合に使用
  • 標準入力・出力・エラー出力を個別に制御可能

proc_openproc_closeを組み合わせることで、外部コマンドやプロセスとの高度な双方向通信が実現できます。適切なエラーハンドリングとリソース管理を行って、安全で効率的なプロセス制御を実装しましょう!

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