MediaWiki master
rebuildLocalisationCache.php
Go to the documentation of this file.
1<?php
2
27
28// @codeCoverageIgnoreStart
29require_once __DIR__ . '/Maintenance.php';
30// @codeCoverageIgnoreEnd
31
38 public function __construct() {
39 parent::__construct();
40 $this->addDescription( 'Rebuild the localisation cache' );
41 $this->addOption( 'dry-run', 'Determine what languages need to be rebuilt without changing anything' );
42 $this->addOption( 'force', 'Rebuild all files, even ones not out of date' );
43 $this->addOption( 'threads', 'Fork more than one thread', false, true );
44 $this->addOption( 'outdir', 'Override the output directory (normally $wgCacheDirectory)',
45 false, true );
46 $this->addOption( 'lang', 'Only rebuild these languages, comma separated.',
47 false, true );
48 $this->addOption(
49 'store-class',
50 'Override the LC store class (normally $wgLocalisationCacheConf[\'storeClass\'])',
51 false,
52 true
53 );
54 $this->addOption(
55 'no-database',
56 'EXPERIMENTAL: Disable the database backend. Setting this option will result in an error ' .
57 'if you have extensions or use site configuration that need the database. This is an ' .
58 'experimental feature to allow offline building of the localisation cache. Known limitations:' .
59 "\n" .
60 '* Incompatible with LCStoreDB, which always requires a database. ' . "\n" .
61 '* The message purge may require a database. See --skip-message-purge.'
62 );
63 // T237148: The Gadget extension (bundled with MediaWiki by default) requires a database`
64 // connection to register its modules for MessageBlobStore.
65 $this->addOption(
66 'skip-message-purge',
67 'Skip purging of MessageBlobStore. The purge operation may require a database, depending ' .
68 'on the configuration and extensions on this wiki. If skipping the purge now, you need to ' .
69 'run purgeMessageBlobStore.php shortly after deployment.'
70 );
71 $this->addOption(
72 'no-progress',
73 "Don't print a message for each rebuilt language file. Use this instead of " .
74 "--quiet to get a brief summary of the operation."
75 );
76 }
77
78 public function finalSetup( SettingsBuilder $settingsBuilder ) {
79 # This script needs to be run to build the initial l10n cache. But if
80 # LanguageCode is not 'en', it won't be able to run because there is
81 # no l10n cache. Break the cycle by forcing the LanguageCode setting to 'en'.
82 $settingsBuilder->putConfigValue( MainConfigNames::LanguageCode, 'en' );
83 parent::finalSetup( $settingsBuilder );
84 }
85
86 public function execute() {
87 $force = $this->hasOption( 'force' );
88 $threads = $this->getOption( 'threads', 1 );
89 if ( $threads < 1 || $threads != intval( $threads ) ) {
90 $this->output( "Invalid thread count specified; running single-threaded.\n" );
91 $threads = 1;
92 }
93 if ( $threads > 1 && wfIsWindows() ) {
94 $this->output( "Threaded rebuild is not supported on Windows; running single-threaded.\n" );
95 $threads = 1;
96 }
97 if ( $threads > 1 && ( !extension_loaded( 'sockets' ) || !function_exists( 'pcntl_fork' ) ) ) {
98 $this->output( "Threaded rebuild requires ext-pcntl and ext-sockets; running single-threaded.\n" );
99 $threads = 1;
100 }
101
102 $conf = $this->getConfig()->get( MainConfigNames::LocalisationCacheConf );
103 // Allow fallbacks to create CDB files
104 $conf['manualRecache'] = false;
105 $conf['forceRecache'] = $force || !empty( $conf['forceRecache'] );
106 if ( $this->hasOption( 'outdir' ) ) {
107 $conf['storeDirectory'] = $this->getOption( 'outdir' );
108 }
109
110 if ( $this->hasOption( 'store-class' ) ) {
111 $conf['storeClass'] = $this->getOption( 'store-class' );
112 }
113
114 // XXX Copy-pasted from ServiceWiring.php. Do we need a factory for this one caller?
115 $services = $this->getServiceContainer();
117 new ServiceOptions(
118 LocalisationCache::CONSTRUCTOR_OPTIONS,
119 $conf,
120 $services->getMainConfig()
121 ),
122 LocalisationCache::getStoreFromConf( $conf, $this->getConfig()->get( MainConfigNames::CacheDirectory ) ),
123 LoggerFactory::getInstance( 'localisation' ),
124 $this->hasOption( 'skip-message-purge' ) ? [] :
125 [ static function () use ( $services ) {
126 MessageBlobStore::clearGlobalCacheEntry( $services->getMainWANObjectCache() );
127 } ],
128 $services->getLanguageNameUtils(),
129 $services->getHookContainer()
130 );
131
132 $allCodes = array_keys( $services
133 ->getLanguageNameUtils()
134 ->getLanguageNames( LanguageNameUtils::AUTONYMS, LanguageNameUtils::SUPPORTED ) );
135 if ( $this->hasOption( 'lang' ) ) {
136 # Validate requested languages
137 $codes = array_intersect( $allCodes,
138 explode( ',', $this->getOption( 'lang' ) ) );
139 # Bailed out if nothing is left
140 if ( count( $codes ) == 0 ) {
141 $this->fatalError( 'None of the languages specified exists.' );
142 }
143 } else {
144 # By default get all languages
145 $codes = $allCodes;
146 }
147 sort( $codes );
148
149 $numRebuilt = 0;
150 $total = count( $codes );
151 $parentStatus = 0;
152
153 if ( $threads <= 1 ) {
154 // Single-threaded implementation
155 $numRebuilt += $this->doRebuild( $codes, $lc, $force );
156 } else {
157 // Multi-threaded implementation
158 $chunks = array_chunk( $codes, ceil( count( $codes ) / $threads ) );
159 // Map from PID to readable socket
160 $sockets = [];
161
162 foreach ( $chunks as $codes ) {
163 $socketpair = [];
164 // Create a pair of sockets so that the child can communicate
165 // the number of rebuilt langs to the parent.
166 if ( !socket_create_pair( AF_UNIX, SOCK_STREAM, 0, $socketpair ) ) {
167 $this->fatalError( 'socket_create_pair failed' );
168 }
169
170 $pid = pcntl_fork();
171
172 if ( $pid === -1 ) {
173 $this->fatalError( ' pcntl_fork failed' );
174 } elseif ( $pid === 0 ) {
175 // Child, reseed because there is no bug in PHP:
176 // https://bugs.php.net/bug.php?id=42465
177 mt_srand( getmypid() );
178
179 $numRebuilt = $this->doRebuild( $codes, $lc, $force );
180 // Report the number of rebuilt langs to the parent.
181 $msg = "$numRebuilt\n";
182 socket_write( $socketpair[1], $msg, strlen( $msg ) );
183 // Child exits.
184 return;
185 } else {
186 // Main thread
187 $sockets[$pid] = $socketpair[0];
188 }
189 }
190
191 // Wait for all children
192 foreach ( $sockets as $pid => $socket ) {
193 $status = 0;
194 pcntl_waitpid( $pid, $status );
195
196 if ( pcntl_wifexited( $status ) ) {
197 $code = pcntl_wexitstatus( $status );
198 if ( $code ) {
199 $this->output( "Pid $pid exited with status $code !!\n" );
200 } else {
201 // Good exit status from child. Read the number of rebuilt langs from it.
202 $res = socket_read( $socket, 512, PHP_NORMAL_READ );
203 if ( $res === false ) {
204 $this->output( "socket_read failed in parent\n" );
205 } else {
206 $numRebuilt += intval( $res );
207 }
208 }
209
210 // Mush all child statuses into a single value in the parent.
211 $parentStatus |= $code;
212 } elseif ( pcntl_wifsignaled( $status ) ) {
213 $signum = pcntl_wtermsig( $status );
214 $this->output( "Pid $pid terminated by signal $signum !!\n" );
215 $parentStatus |= 1;
216 }
217 }
218 }
219
220 $this->output( "$numRebuilt languages rebuilt out of $total\n" );
221 if ( $numRebuilt === 0 ) {
222 $this->output( "Use --force to rebuild the caches which are still fresh.\n" );
223 }
224 if ( $parentStatus ) {
225 $this->fatalError( 'Failed.', $parentStatus );
226 }
227 }
228
237 private function doRebuild( $codes, $lc, $force ) {
238 $numRebuilt = 0;
239 $operation = $this->hasOption( 'dry-run' ) ? "Would rebuild" : "Rebuilding";
240
241 foreach ( $codes as $code ) {
242 if ( $force || $lc->isExpired( $code ) ) {
243 if ( !$this->hasOption( 'no-progress' ) ) {
244 $this->output( "$operation $code...\n" );
245 }
246 if ( !$this->hasOption( 'dry-run' ) ) {
247 $lc->recache( $code );
248 }
249 $numRebuilt++;
250 }
251 }
252
253 return $numRebuilt;
254 }
255
257 public function getDbType() {
258 if ( $this->hasOption( 'no-database' ) ) {
259 return Maintenance::DB_NONE;
260 }
261
262 return parent::getDbType();
263 }
264
270 public function setForce( $forced = true ) {
271 $this->setOption( 'force', $forced );
272 }
273}
274
275// @codeCoverageIgnoreStart
276$maintClass = RebuildLocalisationCache::class;
277require_once RUN_MAINTENANCE_IF_MAIN;
278// @codeCoverageIgnoreEnd
wfIsWindows()
Check if the operating system is Windows.
A class for passing options to services.
A service that provides utilities to do with language names and codes.
LocalisationCache optimised for loading many languages at once.
Caching for the contents of localisation files.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
output( $out, $channel=null)
Throw some output to the user.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
hasOption( $name)
Checks to see if a particular option was set.
setOption(string $name, $value)
Programmatically set the value of the given option.
getOption( $name, $default=null)
Get an option, or return the default.
getServiceContainer()
Returns the main service container.
addDescription( $text)
Set the description text.
This class generates message blobs for use by ResourceLoader.
Builder class for constructing a Config object from a set of sources during bootstrap.
putConfigValue(string $key, $value)
Puts a value into a config variable.
Maintenance script to rebuild the localisation cache.
finalSetup(SettingsBuilder $settingsBuilder)
Handle some last-minute setup here.
getDbType()
Does the script need different DB access? By default, we give Maintenance scripts normal rights to th...
setForce( $forced=true)
Sets whether a run of this maintenance script has the force parameter set.