Skip to content

Commit f80580c

Browse files
committed
feat(backup): add comprehensive error tracking to EasyDash failure callbacks
- Add error type constants for categorized error reporting (validation, config, filesystem, network, database, disk_space, lock, fatal, interrupted, unknown) - Add error tracking properties (message, type, code) to capture failure details - Create capture_error() method to store error information for API callbacks - Enhance dash_shutdown_handler() to auto-capture PHP fatal errors and process interruptions - Update send_dash_failure_callback() to include error details in API payload Error captures added for: - Invalid --dash-auth format (1001) - Unsupported site type (1003) - rclone not installed (2001) - rclone backend not configured (2002) - Concurrent backup process (lock file) (2003) - Insufficient disk space (3001) - Tool installation failures (2010, 2011) - rclone upload failures (4001) - PHP fatal errors (captured from error_get_last) - Process interruptions (Ctrl+C, SIGTERM, SIGKILL)
1 parent c9e0cef commit f80580c

1 file changed

Lines changed: 139 additions & 8 deletions

File tree

src/helper/Site_Backup_Restore.php

Lines changed: 139 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@
1111

1212
class Site_Backup_Restore {
1313

14+
// Error type constants for categorized error reporting
15+
const ERROR_TYPE_VALIDATION = 'validation_error'; // Invalid input
16+
const ERROR_TYPE_CONFIG = 'configuration_error'; // Missing config
17+
const ERROR_TYPE_FILESYSTEM = 'filesystem_error'; // File/dir issues
18+
const ERROR_TYPE_NETWORK = 'network_error'; // Upload/download
19+
const ERROR_TYPE_DATABASE = 'database_error'; // DB operations
20+
const ERROR_TYPE_DISK_SPACE = 'disk_space_error'; // Insufficient space
21+
const ERROR_TYPE_LOCK = 'lock_error'; // Concurrent operation
22+
const ERROR_TYPE_FATAL = 'fatal_error'; // PHP fatal
23+
const ERROR_TYPE_INTERRUPTED = 'interrupted'; // Killed/stopped
24+
const ERROR_TYPE_UNKNOWN = 'unknown_error'; // Unexpected
25+
1426
private $fs;
1527
public $site_data;
1628
private $rclone_config_path;
@@ -24,6 +36,11 @@ class Site_Backup_Restore {
2436
private $dash_backup_completed = false;
2537
private $dash_new_backup_path; // Track new backup path for potential rollback
2638

39+
// Error tracking for EasyDash failure callbacks
40+
private $dash_error_message = '';
41+
private $dash_error_type = 'unknown';
42+
private $dash_error_code = 0;
43+
2744
public function __construct() {
2845
$this->fs = new Filesystem();
2946
}
@@ -48,11 +65,16 @@ public function backup( $args, $assoc_args = [] ) {
4865
// Debug: Log the raw dash_auth value received
4966
EE::debug( 'Received --dash-auth value: ' . $dash_auth );
5067

51-
// Parse backup-id:backup-verification-token format
52-
$auth_parts = explode( ':', $dash_auth, 2 );
53-
if ( count( $auth_parts ) !== 2 || empty( $auth_parts[0] ) || empty( $auth_parts[1] ) ) {
54-
EE::error( 'Invalid --dash-auth format. Expected: backup-id:backup-verification-token' );
55-
}
68+
// Parse backup-id:backup-verification-token format
69+
$auth_parts = explode( ':', $dash_auth, 2 );
70+
if ( count( $auth_parts ) !== 2 || empty( $auth_parts[0] ) || empty( $auth_parts[1] ) ) {
71+
$this->capture_error(
72+
'Invalid --dash-auth format. Expected: backup-id:backup-verification-token',
73+
self::ERROR_TYPE_VALIDATION,
74+
1001
75+
);
76+
EE::error( 'Invalid --dash-auth format. Expected: backup-id:backup-verification-token' );
77+
}
5678

5779
// Check for ed-api-url configuration
5880
$ed_api_url = get_config_value( 'ed-api-url', '' );
@@ -92,6 +114,11 @@ public function backup( $args, $assoc_args = [] ) {
92114
$this->backup_php_wp( $backup_dir );
93115
break;
94116
default:
117+
$this->capture_error(
118+
sprintf( 'Backup is not supported for site type: %s', $this->site_data['site_type'] ),
119+
self::ERROR_TYPE_VALIDATION,
120+
1003
121+
);
95122
EE::error( 'Backup is not supported for this site type.' );
96123
}
97124

@@ -125,10 +152,40 @@ public function backup( $args, $assoc_args = [] ) {
125152
/**
126153
* Shutdown handler to send failure callback to EasyDash if backup didn't complete.
127154
* This is called when script terminates (including via EE::error which calls exit).
155+
*
156+
* Automatically captures fatal errors and interrupted processes if no error was
157+
* explicitly captured during backup execution.
128158
*/
129159
public function dash_shutdown_handler() {
130160
// Only send failure callback if dash auth was enabled and backup didn't complete
131161
if ( $this->dash_auth_enabled && ! $this->dash_backup_completed ) {
162+
163+
// If no error was captured yet, try to capture shutdown error
164+
if ( empty( $this->dash_error_message ) ) {
165+
$last_error = error_get_last();
166+
167+
// Check if this was a fatal PHP error
168+
if ( $last_error && in_array( $last_error['type'], [ E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ] ) ) {
169+
$this->capture_error(
170+
sprintf(
171+
'PHP Fatal Error: %s in %s:%d',
172+
$last_error['message'],
173+
basename( $last_error['file'] ),
174+
$last_error['line']
175+
),
176+
self::ERROR_TYPE_FATAL,
177+
$last_error['type']
178+
);
179+
} else {
180+
// Script was killed, interrupted, or failed unexpectedly
181+
$this->capture_error(
182+
'Backup process was interrupted or killed unexpectedly',
183+
self::ERROR_TYPE_INTERRUPTED,
184+
0
185+
);
186+
}
187+
}
188+
132189
$this->send_dash_failure_callback(
133190
$this->dash_api_url,
134191
$this->dash_backup_id,
@@ -137,6 +194,30 @@ public function dash_shutdown_handler() {
137194
}
138195
}
139196

197+
/**
198+
* Capture error details for EasyDash failure callback.
199+
* Stores error information to be sent when backup fails.
200+
*
201+
* @param string $message Error message describing what went wrong.
202+
* @param string $type Error type category (use ERROR_TYPE_* constants).
203+
* @param int $code Error code for additional context (optional).
204+
*/
205+
private function capture_error( $message, $type = self::ERROR_TYPE_UNKNOWN, $code = 0 ) {
206+
// Only capture the first error (root cause)
207+
if ( empty( $this->dash_error_message ) ) {
208+
$this->dash_error_message = $message;
209+
$this->dash_error_type = $type;
210+
$this->dash_error_code = $code;
211+
212+
EE::debug( sprintf(
213+
'Captured error for EasyDash: [%s] %s (code: %d)',
214+
$type,
215+
$message,
216+
$code
217+
) );
218+
}
219+
}
220+
140221
public function restore( $args, $assoc_args = [] ) {
141222

142223
delem_log( 'site restore start' );
@@ -659,6 +740,11 @@ private function pre_backup_restore_checks() {
659740
$return_code = EE::exec( $command );
660741

661742
if ( ! $return_code ) {
743+
$this->capture_error(
744+
'rclone is not installed',
745+
self::ERROR_TYPE_CONFIG,
746+
2001
747+
);
662748
EE::error( 'rclone is not installed. Please install rclone for backup/restore: https://rclone.org/downloads/#script-download-and-install' );
663749
}
664750

@@ -669,6 +755,11 @@ private function pre_backup_restore_checks() {
669755
$rclone_path = explode( ':', $rclone_path )[0] . ':';
670756

671757
if ( strpos( $output->stdout, $rclone_path ) === false ) {
758+
$this->capture_error(
759+
'rclone backend easyengine does not exist',
760+
self::ERROR_TYPE_CONFIG,
761+
2002
762+
);
672763
EE::error( 'rclone backend easyengine does not exist. Please create it using `rclone config`' );
673764
}
674765

@@ -686,6 +777,11 @@ private function pre_backup_restore_checks() {
686777
$lock_file = EE_BACKUP_DIR . '/' . $this->site_data['site_url'] . '.lock';
687778

688779
if ( $this->fs->exists( $lock_file ) ) {
780+
$this->capture_error(
781+
'Another backup/restore process is already running for this site',
782+
self::ERROR_TYPE_LOCK,
783+
2003
784+
);
689785
EE::error( 'Another backup/restore process is running. Please wait for it to complete.' );
690786
} else {
691787
$this->fs->dumpFile( $lock_file, 'lock' );
@@ -711,6 +807,16 @@ private function pre_backup_check() {
711807
if ( $site_size > $free_space ) {
712808
$error_message = $this->build_disk_space_error_message( 'backup', $site_size, $free_space );
713809

810+
$this->capture_error(
811+
sprintf(
812+
'Insufficient disk space for backup. Required: %s, Available: %s',
813+
$this->format_bytes( $site_size ),
814+
$this->format_bytes( $free_space )
815+
),
816+
self::ERROR_TYPE_DISK_SPACE,
817+
3001
818+
);
819+
714820
$this->fs->remove( EE_BACKUP_DIR . '/' . $this->site_data['site_url'] . '.lock' );
715821
EE::error( $error_message );
716822
}
@@ -751,13 +857,23 @@ private function check_and_install( $command, $name ) {
751857
$status = EE::exec( "command -v $command" );
752858
if ( ! $status ) {
753859
if ( IS_DARWIN ) {
860+
$this->capture_error(
861+
sprintf( '%s is not installed (required for backup/restore)', $name ),
862+
self::ERROR_TYPE_CONFIG,
863+
2010
864+
);
754865
EE::error( "$name is not installed. Please install $name for backup/restore. You can install it using `brew install $name`." );
755866
} else {
756867
$status = EE::exec( 'apt-get --version' );
757868
if ( $status ) {
758869
EE::exec( 'apt-get update' );
759870
EE::exec( "apt-get install -y $name" );
760871
} else {
872+
$this->capture_error(
873+
sprintf( '%s is not installed and could not be auto-installed (apt-get not available)', $name ),
874+
self::ERROR_TYPE_CONFIG,
875+
2011
876+
);
761877
EE::error( "$name is not installed. Please install $name for backup/restore." );
762878
}
763879
}
@@ -1058,6 +1174,11 @@ private function rclone_upload( $path ) {
10581174
$output = EE::launch( $command );
10591175

10601176
if ( $output->return_code ) {
1177+
$this->capture_error(
1178+
'Failed to upload backup to remote storage via rclone',
1179+
self::ERROR_TYPE_NETWORK,
1180+
4001
1181+
);
10611182
EE::error( 'Error uploading backup to remote storage.' );
10621183
} else {
10631184

@@ -1225,6 +1346,7 @@ private function send_dash_success_callback( $ed_api_url, $backup_id, $verify_to
12251346

12261347
/**
12271348
* Send failure callback to EasyDash API after failed backup.
1349+
* Includes error details (message, type, code) for debugging and user feedback.
12281350
*
12291351
* @param string $ed_api_url The EasyDash API URL.
12301352
* @param string $backup_id The backup ID.
@@ -1234,11 +1356,20 @@ private function send_dash_failure_callback( $ed_api_url, $backup_id, $verify_to
12341356
$endpoint = rtrim( $ed_api_url, '/' ) . '/easydash.easydash.doctype.site_backup.site_backup.on_ee_backup_failure';
12351357

12361358
$payload = [
1237-
'site' => $this->site_data['site_url'],
1238-
'backup' => $backup_id,
1239-
'verify' => $verify_token,
1359+
'site' => $this->site_data['site_url'],
1360+
'backup' => $backup_id,
1361+
'verify' => $verify_token,
1362+
'error_message' => $this->dash_error_message ?: 'Unknown error occurred',
1363+
'error_type' => $this->dash_error_type,
1364+
'error_code' => $this->dash_error_code,
12401365
];
12411366

1367+
EE::debug( 'Sending failure callback with error details: ' . json_encode( [
1368+
'error_message' => $payload['error_message'],
1369+
'error_type' => $payload['error_type'],
1370+
'error_code' => $payload['error_code'],
1371+
] ) );
1372+
12421373
$this->send_dash_request( $endpoint, $payload );
12431374
}
12441375

0 commit comments

Comments
 (0)