Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
19.66% |
69 / 351 |
|
21.21% |
14 / 66 |
CRAP | |
0.00% |
0 / 1 |
Maintenance | |
19.71% |
69 / 350 |
|
21.21% |
14 / 66 |
13575.26 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
getParameters | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
canExecuteWithoutLocalSettings | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
supportsOption | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addOption | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
hasOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addArg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
deleteOption | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setAllowUnregisteredOptions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
addDescription | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hasArg | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getArg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getArgs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setArg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBatchSize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setBatchSize | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStdin | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
isQuiet | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
output | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
5.15 | |||
error | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
fatalError | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
cleanupChanneled | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
outputChanneled | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
getDbType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addDefaultParams | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
getConfig | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getServiceContainer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setConfig | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
requireExtension | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkRequiredExtensions | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
runChild | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
setup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
memoryLimit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
clearParamsAndArgs | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setName | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
loadWithArgv | |
55.56% |
5 / 9 |
|
0.00% |
0 / 1 |
3.79 | |||
loadParamsAndArgs | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
validateParamsAndArgs | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
loadSpecialVars | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
8.12 | |||
maybeHelp | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
30 | |||
showHelp | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
finalSetup | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
240 | |||
afterFinalSetup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
purgeRedundantText | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
42 | |||
getDir | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDB | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
setDB | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getReplicaDB | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getPrimaryDB | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setDBProvider | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
beginTransaction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
commitTransaction | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
waitForReplication | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
rollbackTransaction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
countDown | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
posix_isatty | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
readconsole | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
56 | |||
readlineEmulation | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
getTermSize | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
requireTestsAutoloader | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHookContainer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getHookRunner | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
parseIntList | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
validateUserOption | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use MediaWiki\Config\Config; |
22 | use MediaWiki\HookContainer\HookContainer; |
23 | use MediaWiki\HookContainer\HookRunner; |
24 | use MediaWiki\MainConfigNames; |
25 | use MediaWiki\Maintenance\MaintenanceParameters; |
26 | use MediaWiki\MediaWikiServices; |
27 | use MediaWiki\Settings\SettingsBuilder; |
28 | use MediaWiki\Shell\Shell; |
29 | use MediaWiki\User\User; |
30 | use Wikimedia\Rdbms\IConnectionProvider; |
31 | use Wikimedia\Rdbms\IDatabase; |
32 | use Wikimedia\Rdbms\IMaintainableDatabase; |
33 | use Wikimedia\Rdbms\IReadableDatabase; |
34 | |
35 | // NOTE: MaintenanceParameters is needed in the constructor, and we may not have |
36 | // autoloading enabled at this point? |
37 | require_once __DIR__ . '/MaintenanceParameters.php'; |
38 | |
39 | /** |
40 | * Abstract maintenance class for quickly writing and churning out |
41 | * maintenance scripts with minimal effort. All that _must_ be defined |
42 | * is the execute() method. See docs/maintenance.txt for more info |
43 | * and a quick demo of how to use it. |
44 | * |
45 | * Terminology: |
46 | * params: registry of named values that may be passed to the script |
47 | * arg list: registry of positional values that may be passed to the script |
48 | * options: passed param values |
49 | * args: passed positional values |
50 | * |
51 | * In the command: |
52 | * mwscript somescript.php --foo=bar baz |
53 | * foo is a param |
54 | * bar is the option value of the option for param foo |
55 | * baz is the arg value at index 0 in the arg list |
56 | * |
57 | * WARNING: the constructor, MaintenanceRunner::shouldExecute(), setup(), finalSetup(), |
58 | * and getName() are called before Setup.php is complete, which means most of the common |
59 | * infrastructure, like logging or autoloading, is not available. Be careful when changing |
60 | * these methods or the ones called from them. Likewise, be careful with the constructor |
61 | * when subclassing. MediaWikiServices instance is not yet available at this point. |
62 | * |
63 | * @stable to extend |
64 | * |
65 | * @since 1.16 |
66 | * @ingroup Maintenance |
67 | */ |
68 | abstract class Maintenance { |
69 | /** |
70 | * Constants for DB access type |
71 | * @see Maintenance::getDbType() |
72 | */ |
73 | public const DB_NONE = 0; |
74 | public const DB_STD = 1; |
75 | public const DB_ADMIN = 2; |
76 | |
77 | // Const for getStdin() |
78 | public const STDIN_ALL = -1; |
79 | |
80 | // Help group names |
81 | public const SCRIPT_DEPENDENT_PARAMETERS = 'Common options'; |
82 | public const GENERIC_MAINTENANCE_PARAMETERS = 'Script runner options'; |
83 | |
84 | /** |
85 | * @var MaintenanceParameters |
86 | */ |
87 | protected $parameters; |
88 | |
89 | /** |
90 | * Empty. |
91 | * @deprecated since 1.39, use $this->parameters instead. |
92 | * @var array[] |
93 | * @phan-var array<string,array{desc:string,require:bool,withArg:string,shortName:string,multiOccurrence:bool}> |
94 | */ |
95 | protected $mParams = []; |
96 | |
97 | /** |
98 | * @var array This is the list of options that were actually passed |
99 | * @deprecated since 1.39, use $this->parameters instead. |
100 | */ |
101 | protected $mOptions = []; |
102 | |
103 | /** |
104 | * @var array This is the list of arguments that were actually passed |
105 | * @deprecated since 1.39, use $this->parameters instead. |
106 | */ |
107 | protected $mArgs = []; |
108 | |
109 | /** @var string|null Name of the script currently running */ |
110 | protected $mSelf; |
111 | |
112 | /** @var bool Special vars for params that are always used */ |
113 | protected $mQuiet = false; |
114 | protected ?string $mDbUser = null; |
115 | protected ?string $mDbPass = null; |
116 | |
117 | /** |
118 | * @var string A description of the script, children should change this via addDescription() |
119 | * @deprecated since 1.39, use $this->parameters instead. |
120 | */ |
121 | protected $mDescription = ''; |
122 | |
123 | /** |
124 | * @var bool Have we already loaded our user input? |
125 | * @deprecated since 1.39, treat as private to the Maintenance base class |
126 | */ |
127 | protected $mInputLoaded = false; |
128 | |
129 | /** |
130 | * Batch size. If a script supports this, they should set |
131 | * a default with setBatchSize() |
132 | * |
133 | * @var int|null |
134 | */ |
135 | protected $mBatchSize = null; |
136 | |
137 | /** |
138 | * Used by getDB() / setDB() |
139 | * @var IMaintainableDatabase|null |
140 | */ |
141 | private $mDb = null; |
142 | |
143 | /** @var float UNIX timestamp */ |
144 | private $lastReplicationWait = 0.0; |
145 | |
146 | /** |
147 | * Used when creating separate schema files. |
148 | * @var resource|null |
149 | */ |
150 | public $fileHandle; |
151 | |
152 | /** @var HookContainer|null */ |
153 | private $hookContainer; |
154 | |
155 | /** @var HookRunner|null */ |
156 | private $hookRunner; |
157 | |
158 | /** |
159 | * Accessible via getConfig() |
160 | * |
161 | * @var Config|null |
162 | */ |
163 | private $config; |
164 | |
165 | /** |
166 | * @see Maintenance::requireExtension |
167 | * @var array |
168 | */ |
169 | private $requiredExtensions = []; |
170 | |
171 | /** |
172 | * Used to read the options in the order they were passed. |
173 | * Useful for option chaining (Ex. dumpBackup.php). It will |
174 | * be an empty array if the options are passed in through |
175 | * loadParamsAndArgs( $self, $opts, $args ). |
176 | * |
177 | * This is an array of arrays where |
178 | * 0 => the option and 1 => parameter value. |
179 | * |
180 | * @deprecated since 1.39, use $this->parameters instead. |
181 | * @var array |
182 | */ |
183 | public $orderedOptions = []; |
184 | private ?IConnectionProvider $dbProvider = null; |
185 | |
186 | /** |
187 | * Default constructor. Children should call this *first* if implementing |
188 | * their own constructors |
189 | * |
190 | * @stable to call |
191 | */ |
192 | public function __construct() { |
193 | $this->parameters = new MaintenanceParameters(); |
194 | $this->mOptions =& $this->parameters->getFieldReference( 'mOptions' ); |
195 | $this->orderedOptions =& $this->parameters->getFieldReference( 'optionsSequence' ); |
196 | $this->mArgs =& $this->parameters->getFieldReference( 'mArgs' ); |
197 | $this->addDefaultParams(); |
198 | } |
199 | |
200 | /** |
201 | * @since 1.39 |
202 | * @return MaintenanceParameters |
203 | */ |
204 | public function getParameters() { |
205 | return $this->parameters; |
206 | } |
207 | |
208 | /** |
209 | * Do the actual work. All child classes will need to implement this |
210 | * |
211 | * @return bool|null|void True for success, false for failure. Not returning |
212 | * a value, or returning null, is also interpreted as success. Returning |
213 | * false for failure will cause doMaintenance.php to exit the process |
214 | * with a non-zero exit status. |
215 | */ |
216 | abstract public function execute(); |
217 | |
218 | /** |
219 | * Whether this script can run without LocalSettings.php. Scripts that need to be able |
220 | * to run when MediaWiki has not been installed should override this to return true. |
221 | * Scripts that return true from this method must be able to function without |
222 | * a storage backend. When no LocalSettings.php file is present, any attempt to access |
223 | * the database will fail with a fatal error. |
224 | * |
225 | * @note Subclasses that override this method to return true should also override |
226 | * getDbType() to return self::DB_NONE, unless they are going to use the database |
227 | * connection when it is available. |
228 | * |
229 | * @see getDbType() |
230 | * @since 1.40 |
231 | * @stable to override |
232 | * @return bool |
233 | */ |
234 | public function canExecuteWithoutLocalSettings(): bool { |
235 | return false; |
236 | } |
237 | |
238 | /** |
239 | * Checks to see if a particular option in supported. Normally this means it |
240 | * has been registered by the script via addOption. |
241 | * @param string $name The name of the option |
242 | * @return bool true if the option exists, false otherwise |
243 | */ |
244 | protected function supportsOption( $name ) { |
245 | return $this->parameters->supportsOption( $name ); |
246 | } |
247 | |
248 | /** |
249 | * Add a parameter to the script. Will be displayed on --help |
250 | * with the associated description |
251 | * |
252 | * @param string $name The name of the param (help, version, etc) |
253 | * @param string $description The description of the param to show on --help |
254 | * @param bool $required Is the param required? |
255 | * @param bool $withArg Is an argument required with this option? |
256 | * @param string|bool $shortName Character to use as short name |
257 | * @param bool $multiOccurrence Can this option be passed multiple times? |
258 | */ |
259 | protected function addOption( $name, $description, $required = false, |
260 | $withArg = false, $shortName = false, $multiOccurrence = false |
261 | ) { |
262 | $this->parameters->addOption( |
263 | $name, |
264 | $description, |
265 | $required, |
266 | $withArg, |
267 | $shortName, |
268 | $multiOccurrence |
269 | ); |
270 | } |
271 | |
272 | /** |
273 | * Checks to see if a particular option was set. |
274 | * |
275 | * @param string $name The name of the option |
276 | * @return bool |
277 | */ |
278 | protected function hasOption( $name ) { |
279 | return $this->parameters->hasOption( $name ); |
280 | } |
281 | |
282 | /** |
283 | * Get an option, or return the default. |
284 | * |
285 | * If the option was added to support multiple occurrences, |
286 | * this will return an array. |
287 | * |
288 | * @param string $name The name of the param |
289 | * @param mixed|null $default Anything you want, default null |
290 | * @return mixed |
291 | * @return-taint none |
292 | */ |
293 | protected function getOption( $name, $default = null ) { |
294 | return $this->parameters->getOption( $name, $default ); |
295 | } |
296 | |
297 | /** |
298 | * Add some args that are needed |
299 | * @param string $arg Name of the arg, like 'start' |
300 | * @param string $description Short description of the arg |
301 | * @param bool $required Is this required? |
302 | * @param bool $multi Does it allow multiple values? (Last arg only) |
303 | */ |
304 | protected function addArg( $arg, $description, $required = true, $multi = false ) { |
305 | $this->parameters->addArg( $arg, $description, $required, $multi ); |
306 | } |
307 | |
308 | /** |
309 | * Remove an option. Useful for removing options that won't be used in your script. |
310 | * @param string $name The option to remove. |
311 | */ |
312 | protected function deleteOption( $name ) { |
313 | $this->parameters->deleteOption( $name ); |
314 | } |
315 | |
316 | /** |
317 | * Sets whether to allow unregistered options, which are options passed to |
318 | * a script that do not match an expected parameter. |
319 | * @param bool $allow Should we allow? |
320 | */ |
321 | protected function setAllowUnregisteredOptions( $allow ) { |
322 | $this->parameters->setAllowUnregisteredOptions( $allow ); |
323 | } |
324 | |
325 | /** |
326 | * Set the description text. |
327 | * @param string $text The text of the description |
328 | */ |
329 | protected function addDescription( $text ) { |
330 | $this->parameters->setDescription( $text ); |
331 | } |
332 | |
333 | /** |
334 | * Does a given argument exist? |
335 | * @param int|string $argId The index (from zero) of the argument, or |
336 | * the name declared for the argument by addArg(). |
337 | * @return bool |
338 | */ |
339 | protected function hasArg( $argId = 0 ) { |
340 | return $this->parameters->hasArg( $argId ); |
341 | } |
342 | |
343 | /** |
344 | * Get an argument. |
345 | * @param int|string $argId The index (from zero) of the argument, or |
346 | * the name declared for the argument by addArg(). |
347 | * @param mixed|null $default The default if it doesn't exist |
348 | * @return mixed |
349 | * @return-taint none |
350 | */ |
351 | protected function getArg( $argId = 0, $default = null ) { |
352 | return $this->parameters->getArg( $argId, $default ); |
353 | } |
354 | |
355 | /** |
356 | * Get arguments. |
357 | * @since 1.40 |
358 | * |
359 | * @param int|string $offset The index (from zero) of the first argument, or |
360 | * the name declared for the argument by addArg(). |
361 | * @return string[] |
362 | */ |
363 | protected function getArgs( $offset = 0 ) { |
364 | return $this->parameters->getArgs( $offset ); |
365 | } |
366 | |
367 | /** |
368 | * Programmatically set the value of the given option. |
369 | * Useful for setting up child scripts, see runChild(). |
370 | * |
371 | * @since 1.39 |
372 | * |
373 | * @param string $name |
374 | * @param mixed|null $value |
375 | */ |
376 | public function setOption( string $name, $value ): void { |
377 | $this->parameters->setOption( $name, $value ); |
378 | } |
379 | |
380 | /** |
381 | * Programmatically set the value of the given argument. |
382 | * Useful for setting up child scripts, see runChild(). |
383 | * |
384 | * @since 1.39 |
385 | * |
386 | * @param string|int $argId Arg index or name |
387 | * @param mixed|null $value |
388 | */ |
389 | public function setArg( $argId, $value ): void { |
390 | $this->parameters->setArg( $argId, $value ); |
391 | } |
392 | |
393 | /** |
394 | * Returns batch size |
395 | * |
396 | * @since 1.31 |
397 | * |
398 | * @return int|null |
399 | */ |
400 | protected function getBatchSize() { |
401 | return $this->mBatchSize; |
402 | } |
403 | |
404 | /** |
405 | * @param int $s The number of operations to do in a batch |
406 | */ |
407 | protected function setBatchSize( $s = 0 ) { |
408 | $this->mBatchSize = $s; |
409 | |
410 | // If we support $mBatchSize, show the option. |
411 | // Used to be in addDefaultParams, but in order for that to |
412 | // work, subclasses would have to call this function in the constructor |
413 | // before they called parent::__construct which is just weird |
414 | // (and really wasn't done). |
415 | if ( $this->mBatchSize ) { |
416 | $this->addOption( 'batch-size', 'Run this many operations ' . |
417 | 'per batch, default: ' . $this->mBatchSize, false, true ); |
418 | if ( $this->supportsOption( 'batch-size' ) ) { |
419 | // This seems a little ugly... |
420 | $this->parameters->assignGroup( self::SCRIPT_DEPENDENT_PARAMETERS, [ 'batch-size' ] ); |
421 | } |
422 | } |
423 | } |
424 | |
425 | /** |
426 | * Get the script's name |
427 | * @return string |
428 | */ |
429 | public function getName() { |
430 | return $this->mSelf; |
431 | } |
432 | |
433 | /** |
434 | * Return input from stdin. |
435 | * @param int|null $len The number of bytes to read. If null, just |
436 | * return the handle. Maintenance::STDIN_ALL returns the full content |
437 | * @return mixed |
438 | */ |
439 | protected function getStdin( $len = null ) { |
440 | if ( $len == self::STDIN_ALL ) { |
441 | return file_get_contents( 'php://stdin' ); |
442 | } |
443 | $f = fopen( 'php://stdin', 'rt' ); |
444 | if ( !$len ) { |
445 | return $f; |
446 | } |
447 | $input = fgets( $f, $len ); |
448 | fclose( $f ); |
449 | |
450 | return rtrim( $input ); |
451 | } |
452 | |
453 | /** |
454 | * @return bool |
455 | */ |
456 | public function isQuiet() { |
457 | return $this->mQuiet; |
458 | } |
459 | |
460 | /** |
461 | * Throw some output to the user. Scripts can call this with no fears, |
462 | * as we handle all --quiet stuff here |
463 | * @stable to override |
464 | * @param string $out The text to show to the user |
465 | * @param mixed|null $channel Unique identifier for the channel. See function outputChanneled. |
466 | */ |
467 | protected function output( $out, $channel = null ) { |
468 | // This is sometimes called very early, before Setup.php is included. |
469 | if ( defined( 'MW_SERVICE_BOOTSTRAP_COMPLETE' ) ) { |
470 | // Flush stats periodically in long-running CLI scripts to avoid OOM (T181385) |
471 | $stats = $this->getServiceContainer()->getStatsdDataFactory(); |
472 | if ( $stats->getDataCount() > 1000 ) { |
473 | MediaWiki::emitBufferedStatsdData( $stats, $this->getConfig() ); |
474 | } |
475 | } |
476 | |
477 | if ( $this->mQuiet ) { |
478 | return; |
479 | } |
480 | if ( $channel === null ) { |
481 | $this->cleanupChanneled(); |
482 | print $out; |
483 | } else { |
484 | $out = preg_replace( '/\n\z/', '', $out ); |
485 | $this->outputChanneled( $out, $channel ); |
486 | } |
487 | } |
488 | |
489 | /** |
490 | * Throw an error to the user. Doesn't respect --quiet, so don't use |
491 | * this for non-error output |
492 | * @stable to override |
493 | * @param string $err The error to display |
494 | * @param int $die Deprecated since 1.31, use Maintenance::fatalError() instead |
495 | */ |
496 | protected function error( $err, $die = 0 ) { |
497 | if ( intval( $die ) !== 0 ) { |
498 | wfDeprecated( __METHOD__ . '( $err, $die )', '1.31' ); |
499 | $this->fatalError( $err, intval( $die ) ); |
500 | } |
501 | $this->outputChanneled( false ); |
502 | if ( |
503 | ( PHP_SAPI == 'cli' || PHP_SAPI == 'phpdbg' ) && |
504 | !defined( 'MW_PHPUNIT_TEST' ) |
505 | ) { |
506 | fwrite( STDERR, $err . "\n" ); |
507 | } else { |
508 | print $err; |
509 | } |
510 | } |
511 | |
512 | /** |
513 | * Output a message and terminate the current script. |
514 | * |
515 | * @stable to override |
516 | * @param string $msg Error message |
517 | * @param int $exitCode PHP exit status. Should be in range 1-254. |
518 | * @since 1.31 |
519 | * @return never |
520 | */ |
521 | protected function fatalError( $msg, $exitCode = 1 ) { |
522 | $this->error( $msg ); |
523 | exit( $exitCode ); |
524 | } |
525 | |
526 | private $atLineStart = true; |
527 | private $lastChannel = null; |
528 | |
529 | /** |
530 | * Clean up channeled output. Output a newline if necessary. |
531 | */ |
532 | public function cleanupChanneled() { |
533 | if ( !$this->atLineStart ) { |
534 | print "\n"; |
535 | $this->atLineStart = true; |
536 | } |
537 | } |
538 | |
539 | /** |
540 | * Message outputter with channeled message support. Messages on the |
541 | * same channel are concatenated, but any intervening messages in another |
542 | * channel start a new line. |
543 | * @param string|false $msg The message without trailing newline |
544 | * @param string|null $channel Channel identifier or null for no |
545 | * channel. Channel comparison uses ===. |
546 | */ |
547 | public function outputChanneled( $msg, $channel = null ) { |
548 | if ( $msg === false ) { |
549 | $this->cleanupChanneled(); |
550 | |
551 | return; |
552 | } |
553 | |
554 | // End the current line if necessary |
555 | if ( !$this->atLineStart && $channel !== $this->lastChannel ) { |
556 | print "\n"; |
557 | } |
558 | |
559 | print $msg; |
560 | |
561 | $this->atLineStart = false; |
562 | if ( $channel === null ) { |
563 | // For unchanneled messages, output trailing newline immediately |
564 | print "\n"; |
565 | $this->atLineStart = true; |
566 | } |
567 | $this->lastChannel = $channel; |
568 | } |
569 | |
570 | /** |
571 | * Does the script need different DB access? By default, we give Maintenance |
572 | * scripts normal rights to the DB. Sometimes, a script needs admin rights |
573 | * access for a reason and sometimes they want no access. Subclasses should |
574 | * override and return one of the following values, as needed: |
575 | * Maintenance::DB_NONE - For no DB access at all |
576 | * Maintenance::DB_STD - For normal DB access, default |
577 | * Maintenance::DB_ADMIN - For admin DB access |
578 | * |
579 | * @note Subclasses that override this method to return self::DB_NONE should |
580 | * also override canExecuteWithoutLocalSettings() to return true, unless they |
581 | * need the wiki to be set up for reasons beyond access to a database connection. |
582 | * |
583 | * @see canExecuteWithoutLocalSettings() |
584 | * @stable to override |
585 | * @return int |
586 | */ |
587 | public function getDbType() { |
588 | return self::DB_STD; |
589 | } |
590 | |
591 | /** |
592 | * Add the default parameters to the scripts |
593 | */ |
594 | protected function addDefaultParams() { |
595 | # Generic (non-script-dependent) options: |
596 | |
597 | $this->addOption( 'help', 'Display this help message', false, false, 'h' ); |
598 | $this->addOption( 'quiet', 'Whether to suppress non-error output', false, false, 'q' ); |
599 | |
600 | # Save generic options to display them separately in help |
601 | $generic = [ 'help', 'quiet' ]; |
602 | $this->parameters->assignGroup( self::GENERIC_MAINTENANCE_PARAMETERS, $generic ); |
603 | |
604 | # Script-dependent options: |
605 | |
606 | // If we support a DB, show the options |
607 | if ( $this->getDbType() > 0 ) { |
608 | $this->addOption( 'dbuser', 'The DB user to use for this script', false, true ); |
609 | $this->addOption( 'dbpass', 'The password to use for this script', false, true ); |
610 | $this->addOption( 'dbgroupdefault', 'The default DB group to use.', false, true ); |
611 | } |
612 | |
613 | # Save additional script-dependent options to display |
614 | # them separately in help |
615 | $dependent = array_diff( |
616 | $this->parameters->getOptionNames(), |
617 | $generic |
618 | ); |
619 | $this->parameters->assignGroup( self::SCRIPT_DEPENDENT_PARAMETERS, $dependent ); |
620 | } |
621 | |
622 | /** |
623 | * @since 1.24 |
624 | * @stable to override |
625 | * @return Config |
626 | */ |
627 | public function getConfig() { |
628 | if ( $this->config === null ) { |
629 | $this->config = $this->getServiceContainer()->getMainConfig(); |
630 | } |
631 | |
632 | return $this->config; |
633 | } |
634 | |
635 | /** |
636 | * Returns the main service container. |
637 | * |
638 | * @since 1.40 |
639 | * @return MediaWikiServices |
640 | */ |
641 | protected function getServiceContainer() { |
642 | return MediaWikiServices::getInstance(); |
643 | } |
644 | |
645 | /** |
646 | * @since 1.24 |
647 | * @param Config $config |
648 | */ |
649 | public function setConfig( Config $config ) { |
650 | $this->config = $config; |
651 | } |
652 | |
653 | /** |
654 | * Indicate that the specified extension must be |
655 | * loaded before the script can run. |
656 | * |
657 | * This *must* be called in the constructor. |
658 | * |
659 | * @since 1.28 |
660 | * @param string $name |
661 | */ |
662 | protected function requireExtension( $name ) { |
663 | $this->requiredExtensions[] = $name; |
664 | } |
665 | |
666 | /** |
667 | * Verify that the required extensions are installed |
668 | * |
669 | * @since 1.28 |
670 | */ |
671 | public function checkRequiredExtensions() { |
672 | $registry = ExtensionRegistry::getInstance(); |
673 | $missing = []; |
674 | foreach ( $this->requiredExtensions as $name ) { |
675 | if ( !$registry->isLoaded( $name ) ) { |
676 | $missing[] = $name; |
677 | } |
678 | } |
679 | |
680 | if ( $missing ) { |
681 | if ( count( $missing ) === 1 ) { |
682 | $msg = 'The "' . $missing[ 0 ] . '" extension must be installed for this script to run. ' |
683 | . 'Please enable it and then try again.'; |
684 | } else { |
685 | $msg = 'The following extensions must be installed for this script to run: "' |
686 | . implode( '", "', $missing ) . '". Please enable them and then try again.'; |
687 | } |
688 | $this->fatalError( $msg ); |
689 | } |
690 | } |
691 | |
692 | /** |
693 | * Run a child maintenance script. Pass all of the current arguments |
694 | * to it. |
695 | * @param string $maintClass A name of a child maintenance class |
696 | * @param string|null $classFile Full path of where the child is |
697 | * @stable to override |
698 | * @return Maintenance |
699 | */ |
700 | public function runChild( $maintClass, $classFile = null ) { |
701 | // Make sure the class is loaded first |
702 | if ( !class_exists( $maintClass ) ) { |
703 | if ( $classFile ) { |
704 | require_once $classFile; |
705 | } |
706 | if ( !class_exists( $maintClass ) ) { |
707 | $this->fatalError( "Cannot spawn child: $maintClass" ); |
708 | } |
709 | } |
710 | |
711 | /** |
712 | * @var Maintenance $child |
713 | */ |
714 | $child = new $maintClass(); |
715 | $child->loadParamsAndArgs( |
716 | $this->mSelf, |
717 | $this->parameters->getOptions(), |
718 | $this->parameters->getArgs() |
719 | ); |
720 | if ( $this->mDb !== null ) { |
721 | $child->setDB( $this->mDb ); |
722 | } |
723 | if ( $this->dbProvider !== null ) { |
724 | $child->setDBProvider( $this->dbProvider ); |
725 | } |
726 | |
727 | return $child; |
728 | } |
729 | |
730 | /** |
731 | * Provides subclasses with an opportunity to perform initial checks. |
732 | * @stable to override |
733 | */ |
734 | public function setup() { |
735 | // noop |
736 | } |
737 | |
738 | /** |
739 | * Normally we disable the memory_limit when running admin scripts. |
740 | * Some scripts may wish to actually set a limit, however, to avoid |
741 | * blowing up unexpectedly. |
742 | * @stable to override |
743 | * @return string |
744 | */ |
745 | public function memoryLimit() { |
746 | return 'max'; |
747 | } |
748 | |
749 | /** |
750 | * Clear all params and arguments. |
751 | */ |
752 | public function clearParamsAndArgs() { |
753 | $this->parameters->clear(); |
754 | $this->mInputLoaded = false; |
755 | } |
756 | |
757 | /** |
758 | * @since 1.40 |
759 | * @internal |
760 | * @param string $name |
761 | */ |
762 | public function setName( string $name ) { |
763 | $this->mSelf = $name; |
764 | $this->parameters->setName( $this->mSelf ); |
765 | } |
766 | |
767 | /** |
768 | * Load params and arguments from a given array |
769 | * of command-line arguments |
770 | * |
771 | * @since 1.27 |
772 | * @param array $argv The argument array, not including the script itself. |
773 | */ |
774 | public function loadWithArgv( $argv ) { |
775 | if ( $this->mDescription ) { |
776 | $this->parameters->setDescription( $this->mDescription ); |
777 | } |
778 | |
779 | $this->parameters->loadWithArgv( $argv ); |
780 | |
781 | if ( $this->parameters->hasErrors() ) { |
782 | $errors = "\nERROR: " . implode( "\nERROR: ", $this->parameters->getErrors() ) . "\n"; |
783 | $this->error( $errors ); |
784 | $this->maybeHelp( true ); |
785 | } |
786 | |
787 | $this->loadSpecialVars(); |
788 | $this->mInputLoaded = true; |
789 | } |
790 | |
791 | /** |
792 | * Process command line arguments when running as a child script |
793 | * |
794 | * @param string|null $self The name of the script, if any |
795 | * @param array|null $opts An array of options, in form of key=>value |
796 | * @param array|null $args An array of command line arguments |
797 | */ |
798 | public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) { |
799 | # If we were given opts or args, set those and return early |
800 | if ( $self !== null || $opts !== null || $args !== null ) { |
801 | if ( $self !== null ) { |
802 | $this->mSelf = $self; |
803 | $this->parameters->setName( $self ); |
804 | } |
805 | $this->parameters->setOptionsAndArgs( $opts ?? [], $args ?? [] ); |
806 | $this->mInputLoaded = true; |
807 | } |
808 | |
809 | # If we've already loaded input (either by user values or from $argv) |
810 | # skip on loading it again. |
811 | if ( $this->mInputLoaded ) { |
812 | $this->loadSpecialVars(); |
813 | |
814 | return; |
815 | } |
816 | |
817 | global $argv; |
818 | $this->mSelf = $argv[0]; |
819 | $this->parameters->setName( $this->mSelf ); |
820 | $this->loadWithArgv( array_slice( $argv, 1 ) ); |
821 | } |
822 | |
823 | /** |
824 | * Run some validation checks on the params, etc |
825 | * @stable to override |
826 | */ |
827 | public function validateParamsAndArgs() { |
828 | $valid = $this->parameters->validate(); |
829 | |
830 | $this->maybeHelp( !$valid ); |
831 | } |
832 | |
833 | /** |
834 | * Handle the special variables that are global to all scripts |
835 | * @stable to override |
836 | */ |
837 | protected function loadSpecialVars() { |
838 | if ( $this->hasOption( 'dbuser' ) ) { |
839 | $this->mDbUser = $this->getOption( 'dbuser' ); |
840 | } |
841 | if ( $this->hasOption( 'dbpass' ) ) { |
842 | $this->mDbPass = $this->getOption( 'dbpass' ); |
843 | } |
844 | if ( $this->hasOption( 'quiet' ) ) { |
845 | $this->mQuiet = true; |
846 | } |
847 | if ( $this->hasOption( 'batch-size' ) ) { |
848 | $this->mBatchSize = intval( $this->getOption( 'batch-size' ) ); |
849 | } |
850 | } |
851 | |
852 | /** |
853 | * Maybe show the help. If the help is shown, exit. |
854 | * |
855 | * @param bool $force Whether to force the help to show, default false |
856 | */ |
857 | protected function maybeHelp( $force = false ) { |
858 | if ( !$force && !$this->hasOption( 'help' ) ) { |
859 | return; |
860 | } |
861 | |
862 | if ( $this->parameters->hasErrors() && !$this->hasOption( 'help' ) ) { |
863 | $errors = "\nERROR: " . implode( "\nERROR: ", $this->parameters->getErrors() ) . "\n"; |
864 | $this->error( $errors ); |
865 | } |
866 | |
867 | $this->showHelp(); |
868 | die( 1 ); |
869 | } |
870 | |
871 | /** |
872 | * Definitely show the help. Does not exit. |
873 | */ |
874 | protected function showHelp() { |
875 | $this->mQuiet = false; |
876 | $help = $this->parameters->getHelp(); |
877 | $this->output( $help ); |
878 | } |
879 | |
880 | /** |
881 | * Handle some last-minute setup here. |
882 | * |
883 | * @stable to override |
884 | * |
885 | * @param SettingsBuilder $settingsBuilder |
886 | */ |
887 | public function finalSetup( SettingsBuilder $settingsBuilder ) { |
888 | $config = $settingsBuilder->getConfig(); |
889 | $overrides = []; |
890 | $overrides['DBadminuser'] = $config->get( MainConfigNames::DBadminuser ); |
891 | $overrides['DBadminpassword'] = $config->get( MainConfigNames::DBadminpassword ); |
892 | |
893 | # Turn off output buffering again, it might have been turned on in the settings files |
894 | if ( ob_get_level() ) { |
895 | ob_end_flush(); |
896 | } |
897 | |
898 | # Override $wgServer |
899 | if ( $this->hasOption( 'server' ) ) { |
900 | $overrides['Server'] = $this->getOption( 'server', $config->get( MainConfigNames::Server ) ); |
901 | } |
902 | |
903 | # If these were passed, use them |
904 | if ( $this->mDbUser ) { |
905 | $overrides['DBadminuser'] = $this->mDbUser; |
906 | } |
907 | if ( $this->mDbPass ) { |
908 | $overrides['DBadminpassword'] = $this->mDbPass; |
909 | } |
910 | if ( $this->hasOption( 'dbgroupdefault' ) ) { |
911 | $overrides['DBDefaultGroup'] = $this->getOption( 'dbgroupdefault', null ); |
912 | // TODO: once MediaWikiServices::getInstance() starts throwing exceptions |
913 | // and not deprecation warnings for premature access to service container, |
914 | // we can remove this line. This method is called before Setup.php, |
915 | // so it would be guaranteed DBLoadBalancerFactory is not yet initialized. |
916 | if ( MediaWikiServices::hasInstance() ) { |
917 | $service = $this->getServiceContainer()->peekService( 'DBLoadBalancerFactory' ); |
918 | if ( $service ) { |
919 | $service->destroy(); |
920 | } |
921 | } |
922 | } |
923 | |
924 | if ( $this->getDbType() == self::DB_ADMIN && isset( $overrides[ 'DBadminuser' ] ) ) { |
925 | $overrides['DBuser'] = $overrides[ 'DBadminuser' ]; |
926 | $overrides['DBpassword'] = $overrides[ 'DBadminpassword' ]; |
927 | |
928 | /** @var array $dbServers */ |
929 | $dbServers = $config->get( MainConfigNames::DBservers ); |
930 | if ( $dbServers ) { |
931 | foreach ( $dbServers as $i => $server ) { |
932 | $dbServers[$i]['user'] = $overrides['DBuser']; |
933 | $dbServers[$i]['password'] = $overrides['DBpassword']; |
934 | } |
935 | $overrides['DBservers'] = $dbServers; |
936 | } |
937 | |
938 | $lbFactoryConf = $config->get( MainConfigNames::LBFactoryConf ); |
939 | if ( isset( $lbFactoryConf['serverTemplate'] ) ) { |
940 | $lbFactoryConf['serverTemplate']['user'] = $overrides['DBuser']; |
941 | $lbFactoryConf['serverTemplate']['password'] = $overrides['DBpassword']; |
942 | $overrides['LBFactoryConf'] = $lbFactoryConf; |
943 | } |
944 | |
945 | // TODO: once MediaWikiServices::getInstance() starts throwing exceptions |
946 | // and not deprecation warnings for premature access to service container, |
947 | // we can remove this line. This method is called before Setup.php, |
948 | // so it would be guaranteed DBLoadBalancerFactory is not yet initialized. |
949 | if ( MediaWikiServices::hasInstance() ) { |
950 | $service = $this->getServiceContainer()->peekService( 'DBLoadBalancerFactory' ); |
951 | if ( $service ) { |
952 | $service->destroy(); |
953 | } |
954 | } |
955 | } |
956 | |
957 | $this->afterFinalSetup(); |
958 | |
959 | $overrides['ShowExceptionDetails'] = true; |
960 | $overrides['ShowHostname'] = true; |
961 | |
962 | ini_set( 'max_execution_time', '0' ); |
963 | $settingsBuilder->putConfigValues( $overrides ); |
964 | } |
965 | |
966 | /** |
967 | * Override to perform any required operation at the end of initialisation |
968 | * @stable to override |
969 | */ |
970 | protected function afterFinalSetup() { |
971 | } |
972 | |
973 | /** |
974 | * Support function for cleaning up redundant text records |
975 | * @param bool $delete Whether or not to actually delete the records |
976 | * @author Rob Church <robchur@gmail.com> |
977 | */ |
978 | public function purgeRedundantText( $delete = true ) { |
979 | # Data should come off the master, wrapped in a transaction |
980 | $dbw = $this->getPrimaryDB(); |
981 | $this->beginTransaction( $dbw, __METHOD__ ); |
982 | |
983 | # Get "active" text records via the content table |
984 | $cur = []; |
985 | $this->output( 'Searching for active text records via contents table...' ); |
986 | $res = $dbw->newSelectQueryBuilder() |
987 | ->select( 'content_address' ) |
988 | ->distinct() |
989 | ->from( 'content' ) |
990 | ->caller( __METHOD__ )->fetchResultSet(); |
991 | $blobStore = $this->getServiceContainer()->getBlobStore(); |
992 | foreach ( $res as $row ) { |
993 | // @phan-suppress-next-line PhanUndeclaredMethod |
994 | $textId = $blobStore->getTextIdFromAddress( $row->content_address ); |
995 | if ( $textId ) { |
996 | $cur[] = $textId; |
997 | } |
998 | } |
999 | $this->output( "done.\n" ); |
1000 | |
1001 | # Get the IDs of all text records not in these sets |
1002 | $this->output( 'Searching for inactive text records...' ); |
1003 | $res = $dbw->newSelectQueryBuilder() |
1004 | ->select( 'old_id' ) |
1005 | ->distinct() |
1006 | ->from( 'text' ) |
1007 | ->where( $dbw->expr( 'old_id', '!=', $cur ) ) |
1008 | ->caller( __METHOD__ )->fetchResultSet(); |
1009 | $old = []; |
1010 | foreach ( $res as $row ) { |
1011 | $old[] = $row->old_id; |
1012 | } |
1013 | $this->output( "done.\n" ); |
1014 | |
1015 | # Inform the user of what we're going to do |
1016 | $count = count( $old ); |
1017 | $this->output( "$count inactive items found.\n" ); |
1018 | |
1019 | # Delete as appropriate |
1020 | if ( $delete && $count ) { |
1021 | $this->output( 'Deleting...' ); |
1022 | $dbw->newDeleteQueryBuilder() |
1023 | ->deleteFrom( 'text' ) |
1024 | ->where( [ 'old_id' => $old ] ) |
1025 | ->caller( __METHOD__ ) |
1026 | ->execute(); |
1027 | $this->output( "done.\n" ); |
1028 | } |
1029 | |
1030 | $this->commitTransaction( $dbw, __METHOD__ ); |
1031 | } |
1032 | |
1033 | /** |
1034 | * Get the maintenance directory. |
1035 | * @return string |
1036 | */ |
1037 | protected function getDir() { |
1038 | return __DIR__ . '/../'; |
1039 | } |
1040 | |
1041 | /** |
1042 | * Returns a database to be used by current maintenance script. |
1043 | * |
1044 | * This uses the main LBFactory instance by default unless overriden via setDB(). |
1045 | * |
1046 | * This function has the same parameters as LoadBalancer::getConnection(). |
1047 | * |
1048 | * For simple cases, use ::getReplicaDB() or ::getPrimaryDB() instead. |
1049 | * |
1050 | * @stable to override |
1051 | * |
1052 | * @param int $db DB index (DB_REPLICA/DB_PRIMARY) |
1053 | * @param string|string[] $groups default: empty array |
1054 | * @param string|bool $dbDomain default: current wiki |
1055 | * @return IMaintainableDatabase |
1056 | */ |
1057 | protected function getDB( $db, $groups = [], $dbDomain = false ) { |
1058 | if ( $this->mDb === null ) { |
1059 | return $this->getServiceContainer() |
1060 | ->getDBLoadBalancerFactory() |
1061 | ->getMainLB( $dbDomain ) |
1062 | ->getMaintenanceConnectionRef( $db, $groups, $dbDomain ); |
1063 | } |
1064 | |
1065 | return $this->mDb; |
1066 | } |
1067 | |
1068 | /** |
1069 | * Sets database object to be returned by getDB(). |
1070 | * @stable to override |
1071 | * |
1072 | * @param IMaintainableDatabase $db |
1073 | */ |
1074 | public function setDB( IMaintainableDatabase $db ) { |
1075 | $this->mDb = $db; |
1076 | } |
1077 | |
1078 | /** |
1079 | * @return IReadableDatabase |
1080 | * @since 1.42 |
1081 | */ |
1082 | protected function getReplicaDB(): IReadableDatabase { |
1083 | if ( $this->dbProvider === null ) { |
1084 | $this->dbProvider = $this->getServiceContainer()->getConnectionProvider(); |
1085 | } |
1086 | return $this->dbProvider->getReplicaDatabase(); |
1087 | } |
1088 | |
1089 | /** |
1090 | * @return IDatabase |
1091 | * @since 1.42 |
1092 | */ |
1093 | protected function getPrimaryDB(): IDatabase { |
1094 | if ( $this->dbProvider === null ) { |
1095 | $this->dbProvider = $this->getServiceContainer()->getConnectionProvider(); |
1096 | } |
1097 | return $this->dbProvider->getPrimaryDatabase(); |
1098 | } |
1099 | |
1100 | /** |
1101 | * @internal |
1102 | * @param IConnectionProvider $dbProvider |
1103 | * @return void |
1104 | */ |
1105 | public function setDBProvider( IConnectionProvider $dbProvider ) { |
1106 | $this->dbProvider = $dbProvider; |
1107 | } |
1108 | |
1109 | /** |
1110 | * Begin a transaction on a DB |
1111 | * |
1112 | * This method makes it clear that begin() is called from a maintenance script, |
1113 | * which has outermost scope. This is safe, unlike $dbw->begin() called in other places. |
1114 | * |
1115 | * @param IDatabase $dbw |
1116 | * @param string $fname Caller name |
1117 | * @since 1.27 |
1118 | */ |
1119 | protected function beginTransaction( IDatabase $dbw, $fname ) { |
1120 | $dbw->begin( $fname ); |
1121 | } |
1122 | |
1123 | /** |
1124 | * Commit the transaction on a DB handle and wait for replica DBs to catch up |
1125 | * |
1126 | * This method makes it clear that commit() is called from a maintenance script, |
1127 | * which has outermost scope. This is safe, unlike $dbw->commit() called in other places. |
1128 | * |
1129 | * @param IDatabase $dbw |
1130 | * @param string $fname Caller name |
1131 | * @return bool Whether the replica DB wait succeeded |
1132 | * @since 1.27 |
1133 | */ |
1134 | protected function commitTransaction( IDatabase $dbw, $fname ) { |
1135 | $dbw->commit( $fname ); |
1136 | return $this->waitForReplication(); |
1137 | } |
1138 | |
1139 | /** |
1140 | * Wait for replica DBs to catch up. |
1141 | * |
1142 | * @note Since 1.39, this also calls LBFactory::autoReconfigure(). |
1143 | * |
1144 | * @return bool Whether the replica DB wait succeeded |
1145 | * @since 1.36 |
1146 | */ |
1147 | protected function waitForReplication() { |
1148 | $lbFactory = $this->getServiceContainer()->getDBLoadBalancerFactory(); |
1149 | $waitSucceeded = $lbFactory->waitForReplication( |
1150 | [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ] |
1151 | ); |
1152 | $this->lastReplicationWait = microtime( true ); |
1153 | |
1154 | // If possible, apply changes to the database configuration. |
1155 | // The primary use case for this is taking replicas out of rotation. |
1156 | // Long-running scripts may otherwise keep connections to |
1157 | // de-pooled database hosts, and may even re-connect to them. |
1158 | // If no config callback was configured, this has no effect. |
1159 | $lbFactory->autoReconfigure(); |
1160 | |
1161 | return $waitSucceeded; |
1162 | } |
1163 | |
1164 | /** |
1165 | * Rollback the transaction on a DB handle |
1166 | * |
1167 | * This method makes it clear that rollback() is called from a maintenance script, |
1168 | * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places. |
1169 | * |
1170 | * @param IDatabase $dbw |
1171 | * @param string $fname Caller name |
1172 | * @since 1.27 |
1173 | */ |
1174 | protected function rollbackTransaction( IDatabase $dbw, $fname ) { |
1175 | $dbw->rollback( $fname ); |
1176 | } |
1177 | |
1178 | /** |
1179 | * Count down from $seconds to zero on the terminal, with a one-second pause |
1180 | * between showing each number. If the maintenance script is in quiet mode, |
1181 | * this function does nothing. |
1182 | * |
1183 | * @since 1.31 |
1184 | * |
1185 | * @codeCoverageIgnore |
1186 | * @param int $seconds |
1187 | */ |
1188 | protected function countDown( $seconds ) { |
1189 | if ( $this->isQuiet() ) { |
1190 | return; |
1191 | } |
1192 | for ( $i = $seconds; $i >= 0; $i-- ) { |
1193 | if ( $i != $seconds ) { |
1194 | $this->output( str_repeat( "\x08", strlen( (string)( $i + 1 ) ) ) ); |
1195 | } |
1196 | $this->output( (string)$i ); |
1197 | if ( $i ) { |
1198 | sleep( 1 ); |
1199 | } |
1200 | } |
1201 | $this->output( "\n" ); |
1202 | } |
1203 | |
1204 | /** |
1205 | * Wrapper for posix_isatty() |
1206 | * We default as considering stdin a tty (for nice readline methods) |
1207 | * but treating stout as not a tty to avoid color codes |
1208 | * |
1209 | * @param mixed $fd File descriptor |
1210 | * @return bool |
1211 | */ |
1212 | public static function posix_isatty( $fd ) { |
1213 | if ( !function_exists( 'posix_isatty' ) ) { |
1214 | return !$fd; |
1215 | } |
1216 | |
1217 | return posix_isatty( $fd ); |
1218 | } |
1219 | |
1220 | /** |
1221 | * Prompt the console for input |
1222 | * @param string $prompt What to begin the line with, like '> ' |
1223 | * @return string|false Response |
1224 | */ |
1225 | public static function readconsole( $prompt = '> ' ) { |
1226 | static $isatty = null; |
1227 | if ( $isatty === null ) { |
1228 | $isatty = self::posix_isatty( 0 /*STDIN*/ ); |
1229 | } |
1230 | |
1231 | if ( $isatty && function_exists( 'readline' ) ) { |
1232 | return readline( $prompt ); |
1233 | } |
1234 | |
1235 | if ( $isatty ) { |
1236 | $st = self::readlineEmulation( $prompt ); |
1237 | } elseif ( feof( STDIN ) ) { |
1238 | $st = false; |
1239 | } else { |
1240 | $st = fgets( STDIN, 1024 ); |
1241 | } |
1242 | if ( $st === false ) { |
1243 | return false; |
1244 | } |
1245 | |
1246 | return trim( $st ); |
1247 | } |
1248 | |
1249 | /** |
1250 | * Emulate readline() |
1251 | * @param string $prompt What to begin the line with, like '> ' |
1252 | * @return string|false |
1253 | */ |
1254 | private static function readlineEmulation( $prompt ) { |
1255 | $bash = ExecutableFinder::findInDefaultPaths( 'bash' ); |
1256 | if ( !wfIsWindows() && $bash ) { |
1257 | $encPrompt = Shell::escape( $prompt ); |
1258 | $command = "read -er -p $encPrompt && echo \"\$REPLY\""; |
1259 | $result = Shell::command( $bash, '-c', $command ) |
1260 | ->passStdin() |
1261 | ->forwardStderr() |
1262 | ->execute(); |
1263 | |
1264 | if ( $result->getExitCode() == 0 ) { |
1265 | return $result->getStdout(); |
1266 | } |
1267 | |
1268 | if ( $result->getExitCode() == 127 ) { |
1269 | // Couldn't execute bash even though we thought we saw it. |
1270 | // Shell probably spit out an error message, sorry :( |
1271 | // Fall through to fgets()... |
1272 | } else { |
1273 | // EOF/ctrl+D |
1274 | return false; |
1275 | } |
1276 | } |
1277 | |
1278 | // Fallback... we'll have no editing controls, EWWW |
1279 | if ( feof( STDIN ) ) { |
1280 | return false; |
1281 | } |
1282 | print $prompt; |
1283 | |
1284 | return fgets( STDIN, 1024 ); |
1285 | } |
1286 | |
1287 | /** |
1288 | * Get the terminal size as a two-element array where the first element |
1289 | * is the width (number of columns) and the second element is the height |
1290 | * (number of rows). |
1291 | * |
1292 | * @return array |
1293 | */ |
1294 | public static function getTermSize() { |
1295 | static $termSize = null; |
1296 | |
1297 | if ( $termSize !== null ) { |
1298 | return $termSize; |
1299 | } |
1300 | |
1301 | $default = [ 80, 50 ]; |
1302 | |
1303 | if ( wfIsWindows() || Shell::isDisabled() ) { |
1304 | $termSize = $default; |
1305 | |
1306 | return $termSize; |
1307 | } |
1308 | |
1309 | // It's possible to get the screen size with VT-100 terminal escapes, |
1310 | // but reading the responses is not possible without setting raw mode |
1311 | // (unless you want to require the user to press enter), and that |
1312 | // requires an ioctl(), which we can't do. So we have to shell out to |
1313 | // something that can do the relevant syscalls. There are a few |
1314 | // options. Linux and Mac OS X both have "stty size" which does the |
1315 | // job directly. |
1316 | $result = Shell::command( 'stty', 'size' )->passStdin()->execute(); |
1317 | if ( $result->getExitCode() !== 0 || |
1318 | !preg_match( '/^(\d+) (\d+)$/', $result->getStdout(), $m ) |
1319 | ) { |
1320 | $termSize = $default; |
1321 | |
1322 | return $termSize; |
1323 | } |
1324 | |
1325 | $termSize = [ intval( $m[2] ), intval( $m[1] ) ]; |
1326 | |
1327 | return $termSize; |
1328 | } |
1329 | |
1330 | /** |
1331 | * Call this to set up the autoloader to allow classes to be used from the |
1332 | * tests directory. |
1333 | * |
1334 | * @deprecated since 1.41. Set the MW_AUTOLOAD_TEST_CLASSES in file scope instead. |
1335 | */ |
1336 | public static function requireTestsAutoloader() { |
1337 | require_once __DIR__ . '/../../tests/common/TestsAutoLoader.php'; |
1338 | } |
1339 | |
1340 | /** |
1341 | * Get a HookContainer, for running extension hooks or for hook metadata. |
1342 | * |
1343 | * @since 1.35 |
1344 | * @return HookContainer |
1345 | */ |
1346 | protected function getHookContainer() { |
1347 | if ( !$this->hookContainer ) { |
1348 | $this->hookContainer = $this->getServiceContainer()->getHookContainer(); |
1349 | } |
1350 | return $this->hookContainer; |
1351 | } |
1352 | |
1353 | /** |
1354 | * Get a HookRunner for running core hooks. |
1355 | * |
1356 | * @internal This is for use by core only. Hook interfaces may be removed |
1357 | * without notice. |
1358 | * @since 1.35 |
1359 | * @return HookRunner |
1360 | */ |
1361 | protected function getHookRunner() { |
1362 | if ( !$this->hookRunner ) { |
1363 | $this->hookRunner = new HookRunner( $this->getHookContainer() ); |
1364 | } |
1365 | return $this->hookRunner; |
1366 | } |
1367 | |
1368 | /** |
1369 | * Utility function to parse a string (perhaps from a command line option) |
1370 | * into a list of integers (perhaps some kind of numeric IDs). |
1371 | * |
1372 | * @since 1.35 |
1373 | * |
1374 | * @param string $text |
1375 | * |
1376 | * @return int[] |
1377 | */ |
1378 | protected function parseIntList( $text ) { |
1379 | $ids = preg_split( '/[\s,;:|]+/', $text ); |
1380 | $ids = array_map( |
1381 | static function ( $id ) { |
1382 | return (int)$id; |
1383 | }, |
1384 | $ids |
1385 | ); |
1386 | return array_filter( $ids ); |
1387 | } |
1388 | |
1389 | /** |
1390 | * @param string $errorMsg Error message to be displayed if the passed --user or --userid |
1391 | * does not result in a valid existing user object. |
1392 | * |
1393 | * @since 1.37 |
1394 | * |
1395 | * @return User |
1396 | */ |
1397 | protected function validateUserOption( $errorMsg ) { |
1398 | if ( $this->hasOption( "user" ) ) { |
1399 | $user = User::newFromName( $this->getOption( 'user' ) ); |
1400 | } elseif ( $this->hasOption( "userid" ) ) { |
1401 | $user = User::newFromId( $this->getOption( 'userid' ) ); |
1402 | } else { |
1403 | $this->fatalError( $errorMsg ); |
1404 | } |
1405 | if ( !$user || !$user->isRegistered() ) { |
1406 | if ( $this->hasOption( "user" ) ) { |
1407 | $this->fatalError( "No such user: " . $this->getOption( 'user' ) ); |
1408 | } elseif ( $this->hasOption( "userid" ) ) { |
1409 | $this->fatalError( "No such user id: " . $this->getOption( 'userid' ) ); |
1410 | } |
1411 | } |
1412 | |
1413 | return $user; |
1414 | } |
1415 | } |