Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
77.71% |
251 / 323 |
|
53.57% |
15 / 28 |
CRAP | |
0.00% |
0 / 1 |
LuaStandaloneInterpreter | |
77.71% |
251 / 323 |
|
53.57% |
15 / 28 |
265.04 | |
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 | |
81.82% |
18 / 22 |
|
0.00% |
0 / 1 |
9.49 | |||
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 | $this->proc = proc_open( |
160 | $cmd, |
161 | [ |
162 | [ 'pipe', 'r' ], |
163 | [ 'pipe', 'w' ], |
164 | [ 'file', $options['errorFile'], 'a' ] |
165 | ], |
166 | $pipes ); |
167 | if ( !$this->proc ) { |
168 | $err = error_get_last(); |
169 | if ( !empty( $err['message'] ) ) { |
170 | throw $this->engine->newException( 'scribunto-luastandalone-proc-error-msg', |
171 | [ 'args' => [ $err['message'] ] ] ); |
172 | } else { |
173 | throw $this->engine->newException( 'scribunto-luastandalone-proc-error' ); |
174 | } |
175 | } |
176 | $this->writePipe = $pipes[0]; |
177 | $this->readPipe = $pipes[1]; |
178 | } |
179 | |
180 | public function __destruct() { |
181 | $this->terminate(); |
182 | } |
183 | |
184 | /** |
185 | * Fetch the Lua version |
186 | * @param array $options Engine options |
187 | * @return string|null |
188 | */ |
189 | public static function getLuaVersion( array $options ) { |
190 | if ( $options['luaPath'] === null ) { |
191 | // We know which versions are distributed, no need to run them. |
192 | if ( PHP_OS == 'Linux' ) { |
193 | return 'Lua 5.1.5'; |
194 | } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) { |
195 | return 'Lua 5.1.5'; |
196 | } elseif ( PHP_OS == 'Darwin' ) { |
197 | return 'Lua 5.1.5'; |
198 | } else { |
199 | return null; |
200 | } |
201 | } |
202 | |
203 | // Ask the interpreter what version it is, using the "-v" option. |
204 | // The output is expected to be one line, something like these: |
205 | // Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio |
206 | // LuaJIT 2.0.0 -- Copyright (C) 2005-2012 Mike Pall. http://luajit.org/ |
207 | $cmd = wfEscapeShellArg( $options['luaPath'] ) . ' -v'; |
208 | $handle = popen( $cmd, 'r' ); |
209 | if ( $handle ) { |
210 | $ret = fgets( $handle, 80 ); |
211 | pclose( $handle ); |
212 | if ( $ret && preg_match( '/^Lua(?:JIT)? \S+/', $ret, $m ) ) { |
213 | return $m[0]; |
214 | } |
215 | } |
216 | return null; |
217 | } |
218 | |
219 | public function terminate() { |
220 | if ( $this->proc ) { |
221 | $this->logger->debug( __METHOD__ . ": terminating" ); |
222 | proc_terminate( $this->proc ); |
223 | proc_close( $this->proc ); |
224 | $this->proc = false; |
225 | } |
226 | } |
227 | |
228 | public function quit() { |
229 | if ( !$this->proc ) { |
230 | return; |
231 | } |
232 | $this->dispatch( [ 'op' => 'quit' ] ); |
233 | proc_close( $this->proc ); |
234 | } |
235 | |
236 | public function testquit() { |
237 | if ( !$this->proc ) { |
238 | return; |
239 | } |
240 | $this->dispatch( [ 'op' => 'testquit' ] ); |
241 | proc_close( $this->proc ); |
242 | } |
243 | |
244 | /** |
245 | * @param string $text |
246 | * @param string $chunkName |
247 | * @return LuaStandaloneInterpreterFunction |
248 | */ |
249 | public function loadString( $text, $chunkName ) { |
250 | $this->cleanupLuaChunks(); |
251 | |
252 | $result = $this->dispatch( [ |
253 | 'op' => 'loadString', |
254 | 'text' => $text, |
255 | 'chunkName' => $chunkName |
256 | ] ); |
257 | return new LuaStandaloneInterpreterFunction( $this->id, $result[1] ); |
258 | } |
259 | |
260 | /** @inheritDoc */ |
261 | public function callFunction( $func, ...$args ) { |
262 | if ( !( $func instanceof LuaStandaloneInterpreterFunction ) ) { |
263 | throw new InvalidArgumentException( __METHOD__ . ': invalid function type' ); |
264 | } |
265 | if ( $func->interpreterId !== $this->id ) { |
266 | throw new InvalidArgumentException( __METHOD__ . ': function belongs to a different interpreter' ); |
267 | } |
268 | $args = func_get_args(); |
269 | unset( $args[0] ); |
270 | // $args is now conveniently a 1-based array, as required by the Lua server |
271 | |
272 | $this->cleanupLuaChunks(); |
273 | |
274 | $result = $this->dispatch( [ |
275 | 'op' => 'call', |
276 | 'id' => $func->id, |
277 | 'nargs' => count( $args ), |
278 | 'args' => $args, |
279 | ] ); |
280 | // Convert return values to zero-based |
281 | return array_values( $result ); |
282 | } |
283 | |
284 | /** @inheritDoc */ |
285 | public function wrapPhpFunction( $callable ) { |
286 | static $uid = 0; |
287 | $id = "anonymous*" . ++$uid; |
288 | $this->callbacks[$id] = $callable; |
289 | $ret = $this->dispatch( [ |
290 | 'op' => 'wrapPhpFunction', |
291 | 'id' => $id, |
292 | ] ); |
293 | return $ret[1]; |
294 | } |
295 | |
296 | public function cleanupLuaChunks() { |
297 | if ( isset( LuaStandaloneInterpreterFunction::$anyChunksDestroyed[$this->id] ) ) { |
298 | unset( LuaStandaloneInterpreterFunction::$anyChunksDestroyed[$this->id] ); |
299 | $this->dispatch( [ |
300 | 'op' => 'cleanupChunks', |
301 | 'ids' => LuaStandaloneInterpreterFunction::$activeChunkIds[$this->id] |
302 | ] ); |
303 | } |
304 | } |
305 | |
306 | /** @inheritDoc */ |
307 | public function isLuaFunction( $object ) { |
308 | return $object instanceof LuaStandaloneInterpreterFunction; |
309 | } |
310 | |
311 | /** @inheritDoc */ |
312 | public function registerLibrary( $name, array $functions ) { |
313 | // Make sure all ids are unique, even when libraries share the same name |
314 | // which is especially relevant for "mw_interface" (T211203). |
315 | static $uid = 0; |
316 | $uid++; |
317 | |
318 | $functionIds = []; |
319 | foreach ( $functions as $funcName => $callback ) { |
320 | $id = "$name-$funcName-$uid"; |
321 | $this->callbacks[$id] = $callback; |
322 | $functionIds[$funcName] = $id; |
323 | } |
324 | $this->dispatch( [ |
325 | 'op' => 'registerLibrary', |
326 | 'name' => $name, |
327 | 'functions' => $functionIds, |
328 | ] ); |
329 | } |
330 | |
331 | /** |
332 | * Get interpreter status |
333 | * @return array |
334 | */ |
335 | public function getStatus() { |
336 | $result = $this->dispatch( [ |
337 | 'op' => 'getStatus', |
338 | ] ); |
339 | return $result[1]; |
340 | } |
341 | |
342 | public function pauseUsageTimer() { |
343 | } |
344 | |
345 | public function unpauseUsageTimer() { |
346 | } |
347 | |
348 | /** |
349 | * Fill in missing nulls in a list received from Lua |
350 | * |
351 | * @param array $array List received from Lua |
352 | * @param int $count Number of values that should be in the list |
353 | * @return array Non-sparse array |
354 | */ |
355 | private static function fixNulls( array $array, $count ) { |
356 | if ( count( $array ) === $count ) { |
357 | return $array; |
358 | } else { |
359 | return array_replace( array_fill( 1, $count, null ), $array ); |
360 | } |
361 | } |
362 | |
363 | /** |
364 | * Handle a protocol 'call' message from Lua |
365 | * @param array $message |
366 | * @return array Response message to send to Lua |
367 | */ |
368 | protected function handleCall( $message ) { |
369 | $message['args'] = self::fixNulls( $message['args'], $message['nargs'] ); |
370 | try { |
371 | $result = $this->callback( $message['id'], $message['args'] ); |
372 | } catch ( LuaError $e ) { |
373 | return [ |
374 | 'op' => 'error', |
375 | 'value' => $e->getLuaMessage(), |
376 | ]; |
377 | } |
378 | |
379 | // Convert to a 1-based array |
380 | if ( $result !== null && count( $result ) ) { |
381 | $result = array_combine( range( 1, count( $result ) ), $result ); |
382 | } else { |
383 | $result = []; |
384 | } |
385 | |
386 | return [ |
387 | 'op' => 'return', |
388 | 'nvalues' => count( $result ), |
389 | 'values' => $result |
390 | ]; |
391 | } |
392 | |
393 | /** |
394 | * Call a registered/wrapped PHP function from Lua |
395 | * @param string $id Callback ID |
396 | * @param array $args Arguments to pass to the callback |
397 | * @return mixed Return value from the callback |
398 | */ |
399 | protected function callback( $id, array $args ) { |
400 | return ( $this->callbacks[$id] )( ...$args ); |
401 | } |
402 | |
403 | /** |
404 | * Handle a protocol error response |
405 | * |
406 | * Converts the encoded Lua error to an appropriate exception and throws it. |
407 | * |
408 | * @param array $message |
409 | * @return never |
410 | */ |
411 | protected function handleError( $message ) { |
412 | $opts = []; |
413 | $message['value'] = Validator::cleanUp( $message['value'] ); |
414 | if ( preg_match( '/^(.*?):(\d+): (.*)$/', $message['value'], $m ) ) { |
415 | $opts['module'] = $m[1]; |
416 | $opts['line'] = $m[2]; |
417 | $message['value'] = $m[3]; |
418 | } |
419 | if ( isset( $message['trace'] ) ) { |
420 | foreach ( $message['trace'] as &$val ) { |
421 | $val = array_map( static function ( $val ) { |
422 | if ( is_string( $val ) ) { |
423 | $val = Validator::cleanUp( $val ); |
424 | } |
425 | return $val; |
426 | }, $val ); |
427 | } |
428 | $opts['trace'] = array_values( $message['trace'] ); |
429 | } |
430 | throw $this->engine->newLuaError( $message['value'], $opts ); |
431 | } |
432 | |
433 | /** |
434 | * Send a protocol message to Lua, and handle any responses |
435 | * @param array $msgToLua |
436 | * @return mixed Response data |
437 | */ |
438 | protected function dispatch( $msgToLua ) { |
439 | $this->sendMessage( $msgToLua ); |
440 | while ( true ) { |
441 | $msgFromLua = $this->receiveMessage(); |
442 | |
443 | switch ( $msgFromLua['op'] ) { |
444 | case 'return': |
445 | return self::fixNulls( $msgFromLua['values'], $msgFromLua['nvalues'] ); |
446 | case 'call': |
447 | $msgToLua = $this->handleCall( $msgFromLua ); |
448 | $this->sendMessage( $msgToLua ); |
449 | break; |
450 | case 'error': |
451 | $this->handleError( $msgFromLua ); |
452 | // handleError prevents continuation |
453 | default: |
454 | $this->logger->error( __METHOD__ . ": invalid response op \"{$msgFromLua['op']}\"" ); |
455 | throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); |
456 | } |
457 | } |
458 | } |
459 | |
460 | /** |
461 | * Send a protocol message to Lua |
462 | * @param array $msg |
463 | */ |
464 | protected function sendMessage( $msg ) { |
465 | $this->debug( "TX ==> {$msg['op']}" ); |
466 | $this->checkValid(); |
467 | // Send the message |
468 | $encMsg = $this->encodeMessage( $msg ); |
469 | if ( !fwrite( $this->writePipe, $encMsg ) ) { |
470 | // Write error, probably the process has terminated |
471 | // If it has, handleIOError() will throw. If not, throw an exception ourselves. |
472 | $this->handleIOError(); |
473 | throw $this->engine->newException( 'scribunto-luastandalone-write-error' ); |
474 | } |
475 | } |
476 | |
477 | /** |
478 | * Receive a protocol message from Lua |
479 | * @return array |
480 | */ |
481 | protected function receiveMessage() { |
482 | $this->checkValid(); |
483 | // Read the header |
484 | $header = fread( $this->readPipe, 16 ); |
485 | if ( strlen( $header ) !== 16 ) { |
486 | $this->handleIOError(); |
487 | throw $this->engine->newException( 'scribunto-luastandalone-read-error' ); |
488 | } |
489 | $length = $this->decodeHeader( $header ); |
490 | |
491 | // Read the reply body |
492 | $body = ''; |
493 | $lengthRemaining = $length; |
494 | while ( $lengthRemaining ) { |
495 | $buffer = fread( $this->readPipe, $lengthRemaining ); |
496 | if ( $buffer === false || feof( $this->readPipe ) ) { |
497 | $this->handleIOError(); |
498 | throw $this->engine->newException( 'scribunto-luastandalone-read-error' ); |
499 | } |
500 | $body .= $buffer; |
501 | $lengthRemaining -= strlen( $buffer ); |
502 | } |
503 | $body = strtr( $body, [ |
504 | '\\r' => "\r", |
505 | '\\n' => "\n", |
506 | '\\\\' => '\\', |
507 | ] ); |
508 | $msg = unserialize( $body ); |
509 | $this->debug( "RX <== {$msg['op']}" ); |
510 | return $msg; |
511 | } |
512 | |
513 | /** |
514 | * Encode a protocol message to send to Lua |
515 | * @param mixed $message |
516 | * @return string |
517 | */ |
518 | protected function encodeMessage( $message ) { |
519 | $serialized = $this->encodeLuaVar( $message ); |
520 | $length = strlen( $serialized ); |
521 | $check = $length * 2 - 1; |
522 | |
523 | return sprintf( '%08x%08x%s', $length, $check, $serialized ); |
524 | } |
525 | |
526 | /** |
527 | * @param mixed $var |
528 | * @param int $level |
529 | * |
530 | * @return string |
531 | */ |
532 | protected function encodeLuaVar( $var, $level = 0 ) { |
533 | if ( $level > 100 ) { |
534 | throw new RuntimeException( __METHOD__ . ': recursion depth limit exceeded' ); |
535 | } |
536 | $type = gettype( $var ); |
537 | switch ( $type ) { |
538 | case 'boolean': |
539 | return $var ? 'true' : 'false'; |
540 | case 'integer': |
541 | return $var; |
542 | case 'double': |
543 | if ( !is_finite( $var ) ) { |
544 | if ( is_nan( $var ) ) { |
545 | return '(0/0)'; |
546 | } |
547 | if ( $var === INF ) { |
548 | return '(1/0)'; |
549 | } |
550 | if ( $var === -INF ) { |
551 | return '(-1/0)'; |
552 | } |
553 | throw new InvalidArgumentException( __METHOD__ . ': cannot convert non-finite number' ); |
554 | } |
555 | return sprintf( '%.17g', $var ); |
556 | case 'string': |
557 | return '"' . |
558 | strtr( $var, [ |
559 | '"' => '\\"', |
560 | '\\' => '\\\\', |
561 | "\n" => '\\n', |
562 | "\r" => '\\r', |
563 | "\000" => '\\000', |
564 | ] ) . |
565 | '"'; |
566 | case 'array': |
567 | $s = '{'; |
568 | foreach ( $var as $key => $element ) { |
569 | if ( $s !== '{' ) { |
570 | $s .= ','; |
571 | } |
572 | |
573 | // Lua's number type can't represent most integers beyond 2**53, so stringify such keys |
574 | if ( is_int( $key ) && ( $key > 9007199254740992 || $key < -9007199254740992 ) ) { |
575 | $key = (string)$key; |
576 | } |
577 | |
578 | $s .= '[' . $this->encodeLuaVar( $key, $level + 1 ) . ']' . |
579 | '=' . $this->encodeLuaVar( $element, $level + 1 ); |
580 | } |
581 | $s .= '}'; |
582 | return $s; |
583 | case 'object': |
584 | if ( !( $var instanceof LuaStandaloneInterpreterFunction ) ) { |
585 | throw new InvalidArgumentException( __METHOD__ . ': unable to convert object of type ' . |
586 | get_class( $var ) ); |
587 | } elseif ( $var->interpreterId !== $this->id ) { |
588 | throw new InvalidArgumentException( |
589 | __METHOD__ . ': unable to convert function belonging to a different interpreter' |
590 | ); |
591 | } else { |
592 | return 'chunks[' . intval( $var->id ) . ']'; |
593 | } |
594 | case 'resource': |
595 | throw new InvalidArgumentException( __METHOD__ . ': unable to convert resource' ); |
596 | case 'NULL': |
597 | return 'nil'; |
598 | default: |
599 | throw new InvalidArgumentException( __METHOD__ . ': unable to convert variable of unknown type' ); |
600 | } |
601 | } |
602 | |
603 | /** |
604 | * Verify protocol header and extract the body length. |
605 | * @param string $header |
606 | * @return int Length |
607 | */ |
608 | protected function decodeHeader( $header ) { |
609 | $length = substr( $header, 0, 8 ); |
610 | $check = substr( $header, 8, 8 ); |
611 | if ( !preg_match( '/^[0-9a-f]+$/', $length ) || !preg_match( '/^[0-9a-f]+$/', $check ) ) { |
612 | throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); |
613 | } |
614 | $length = hexdec( $length ); |
615 | $check = hexdec( $check ); |
616 | if ( $length * 2 - 1 !== $check ) { |
617 | throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); |
618 | } |
619 | return $length; |
620 | } |
621 | |
622 | /** |
623 | * @throws ScribuntoException |
624 | */ |
625 | protected function checkValid() { |
626 | if ( !$this->proc ) { |
627 | $this->logger->error( __METHOD__ . ": process already terminated" ); |
628 | if ( $this->exitError ) { |
629 | throw $this->exitError; |
630 | } else { |
631 | throw $this->engine->newException( 'scribunto-luastandalone-gone' ); |
632 | } |
633 | } |
634 | } |
635 | |
636 | /** |
637 | * @throws ScribuntoException |
638 | */ |
639 | protected function handleIOError() { |
640 | $this->checkValid(); |
641 | |
642 | // Terminate, fetch the status, then close. proc_close()'s return |
643 | // value isn't helpful here because there's no way to differentiate a |
644 | // signal-kill from a normal exit. |
645 | proc_terminate( $this->proc ); |
646 | while ( true ) { |
647 | $status = proc_get_status( $this->proc ); |
648 | // XXX: Should proc_get_status docs be changed so that |
649 | // its documented as possibly returning false? |
650 | '@phan-var array|false $status'; |
651 | if ( $status === false ) { |
652 | // WTF? Let the caller throw an appropriate error. |
653 | return; |
654 | } |
655 | if ( !$status['running'] ) { |
656 | break; |
657 | } |
658 | // Give the killed process a chance to be scheduled |
659 | usleep( 10000 ); |
660 | } |
661 | proc_close( $this->proc ); |
662 | $this->proc = false; |
663 | |
664 | // proc_open() sometimes uses a shell, check for shell-style signal reporting. |
665 | if ( !$status['signaled'] && ( $status['exitcode'] & 0x80 ) === 0x80 ) { |
666 | $status['signaled'] = true; |
667 | $status['termsig'] = $status['exitcode'] - 128; |
668 | } |
669 | |
670 | if ( $status['signaled'] ) { |
671 | if ( defined( 'SIGXCPU' ) && $status['termsig'] === SIGXCPU ) { |
672 | $this->exitError = $this->engine->newException( 'scribunto-common-timeout' ); |
673 | } else { |
674 | $this->exitError = $this->engine->newException( 'scribunto-luastandalone-signal', |
675 | [ 'args' => [ $status['termsig'] ] ] ); |
676 | } |
677 | } else { |
678 | $this->exitError = $this->engine->newException( 'scribunto-luastandalone-exited', |
679 | [ 'args' => [ $status['exitcode'] ] ] ); |
680 | } |
681 | throw $this->exitError; |
682 | } |
683 | |
684 | /** |
685 | * @param string $msg |
686 | */ |
687 | protected function debug( $msg ) { |
688 | if ( $this->enableDebug ) { |
689 | $this->logger->debug( "Lua: $msg" ); |
690 | } |
691 | } |
692 | } |