Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
24.41% |
72 / 295 |
|
16.13% |
5 / 31 |
CRAP | |
0.00% |
0 / 1 |
MaintenanceRunner | |
24.41% |
72 / 295 |
|
16.13% |
5 / 31 |
5336.78 | |
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 | |||
getMwInstallPath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 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 |
8.23 | |||
getScriptClass | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
6.99 | |||
findScriptClass | |
93.33% |
14 / 15 |
|
0.00% |
0 / 1 |
7.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 / 18 |
|
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 | /** |
303 | * @return string The value of the constant MW_INSTALL_PATH. This method mocked in unit tests. |
304 | */ |
305 | protected function getMwInstallPath(): string { |
306 | return MW_INSTALL_PATH; |
307 | } |
308 | |
309 | private function expandScriptFile( string $scriptName, ?array $extension ): string { |
310 | // Append ".php" if not present |
311 | $scriptFile = $scriptName; |
312 | if ( !str_ends_with( $scriptFile, '.php' ) ) { |
313 | $scriptFile .= '.php'; |
314 | } |
315 | |
316 | // If the path is not explicitly relative (starting with "./" or "../") and not absolute, |
317 | // then look in the maintenance dir. |
318 | if ( !preg_match( '!^\.\.?[/\\\\]!', $scriptFile ) && !self::isAbsolutePath( $scriptFile ) ) { |
319 | if ( $extension !== null ) { |
320 | // Look in the extension's maintenance dir |
321 | $scriptFile = dirname( $extension['path'] ) . "/maintenance/{$scriptFile}"; |
322 | } else { |
323 | // It's a core script. |
324 | $scriptFile = $this->getMwInstallPath() . "/maintenance/{$scriptFile}"; |
325 | } |
326 | } |
327 | |
328 | return $scriptFile; |
329 | } |
330 | |
331 | private function expandScriptClass( string $scriptName, ?array $extension ): string { |
332 | $scriptClass = $scriptName; |
333 | |
334 | // Support "$ext:$script" format |
335 | if ( $extension ) { |
336 | $scriptClass = "{$extension['namespace']}\\Maintenance\\$scriptClass"; |
337 | } |
338 | |
339 | // Accept dot (.) as namespace separators as well. |
340 | // Backslashes are just annoying on the command line. |
341 | $scriptClass = strtr( $scriptClass, '.', '\\' ); |
342 | |
343 | return $scriptClass; |
344 | } |
345 | |
346 | /** |
347 | * Preload the script file, so any defines in file level code are executed. |
348 | * This way, scripts can control what Setup.php does. |
349 | * |
350 | * @internal |
351 | * @param string $script |
352 | */ |
353 | protected function preloadScriptFile( string $script ): void { |
354 | if ( $this->scriptClass !== null && class_exists( $this->scriptClass ) ) { |
355 | // We know the script class, and file-level code was executed because class_exists triggers auto-loading. |
356 | return; |
357 | } |
358 | |
359 | [ $extName, $scriptName ] = $this->splitScript( $script ); |
360 | |
361 | if ( $extName !== null ) { |
362 | // Preloading is not supported. findScriptClass() will try to find the script later. |
363 | return; |
364 | } |
365 | |
366 | $scriptClass = $this->expandScriptClass( $scriptName, null ); |
367 | $scriptFile = $this->expandScriptFile( $scriptName, null ); |
368 | |
369 | if ( !class_exists( $scriptClass ) && file_exists( $scriptFile ) ) { |
370 | $scriptFileClass = $this->loadScriptFile( $scriptFile ); |
371 | if ( $scriptFileClass ) { |
372 | $scriptClass = $scriptFileClass; |
373 | } |
374 | } |
375 | |
376 | // NOTE: class_exists will trigger auto-loading, so file-level code in the script file will run. |
377 | if ( class_exists( $scriptClass ) ) { |
378 | // Set the script class name we found, so we don't try to load the file again! |
379 | $this->scriptClass = $scriptClass; |
380 | } |
381 | |
382 | // Preloading failed. Let findScriptClass() try to find the script later. |
383 | } |
384 | |
385 | /** |
386 | * @return class-string<Maintenance> |
387 | */ |
388 | protected function getScriptClass(): string { |
389 | if ( $this->scriptClass === null ) { |
390 | if ( $this->runFromWrapper ) { |
391 | $this->scriptClass = $this->findScriptClass( $this->script ); |
392 | } else { |
393 | $this->scriptClass = $this->script; |
394 | } |
395 | } |
396 | |
397 | if ( !class_exists( $this->scriptClass ) ) { |
398 | $this->fatalError( "Script class {$this->scriptClass} not found.\n" ); |
399 | } |
400 | |
401 | return $this->scriptClass; |
402 | } |
403 | |
404 | /** |
405 | * @internal |
406 | * @param string $script |
407 | * |
408 | * @return class-string<Maintenance> |
409 | */ |
410 | protected function findScriptClass( string $script ): string { |
411 | [ $extName, $scriptName ] = $this->splitScript( $script ); |
412 | |
413 | if ( $extName !== null ) { |
414 | $extension = $this->getExtensionInfo( $extName ); |
415 | |
416 | if ( !$extension ) { |
417 | $this->fatalError( "Extension '{$extName}' not found.\n" ); |
418 | } |
419 | } else { |
420 | $extension = null; |
421 | } |
422 | |
423 | $scriptClass = $this->expandScriptClass( $scriptName, $extension ); |
424 | $scriptFile = $this->expandScriptFile( $scriptName, $extension ); |
425 | |
426 | if ( !class_exists( $scriptClass ) && file_exists( $scriptFile ) ) { |
427 | $scriptFileClass = $this->loadScriptFile( $scriptFile ); |
428 | if ( $scriptFileClass ) { |
429 | $scriptClass = $scriptFileClass; |
430 | } |
431 | } |
432 | |
433 | if ( !class_exists( $scriptClass ) ) { |
434 | $this->fatalError( "Script '{$script}' not found (tried path '$scriptFile' and class '$scriptClass').\n" ); |
435 | } |
436 | |
437 | return $scriptClass; |
438 | } |
439 | |
440 | /** |
441 | * MW_FINAL_SETUP_CALLBACK handler, for setting up the Maintenance object. |
442 | * |
443 | * @param SettingsBuilder $settings |
444 | */ |
445 | public function setup( SettingsBuilder $settings ) { |
446 | // NOTE: this has to happen after the autoloader has been initialized. |
447 | $scriptClass = $this->getScriptClass(); |
448 | |
449 | $cls = new ReflectionClass( $scriptClass ); |
450 | if ( !$cls->isSubclassOf( Maintenance::class ) ) { |
451 | $this->fatalError( "Class {$this->script} is not a subclass of Maintenance.\n" ); |
452 | } |
453 | |
454 | // Initialize the actual Maintenance object |
455 | try { |
456 | $this->scriptObject = new $scriptClass; |
457 | $this->scriptObject->setName( $this->getName() ); |
458 | } catch ( Throwable $ex ) { |
459 | $this->fatalError( |
460 | "Failed to initialize Maintenance object.\n" . |
461 | "(Did you forget to call parent::__construct() in your maintenance script?)\n" . |
462 | "$ex\n" |
463 | ); |
464 | } |
465 | |
466 | if ( !$this->scriptObject instanceof Maintenance ) { |
467 | // This should never happen, we already checked if the class is a subclass of Maintenance! |
468 | throw new LogicException( 'Incompatible script object' ); |
469 | } |
470 | |
471 | // Inject runner stuff into the script's parameter definitions. |
472 | // This is mainly used when printing help. |
473 | $scriptParameters = $this->scriptObject->getParameters(); |
474 | |
475 | if ( $this->runFromWrapper ) { |
476 | $scriptParameters->setUsagePrefix( 'php ' . $this->parameters->getName() ); |
477 | } |
478 | |
479 | $scriptParameters->mergeOptions( $this->parameters ); |
480 | $this->parameters = $scriptParameters; |
481 | |
482 | // Ingest argv |
483 | $this->scriptObject->loadWithArgv( $this->scriptArgv ); |
484 | |
485 | // Basic checks and such |
486 | $this->scriptObject->setup(); |
487 | |
488 | // Set the memory limit |
489 | $this->adjustMemoryLimit(); |
490 | |
491 | // Override any config settings |
492 | $this->overrideConfig( $settings ); |
493 | } |
494 | |
495 | /** |
496 | * Returns the maintenance script name to show in the help message. |
497 | * |
498 | * @return string |
499 | */ |
500 | public function getName(): string { |
501 | // Once one of the init methods was called, getArg( 0 ) should always |
502 | // return something. |
503 | return $this->parameters->getArg( 0 ) ?? 'UNKNOWN'; |
504 | } |
505 | |
506 | /** |
507 | * Normally we disable the memory_limit when running admin scripts. |
508 | * Some scripts may wish to actually set a limit, however, to avoid |
509 | * blowing up unexpectedly. |
510 | * @see Maintenance::memoryLimit() |
511 | * @return string |
512 | */ |
513 | private function memoryLimit() { |
514 | if ( $this->parameters->hasOption( 'memory-limit' ) ) { |
515 | $limit = $this->parameters->getOption( 'memory-limit', 'max' ); |
516 | $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood |
517 | return $limit; |
518 | } |
519 | |
520 | $limit = $this->scriptObject->memoryLimit(); |
521 | return $limit ?: 'max'; |
522 | } |
523 | |
524 | /** |
525 | * Adjusts PHP's memory limit to better suit our needs, if needed. |
526 | */ |
527 | private function adjustMemoryLimit() { |
528 | $limit = $this->memoryLimit(); |
529 | if ( $limit == 'max' ) { |
530 | $limit = -1; // no memory limit |
531 | } |
532 | if ( $limit != 'default' ) { |
533 | ini_set( 'memory_limit', $limit ); |
534 | } |
535 | } |
536 | |
537 | /** |
538 | * Define how settings are loaded (e.g. LocalSettings.php) |
539 | * @note Called before Setup.php |
540 | * |
541 | * @internal |
542 | * @return void |
543 | */ |
544 | public function defineSettings() { |
545 | global $IP; |
546 | |
547 | if ( $this->parameters->hasOption( 'conf' ) ) { |
548 | // Define the constant instead of directly setting $settingsFile |
549 | // to ensure consistency. wfDetectLocalSettingsFile() will return |
550 | // MW_CONFIG_FILE if it is defined. |
551 | define( 'MW_CONFIG_FILE', $this->parameters->getOption( 'conf' ) ); |
552 | |
553 | if ( !is_readable( MW_CONFIG_FILE ) ) { |
554 | $this->fatalError( "\nConfig file " . MW_CONFIG_FILE . " was not found or is not readable.\n\n" ); |
555 | } |
556 | } |
557 | $settingsFile = wfDetectLocalSettingsFile( $IP ); |
558 | |
559 | if ( $this->parameters->hasOption( 'wiki' ) ) { |
560 | $wikiName = $this->parameters->getOption( 'wiki' ); |
561 | $bits = explode( '-', $wikiName, 2 ); |
562 | define( 'MW_DB', $bits[0] ); |
563 | define( 'MW_PREFIX', $bits[1] ?? '' ); |
564 | define( 'MW_WIKI_NAME', $wikiName ); |
565 | } elseif ( $this->parameters->hasOption( 'server' ) ) { |
566 | // Provide the option for site admins to detect and configure |
567 | // multiple wikis based on server names. This offers --server |
568 | // as alternative to --wiki. |
569 | // See https://www.mediawiki.org/wiki/Manual:Wiki_family |
570 | $_SERVER['SERVER_NAME'] = $this->parameters->getOption( 'server' ); |
571 | } |
572 | |
573 | // Try to load the script file before running Setup.php if possible. |
574 | // This allows the script file to define constants that change the behavior |
575 | // of Setup.php. |
576 | // Note that this will only work reliably for core scripts. |
577 | if ( $this->runFromWrapper ) { |
578 | $this->preloadScriptFile( $this->script ); |
579 | } |
580 | |
581 | if ( !is_readable( $settingsFile ) ) { |
582 | // NOTE: Some maintenance scripts can (and need to) run without LocalSettings. |
583 | // But we only know that once we have instantiated the Maintenance object. |
584 | // So go into no-settings mode for now, and fail later of the script doesn't support it. |
585 | if ( !defined( 'MW_CONFIG_CALLBACK' ) ) { |
586 | define( 'MW_CONFIG_CALLBACK', __CLASS__ . '::emulateConfig' ); |
587 | } |
588 | $this->withoutLocalSettings = true; |
589 | } |
590 | } |
591 | |
592 | /** |
593 | * @param SettingsBuilder $settings |
594 | * |
595 | * @internal Handler for MW_CONFIG_CALLBACK, used when no LocalSettings.php was found. |
596 | */ |
597 | public static function emulateConfig( SettingsBuilder $settings ) { |
598 | // NOTE: The config schema is already loaded at this point, so default values are known. |
599 | |
600 | $settings->overrideConfigValues( [ |
601 | // Server must be set, but we don't care to what |
602 | MainConfigNames::Server => 'https://unknown.invalid', |
603 | // If InvalidateCacheOnLocalSettingsChange is enabled, filemtime( MW_CONFIG_FILE ), |
604 | // which will produce a warning if there is no settings file. |
605 | MainConfigNames::InvalidateCacheOnLocalSettingsChange => false, |
606 | ] ); |
607 | } |
608 | |
609 | /** |
610 | * @param SettingsBuilder $settingsBuilder |
611 | * |
612 | * @return void |
613 | */ |
614 | private function overrideConfig( SettingsBuilder $settingsBuilder ) { |
615 | $config = $settingsBuilder->getConfig(); |
616 | |
617 | if ( $this->scriptObject->getDbType() === Maintenance::DB_NONE ) { |
618 | $cacheConf = $config->get( MainConfigNames::LocalisationCacheConf ); |
619 | if ( $cacheConf['storeClass'] === false |
620 | && ( $cacheConf['store'] == 'db' |
621 | || ( $cacheConf['store'] == 'detect' |
622 | && !$config->get( MainConfigNames::CacheDirectory ) ) ) |
623 | ) { |
624 | $cacheConf['storeClass'] = LCStoreNull::class; |
625 | $settingsBuilder->putConfigValue( MainConfigNames::LocalisationCacheConf, $cacheConf ); |
626 | } |
627 | } |
628 | |
629 | $output = $this->parameters->getOption( 'profiler' ); |
630 | if ( $output ) { |
631 | // Per-script profiling; useful for debugging |
632 | $profilerConf = $config->get( MainConfigNames::Profiler ); |
633 | if ( isset( $profilerConf['class'] ) ) { |
634 | $profilerConf = [ |
635 | 'sampling' => 1, |
636 | 'output' => [ $output ], |
637 | 'cliEnable' => true, |
638 | ] + $profilerConf; |
639 | // Override $wgProfiler. This is passed to Profiler::init() by Setup.php. |
640 | $settingsBuilder->putConfigValue( MainConfigNames::Profiler, $profilerConf ); |
641 | } |
642 | } |
643 | |
644 | $this->scriptObject->finalSetup( $settingsBuilder ); |
645 | } |
646 | |
647 | private function getServiceContainer(): MediaWikiServices { |
648 | return MediaWikiServices::getInstance(); |
649 | } |
650 | |
651 | /** |
652 | * Run the maintenance script. |
653 | * |
654 | * @note The process should exit immediately after this method returns. |
655 | * At that point, MediaWiki will already have been shut down. |
656 | * It is no longer safe to perform any write operations on the database. |
657 | * |
658 | * @note Any exceptions thrown by the maintenance script will cause this |
659 | * method to terminate the process after reporting the error to the user, |
660 | * without shutdown and cleanup. |
661 | * |
662 | * @return bool true on success, false on failure, |
663 | * passed through from Maintenance::execute(). |
664 | */ |
665 | public function run(): bool { |
666 | $config = $this->getConfig(); |
667 | |
668 | // Apply warning thresholds and output mode to Profiler. |
669 | // This MUST happen after Setup.php calls MaintenanceRunner::setup, |
670 | // $wgSettings->apply(), and Profiler::init(). Otherwise, calling |
671 | // Profiler::instance() would create a ProfilerStub even when $wgProfiler |
672 | // and --profiler are set. |
673 | $limits = $config->get( MainConfigNames::TrxProfilerLimits ); |
674 | $trxProfiler = Profiler::instance()->getTransactionProfiler(); |
675 | $trxProfiler->setLogger( LoggerFactory::getInstance( 'rdbms' ) ); |
676 | $trxProfiler->setExpectations( $limits['Maintenance'], __METHOD__ ); |
677 | Profiler::instance()->setAllowOutput(); |
678 | |
679 | // Initialize main config instance |
680 | $this->scriptObject->setConfig( $config ); |
681 | |
682 | // Double check required extensions are installed |
683 | $this->scriptObject->checkRequiredExtensions(); |
684 | |
685 | if ( $this->withoutLocalSettings && !$this->scriptObject->canExecuteWithoutLocalSettings() ) { |
686 | $this->fatalError( |
687 | "\nThe LocalSettings.php file was not found or is not readable.\n" . |
688 | "Use --conf to specify an alternative config file.\n\n" |
689 | ); |
690 | } |
691 | |
692 | if ( $this->scriptObject->getDbType() == Maintenance::DB_NONE || $this->withoutLocalSettings ) { |
693 | // Be strict with maintenance tasks that claim to not need a database by |
694 | // disabling the storage backend. |
695 | MediaWikiServices::resetGlobalInstance( $config ); |
696 | MediaWikiServices::getInstance()->disableStorage(); |
697 | } |
698 | |
699 | $this->scriptObject->validateParamsAndArgs(); |
700 | |
701 | // Do the work |
702 | try { |
703 | $success = $this->scriptObject->execute() !== false; |
704 | |
705 | // Potentially debug globals |
706 | if ( $this->parameters->hasOption( 'globals' ) ) { |
707 | print_r( $GLOBALS ); |
708 | } |
709 | |
710 | $this->shutdown(); |
711 | |
712 | return $success; |
713 | } catch ( Exception $ex ) { |
714 | $exReportMessage = ''; |
715 | while ( $ex ) { |
716 | $cls = get_class( $ex ); |
717 | $exReportMessage .= "$cls from line {$ex->getLine()} of {$ex->getFile()}: {$ex->getMessage()}\n"; |
718 | $exReportMessage .= $ex->getTraceAsString() . "\n"; |
719 | $ex = $ex->getPrevious(); |
720 | } |
721 | $this->error( $exReportMessage ); |
722 | |
723 | // Exit now because process is in an unsafe state. |
724 | // Also to avoid DBTransactionError (T305730). |
725 | // Do not commit database writes, do not run deferreds, do not pass Go. |
726 | exit( 1 ); |
727 | } |
728 | } |
729 | |
730 | /** |
731 | * Output a message and terminate the current script. |
732 | * |
733 | * @param string $msg Error message |
734 | * @param int $exitCode PHP exit status. Should be in range 1-254. |
735 | * @return never |
736 | */ |
737 | protected function fatalError( $msg, $exitCode = 1 ) { |
738 | $this->error( $msg ); |
739 | exit( $exitCode ); |
740 | } |
741 | |
742 | /** |
743 | * @param string $msg |
744 | */ |
745 | protected function error( string $msg ) { |
746 | // Print to stderr if possible, don't mix it in with stdout output. |
747 | if ( defined( 'STDERR' ) ) { |
748 | fwrite( STDERR, $msg ); |
749 | } else { |
750 | echo $msg; |
751 | } |
752 | } |
753 | |
754 | /** |
755 | * Should we execute the maintenance script, or just allow it to be included |
756 | * as a standalone class? It checks that the call stack only includes this |
757 | * function and "requires" (meaning was called from the file scope) |
758 | * |
759 | * @return bool |
760 | */ |
761 | public static function shouldExecute() { |
762 | if ( !function_exists( 'debug_backtrace' ) ) { |
763 | // If someone has a better idea... |
764 | return MW_ENTRY_POINT === 'cli'; |
765 | } |
766 | |
767 | $bt = debug_backtrace(); |
768 | $count = count( $bt ); |
769 | if ( $bt[0]['class'] !== self::class || $bt[0]['function'] !== 'shouldExecute' ) { |
770 | return false; // last call should be to this function |
771 | } |
772 | $includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ]; |
773 | for ( $i = 1; $i < $count; $i++ ) { |
774 | if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) { |
775 | return false; // previous calls should all be "requires" |
776 | } |
777 | } |
778 | |
779 | return true; |
780 | } |
781 | |
782 | /** |
783 | * Handler for register_shutdown_function |
784 | * @internal |
785 | * @return void |
786 | */ |
787 | public function cleanup() { |
788 | if ( $this->scriptObject ) { |
789 | $this->scriptObject->cleanupChanneled(); |
790 | } |
791 | } |
792 | |
793 | /** |
794 | * Call before exiting CLI process for the last DB commit, and flush |
795 | * any remaining buffers and other deferred work. |
796 | * |
797 | * Equivalent to MediaWiki::restInPeace which handles shutdown for web requests, |
798 | * and should perform the same operations and in the same order. |
799 | * |
800 | * @since 1.41 |
801 | */ |
802 | private function shutdown() { |
803 | $lbFactory = null; |
804 | if ( |
805 | $this->scriptObject->getDbType() !== Maintenance::DB_NONE && |
806 | // Service might be disabled, e.g. when running install.php |
807 | !$this->getServiceContainer()->isServiceDisabled( 'DBLoadBalancerFactory' ) |
808 | ) { |
809 | $lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory(); |
810 | if ( $lbFactory->isReadyForRoundOperations() ) { |
811 | $lbFactory->commitPrimaryChanges( get_class( $this ) ); |
812 | } |
813 | |
814 | DeferredUpdates::doUpdates(); |
815 | } |
816 | |
817 | // Handle profiler outputs |
818 | // NOTE: MaintenanceRunner ensures Profiler::setAllowOutput() during setup |
819 | $profiler = Profiler::instance(); |
820 | $profiler->logData(); |
821 | $profiler->logDataPageOutputOnly(); |
822 | |
823 | MediaWiki::emitBufferedStats( |
824 | $this->getServiceContainer()->getStatsFactory(), |
825 | $this->getServiceContainer()->getStatsdDataFactory(), |
826 | $this->getConfig() |
827 | ); |
828 | |
829 | if ( $lbFactory ) { |
830 | if ( $lbFactory->isReadyForRoundOperations() ) { |
831 | $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT ); |
832 | } |
833 | } |
834 | } |
835 | |
836 | } |