Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions features/eval.feature
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,25 @@ Feature: Evaluating PHP code and files.
Args: arg1 arg2
"""

Scenario: Eval-file with STDIN should work with alias groups
Given a WP installation in 'site1'
And a WP installation in 'site2'
And a wp-cli.yml file:
"""
@group:
- @site1
- @site2
@site1:
path: site1
@site2:
path: site2
"""
And a stdin-test.php file:
"""
<?php
echo "Executed from STDIN\n";
"""

When I run `cat stdin-test.php | wp @group eval-file -`
Then STDOUT should match /Executed from STDIN.*Executed from STDIN/s

121 changes: 120 additions & 1 deletion src/EvalFile_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ class EvalFile_Command extends WP_CLI_Command {
*/
const SHEBANG_PATTERN = '/^(#!.*)$/m';

/**
* Cache for STDIN contents to support alias groups.
*
* When using alias groups, the same command is executed multiple times
* in separate processes. STDIN can only be read once, so we cache it
* in a temporary file that persists across processes.
*
* @var string|null
*/
private static $stdin_cache = null;

/**
* Temporary file path for STDIN cache.
*
* @var string|null
*/
private static $stdin_cache_file = null;

/**
* Loads and executes a PHP file.
*
Expand Down Expand Up @@ -96,7 +114,79 @@ private static function execute_eval( $file, $positional_args, $use_include ) {
unset( $positional_args );

if ( '-' === $file ) {
eval( '?>' . file_get_contents( 'php://stdin' ) );
// Cache STDIN contents to support alias groups.
// When using alias groups, each site runs in a separate process,
// but they all share the same STDIN. Once STDIN is read, it's consumed.
// We cache it in a temporary file that persists across processes.
if ( null === self::$stdin_cache ) {
// Get a unique identifier for the cache file based on STDIN's inode.
// All processes sharing the same STDIN pipe will have the same inode,
// allowing them to share the cache file even if they have different PPIDs.
$cache_id = false;
$stdin_stat = fstat( STDIN );
if ( false !== $stdin_stat && isset( $stdin_stat['ino'] ) && isset( $stdin_stat['dev'] ) ) {
// Use device + inode to uniquely identify this STDIN stream
$cache_id = $stdin_stat['dev'] . '-' . $stdin_stat['ino'];
} elseif ( function_exists( 'posix_getppid' ) ) {
// Fallback to parent PID if fstat fails.
$ppid = posix_getppid();
// PHPStan doesn't understand that posix_getppid() can return false even after function_exists check.
// @phpstan-ignore-next-line
if ( false !== $ppid ) {
$cache_id = (string) $ppid;
}
}

// If we couldn't determine a shared cache key, fall back to environment variable or process ID.
if ( false === $cache_id ) {
$cache_id = getenv( 'WP_CLI_STDIN_CACHE_ID' );
if ( ! $cache_id ) {
$cache_id = getmypid();
WP_CLI::debug(
sprintf(
'Could not determine a shared cache key for STDIN. Falling back to process ID (%d). STDIN with alias groups may not work on this system without setting WP_CLI_STDIN_CACHE_ID.',
$cache_id
),
'eval'
);
}
}
self::$stdin_cache_file = sys_get_temp_dir() . '/wp-cli-eval-stdin-' . $cache_id . '.php';

// Check if cache file already exists (created by a previous subprocess)
if ( file_exists( self::$stdin_cache_file ) ) {
$stdin_contents = file_get_contents( self::$stdin_cache_file );
if ( false === $stdin_contents ) {
WP_CLI::error( 'Failed to read from STDIN cache file.' );
}
self::$stdin_cache = (string) $stdin_contents;

// Clean up old cache files (older than 1 hour) to prevent accumulation
self::cleanup_old_cache_files();
} else {
// First process: read from STDIN and cache it
$stdin_contents = file_get_contents( 'php://stdin' );
if ( false === $stdin_contents ) {
WP_CLI::error( 'Failed to read from STDIN.' );
}
self::$stdin_cache = (string) $stdin_contents;

// Save to cache file for subsequent processes
$write_result = file_put_contents( self::$stdin_cache_file, self::$stdin_cache );
if ( false === $write_result ) {
WP_CLI::error( 'Failed to write STDIN cache file.' );
}

// Clean up old cache files (older than 1 hour) to prevent accumulation
self::cleanup_old_cache_files();

// Note: We intentionally don't clean up the current cache file immediately.
// If we delete it in the shutdown function, subsequent child processes
// won't be able to read it. The cleanup_old_cache_files() method handles
// removal of stale cache files.
}
}
eval( '?>' . self::$stdin_cache );
} elseif ( $use_include ) {
include $file;
} else {
Expand All @@ -113,4 +203,33 @@ private static function execute_eval( $file, $positional_args, $use_include ) {
eval( '?>' . $file_contents );
}
}

/**
* Clean up old STDIN cache files to prevent accumulation.
*
* Removes cache files older than 1 hour from the system temp directory.
* This is called when creating or reading a cache file to ensure stale
* files don't accumulate if processes terminate unexpectedly.
*/
private static function cleanup_old_cache_files() {
$temp_dir = sys_get_temp_dir();
$cache_pattern = $temp_dir . '/wp-cli-eval-stdin-*.php';
$cache_files = glob( $cache_pattern );
$one_hour_ago = time() - 3600;

if ( ! empty( $cache_files ) ) {
foreach ( $cache_files as $cache_file ) {
// Skip the current cache file
if ( $cache_file === self::$stdin_cache_file ) {
continue;
}

// Delete files older than 1 hour
$file_time = filemtime( $cache_file );
if ( false !== $file_time && $file_time < $one_hour_ago ) {
@unlink( $cache_file );
}
}
}
}
}
Loading