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