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