X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=maintenance%2FMaintenance.php;h=fda12f9e7e6224018d2e6b078ace8b7b0b64032f;hb=5f230a3a1958ee607f2fd3fe73420bbfc84e45c7;hp=d57bfc8b255f66d4a2806e3b36462bbdd8b7ef33;hpb=0f8da9af167a7ca798d5229ce20622d5cb3911ef;p=lhc%2Fweb%2Fwiklou.git diff --git a/maintenance/Maintenance.php b/maintenance/Maintenance.php index d57bfc8b25..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 */ @@ -114,9 +164,13 @@ abstract class Maintenance { * @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; + } } /** @@ -158,6 +212,22 @@ abstract class Maintenance { ); } + /** + * 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 Integer: the integer value (from zero) for the arg @@ -183,6 +253,20 @@ abstract class Maintenance { */ 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']; + } + } } /** @@ -213,6 +297,13 @@ abstract class Maintenance { 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 @@ -226,12 +317,12 @@ abstract class Maintenance { } if ( $channel === null ) { $this->cleanupChanneled(); - - $f = fopen( 'php://stdout', 'w' ); - fwrite( $f, $out ); - fclose( $f ); - } - else { + if( php_sapi_name() == 'cli' ) { + fwrite( STDOUT, $out ); + } else { + print( $out ); + } + } else { $out = preg_replace( '/\n\z/', '', $out ); $this->outputChanneled( $out, $channel ); } @@ -241,19 +332,18 @@ abstract class Maintenance { * 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 $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; } - if ( $die ) { - die(); + $die = intval( $die ); + if ( $die > 0 ) { + die( $die ); } } @@ -265,9 +355,11 @@ abstract class Maintenance { */ public function cleanupChanneled() { if ( !$this->atLineStart ) { - $handle = fopen( 'php://stdout', 'w' ); - fwrite( $handle, "\n" ); - fclose( $handle ); + if( php_sapi_name() == 'cli' ) { + fwrite( STDOUT, "\n" ); + } else { + print "\n"; + } $this->atLineStart = true; } } @@ -277,7 +369,7 @@ abstract class Maintenance { * 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 + * @param $channel string Channel identifier or null for no * channel. Channel comparison uses ===. */ public function outputChanneled( $msg, $channel = null ) { @@ -286,25 +378,34 @@ abstract class Maintenance { return; } - $handle = fopen( 'php://stdout', 'w' ); + $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 ); } /** @@ -325,21 +426,33 @@ 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' ); + + # 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 ); - } + + # Save additional script dependant options to display + # them separately in help + $this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters ); } /** @@ -349,48 +462,41 @@ abstract class Maintenance { * @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 ( !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 ); - return $child; - } - - /** - * Disable Setup.php mostly - */ - protected static function disableSetup() { - if ( !defined( 'MW_NO_SETUP' ) ) { - define( 'MW_NO_SETUP', true ); + if ( !is_null( $this->mDb ) ) { + $child->setDB( $this->mDb ); } + return $child; } /** * 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 ) ) { + 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' ) ) { + 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 ); } @@ -409,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." @@ -423,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 ); + } } /** @@ -506,10 +627,14 @@ 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; @@ -527,10 +652,17 @@ abstract class Maintenance { # Short options for ( $p = 1; $p < strlen( $arg ); $p++ ) { $option = $arg { $p } ; + if ( !isset( $this->mParams[$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; @@ -588,7 +720,7 @@ abstract class Maintenance { $this->mQuiet = true; } if ( $this->hasOption( 'batch-size' ) ) { - $this->mBatchSize = $this->getOption( 'batch-size' ); + $this->mBatchSize = intval( $this->getOption( 'batch-size' ) ); } } @@ -597,52 +729,123 @@ 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; + + // Description ... + if ( $this->mDescription ) { + $this->output( "\n" . $this->mDescription . "\n" ); + } + $output = "\nUsage: php " . basename( $this->mSelf ); - if ( $this->mDescription ) { - $this->output( "\n" . $this->mDescription . "\n" ); + // ... 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 " . basename( $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( "\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, + wordwrap( "$tab--$par: " . $info['desc'], $descWidth, "\n$tab$tab" ) . "\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<" . $info['name'] . "> : " . + 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 $wgProfiling, $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 @@ -652,6 +855,11 @@ abstract class Maintenance { # 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 ) { $wgDBadminuser = $this->mDbUser; @@ -665,6 +873,9 @@ abstract class Maintenance { $wgDBpassword = $wgDBadminpassword; if ( $wgDBservers ) { + /** + * @var $wgDBservers array + */ foreach ( $wgDBservers as $i => $server ) { $wgDBservers[$i]['user'] = $wgDBuser; $wgDBservers[$i]['password'] = $wgDBpassword; @@ -674,18 +885,23 @@ abstract class Maintenance { $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() ); + $this->adjustMemoryLimit(); + } - $wgProfiling = false; // only for Profiler.php mode; avoids OOM errors + /** + * Execute a callback function at the end of initialisation + */ + protected function afterFinalSetup() { + if ( defined( 'MW_CMDLINE_CALLBACK' ) ) { + call_user_func( MW_CMDLINE_CALLBACK ); + } } /** @@ -698,65 +914,17 @@ abstract class Maintenance { } } - /** - * Do setup specific to WMF - */ - 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 ); - - ini_set( 'include_path', ".:$IP:$IP/includes:$IP/languages:$IP/maintenance" ); - - if ( $lang == 'test' && $site == 'wikipedia' ) { - define( 'TESTWIKI', 1 ); - } - } - /** * Generic setup for most installs. Returns the location of LocalSettings * @return String */ public function loadSettings() { - global $wgWikiFarm, $wgCommandLineMode, $IP; + 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"; } @@ -771,7 +939,8 @@ 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; return $settingsFile; @@ -784,8 +953,8 @@ abstract class Maintenance { */ 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' ); @@ -830,11 +999,12 @@ abstract class Maintenance { } # Done - $dbw->commit(); + $dbw->commit( __METHOD__ ); } /** * Get the maintenance directory. + * @return string */ protected function getDir() { return dirname( __FILE__ ); @@ -857,10 +1027,8 @@ abstract class Maintenance { */ protected static function getCoreScripts() { if ( !self::$mCoreScripts ) { - self::disableSetup(); $paths = array( dirname( __FILE__ ), - dirname( __FILE__ ) . '/gearman', dirname( __FILE__ ) . '/language', dirname( __FILE__ ) . '/storage', ); @@ -888,9 +1056,33 @@ 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 Database object + * @param &$db DatabaseBase object */ private function lockSearchindex( &$db ) { $write = array( 'searchindex' ); @@ -900,7 +1092,7 @@ abstract class Maintenance { /** * Unlock the tables - * @param &$db Database object + * @param &$db DatabaseBase object */ private function unlockSearchindex( &$db ) { $db->unlockTables( __CLASS__ . '::' . __METHOD__ ); @@ -909,7 +1101,7 @@ abstract class Maintenance { /** * Unlock and lock again * Since the lock is low-priority, queued reads will be able to complete - * @param &$db Database object + * @param &$db DatabaseBase object */ private function relockSearchindex( &$db ) { $this->unlockSearchindex( $db ); @@ -920,7 +1112,7 @@ abstract class Maintenance { * 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 Database object + * @param $dbw DatabaseBase object * @param $results */ public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) { @@ -957,8 +1149,9 @@ abstract class Maintenance { /** * Update the searchindex table for a given pageid - * @param $dbw Database: a database write handle + * @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 @@ -976,4 +1169,160 @@ abstract class Maintenance { 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(); }