class-events.php 19.1 KB
Newer Older
1
2
3
4
5
<?php

namespace Automattic\WP\Cron_Control\CLI;

/**
6
 * Manage Cron Control's data
7
 */
8
class Events extends \WP_CLI_Command {
9
10
11
12
13
	/**
	 * List cron events
	 *
	 * Intentionally bypasses caching to ensure latest data is shown
	 *
14
	 * @subcommand list
15
	 * @synopsis [--status=<pending|completed>] [--page=<page>] [--limit=<limit>] [--format=<format>]
16
	 */
17
	public function list_events( $args, $assoc_args ) {
18
		$events = $this->get_events( $args, $assoc_args );
19

20
		// Prevent one from requesting a page that doesn't exist
21
22
		// Shouldn't error when first page is requested, though, as that is handled below and is an odd behaviour otherwise
		if ( $events['page'] > $events['total_pages'] && $events['page'] > 1 ) {
23
24
25
			\WP_CLI::error( __( 'Invalid page requested', 'automattic-cron-control' ) );
		}

26
27
28
29
30
31
32
33
34
35
		// Output in the requested format
		if ( isset( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) {
			echo implode( ' ', wp_list_pluck( $events['items'], 'ID' ) );
		} else {
			// Lest someone think the `completed` record should be...complete
			if ( isset( $assoc_args['status'] ) && 'completed' === $assoc_args['status'] ) {
				\WP_CLI::warning( __( 'Entries are purged automatically, so this cannot be relied upon as a record of past event execution.', 'automattic-cron-control' ) );
			}

			// Not much to do
36
37
			if ( 0 === $events['total_items'] || empty( $events['items'] ) ) {
				\WP_CLI::warning( __( 'No events to display', 'automattic-cron-control' ) );
38
39
40
41
42
43
44
45
46
				return;
			}

			// Prepare events for display
			$events_for_display      = $this->format_events( $events['items'] );
			$total_events_to_display = count( $events_for_display );

			// Count, noting if showing fewer than all
			if ( $events['total_items'] <= $total_events_to_display ) {
47
				\WP_CLI::log( sprintf( _n( 'Displaying one entry', 'Displaying all %s entries', $total_events_to_display, 'automattic-cron-control' ), number_format_i18n( $total_events_to_display ) ) );
48
			} else {
49
				\WP_CLI::log( sprintf( __( 'Displaying %1$s of %2$s entries, page %3$s of %4$s', 'automattic-cron-control' ), number_format_i18n( $total_events_to_display ), number_format_i18n( $events['total_items'] ), number_format_i18n( $events['page'] ), number_format_i18n( $events['total_pages'] ) ) );
50
51
52
			}

			// And reformat
53
54
55
56
			$format = 'table';
			if ( isset( $assoc_args['format'] ) ) {
				$format = $assoc_args['format'];
			}
Erick Hitter's avatar
Erick Hitter committed
57

58
59
60
61
62
63
			\WP_CLI\Utils\format_items( $format, $events_for_display, array(
				'ID',
				'action',
				'instance',
				'next_run_gmt',
				'next_run_relative',
Erick Hitter's avatar
Erick Hitter committed
64
				'last_updated_gmt',
65
				'recurrence',
66
				'internal_event',
67
				'schedule_name',
68
69
70
				'event_args',
			) );
		}
71
72
	}

73
	/**
74
	 * Remove events
75
76
	 *
	 * @subcommand delete
77
	 * @synopsis [--event_id=<event_id>] [--action=<action>] [--completed]
78
79
80
81
82
	 */
	public function delete_events( $args, $assoc_args ) {
		// Remove a specific event
		if ( isset( $assoc_args['event_id'] ) ) {
			$this->delete_event_by_id( $args, $assoc_args );
83
			return;
84
85
86
87
		}

		// Remove all events with a given action
		if ( isset( $assoc_args['action'] ) ) {
Erick Hitter's avatar
Typo  
Erick Hitter committed
88
			$this->delete_event_by_action( $args, $assoc_args );
89
			return;
90
91
		}

92
93
		// Remove all completed events
		if ( isset( $assoc_args['completed'] ) ) {
94
			$this->delete_completed_events( $args, $assoc_args );
95
96
97
			return;
		}

98
99
100
		\WP_CLI::error( __( 'Specify something to delete, or see the `cron-control-fixers` command to remove all data.', 'automattic-cron-control' ) );
	}

101
102
103
104
	/**
	 * Run an event given an ID
	 *
	 * @subcommand run
105
	 * @synopsis <event_id>
106
107
	 */
	public function run_event( $args, $assoc_args ) {
108
109
110
		// Validate ID
		if ( ! is_numeric( $args[0] ) ) {
			\WP_CLI::error( __( 'Specify the ID of an event to run', 'automattic-cron-control' ) );
111
112
		}

113
114
		// Retrieve information needed to execute event
		$event = \Automattic\WP\Cron_Control\get_event_by_id( $args[0] );
115

Erick Hitter's avatar
Erick Hitter committed
116
		if ( ! is_object( $event ) ) {
117
			\WP_CLI::error( sprintf( __( 'Failed to locate event %d. Please confirm that the entry exists and that the ID is that of an event.', 'automattic-cron-control' ), $args[0] ) );
118
119
		}

120
		\WP_CLI::log( sprintf( __( 'Found event %1$d with action `%2$s` and instance identifier `%3$s`', 'automattic-cron-control' ), $args[0], $event->action, $event->instance ) );
121
122

		// Proceed?
123
		$now = time();
Erick Hitter's avatar
Erick Hitter committed
124
125
		if ( $event->timestamp > $now ) {
			\WP_CLI::warning( sprintf( __( 'This event is not scheduled to run until %1$s GMT (%2$s)', 'automattic-cron-control' ), date( TIME_FORMAT, $event->timestamp ), $this->calculate_interval( $event->timestamp - $now ) ) );
126
127
		}

128
		\WP_CLI::confirm( sprintf( __( 'Run this event?', 'automattic-cron-control' ) ) );
129
130

		// Environment preparation
131
		\Automattic\WP\Cron_Control\set_doing_cron();
132
133

		// Run the event
Erick Hitter's avatar
Erick Hitter committed
134
		$run = \Automattic\WP\Cron_Control\run_event( $event->timestamp, $event->action_hashed, $event->instance, true );
135
136
137
138
139
140
141
142
143
144
145

		// Output based on run attempt
		if ( is_array( $run ) ) {
			\WP_CLI::success( $run['message'] );
		} elseif ( is_wp_error( $run ) ) {
			\WP_CLI::error( $run->get_error_message() );
		} else {
			\WP_CLI::error( __( 'Failed to run event', 'automattic-cron-control' ) );
		}
	}

146
147
148
149
	/**
	 * Retrieve list of events, and related data, for a given request
	 */
	private function get_events( $args, $assoc_args ) {
150
		// Accept a status argument, with a default
151
152
153
154
155
		$status = 'pending';
		if ( isset( $assoc_args['status'] ) ) {
			$status = $assoc_args['status'];
		}

156
		// Convert to status used by Event Store
157
		$event_status = null;
158
159
		switch ( $status ) {
			case 'pending' :
160
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_PENDING;
161
162
				break;

163
164
165
166
			case 'running' :
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_RUNNING;
				break;

167
			case 'completed' :
168
				$event_status = \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED;
169
170
171
				break;
		}

172
173
174
175
176
177
		if ( is_null( $event_status ) ) {
			\WP_CLI::error( __( 'Invalid status specified', 'automattic-cron-control' ) );
		}

		unset( $status );

178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
		// Total to show
		$limit = 25;
		if ( isset( $assoc_args['limit'] ) && is_numeric( $assoc_args['limit'] ) ) {
			$limit = max( 1, min( absint( $assoc_args['limit'] ), 500 ) );
		}

		// Pagination
		$page = 1;
		if ( isset( $assoc_args['page'] ) && is_numeric( $assoc_args['page'] ) ) {
			$page = absint( $assoc_args['page'] );
		}

		$offset = absint( ( $page - 1 ) * $limit );

		// Query
Erick Hitter's avatar
Erick Hitter committed
193
		$items = \Automattic\WP\Cron_Control\get_events( array(
194
195
196
197
			'status'     => $event_status,
			'quantity'   => $limit,
			'page'       => $page,
			'force_sort' => true,
198
		) );
199
200
201
202
203
204

		// Bail if we don't get results
		if ( ! is_array( $items ) ) {
			\WP_CLI::error( __( 'Problem retrieving events', 'automattic-cron-control' ) );
		}

Erick Hitter's avatar
Erick Hitter committed
205
		// Include totals for pagination etc
206
		$total_items = \Automattic\WP\Cron_Control\count_events_by_status( $event_status );
Erick Hitter's avatar
Erick Hitter committed
207
		$total_pages = ceil( $total_items / $limit );
208

Erick Hitter's avatar
Erick Hitter committed
209
		return compact( 'status', 'limit', 'page', 'offset', 'items', 'total_items', 'total_pages' );
210
211
212
	}

	/**
213
	 * Format event data into something human-readable
214
	 */
215
216
217
218
219
220
	private function format_events( $events ) {
		$formatted_events = array();

		// Reformat events
		foreach ( $events as $event ) {
			$row = array(
Erick Hitter's avatar
Erick Hitter committed
221
				'ID'                => $event->ID,
222
223
224
				'action'            => $event->action,
				'instance'          => $event->instance,
				'next_run_gmt'      => date( TIME_FORMAT, $event->timestamp ),
225
				'next_run_relative' => '',
226
				'last_updated_gmt'  => date( TIME_FORMAT, strtotime( $event->last_modified ) ),
227
				'recurrence'        => __( 'Non-repeating', 'automattic-cron-control' ),
228
				'internal_event'    => '',
229
				'schedule_name'     => __( 'n/a', 'automattic-cron-control' ),
230
231
232
				'event_args'        => '',
			);

233
234
			if ( $event->status === \Automattic\WP\Cron_Control\Events_Store::STATUS_PENDING ) {
				$row['next_run_relative'] = $this->calculate_interval( $event->timestamp - time() );
235
236
			}

237
238
239
240
			$row['internal_event'] = \Automattic\WP\Cron_Control\is_internal_event( $event->action ) ? __( 'true', 'automattic-cron-control' ) : '';

			$row['event_args'] = maybe_serialize( $event->args );

241
242
243
244
245
			if ( \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED === $event->status ) {
				$instance = md5( $row['event_args'] );
				$row['instance'] = "{$instance} - {$row['instance']}";
			}

246
247
248
249
250
251
			if ( isset( $event->interval ) && $event->interval ) {
				$row['recurrence'] = $this->calculate_interval( $event->interval );
			}

			if ( isset( $event->schedule ) && $event->schedule ) {
				$row['schedule_name'] = $event->schedule;
Erick Hitter's avatar
Erick Hitter committed
252
253
			}

254
255
256
			$formatted_events[] = $row;
		}

257
258
259
260
261
		// Sort results
		if ( ! empty( $formatted_events ) ) {
			usort( $formatted_events, array( $this, 'sort_events' ) );
		}

262
263
		return $formatted_events;
	}
Erick Hitter's avatar
Erick Hitter committed
264

265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
	/**
	 * Sort events by timestamp, then action name
	 */
	private function sort_events( $first, $second ) {
		// Timestamp is usually sufficient
		if ( isset( $first['next_run_gmt'] ) ) {
			$first_timestamp = strtotime( $first['next_run_gmt'] );
			$second_timestamp = strtotime( $second['next_run_gmt'] );
		} elseif ( isset( $first['timestamp'] ) ) {
			$first_timestamp = $first['timestamp'];
			$second_timestamp = $second['timestamp'];
		} else {
			return 0;
		}

280
281
		if ( $first_timestamp !== $second_timestamp ) {
			return $first_timestamp - $second_timestamp;
282
283
284
		}

		// If timestamps are equal, consider action
285
		return strnatcasecmp( $first['action'], $second['action'] );
286
287
	}

Erick Hitter's avatar
Erick Hitter committed
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
	/**
	 * Convert a time interval into human-readable format.
	 *
	 * Similar to WordPress' built-in `human_time_diff()` but returns two time period chunks instead of just one.
	 *
	 * Borrowed from WP-CLI
	 *
	 * @param int $since An interval of time in seconds
	 * @return string The interval in human readable format
	 */
	private function calculate_interval( $since ) {
		if ( $since <= 0 ) {
			return 'now';
		}

		$since = absint( $since );

		// array of time period chunks
		$chunks = array(
			array( 60 * 60 * 24 * 365 , \_n_noop( '%s year', '%s years' ) ),
			array( 60 * 60 * 24 * 30 , \_n_noop( '%s month', '%s months' ) ),
			array( 60 * 60 * 24 * 7, \_n_noop( '%s week', '%s weeks' ) ),
			array( 60 * 60 * 24 , \_n_noop( '%s day', '%s days' ) ),
			array( 60 * 60 , \_n_noop( '%s hour', '%s hours' ) ),
			array( 60 , \_n_noop( '%s minute', '%s minutes' ) ),
			array(  1 , \_n_noop( '%s second', '%s seconds' ) ),
		);

		// we only want to output two chunks of time here, eg:
		// x years, xx months
		// x days, xx hours
		// so there's only two bits of calculation below:

		// step one: the first chunk
		for ( $i = 0, $j = count( $chunks ); $i < $j; $i++ ) {
			$seconds = $chunks[$i][0];
			$name = $chunks[$i][1];

			// finding the biggest chunk (if the chunk fits, break)
			if ( ( $count = floor( $since / $seconds ) ) != 0 ){
				break;
			}
		}

		// set output var
		$output = sprintf( \_n( $name[0], $name[1], $count ), $count );

		// step two: the second chunk
		if ( $i + 1 < $j ) {
			$seconds2 = $chunks[$i + 1][0];
			$name2    = $chunks[$i + 1][1];

			if ( ( $count2 = floor( ( $since - ( $seconds * $count ) ) / $seconds2 ) ) != 0 ) {
				// add to output var
				$output .= ' ' . sprintf( \_n( $name2[0], $name2[1], $count2 ), $count2 );
			}
		}

		return $output;
	}
348
349
350
351
352
353
354
355
356
357
358
359

	/**
	 * Delete an event by ID
	 */
	private function delete_event_by_id( $args, $assoc_args ) {
		$jid = absint( $assoc_args['event_id'] );

		// Validate ID
		if ( ! $jid ) {
			\WP_CLI::error( __( 'Invalid event ID', 'automattic-cron-control' ) );
		}

360
		\WP_CLI::log( __( 'Locating event...', 'automattic-cron-control' ) . "\n" );
361

362
		// Look up full event object
363
		$event = \Automattic\WP\Cron_Control\get_event_by_id( $jid );
364

Erick Hitter's avatar
Erick Hitter committed
365
		if ( is_object( $event ) ) {
366
			// Warning about Internal Events
Erick Hitter's avatar
Erick Hitter committed
367
			if ( \Automattic\WP\Cron_Control\is_internal_event( $event->action ) ) {
368
369
370
				\WP_CLI::warning( __( 'This is an event created by the Cron Control plugin. It will recreated automatically.', 'automattic-cron-control' ) );
			}

371
372
373
374
			\WP_CLI::log( sprintf( __( 'Execution time: %s GMT', 'automattic-cron-control' ), date( TIME_FORMAT, $event->timestamp ) ) );
			\WP_CLI::log( sprintf( __( 'Action: %s', 'automattic-cron-control' ), $event->action ) );
			\WP_CLI::log( sprintf( __( 'Instance identifier: %s', 'automattic-cron-control' ), $event->instance ) );
			\WP_CLI::log( '' );
375
376
			\WP_CLI::confirm( sprintf( __( 'Are you sure you want to delete this event?', 'automattic-cron-control' ) ) );

Erick Hitter's avatar
Erick Hitter committed
377
			// Try to delete the item and provide some relevant output
Erick Hitter's avatar
Erick Hitter committed
378
379
380
			\Automattic\WP\Cron_Control\_suspend_event_creation();
			$deleted = \Automattic\WP\Cron_Control\delete_event_by_id( $event->ID, true );
			\Automattic\WP\Cron_Control\_resume_event_creation();
381

Erick Hitter's avatar
Erick Hitter committed
382
			if ( false === $deleted ) {
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
				\WP_CLI::error( sprintf( __( 'Failed to delete event %d', 'automattic-cron-control' ), $jid ) );
			} else {
				\Automattic\WP\Cron_Control\_flush_internal_caches();
				\WP_CLI::success( sprintf( __( 'Removed event %d', 'automattic-cron-control' ), $jid ) );
				return;
			}
		}

		\WP_CLI::error( sprintf( __( 'Failed to delete event %d. Please confirm that the entry exists and that the ID is that of an event.', 'automattic-cron-control' ), $jid ) );
	}

	/**
	 * Delete all events of the same action
	 */
	private function delete_event_by_action( $args, $assoc_args ) {
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
		$action = $assoc_args['action'];

		// Validate entry
		if ( empty( $action ) ) {
			\WP_CLI::error( __( 'Invalid action', 'automattic-cron-control' ) );
		}

		// Warning about Internal Events
		if ( \Automattic\WP\Cron_Control\is_internal_event( $action ) ) {
			\WP_CLI::warning( __( 'This is an event created by the Cron Control plugin. It will recreated automatically.', 'automattic-cron-control' ) );
		}

		// Set defaults needed to gather all events
		$assoc_args['page']  = 1;
		$assoc_args['limit'] = 50;

		// Gather events
415
		\WP_CLI::log( __( 'Gathering events...', 'automattic-cron-control' ) );
416
417
418
419
420

		$events_to_delete = array();

		$events = $this->get_events( $args, $assoc_args );

421
		\WP_CLI::log( sprintf( _n( 'Found one event to check', 'Found %s events to check', $events['total_items'], 'automattic-cron-control' ), number_format_i18n( $events['total_items'] ) ) );
422
423
424
425
426
427
428
429
430
431
432

		$search_progress = \WP_CLI\Utils\make_progress_bar( sprintf( __( 'Searching events for those with the action `%s`', 'automattic-cron-control' ), $action ), $events['total_items'] );

		// Loop and pull out events to be deleted
		do {
			if ( ! is_array( $events ) || empty( $events['items'] ) ) {
				break;
			}

			// Check events for those that should be deleted
			foreach ( $events['items'] as $single_event ) {
Erick Hitter's avatar
Erick Hitter committed
433
434
				if ( $single_event->action === $action ) {
					$events_to_delete[] = (array) $single_event;
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
				}

				$search_progress->tick();
			}

			// Proceed to next batch
			$assoc_args['page']++;

			if ( $assoc_args['page'] > $events['total_pages'] ) {
				break;
			}

			$events = $this->get_events( $args, $assoc_args );
		} while( $events['page'] <= $events['total_pages'] );

		$search_progress->finish();

452
		\WP_CLI::log( '' );
453
454
455
456
457
458
459
460
461

		// Nothing more to do
		if ( empty( $events_to_delete ) ) {
			\WP_CLI::error( sprintf( __( 'No events with action `%s` found', 'automattic-cron-control' ), $action ) );
		}

		// List the items to remove
		$total_to_delete = count( $events_to_delete );

462
		\WP_CLI::log( sprintf( _n( 'Found one event with action `%2$s`:', 'Found %1$s events with action `%2$s`:', $total_to_delete, 'automattic-cron-control' ), number_format_i18n( $total_to_delete ), $action ) );
463
464

		if ( $total_to_delete <= $assoc_args['limit'] ) {
465
466
467
468
469
470
			// Sort results
			if ( ! empty( $events_to_delete ) ) {
				usort( $events_to_delete, array( $this, 'sort_events' ) );
			}


471
472
			\WP_CLI\Utils\format_items( 'table', $events_to_delete, array(
				'ID',
Erick Hitter's avatar
Erick Hitter committed
473
474
				'created',
				'last_modified',
475
476
477
478
479
480
481
				'timestamp',
				'instance',
			) );
		} else {
			\WP_CLI::warning( sprintf( __( 'Events are not displayed as there are more than %s to remove', 'automattic-cron-control' ), number_format_i18n( $assoc_args['limit'] ) ) );
		}

482
		\WP_CLI::log( '' );
483
484
485
486
487
488
489
490
		\WP_CLI::confirm( _n( 'Are you sure you want to delete this event?', 'Are you sure you want to delete these events?', $total_to_delete, 'automattic-cron-control' ) );

		// Remove the items
		$delete_progress = \WP_CLI\Utils\make_progress_bar( __( 'Deleting events', 'automattic-cron-control' ), $total_to_delete );

		$events_deleted       = array();
		$events_deleted_count = $events_failed_delete = 0;

491
		// Don't create new events while deleting events
Erick Hitter's avatar
Erick Hitter committed
492
		\Automattic\WP\Cron_Control\_suspend_event_creation();
493

494
		foreach ( $events_to_delete as $event_to_delete ) {
Erick Hitter's avatar
Erick Hitter committed
495
			$deleted = \Automattic\WP\Cron_Control\delete_event_by_id( $event_to_delete['ID'], false );
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517

			$events_deleted[] = array(
				'ID'      => $event_to_delete['ID'],
				'deleted' => false === $deleted ? 'no' : 'yes',
			);

			if ( $deleted ) {
				$events_deleted_count++;
			} else {
				$events_failed_delete++;
			}

			$delete_progress->tick();
		}

		$delete_progress->finish();

		// When deletes succeed, sync internal caches
		if ( $events_deleted_count > 0 ) {
			\Automattic\WP\Cron_Control\_flush_internal_caches();
		}

518
		// New events can be created now that removal is complete
Erick Hitter's avatar
Erick Hitter committed
519
		\Automattic\WP\Cron_Control\_resume_event_creation();
520

521
		// List the removed items
522
		\WP_CLI::log( "\n" . __( 'RESULTS:', 'automattic-cron-control' ) );
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543

		if ( 1 === $total_to_delete && 1 === $events_deleted_count ) {
			\WP_CLI::success( sprintf( __( 'Deleted one event: %d', 'automattic-cron-control' ), $events_deleted[0]['ID'] ) );
		} else {
			if ( $events_deleted_count === $total_to_delete ) {
				\WP_CLI::success( sprintf( __( 'Deleted %s events', 'automattic-cron-control' ), number_format_i18n( $events_deleted_count ) ) );
			} else {
				\WP_CLI::warning( sprintf( __( 'Expected to delete %1$s events, but could only delete %2$s events. It\'s likely that some events were executed while this command ran.', 'automattic-cron-control' ), number_format_i18n( $total_to_delete ), number_format_i18n( $events_deleted_count ) ) );
			}

			// Limit just to failed deletes when many events are removed
			if ( count( $events_deleted ) > $assoc_args['limit'] ) {
				$events_deleted = array_filter( $events_deleted, function( $event ) {
					if ( 'no' === $event['deleted'] ) {
						return $event;
					} else {
						return false;
					}
				} );

				if ( count( $events_deleted ) > 0 ) {
544
					\WP_CLI::log( "\n" . __( 'Events that couldn\'t be deleted:', 'automattic-cron-control' ) );
545
546
				}
			} else {
547
				\WP_CLI::log( "\n" . __( 'Events deleted:', 'automattic-cron-control' ) );
548
549
550
551
552
553
554
555
556
557
			}

			// Don't display a table if there's nothing to display
			if ( count( $events_deleted ) > 0 ) {
				\WP_CLI\Utils\format_items( 'table', $events_deleted, array(
					'ID',
					'deleted',
				) );
			}
		}
558
559
560

		return;
	}
561
562
563
564
565
566
567
568
569
570
571
572
573

	/**
	 * Delete all completed events
	 */
	private function delete_completed_events( $args, $assoc_args ) {
		$count = \Automattic\WP\Cron_Control\count_events_by_status( \Automattic\WP\Cron_Control\Events_Store::STATUS_COMPLETED );

		\WP_CLI::confirm( sprintf( _n( 'Found one completed event to remove. Continue?', 'Found %s completed events to remove. Continue?', $count, 'automattic-cron-control' ), number_format_i18n( $count ) ) );

		\Automattic\WP\Cron_Control\Events_Store::instance()->purge_completed_events( false );

		\WP_CLI::success( __( 'Entries removed', 'automattic-cron-control' ) );
	}
574
575
}

576
\WP_CLI::add_command( 'cron-control events', 'Automattic\WP\Cron_Control\CLI\Events' );