Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.71% covered (warning)
77.71%
251 / 323
53.57% covered (warning)
53.57%
15 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
LuaStandaloneInterpreter
77.71% covered (warning)
77.71%
251 / 323
53.57% covered (warning)
53.57%
15 / 28
265.04
0.00% covered (danger)
0.00%
0 / 1
 __construct
69.12% covered (warning)
69.12%
47 / 68
0.00% covered (danger)
0.00%
0 / 1
29.63
 __destruct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLuaVersion
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
110
 terminate
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 quit
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 testquit
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 loadString
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 callFunction
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
3.03
 wrapPhpFunction
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 cleanupLuaChunks
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 isLuaFunction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 registerLibrary
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
 getStatus
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 pauseUsageTimer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 unpauseUsageTimer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fixNulls
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 handleCall
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
4
 callback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handleError
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 dispatch
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
6.10
 sendMessage
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
2.15
 receiveMessage
86.96% covered (warning)
86.96%
20 / 23
0.00% covered (danger)
0.00%
0 / 1
5.06
 encodeMessage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 encodeLuaVar
82.69% covered (warning)
82.69%
43 / 52
0.00% covered (danger)
0.00%
0 / 1
25.74
 decodeHeader
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 checkValid
20.00% covered (danger)
20.00%
1 / 5
0.00% covered (danger)
0.00%
0 / 1
7.61
 handleIOError
81.82% covered (warning)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 1
9.49
 debug
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
1<?php
2
3namespace MediaWiki\Extension\Scribunto\Engines\LuaStandalone;
4
5use InvalidArgumentException;
6use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaError;
7use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaInterpreter;
8use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaInterpreterNotExecutableError;
9use MediaWiki\Extension\Scribunto\Engines\LuaCommon\LuaInterpreterNotFoundError;
10use MediaWiki\Extension\Scribunto\ScribuntoException;
11use Psr\Log\LoggerInterface;
12use Psr\Log\NullLogger;
13use RuntimeException;
14use UtfNormal\Validator;
15
16class 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}