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