diff --git a/features/cron-event.feature b/features/cron-event.feature index 648a71f4..705ecf6f 100644 --- a/features/cron-event.feature +++ b/features/cron-event.feature @@ -159,6 +159,45 @@ Feature: Manage WP Cron events Debug: Arguments: """ + Scenario: Run cron events with --network flag on non-multisite + When I try `wp cron event run --due-now --network` + Then STDERR should be: + """ + Error: This is not a multisite installation. + """ + And the return code should be 1 + + Scenario: Run cron events with --network flag on multisite + Given a WP multisite subdirectory install + And I run `wp site create --slug=site2` + And I run `wp site create --slug=site3` + + When I run `wp cron event schedule wp_cli_network_test now` + Then STDOUT should contain: + """ + Success: Scheduled event with hook 'wp_cli_network_test' + """ + + When I run `wp --url=example.com/site2 cron event schedule wp_cli_network_test_site2 now` + Then STDOUT should contain: + """ + Success: Scheduled event with hook 'wp_cli_network_test_site2' + """ + + When I run `wp cron event run --due-now --network --exclude=wp_privacy_delete_old_export_files,wp_version_check,wp_update_plugins,wp_update_themes,wp_site_health_scheduled_check,wp_update_user_counts,wp_scheduled_delete,wp_scheduled_auto_draft_delete` + Then STDOUT should contain: + """ + Executed the cron event 'wp_cli_network_test' + """ + And STDOUT should contain: + """ + Executed the cron event 'wp_cli_network_test_site2' + """ + And STDOUT should contain: + """ + Success: Executed a total of 2 cron events across 3 sites. + """ + Scenario: List cron events with actions field Given a wp-content/mu-plugins/test-cron-actions.php file: """ diff --git a/src/Cron_Event_Command.php b/src/Cron_Event_Command.php index 561dee57..9dba3ad8 100644 --- a/src/Cron_Event_Command.php +++ b/src/Cron_Event_Command.php @@ -235,6 +235,9 @@ public function schedule( $args, $assoc_args ) { * [--all] * : Run all hooks. * + * [--network] + * : Run hooks across all sites in a multisite installation. + * * ## EXAMPLES * * # Run all cron events due right now @@ -242,14 +245,88 @@ public function schedule( $args, $assoc_args ) { * Executed the cron event 'cron_test_1' in 0.01s. * Executed the cron event 'cron_test_2' in 0.006s. * Success: Executed a total of 2 cron events. + * + * # Run all cron events due right now across all sites in a multisite + * $ wp cron event run --due-now --network + * Executed the cron event 'cron_test_1' in 0.01s. + * Executed the cron event 'cron_test_2' in 0.006s. + * Success: Executed a total of 2 cron events across 3 sites. */ public function run( $args, $assoc_args ) { $due_now = Utils\get_flag_value( $assoc_args, 'due-now' ); + $network = Utils\get_flag_value( $assoc_args, 'network' ); + $lock_timeout = defined( 'WP_CRON_LOCK_TIMEOUT' ) ? WP_CRON_LOCK_TIMEOUT : 60; + + if ( $network ) { + if ( ! is_multisite() ) { + WP_CLI::error( 'This is not a multisite installation.' ); + } + + $sites = get_sites( + array( + 'fields' => 'ids', + 'number' => 0, + ) + ); + + if ( empty( $sites ) ) { + WP_CLI::error( 'No sites found in the network.' ); + } + + // Remove network flag before passing to get_selected_cron_events. + $network_assoc_args = $assoc_args; + unset( $network_assoc_args['network'] ); + + $total_executed = 0; + $site_count = count( $sites ); + + foreach ( $sites as $site_id ) { + switch_to_blog( $site_id ); + + $doing_cron_value = null; + + if ( $due_now ) { + $doing_cron_transient = get_transient( 'doing_cron' ); + if ( is_numeric( $doing_cron_transient ) && (float) $doing_cron_transient > microtime( true ) - $lock_timeout ) { + WP_CLI::warning( 'A cron event run is already in progress; skipping.' ); + return; + } + $doing_cron_value = sprintf( '%.22F', microtime( true ) ); + set_transient( 'doing_cron', $doing_cron_value, $lock_timeout ); + } + + $events = self::get_selected_cron_events( $args, $network_assoc_args ); + + if ( ! is_wp_error( $events ) ) { + $total_executed += self::run_events( $events ); + if ( $due_now && get_transient( 'doing_cron' ) === $doing_cron_value ) { + delete_transient( 'doing_cron' ); + } + } else { + if ( $due_now && get_transient( 'doing_cron' ) === $doing_cron_value ) { + delete_transient( 'doing_cron' ); + } + WP_CLI::debug( sprintf( 'No events found for site %d: %s', $site_id, $events->get_error_message() ), 'cron' ); + } + + restore_current_blog(); + } + + $message = sprintf( + 'Executed a total of %d %s across %d %s.', + $total_executed, + Utils\pluralize( 'cron event', $total_executed ), + $site_count, + Utils\pluralize( 'site', $site_count ) + ); + WP_CLI::success( $message ); + return; + } + $doing_cron_value = null; if ( $due_now ) { - $lock_timeout = defined( 'WP_CRON_LOCK_TIMEOUT' ) ? WP_CRON_LOCK_TIMEOUT : 60; $doing_cron_transient = get_transient( 'doing_cron' ); if ( is_numeric( $doing_cron_transient ) && (float) $doing_cron_transient > microtime( true ) - $lock_timeout ) { WP_CLI::warning( 'A cron event run is already in progress; skipping.' ); @@ -268,18 +345,7 @@ public function run( $args, $assoc_args ) { WP_CLI::error( $events ); } - $executed = 0; - foreach ( $events as $event ) { - WP_CLI::debug( sprintf( "Beginning execution of cron event '%s'.", $event->hook ), 'cron' ); - $start = microtime( true ); - $result = self::run_event( $event ); - $total = round( microtime( true ) - $start, 3 ); - ++$executed; - WP_CLI::log( sprintf( "Executed the cron event '%s' in %ss.", $event->hook, $total ) ); - if ( ! empty( $event->args ) ) { - WP_CLI::debug( sprintf( 'Arguments: %s', wp_json_encode( $event->args ) ), 'cron' ); - } - } + $executed = self::run_events( $events ); if ( $due_now && get_transient( 'doing_cron' ) === $doing_cron_value ) { delete_transient( 'doing_cron' ); @@ -437,6 +503,30 @@ function ( $event ) use ( $decoded_args ) { WP_CLI::success( sprintf( $message, $deleted ) ); } + /** + * Runs multiple cron events and logs their execution. + * + * @param array $events Array of event objects to run. + * @return int The number of events executed. + */ + private static function run_events( array $events ) { + $executed = 0; + + foreach ( $events as $event ) { + WP_CLI::debug( sprintf( "Beginning execution of cron event '%s'.", $event->hook ), 'cron' ); + $start = microtime( true ); + self::run_event( $event ); + $total = round( microtime( true ) - $start, 3 ); + ++$executed; + WP_CLI::log( sprintf( "Executed the cron event '%s' in %ss.", $event->hook, $total ) ); + if ( ! empty( $event->args ) ) { + WP_CLI::debug( sprintf( 'Arguments: %s', wp_json_encode( $event->args ) ), 'cron' ); + } + } + + return $executed; + } + /** * Executes an event immediately. * @@ -520,6 +610,10 @@ protected static function get_cron_events( $is_due_now = false ) { ); } + if ( ! is_array( $crons ) ) { + return []; + } + foreach ( $crons as $time => $hooks ) { // Incorrectly registered cron events can produce a string key.