4 use Psr\Log\LoggerInterface;
5 use Psr\Log\NullLogger;
19 if ( php_uname(
's' ) ===
'Linux' ) {
20 $this->initialStatus = $this->interpreter->getStatus();
22 $this->initialStatus =
false;
28 'phpCallsRequireSerialization' =>
true,
35 }
catch ( Exception $e ) {
38 if ( $this->initialStatus ) {
39 $status = $this->interpreter->getStatus();
40 $output->setLimitReportData(
'scribunto-limitreport-timeusage',
44 rtrim( rtrim( sprintf(
"%.3f", $this->options[
'cpuLimit'] ),
'0' ),
'.' )
47 $output->setLimitReportData(
'scribunto-limitreport-virtmemusage',
50 $this->options[
'memoryLimit']
53 $output->setLimitReportData(
'scribunto-limitreport-estmemusage',
54 $status[
'vsize'] - $this->initialStatus[
'vsize']
59 $output->addModules(
'ext.scribunto.logs' );
60 $output->setLimitReportData(
'scribunto-limitreport-logs', $logs );
68 case 'scribunto-limitreport-logs':
73 case 'scribunto-limitreport-virtmemusage':
74 $value = array_map( [
$lang,
'formatSize' ], $value );
76 case 'scribunto-limitreport-estmemusage':
78 $value =
$lang->formatSize( $value );
88 if ( self::$clockTick ===
null ) {
89 Wikimedia\suppressWarnings();
90 self::$clockTick = intval( shell_exec(
'getconf CLK_TCK' ) );
91 Wikimedia\restoreWarnings();
92 if ( !self::$clockTick ) {
93 self::$clockTick = 100;
104 'logger' => LoggerFactory::getInstance(
'Scribunto' )
110 if ( $ver !==
null ) {
111 if ( substr( $ver, 0, 6 ) ===
'LuaJIT' ) {
112 $software[
'[http://luajit.org/ LuaJIT]'] = str_replace(
'LuaJIT ',
'', $ver );
114 $software[
'[http://www.lua.org/ Lua]'] = str_replace(
'Lua ',
'', $ver );
176 $this->
id = self::$nextInterpreterId++;
178 if ( $options[
'errorFile'] ===
null ) {
182 if ( $options[
'luaPath'] ===
null ) {
186 if ( PHP_OS ==
'Linux' ) {
187 if ( PHP_INT_SIZE == 4 ) {
188 $path =
'lua5_1_5_linux_32_generic/lua';
189 } elseif ( PHP_INT_SIZE == 8 ) {
190 $path =
'lua5_1_5_linux_64_generic/lua';
192 } elseif ( PHP_OS ==
'Windows' || PHP_OS ==
'WINNT' || PHP_OS ==
'Win32' ) {
193 if ( PHP_INT_SIZE == 4 ) {
194 $path =
'lua5_1_5_Win32_bin/lua5.1.exe';
195 } elseif ( PHP_INT_SIZE == 8 ) {
196 $path =
'lua5_1_5_Win64_bin/lua5.1.exe';
198 } elseif ( PHP_OS ==
'Darwin' ) {
199 $path =
'lua5_1_5_mac_lion_fat_generic/lua';
201 if (
$path ===
false ) {
203 'No Lua interpreter was given in the configuration, ' .
204 'and no bundled binary exists for this platform.' );
206 $options[
'luaPath'] = __DIR__ .
"/binaries/$path";
208 if ( !is_executable( $options[
'luaPath'] ) ) {
210 sprintf(
'The lua binary (%s) is not executable.', $options[
'luaPath'] )
216 $this->enableDebug = !empty( $options[
'debug'] );
217 $this->logger = $options[
'logger'] ??
new NullLogger();
222 __DIR__ .
'/mw_main.lua',
223 dirname( dirname( __DIR__ ) ),
227 if ( php_uname(
's' ) ==
'Linux' ) {
230 'exec', # proc_open() passes $cmd to
'sh -c' on Linux, so add an
'exec' to bypass it
232 __DIR__ .
'/lua_ulimit.sh',
233 $options[
'cpuLimit'], # soft limit (SIGXCPU)
234 $options[
'cpuLimit'] + 1, # hard limit
235 intval( $options[
'memoryLimit'] / 1024 ),
239 if ( php_uname(
's' ) ==
'Windows NT' ) {
245 $cmd =
'"' . $cmd .
'"';
248 $this->logger->debug( __METHOD__ .
": creating interpreter: $cmd\n" );
252 if ( !function_exists(
'proc_open' ) ) {
253 throw $this->engine->newException(
'scribunto-luastandalone-proc-error-proc-open' );
258 Wikimedia\suppressWarnings();
260 Wikimedia\restoreWarnings();
262 $this->proc = proc_open(
267 [
'file', $options[
'errorFile'],
'a' ]
270 if ( !$this->proc ) {
271 $err = error_get_last();
272 if ( !empty( $err[
'message'] ) ) {
273 throw $this->engine->newException(
'scribunto-luastandalone-proc-error-msg',
274 [
'args' => [ $err[
'message'] ] ] );
276 throw $this->engine->newException(
'scribunto-luastandalone-proc-error' );
279 $this->writePipe = $pipes[0];
280 $this->readPipe = $pipes[1];
288 if ( $options[
'luaPath'] ===
null ) {
290 if ( PHP_OS ==
'Linux' ) {
292 } elseif ( PHP_OS ==
'Windows' || PHP_OS ==
'WINNT' || PHP_OS ==
'Win32' ) {
294 } elseif ( PHP_OS ==
'Darwin' ) {
306 $handle = popen( $cmd,
'r' );
308 $ret = fgets( $handle, 80 );
310 if ( $ret && preg_match(
'/^Lua(?:JIT)? \S+/', $ret, $m ) ) {
319 $this->logger->debug( __METHOD__ .
": terminating\n" );
320 proc_terminate( $this->proc );
321 proc_close( $this->proc );
327 if ( !$this->proc ) {
330 $this->
dispatch( [
'op' =>
'quit' ] );
331 proc_close( $this->proc );
335 if ( !$this->proc ) {
338 $this->
dispatch( [
'op' =>
'testquit' ] );
339 proc_close( $this->proc );
351 'op' =>
'loadString',
353 'chunkName' => $chunkName
360 throw new MWException( __METHOD__ .
': invalid function type' );
362 if ( $func->interpreterId !== $this->id ) {
363 throw new MWException( __METHOD__ .
': function belongs to a different interpreter' );
365 $args = func_get_args();
374 'nargs' => count(
$args ),
378 return array_values( $result );
383 $id =
"anonymous*" . ++$uid;
384 $this->callbacks[
$id] = $callable;
386 'op' =>
'wrapPhpFunction',
396 'op' =>
'cleanupChunks',
413 foreach ( $functions as $funcName => $callback ) {
414 $id =
"$name-$funcName-$uid";
415 $this->callbacks[
$id] = $callback;
416 $functionIds[$funcName] =
$id;
419 'op' =>
'registerLibrary',
421 'functions' => $functionIds,
445 private static function fixNulls( array $array, $count ) {
446 if ( count( $array ) === $count ) {
449 return array_replace( array_fill( 1, $count,
null ), $array );
454 $message[
'args'] =
self::fixNulls( $message[
'args'], $message[
'nargs'] );
456 $result = $this->
callback( $message[
'id'], $message[
'args'] );
465 if ( $result !==
null && count( $result ) ) {
466 $result = array_combine( range( 1, count( $result ) ), $result );
473 'nvalues' => count( $result ),
479 return ( $this->callbacks[
$id] )( ...$args );
484 if ( preg_match(
'/^(.*?):(\d+): (.*)$/', $message[
'value'], $m ) ) {
485 $opts[
'module'] = $m[1];
486 $opts[
'line'] = $m[2];
487 $message[
'value'] = $m[3];
489 if ( isset( $message[
'trace'] ) ) {
490 $opts[
'trace'] = array_values( $message[
'trace'] );
492 throw $this->engine->newLuaError( $message[
'value'], $opts );
500 switch ( $msgFromLua[
'op'] ) {
502 return self::fixNulls( $msgFromLua[
'values'], $msgFromLua[
'nvalues'] );
511 $this->logger->error( __METHOD__ .
": invalid response op \"{$msgFromLua['op']}\"\n" );
512 throw $this->engine->newException(
'scribunto-luastandalone-decode-error' );
518 $this->
debug(
"TX ==> {$msg['op']}" );
522 if ( !fwrite( $this->writePipe, $encMsg ) ) {
526 throw $this->engine->newException(
'scribunto-luastandalone-write-error' );
533 $header = fread( $this->readPipe, 16 );
534 if ( strlen(
$header ) !== 16 ) {
536 throw $this->engine->newException(
'scribunto-luastandalone-read-error' );
542 $lengthRemaining = $length;
543 while ( $lengthRemaining ) {
544 $buffer = fread( $this->readPipe, $lengthRemaining );
545 if ( $buffer ===
false || feof( $this->readPipe ) ) {
547 throw $this->engine->newException(
'scribunto-luastandalone-read-error' );
550 $lengthRemaining -= strlen( $buffer );
552 $body = strtr( $body, [
558 $this->
debug(
"RX <== {$msg['op']}" );
565 $check = $length * 2 - 1;
567 return sprintf(
'%08x%08x%s', $length, $check,
$serialized );
578 if ( $level > 100 ) {
579 throw new MWException( __METHOD__ .
': recursion depth limit exceeded' );
581 $type = gettype( $var );
584 return $var ?
'true' :
'false';
588 if ( !is_finite( $var ) ) {
589 if ( is_nan( $var ) ) {
592 if ( $var === INF ) {
595 if ( $var === -INF ) {
598 throw new MWException( __METHOD__ .
': cannot convert non-finite number' );
600 return sprintf(
'%.17g', $var );
613 foreach ( $var as $key => $element ) {
619 if ( is_int( $key ) && ( $key > 9007199254740992 || $key < -9007199254740992 ) ) {
620 $key = sprintf(
'%d', $key );
630 throw new MWException( __METHOD__ .
': unable to convert object of type ' .
632 } elseif ( $var->interpreterId !== $this->id ) {
634 __METHOD__ .
': unable to convert function belonging to a different interpreter'
637 return 'chunks[' . intval( $var->id ) .
']';
640 throw new MWException( __METHOD__ .
': unable to convert resource' );
644 throw new MWException( __METHOD__ .
': unable to convert variable of unknown type' );
649 $length = substr(
$header, 0, 8 );
650 $check = substr(
$header, 8, 8 );
651 if ( !preg_match(
'/^[0-9a-f]+$/', $length ) || !preg_match(
'/^[0-9a-f]+$/', $check ) ) {
652 throw $this->engine->newException(
'scribunto-luastandalone-decode-error' );
654 $length = hexdec( $length );
655 $check = hexdec( $check );
656 if ( $length * 2 - 1 !== $check ) {
657 throw $this->engine->newException(
'scribunto-luastandalone-decode-error' );
666 if ( !$this->proc ) {
667 $this->logger->error( __METHOD__ .
": process already terminated\n" );
668 if ( $this->exitError ) {
671 throw $this->engine->newException(
'scribunto-luastandalone-gone' );
685 proc_terminate( $this->proc );
687 $status = proc_get_status( $this->proc );
700 proc_close( $this->proc );
704 if ( !
$status[
'signaled'] && (
$status[
'exitcode'] & 0x80 ) === 0x80 ) {
710 if ( defined(
'SIGXCPU' ) &&
$status[
'termsig'] === SIGXCPU ) {
711 $this->exitError = $this->engine->newException(
'scribunto-common-timeout' );
713 $this->exitError = $this->engine->newException(
'scribunto-luastandalone-signal',
714 [
'args' => [
$status[
'termsig'] ] ] );
717 $this->exitError = $this->engine->newException(
'scribunto-luastandalone-exited',
718 [
'args' => [
$status[
'exitcode'] ] ] );
724 if ( $this->enableDebug ) {
725 $this->logger->debug(
"Lua: $msg\n" );
767 if ( !isset( self::$activeChunkIds[$this->interpreterId] ) ) {
769 } elseif ( !isset( self::$activeChunkIds[$this->interpreterId][$this->
id] ) ) {
777 if ( isset( self::$activeChunkIds[$this->interpreterId][$this->
id] ) ) {
778 if ( --self::$activeChunkIds[$this->interpreterId][$this->
id] <= 0 ) {
779 unset( self::$activeChunkIds[$this->interpreterId][$this->
id] );