Skip to content
Draft
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
84 changes: 81 additions & 3 deletions src/Context/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,7 @@ public function afterScenario( AfterScenarioScope $scope ): void {
$is_ci = getenv( 'CI' );
$is_debug = getenv( 'WP_CLI_TEST_DEBUG_BEHAT_ENV' );
if ( $scope->getTestResult()->getResultCode() <= 10 || ( $is_ci && ! $is_debug ) ) {
self::remove_dir( self::$run_dir );
self::remove_dir_in_background( self::$run_dir );
}
self::$run_dir = null;
}
Expand All @@ -1078,7 +1078,7 @@ public function afterScenario( AfterScenarioScope $scope ): void {

// Remove SUITE_CACHE_DIR if any.
if ( self::$suite_cache_dir ) {
self::remove_dir( self::$suite_cache_dir );
self::remove_dir_in_background( self::$suite_cache_dir );
self::$suite_cache_dir = null;
}

Expand Down Expand Up @@ -1700,13 +1700,20 @@ public function background_proc( $cmd ): void {
];
}

// If something else is already listening on the port, the process failing to bind to it
// would go unnoticed when waiting for the port, so fall back to waiting the full second.
$port = self::get_listening_port( $cmd );
if ( null !== $port && self::is_port_open( $port ) ) {
$port = null;
}

$proc = proc_open( $cmd, $descriptors, $pipes, $this->variables['RUN_DIR'], self::get_process_env_variables() );

if ( Utils\is_windows() ) {
fclose( $pipes[0] );
}

sleep( 1 );
self::wait_for_background_proc( $proc, $port );

$status = proc_get_status( $proc );

Expand All @@ -1731,6 +1738,61 @@ public function background_proc( $cmd ): void {
}
}

/**
* Give a background process up to a second to start.
*
* Returns early once the process accepts connections on the given port.
*
* @param resource $proc
* @param int|null $port Port the process is expected to listen on, if known.
*/
private static function wait_for_background_proc( $proc, $port ): void {
$deadline = microtime( true ) + 1;

while ( microtime( true ) < $deadline ) {
$status = proc_get_status( $proc );

if ( ! $status['running'] || ( null !== $port && self::is_port_open( $port ) ) ) {
return;
}

usleep( 50000 );
}
}

/**
* Get the local port a background command is going to listen on, if it can be told from the command.
*
* @param string $cmd
* @return int|null
*/
private static function get_listening_port( $cmd ): ?int {
if ( preg_match( '/(?:localhost|127\.0\.0\.1):(\d+)/', $cmd, $matches ) || preg_match( '/--port[= ](\d+)/', $cmd, $matches ) ) {
return (int) $matches[1];
}

return null;
}

/**
* Whether a local port accepts connections.
*
* @param int $port
* @return bool
*/
private static function is_port_open( $port ): bool {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- A refused connection is the expected answer for a closed port.
$connection = @fsockopen( 'localhost', $port, $errno, $errstr, 0.1 );

if ( false === $connection ) {
return false;
}

fclose( $connection );

return true;
}

/**
* Clean up temporary files safely.
*
Expand Down Expand Up @@ -1787,6 +1849,22 @@ public static function remove_dir( $dir ): void {
@rmdir( $dir );
}

/**
* Remove a directory without waiting for it to be gone.
*
* Only meant for directories with a unique name, which no later scenario is going to use.
*
* @param string $dir
*/
private static function remove_dir_in_background( $dir ): void {
if ( Utils\is_windows() || ! is_dir( $dir ) ) {
self::remove_dir( $dir );
return;
}

exec( 'rm -rf ' . escapeshellarg( $dir ) . ' > /dev/null 2>&1 &' );
}

/**
* Copy a directory (recursive). Destination directory must exist.
*
Expand Down
Loading