Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
78.02% |
252 / 323 |
|
53.57% |
15 / 28 |
CRAP | |
0.00% |
0 / 1 |
LuaStandaloneInterpreter | |
78.02% |
252 / 323 |
|
53.57% |
15 / 28 |
258.92 | |
0.00% |
0 / 1 |
__construct | |
69.12% |
47 / 68 |
|
0.00% |
0 / 1 |
29.63 | |||
__destruct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLuaVersion | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
110 | |||
terminate | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
quit | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
testquit | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
loadString | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
callFunction | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
3.03 | |||
wrapPhpFunction | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
cleanupLuaChunks | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
isLuaFunction | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
registerLibrary | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
getStatus | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
pauseUsageTimer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
unpauseUsageTimer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fixNulls | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
handleCall | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
4 | |||
callback | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
handleError | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
5 | |||
dispatch | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
6.10 | |||
sendMessage | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
2.15 | |||
receiveMessage | |
86.96% |
20 / 23 |
|
0.00% |
0 / 1 |
5.06 | |||
encodeMessage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
encodeLuaVar | |
82.69% |
43 / 52 |
|
0.00% |
0 / 1 |
25.74 | |||
decodeHeader | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
checkValid | |
20.00% |
1 / 5 |
|
0.00% |
0 / 1 |
7.61 | |||
handleIOError | |
86.36% |
19 / 22 |
|
0.00% |
0 / 1 |
9.21 | |||
debug | |
50.00% |
1 / 2 |
|
0.00% |
0 / 1 |
2.50 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Scribunto\Engines\LuaStandalone; |
4 | |
5 | use InvalidArgumentException; |
6 | use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaError; |
7 | use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaInterpreter; |
8 | use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaInterpreterNotExecutableError; |
9 | use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaInterpreterNotFoundError; |
10 | use MediaWiki\Extension\Scribunto\ScribuntoException; |
11 | use Psr\Log\LoggerInterface; |
12 | use Psr\Log\NullLogger; |
13 | use RuntimeException; |
14 | use UtfNormal\Validator; |
15 | |
16 | class LuaStandaloneInterpreter extends LuaInterpreter { |
17 | /** @var int */ |
18 | protected static $nextInterpreterId = 0; |
19 | |
20 | /** |
21 | * @var LuaStandaloneEngine |
22 | */ |
23 | public $engine; |
24 | |
25 | /** |
26 | * @var bool |
27 | */ |
28 | public $enableDebug; |
29 | |
30 | /** |
31 | * @var resource|bool |
32 | */ |
33 | public $proc; |
34 | |
35 | /** |
36 | * @var resource |
37 | */ |
38 | public $writePipe; |
39 | |
40 | /** |
41 | * @var resource |
42 | */ |
43 | public $readPipe; |
44 | |
45 | /** |
46 | * @var ScribuntoException |
47 | */ |
48 | public $exitError; |
49 | |
50 | /** |
51 | * @var int |
52 | */ |
53 | public $id; |
54 | |
55 | /** |
56 | * @var LoggerInterface |
57 | */ |
58 | protected $logger; |
59 | |
60 | /** |
61 | * @var callable[] |
62 | */ |
63 | protected $callbacks; |
64 | |
65 | /** |
66 | * @param LuaStandaloneEngine $engine |
67 | * @param array $options |
68 | * @throws LuaInterpreterNotFoundError |
69 | * @throws ScribuntoException |
70 | */ |
71 | public function __construct( $engine, array $options ) { |
72 | $this->id = self::$nextInterpreterId++; |
73 | |
74 | if ( $options['errorFile'] === null ) { |
75 | $options['errorFile'] = wfGetNull(); |
76 | } |
77 | |
78 | if ( $options['luaPath'] === null ) { |
79 | $path = false; |
80 | |
81 | // Note, if you alter these, also alter getLuaVersion() below |
82 | if ( PHP_OS == 'Linux' ) { |
83 | if ( PHP_INT_SIZE == 4 ) { |
84 | $path = 'lua5_1_5_linux_32_generic/lua'; |
85 | } elseif ( PHP_INT_SIZE == 8 ) { |
86 | $path = 'lua5_1_5_linux_64_generic/lua'; |
87 | } |
88 | } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) { |
89 | if ( PHP_INT_SIZE == 4 ) { |
90 | $path = 'lua5_1_5_Win32_bin/lua5.1.exe'; |
91 | } elseif ( PHP_INT_SIZE == 8 ) { |
92 | $path = 'lua5_1_5_Win64_bin/lua5.1.exe'; |
93 | } |
94 | } elseif ( PHP_OS == 'Darwin' ) { |
95 | $path = 'lua5_1_5_mac_lion_fat_generic/lua'; |
96 | } |
97 | if ( $path === false ) { |
98 | throw new LuaInterpreterNotFoundError( |
99 | 'No Lua interpreter was given in the configuration, ' . |
100 | 'and no bundled binary exists for this platform.' ); |
101 | } |
102 | $options['luaPath'] = __DIR__ . "/binaries/$path"; |
103 | |
104 | if ( !is_executable( $options['luaPath'] ) ) { |
105 | throw new LuaInterpreterNotExecutableError( |
106 | sprintf( 'The lua binary (%s) is not executable.', $options['luaPath'] ) |
107 | ); |
108 | } |
109 | } |
110 | |
111 | $this->engine = $engine; |
112 | $this->enableDebug = !empty( $options['debug'] ); |
113 | $this->logger = $options['logger'] ?? new NullLogger(); |
114 | |
115 | $pipes = null; |
116 | $cmd = wfEscapeShellArg( |
117 | $options['luaPath'], |
118 | __DIR__ . '/mw_main.lua', |
119 | dirname( dirname( __DIR__ ) ), |
120 | (string)$this->id, |
121 | (string)PHP_INT_SIZE |
122 | ); |
123 | if ( php_uname( 's' ) == 'Linux' ) { |
124 | // Limit memory and CPU |
125 | $cmd = wfEscapeShellArg( |
126 | # proc_open() passes $cmd to 'sh -c' on Linux, so add an 'exec' to bypass it |
127 | 'exec', |
128 | '/bin/sh', |
129 | __DIR__ . '/lua_ulimit.sh', |
130 | # soft limit (SIGXCPU) |
131 | (string)$options['cpuLimit'], |
132 | # hard limit |
133 | (string)( $options['cpuLimit'] + 1 ), |
134 | (string)intval( $options['memoryLimit'] / 1024 ), |
135 | $cmd ); |
136 | } |
137 | |
138 | if ( php_uname( 's' ) == 'Windows NT' ) { |
139 | // Like the passthru() in older versions of PHP, |
140 | // PHP's invokation of cmd.exe in proc_open() is broken: |
141 | // http://news.php.net/php.internals/21796 |
142 | // Unlike passthru(), it is not fixed in any PHP version, |
143 | // so we use the fix similar to one in wfShellExec() |
144 | $cmd = '"' . $cmd . '"'; |
145 | } |
146 | |
147 | $this->logger->debug( __METHOD__ . ": creating interpreter: $cmd" ); |
148 | |
149 | // Check whether proc_open is available before trying to call it (e.g. |
150 | // PHP's disable_functions may have removed it) |
151 | if ( !function_exists( 'proc_open' ) ) { |
152 | throw $this->engine->newException( 'scribunto-luastandalone-proc-error-proc-open' ); |
153 | } |
154 | |
155 | // Clear the "last error", so if proc_open fails we can know any |
156 | // warning was generated by that. |
157 | error_clear_last(); |
158 | |
159 | // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.proc_open |
160 | $this->proc = proc_open( |
161 | $cmd, |
162 | [ |
163 | [ 'pipe', 'r' ], |
164 | [ 'pipe', 'w' ], |
165 | [ 'file', $options['errorFile'], 'a' ] |
166 | ], |
167 | $pipes ); |
168 | if ( !$this->proc ) { |
169 | $err = error_get_last(); |
170 | if ( !empty( $err['message'] ) ) { |
171 | throw $this->engine->newException( 'scribunto-luastandalone-proc-error-msg', |
172 | [ 'args' => [ $err['message'] ] ] ); |
173 | } else { |
174 | throw $this->engine->newException( 'scribunto-luastandalone-proc-error' ); |
175 | } |
176 | } |
177 | $this->writePipe = $pipes[0]; |
178 | $this->readPipe = $pipes[1]; |
179 | } |
180 | |
181 | public function __destruct() { |
182 | $this->terminate(); |
183 | } |
184 | |
185 | /** |
186 | * Fetch the Lua version |
187 | * @param array $options Engine options |
188 | * @return string|null |
189 | */ |
190 | public static function getLuaVersion( array $options ) { |
191 | if ( $options['luaPath'] === null ) { |
192 | // We know which versions are distributed, no need to run them. |
193 | if ( PHP_OS == 'Linux' ) { |
194 | return 'Lua 5.1.5'; |
195 | } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) { |
196 | return 'Lua 5.1.5'; |
197 | } elseif ( PHP_OS == 'Darwin' ) { |
198 | return 'Lua 5.1.5'; |
199 | } else { |
200 | return null; |
201 | } |
202 | } |
203 | |
204 | // Ask the interpreter what version it is, using the "-v" option. |
205 | // The output is expected to be one line, something like these: |
206 | // Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio |
207 | // LuaJIT 2.0.0 -- Copyright (C) 2005-2012 Mike Pall. http://luajit.org/ |
208 | $cmd = wfEscapeShellArg( $options['luaPath'] ) . ' -v 2>&1'; |
209 | // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.popen |
210 | $handle = popen( $cmd, 'r' ); |
211 | if ( $handle ) { |
212 | $ret = fgets( $handle, 80 ); |
213 | pclose( $handle ); |
214 | if ( $ret && preg_match( '/^Lua(?:JIT)? \S+/', $ret, $m ) ) { |
215 | return $m[0]; |
216 | } |
217 | } |
218 | return null; |
219 | } |
220 | |
221 | public function terminate() { |
222 | if ( $this->proc ) { |
223 | $this->logger->debug( __METHOD__ . ": terminating" ); |
224 | proc_terminate( $this->proc ); |
225 | proc_close( $this->proc ); |
226 | $this->proc = false; |
227 | } |
228 | } |
229 | |
230 | public function quit() { |
231 | if ( !$this->proc ) { |
232 | return; |
233 | } |
234 | $this->dispatch( [ 'op' => 'quit' ] ); |
235 | proc_close( $this->proc ); |
236 | } |
237 | |
238 | public function testquit() { |
239 | if ( !$this->proc ) { |
240 | return; |
241 | } |
242 | $this->dispatch( [ 'op' => 'testquit' ] ); |
243 | proc_close( $this->proc ); |
244 | } |
245 | |
246 | /** |
247 | * @param string $text |
248 | * @param string $chunkName |
249 | * @return LuaStandaloneInterpreterFunction |
250 | */ |
251 | public function loadString( $text, $chunkName ) { |
252 | $this->cleanupLuaChunks(); |
253 | |
254 | $result = $this->dispatch( [ |
255 | 'op' => 'loadString', |
256 | 'text' => $text, |
257 | 'chunkName' => $chunkName |
258 | ] ); |
259 | return new LuaStandaloneInterpreterFunction( $this->id, $result[1] ); |
260 | } |
261 | |
262 | /** @inheritDoc */ |
263 | public function callFunction( $func, ...$args ) { |
264 | if ( !( $func instanceof LuaStandaloneInterpreterFunction ) ) { |
265 | throw new InvalidArgumentException( __METHOD__ . ': invalid function type' ); |
266 | } |
267 | if ( $func->interpreterId !== $this->id ) { |
268 | throw new InvalidArgumentException( __METHOD__ . ': function belongs to a different interpreter' ); |
269 | } |
270 | $args = func_get_args(); |
271 | unset( $args[0] ); |
272 | // $args is now conveniently a 1-based array, as required by the Lua server |
273 | |
274 | $this->cleanupLuaChunks(); |
275 | |
276 | $result = $this->dispatch( [ |
277 | 'op' => 'call', |
278 | 'id' => $func->id, |
279 | 'nargs' => count( $args ), |
280 | 'args' => $args, |
281 | ] ); |
282 | // Convert return values to zero-based |
283 | return array_values( $result ); |
284 | } |
285 | |
286 | /** @inheritDoc */ |
287 | public function wrapPhpFunction( $callable ) { |
288 | static $uid = 0; |
289 | $id = "anonymous*" . ++$uid; |
290 | $this->callbacks[$id] = $callable; |
291 | $ret = $this->dispatch( [ |
292 | 'op' => 'wrapPhpFunction', |
293 | 'id' => $id, |
294 | ] ); |
295 | return $ret[1]; |
296 | } |
297 | |
298 | public function cleanupLuaChunks() { |
299 | if ( isset( LuaStandaloneInterpreterFunction::$anyChunksDestroyed[$this->id] ) ) { |
300 | unset( LuaStandaloneInterpreterFunction::$anyChunksDestroyed[$this->id] ); |
301 | $this->dispatch( [ |
302 | 'op' => 'cleanupChunks', |
303 | 'ids' => LuaStandaloneInterpreterFunction::$activeChunkIds[$this->id] |
304 | ] ); |
305 | } |
306 | } |
307 | |
308 | /** @inheritDoc */ |
309 | public function isLuaFunction( $object ) { |
310 | return $object instanceof LuaStandaloneInterpreterFunction; |
311 | } |
312 | |
313 | /** @inheritDoc */ |
314 | public function registerLibrary( $name, array $functions ) { |
315 | // Make sure all ids are unique, even when libraries share the same name |
316 | // which is especially relevant for "mw_interface" (T211203). |
317 | static $uid = 0; |
318 | $uid++; |
319 | |
320 | $functionIds = []; |
321 | foreach ( $functions as $funcName => $callback ) { |
322 | $id = "$name-$funcName-$uid"; |
323 | $this->callbacks[$id] = $callback; |
324 | $functionIds[$funcName] = $id; |
325 | } |
326 | $this->dispatch( [ |
327 | 'op' => 'registerLibrary', |
328 | 'name' => $name, |
329 | 'functions' => $functionIds, |
330 | ] ); |
331 | } |
332 | |
333 | /** |
334 | * Get interpreter status |
335 | * @return array |
336 | */ |
337 | public function getStatus() { |
338 | $result = $this->dispatch( [ |
339 | 'op' => 'getStatus', |
340 | ] ); |
341 | return $result[1]; |
342 | } |
343 | |
344 | public function pauseUsageTimer() { |
345 | } |
346 | |
347 | public function unpauseUsageTimer() { |
348 | } |
349 | |
350 | /** |
351 | * Fill in missing nulls in a list received from Lua |
352 | * |
353 | * @param array $array List received from Lua |
354 | * @param int $count Number of values that should be in the list |
355 | * @return array Non-sparse array |
356 | */ |
357 | private static function fixNulls( array $array, $count ) { |
358 | if ( count( $array ) === $count ) { |
359 | return $array; |
360 | } else { |
361 | return array_replace( array_fill( 1, $count, null ), $array ); |
362 | } |
363 | } |
364 | |
365 | /** |
366 | * Handle a protocol 'call' message from Lua |
367 | * @param array $message |
368 | * @return array Response message to send to Lua |
369 | */ |
370 | protected function handleCall( $message ) { |
371 | $message['args'] = self::fixNulls( $message['args'], $message['nargs'] ); |
372 | try { |
373 | $result = $this->callback( $message['id'], $message['args'] ); |
374 | } catch ( LuaError $e ) { |
375 | return [ |
376 | 'op' => 'error', |
377 | 'value' => $e->getLuaMessage(), |
378 | ]; |
379 | } |
380 | |
381 | // Convert to a 1-based array |
382 | if ( $result !== null && count( $result ) ) { |
383 | $result = array_combine( range( 1, count( $result ) ), $result ); |
384 | } else { |
385 | $result = []; |
386 | } |
387 | |
388 | return [ |
389 | 'op' => 'return', |
390 | 'nvalues' => count( $result ), |
391 | 'values' => $result |
392 | ]; |
393 | } |
394 | |
395 | /** |
396 | * Call a registered/wrapped PHP function from Lua |
397 | * @param string $id Callback ID |
398 | * @param array $args Arguments to pass to the callback |
399 | * @return mixed Return value from the callback |
400 | */ |
401 | protected function callback( $id, array $args ) { |
402 | return ( $this->callbacks[$id] )( ...$args ); |
403 | } |
404 | |
405 | /** |
406 | * Handle a protocol error response |
407 | * |
408 | * Converts the encoded Lua error to an appropriate exception and throws it. |
409 | * |
410 | * @param array $message |
411 | * @return never |
412 | */ |
413 | protected function handleError( $message ) { |
414 | $opts = []; |
415 | $message['value'] = Validator::cleanUp( $message['value'] ); |
416 | if ( preg_match( '/^(.*?):(\d+): (.*)$/', $message['value'], $m ) ) { |
417 | $opts['module'] = $m[1]; |
418 | $opts['line'] = $m[2]; |
419 | $message['value'] = $m[3]; |
420 | } |
421 | if ( isset( $message['trace'] ) ) { |
422 | foreach ( $message['trace'] as &$val ) { |
423 | $val = array_map( static function ( $val ) { |
424 | if ( is_string( $val ) ) { |
425 | $val = Validator::cleanUp( $val ); |
426 | } |
427 | return $val; |
428 | }, $val ); |
429 | } |
430 | $opts['trace'] = array_values( $message['trace'] ); |
431 | } |
432 | throw $this->engine->newLuaError( $message['value'], $opts ); |
433 | } |
434 | |
435 | /** |
436 | * Send a protocol message to Lua, and handle any responses |
437 | * @param array $msgToLua |
438 | * @return mixed Response data |
439 | */ |
440 | protected function dispatch( $msgToLua ) { |
441 | $this->sendMessage( $msgToLua ); |
442 | while ( true ) { |
443 | $msgFromLua = $this->receiveMessage(); |
444 | |
445 | switch ( $msgFromLua['op'] ) { |
446 | case 'return': |
447 | return self::fixNulls( $msgFromLua['values'], $msgFromLua['nvalues'] ); |
448 | case 'call': |
449 | $msgToLua = $this->handleCall( $msgFromLua ); |
450 | $this->sendMessage( $msgToLua ); |
451 | break; |
452 | case 'error': |
453 | $this->handleError( $msgFromLua ); |
454 | // handleError prevents continuation |
455 | default: |
456 | $this->logger->error( __METHOD__ . ": invalid response op \"{$msgFromLua['op']}\"" ); |
457 | throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); |
458 | } |
459 | } |
460 | } |
461 | |
462 | /** |
463 | * Send a protocol message to Lua |
464 | * @param array $msg |
465 | */ |
466 | protected function sendMessage( $msg ) { |
467 | $this->debug( "TX ==> {$msg['op']}" ); |
468 | $this->checkValid(); |
469 | // Send the message |
470 | $encMsg = $this->encodeMessage( $msg ); |
471 | if ( !fwrite( $this->writePipe, $encMsg ) ) { |
472 | // Write error, probably the process has terminated |
473 | // If it has, handleIOError() will throw. If not, throw an exception ourselves. |
474 | $this->handleIOError(); |
475 | throw $this->engine->newException( 'scribunto-luastandalone-write-error' ); |
476 | } |
477 | } |
478 | |
479 | /** |
480 | * Receive a protocol message from Lua |
481 | * @return array |
482 | */ |
483 | protected function receiveMessage() { |
484 | $this->checkValid(); |
485 | // Read the header |
486 | $header = fread( $this->readPipe, 16 ); |
487 | if ( strlen( $header ) !== 16 ) { |
488 | $this->handleIOError(); |
489 | throw $this->engine->newException( 'scribunto-luastandalone-read-error' ); |
490 | } |
491 | $length = $this->decodeHeader( $header ); |
492 | |
493 | // Read the reply body |
494 | $body = ''; |
495 | $lengthRemaining = $length; |
496 | while ( $lengthRemaining ) { |
497 | $buffer = fread( $this->readPipe, $lengthRemaining ); |
498 | if ( $buffer === false || feof( $this->readPipe ) ) { |
499 | $this->handleIOError(); |
500 | throw $this->engine->newException( 'scribunto-luastandalone-read-error' ); |
501 | } |
502 | $body .= $buffer; |
503 | $lengthRemaining -= strlen( $buffer ); |
504 | } |
505 | $body = strtr( $body, [ |
506 | '\\r' => "\r", |
507 | '\\n' => "\n", |
508 | '\\\\' => '\\', |
509 | ] ); |
510 | $msg = unserialize( $body ); |
511 | $this->debug( "RX <== {$msg['op']}" ); |
512 | return $msg; |
513 | } |
514 | |
515 | /** |
516 | * Encode a protocol message to send to Lua |
517 | * @param mixed $message |
518 | * @return string |
519 | */ |
520 | protected function encodeMessage( $message ) { |
521 | $serialized = $this->encodeLuaVar( $message ); |
522 | $length = strlen( $serialized ); |
523 | $check = $length * 2 - 1; |
524 | |
525 | return sprintf( '%08x%08x%s', $length, $check, $serialized ); |
526 | } |
527 | |
528 | /** |
529 | * @param mixed $var |
530 | * @param int $level |
531 | * |
532 | * @return string |
533 | */ |
534 | protected function encodeLuaVar( $var, $level = 0 ) { |
535 | if ( $level > 100 ) { |
536 | throw new RuntimeException( __METHOD__ . ': recursion depth limit exceeded' ); |
537 | } |
538 | $type = gettype( $var ); |
539 | switch ( $type ) { |
540 | case 'boolean': |
541 | return $var ? 'true' : 'false'; |
542 | case 'integer': |
543 | return $var; |
544 | case 'double': |
545 | if ( !is_finite( $var ) ) { |
546 | if ( is_nan( $var ) ) { |
547 | return '(0/0)'; |
548 | } |
549 | if ( $var === INF ) { |
550 | return '(1/0)'; |
551 | } |
552 | if ( $var === -INF ) { |
553 | return '(-1/0)'; |
554 | } |
555 | throw new InvalidArgumentException( __METHOD__ . ': cannot convert non-finite number' ); |
556 | } |
557 | return sprintf( '%.17g', $var ); |
558 | case 'string': |
559 | return '"' . |
560 | strtr( $var, [ |
561 | '"' => '\\"', |
562 | '\\' => '\\\\', |
563 | "\n" => '\\n', |
564 | "\r" => '\\r', |
565 | "\000" => '\\000', |
566 | ] ) . |
567 | '"'; |
568 | case 'array': |
569 | $s = '{'; |
570 | foreach ( $var as $key => $element ) { |
571 | if ( $s !== '{' ) { |
572 | $s .= ','; |
573 | } |
574 | |
575 | // Lua's number type can't represent most integers beyond 2**53, so stringify such keys |
576 | if ( is_int( $key ) && ( $key > 9007199254740992 || $key < -9007199254740992 ) ) { |
577 | $key = (string)$key; |
578 | } |
579 | |
580 | $s .= '[' . $this->encodeLuaVar( $key, $level + 1 ) . ']' . |
581 | '=' . $this->encodeLuaVar( $element, $level + 1 ); |
582 | } |
583 | $s .= '}'; |
584 | return $s; |
585 | case 'object': |
586 | if ( !( $var instanceof LuaStandaloneInterpreterFunction ) ) { |
587 | throw new InvalidArgumentException( __METHOD__ . ': unable to convert object of type ' . |
588 | get_class( $var ) ); |
589 | } elseif ( $var->interpreterId !== $this->id ) { |
590 | throw new InvalidArgumentException( |
591 | __METHOD__ . ': unable to convert function belonging to a different interpreter' |
592 | ); |
593 | } else { |
594 | return 'chunks[' . intval( $var->id ) . ']'; |
595 | } |
596 | case 'resource': |
597 | throw new InvalidArgumentException( __METHOD__ . ': unable to convert resource' ); |
598 | case 'NULL': |
599 | return 'nil'; |
600 | default: |
601 | throw new InvalidArgumentException( __METHOD__ . ': unable to convert variable of unknown type' ); |
602 | } |
603 | } |
604 | |
605 | /** |
606 | * Verify protocol header and extract the body length. |
607 | * @param string $header |
608 | * @return int Length |
609 | */ |
610 | protected function decodeHeader( $header ) { |
611 | $length = substr( $header, 0, 8 ); |
612 | $check = substr( $header, 8, 8 ); |
613 | if ( !preg_match( '/^[0-9a-f]+$/', $length ) || !preg_match( '/^[0-9a-f]+$/', $check ) ) { |
614 | throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); |
615 | } |
616 | $length = hexdec( $length ); |
617 | $check = hexdec( $check ); |
618 | if ( $length * 2 - 1 !== $check ) { |
619 | throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); |
620 | } |
621 | return $length; |
622 | } |
623 | |
624 | /** |
625 | * @throws ScribuntoException |
626 | */ |
627 | protected function checkValid() { |
628 | if ( !$this->proc ) { |
629 | $this->logger->error( __METHOD__ . ": process already terminated" ); |
630 | if ( $this->exitError ) { |
631 | throw $this->exitError; |
632 | } else { |
633 | throw $this->engine->newException( 'scribunto-luastandalone-gone' ); |
634 | } |
635 | } |
636 | } |
637 | |
638 | /** |
639 | * @throws ScribuntoException |
640 | */ |
641 | protected function handleIOError() { |
642 | $this->checkValid(); |
643 | |
644 | // Terminate, fetch the status, then close. proc_close()'s return |
645 | // value isn't helpful here because there's no way to differentiate a |
646 | // signal-kill from a normal exit. |
647 | proc_terminate( $this->proc ); |
648 | while ( true ) { |
649 | $status = proc_get_status( $this->proc ); |
650 | // XXX: Should proc_get_status docs be changed so that |
651 | // its documented as possibly returning false? |
652 | '@phan-var array|false $status'; |
653 | if ( $status === false ) { |
654 | // WTF? Let the caller throw an appropriate error. |
655 | return; |
656 | } |
657 | if ( !$status['running'] ) { |
658 | break; |
659 | } |
660 | // Give the killed process a chance to be scheduled |
661 | usleep( 10000 ); |
662 | } |
663 | proc_close( $this->proc ); |
664 | $this->proc = false; |
665 | |
666 | // proc_open() sometimes uses a shell, check for shell-style signal reporting. |
667 | if ( !$status['signaled'] && ( $status['exitcode'] & 0x80 ) === 0x80 ) { |
668 | $status['signaled'] = true; |
669 | $status['termsig'] = $status['exitcode'] - 128; |
670 | } |
671 | |
672 | if ( $status['signaled'] ) { |
673 | if ( defined( 'SIGXCPU' ) && $status['termsig'] === SIGXCPU ) { |
674 | $this->exitError = $this->engine->newException( 'scribunto-common-timeout' ); |
675 | } else { |
676 | $this->exitError = $this->engine->newException( 'scribunto-luastandalone-signal', |
677 | [ 'args' => [ $status['termsig'] ] ] ); |
678 | } |
679 | } else { |
680 | $this->exitError = $this->engine->newException( 'scribunto-luastandalone-exited', |
681 | [ 'args' => [ $status['exitcode'] ] ] ); |
682 | } |
683 | throw $this->exitError; |
684 | } |
685 | |
686 | /** |
687 | * @param string $msg |
688 | */ |
689 | protected function debug( $msg ) { |
690 | if ( $this->enableDebug ) { |
691 | $this->logger->debug( "Lua: $msg" ); |
692 | } |
693 | } |
694 | } |