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:
root
2025-10-21 05:25:33 +00:00
parent 94c68861cc
commit 1c561dd301
5 changed files with 192 additions and 357 deletions

View File

@@ -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)) : [];