MediaWiki REL1_35
Maintenance.php
Go to the documentation of this file.
1<?php
31
55abstract class Maintenance {
60 public const DB_NONE = 0;
61 public const DB_STD = 1;
62 public const DB_ADMIN = 2;
63
64 // Const for getStdin()
65 public const STDIN_ALL = 'all';
66
72 protected $mParams = [];
73
74 // Array of mapping short parameters to long ones
75 protected $mShortParamsMap = [];
76
77 // Array of desired/allowed args
78 protected $mArgList = [];
79
80 // This is the list of options that were actually passed
81 protected $mOptions = [];
82
83 // This is the list of arguments that were actually passed
84 protected $mArgs = [];
85
86 // Allow arbitrary options to be passed, or only specified ones?
87 protected $mAllowUnregisteredOptions = false;
88
89 // Name of the script currently running
90 protected $mSelf;
91
92 // Special vars for params that are always used
93 protected $mQuiet = false;
94 protected $mDbUser, $mDbPass;
95
96 // A description of the script, children should change this via addDescription()
97 protected $mDescription = '';
98
99 // Have we already loaded our user input?
100 protected $mInputLoaded = false;
101
108 protected $mBatchSize = null;
109
122
127 private $mDb = null;
128
130 private $lastReplicationWait = 0.0;
131
137
140
142 private $hookRunner;
143
149 private $config;
150
156
168 public $orderedOptions = [];
169
176 public function __construct() {
177 $this->addDefaultParams();
178 register_shutdown_function( [ $this, 'outputChanneled' ], false );
179 }
180
188 public static function shouldExecute() {
189 global $wgCommandLineMode;
190
191 if ( !function_exists( 'debug_backtrace' ) ) {
192 // If someone has a better idea...
193 return $wgCommandLineMode;
194 }
195
196 $bt = debug_backtrace();
197 $count = count( $bt );
198 if ( $count < 2 ) {
199 return false; // sanity
200 }
201 if ( $bt[0]['class'] !== self::class || $bt[0]['function'] !== 'shouldExecute' ) {
202 return false; // last call should be to this function
203 }
204 $includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ];
205 for ( $i = 1; $i < $count; $i++ ) {
206 if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) {
207 return false; // previous calls should all be "requires"
208 }
209 }
210
211 return true;
212 }
213
222 abstract public function execute();
223
230 protected function supportsOption( $name ) {
231 return isset( $this->mParams[$name] );
232 }
233
245 protected function addOption( $name, $description, $required = false,
246 $withArg = false, $shortName = false, $multiOccurrence = false
247 ) {
248 $this->mParams[$name] = [
249 'desc' => $description,
250 'require' => $required,
251 'withArg' => $withArg,
252 'shortName' => $shortName,
253 'multiOccurrence' => $multiOccurrence
254 ];
255
256 if ( $shortName !== false ) {
257 $this->mShortParamsMap[$shortName] = $name;
258 }
259 }
260
267 protected function hasOption( $name ) {
268 return isset( $this->mOptions[$name] );
269 }
270
282 protected function getOption( $name, $default = null ) {
283 if ( $this->hasOption( $name ) ) {
284 return $this->mOptions[$name];
285 } else {
286 return $default;
287 }
288 }
289
296 protected function addArg( $arg, $description, $required = true ) {
297 $this->mArgList[] = [
298 'name' => $arg,
299 'desc' => $description,
300 'require' => $required
301 ];
302 }
303
308 protected function deleteOption( $name ) {
309 unset( $this->mParams[$name] );
310 }
311
317 protected function setAllowUnregisteredOptions( $allow ) {
318 $this->mAllowUnregisteredOptions = $allow;
319 }
320
325 protected function addDescription( $text ) {
326 $this->mDescription = $text;
327 }
328
334 protected function hasArg( $argId = 0 ) {
335 if ( func_num_args() === 0 ) {
336 wfDeprecated( __METHOD__ . ' without an $argId', '1.33' );
337 }
338
339 return isset( $this->mArgs[$argId] );
340 }
341
349 protected function getArg( $argId = 0, $default = null ) {
350 if ( func_num_args() === 0 ) {
351 wfDeprecated( __METHOD__ . ' without an $argId', '1.33' );
352 }
353
354 return $this->mArgs[$argId] ?? $default;
355 }
356
364 protected function getBatchSize() {
365 return $this->mBatchSize;
366 }
367
372 protected function setBatchSize( $s = 0 ) {
373 $this->mBatchSize = $s;
374
375 // If we support $mBatchSize, show the option.
376 // Used to be in addDefaultParams, but in order for that to
377 // work, subclasses would have to call this function in the constructor
378 // before they called parent::__construct which is just weird
379 // (and really wasn't done).
380 if ( $this->mBatchSize ) {
381 $this->addOption( 'batch-size', 'Run this many operations ' .
382 'per batch, default: ' . $this->mBatchSize, false, true );
383 if ( isset( $this->mParams['batch-size'] ) ) {
384 // This seems a little ugly...
385 $this->mDependantParameters['batch-size'] = $this->mParams['batch-size'];
386 }
387 }
388 }
389
394 public function getName() {
395 return $this->mSelf;
396 }
397
404 protected function getStdin( $len = null ) {
405 if ( $len == self::STDIN_ALL ) {
406 return file_get_contents( 'php://stdin' );
407 }
408 $f = fopen( 'php://stdin', 'rt' );
409 if ( !$len ) {
410 return $f;
411 }
412 $input = fgets( $f, $len );
413 fclose( $f );
414
415 return rtrim( $input );
416 }
417
421 public function isQuiet() {
422 return $this->mQuiet;
423 }
424
432 protected function output( $out, $channel = null ) {
433 // This is sometimes called very early, before Setup.php is included.
434 if ( class_exists( MediaWikiServices::class ) ) {
435 // Try to periodically flush buffered metrics to avoid OOMs
436 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
437 if ( $stats->getDataCount() > 1000 ) {
438 MediaWiki::emitBufferedStatsdData( $stats, $this->getConfig() );
439 }
440 }
441
442 if ( $this->mQuiet ) {
443 return;
444 }
445 if ( $channel === null ) {
446 $this->cleanupChanneled();
447 print $out;
448 } else {
449 $out = preg_replace( '/\n\z/', '', $out );
450 $this->outputChanneled( $out, $channel );
451 }
452 }
453
461 protected function error( $err, $die = 0 ) {
462 if ( intval( $die ) !== 0 ) {
463 wfDeprecated( __METHOD__ . '( $err, $die )', '1.31' );
464 $this->fatalError( $err, intval( $die ) );
465 }
466 $this->outputChanneled( false );
467 if (
468 ( PHP_SAPI == 'cli' || PHP_SAPI == 'phpdbg' ) &&
469 !defined( 'MW_PHPUNIT_TEST' )
470 ) {
471 fwrite( STDERR, $err . "\n" );
472 } else {
473 print $err;
474 }
475 }
476
485 protected function fatalError( $msg, $exitCode = 1 ) {
486 $this->error( $msg );
487 exit( $exitCode );
488 }
489
490 private $atLineStart = true;
491 private $lastChannel = null;
492
496 public function cleanupChanneled() {
497 if ( !$this->atLineStart ) {
498 print "\n";
499 $this->atLineStart = true;
500 }
501 }
502
511 public function outputChanneled( $msg, $channel = null ) {
512 if ( $msg === false ) {
513 $this->cleanupChanneled();
514
515 return;
516 }
517
518 // End the current line if necessary
519 if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
520 print "\n";
521 }
522
523 print $msg;
524
525 $this->atLineStart = false;
526 if ( $channel === null ) {
527 // For unchanneled messages, output trailing newline immediately
528 print "\n";
529 $this->atLineStart = true;
530 }
531 $this->lastChannel = $channel;
532 }
533
545 public function getDbType() {
546 return self::DB_STD;
547 }
548
552 protected function addDefaultParams() {
553 # Generic (non script dependant) options:
554
555 $this->addOption( 'help', 'Display this help message', false, false, 'h' );
556 $this->addOption( 'quiet', 'Whether to suppress non-error output', false, false, 'q' );
557 $this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true );
558 $this->addOption( 'wiki', 'For specifying the wiki ID', false, true );
559 $this->addOption( 'globals', 'Output globals at the end of processing for debugging' );
560 $this->addOption(
561 'memory-limit',
562 'Set a specific memory limit for the script, '
563 . '"max" for no limit or "default" to avoid changing it',
564 false,
565 true
566 );
567 $this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " .
568 "http://en.wikipedia.org. This is sometimes necessary because " .
569 "server name detection may fail in command line scripts.", false, true );
570 $this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true );
571
572 # Save generic options to display them separately in help
573 $this->mGenericParameters = $this->mParams;
574
575 # Script dependant options:
576
577 // If we support a DB, show the options
578 if ( $this->getDbType() > 0 ) {
579 $this->addOption( 'dbuser', 'The DB user to use for this script', false, true );
580 $this->addOption( 'dbpass', 'The password to use for this script', false, true );
581 $this->addOption( 'dbgroupdefault', 'The default DB group to use.', false, true );
582 }
583
584 # Save additional script dependant options to display
585 #  them separately in help
586 $this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters );
587 }
588
594 public function getConfig() {
595 if ( $this->config === null ) {
596 $this->config = MediaWikiServices::getInstance()->getMainConfig();
597 }
598
599 return $this->config;
600 }
601
606 public function setConfig( Config $config ) {
607 $this->config = $config;
608 }
609
619 protected function requireExtension( $name ) {
620 $this->requiredExtensions[] = $name;
621 }
622
628 public function checkRequiredExtensions() {
629 $registry = ExtensionRegistry::getInstance();
630 $missing = [];
631 foreach ( $this->requiredExtensions as $name ) {
632 if ( !$registry->isLoaded( $name ) ) {
633 $missing[] = $name;
634 }
635 }
636
637 if ( $missing ) {
638 $joined = implode( ', ', $missing );
639 $msg = "The following extensions are required to be installed "
640 . "for this script to run: $joined. Please enable them and then try again.";
641 $this->fatalError( $msg );
642 }
643 }
644
649 public function setAgentAndTriggers() {
650 if ( function_exists( 'posix_getpwuid' ) ) {
651 $agent = posix_getpwuid( posix_geteuid() )['name'];
652 } else {
653 $agent = 'sysadmin';
654 }
655 $agent .= '@' . wfHostname();
656
657 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
658 // Add a comment for easy SHOW PROCESSLIST interpretation
659 $lbFactory->setAgentName(
660 mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent
661 );
662 self::setLBFactoryTriggers( $lbFactory, $this->getConfig() );
663 }
664
670 public static function setLBFactoryTriggers( LBFactory $LBFactory, Config $config ) {
671 $services = MediaWikiServices::getInstance();
672 $stats = $services->getStatsdDataFactory();
673 // Hook into period lag checks which often happen in long-running scripts
674 $lbFactory = $services->getDBLoadBalancerFactory();
675 $lbFactory->setWaitForReplicationListener(
676 __METHOD__,
677 function () use ( $stats, $config ) {
678 // Check config in case of JobRunner and unit tests
679 if ( $config->get( 'CommandLineMode' ) ) {
680 DeferredUpdates::tryOpportunisticExecute( 'run' );
681 }
682 // Try to periodically flush buffered metrics to avoid OOMs
683 MediaWiki::emitBufferedStatsdData( $stats, $config );
684 }
685 );
686 // Check for other windows to run them. A script may read or do a few writes
687 // to the master but mostly be writing to something else, like a file store.
688 $lbFactory->getMainLB()->setTransactionListener(
689 __METHOD__,
690 function ( $trigger ) use ( $stats, $config ) {
691 // Check config in case of JobRunner and unit tests
692 if ( $config->get( 'CommandLineMode' ) && $trigger === IDatabase::TRIGGER_COMMIT ) {
693 DeferredUpdates::tryOpportunisticExecute( 'run' );
694 }
695 // Try to periodically flush buffered metrics to avoid OOMs
696 MediaWiki::emitBufferedStatsdData( $stats, $config );
697 }
698 );
699 }
700
708 public function runChild( $maintClass, $classFile = null ) {
709 // Make sure the class is loaded first
710 if ( !class_exists( $maintClass ) ) {
711 if ( $classFile ) {
712 require_once $classFile;
713 }
714 if ( !class_exists( $maintClass ) ) {
715 $this->error( "Cannot spawn child: $maintClass" );
716 }
717 }
718
722 $child = new $maintClass();
723 $child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
724 if ( $this->mDb !== null ) {
725 $child->setDB( $this->mDb );
726 }
727
728 return $child;
729 }
730
734 public function setup() {
735 global $IP, $wgCommandLineMode;
736
737 # Abort if called from a web server
738 # wfIsCLI() is not available yet
739 if ( PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' ) {
740 $this->fatalError( 'This script must be run from the command line' );
741 }
742
743 if ( $IP === null ) {
744 $this->fatalError( "\$IP not set, aborting!\n" .
745 '(Did you forget to call parent::__construct() in your maintenance script?)' );
746 }
747
748 # Make sure we can handle script parameters
749 if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) {
750 $this->fatalError( 'Cannot get command line arguments, register_argc_argv is set to false' );
751 }
752
753 // Send PHP warnings and errors to stderr instead of stdout.
754 // This aids in diagnosing problems, while keeping messages
755 // out of redirected output.
756 if ( ini_get( 'display_errors' ) ) {
757 ini_set( 'display_errors', 'stderr' );
758 }
759
760 $this->loadParamsAndArgs();
761
762 # Set the memory limit
763 # Note we need to set it again later in cache LocalSettings changed it
764 $this->adjustMemoryLimit();
765
766 # Set max execution time to 0 (no limit). PHP.net says that
767 # "When running PHP from the command line the default setting is 0."
768 # But sometimes this doesn't seem to be the case.
769 ini_set( 'max_execution_time', 0 );
770
771 $wgCommandLineMode = true;
772
773 # Turn off output buffering if it's on
774 while ( ob_get_level() > 0 ) {
775 ob_end_flush();
776 }
777 }
778
789 public function memoryLimit() {
790 $limit = $this->getOption( 'memory-limit', 'max' );
791 $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
792 return $limit;
793 }
794
798 protected function adjustMemoryLimit() {
799 $limit = $this->memoryLimit();
800 if ( $limit == 'max' ) {
801 $limit = -1; // no memory limit
802 }
803 if ( $limit != 'default' ) {
804 ini_set( 'memory_limit', $limit );
805 }
806 }
807
811 protected function activateProfiler() {
813
814 $output = $this->getOption( 'profiler' );
815 if ( !$output ) {
816 return;
817 }
818
819 if ( isset( $wgProfiler['class'] ) ) {
820 $class = $wgProfiler['class'];
822 $profiler = new $class(
823 [ 'sampling' => 1, 'output' => [ $output ] ]
825 + [ 'threshold' => 0.0 ]
826 );
827 $profiler->setAllowOutput();
828 Profiler::replaceStubInstance( $profiler );
829 }
830
831 $trxProfiler = Profiler::instance()->getTransactionProfiler();
832 $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
833 $trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ );
834 }
835
839 public function clearParamsAndArgs() {
840 $this->mOptions = [];
841 $this->mArgs = [];
842 $this->mInputLoaded = false;
843 }
844
852 public function loadWithArgv( $argv ) {
853 $options = [];
854 $args = [];
855 $this->orderedOptions = [];
856
857 # Parse arguments
858 for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
859 if ( $arg == '--' ) {
860 # End of options, remainder should be considered arguments
861 $arg = next( $argv );
862 while ( $arg !== false ) {
863 $args[] = $arg;
864 $arg = next( $argv );
865 }
866 break;
867 } elseif ( substr( $arg, 0, 2 ) == '--' ) {
868 # Long options
869 $option = substr( $arg, 2 );
870 if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
871 $param = next( $argv );
872 if ( $param === false ) {
873 $this->error( "\nERROR: $option parameter needs a value after it\n" );
874 $this->maybeHelp( true );
875 }
876
877 $this->setParam( $options, $option, $param );
878 } else {
879 $bits = explode( '=', $option, 2 );
880 $this->setParam( $options, $bits[0], $bits[1] ?? 1 );
881 }
882 } elseif ( $arg == '-' ) {
883 # Lonely "-", often used to indicate stdin or stdout.
884 $args[] = $arg;
885 } elseif ( substr( $arg, 0, 1 ) == '-' ) {
886 # Short options
887 $argLength = strlen( $arg );
888 for ( $p = 1; $p < $argLength; $p++ ) {
889 $option = $arg[$p];
890 if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
891 $option = $this->mShortParamsMap[$option];
892 }
893
894 if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
895 $param = next( $argv );
896 if ( $param === false ) {
897 $this->error( "\nERROR: $option parameter needs a value after it\n" );
898 $this->maybeHelp( true );
899 }
900 $this->setParam( $options, $option, $param );
901 } else {
902 $this->setParam( $options, $option, 1 );
903 }
904 }
905 } else {
906 $args[] = $arg;
907 }
908 }
909
910 $this->mOptions = $options;
911 $this->mArgs = $args;
912 $this->loadSpecialVars();
913 $this->mInputLoaded = true;
914 }
915
928 private function setParam( &$options, $option, $value ) {
929 $this->orderedOptions[] = [ $option, $value ];
930
931 if ( isset( $this->mParams[$option] ) ) {
932 $multi = $this->mParams[$option]['multiOccurrence'];
933 } else {
934 $multi = false;
935 }
936 $exists = array_key_exists( $option, $options );
937 if ( $multi && $exists ) {
938 $options[$option][] = $value;
939 } elseif ( $multi ) {
940 $options[$option] = [ $value ];
941 } elseif ( !$exists ) {
942 $options[$option] = $value;
943 } else {
944 $this->error( "\nERROR: $option parameter given twice\n" );
945 $this->maybeHelp( true );
946 }
947 }
948
958 public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
959 # If we were given opts or args, set those and return early
960 if ( $self ) {
961 $this->mSelf = $self;
962 $this->mInputLoaded = true;
963 }
964 if ( $opts ) {
965 $this->mOptions = $opts;
966 $this->mInputLoaded = true;
967 }
968 if ( $args ) {
969 $this->mArgs = $args;
970 $this->mInputLoaded = true;
971 }
972
973 # If we've already loaded input (either by user values or from $argv)
974 # skip on loading it again. The array_shift() will corrupt values if
975 # it's run again and again
976 if ( $this->mInputLoaded ) {
977 $this->loadSpecialVars();
978
979 return;
980 }
981
982 global $argv;
983 $this->mSelf = $argv[0];
984 $this->loadWithArgv( array_slice( $argv, 1 ) );
985 }
986
991 public function validateParamsAndArgs() {
992 $die = false;
993 # Check to make sure we've got all the required options
994 foreach ( $this->mParams as $opt => $info ) {
995 if ( $info['require'] && !$this->hasOption( $opt ) ) {
996 $this->error( "Param $opt required!" );
997 $die = true;
998 }
999 }
1000 # Check arg list too
1001 foreach ( $this->mArgList as $k => $info ) {
1002 if ( $info['require'] && !$this->hasArg( $k ) ) {
1003 $this->error( 'Argument <' . $info['name'] . '> required!' );
1004 $die = true;
1005 }
1006 }
1007 if ( !$this->mAllowUnregisteredOptions ) {
1008 # Check for unexpected options
1009 foreach ( $this->mOptions as $opt => $val ) {
1010 if ( !$this->supportsOption( $opt ) ) {
1011 $this->error( "Unexpected option $opt!" );
1012 $die = true;
1013 }
1014 }
1015 }
1016
1017 $this->maybeHelp( $die );
1018 }
1019
1024 protected function loadSpecialVars() {
1025 if ( $this->hasOption( 'dbuser' ) ) {
1026 $this->mDbUser = $this->getOption( 'dbuser' );
1027 }
1028 if ( $this->hasOption( 'dbpass' ) ) {
1029 $this->mDbPass = $this->getOption( 'dbpass' );
1030 }
1031 if ( $this->hasOption( 'quiet' ) ) {
1032 $this->mQuiet = true;
1033 }
1034 if ( $this->hasOption( 'batch-size' ) ) {
1035 $this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
1036 }
1037 }
1038
1044 protected function maybeHelp( $force = false ) {
1045 if ( !$force && !$this->hasOption( 'help' ) ) {
1046 return;
1047 }
1048 $this->showHelp();
1049 die( 1 );
1050 }
1051
1055 protected function showHelp() {
1056 $screenWidth = 80; // TODO: Calculate this!
1057 $tab = " ";
1058 $descWidth = $screenWidth - ( 2 * strlen( $tab ) );
1059
1060 ksort( $this->mParams );
1061 $this->mQuiet = false;
1062
1063 // Description ...
1064 if ( $this->mDescription ) {
1065 $this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" );
1066 }
1067 $output = "\nUsage: php " . basename( $this->mSelf );
1068
1069 // ... append parameters ...
1070 if ( $this->mParams ) {
1071 $output .= " [--" . implode( "|--", array_keys( $this->mParams ) ) . "]";
1072 }
1073
1074 // ... and append arguments.
1075 if ( $this->mArgList ) {
1076 $output .= ' ';
1077 foreach ( $this->mArgList as $k => $arg ) {
1078 if ( $arg['require'] ) {
1079 $output .= '<' . $arg['name'] . '>';
1080 } else {
1081 $output .= '[' . $arg['name'] . ']';
1082 }
1083 if ( $k < count( $this->mArgList ) - 1 ) {
1084 $output .= ' ';
1085 }
1086 }
1087 }
1088 $this->output( "$output\n\n" );
1089
1090 # TODO abstract some repetitive code below
1091
1092 // Generic parameters
1093 $this->output( "Generic maintenance parameters:\n" );
1094 foreach ( $this->mGenericParameters as $par => $info ) {
1095 if ( $info['shortName'] !== false ) {
1096 $par .= " (-{$info['shortName']})";
1097 }
1098 $this->output(
1099 wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1100 "\n$tab$tab" ) . "\n"
1101 );
1102 }
1103 $this->output( "\n" );
1104
1105 $scriptDependantParams = $this->mDependantParameters;
1106 if ( count( $scriptDependantParams ) > 0 ) {
1107 $this->output( "Script dependant parameters:\n" );
1108 // Parameters description
1109 foreach ( $scriptDependantParams as $par => $info ) {
1110 if ( $info['shortName'] !== false ) {
1111 $par .= " (-{$info['shortName']})";
1112 }
1113 $this->output(
1114 wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1115 "\n$tab$tab" ) . "\n"
1116 );
1117 }
1118 $this->output( "\n" );
1119 }
1120
1121 // Script specific parameters not defined on construction by
1122 // Maintenance::addDefaultParams()
1123 $scriptSpecificParams = array_diff_key(
1124 # all script parameters:
1126 # remove the Maintenance default parameters:
1129 );
1130 '@phan-var array[] $scriptSpecificParams';
1131 if ( count( $scriptSpecificParams ) > 0 ) {
1132 $this->output( "Script specific parameters:\n" );
1133 // Parameters description
1134 foreach ( $scriptSpecificParams as $par => $info ) {
1135 if ( $info['shortName'] !== false ) {
1136 $par .= " (-{$info['shortName']})";
1137 }
1138 $this->output(
1139 wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1140 "\n$tab$tab" ) . "\n"
1141 );
1142 }
1143 $this->output( "\n" );
1144 }
1145
1146 // Print arguments
1147 if ( count( $this->mArgList ) > 0 ) {
1148 $this->output( "Arguments:\n" );
1149 // Arguments description
1150 foreach ( $this->mArgList as $info ) {
1151 $openChar = $info['require'] ? '<' : '[';
1152 $closeChar = $info['require'] ? '>' : ']';
1153 $this->output(
1154 wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " .
1155 $info['desc'], $descWidth, "\n$tab$tab" ) . "\n"
1156 );
1157 }
1158 $this->output( "\n" );
1159 }
1160 }
1161
1166 public function finalSetup() {
1170
1171 # Turn off output buffering again, it might have been turned on in the settings files
1172 if ( ob_get_level() ) {
1173 ob_end_flush();
1174 }
1175 # Same with these
1176 $wgCommandLineMode = true;
1177
1178 # Override $wgServer
1179 if ( $this->hasOption( 'server' ) ) {
1180 $wgServer = $this->getOption( 'server', $wgServer );
1181 }
1182
1183 # If these were passed, use them
1184 if ( $this->mDbUser ) {
1186 }
1187 if ( $this->mDbPass ) {
1189 }
1190 if ( $this->hasOption( 'dbgroupdefault' ) ) {
1191 $wgDBDefaultGroup = $this->getOption( 'dbgroupdefault', null );
1192
1193 $service = MediaWikiServices::getInstance()->peekService( 'DBLoadBalancerFactory' );
1194 if ( $service ) {
1195 $service->destroy();
1196 }
1197 }
1198
1199 if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) {
1202
1203 if ( $wgDBservers ) {
1207 foreach ( $wgDBservers as $i => $server ) {
1208 $wgDBservers[$i]['user'] = $wgDBuser;
1209 $wgDBservers[$i]['password'] = $wgDBpassword;
1210 }
1211 }
1212 if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
1213 $wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
1214 $wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
1215 }
1216 $service = MediaWikiServices::getInstance()->peekService( 'DBLoadBalancerFactory' );
1217 if ( $service ) {
1218 $service->destroy();
1219 }
1220 }
1221
1222 // Per-script profiling; useful for debugging
1223 $this->activateProfiler();
1224
1225 $this->afterFinalSetup();
1226
1227 $wgShowExceptionDetails = true;
1228 $wgShowHostnames = true;
1229
1230 Wikimedia\suppressWarnings();
1231 set_time_limit( 0 );
1232 Wikimedia\restoreWarnings();
1233
1234 $this->adjustMemoryLimit();
1235 }
1236
1241 protected function afterFinalSetup() {
1242 if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
1243 call_user_func( MW_CMDLINE_CALLBACK );
1244 }
1245 }
1246
1251 public function globals() {
1252 if ( $this->hasOption( 'globals' ) ) {
1253 print_r( $GLOBALS );
1254 }
1255 }
1256
1261 public function loadSettings() {
1262 global $wgCommandLineMode, $IP;
1263
1264 if ( isset( $this->mOptions['conf'] ) ) {
1265 $settingsFile = $this->mOptions['conf'];
1266 } elseif ( defined( "MW_CONFIG_FILE" ) ) {
1267 $settingsFile = MW_CONFIG_FILE;
1268 } else {
1269 $settingsFile = "$IP/LocalSettings.php";
1270 }
1271 if ( isset( $this->mOptions['wiki'] ) ) {
1272 $bits = explode( '-', $this->mOptions['wiki'], 2 );
1273 define( 'MW_DB', $bits[0] );
1274 define( 'MW_PREFIX', $bits[1] ?? '' );
1275 } elseif ( isset( $this->mOptions['server'] ) ) {
1276 // Provide the option for site admins to detect and configure
1277 // multiple wikis based on server names. This offers --server
1278 // as alternative to --wiki.
1279 // See https://www.mediawiki.org/wiki/Manual:Wiki_family
1280 $_SERVER['SERVER_NAME'] = $this->mOptions['server'];
1281 }
1282
1283 if ( !is_readable( $settingsFile ) ) {
1284 $this->fatalError( "A copy of your installation's LocalSettings.php\n" .
1285 "must exist and be readable in the source directory.\n" .
1286 "Use --conf to specify it." );
1287 }
1288 $wgCommandLineMode = true;
1289
1290 return $settingsFile;
1291 }
1292
1298 public function purgeRedundantText( $delete = true ) {
1299 # Data should come off the master, wrapped in a transaction
1300 $dbw = $this->getDB( DB_MASTER );
1301 $this->beginTransaction( $dbw, __METHOD__ );
1302
1303 # Get "active" text records via the content table
1304 $cur = [];
1305 $this->output( 'Searching for active text records via contents table...' );
1306 $res = $dbw->select( 'content', 'content_address', [], __METHOD__, [ 'DISTINCT' ] );
1307 $blobStore = MediaWikiServices::getInstance()->getBlobStore();
1308 foreach ( $res as $row ) {
1309 // @phan-suppress-next-line PhanUndeclaredMethod
1310 $textId = $blobStore->getTextIdFromAddress( $row->content_address );
1311 if ( $textId ) {
1312 $cur[] = $textId;
1313 }
1314 }
1315 $this->output( "done.\n" );
1316
1317 # Get the IDs of all text records not in these sets
1318 $this->output( 'Searching for inactive text records...' );
1319 $cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )';
1320 $res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] );
1321 $old = [];
1322 foreach ( $res as $row ) {
1323 $old[] = $row->old_id;
1324 }
1325 $this->output( "done.\n" );
1326
1327 # Inform the user of what we're going to do
1328 $count = count( $old );
1329 $this->output( "$count inactive items found.\n" );
1330
1331 # Delete as appropriate
1332 if ( $delete && $count ) {
1333 $this->output( 'Deleting...' );
1334 $dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ );
1335 $this->output( "done.\n" );
1336 }
1337
1338 $this->commitTransaction( $dbw, __METHOD__ );
1339 }
1340
1345 protected function getDir() {
1346 return __DIR__ . '/../';
1347 }
1348
1363 protected function getDB( $db, $groups = [], $dbDomain = false ) {
1364 if ( $this->mDb === null ) {
1365 return MediaWikiServices::getInstance()
1366 ->getDBLoadBalancerFactory()
1367 ->getMainLB( $dbDomain )
1368 ->getMaintenanceConnectionRef( $db, $groups, $dbDomain );
1369 }
1370
1371 return $this->mDb;
1372 }
1373
1380 public function setDB( IMaintainableDatabase $db ) {
1381 $this->mDb = $db;
1382 }
1383
1394 protected function beginTransaction( IDatabase $dbw, $fname ) {
1395 $dbw->begin( $fname );
1396 }
1397
1409 protected function commitTransaction( IDatabase $dbw, $fname ) {
1410 $dbw->commit( $fname );
1411 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
1412 $waitSucceeded = $lbFactory->waitForReplication(
1413 [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
1414 );
1415 $this->lastReplicationWait = microtime( true );
1416 return $waitSucceeded;
1417 }
1418
1429 protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1430 $dbw->rollback( $fname );
1431 }
1432
1437 private function lockSearchindex( $db ) {
1438 $write = [ 'searchindex' ];
1439 $read = [
1440 'page',
1441 'revision',
1442 'text',
1443 'interwiki',
1444 'l10n_cache',
1445 'user',
1446 'page_restrictions'
1447 ];
1448 $db->lockTables( $read, $write, __CLASS__ . '-searchIndexLock' );
1449 }
1450
1455 private function unlockSearchindex( $db ) {
1456 $db->unlockTables( __CLASS__ . '-searchIndexLock' );
1457 }
1458
1464 private function relockSearchindex( $db ) {
1465 $this->unlockSearchindex( $db );
1466 $this->lockSearchindex( $db );
1467 }
1468
1476 public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1477 $lockTime = time();
1478
1479 # Lock searchindex
1480 if ( $maxLockTime ) {
1481 $this->output( " --- Waiting for lock ---" );
1482 $this->lockSearchindex( $dbw );
1483 $lockTime = time();
1484 $this->output( "\n" );
1485 }
1486
1487 # Loop through the results and do a search update
1488 foreach ( $results as $row ) {
1489 # Allow reads to be processed
1490 if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1491 $this->output( " --- Relocking ---" );
1492 $this->relockSearchindex( $dbw );
1493 $lockTime = time();
1494 $this->output( "\n" );
1495 }
1496 call_user_func( $callback, $row );
1497 }
1498
1499 # Unlock searchindex
1500 if ( $maxLockTime ) {
1501 $this->output( " --- Unlocking --" );
1502 $this->unlockSearchindex( $dbw );
1503 $this->output( "\n" );
1504 }
1505 }
1506
1512 public function updateSearchIndexForPage( int $pageId ) {
1513 // Get current revision
1514 $rev = MediaWikiServices::getInstance()
1515 ->getRevisionLookup()
1516 ->getRevisionByPageId( $pageId, 0, IDBAccessObject::READ_LATEST );
1517 $title = null;
1518 if ( $rev ) {
1519 $titleObj = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
1520 $title = $titleObj->getPrefixedDBkey();
1521 $this->output( "$title..." );
1522 # Update searchindex
1523 $u = new SearchUpdate( $pageId, $titleObj, $rev->getContent( SlotRecord::MAIN ) );
1524 $u->doUpdate();
1525 $this->output( "\n" );
1526 }
1527
1528 return $title;
1529 }
1530
1541 protected function countDown( $seconds ) {
1542 if ( $this->isQuiet() ) {
1543 return;
1544 }
1545 for ( $i = $seconds; $i >= 0; $i-- ) {
1546 if ( $i != $seconds ) {
1547 $this->output( str_repeat( "\x08", strlen( $i + 1 ) ) );
1548 }
1549 $this->output( $i );
1550 if ( $i ) {
1551 sleep( 1 );
1552 }
1553 }
1554 $this->output( "\n" );
1555 }
1556
1565 public static function posix_isatty( $fd ) {
1566 if ( !function_exists( 'posix_isatty' ) ) {
1567 return !$fd;
1568 } else {
1569 return posix_isatty( $fd );
1570 }
1571 }
1572
1578 public static function readconsole( $prompt = '> ' ) {
1579 static $isatty = null;
1580 if ( $isatty === null ) {
1581 $isatty = self::posix_isatty( 0 /*STDIN*/ );
1582 }
1583
1584 if ( $isatty && function_exists( 'readline' ) ) {
1585 return readline( $prompt );
1586 } else {
1587 if ( $isatty ) {
1588 $st = self::readlineEmulation( $prompt );
1589 } else {
1590 if ( feof( STDIN ) ) {
1591 $st = false;
1592 } else {
1593 $st = fgets( STDIN, 1024 );
1594 }
1595 }
1596 if ( $st === false ) {
1597 return false;
1598 }
1599 $resp = trim( $st );
1600
1601 return $resp;
1602 }
1603 }
1604
1610 private static function readlineEmulation( $prompt ) {
1611 $bash = ExecutableFinder::findInDefaultPaths( 'bash' );
1612 if ( !wfIsWindows() && $bash ) {
1613 $retval = false;
1614 $encPrompt = Shell::escape( $prompt );
1615 $command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1616 $encCommand = Shell::escape( $command );
1617 $line = Shell::escape( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] );
1618
1619 // @phan-suppress-next-line PhanImpossibleCondition,PhanSuspiciousValueComparison
1620 if ( $retval == 0 ) {
1621 return $line;
1622 } elseif ( $retval == 127 ) {
1623 // Couldn't execute bash even though we thought we saw it.
1624 // Shell probably spit out an error message, sorry :(
1625 // Fall through to fgets()...
1626 } else {
1627 // EOF/ctrl+D
1628 return false;
1629 }
1630 }
1631
1632 // Fallback... we'll have no editing controls, EWWW
1633 if ( feof( STDIN ) ) {
1634 return false;
1635 }
1636 print $prompt;
1637
1638 return fgets( STDIN, 1024 );
1639 }
1640
1648 public static function getTermSize() {
1649 $default = [ 80, 50 ];
1650 if ( wfIsWindows() ) {
1651 return $default;
1652 }
1653 if ( Shell::isDisabled() ) {
1654 return $default;
1655 }
1656 // It's possible to get the screen size with VT-100 terminal escapes,
1657 // but reading the responses is not possible without setting raw mode
1658 // (unless you want to require the user to press enter), and that
1659 // requires an ioctl(), which we can't do. So we have to shell out to
1660 // something that can do the relevant syscalls. There are a few
1661 // options. Linux and Mac OS X both have "stty size" which does the
1662 // job directly.
1663 $result = Shell::command( 'stty', 'size' )
1664 ->execute();
1665 if ( $result->getExitCode() !== 0 ) {
1666 return $default;
1667 }
1668 if ( !preg_match( '/^(\d+) (\d+)$/', $result->getStdout(), $m ) ) {
1669 return $default;
1670 }
1671 return [ intval( $m[2] ), intval( $m[1] ) ];
1672 }
1673
1678 public static function requireTestsAutoloader() {
1679 require_once __DIR__ . '/../../tests/common/TestsAutoLoader.php';
1680 }
1681
1688 protected function getHookContainer() {
1689 if ( !$this->hookContainer ) {
1690 $this->hookContainer = MediaWikiServices::getInstance()->getHookContainer();
1691 }
1692 return $this->hookContainer;
1693 }
1694
1703 protected function getHookRunner() {
1704 if ( !$this->hookRunner ) {
1705 $this->hookRunner = new HookRunner( $this->getHookContainer() );
1706 }
1707 return $this->hookRunner;
1708 }
1709
1720 protected function parseIntList( $text ) {
1721 $ids = preg_split( '/[\s,;:|]+/', $text );
1722 $ids = array_map(
1723 function ( $id ) {
1724 return (int)$id;
1725 },
1726 $ids
1727 );
1728 return array_filter( $ids );
1729 }
1730}
getDB()
$GLOBALS['IP']
$wgDBuser
Database username.
$wgDBadminuser
Separate username for maintenance tasks.
$wgTrxProfilerLimits
Performance expectations for DB usage.
$wgDBservers
Database load balancer This is a two-dimensional array, an array of server info structures Fields are...
$wgShowHostnames
Expose backend server host names through the API and various HTML comments.
$wgDBDefaultGroup
Default group to use when getting database connections.
$wgDBadminpassword
Separate password for maintenance tasks.
$wgProfiler
Profiler configuration.
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
$wgServer
URL of the server.
$wgLBFactoryConf
Load balancer factory configuration To set up a multi-master wiki farm, set the class here to somethi...
$wgDBpassword
Database user's password.
global $wgCommandLineMode
wfHostname()
Get host name of the current machine, for use in error reporting.
wfIsWindows()
Check if the operating system is Windows.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
$IP
Definition WebStart.php:49
$maintClass
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
getDB( $db, $groups=[], $dbDomain=false)
Returns a database to be used by current maintenance script.
setup()
Do some sanity checking and basic setup.
array[] $mParams
Array of desired/allowed params.
__construct()
Default constructor.
error( $err, $die=0)
Throw an error to the user.
const STDIN_ALL
getName()
Get the script's name.
array[] $mGenericParameters
Generic options added by addDefaultParams()
addArg( $arg, $description, $required=true)
Add some args that are needed.
requireExtension( $name)
Indicate that the specified extension must be loaded before the script can run.
showHelp()
Definitely show the help.
relockSearchindex( $db)
Unlock and lock again Since the lock is low-priority, queued reads will be able to complete.
int $mBatchSize
Batch size.
setAgentAndTriggers()
Set triggers like when to try to run deferred updates.
setAllowUnregisteredOptions( $allow)
Sets whether to allow unregistered options, which are options passed to a script that do not match an...
beginTransaction(IDatabase $dbw, $fname)
Begin a transcation on a DB.
static getTermSize()
Get the terminal size as a two-element array where the first element is the width (number of columns)...
HookContainer null $hookContainer
clearParamsAndArgs()
Clear all params and arguments.
static setLBFactoryTriggers(LBFactory $LBFactory, Config $config)
array $requiredExtensions
setParam(&$options, $option, $value)
Helper function used solely by loadParamsAndArgs to prevent code duplication.
const DB_NONE
Constants for DB access type.
commitTransaction(IDatabase $dbw, $fname)
Commit the transcation on a DB handle and wait for replica DBs to catch up.
array[] $mDependantParameters
Generic options which might or not be supported by the script.
output( $out, $channel=null)
Throw some output to the user.
supportsOption( $name)
Checks to see if a particular option in supported.
getStdin( $len=null)
Return input from stdin.
cleanupChanneled()
Clean up channeled output.
memoryLimit()
Normally we disable the memory_limit when running admin scripts.
getHookRunner()
Get a HookRunner for running core hooks.
afterFinalSetup()
Execute a callback function at the end of initialisation Stable for overriding.
hasArg( $argId=0)
Does a given argument exist?
getDir()
Get the maintenance directory.
addDefaultParams()
Add the default parameters to the scripts.
deleteOption( $name)
Remove an option.
static readlineEmulation( $prompt)
Emulate readline()
static requireTestsAutoloader()
Call this to set up the autoloader to allow classes to be used from the tests directory.
loadParamsAndArgs( $self=null, $opts=null, $args=null)
Process command line arguments $mOptions becomes an array with keys set to the option names $mArgs be...
outputChanneled( $msg, $channel=null)
Message outputter with channeled message support.
finalSetup()
Handle some last-minute setup here.
loadSpecialVars()
Handle the special variables that are global to all scripts Stable for overriding.
setDB(IMaintainableDatabase $db)
Sets database object to be returned by getDB().
float $lastReplicationWait
UNIX timestamp.
array $orderedOptions
Used to read the options in the order they were passed.
loadSettings()
Generic setup for most installs.
hasOption( $name)
Checks to see if a particular option was set.
purgeRedundantText( $delete=true)
Support function for cleaning up redundant text records.
countDown( $seconds)
Count down from $seconds to zero on the terminal, with a one-second pause between showing each number...
runChild( $maintClass, $classFile=null)
Run a child maintenance script.
IMaintainableDatabase $mDb
Used by getDB() / setDB()
execute()
Do the actual work.
static readconsole( $prompt='> ')
Prompt the console for input.
static posix_isatty( $fd)
Wrapper for posix_isatty() We default as considering stdin a tty (for nice readline methods) but trea...
adjustMemoryLimit()
Adjusts PHP's memory limit to better suit our needs, if needed.
validateParamsAndArgs()
Run some validation checks on the params, etc Stable for overriding.
getHookContainer()
Get a HookContainer, for running extension hooks or for hook metadata.
HookRunner null $hookRunner
getDbType()
Does the script need different DB access? By default, we give Maintenance scripts normal rights to th...
getBatchSize()
Returns batch size.
unlockSearchindex( $db)
Unlock the tables.
parseIntList( $text)
Utility function to parse a string (perhaps from a command line option) into a list of integers (perh...
getArg( $argId=0, $default=null)
Get an argument.
addDescription( $text)
Set the description text.
activateProfiler()
Activate the profiler (assuming $wgProfiler is set)
maybeHelp( $force=false)
Maybe show the help.
resource $fileHandle
Used when creating separate schema files.
updateSearchIndexForPage(int $pageId)
Update the searchindex table for a given pageid.
loadWithArgv( $argv)
Load params and arguments from a given array of command-line arguments.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
static shouldExecute()
Should we execute the maintenance script, or just allow it to be included as a standalone class?...
getOption( $name, $default=null)
Get an option, or return the default.
Config $config
Accessible via getConfig()
lockSearchindex( $db)
Lock the search index.
checkRequiredExtensions()
Verify that the required extensions are installed.
rollbackTransaction(IDatabase $dbw, $fname)
Rollback the transcation on a DB handle.
updateSearchIndex( $maxLockTime, $callback, $dbw, $results)
Perform a search index update with locking.
globals()
Potentially debug globals.
setConfig(Config $config)
setBatchSize( $s=0)
Set the batch size.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Value object representing a content slot associated with a page revision.
Executes shell commands.
Definition Shell.php:44
Database independant search index updater.
An interface for generating database load balancers.
Definition LBFactory.php:41
while(( $__line=Maintenance::readconsole()) !==false) print
Definition eval.php:64
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
rollback( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Rollback a transaction previously started using begin()
commit( $fname=__METHOD__, $flush=self::FLUSHING_ONE)
Commits a transaction previously started using begin()
begin( $fname=__METHOD__, $mode=self::TRANSACTION_EXPLICIT)
Begin a transaction.
Advanced database interface for IDatabase handles that include maintenance methods.
Result wrapper for grabbing data queried from an IDatabase object.
$line
Definition mcc.php:119
$command
Definition mcc.php:125
if( $line===false) $args
Definition mcc.php:124
const DB_MASTER
Definition defines.php:29