X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=maintenance%2FMaintenance.php;h=fda12f9e7e6224018d2e6b078ace8b7b0b64032f;hb=5f230a3a1958ee607f2fd3fe73420bbfc84e45c7;hp=8dc9d5324a820a98c96ea110dd43e75efa465b6e;hpb=6a238e13cab53067e1e6609627e52f777c49f316;p=lhc%2Fweb%2Fwiklou.git diff --git a/maintenance/Maintenance.php b/maintenance/Maintenance.php index 8dc9d5324a..fda12f9e7e 100644 --- a/maintenance/Maintenance.php +++ b/maintenance/Maintenance.php @@ -1,21 +1,40 @@ * @since 1.16 * @ingroup Maintenance @@ -59,6 +63,9 @@ abstract class Maintenance { // This is the desired params protected $mParams = array(); + // Array of mapping short parameters to long ones + protected $mShortParamsMap = array(); + // Array of desired args protected $mArgList = array(); @@ -81,10 +88,22 @@ abstract class Maintenance { // Have we already loaded our user input? protected $mInputLoaded = false; - // Batch size. If a script supports this, they should set - // a default with setBatchSize() + /** + * Batch size. If a script supports this, they should set + * a default with setBatchSize() + * + * @var int + */ protected $mBatchSize = null; + // Generic options added by addDefaultParams() + private $mGenericParameters = array(); + // Generic options which might or not be supported by the script + private $mDependantParameters = array(); + + // Used by getDD() / setDB() + private $mDb = null; + /** * List of all the core maintenance scripts. This is added * to scripts added by extensions in $wgMaintenanceScripts @@ -93,14 +112,45 @@ abstract class Maintenance { protected static $mCoreScripts = null; /** - * Default constructor. Children should call this if implementing + * Default constructor. Children should call this *first* if implementing * their own constructors */ public function __construct() { + // Setup $IP, using MW_INSTALL_PATH if it exists + global $IP; + $IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== '' + ? getenv( 'MW_INSTALL_PATH' ) + : realpath( dirname( __FILE__ ) . '/..' ); + $this->addDefaultParams(); register_shutdown_function( array( $this, 'outputChanneled' ), false ); } + /** + * Should we execute the maintenance script, or just allow it to be included + * as a standalone class? It checks that the call stack only includes this + * function and "requires" (meaning was called from the file scope) + * + * @return Boolean + */ + public static function shouldExecute() { + $bt = debug_backtrace(); + $count = count( $bt ); + if ( $count < 2 ) { + return false; // sanity + } + if ( $bt[0]['class'] !== 'Maintenance' || $bt[0]['function'] !== 'shouldExecute' ) { + return false; // last call should be to this function + } + $includeFuncs = array( 'require_once', 'require', 'include', 'include_once' ); + for( $i=1; $i < $count; $i++ ) { + if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) { + return false; // previous calls should all be "requires" + } + } + return true; + } + /** * Do the actual work. All child classes will need to implement this */ @@ -110,19 +160,23 @@ abstract class Maintenance { * Add a parameter to the script. Will be displayed on --help * with the associated description * - * @param $name String The name of the param (help, version, etc) - * @param $description String The description of the param to show on --help - * @param $required boolean Is the param required? - * @param $withArg Boolean Is an argument required with this option? + * @param $name String: the name of the param (help, version, etc) + * @param $description String: the description of the param to show on --help + * @param $required Boolean: is the param required? + * @param $withArg Boolean: is an argument required with this option? + * @param $shortName String: character to use as short name */ - protected function addOption( $name, $description, $required = false, $withArg = false ) { - $this->mParams[$name] = array( 'desc' => $description, 'require' => $required, 'withArg' => $withArg ); + protected function addOption( $name, $description, $required = false, $withArg = false, $shortName = false ) { + $this->mParams[$name] = array( 'desc' => $description, 'require' => $required, 'withArg' => $withArg, 'shortName' => $shortName ); + if ( $shortName !== false ) { + $this->mShortParamsMap[$shortName] = $name; + } } /** * Checks to see if a particular param exists. - * @param $name String The name of the param - * @return boolean + * @param $name String: the name of the param + * @return Boolean */ protected function hasOption( $name ) { return isset( $this->mOptions[$name] ); @@ -130,12 +184,12 @@ abstract class Maintenance { /** * Get an option, or return the default - * @param $name String The name of the param - * @param $default mixed Anything you want, default null - * @return mixed + * @param $name String: the name of the param + * @param $default Mixed: anything you want, default null + * @return Mixed */ protected function getOption( $name, $default = null ) { - if( $this->hasOption( $name ) ) { + if ( $this->hasOption( $name ) ) { return $this->mOptions[$name]; } else { // Set it so we don't have to provide the default again @@ -146,22 +200,38 @@ abstract class Maintenance { /** * Add some args that are needed - * @param $arg String Name of the arg, like 'start' - * @param $description String Short description of the arg - * @param $required Boolean Is this required? + * @param $arg String: name of the arg, like 'start' + * @param $description String: short description of the arg + * @param $required Boolean: is this required? */ protected function addArg( $arg, $description, $required = true ) { - $this->mArgList[] = array( + $this->mArgList[] = array( 'name' => $arg, - 'desc' => $description, - 'require' => $required + 'desc' => $description, + 'require' => $required ); } + /** + * Remove an option. Useful for removing options that won't be used in your script. + * @param $name String: the option to remove. + */ + protected function deleteOption( $name ) { + unset( $this->mParams[$name] ); + } + + /** + * Set the description text. + * @param $text String: the text of the description + */ + protected function addDescription( $text ) { + $this->mDescription = $text; + } + /** * Does a given argument exist? - * @param $argId int The integer value (from zero) for the arg - * @return boolean + * @param $argId Integer: the integer value (from zero) for the arg + * @return Boolean */ protected function hasArg( $argId = 0 ) { return isset( $this->mArgs[$argId] ); @@ -169,8 +239,8 @@ abstract class Maintenance { /** * Get an argument. - * @param $argId int The integer value (from zero) for the arg - * @param $default mixed The default if it doesn't exist + * @param $argId Integer: the integer value (from zero) for the arg + * @param $default Mixed: the default if it doesn't exist * @return mixed */ protected function getArg( $argId = 0, $default = null ) { @@ -179,10 +249,24 @@ abstract class Maintenance { /** * Set the batch size. - * @param $s int The number of operations to do in a batch + * @param $s Integer: the number of operations to do in a batch */ protected function setBatchSize( $s = 0 ) { $this->mBatchSize = $s; + + // If we support $mBatchSize, show the option. + // Used to be in addDefaultParams, but in order for that to + // work, subclasses would have to call this function in the constructor + // before they called parent::__construct which is just weird + // (and really wasn't done). + if ( $this->mBatchSize ) { + $this->addOption( 'batch-size', 'Run this many operations ' . + 'per batch, default: ' . $this->mBatchSize, false, true ); + if ( isset( $this->mParams['batch-size'] ) ) { + // This seems a little ugly... + $this->mDependantParameters['batch-size'] = $this->mParams['batch-size']; + } + } } /** @@ -195,104 +279,146 @@ abstract class Maintenance { /** * Return input from stdin. - * @param $length int The number of bytes to read. If null, + * @param $len Integer: the number of bytes to read. If null, * just return the handle. Maintenance::STDIN_ALL returns * the full length - * @return mixed + * @return Mixed */ protected function getStdin( $len = null ) { - if ( $len == Maintenance::STDIN_ALL ) + if ( $len == Maintenance::STDIN_ALL ) { return file_get_contents( 'php://stdin' ); + } $f = fopen( 'php://stdin', 'rt' ); - if( !$len ) + if ( !$len ) { return $f; + } $input = fgets( $f, $len ); fclose( $f ); return rtrim( $input ); } + /** + * @return bool + */ + public function isQuiet() { + return $this->mQuiet; + } + /** * Throw some output to the user. Scripts can call this with no fears, * as we handle all --quiet stuff here - * @param $out String The text to show to the user - * @param $channel Mixed Unique identifier for the channel. See function outputChanneled. + * @param $out String: the text to show to the user + * @param $channel Mixed: unique identifier for the channel. See + * function outputChanneled. */ protected function output( $out, $channel = null ) { - if( $this->mQuiet ) { + if ( $this->mQuiet ) { return; } - $out = preg_replace( '/\n\z/', '', $out ); - $this->outputChanneled( $out, $channel ); + if ( $channel === null ) { + $this->cleanupChanneled(); + if( php_sapi_name() == 'cli' ) { + fwrite( STDOUT, $out ); + } else { + print( $out ); + } + } else { + $out = preg_replace( '/\n\z/', '', $out ); + $this->outputChanneled( $out, $channel ); + } } /** * Throw an error to the user. Doesn't respect --quiet, so don't use * this for non-error output - * @param $err String The error to display - * @param $die boolean If true, go ahead and die out. + * @param $err String: the error to display + * @param $die Int: if > 0, go ahead and die out using this int as the code */ - protected function error( $err, $die = false ) { + protected function error( $err, $die = 0 ) { $this->outputChanneled( false ); if ( php_sapi_name() == 'cli' ) { fwrite( STDERR, $err . "\n" ); } else { - $f = fopen( 'php://stderr', 'w' ); - fwrite( $f, $err . "\n" ); - fclose( $f ); + print $err; + } + $die = intval( $die ); + if ( $die > 0 ) { + die( $die ); } - if( $die ) die(); } private $atLineStart = true; private $lastChannel = null; - + + /** + * Clean up channeled output. Output a newline if necessary. + */ + public function cleanupChanneled() { + if ( !$this->atLineStart ) { + if( php_sapi_name() == 'cli' ) { + fwrite( STDOUT, "\n" ); + } else { + print "\n"; + } + $this->atLineStart = true; + } + } + /** * Message outputter with channeled message support. Messages on the * same channel are concatenated, but any intervening messages in another * channel start a new line. - * @param $msg String The message without trailing newline - * @param $channel Channel identifier or null for no channel. Channel comparison uses ===. + * @param $msg String: the message without trailing newline + * @param $channel string Channel identifier or null for no + * channel. Channel comparison uses ===. */ public function outputChanneled( $msg, $channel = null ) { - $handle = fopen( 'php://stdout', 'w' ); - if ( $msg === false ) { - // For cleanup - if ( !$this->atLineStart ) fwrite( $handle, "\n" ); - fclose( $handle ); + $this->cleanupChanneled(); return; } + $cli = php_sapi_name() == 'cli'; + // End the current line if necessary if ( !$this->atLineStart && $channel !== $this->lastChannel ) { - fwrite( $handle, "\n" ); + if( $cli ) { + fwrite( STDOUT, "\n" ); + } else { + print "\n"; + } } - fwrite( $handle, $msg ); + if( $cli ) { + fwrite( STDOUT, $msg ); + } else { + print $msg; + } $this->atLineStart = false; if ( $channel === null ) { // For unchanneled messages, output trailing newline immediately - fwrite( $handle, "\n" ); + if( $cli ) { + fwrite( STDOUT, "\n" ); + } else { + print "\n"; + } $this->atLineStart = true; } $this->lastChannel = $channel; - - // Cleanup handle - fclose( $handle ); } /** * Does the script need different DB access? By default, we give Maintenance * scripts normal rights to the DB. Sometimes, a script needs admin rights - * access for a reason and sometimes they want no access. Subclasses should + * access for a reason and sometimes they want no access. Subclasses should * override and return one of the following values, as needed: * Maintenance::DB_NONE - For no DB access at all * Maintenance::DB_STD - For normal DB access, default * Maintenance::DB_ADMIN - For admin DB access - * @return int + * @return Integer */ - protected function getDbType() { + public function getDbType() { return Maintenance::DB_STD; } @@ -300,79 +426,85 @@ abstract class Maintenance { * Add the default parameters to the scripts */ protected function addDefaultParams() { - $this->addOption( 'help', "Display this help message" ); - $this->addOption( 'quiet', "Whether to supress non-error output" ); - $this->addOption( 'conf', "Location of LocalSettings.php, if not default", false, true ); - $this->addOption( 'wiki', "For specifying the wiki ID", false, true ); - $this->addOption( 'globals', "Output globals at the end of processing for debugging" ); + + # Generic (non script dependant) options: + + $this->addOption( 'help', 'Display this help message', false, false, 'h' ); + $this->addOption( 'quiet', 'Whether to supress non-error output', false, false, 'q' ); + $this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true ); + $this->addOption( 'wiki', 'For specifying the wiki ID', false, true ); + $this->addOption( 'globals', 'Output globals at the end of processing for debugging' ); + $this->addOption( 'memory-limit', 'Set a specific memory limit for the script, "max" for no limit or "default" to avoid changing it' ); + $this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " . + "http://en.wikipedia.org. This is sometimes necessary because " . + "server name detection may fail in command line scripts.", false, true ); + + # Save generic options to display them separately in help + $this->mGenericParameters = $this->mParams ; + + # Script dependant options: + // If we support a DB, show the options - if( $this->getDbType() > 0 ) { - $this->addOption( 'dbuser', "The DB user to use for this script", false, true ); - $this->addOption( 'dbpass', "The password to use for this script", false, true ); - } - // If we support $mBatchSize, show the option - if( $this->mBatchSize ) { - $this->addOption( 'batch-size', 'Run this many operations ' . - 'per batch, default: ' . $this->mBatchSize , false, true ); + if ( $this->getDbType() > 0 ) { + $this->addOption( 'dbuser', 'The DB user to use for this script', false, true ); + $this->addOption( 'dbpass', 'The password to use for this script', false, true ); } + + # Save additional script dependant options to display + # them separately in help + $this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters ); } /** * Run a child maintenance script. Pass all of the current arguments * to it. - * @param $maintClass String A name of a child maintenance class - * @param $classFile String Full path of where the child is + * @param $maintClass String: a name of a child maintenance class + * @param $classFile String: full path of where the child is * @return Maintenance child */ - protected function runChild( $maintClass, $classFile = null ) { - // If we haven't already specified, kill setup procedures - // for child scripts, we've already got a sane environment - self::disableSetup(); - + public function runChild( $maintClass, $classFile = null ) { // Make sure the class is loaded first - if( !class_exists( $maintClass ) ) { - if( $classFile ) { + if ( !MWInit::classExists( $maintClass ) ) { + if ( $classFile ) { require_once( $classFile ); } - if( !class_exists( $maintClass ) ) { + if ( !MWInit::classExists( $maintClass ) ) { $this->error( "Cannot spawn child: $maintClass" ); } } + /** + * @var $child Maintenance + */ $child = new $maintClass(); $child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs ); + if ( !is_null( $this->mDb ) ) { + $child->setDB( $this->mDb ); + } return $child; } - /** - * Disable Setup.php mostly - */ - protected static function disableSetup() { - if( !defined( 'MW_NO_SETUP' ) ) - define( 'MW_NO_SETUP', true ); - } - /** * Do some sanity checking and basic setup */ public function setup() { - global $IP, $wgCommandLineMode, $wgRequestTime; + global $wgCommandLineMode, $wgRequestTime; # Abort if called from a web server - if ( isset( $_SERVER ) && array_key_exists( 'REQUEST_METHOD', $_SERVER ) ) { - $this->error( "This script must be run from the command line", true ); + if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) { + $this->error( 'This script must be run from the command line', true ); } # Make sure we can handle script parameters - if( !ini_get( 'register_argc_argv' ) ) { - $this->error( "Cannot get command line arguments, register_argc_argv is set to false", true ); + if ( !function_exists( 'hphp_thread_set_warmup_enabled' ) && !ini_get( 'register_argc_argv' ) ) { + $this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true ); } - if( version_compare( phpversion(), '5.2.4' ) >= 0 ) { + if ( version_compare( phpversion(), '5.2.4' ) >= 0 ) { // Send PHP warnings and errors to stderr instead of stdout. // This aids in diagnosing problems, while keeping messages // out of redirected output. - if( ini_get( 'display_errors' ) ) { + if ( ini_get( 'display_errors' ) ) { ini_set( 'display_errors', 'stderr' ); } @@ -383,9 +515,12 @@ abstract class Maintenance { // command-line mode is on, regardless of PHP version. } + $this->loadParamsAndArgs(); + $this->maybeHelp(); + # Set the memory limit # Note we need to set it again later in cache LocalSettings changed it - ini_set( 'memory_limit', $this->memoryLimit() ); + $this->adjustMemoryLimit(); # Set max execution time to 0 (no limit). PHP.net says that # "When running PHP from the command line the default setting is 0." @@ -397,27 +532,39 @@ abstract class Maintenance { # Define us as being in MediaWiki define( 'MEDIAWIKI', true ); - # Setup $IP, using MW_INSTALL_PATH if it exists - $IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== '' - ? getenv( 'MW_INSTALL_PATH' ) - : realpath( dirname( __FILE__ ) . '/..' ); - $wgCommandLineMode = true; # Turn off output buffering if it's on @ob_end_flush(); - $this->loadParamsAndArgs(); - $this->maybeHelp(); $this->validateParamsAndArgs(); } /** * Normally we disable the memory_limit when running admin scripts. * Some scripts may wish to actually set a limit, however, to avoid - * blowing up unexpectedly. + * blowing up unexpectedly. We also support a --memory-limit option, + * to allow sysadmins to explicitly set one if they'd prefer to override + * defaults (or for people using Suhosin which yells at you for trying + * to disable the limits) + * @return string */ public function memoryLimit() { - return -1; + $limit = $this->getOption( 'memory-limit', 'max' ); + $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood + return $limit; + } + + /** + * Adjusts PHP's memory limit to better suit our needs, if needed. + */ + protected function adjustMemoryLimit() { + $limit = $this->memoryLimit(); + if ( $limit == 'max' ) { + $limit = -1; // no memory limit + } + if ( $limit != 'default' ) { + ini_set( 'memory_limit', $limit ); + } } /** @@ -440,15 +587,15 @@ abstract class Maintenance { */ public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) { # If we were given opts or args, set those and return early - if( $self ) { + if ( $self ) { $this->mSelf = $self; $this->mInputLoaded = true; } - if( $opts ) { + if ( $opts ) { $this->mOptions = $opts; $this->mInputLoaded = true; } - if( $args ) { + if ( $args ) { $this->mArgs = $args; $this->mInputLoaded = true; } @@ -456,7 +603,7 @@ abstract class Maintenance { # If we've already loaded input (either by user values or from $argv) # skip on loading it again. The array_shift() will corrupt values if # it's run again and again - if( $this->mInputLoaded ) { + if ( $this->mInputLoaded ) { $this->loadSpecialVars(); return; } @@ -468,11 +615,11 @@ abstract class Maintenance { $args = array(); # Parse arguments - for( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) { + for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) { if ( $arg == '--' ) { # End of options, remainder should be considered arguments $arg = next( $argv ); - while( $arg !== false ) { + while ( $arg !== false ) { $args[] = $arg; $arg = next( $argv ); } @@ -480,16 +627,20 @@ abstract class Maintenance { } elseif ( substr( $arg, 0, 2 ) == '--' ) { # Long options $option = substr( $arg, 2 ); + if ( array_key_exists( $option, $options ) ) { + $this->error( "\nERROR: $option parameter given twice\n" ); + $this->maybeHelp( true ); + } if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) { $param = next( $argv ); if ( $param === false ) { - $this->error( "\nERROR: $option needs a value after it\n" ); + $this->error( "\nERROR: $option parameter needs a value after it\n" ); $this->maybeHelp( true ); } $options[$option] = $param; } else { $bits = explode( '=', $option, 2 ); - if( count( $bits ) > 1 ) { + if ( count( $bits ) > 1 ) { $option = $bits[0]; $param = $bits[1]; } else { @@ -499,12 +650,19 @@ abstract class Maintenance { } } elseif ( substr( $arg, 0, 1 ) == '-' ) { # Short options - for ( $p=1; $pmParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) { + $option = $this->mShortParamsMap[$option]; + } + if ( array_key_exists( $option, $options ) ) { + $this->error( "\nERROR: $option parameter given twice\n" ); + $this->maybeHelp( true ); + } if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) { $param = next( $argv ); if ( $param === false ) { - $this->error( "\nERROR: $option needs a value after it\n" ); + $this->error( "\nERROR: $option parameter needs a value after it\n" ); $this->maybeHelp( true ); } $options[$option] = $param; @@ -529,35 +687,41 @@ abstract class Maintenance { protected function validateParamsAndArgs() { $die = false; # Check to make sure we've got all the required options - foreach( $this->mParams as $opt => $info ) { - if( $info['require'] && !$this->hasOption( $opt ) ) { + foreach ( $this->mParams as $opt => $info ) { + if ( $info['require'] && !$this->hasOption( $opt ) ) { $this->error( "Param $opt required!" ); $die = true; } } # Check arg list too - foreach( $this->mArgList as $k => $info ) { - if( $info['require'] && !$this->hasArg($k) ) { - $this->error( "Argument <" . $info['name'] . "> required!" ); + foreach ( $this->mArgList as $k => $info ) { + if ( $info['require'] && !$this->hasArg( $k ) ) { + $this->error( 'Argument <' . $info['name'] . '> required!' ); $die = true; } } - - if( $die ) $this->maybeHelp( true ); + + if ( $die ) { + $this->maybeHelp( true ); + } } /** * Handle the special variables that are global to all scripts */ protected function loadSpecialVars() { - if( $this->hasOption( 'dbuser' ) ) + if ( $this->hasOption( 'dbuser' ) ) { $this->mDbUser = $this->getOption( 'dbuser' ); - if( $this->hasOption( 'dbpass' ) ) + } + if ( $this->hasOption( 'dbpass' ) ) { $this->mDbPass = $this->getOption( 'dbpass' ); - if( $this->hasOption( 'quiet' ) ) + } + if ( $this->hasOption( 'quiet' ) ) { $this->mQuiet = true; - if( $this->hasOption( 'batch-size' ) ) - $this->mBatchSize = $this->getOption( 'batch-size' ); + } + if ( $this->hasOption( 'batch-size' ) ) { + $this->mBatchSize = intval( $this->getOption( 'batch-size' ) ); + } } /** @@ -565,142 +729,188 @@ abstract class Maintenance { * @param $force boolean Whether to force the help to show, default false */ protected function maybeHelp( $force = false ) { + if( !$force && !$this->hasOption( 'help' ) ) { + return; + } + + $screenWidth = 80; // TODO: Caculate this! + $tab = " "; + $descWidth = $screenWidth - ( 2 * strlen( $tab ) ); + ksort( $this->mParams ); - if( $this->hasOption( 'help' ) || $force ) { - $this->mQuiet = false; + $this->mQuiet = false; - if( $this->mDescription ) { - $this->output( "\n" . $this->mDescription . "\n" ); + // Description ... + if ( $this->mDescription ) { + $this->output( "\n" . $this->mDescription . "\n" ); + } + $output = "\nUsage: php " . basename( $this->mSelf ); + + // ... append parameters ... + if ( $this->mParams ) { + $output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]"; + } + + // ... and append arguments. + if ( $this->mArgList ) { + $output .= ' '; + foreach ( $this->mArgList as $k => $arg ) { + if ( $arg['require'] ) { + $output .= '<' . $arg['name'] . '>'; + } else { + $output .= '[' . $arg['name'] . ']'; + } + if ( $k < count( $this->mArgList ) - 1 ) + $output .= ' '; } - $output = "\nUsage: php " . $this->mSelf; - if( $this->mParams ) { - $output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]"; + } + $this->output( "$output\n\n" ); + + # TODO abstract some repetitive code below + + // Generic parameters + $this->output( "Generic maintenance parameters:\n" ); + foreach ( $this->mGenericParameters as $par => $info ) { + if ( $info['shortName'] !== false ) { + $par .= " (-{$info['shortName']})"; } - if( $this->mArgList ) { - $output .= " <"; - foreach( $this->mArgList as $k => $arg ) { - $output .= $arg['name'] . ">"; - if( $k < count( $this->mArgList ) - 1 ) - $output .= " <"; + $this->output( + wordwrap( "$tab--$par: " . $info['desc'], $descWidth, + "\n$tab$tab" ) . "\n" + ); + } + $this->output( "\n" ); + + $scriptDependantParams = $this->mDependantParameters; + if( count($scriptDependantParams) > 0 ) { + $this->output( "Script dependant parameters:\n" ); + // Parameters description + foreach ( $scriptDependantParams as $par => $info ) { + if ( $info['shortName'] !== false ) { + $par .= " (-{$info['shortName']})"; } + $this->output( + wordwrap( "$tab--$par: " . $info['desc'], $descWidth, + "\n$tab$tab" ) . "\n" + ); } - $this->output( "$output\n" ); - foreach( $this->mParams as $par => $info ) { - $this->output( "\t$par : " . $info['desc'] . "\n" ); + $this->output( "\n" ); + } + + + // Script specific parameters not defined on construction by + // Maintenance::addDefaultParams() + $scriptSpecificParams = array_diff_key( + # all script parameters: + $this->mParams, + # remove the Maintenance default parameters: + $this->mGenericParameters, + $this->mDependantParameters + ); + if( count($scriptSpecificParams) > 0 ) { + $this->output( "Script specific parameters:\n" ); + // Parameters description + foreach ( $scriptSpecificParams as $par => $info ) { + if ( $info['shortName'] !== false ) { + $par .= " (-{$info['shortName']})"; + } + $this->output( + wordwrap( "$tab--$par: " . $info['desc'], $descWidth, + "\n$tab$tab" ) . "\n" + ); } - foreach( $this->mArgList as $info ) { - $this->output( "\t<" . $info['name'] . "> : " . $info['desc'] . "\n" ); + $this->output( "\n" ); + } + + // Print arguments + if( count( $this->mArgList ) > 0 ) { + $this->output( "Arguments:\n" ); + // Arguments description + foreach ( $this->mArgList as $info ) { + $openChar = $info['require'] ? '<' : '['; + $closeChar = $info['require'] ? '>' : ']'; + $this->output( + wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " . + $info['desc'], $descWidth, "\n$tab$tab" ) . "\n" + ); } - die( 1 ); + $this->output( "\n" ); } + + die( 1 ); } /** * Handle some last-minute setup here. */ public function finalSetup() { - global $wgCommandLineMode, $wgShowSQLErrors; - global $wgTitle, $wgProfiling, $IP, $wgDBadminuser, $wgDBadminpassword; + global $wgCommandLineMode, $wgShowSQLErrors, $wgServer; + global $wgDBadminuser, $wgDBadminpassword; global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf; # Turn off output buffering again, it might have been turned on in the settings files - if( ob_get_level() ) { + if ( ob_get_level() ) { ob_end_flush(); } # Same with these $wgCommandLineMode = true; + # Override $wgServer + if( $this->hasOption( 'server') ) { + $wgServer = $this->getOption( 'server', $wgServer ); + } + # If these were passed, use them - if( $this->mDbUser ) + if ( $this->mDbUser ) { $wgDBadminuser = $this->mDbUser; - if( $this->mDbPass ) + } + if ( $this->mDbPass ) { $wgDBadminpassword = $this->mDbPass; + } if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) { $wgDBuser = $wgDBadminuser; $wgDBpassword = $wgDBadminpassword; - if( $wgDBservers ) { + if ( $wgDBservers ) { + /** + * @var $wgDBservers array + */ foreach ( $wgDBservers as $i => $server ) { $wgDBservers[$i]['user'] = $wgDBuser; $wgDBservers[$i]['password'] = $wgDBpassword; } } - if( isset( $wgLBFactoryConf['serverTemplate'] ) ) { + if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) { $wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser; $wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword; } + LBFactory::destroyInstance(); } - if ( defined( 'MW_CMDLINE_CALLBACK' ) ) { - $fn = MW_CMDLINE_CALLBACK; - $fn(); - } + $this->afterFinalSetup(); $wgShowSQLErrors = true; @set_time_limit( 0 ); - ini_set( 'memory_limit', $this->memoryLimit() ); - - $wgProfiling = false; // only for Profiler.php mode; avoids OOM errors + $this->adjustMemoryLimit(); } /** - * Potentially debug globals. Originally a feature only - * for refreshLinks + * Execute a callback function at the end of initialisation */ - public function globals() { - if( $this->hasOption( 'globals' ) ) { - print_r( $GLOBALS ); + protected function afterFinalSetup() { + if ( defined( 'MW_CMDLINE_CALLBACK' ) ) { + call_user_func( MW_CMDLINE_CALLBACK ); } } /** - * Do setup specific to WMF + * Potentially debug globals. Originally a feature only + * for refreshLinks */ - public function loadWikimediaSettings() { - global $IP, $wgNoDBParam, $wgUseNormalUser, $wgConf, $site, $lang; - - if ( empty( $wgNoDBParam ) ) { - # Check if we were passed a db name - if ( isset( $this->mOptions['wiki'] ) ) { - $db = $this->mOptions['wiki']; - } else { - $db = array_shift( $this->mArgs ); - } - list( $site, $lang ) = $wgConf->siteFromDB( $db ); - - # If not, work out the language and site the old way - if ( is_null( $site ) || is_null( $lang ) ) { - if ( !$db ) { - $lang = 'aa'; - } else { - $lang = $db; - } - if ( isset( $this->mArgs[0] ) ) { - $site = array_shift( $this->mArgs ); - } else { - $site = 'wikipedia'; - } - } - } else { - $lang = 'aa'; - $site = 'wikipedia'; - } - - # This is for the IRC scripts, which now run as the apache user - # The apache user doesn't have access to the wikiadmin_pass command - if ( $_ENV['USER'] == 'apache' ) { - #if ( posix_geteuid() == 48 ) { - $wgUseNormalUser = true; - } - - putenv( 'wikilang=' . $lang ); - - $DP = $IP; - ini_set( 'include_path', ".:$IP:$IP/includes:$IP/languages:$IP/maintenance" ); - - if ( $lang == 'test' && $site == 'wikipedia' ) { - define( 'TESTWIKI', 1 ); + public function globals() { + if ( $this->hasOption( 'globals' ) ) { + print_r( $GLOBALS ); } } @@ -709,11 +919,12 @@ abstract class Maintenance { * @return String */ public function loadSettings() { - global $wgWikiFarm, $wgCommandLineMode, $IP, $DP; + global $wgCommandLineMode, $IP; - $wgWikiFarm = false; if ( isset( $this->mOptions['conf'] ) ) { $settingsFile = $this->mOptions['conf']; + } elseif ( defined("MW_CONFIG_FILE") ) { + $settingsFile = MW_CONFIG_FILE; } else { $settingsFile = "$IP/LocalSettings.php"; } @@ -728,49 +939,49 @@ abstract class Maintenance { if ( !is_readable( $settingsFile ) ) { $this->error( "A copy of your installation's LocalSettings.php\n" . - "must exist and be readable in the source directory.", true ); + "must exist and be readable in the source directory.\n" . + "Use --conf to specify it." , true ); } $wgCommandLineMode = true; - $DP = $IP; return $settingsFile; } /** * Support function for cleaning up redundant text records - * @param $delete boolean Whether or not to actually delete the records + * @param $delete Boolean: whether or not to actually delete the records * @author Rob Church */ - protected function purgeRedundantText( $delete = true ) { + public function purgeRedundantText( $delete = true ) { # Data should come off the master, wrapped in a transaction - $dbw = wfGetDB( DB_MASTER ); - $dbw->begin(); + $dbw = $this->getDB( DB_MASTER ); + $dbw->begin( __METHOD__ ); $tbl_arc = $dbw->tableName( 'archive' ); $tbl_rev = $dbw->tableName( 'revision' ); $tbl_txt = $dbw->tableName( 'text' ); # Get "active" text records from the revisions table - $this->output( "Searching for active text records in revisions table..." ); + $this->output( 'Searching for active text records in revisions table...' ); $res = $dbw->query( "SELECT DISTINCT rev_text_id FROM $tbl_rev" ); - foreach( $res as $row ) { + foreach ( $res as $row ) { $cur[] = $row->rev_text_id; } $this->output( "done.\n" ); # Get "active" text records from the archive table - $this->output( "Searching for active text records in archive table..." ); + $this->output( 'Searching for active text records in archive table...' ); $res = $dbw->query( "SELECT DISTINCT ar_text_id FROM $tbl_arc" ); - foreach( $res as $row ) { + foreach ( $res as $row ) { $cur[] = $row->ar_text_id; } $this->output( "done.\n" ); # Get the IDs of all text records not in these sets - $this->output( "Searching for inactive text records..." ); + $this->output( 'Searching for inactive text records...' ); $set = implode( ', ', $cur ); $res = $dbw->query( "SELECT old_id FROM $tbl_txt WHERE old_id NOT IN ( $set )" ); $old = array(); - foreach( $res as $row ) { + foreach ( $res as $row ) { $old[] = $row->old_id; } $this->output( "done.\n" ); @@ -780,19 +991,20 @@ abstract class Maintenance { $this->output( "$count inactive items found.\n" ); # Delete as appropriate - if( $delete && $count ) { - $this->output( "Deleting..." ); + if ( $delete && $count ) { + $this->output( 'Deleting...' ); $set = implode( ', ', $old ); $dbw->query( "DELETE FROM $tbl_txt WHERE old_id IN ( $set )" ); $this->output( "done.\n" ); } # Done - $dbw->commit(); + $dbw->commit( __METHOD__ ); } /** * Get the maintenance directory. + * @return string */ protected function getDir() { return dirname( __FILE__ ); @@ -802,7 +1014,7 @@ abstract class Maintenance { * Get the list of available maintenance scripts. Note * that if you call this _before_ calling doMaintenance * you won't have any extensions in it yet - * @return array + * @return Array */ public static function getMaintenanceScripts() { global $wgMaintenanceScripts; @@ -814,28 +1026,27 @@ abstract class Maintenance { * @return array */ protected static function getCoreScripts() { - if( !self::$mCoreScripts ) { - self::disableSetup(); + if ( !self::$mCoreScripts ) { $paths = array( dirname( __FILE__ ), - dirname( __FILE__ ) . '/gearman', dirname( __FILE__ ) . '/language', dirname( __FILE__ ) . '/storage', ); self::$mCoreScripts = array(); - foreach( $paths as $p ) { + foreach ( $paths as $p ) { $handle = opendir( $p ); - while( ( $file = readdir( $handle ) ) !== false ) { - if( $file == 'Maintenance.php' ) + while ( ( $file = readdir( $handle ) ) !== false ) { + if ( $file == 'Maintenance.php' ) { continue; + } $file = $p . '/' . $file; - if( is_dir( $file ) || !strpos( $file, '.php' ) || + if ( is_dir( $file ) || !strpos( $file, '.php' ) || ( strpos( file_get_contents( $file ), '$maintClass' ) === false ) ) { continue; } require( $file ); $vars = get_defined_vars(); - if( array_key_exists( 'maintClass', $vars ) ) { + if ( array_key_exists( 'maintClass', $vars ) ) { self::$mCoreScripts[$vars['maintClass']] = $file; } } @@ -844,4 +1055,274 @@ abstract class Maintenance { } return self::$mCoreScripts; } + + /** + * Returns a database to be used by current maintenance script. It can be set by setDB(). + * If not set, wfGetDB() will be used. + * This function has the same parameters as wfGetDB() + * + * @return DatabaseBase + */ + protected function &getDB( $db, $groups = array(), $wiki = false ) { + if ( is_null( $this->mDb ) ) { + return wfGetDB( $db, $groups, $wiki ); + } else { + return $this->mDb; + } + } + + /** + * Sets database object to be returned by getDB(). + * + * @param $db DatabaseBase: Database object to be used + */ + public function setDB( &$db ) { + $this->mDb = $db; + } + + /** + * Lock the search index + * @param &$db DatabaseBase object + */ + private function lockSearchindex( &$db ) { + $write = array( 'searchindex' ); + $read = array( 'page', 'revision', 'text', 'interwiki', 'l10n_cache' ); + $db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ ); + } + + /** + * Unlock the tables + * @param &$db DatabaseBase object + */ + private function unlockSearchindex( &$db ) { + $db->unlockTables( __CLASS__ . '::' . __METHOD__ ); + } + + /** + * Unlock and lock again + * Since the lock is low-priority, queued reads will be able to complete + * @param &$db DatabaseBase object + */ + private function relockSearchindex( &$db ) { + $this->unlockSearchindex( $db ); + $this->lockSearchindex( $db ); + } + + /** + * Perform a search index update with locking + * @param $maxLockTime Integer: the maximum time to keep the search index locked. + * @param $callback callback String: the function that will update the function. + * @param $dbw DatabaseBase object + * @param $results + */ + public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) { + $lockTime = time(); + + # Lock searchindex + if ( $maxLockTime ) { + $this->output( " --- Waiting for lock ---" ); + $this->lockSearchindex( $dbw ); + $lockTime = time(); + $this->output( "\n" ); + } + + # Loop through the results and do a search update + foreach ( $results as $row ) { + # Allow reads to be processed + if ( $maxLockTime && time() > $lockTime + $maxLockTime ) { + $this->output( " --- Relocking ---" ); + $this->relockSearchindex( $dbw ); + $lockTime = time(); + $this->output( "\n" ); + } + call_user_func( $callback, $dbw, $row ); + } + + # Unlock searchindex + if ( $maxLockTime ) { + $this->output( " --- Unlocking --" ); + $this->unlockSearchindex( $dbw ); + $this->output( "\n" ); + } + + } + + /** + * Update the searchindex table for a given pageid + * @param $dbw DatabaseBase a database write handle + * @param $pageId Integer: the page ID to update. + * @return null|string + */ + public function updateSearchIndexForPage( $dbw, $pageId ) { + // Get current revision + $rev = Revision::loadFromPageId( $dbw, $pageId ); + $title = null; + if ( $rev ) { + $titleObj = $rev->getTitle(); + $title = $titleObj->getPrefixedDBkey(); + $this->output( "$title..." ); + # Update searchindex + $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getText() ); + $u->doUpdate(); + $this->output( "\n" ); + } + return $title; + } + + /** + * Wrapper for posix_isatty() + * We default as considering stdin a tty (for nice readline methods) + * but treating stout as not a tty to avoid color codes + * + * @param $fd int File descriptor + * @return bool + */ + public static function posix_isatty( $fd ) { + if ( !MWInit::functionExists( 'posix_isatty' ) ) { + return !$fd; + } else { + return posix_isatty( $fd ); + } + } + + /** + * Prompt the console for input + * @param $prompt String what to begin the line with, like '> ' + * @return String response + */ + public static function readconsole( $prompt = '> ' ) { + static $isatty = null; + if ( is_null( $isatty ) ) { + $isatty = self::posix_isatty( 0 /*STDIN*/ ); + } + + if ( $isatty && function_exists( 'readline' ) ) { + return readline( $prompt ); + } else { + if ( $isatty ) { + $st = self::readlineEmulation( $prompt ); + } else { + if ( feof( STDIN ) ) { + $st = false; + } else { + $st = fgets( STDIN, 1024 ); + } + } + if ( $st === false ) return false; + $resp = trim( $st ); + return $resp; + } + } + + /** + * Emulate readline() + * @param $prompt String what to begin the line with, like '> ' + * @return String + */ + private static function readlineEmulation( $prompt ) { + $bash = Installer::locateExecutableInDefaultPaths( array( 'bash' ) ); + if ( !wfIsWindows() && $bash ) { + $retval = false; + $encPrompt = wfEscapeShellArg( $prompt ); + $command = "read -er -p $encPrompt && echo \"\$REPLY\""; + $encCommand = wfEscapeShellArg( $command ); + $line = wfShellExec( "$bash -c $encCommand", $retval ); + + if ( $retval == 0 ) { + return $line; + } elseif ( $retval == 127 ) { + // Couldn't execute bash even though we thought we saw it. + // Shell probably spit out an error message, sorry :( + // Fall through to fgets()... + } else { + // EOF/ctrl+D + return false; + } + } + + // Fallback... we'll have no editing controls, EWWW + if ( feof( STDIN ) ) { + return false; + } + print $prompt; + return fgets( STDIN, 1024 ); + } +} + +/** + * Fake maintenance wrapper, mostly used for the web installer/updater + */ +class FakeMaintenance extends Maintenance { + protected $mSelf = "FakeMaintenanceScript"; + public function execute() { + return; + } +} + +/** + * Class for scripts that perform database maintenance and want to log the + * update in `updatelog` so we can later skip it + */ +abstract class LoggedUpdateMaintenance extends Maintenance { + public function __construct() { + parent::__construct(); + $this->addOption( 'force', 'Run the update even if it was completed already' ); + $this->setBatchSize( 200 ); + } + + public function execute() { + $db = $this->getDB( DB_MASTER ); + $key = $this->getUpdateKey(); + + if ( !$this->hasOption( 'force' ) && + $db->selectRow( 'updatelog', '1', array( 'ul_key' => $key ), __METHOD__ ) ) + { + $this->output( "..." . $this->updateSkippedMessage() . "\n" ); + return true; + } + + if ( !$this->doDBUpdates() ) { + return false; + } + + if ( + $db->insert( 'updatelog', array( 'ul_key' => $key ), __METHOD__, 'IGNORE' ) ) + { + return true; + } else { + $this->output( $this->updatelogFailedMessage() . "\n" ); + return false; + } + } + + /** + * Message to show that the update was done already and was just skipped + * @return String + */ + protected function updateSkippedMessage() { + $key = $this->getUpdateKey(); + return "Update '{$key}' already logged as completed."; + } + + /** + * Message to show the the update log was unable to log the completion of this update + * @return String + */ + protected function updatelogFailedMessage() { + $key = $this->getUpdateKey(); + return "Unable to log update '{$key}' as completed."; + } + + /** + * Do the actual work. All child classes will need to implement this. + * Return true to log the update as done or false (usually on failure). + * @return Bool + */ + abstract protected function doDBUpdates(); + + /** + * Get the update key name to go in the update log table + * @return String + */ + abstract protected function getUpdateKey(); }