From d5d4979921a7ce1d004885223ff77f3619e5c255 Mon Sep 17 00:00:00 2001 From: Andrew Ozz Date: Tue, 2 Apr 2019 23:32:31 +0000 Subject: [PATCH] Site health: - Prevent fatal errors from timeouts on the Tools => Site Health => Info tab. - Use the `get_dirsize()` and `recurse_dirsize()` functions to calculate directory sizes. The results are cached. - Introduce "timeout protection" in `recurse_dirsize()`. Props pento, Clorith, xkon, afercia, jeremyfelt, azaozz. Fixes #46645. git-svn-id: https://develop.svn.wordpress.org/trunk@45104 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-debug-data.php | 122 ++++++++---------- src/wp-includes/default-constants.php | 5 + src/wp-includes/functions.php | 100 ++++++++++++++ src/wp-includes/ms-functions.php | 74 ----------- 4 files changed, 159 insertions(+), 142 deletions(-) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index d2089b20f8..bde398ea23 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -32,7 +32,6 @@ class WP_Debug_Data { */ static function debug_data( $locale = null ) { global $wpdb; - if ( ! empty( $locale ) ) { // Change the language used for translations if ( function_exists( 'switch_to_locale' ) ) { @@ -312,21 +311,33 @@ class WP_Debug_Data { ); } + $size_db = WP_Debug_Data::get_database_size(); + // Go through the various installation directories and calculate their sizes. $uploads_dir = wp_upload_dir(); - $inaccurate = false; /* * We will be using the PHP max execution time to prevent the size calculations - * from causing a timeout. We provide a default value of 30 seconds, as some + * from causing a timeout. The default value is 30 seconds, and some * hosts do not allow you to read configuration values. */ - $max_execution_time = 30; - $start_execution_time = microtime( true ); + $max_execution_time = 30; + if ( function_exists( 'ini_get' ) ) { $max_execution_time = ini_get( 'max_execution_time' ); } + // Here 20 seconds is a "sensible default" for how long to make the user wait for the directory size calculation. + // When testing 20 seconds seem enough in nearly all cases. The remaining edge cases are likely testing or development sites + // that have very large number of files, for example `node_modules` in plugins or themes, etc. + if ( $max_execution_time > 20 ) { + $max_execution_time = 20; + } elseif ( $max_execution_time > 2 ) { + // If the max_execution_time is set to lower than 20 seconds, reduce it a bit to prevent + // edge-case timeouts that may happen after the size loop has finished running. + $max_execution_time -= 1; + } + $size_directories = array( 'wordpress' => array( 'path' => ABSPATH, @@ -346,35 +357,44 @@ class WP_Debug_Data { ), ); + $timeout = __( 'The directory size calculation has timed out. Usually caused by a very large number of sub-directories and files.' ); + $inaccessible = __( 'The size cannot be calculated. The directory is not accessible. Usually caused by invalid permissions.' ); + $size_total = 0; + // Loop over all the directories we want to gather the sizes for. foreach ( $size_directories as $size => $attributes ) { - /* - * We run a helper function with a RecursiveIterator, which - * may throw an exception if it can't access directories. - * - * If a failure is detected we mark the result as inaccurate. - */ - try { - $calculated_size = WP_Debug_data::get_directory_size( $attributes['path'], $max_execution_time, $start_execution_time ); + $dir_size = null; // Default to timeout. - $size_directories[ $size ]['size'] = $calculated_size; - - /* - * If the size returned is -1, this means execution has - * exceeded the maximum execution time, also denoting an - * inaccurate value in the end. - */ - if ( -1 === $calculated_size ) { - $inaccurate = true; - } - } catch ( Exception $e ) { - $inaccurate = true; + if ( microtime( true ) - WP_START_TIMESTAMP < $max_execution_time ) { + $dir_size = get_dirsize( $attributes['path'], $max_execution_time ); } + + if ( $dir_size === false ) { + // Error reading + $dir_size = $inaccessible; + $size_total = null; + } elseif ( $dir_size === null ) { + // Timeout + $dir_size = $timeout; + $size_total = null; + } else { + $is_subdir = ( strpos( $size_directories[ $size ]['path'], ABSPATH ) === 0 ); + + if ( $size_total !== null && ( $size === 'wordpress' || ! $is_subdir ) ) { + $size_total += $dir_size; + } + + $dir_size = size_format( $dir_size, 2 ); + } + + $size_directories[ $size ]['size'] = $dir_size; } - $size_db = WP_Debug_Data::get_database_size(); - - $size_total = $size_directories['wordpress']['size'] + $size_db; + if ( $size_total !== null && $size_db > 0 ) { + $size_total = size_format( $size_total + $size_db, 2 ); + } else { + $size_total = __( 'Total size is not available. Some errors were encountered when determining the size of your installation.' ); + } $info['wp-paths-sizes']['fields'] = array( array( @@ -383,7 +403,7 @@ class WP_Debug_Data { ), array( 'label' => __( 'Uploads Directory Size' ), - 'value' => ( -1 === $size_directories['uploads']['size'] ? __( 'Unable to determine the size of this directory' ) : size_format( $size_directories['uploads']['size'], 2 ) ), + 'value' => $size_directories['uploads']['size'], ), array( 'label' => __( 'Themes Directory Location' ), @@ -395,7 +415,7 @@ class WP_Debug_Data { ), array( 'label' => __( 'Themes Directory Size' ), - 'value' => ( -1 === $size_directories['themes']['size'] ? __( 'Unable to determine the size of this directory' ) : size_format( $size_directories['themes']['size'], 2 ) ), + 'value' => $size_directories['themes']['size'], ), array( 'label' => __( 'Plugins Directory Location' ), @@ -403,7 +423,7 @@ class WP_Debug_Data { ), array( 'label' => __( 'Plugins Directory Size' ), - 'value' => ( -1 === $size_directories['plugins']['size'] ? __( 'Unable to determine the size of this directory' ) : size_format( $size_directories['plugins']['size'], 2 ) ), + 'value' => $size_directories['plugins']['size'], ), array( 'label' => __( 'WordPress Directory Location' ), @@ -411,7 +431,7 @@ class WP_Debug_Data { ), array( 'label' => __( 'WordPress Directory Size' ), - 'value' => size_format( $size_directories['wordpress']['size'], 2 ), + 'value' => $size_directories['wordpress']['size'], ), array( 'label' => __( 'Database size' ), @@ -419,11 +439,7 @@ class WP_Debug_Data { ), array( 'label' => __( 'Total installation size' ), - 'value' => sprintf( - '%s%s', - size_format( $size_total, 2 ), - ( false === $inaccurate ? '' : __( '- Some errors, likely caused by invalid permissions, were encountered when determining the size of your installation. This means the values represented may be inaccurate.' ) ) - ), + 'value' => $size_total, ), ); @@ -934,36 +950,6 @@ class WP_Debug_Data { return $return; } - /** - * Return the size of a directory, including all subdirectories. - * - * @since 5.2.0 - * - * @param string $path The directory to check. - * @param string|int $max_execution_time How long a PHP script can run on this host. - * @param float $start_execution_time When we started executing this section of the script. - * - * @return int The directory size, in bytes. - */ - public static function get_directory_size( $path, $max_execution_time, $start_execution_time ) { - $size = 0; - - foreach ( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path ) ) as $file ) { - // Check if the maximum execution time is a value considered "infinite". - if ( 0 !== $max_execution_time && -1 !== $max_execution_time ) { - $runtime = ( microtime( true ) - $start_execution_time ); - - // If the script has been running as long, or longer, as it is allowed, return a failure message. - if ( $runtime >= $max_execution_time ) { - return -1; - } - } - $size += $file->getSize(); - } - - return $size; - } - /** * Fetch the total size of all the database tables for the active database user. * @@ -982,6 +968,6 @@ class WP_Debug_Data { } } - return $size; + return (int) $size; } } diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index b1830ac76d..ee22547ca9 100644 --- a/src/wp-includes/default-constants.php +++ b/src/wp-includes/default-constants.php @@ -29,6 +29,11 @@ function wp_initial_constants() { define( 'TB_IN_BYTES', 1024 * GB_IN_BYTES ); /**#@-*/ + // Start of run timestamp. + if ( ! defined( 'WP_START_TIMESTAMP' ) ) { + define( 'WP_START_TIMESTAMP', microtime( true ) ); + } + $current_limit = @ini_get( 'memory_limit' ); $current_limit_int = wp_convert_hr_to_bytes( $current_limit ); diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index ff7841aba8..90a3bafbb6 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -7006,3 +7006,103 @@ function wp_direct_php_update_button() { ); echo '

'; } + +/** + * Get the size of a directory. + * + * A helper function that is used primarily to check whether + * a blog has exceeded its allowed upload space. + * + * @since MU (3.0.0) + * + * @param string $directory Full path of a directory. + * @param int $max_execution_time Maximum time to run before giving up. In seconds. + * The timeout is global and is measured from the moment WordPress started to load. + * @return int|false|null Size in MB if a valid directory. False if not. Null if timeout. + */ +function get_dirsize( $directory, $max_execution_time = null ) { + $dirsize = get_transient( 'dirsize_cache' ); + + if ( is_array( $dirsize ) && isset( $dirsize[ $directory ]['size'] ) ) { + return $dirsize[ $directory ]['size']; + } + + if ( ! is_array( $dirsize ) ) { + $dirsize = array(); + } + + // Exclude individual site directories from the total when checking the main site of a network + // as they are subdirectories and should not be counted. + if ( is_multisite() && is_main_site() ) { + $dirsize[ $directory ]['size'] = recurse_dirsize( $directory, $directory . '/sites', $max_execution_time ); + } else { + $dirsize[ $directory ]['size'] = recurse_dirsize( $directory, null, $max_execution_time ); + } + + set_transient( 'dirsize_cache', $dirsize, HOUR_IN_SECONDS ); + return $dirsize[ $directory ]['size']; +} + +/** + * Get the size of a directory recursively. + * + * Used by get_dirsize() to get a directory's size when it contains + * other directories. + * + * @since MU (3.0.0) + * @since 4.3.0 $exclude parameter added. + * + * @param string $directory Full path of a directory. + * @param string $exclude Optional. Full path of a subdirectory to exclude from the total. + * @param int $max_execution_time Maximum time to run before giving up. In seconds. + * The timeout is global and is measured from the moment WordPress started to load. + * @return int|false|null Size in MB if a valid directory. False if not. Null if timeout. + */ +function recurse_dirsize( $directory, $exclude = null, $max_execution_time = null ) { + $size = 0; + + $directory = untrailingslashit( $directory ); + + if ( ! file_exists( $directory ) || ! is_dir( $directory ) || ! is_readable( $directory ) || $directory === $exclude ) { + return false; + } + + if ( ! $max_execution_time ) { + // Keep the previous behavior but attempt to prevent fatal errors from timeout. + if ( function_exists( 'ini_get' ) ) { + $max_execution_time = ini_get( 'max_execution_time' ); + } else { + // Use PHP default. + $max_execution_time = 30; + } + + // Leave 1 second "buffer" for other operations if $max_execution_time has reasonable value. + if ( $max_execution_time > 10 ) { + $max_execution_time -= 1; + } + } + + if ( $handle = opendir( $directory ) ) { + while ( ( $file = readdir( $handle ) ) !== false ) { + $path = $directory . '/' . $file; + if ( $file != '.' && $file != '..' ) { + if ( is_file( $path ) ) { + $size += filesize( $path ); + } elseif ( is_dir( $path ) ) { + $handlesize = recurse_dirsize( $path, $exclude, $max_execution_time ); + if ( $handlesize > 0 ) { + $size += $handlesize; + } + } + + if ( microtime( true ) - WP_START_TIMESTAMP > $max_execution_time ) { + // Time exceeded. Give up instead of risking a fatal timeout. + $size = null; + break; + } + } + } + closedir( $handle ); + } + return $size; +} diff --git a/src/wp-includes/ms-functions.php b/src/wp-includes/ms-functions.php index 5293cd2159..1e661dde24 100644 --- a/src/wp-includes/ms-functions.php +++ b/src/wp-includes/ms-functions.php @@ -1777,80 +1777,6 @@ function get_most_recent_post_of_user( $user_id ) { // Misc functions -/** - * Get the size of a directory. - * - * A helper function that is used primarily to check whether - * a blog has exceeded its allowed upload space. - * - * @since MU (3.0.0) - * - * @param string $directory Full path of a directory. - * @return int Size of the directory in MB. - */ -function get_dirsize( $directory ) { - $dirsize = get_transient( 'dirsize_cache' ); - if ( is_array( $dirsize ) && isset( $dirsize[ $directory ]['size'] ) ) { - return $dirsize[ $directory ]['size']; - } - - if ( ! is_array( $dirsize ) ) { - $dirsize = array(); - } - - // Exclude individual site directories from the total when checking the main site, - // as they are subdirectories and should not be counted. - if ( is_main_site() ) { - $dirsize[ $directory ]['size'] = recurse_dirsize( $directory, $directory . '/sites' ); - } else { - $dirsize[ $directory ]['size'] = recurse_dirsize( $directory ); - } - - set_transient( 'dirsize_cache', $dirsize, HOUR_IN_SECONDS ); - return $dirsize[ $directory ]['size']; -} - -/** - * Get the size of a directory recursively. - * - * Used by get_dirsize() to get a directory's size when it contains - * other directories. - * - * @since MU (3.0.0) - * @since 4.3.0 $exclude parameter added. - * - * @param string $directory Full path of a directory. - * @param string $exclude Optional. Full path of a subdirectory to exclude from the total. - * @return int|false Size in MB if a valid directory. False if not. - */ -function recurse_dirsize( $directory, $exclude = null ) { - $size = 0; - - $directory = untrailingslashit( $directory ); - - if ( ! file_exists( $directory ) || ! is_dir( $directory ) || ! is_readable( $directory ) || $directory === $exclude ) { - return false; - } - - if ( $handle = opendir( $directory ) ) { - while ( ( $file = readdir( $handle ) ) !== false ) { - $path = $directory . '/' . $file; - if ( $file != '.' && $file != '..' ) { - if ( is_file( $path ) ) { - $size += filesize( $path ); - } elseif ( is_dir( $path ) ) { - $handlesize = recurse_dirsize( $path, $exclude ); - if ( $handlesize > 0 ) { - $size += $handlesize; - } - } - } - } - closedir( $handle ); - } - return $size; -} - /** * Check an array of MIME types against a whitelist. *