do/commands/cron
2025-06-21 14:52:40 -04:00

428 lines
No EOL
14 KiB
Bash
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ----------------------------------------------------
# Cron Commands
# Manages scheduled tasks for the _do script.
# ----------------------------------------------------
# ----------------------------------------------------
# (Helper) PHP script to manage cron events.
# ----------------------------------------------------
function _get_cron_manager_php_script() {
read -r -d '' php_script <<'PHP'
<?php
$argv = WP_CLI::get_runner()->arguments;
array_shift( $argv );
// This script is a self-contained manager for cron events stored in a WP option.
// It is designed to be called with specific actions and arguments.
// Prevent direct execution.
if (empty($argv) || !isset($argv[1])) {
return;
}
$action = $argv[1] ?? null;
// The main function for this script is to get the option, unserialize it,
// perform an action, then serialize and save the result.
function get_events() {
// get_option will return the value of the option, already unserialized.
// The second argument is the default value if the option does not exist.
$events = get_option("captaincore_do_cron", []);
return is_array($events) ? $events : [];
}
function save_events($events) {
// update_option will create the option if it does not exist.
// The third argument 'no' sets autoload to false.
update_option( "captaincore_do_cron", $events, 'no');
}
// --- Action Router ---
if ($action === 'list_all') {
$events = get_events();
if (empty($events)) {
return;
}
// Sort events by the next_run timestamp to show the soonest first.
usort($events, function($a, $b) {
return ($a['next_run'] ?? 0) <=> ($b['next_run'] ?? 0);
});
// MODIFICATION: Get the WordPress timezone once before the loop.
$wp_timezone = wp_timezone();
foreach ($events as $event) {
// MODIFICATION: Convert the stored UTC timestamp to the WP timezone for display.
$next_run_formatted = 'N/A';
if (isset($event['next_run'])) {
// Create a DateTime object from the UTC timestamp
$next_run_dt = new DateTime("@" . $event['next_run']);
// Set the object's timezone to the WordPress configured timezone
$next_run_dt->setTimezone($wp_timezone);
// Format for output
$next_run_formatted = $next_run_dt->format('Y-m-d H:i:s T');
}
// Output in CSV format for gum table
echo implode(',', [
$event['id'] ?? 'N/A',
'"' . ($event['command'] ?? 'N/A') . '"', // Quote command in case it has spaces
$next_run_formatted,
$event['frequency'] ?? 'N/A'
]) . "\n";
}
}
elseif ($action === 'list_due') {
$now = time();
$due_events = [];
foreach (get_events() as $event) {
if (isset($event['next_run']) && $event['next_run'] <= $now) {
$due_events[] = $event;
}
}
echo json_encode($due_events);
}
elseif ($action === 'add') {
$id = uniqid('event_');
$command = $argv[2] ?? null;
$next_run_str = $argv[3] ?? null;
$frequency = $argv[4] ?? null;
if (!$command || !$next_run_str || !$frequency) {
error_log('Error: Missing arguments for add action.');
return;
}
// --- Frequency Translation ---
$freq_lower = strtolower($frequency);
if ($freq_lower === 'weekly') {
$frequency = '1 week';
} elseif ($freq_lower === 'daily') {
$frequency = '1 day';
} elseif ($freq_lower === 'monthly') {
$frequency = '1 month';
} elseif ($freq_lower === 'hourly') {
$frequency = '1 hour';
}
// --- End Translation ---
try {
// Use the WordPress configured timezone for parsing the date string.
$wp_timezone = wp_timezone();
$next_run_dt = new DateTime($next_run_str, $wp_timezone);
$next_run_timestamp = $next_run_dt->getTimestamp();
} catch (Exception $e) {
error_log('Error: Invalid date/time string for next_run: ' . $e->getMessage());
return;
}
$events = get_events();
$events[] = [
'id' => $id,
'command' => $command,
'next_run' => $next_run_timestamp,
'frequency' => $frequency,
];
save_events($events);
echo "✅ Event '$id' added. Input '{$next_run_str}' interpreted using WordPress timezone ({$wp_timezone->getName()}). Next run: " . date('Y-m-d H:i:s T', $next_run_timestamp) . "\n";
}
elseif ($action === 'delete') {
$id_to_delete = $argv[2] ?? null;
if (!$id_to_delete) {
error_log('Error: No ID provided for delete action.');
return;
}
$events = get_events();
$updated_events = [];
$found = false;
foreach ($events as $event) {
if (isset($event['id']) && $event['id'] === $id_to_delete) {
$found = true;
} else {
$updated_events[] = $event;
}
}
if ($found) {
save_events($updated_events);
echo "✅ Event '$id_to_delete' deleted successfully.\n";
} else {
echo "❌ Error: Event with ID '$id_to_delete' not found.\n";
}
}
elseif ($action === 'update_next_run') {
$id = $argv[2] ?? null;
if (!$id) {
error_log('Error: No ID provided to update_next_run.');
return;
}
$events = get_events();
$found = false;
foreach ($events as &$event) {
if (isset($event['id']) && $event['id'] === $id) {
try {
$last_run_ts = $event['next_run'] ??
time();
$next_run_dt = new DateTime("@{$last_run_ts}", new DateTimeZone('UTC'));
do {
$next_run_dt->modify('+ ' . $event['frequency']);
} while ($next_run_dt->getTimestamp() <= time());
$event['next_run'] = $next_run_dt->getTimestamp();
$found = true;
break;
} catch (Exception $e) {
error_log('Error: Invalid frequency string "' . ($event['frequency'] ?? '') . '": ' . $e->getMessage());
return;
}
}
}
if ($found) {
save_events($events);
}
}
PHP
echo "$php_script"
}
# ----------------------------------------------------
# Configures the global cron runner by installing the latest script.
# ----------------------------------------------------
function cron_enable() {
echo "Attempting to configure cron runner..."
if ! setup_wp_cli || ! "$WP_CLI_CMD" core is-installed --quiet; then
echo "❌ Error: This command must be run from within a WordPress directory." >&2
return 1
fi
if ! command -v realpath &> /dev/null || ! command -v md5sum &> /dev/null; then
echo "❌ Error: 'realpath' and 'md5sum' commands are required." >&2
return 1
fi
# Determine the absolute path of the WordPress installation
local wp_path
wp_path=$(realpath ".")
if [[ ! -f "$wp_path/wp-load.php" ]]; then
echo "❌ Error: Could not confirm WordPress root at '$wp_path'." >&2
return 1
fi
local private_dir
if ! private_dir=$(_get_private_dir); then return 1; fi
local script_path="$private_dir/_do.sh"
echo " Downloading the latest version of the _do script..."
if ! command -v curl &> /dev/null; then
echo "❌ Error: 'curl' is required to download the script." >&2; return 1;
fi
if ! curl -sL "https://captaincore.io/do" -o "$script_path"; then
echo "❌ Error: Failed to download the _do script." >&2; return 1;
fi
chmod +x "$script_path"
echo "✅ Script installed/updated at: $script_path"
# Make the marker unique to the path to allow multiple cron jobs
local path_hash
path_hash=$(echo "$wp_path" | md5sum | cut -d' ' -f1)
local cron_marker="#_DO_CRON_RUNNER_$path_hash"
local cron_command="bash \"$script_path\" cron run --path=\"$wp_path\""
local cron_job="*/10 * * * * $cron_command $cron_marker"
# Atomically update the crontab
local current_crontab
current_crontab=$(crontab -l 2>/dev/null | grep -v "$cron_marker")
(echo "$current_crontab"; echo "$cron_job") | crontab -
if [ $? -eq 0 ]; then
local site_url
site_url=$("$WP_CLI_CMD" option get home --skip-plugins --skip-themes 2>/dev/null)
echo "✅ Cron runner enabled for site: $site_url ($wp_path)"
else
echo "❌ Error: Could not modify crontab. Please check your permissions." >&2
return 1
fi
echo "Current crontab:"
crontab -l
}
# ----------------------------------------------------
# Runs the cron process, executing any due events.
# ----------------------------------------------------
function cron_run() {
# If a path is provided, change to that directory first.
if [[ -n "$path_flag" ]]; then
if [ -d "$path_flag" ]; then
cd "$path_flag" || { echo "[$(date)] Cron Error: Could not change directory to '$path_flag'." >> /tmp/_do_cron.log; return 1; }
else
echo "[$(date)] Cron Error: Provided path '$path_flag' does not exist." >> /tmp/_do_cron.log;
return 1
fi
fi
# Call the setup function to ensure the wp command path is known.
if ! setup_wp_cli; then
echo "[$(date)] Cron Error: WP-CLI setup failed." >> /tmp/_do_cron.log
return 1
fi
# Check if this is a WordPress installation using the full command path.
if ! "$WP_CLI_CMD" core is-installed --quiet; then
return 1
fi
local php_script;
php_script=$(_get_cron_manager_php_script)
local due_events_json;
# Use the full command path for all wp-cli calls.
due_events_json=$(echo "$php_script" | "$WP_CLI_CMD" eval-file - 'list_due' 2>&1)
if [ $? -ne 0 ];
then
echo "[$(date)] Cron run failed: $due_events_json" >> /tmp/_do_cron.log
return 1
fi
if [ -z "$due_events_json" ] || [[ "$due_events_json" == "[]" ]];
then
return 0
fi
local php_parser='
$json = file_get_contents("php://stdin");
$events = json_decode($json, true);
if (is_array($events)) {
foreach($events as $event) {
echo $event["id"] . "|" . $event["command"] . "\n";
}
}
'
local due_events_list;
due_events_list=$(echo "$due_events_json" | php -r "$php_parser")
if [ -z "$due_events_list" ];
then
return 0
fi
echo "Found due events, processing..."
while IFS='|' read -r id command; do
if [ -z "$id" ] || [ -z "$command" ]; then
continue
fi
echo "-> Running event '$id': _do $command"
local script_path
script_path=$(realpath "$0")
bash "$script_path" $command
echo "-> Updating next run time for event '$id'"
# Use the full command path here as well.
echo "$php_script" | "$WP_CLI_CMD" eval-file - 'update_next_run' "$id"
done <<< "$due_events_list"
echo "Cron run complete."
}
# ----------------------------------------------------
# Adds a new command to the cron schedule.
# ----------------------------------------------------
function cron_add() {
local command="$1"
local next_run="$2"
local frequency="$3"
if [ -z "$command" ] || [ -z "$next_run" ] || [ -z "$frequency" ]; then
echo "❌ Error: Missing arguments." >&2
show_command_help "cron"
return 1
fi
if ! setup_wp_cli; then echo "❌ Error: WP-CLI not found." >&2; return 1; fi
if ! "$WP_CLI_CMD" core is-installed --quiet; then echo "❌ Error: Not in a WordPress installation." >&2; return 1; fi
echo "Adding new cron event..."
local php_script; php_script=$(_get_cron_manager_php_script)
# Capture both stdout and stderr to a variable
local output; output=$(echo "$php_script" | "$WP_CLI_CMD" eval-file - "add" "$command" "$next_run" "$frequency" 2>&1)
# Check the exit code of the wp-cli command
if [ $? -ne 0 ]; then
echo "❌ Error: The wp-cli command failed to execute."
echo " Output:"
# Indent the output for readability
echo "$output" | sed 's/^/ /'
else
# Print the success message from the PHP script
echo "$output"
fi
}
# ----------------------------------------------------
# Deletes a scheduled cron event by its ID.
# ----------------------------------------------------
function cron_delete() {
local event_id="$1"
if [ -z "$event_id" ]; then
echo "❌ Error: No event ID provided." >&2
show_command_help "cron"
return 1
fi
if ! setup_wp_cli; then echo "❌ Error: WP-CLI not found." >&2; return 1; fi
if ! "$WP_CLI_CMD" core is-installed --quiet; then echo "❌ Error: Not in a WordPress installation." >&2; return 1; fi
echo "Attempting to delete event '$event_id'..."
local php_script
php_script=$(_get_cron_manager_php_script)
# Capture and display output from the PHP script
local output
output=$(echo "$php_script" | "$WP_CLI_CMD" eval-file - "delete" "$event_id" 2>&1)
# The PHP script now prints success or error, so just display it.
echo "$output"
}
# ----------------------------------------------------
# Lists all scheduled cron events in a table.
# ----------------------------------------------------
function cron_list() {
if ! setup_wp_cli; then echo "❌ Error: WP-CLI not found." >&2; return 1; fi
if ! "$WP_CLI_CMD" core is-installed --quiet; then echo "❌ Error: Not in a WordPress installation." >&2; return 1; fi
if ! setup_gum; then return 1; fi
echo "🔎 Fetching scheduled events..."
local php_script; php_script=$(_get_cron_manager_php_script)
# Capture stdout and stderr
local events_csv; events_csv=$(echo "$php_script" | "$WP_CLI_CMD" eval-file - 'list_all' 2>&1)
# Check exit code
if [ $? -ne 0 ]; then
echo "❌ Error: The wp-cli command failed while listing events."
echo " Output:"
echo "$events_csv" | sed 's/^/ /'
return 1
fi
if [ -z "$events_csv" ]; then
echo " No scheduled cron events found."
return 0
fi
local table_header="ID,Command,Next Run,Frequency"
# Prepend the header and pipe to gum table for a formatted view
(echo "$table_header"; echo "$events_csv") | "$GUM_CMD" table --print --separator ","
}