Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.02% covered (warning)
78.02%
252 / 323
53.57% covered (warning)
53.57%
15 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 1
LuaStandaloneInterpreter
78.02% covered (warning)
78.02%
252 / 323
53.57% covered (warning)
53.57%
15 / 28
258.92
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
86.36% covered (warning)
86.36%
19 / 22
0.00% covered (danger)
0.00%
0 / 1
9.21
 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        // 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}