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