Ban proc_open() and exec() entirely - replace with shell_exec() and file redirection
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -910,59 +910,24 @@ function shell_exec_pretty($command, $real_time = true, $throw_on_error = false)
|
||||
echo $gray . '> ' . $command . $reset . PHP_EOL;
|
||||
|
||||
if ($real_time) {
|
||||
// Use proc_open for real-time output
|
||||
$descriptors = [
|
||||
0 => ['pipe', 'r'], // stdin
|
||||
1 => ['pipe', 'w'], // stdout
|
||||
2 => ['pipe', 'w'], // stderr
|
||||
];
|
||||
// Use passthru() for real-time output without proc_open() pipe buffer issues
|
||||
// Redirect to temp file to capture output for return value
|
||||
$temp_file = storage_path('rsx-tmp/shell_exec_pretty_' . uniqid() . '.txt');
|
||||
|
||||
$process = proc_open($command, $descriptors, $pipes);
|
||||
// Use script command wrapper to show real-time output AND capture to file
|
||||
// passthru() shows output but doesn't capture it, so we use tee to do both
|
||||
$full_command = "($command 2>&1) | tee " . escapeshellarg($temp_file);
|
||||
|
||||
if (!is_resource($process)) {
|
||||
$error = "Failed to execute command: $command";
|
||||
echo $red . $error . $reset . PHP_EOL;
|
||||
if ($throw_on_error) {
|
||||
throw new RuntimeException($error);
|
||||
}
|
||||
|
||||
return ['output' => '', 'error' => $error, 'exit_code' => -1];
|
||||
}
|
||||
|
||||
// Close stdin
|
||||
fclose($pipes[0]);
|
||||
|
||||
// Set stdout to non-blocking
|
||||
stream_set_blocking($pipes[1], false);
|
||||
stream_set_blocking($pipes[2], false);
|
||||
// passthru() displays output in real-time and returns the exit code via $exit_code
|
||||
passthru($full_command, $exit_code);
|
||||
|
||||
// Read captured output from file
|
||||
$output = '';
|
||||
$error = '';
|
||||
|
||||
// Read output in real-time
|
||||
while (!feof($pipes[1]) || !feof($pipes[2])) {
|
||||
// Read stdout
|
||||
$stdout = fread($pipes[1], 1024);
|
||||
if ($stdout !== false && $stdout !== '') {
|
||||
echo $stdout;
|
||||
$output .= $stdout;
|
||||
}
|
||||
|
||||
// Read stderr
|
||||
$stderr = fread($pipes[2], 1024);
|
||||
if ($stderr !== false && $stderr !== '') {
|
||||
echo $red . $stderr . $reset;
|
||||
$error .= $stderr;
|
||||
}
|
||||
|
||||
// Small delay to prevent CPU spinning
|
||||
usleep(10000); // 10ms
|
||||
if (file_exists($temp_file)) {
|
||||
$output = file_get_contents($temp_file);
|
||||
unlink($temp_file); // Clean up
|
||||
}
|
||||
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
|
||||
$exit_code = proc_close($process);
|
||||
} else {
|
||||
// Use shell_exec for simple execution
|
||||
$full_command = $command . ' 2>&1';
|
||||
@@ -1009,14 +974,14 @@ function command_exists($command)
|
||||
|
||||
/**
|
||||
* Execute command without exec()'s output truncation issues
|
||||
* Drop-in replacement for exec() using proc_open()
|
||||
* Drop-in replacement for exec() using shell_exec() and file redirection
|
||||
*
|
||||
* exec() has a critical flaw: it reads command output line-by-line into an array,
|
||||
* which can hit memory/buffer limits on large outputs (>1MB typical), causing
|
||||
* SILENT TRUNCATION without throwing errors or exceptions.
|
||||
*
|
||||
* \exec_safe() uses proc_open() internally to stream unlimited output without
|
||||
* size limits, while maintaining the exact same signature as exec().
|
||||
* \exec_safe() uses shell_exec() internally which handles unlimited output without
|
||||
* pipe buffer truncation issues, while maintaining the exact same signature as exec().
|
||||
*
|
||||
* Usage:
|
||||
* // Before:
|
||||
@@ -1032,58 +997,30 @@ function command_exists($command)
|
||||
*/
|
||||
function exec_safe(string $command, array &$output = [], int &$return_var = 0): string|false
|
||||
{
|
||||
$descriptors = [
|
||||
0 => ['pipe', 'r'], // stdin
|
||||
1 => ['pipe', 'w'], // stdout
|
||||
2 => ['pipe', 'w'] // stderr
|
||||
];
|
||||
// Use shell_exec() for reliable output capture without pipe buffer truncation
|
||||
// shell_exec() doesn't provide exit codes, so use exec() with file redirection for that
|
||||
$temp_file = storage_path('rsx-tmp/exec_safe_' . uniqid() . '.txt');
|
||||
|
||||
$process = proc_open($command, $descriptors, $pipes);
|
||||
// Redirect output to temp file to get both output and exit code reliably
|
||||
$full_command = "($command) > " . escapeshellarg($temp_file) . " 2>&1; echo $?";
|
||||
|
||||
if (!is_resource($process)) {
|
||||
// Execute and capture just the exit code (last line)
|
||||
$result = shell_exec($full_command);
|
||||
$return_var = (int)trim($result);
|
||||
|
||||
// Read the full output from file
|
||||
$combined = '';
|
||||
if (file_exists($temp_file)) {
|
||||
$combined = file_get_contents($temp_file);
|
||||
unlink($temp_file); // Clean up
|
||||
}
|
||||
|
||||
if ($combined === false) {
|
||||
$return_var = -1;
|
||||
$output = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
fclose($pipes[0]);
|
||||
|
||||
// Set blocking mode on output streams to ensure complete reads
|
||||
// Without this, fread() can return partial data even in a loop
|
||||
stream_set_blocking($pipes[1], true);
|
||||
stream_set_blocking($pipes[2], true);
|
||||
|
||||
// Read stdout and stderr in chunks to handle outputs larger than pipe buffer (8KB limit)
|
||||
// CRITICAL: stream_get_contents() can truncate at 8192 bytes for large outputs
|
||||
$stdout = '';
|
||||
while (!feof($pipes[1])) {
|
||||
$chunk = fread($pipes[1], 8192);
|
||||
if ($chunk === false) {
|
||||
break;
|
||||
}
|
||||
$stdout .= $chunk;
|
||||
}
|
||||
|
||||
$stderr = '';
|
||||
while (!feof($pipes[2])) {
|
||||
$chunk = fread($pipes[2], 8192);
|
||||
if ($chunk === false) {
|
||||
break;
|
||||
}
|
||||
$stderr .= $chunk;
|
||||
}
|
||||
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
|
||||
$return_var = proc_close($process);
|
||||
|
||||
// Combine stderr with stdout like exec() does with 2>&1
|
||||
$combined = $stdout;
|
||||
if (!empty($stderr)) {
|
||||
$combined = trim($stderr) . "\n" . trim($stdout);
|
||||
}
|
||||
|
||||
// Split into lines like exec() does
|
||||
$output = $combined ? explode("\n", trim($combined)) : [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user