Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
24.57% |
72 / 293 |
|
16.67% |
5 / 30 |
CRAP | |
0.00% |
0 / 1 |
MaintenanceRunner | |
24.57% |
72 / 293 |
|
16.67% |
5 / 30 |
5019.94 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getConfig | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
addDefaultParams | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
1 | |||
showHelpAndExit | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
initFromWrapper | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
initForClass | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
initInternal | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
isAbsolutePath | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
8.12 | |||
getExtensionInfo | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
loadScriptFile | |
66.67% |
6 / 9 |
|
0.00% |
0 / 1 |
3.33 | |||
splitScript | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
expandScriptFile | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
expandScriptClass | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
preloadScriptFile | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
7.18 | |||
getScriptClass | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
6.99 | |||
findScriptClass | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
6.01 | |||
setup | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
memoryLimit | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
adjustMemoryLimit | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
defineSettings | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
emulateConfig | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
overrideConfig | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
72 | |||
getServiceContainer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
72 | |||
fatalError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
error | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
shouldExecute | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
42 | |||
cleanup | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
shutdown | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Maintenance; |
4 | |
5 | use Exception; |
6 | use LCStoreNull; |
7 | use LogicException; |
8 | use Maintenance; |
9 | use MediaWiki; |
10 | use MediaWiki\Config\Config; |
11 | use MediaWiki\Deferred\DeferredUpdates; |
12 | use MediaWiki\Logger\LoggerFactory; |
13 | use MediaWiki\MainConfigNames; |
14 | use MediaWiki\MediaWikiServices; |
15 | use MediaWiki\Settings\SettingsBuilder; |
16 | use Profiler; |
17 | use ReflectionClass; |
18 | use Throwable; |
19 | |
20 | /** |
21 | * A runner for maintenance scripts. |
22 | * |
23 | * @since 1.39 |
24 | * @unstable |
25 | */ |
26 | class MaintenanceRunner { |
27 | |
28 | /** |
29 | * Identifies the script to execute. This may be a class name, the relative or absolute |
30 | * path of a script file, a plain name with or without an extension prefix, etc. |
31 | * |
32 | * @var ?string |
33 | */ |
34 | private $script = null; |
35 | |
36 | /** |
37 | * The class name of the script to execute. |
38 | * |
39 | * @var ?class-string<Maintenance> |
40 | */ |
41 | private $scriptClass = null; |
42 | |
43 | /** @var string[]|null */ |
44 | private $scriptArgv = null; |
45 | |
46 | /** @var Maintenance|null */ |
47 | private $scriptObject = null; |
48 | |
49 | /** @var MaintenanceParameters */ |
50 | private $parameters; |
51 | |
52 | /** @var bool */ |
53 | private $runFromWrapper = false; |
54 | |
55 | /** @var bool */ |
56 | private bool $withoutLocalSettings = false; |
57 | |
58 | /** @var ?Config */ |
59 | private ?Config $config = null; |
60 | |
61 | /** |
62 | * Default constructor. Children should call this *first* if implementing |
63 | * their own constructors |
64 | * |
65 | * @stable to call |
66 | */ |
67 | public function __construct() { |
68 | $this->parameters = new MaintenanceParameters(); |
69 | $this->addDefaultParams(); |
70 | } |
71 | |
72 | private function getConfig() { |
73 | if ( $this->config === null ) { |
74 | $this->config = $this->getServiceContainer()->getMainConfig(); |
75 | } |
76 | |
77 | return $this->config; |
78 | } |
79 | |
80 | /** |
81 | * Add the default parameters to the scripts |
82 | */ |
83 | protected function addDefaultParams() { |
84 | // Generic (non-script-dependent) options: |
85 | |
86 | $this->parameters->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true ); |
87 | $this->parameters->addOption( 'wiki', 'For specifying the wiki ID', false, true ); |
88 | $this->parameters->addOption( 'globals', 'Output globals at the end of processing for debugging' ); |
89 | $this->parameters->addOption( |
90 | 'memory-limit', |
91 | 'Set a specific memory limit for the script, ' |
92 | . '"max" for no limit or "default" to avoid changing it', |
93 | false, |
94 | true |
95 | ); |
96 | $this->parameters->addOption( 'server', "The protocol and server name to use in URLs, e.g. " . |
97 | "https://en.wikipedia.org. This is sometimes necessary because " . |
98 | "server name detection may fail in command line scripts.", false, true ); |
99 | $this->parameters->addOption( 'profiler', 'Profiler output format (usually "text")', false, true ); |
100 | |
101 | // Save generic options to display them separately in help |
102 | $generic = $this->parameters->getOptionNames(); |
103 | $this->parameters->assignGroup( Maintenance::GENERIC_MAINTENANCE_PARAMETERS, $generic ); |
104 | } |
105 | |
106 | /** |
107 | * @param int $code |
108 | * |
109 | * @return never |
110 | */ |
111 | private function showHelpAndExit( $code = 0 ) { |
112 | foreach ( $this->parameters->getErrors() as $error ) { |
113 | $this->error( "$error\n" ); |
114 | $code = 1; |
115 | } |
116 | |
117 | $this->parameters->setDescription( 'Runner for maintenance scripts' ); |
118 | |
119 | $help = $this->parameters->getHelp(); |
120 | echo $help; |
121 | exit( $code ); |
122 | } |
123 | |
124 | /** |
125 | * Initialize the runner from the given command line arguments |
126 | * as passed to a wrapper script. |
127 | * |
128 | * @note Called before Setup.php |
129 | * |
130 | * @param string[] $argv The arguments passed from the command line, |
131 | * including the wrapper script at index 0, and usually |
132 | * the script to run at index 1. |
133 | */ |
134 | public function initFromWrapper( array $argv ) { |
135 | $script = null; |
136 | |
137 | $this->parameters->setName( $argv[0] ); |
138 | $this->parameters->setAllowUnregisteredOptions( true ); |
139 | $this->parameters->addArg( |
140 | 'script', |
141 | 'The name of the maintenance script to run. ' . |
142 | 'Can be given as a class name or file path. The `.php` suffix is optional. ' . |
143 | 'Paths starting with `./` or `../` are interpreted to be relative to the current working directory. ' . |
144 | 'Other relative paths are interpreted relative to the maintenance script directory. ' . |
145 | 'Dots (.) are supported as namespace separators in class names. ' . |
146 | 'An extension name may be provided as a prefix, followed by a colon, e.g. "MyExtension:...", ' . |
147 | 'to indicate that the path or class name should be interpreted relative to the extension.' |
148 | ); |
149 | |
150 | $this->runFromWrapper = true; |
151 | $this->parameters->loadWithArgv( $argv, 1 ); |
152 | |
153 | // script params |
154 | $argv = array_slice( $argv, 2 ); |
155 | |
156 | if ( $this->parameters->validate() ) { |
157 | $script = $this->parameters->getArg( 0 ); |
158 | |
159 | // Special handling for the 'help' command |
160 | if ( $script === 'help' ) { |
161 | if ( $this->parameters->hasArg( 1 ) ) { |
162 | $script = $this->parameters->getArg( 1 ); |
163 | |
164 | // turn <help> <command> into <command> --help |
165 | $this->parameters->loadWithArgv( [ $script ] ); |
166 | $argv = [ '--help' ]; |
167 | } else { |
168 | // same as no command |
169 | $script = null; |
170 | } |
171 | } |
172 | } |
173 | |
174 | if ( $script ) { |
175 | // Strip another argument from $argv! |
176 | $this->initInternal( $script, $argv ); |
177 | } else { |
178 | $this->showHelpAndExit(); |
179 | } |
180 | } |
181 | |
182 | /** |
183 | * Initialize the runner for the given class. |
184 | * This is used when running scripts directly, without a wrapper. |
185 | * |
186 | * @note Called before Setup.php |
187 | * |
188 | * @param string $scriptClass The script class to run |
189 | * @param string[] $argv The arguments to passed to the script, including |
190 | * the script itself at index 0. |
191 | */ |
192 | public function initForClass( string $scriptClass, $argv ) { |
193 | $this->runFromWrapper = false; |
194 | $this->script = $scriptClass; |
195 | $this->scriptClass = $scriptClass; |
196 | $this->parameters->setName( $argv[0] ); |
197 | $this->parameters->loadWithArgv( $argv ); |
198 | $this->initInternal( $scriptClass, array_slice( $argv, 1 ) ); |
199 | } |
200 | |
201 | /** |
202 | * Initialize the runner. |
203 | * |
204 | * @note Called before Setup.php |
205 | * |
206 | * @param string $script The script to run |
207 | * @param string[] $scriptArgv The arguments to pass to the maintenance script, |
208 | * not including the script itself. |
209 | */ |
210 | private function initInternal( string $script, array $scriptArgv ) { |
211 | $this->script = $script; |
212 | $this->scriptArgv = $scriptArgv; |
213 | |
214 | // Send PHP warnings and errors to stderr instead of stdout. |
215 | // This aids in diagnosing problems, while keeping messages |
216 | // out of redirected output. |
217 | if ( ini_get( 'display_errors' ) ) { |
218 | ini_set( 'display_errors', 'stderr' ); |
219 | } |
220 | |
221 | // make sure we clean up after ourselves. |
222 | register_shutdown_function( [ $this, 'cleanup' ] ); |
223 | |
224 | // Turn off output buffering if it's on |
225 | while ( ob_get_level() > 0 ) { |
226 | ob_end_flush(); |
227 | } |
228 | } |
229 | |
230 | private static function isAbsolutePath( $path ) { |
231 | if ( str_starts_with( $path, '/' ) ) { |
232 | return true; |
233 | } |
234 | |
235 | if ( wfIsWindows() ) { |
236 | if ( str_starts_with( $path, '\\' ) ) { |
237 | return true; |
238 | } |
239 | if ( preg_match( '!^[a-zA-Z]:[/\\\\]!', $path ) ) { |
240 | return true; |
241 | } |
242 | } |
243 | |
244 | return false; |
245 | } |
246 | |
247 | protected function getExtensionInfo( string $extName ): ?array { |
248 | // NOTE: Don't go by the extension registry, since some extensions |
249 | // register under a name different from what is used in wfLoadExtension. |
250 | // E.g. AbuseFilter is registered as "Abuse Filter" with a space. |
251 | |
252 | $config = SettingsBuilder::getInstance()->getConfig(); |
253 | $extDir = $config->get( MainConfigNames::ExtensionDirectory ); |
254 | $skinDir = $config->get( MainConfigNames::StyleDirectory ); |
255 | |
256 | $extension = []; |
257 | if ( file_exists( "$extDir/$extName/extension.json" ) ) { |
258 | $extension['path'] = "$extDir/$extName/extension.json"; |
259 | $extension['namespace'] = "MediaWiki\\Extension\\$extName"; |
260 | } elseif ( file_exists( "$skinDir/$extName/skin.json" ) ) { |
261 | $extension['path'] = "$skinDir/$extName/skin.json"; |
262 | $extension['namespace'] = "MediaWiki\\Skins\\$extName"; |
263 | } else { |
264 | return null; |
265 | } |
266 | |
267 | return $extension; |
268 | } |
269 | |
270 | private function loadScriptFile( string $scriptFile ): string { |
271 | $maintClass = null; |
272 | |
273 | // It's a file, include it |
274 | // If it returns something, it should be the name of the maintenance class. |
275 | $scriptClass = include $scriptFile; |
276 | |
277 | // Traditional script files set the $maintClass variable |
278 | // at the end of the file. |
279 | // @phan-suppress-next-line PhanImpossibleCondition Phan doesn't understand includes. |
280 | if ( $maintClass ) { |
281 | $scriptClass = $maintClass; |
282 | } |
283 | |
284 | if ( !is_string( $scriptClass ) ) { |
285 | $this->error( "ERROR: The script file '{$scriptFile}' cannot be executed using MaintenanceRunner.\n" ); |
286 | $this->error( "It does not set \$maintClass and does not return a class name.\n" ); |
287 | $this->fatalError( "Try running it directly as a php script: php $scriptFile\n" ); |
288 | } |
289 | |
290 | return $scriptClass; |
291 | } |
292 | |
293 | private function splitScript( string $script ): array { |
294 | // Support "$ext:$script" format for extensions |
295 | if ( preg_match( '!^(\w+):(.*)$!', $script, $m ) ) { |
296 | return [ $m[1], $m[2] ]; |
297 | } |
298 | |
299 | return [ null, $script ]; |
300 | } |
301 | |
302 | private function expandScriptFile( string $scriptName, ?array $extension ): string { |
303 | // Append ".php" if not present |
304 | $scriptFile = $scriptName; |
305 | if ( !str_ends_with( $scriptFile, '.php' ) ) { |
306 | $scriptFile .= '.php'; |
307 | } |
308 | |
309 | // If the path is not explicitly relative (starting with "./" or "../") and not absolute, |
310 | // then look in the maintenance dir. |
311 | if ( !preg_match( '!^\.\.?[/\\\\]!', $scriptFile ) && !self::isAbsolutePath( $scriptFile ) ) { |
312 | if ( $extension !== null ) { |
313 | // Look in the extension's maintenance dir |
314 | $scriptFile = dirname( $extension['path'] ) . "/maintenance/{$scriptFile}"; |
315 | } else { |
316 | // It's a core script. |
317 | $scriptFile = MW_INSTALL_PATH . "/maintenance/{$scriptFile}"; |
318 | } |
319 | } |
320 | |
321 | return $scriptFile; |
322 | } |
323 | |
324 | private function expandScriptClass( string $scriptName, ?array $extension ): string { |
325 | $scriptClass = $scriptName; |
326 | |
327 | // Support "$ext:$script" format |
328 | if ( $extension ) { |
329 | $scriptClass = "{$extension['namespace']}\\Maintenance\\$scriptClass"; |
330 | } |
331 | |
332 | // Accept dot (.) as namespace separators as well. |
333 | // Backslashes are just annoying on the command line. |
334 | $scriptClass = strtr( $scriptClass, '.', '\\' ); |
335 | |
336 | return $scriptClass; |
337 | } |
338 | |
339 | /** |
340 | * Preload the script file, so any defines in file level code are executed. |
341 | * This way, scripts can control what Setup.php does. |
342 | * |
343 | * @internal |
344 | * @param string $script |
345 | */ |
346 | protected function preloadScriptFile( string $script ): void { |
347 | if ( $this->scriptClass !== null && class_exists( $this->scriptClass ) ) { |
348 | // We know the script class, and file-level code was executed because class_exists triggers auto-loading. |
349 | return; |
350 | } |
351 | |
352 | [ $extName, $scriptName ] = $this->splitScript( $script ); |
353 | |
354 | if ( $extName !== null ) { |
355 | // Preloading is not supported. findScriptClass() will try to find the script later. |
356 | return; |
357 | } |
358 | |
359 | $scriptFile = $this->expandScriptFile( $scriptName, null ); |
360 | |
361 | $scriptClass = null; |
362 | if ( file_exists( $scriptFile ) ) { |
363 | $scriptClass = $this->loadScriptFile( $scriptFile ); |
364 | } |
365 | |
366 | if ( !$scriptClass ) { |
367 | $scriptClass = $this->expandScriptClass( $scriptName, null ); |
368 | } |
369 | |
370 | // NOTE: class_exists will trigger auto-loading, so file-level code in the script file will run. |
371 | if ( class_exists( $scriptClass ) ) { |
372 | // Set the script class name we found, so we don't try to load the file again! |
373 | $this->scriptClass = $scriptClass; |
374 | } |
375 | |
376 | // Preloading failed. Let findScriptClass() try to find the script later. |
377 | } |
378 | |
379 | /** |
380 | * @return class-string<Maintenance> |
381 | */ |
382 | protected function getScriptClass(): string { |
383 | if ( $this->scriptClass === null ) { |
384 | if ( $this->runFromWrapper ) { |
385 | $this->scriptClass = $this->findScriptClass( $this->script ); |
386 | } else { |
387 | $this->scriptClass = $this->script; |
388 | } |
389 | } |
390 | |
391 | if ( !class_exists( $this->scriptClass ) ) { |
392 | $this->fatalError( "Script class {$this->scriptClass} not found.\n" ); |
393 | } |
394 | |
395 | return $this->scriptClass; |
396 | } |
397 | |
398 | /** |
399 | * @internal |
400 | * @param string $script |
401 | * |
402 | * @return class-string<Maintenance> |
403 | */ |
404 | protected function findScriptClass( string $script ): string { |
405 | [ $extName, $scriptName ] = $this->splitScript( $script ); |
406 | |
407 | if ( $extName !== null ) { |
408 | $extension = $this->getExtensionInfo( $extName ); |
409 | |
410 | if ( !$extension ) { |
411 | $this->fatalError( "Extension '{$extName}' not found.\n" ); |
412 | } |
413 | } else { |
414 | $extension = null; |
415 | } |
416 | |
417 | $scriptFile = $this->expandScriptFile( $scriptName, $extension ); |
418 | |
419 | $scriptClass = null; |
420 | if ( file_exists( $scriptFile ) ) { |
421 | $scriptClass = $this->loadScriptFile( $scriptFile ); |
422 | } |
423 | |
424 | if ( !$scriptClass ) { |
425 | $scriptClass = $this->expandScriptClass( $scriptName, $extension ); |
426 | } |
427 | |
428 | if ( !class_exists( $scriptClass ) ) { |
429 | $this->fatalError( "Script '{$script}' not found (tried path '$scriptFile' and class '$scriptClass').\n" ); |
430 | } |
431 | |
432 | return $scriptClass; |
433 | } |
434 | |
435 | /** |
436 | * MW_FINAL_SETUP_CALLBACK handler, for setting up the Maintenance object. |
437 | * |
438 | * @param SettingsBuilder $settings |
439 | */ |
440 | public function setup( SettingsBuilder $settings ) { |
441 | // NOTE: this has to happen after the autoloader has been initialized. |
442 | $scriptClass = $this->getScriptClass(); |
443 | |
444 | $cls = new ReflectionClass( $scriptClass ); |
445 | if ( !$cls->isSubclassOf( Maintenance::class ) ) { |
446 | $this->fatalError( "Class {$this->script} is not a subclass of Maintenance.\n" ); |
447 | } |
448 | |
449 | // Initialize the actual Maintenance object |
450 | try { |
451 | $this->scriptObject = new $scriptClass; |
452 | $this->scriptObject->setName( $this->getName() ); |
453 | } catch ( Throwable $ex ) { |
454 | $this->fatalError( |
455 | "Failed to initialize Maintenance object.\n" . |
456 | "(Did you forget to call parent::__construct() in your maintenance script?)\n" . |
457 | "$ex\n" |
458 | ); |
459 | } |
460 | |
461 | if ( !$this->scriptObject instanceof Maintenance ) { |
462 | // This should never happen, we already checked if the class is a subclass of Maintenance! |
463 | throw new LogicException( 'Incompatible script object' ); |
464 | } |
465 | |
466 | // Inject runner stuff into the script's parameter definitions. |
467 | // This is mainly used when printing help. |
468 | $scriptParameters = $this->scriptObject->getParameters(); |
469 | |
470 | if ( $this->runFromWrapper ) { |
471 | $scriptParameters->setUsagePrefix( 'php ' . $this->parameters->getName() ); |
472 | } |
473 | |
474 | $scriptParameters->mergeOptions( $this->parameters ); |
475 | $this->parameters = $scriptParameters; |
476 | |
477 | // Ingest argv |
478 | $this->scriptObject->loadWithArgv( $this->scriptArgv ); |
479 | |
480 | // Basic checks and such |
481 | $this->scriptObject->setup(); |
482 | |
483 | // Set the memory limit |
484 | $this->adjustMemoryLimit(); |
485 | |
486 | // Override any config settings |
487 | $this->overrideConfig( $settings ); |
488 | } |
489 | |
490 | /** |
491 | * Returns the maintenance script name to show in the help message. |
492 | * |
493 | * @return string |
494 | */ |
495 | public function getName(): string { |
496 | // Once one of the init methods was called, getArg( 0 ) should always |
497 | // return something. |
498 | return $this->parameters->getArg( 0 ) ?? 'UNKNOWN'; |
499 | } |
500 | |
501 | /** |
502 | * Normally we disable the memory_limit when running admin scripts. |
503 | * Some scripts may wish to actually set a limit, however, to avoid |
504 | * blowing up unexpectedly. |
505 | * @see Maintenance::memoryLimit() |
506 | * @return string |
507 | */ |
508 | private function memoryLimit() { |
509 | if ( $this->parameters->hasOption( 'memory-limit' ) ) { |
510 | $limit = $this->parameters->getOption( 'memory-limit', 'max' ); |
511 | $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood |
512 | return $limit; |
513 | } |
514 | |
515 | $limit = $this->scriptObject->memoryLimit(); |
516 | return $limit ?: 'max'; |
517 | } |
518 | |
519 | /** |
520 | * Adjusts PHP's memory limit to better suit our needs, if needed. |
521 | */ |
522 | private function adjustMemoryLimit() { |
523 | $limit = $this->memoryLimit(); |
524 | if ( $limit == 'max' ) { |
525 | $limit = -1; // no memory limit |
526 | } |
527 | if ( $limit != 'default' ) { |
528 | ini_set( 'memory_limit', $limit ); |
529 | } |
530 | } |
531 | |
532 | /** |
533 | * Define how settings are loaded (e.g. LocalSettings.php) |
534 | * @note Called before Setup.php |
535 | * |
536 | * @internal |
537 | * @return void |
538 | */ |
539 | public function defineSettings() { |
540 | global $IP; |
541 | |
542 | if ( $this->parameters->hasOption( 'conf' ) ) { |
543 | // Define the constant instead of directly setting $settingsFile |
544 | // to ensure consistency. wfDetectLocalSettingsFile() will return |
545 | // MW_CONFIG_FILE if it is defined. |
546 | define( 'MW_CONFIG_FILE', $this->parameters->getOption( 'conf' ) ); |
547 | |
548 | if ( !is_readable( MW_CONFIG_FILE ) ) { |
549 | $this->fatalError( "\nConfig file " . MW_CONFIG_FILE . " was not found or is not readable.\n\n" ); |
550 | } |
551 | } |
552 | $settingsFile = wfDetectLocalSettingsFile( $IP ); |
553 | |
554 | if ( $this->parameters->hasOption( 'wiki' ) ) { |
555 | $wikiName = $this->parameters->getOption( 'wiki' ); |
556 | $bits = explode( '-', $wikiName, 2 ); |
557 | define( 'MW_DB', $bits[0] ); |
558 | define( 'MW_PREFIX', $bits[1] ?? '' ); |
559 | define( 'MW_WIKI_NAME', $wikiName ); |
560 | } elseif ( $this->parameters->hasOption( 'server' ) ) { |
561 | // Provide the option for site admins to detect and configure |
562 | // multiple wikis based on server names. This offers --server |
563 | // as alternative to --wiki. |
564 | // See https://www.mediawiki.org/wiki/Manual:Wiki_family |
565 | $_SERVER['SERVER_NAME'] = $this->parameters->getOption( 'server' ); |
566 | } |
567 | |
568 | // Try to load the script file before running Setup.php if possible. |
569 | // This allows the script file to define constants that change the behavior |
570 | // of Setup.php. |
571 | // Note that this will only work reliably for core scripts. |
572 | if ( $this->runFromWrapper ) { |
573 | $this->preloadScriptFile( $this->script ); |
574 | } |
575 | |
576 | if ( !is_readable( $settingsFile ) ) { |
577 | // NOTE: Some maintenance scripts can (and need to) run without LocalSettings. |
578 | // But we only know that once we have instantiated the Maintenance object. |
579 | // So go into no-settings mode for now, and fail later of the script doesn't support it. |
580 | if ( !defined( 'MW_CONFIG_CALLBACK' ) ) { |
581 | define( 'MW_CONFIG_CALLBACK', __CLASS__ . '::emulateConfig' ); |
582 | } |
583 | $this->withoutLocalSettings = true; |
584 | } |
585 | } |
586 | |
587 | /** |
588 | * @param SettingsBuilder $settings |
589 | * |
590 | * @internal Handler for MW_CONFIG_CALLBACK, used when no LocalSettings.php was found. |
591 | */ |
592 | public static function emulateConfig( SettingsBuilder $settings ) { |
593 | // NOTE: The config schema is already loaded at this point, so default values are known. |
594 | |
595 | $settings->overrideConfigValues( [ |
596 | // Server must be set, but we don't care to what |
597 | MainConfigNames::Server => 'https://unknown.invalid', |
598 | // If InvalidateCacheOnLocalSettingsChange is enabled, filemtime( MW_CONFIG_FILE ), |
599 | // which will produce a warning if there is no settings file. |
600 | MainConfigNames::InvalidateCacheOnLocalSettingsChange => false, |
601 | ] ); |
602 | } |
603 | |
604 | /** |
605 | * @param SettingsBuilder $settingsBuilder |
606 | * |
607 | * @return void |
608 | */ |
609 | private function overrideConfig( SettingsBuilder $settingsBuilder ) { |
610 | $config = $settingsBuilder->getConfig(); |
611 | |
612 | if ( $this->scriptObject->getDbType() === Maintenance::DB_NONE ) { |
613 | $cacheConf = $config->get( MainConfigNames::LocalisationCacheConf ); |
614 | if ( $cacheConf['storeClass'] === false |
615 | && ( $cacheConf['store'] == 'db' |
616 | || ( $cacheConf['store'] == 'detect' |
617 | && !$config->get( MainConfigNames::CacheDirectory ) ) ) |
618 | ) { |
619 | $cacheConf['storeClass'] = LCStoreNull::class; |
620 | $settingsBuilder->putConfigValue( MainConfigNames::LocalisationCacheConf, $cacheConf ); |
621 | } |
622 | } |
623 | |
624 | $output = $this->parameters->getOption( 'profiler' ); |
625 | if ( $output ) { |
626 | // Per-script profiling; useful for debugging |
627 | $profilerConf = $config->get( MainConfigNames::Profiler ); |
628 | if ( isset( $profilerConf['class'] ) ) { |
629 | $profilerConf = [ |
630 | 'sampling' => 1, |
631 | 'output' => [ $output ], |
632 | 'cliEnable' => true, |
633 | ] + $profilerConf; |
634 | // Override $wgProfiler. This is passed to Profiler::init() by Setup.php. |
635 | $settingsBuilder->putConfigValue( MainConfigNames::Profiler, $profilerConf ); |
636 | } |
637 | } |
638 | |
639 | $this->scriptObject->finalSetup( $settingsBuilder ); |
640 | } |
641 | |
642 | private function getServiceContainer(): MediaWikiServices { |
643 | return MediaWikiServices::getInstance(); |
644 | } |
645 | |
646 | /** |
647 | * Run the maintenance script. |
648 | * |
649 | * @note The process should exit immediately after this method returns. |
650 | * At that point, MediaWiki will already have been shut down. |
651 | * It is no longer safe to perform any write operations on the database. |
652 | * |
653 | * @note Any exceptions thrown by the maintenance script will cause this |
654 | * method to terminate the process after reporting the error to the user, |
655 | * without shutdown and cleanup. |
656 | * |
657 | * @return bool true on success, false on failure, |
658 | * passed through from Maintenance::execute(). |
659 | */ |
660 | public function run(): bool { |
661 | $config = $this->getConfig(); |
662 | |
663 | // Apply warning thresholds and output mode to Profiler. |
664 | // This MUST happen after Setup.php calls MaintenanceRunner::setup, |
665 | // $wgSettings->apply(), and Profiler::init(). Otherwise, calling |
666 | // Profiler::instance() would create a ProfilerStub even when $wgProfiler |
667 | // and --profiler are set. |
668 | $limits = $config->get( MainConfigNames::TrxProfilerLimits ); |
669 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
670 | $trxProfiler->setLogger( LoggerFactory::getInstance( 'rdbms' ) ); |
671 | $trxProfiler->setExpectations( $limits['Maintenance'], __METHOD__ ); |
672 | Profiler::instance()->setAllowOutput(); |
673 | |
674 | // Initialize main config instance |
675 | $this->scriptObject->setConfig( $config ); |
676 | |
677 | // Double check required extensions are installed |
678 | $this->scriptObject->checkRequiredExtensions(); |
679 | |
680 | if ( $this->withoutLocalSettings && !$this->scriptObject->canExecuteWithoutLocalSettings() ) { |
681 | $this->fatalError( |
682 | "\nThe LocalSettings.php file was not found or is not readable.\n" . |
683 | "Use --conf to specify an alternative config file.\n\n" |
684 | ); |
685 | } |
686 | |
687 | if ( $this->scriptObject->getDbType() == Maintenance::DB_NONE || $this->withoutLocalSettings ) { |
688 | // Be strict with maintenance tasks that claim to not need a database by |
689 | // disabling the storage backend. |
690 | MediaWikiServices::resetGlobalInstance( $config ); |
691 | MediaWikiServices::getInstance()->disableStorage(); |
692 | } |
693 | |
694 | $this->scriptObject->validateParamsAndArgs(); |
695 | |
696 | // Do the work |
697 | try { |
698 | $success = $this->scriptObject->execute() !== false; |
699 | |
700 | // Potentially debug globals |
701 | if ( $this->parameters->hasOption( 'globals' ) ) { |
702 | print_r( $GLOBALS ); |
703 | } |
704 | |
705 | $this->shutdown(); |
706 | |
707 | return $success; |
708 | } catch ( Exception $ex ) { |
709 | $exReportMessage = ''; |
710 | while ( $ex ) { |
711 | $cls = get_class( $ex ); |
712 | $exReportMessage .= "$cls from line {$ex->getLine()} of {$ex->getFile()}: {$ex->getMessage()}\n"; |
713 | $exReportMessage .= $ex->getTraceAsString() . "\n"; |
714 | $ex = $ex->getPrevious(); |
715 | } |
716 | $this->error( $exReportMessage ); |
717 | |
718 | // Exit now because process is in an unsafe state. |
719 | // Also to avoid DBTransactionError (T305730). |
720 | // Do not commit database writes, do not run deferreds, do not pass Go. |
721 | exit( 1 ); |
722 | } |
723 | } |
724 | |
725 | /** |
726 | * Output a message and terminate the current script. |
727 | * |
728 | * @param string $msg Error message |
729 | * @param int $exitCode PHP exit status. Should be in range 1-254. |
730 | * @return never |
731 | */ |
732 | protected function fatalError( $msg, $exitCode = 1 ) { |
733 | $this->error( $msg ); |
734 | exit( $exitCode ); |
735 | } |
736 | |
737 | /** |
738 | * @param string $msg |
739 | */ |
740 | protected function error( string $msg ) { |
741 | // Print to stderr if possible, don't mix it in with stdout output. |
742 | if ( defined( 'STDERR' ) ) { |
743 | fwrite( STDERR, $msg ); |
744 | } else { |
745 | echo $msg; |
746 | } |
747 | } |
748 | |
749 | /** |
750 | * Should we execute the maintenance script, or just allow it to be included |
751 | * as a standalone class? It checks that the call stack only includes this |
752 | * function and "requires" (meaning was called from the file scope) |
753 | * |
754 | * @return bool |
755 | */ |
756 | public static function shouldExecute() { |
757 | if ( !function_exists( 'debug_backtrace' ) ) { |
758 | // If someone has a better idea... |
759 | return MW_ENTRY_POINT === 'cli'; |
760 | } |
761 | |
762 | $bt = debug_backtrace(); |
763 | $count = count( $bt ); |
764 | if ( $bt[0]['class'] !== self::class || $bt[0]['function'] !== 'shouldExecute' ) { |
765 | return false; // last call should be to this function |
766 | } |
767 | $includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ]; |
768 | for ( $i = 1; $i < $count; $i++ ) { |
769 | if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) { |
770 | return false; // previous calls should all be "requires" |
771 | } |
772 | } |
773 | |
774 | return true; |
775 | } |
776 | |
777 | /** |
778 | * Handler for register_shutdown_function |
779 | * @internal |
780 | * @return void |
781 | */ |
782 | public function cleanup() { |
783 | if ( $this->scriptObject ) { |
784 | $this->scriptObject->cleanupChanneled(); |
785 | } |
786 | } |
787 | |
788 | /** |
789 | * Call before exiting CLI process for the last DB commit, and flush |
790 | * any remaining buffers and other deferred work. |
791 | * |
792 | * Equivalent to MediaWiki::restInPeace which handles shutdown for web requests, |
793 | * and should perform the same operations and in the same order. |
794 | * |
795 | * @since 1.41 |
796 | */ |
797 | private function shutdown() { |
798 | $lbFactory = null; |
799 | if ( |
800 | $this->scriptObject->getDbType() !== Maintenance::DB_NONE && |
801 | // Service might be disabled, e.g. when running install.php |
802 | !$this->getServiceContainer()->isServiceDisabled( 'DBLoadBalancerFactory' ) |
803 | ) { |
804 | $lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory(); |
805 | if ( $lbFactory->isReadyForRoundOperations() ) { |
806 | $lbFactory->commitPrimaryChanges( get_class( $this ) ); |
807 | } |
808 | |
809 | DeferredUpdates::doUpdates(); |
810 | } |
811 | |
812 | // Handle profiler outputs |
813 | // NOTE: MaintenanceRunner ensures Profiler::setAllowOutput() during setup |
814 | $profiler = Profiler::instance(); |
815 | $profiler->logData(); |
816 | $profiler->logDataPageOutputOnly(); |
817 | |
818 | MediaWiki::emitBufferedStatsdData( |
819 | $this->getServiceContainer()->getStatsdDataFactory(), |
820 | $this->getConfig() |
821 | ); |
822 | |
823 | if ( $lbFactory ) { |
824 | if ( $lbFactory->isReadyForRoundOperations() ) { |
825 | $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT ); |
826 | } |
827 | } |
828 | } |
829 | |
830 | } |