diff options
Diffstat (limited to 'www/wiki/extensions/Scribunto/includes/engines/LuaStandalone')
12 files changed, 1743 insertions, 0 deletions
diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/LuaStandaloneEngine.php b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/LuaStandaloneEngine.php new file mode 100644 index 00000000..330a7b9a --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/LuaStandaloneEngine.php @@ -0,0 +1,781 @@ +<?php + +use MediaWiki\Logger\LoggerFactory; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaStandaloneEngine extends Scribunto_LuaEngine { + protected static $clockTick; + public $initialStatus; + + /** + * @var Scribunto_LuaStandaloneInterpreter + */ + protected $interpreter; + + public function load() { + parent::load(); + if ( php_uname( 's' ) === 'Linux' ) { + $this->initialStatus = $this->interpreter->getStatus(); + } else { + $this->initialStatus = false; + } + } + + public function getPerformanceCharacteristics() { + return [ + 'phpCallsRequireSerialization' => true, + ]; + } + + function reportLimitData( ParserOutput $output ) { + try { + $this->load(); + } catch ( Exception $e ) { + return; + } + if ( $this->initialStatus ) { + $status = $this->interpreter->getStatus(); + $output->setLimitReportData( 'scribunto-limitreport-timeusage', + [ + sprintf( "%.3f", $status['time'] / $this->getClockTick() ), + sprintf( "%.3f", $this->options['cpuLimit'] ) + ] + ); + $output->setLimitReportData( 'scribunto-limitreport-virtmemusage', + [ + $status['vsize'], + $this->options['memoryLimit'] + ] + ); + $output->setLimitReportData( 'scribunto-limitreport-estmemusage', + $status['vsize'] - $this->initialStatus['vsize'] + ); + } + $logs = $this->getLogBuffer(); + if ( $logs !== '' ) { + $output->addModules( 'ext.scribunto.logs' ); + $output->setLimitReportData( 'scribunto-limitreport-logs', $logs ); + } + } + + function formatLimitData( $key, &$value, &$report, $isHTML, $localize ) { + global $wgLang; + $lang = $localize ? $wgLang : Language::factory( 'en' ); + switch ( $key ) { + case 'scribunto-limitreport-logs': + if ( $isHTML ) { + $report .= $this->formatHtmlLogs( $value, $localize ); + } + return false; + case 'scribunto-limitreport-virtmemusage': + $value = array_map( [ $lang, 'formatSize' ], $value ); + break; + case 'scribunto-limitreport-estmemusage': + /** @suppress PhanTypeMismatchArgument */ + $value = $lang->formatSize( $value ); + break; + } + return true; + } + + /** + * @return mixed + */ + function getClockTick() { + if ( self::$clockTick === null ) { + Wikimedia\suppressWarnings(); + self::$clockTick = intval( shell_exec( 'getconf CLK_TCK' ) ); + Wikimedia\restoreWarnings(); + if ( !self::$clockTick ) { + self::$clockTick = 100; + } + } + return self::$clockTick; + } + + /** + * @return Scribunto_LuaStandaloneInterpreter + */ + function newInterpreter() { + return new Scribunto_LuaStandaloneInterpreter( $this, $this->options + [ + 'logger' => LoggerFactory::getInstance( 'Scribunto' ) + ] ); + } + + public function getSoftwareInfo( array &$software ) { + $ver = Scribunto_LuaStandaloneInterpreter::getLuaVersion( $this->options ); + if ( $ver !== null ) { + if ( substr( $ver, 0, 6 ) === 'LuaJIT' ) { + $software['[http://luajit.org/ LuaJIT]'] = str_replace( 'LuaJIT ', '', $ver ); + } else { + $software['[http://www.lua.org/ Lua]'] = str_replace( 'Lua ', '', $ver ); + } + } + } +} + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaStandaloneInterpreter extends Scribunto_LuaInterpreter { + protected static $nextInterpreterId = 0; + + /** + * @var Scribunto_LuaStandaloneEngine + */ + public $engine; + + /** + * @var bool + */ + public $enableDebug; + + /** + * @var resource + */ + public $proc; + + /** + * @var resource + */ + public $writePipe; + + /** + * @var resource + */ + public $readPipe; + + /** + * @var ScribuntoException + */ + public $exitError; + + /** + * @var int + */ + public $id; + + /** + * @var LoggerInterface + */ + protected $logger; + + /** + * @var callable[] + */ + protected $callbacks; + + /** + * @param Scribunto_LuaStandaloneEngine $engine + * @param array $options + * @throws MWException + * @throws Scribunto_LuaInterpreterNotFoundError + * @throws ScribuntoException + */ + function __construct( $engine, array $options ) { + $this->id = self::$nextInterpreterId++; + + if ( $options['errorFile'] === null ) { + $options['errorFile'] = wfGetNull(); + } + + if ( $options['luaPath'] === null ) { + $path = false; + + // Note, if you alter these, also alter getLuaVersion() below + if ( PHP_OS == 'Linux' ) { + if ( PHP_INT_SIZE == 4 ) { + $path = 'lua5_1_5_linux_32_generic/lua'; + } elseif ( PHP_INT_SIZE == 8 ) { + $path = 'lua5_1_5_linux_64_generic/lua'; + } + } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) { + if ( PHP_INT_SIZE == 4 ) { + $path = 'lua5_1_5_Win32_bin/lua5.1.exe'; + } elseif ( PHP_INT_SIZE == 8 ) { + $path = 'lua5_1_5_Win64_bin/lua5.1.exe'; + } + } elseif ( PHP_OS == 'Darwin' ) { + $path = 'lua5_1_5_mac_lion_fat_generic/lua'; + } + if ( $path === false ) { + throw new Scribunto_LuaInterpreterNotFoundError( + 'No Lua interpreter was given in the configuration, ' . + 'and no bundled binary exists for this platform.' ); + } + $options['luaPath'] = __DIR__ . "/binaries/$path"; + + if ( !is_executable( $options['luaPath'] ) ) { + throw new MWException( + sprintf( 'The lua binary (%s) is not executable.', $options['luaPath'] ) + ); + } + } + + $this->engine = $engine; + $this->enableDebug = !empty( $options['debug'] ); + $this->logger = isset( $options['logger'] ) + ? $options['logger'] + : new NullLogger(); + + $pipes = null; + $cmd = wfEscapeShellArg( + $options['luaPath'], + __DIR__ . '/mw_main.lua', + dirname( dirname( __DIR__ ) ), + $this->id, + PHP_INT_SIZE + ); + if ( php_uname( 's' ) == 'Linux' ) { + // Limit memory and CPU + $cmd = wfEscapeShellArg( + 'exec', # proc_open() passes $cmd to 'sh -c' on Linux, so add an 'exec' to bypass it + '/bin/sh', + __DIR__ . '/lua_ulimit.sh', + $options['cpuLimit'], # soft limit (SIGXCPU) + $options['cpuLimit'] + 1, # hard limit + intval( $options['memoryLimit'] / 1024 ), + $cmd ); + } + + if ( php_uname( 's' ) == 'Windows NT' ) { + // Like the passthru() in older versions of PHP, + // PHP's invokation of cmd.exe in proc_open() is broken: + // http://news.php.net/php.internals/21796 + // Unlike passthru(), it is not fixed in any PHP version, + // so we use the fix similar to one in wfShellExec() + $cmd = '"' . $cmd . '"'; + } + + $this->logger->debug( __METHOD__.": creating interpreter: $cmd\n" ); + + // Check whether proc_open is available before trying to call it (e.g. + // PHP's disable_functions may have removed it) + if ( !function_exists( 'proc_open' ) ) { + throw $this->engine->newException( 'scribunto-luastandalone-proc-error-proc-open' ); + } + + // Clear the "last error", so if proc_open fails we can know any + // warning was generated by that. + Wikimedia\suppressWarnings(); + trigger_error( '' ); + Wikimedia\restoreWarnings(); + + $this->proc = proc_open( + $cmd, + [ + [ 'pipe', 'r' ], + [ 'pipe', 'w' ], + [ 'file', $options['errorFile'], 'a' ] + ], + $pipes ); + if ( !$this->proc ) { + $err = error_get_last(); + if ( !empty( $err['message'] ) ) { + throw $this->engine->newException( 'scribunto-luastandalone-proc-error-msg', + [ 'args' => [ $err['message'] ] ] ); + } else { + throw $this->engine->newException( 'scribunto-luastandalone-proc-error' ); + } + } + $this->writePipe = $pipes[0]; + $this->readPipe = $pipes[1]; + } + + function __destruct() { + $this->terminate(); + } + + public static function getLuaVersion( array $options ) { + if ( $options['luaPath'] === null ) { + // We know which versions are distributed, no need to run them. + if ( PHP_OS == 'Linux' ) { + return 'Lua 5.1.5'; + } elseif ( PHP_OS == 'Windows' || PHP_OS == 'WINNT' || PHP_OS == 'Win32' ) { + return 'Lua 5.1.4'; + } elseif ( PHP_OS == 'Darwin' ) { + return 'Lua 5.1.5'; + } else { + return null; + } + } + + // Ask the interpreter what version it is, using the "-v" option. + // The output is expected to be one line, something like these: + // Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio + // LuaJIT 2.0.0 -- Copyright (C) 2005-2012 Mike Pall. http://luajit.org/ + $cmd = wfEscapeShellArg( $options['luaPath'] ) . ' -v'; + $handle = popen( $cmd, 'r' ); + if ( $handle ) { + $ret = fgets( $handle, 80 ); + pclose( $handle ); + if ( $ret && preg_match( '/^Lua(?:JIT)? \S+/', $ret, $m ) ) { + return $m[0]; + } + } + return null; + } + + public function terminate() { + if ( $this->proc ) { + $this->logger->debug( __METHOD__.": terminating\n" ); + proc_terminate( $this->proc ); + proc_close( $this->proc ); + $this->proc = false; + } + } + + public function quit() { + if ( !$this->proc ) { + return; + } + $this->dispatch( [ 'op' => 'quit' ] ); + proc_close( $this->proc ); + } + + public function testquit() { + if ( !$this->proc ) { + return; + } + $this->dispatch( [ 'op' => 'testquit' ] ); + proc_close( $this->proc ); + } + + /** + * @param string $text + * @param string $chunkName + * @return Scribunto_LuaStandaloneInterpreterFunction + */ + public function loadString( $text, $chunkName ) { + $this->cleanupLuaChunks(); + + $result = $this->dispatch( [ + 'op' => 'loadString', + 'text' => $text, + 'chunkName' => $chunkName + ] ); + return new Scribunto_LuaStandaloneInterpreterFunction( $this->id, $result[1] ); + } + + public function callFunction( $func /* ... */ ) { + if ( !( $func instanceof Scribunto_LuaStandaloneInterpreterFunction ) ) { + throw new MWException( __METHOD__.': invalid function type' ); + } + if ( $func->interpreterId !== $this->id ) { + throw new MWException( __METHOD__.': function belongs to a different interpreter' ); + } + $args = func_get_args(); + unset( $args[0] ); + // $args is now conveniently a 1-based array, as required by the Lua server + + $this->cleanupLuaChunks(); + + $result = $this->dispatch( [ + 'op' => 'call', + 'id' => $func->id, + 'nargs' => count( $args ), + 'args' => $args, + ] ); + // Convert return values to zero-based + return array_values( $result ); + } + + public function wrapPhpFunction( $callable ) { + static $uid = 0; + $id = "anonymous*" . ++$uid; + $this->callbacks[$id] = $callable; + $ret = $this->dispatch( [ + 'op' => 'wrapPhpFunction', + 'id' => $id, + ] ); + return $ret[1]; + } + + public function cleanupLuaChunks() { + if ( isset( Scribunto_LuaStandaloneInterpreterFunction::$anyChunksDestroyed[$this->id] ) ) { + unset( Scribunto_LuaStandaloneInterpreterFunction::$anyChunksDestroyed[$this->id] ); + $this->dispatch( [ + 'op' => 'cleanupChunks', + 'ids' => Scribunto_LuaStandaloneInterpreterFunction::$activeChunkIds[$this->id] + ] ); + } + } + + public function isLuaFunction( $object ) { + return $object instanceof Scribunto_LuaStandaloneInterpreterFunction; + } + + public function registerLibrary( $name, array $functions ) { + $functionIds = []; + foreach ( $functions as $funcName => $callback ) { + $id = "$name-$funcName"; + $this->callbacks[$id] = $callback; + $functionIds[$funcName] = $id; + } + $this->dispatch( [ + 'op' => 'registerLibrary', + 'name' => $name, + 'functions' => $functionIds, + ] ); + } + + public function getStatus() { + $result = $this->dispatch( [ + 'op' => 'getStatus', + ] ); + return $result[1]; + } + + public function pauseUsageTimer() { + } + + public function unpauseUsageTimer() { + } + + /** + * Fill in missing nulls in a list received from Lua + * + * @param array $array List received from Lua + * @param int $count Number of values that should be in the list + * @return array Non-sparse array + */ + private static function fixNulls( array $array, $count ) { + if ( count( $array ) === $count ) { + return $array; + } else { + return array_replace( array_fill( 1, $count, null ), $array ); + } + } + + protected function handleCall( $message ) { + $message['args'] = self::fixNulls( $message['args'], $message['nargs'] ); + try { + $result = $this->callback( $message['id'], $message['args'] ); + } catch ( Scribunto_LuaError $e ) { + return [ + 'op' => 'error', + 'value' => $e->getLuaMessage(), + ]; + } + + // Convert to a 1-based array + if ( $result !== null && count( $result ) ) { + $result = array_combine( range( 1, count( $result ) ), $result ); + } else { + $result = []; + } + + return [ + 'op' => 'return', + 'nvalues' => count( $result ), + 'values' => $result + ]; + } + + protected function callback( $id, array $args ) { + return call_user_func_array( $this->callbacks[$id], $args ); + } + + protected function handleError( $message ) { + $opts = []; + if ( preg_match( '/^(.*?):(\d+): (.*)$/', $message['value'], $m ) ) { + $opts['module'] = $m[1]; + $opts['line'] = $m[2]; + $message['value'] = $m[3]; + } + if ( isset( $message['trace'] ) ) { + $opts['trace'] = array_values( $message['trace'] ); + } + throw $this->engine->newLuaError( $message['value'], $opts ); + } + + protected function dispatch( $msgToLua ) { + $this->sendMessage( $msgToLua ); + while ( true ) { + $msgFromLua = $this->receiveMessage(); + + switch ( $msgFromLua['op'] ) { + case 'return': + return self::fixNulls( $msgFromLua['values'], $msgFromLua['nvalues'] ); + case 'call': + $msgToLua = $this->handleCall( $msgFromLua ); + $this->sendMessage( $msgToLua ); + break; + case 'error': + $this->handleError( $msgFromLua ); + return; // not reached + default: + $this->logger->error( __METHOD__ .": invalid response op \"{$msgFromLua['op']}\"\n" ); + throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); + } + } + } + + protected function sendMessage( $msg ) { + $this->debug( "TX ==> {$msg['op']}" ); + $this->checkValid(); + // Send the message + $encMsg = $this->encodeMessage( $msg ); + if ( !fwrite( $this->writePipe, $encMsg ) ) { + // Write error, probably the process has terminated + // If it has, handleIOError() will throw. If not, throw an exception ourselves. + $this->handleIOError(); + throw $this->engine->newException( 'scribunto-luastandalone-write-error' ); + } + } + + protected function receiveMessage() { + $this->checkValid(); + // Read the header + $header = fread( $this->readPipe, 16 ); + if ( strlen( $header ) !== 16 ) { + $this->handleIOError(); + throw $this->engine->newException( 'scribunto-luastandalone-read-error' ); + } + $length = $this->decodeHeader( $header ); + + // Read the reply body + $body = ''; + $lengthRemaining = $length; + while ( $lengthRemaining ) { + $buffer = fread( $this->readPipe, $lengthRemaining ); + if ( $buffer === false || feof( $this->readPipe ) ) { + $this->handleIOError(); + throw $this->engine->newException( 'scribunto-luastandalone-read-error' ); + } + $body .= $buffer; + $lengthRemaining -= strlen( $buffer ); + } + $body = strtr( $body, [ + '\\r' => "\r", + '\\n' => "\n", + '\\\\' => '\\', + ] ); + $msg = unserialize( $body ); + $this->debug( "RX <== {$msg['op']}" ); + return $msg; + } + + protected function encodeMessage( $message ) { + $serialized = $this->encodeLuaVar( $message ); + $length = strlen( $serialized ); + $check = $length * 2 - 1; + + return sprintf( '%08x%08x%s', $length, $check, $serialized ); + } + + /** + * @param mixed $var + * @param int $level + * + * @return string + * @throws MWException + */ + protected function encodeLuaVar( $var, $level = 0 ) { + if ( $level > 100 ) { + throw new MWException( __METHOD__.': recursion depth limit exceeded' ); + } + $type = gettype( $var ); + switch ( $type ) { + case 'boolean': + return $var ? 'true' : 'false'; + case 'integer': + return $var; + case 'double': + if ( !is_finite( $var ) ) { + if ( is_nan( $var ) ) { + return '(0/0)'; + } + if ( $var === INF ) { + return '(1/0)'; + } + if ( $var === -INF ) { + return '(-1/0)'; + } + throw new MWException( __METHOD__.': cannot convert non-finite number' ); + } + return sprintf( '%.17g', $var ); + case 'string': + return '"' . + strtr( $var, [ + '"' => '\\"', + '\\' => '\\\\', + "\n" => '\\n', + "\r" => '\\r', + "\000" => '\\000', + ] ) . + '"'; + case 'array': + $s = '{'; + foreach ( $var as $key => $element ) { + if ( $s !== '{' ) { + $s .= ','; + } + + // Lua's number type can't represent most integers beyond 2**53, so stringify such keys + if ( is_int( $key ) && ( $key > 9007199254740992 || $key < -9007199254740992 ) ) { + $key = sprintf( '%d', $key ); + } + + $s .= '[' . $this->encodeLuaVar( $key, $level + 1 ) . ']' . + '=' . $this->encodeLuaVar( $element, $level + 1 ); + } + $s .= '}'; + return $s; + case 'object': + if ( !( $var instanceof Scribunto_LuaStandaloneInterpreterFunction ) ) { + throw new MWException( __METHOD__.': unable to convert object of type ' . + get_class( $var ) ); + } elseif ( $var->interpreterId !== $this->id ) { + throw new MWException( + __METHOD__.': unable to convert function belonging to a different interpreter' + ); + } else { + return 'chunks[' . intval( $var->id ) . ']'; + } + case 'resource': + throw new MWException( __METHOD__.': unable to convert resource' ); + case 'NULL': + return 'nil'; + default: + throw new MWException( __METHOD__.': unable to convert variable of unknown type' ); + } + } + + protected function decodeHeader( $header ) { + $length = substr( $header, 0, 8 ); + $check = substr( $header, 8, 8 ); + if ( !preg_match( '/^[0-9a-f]+$/', $length ) || !preg_match( '/^[0-9a-f]+$/', $check ) ) { + throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); + } + $length = hexdec( $length ); + $check = hexdec( $check ); + if ( $length * 2 - 1 !== $check ) { + throw $this->engine->newException( 'scribunto-luastandalone-decode-error' ); + } + return $length; + } + + /** + * @throws ScribuntoException + */ + protected function checkValid() { + if ( !$this->proc ) { + $this->logger->error( __METHOD__ . ": process already terminated\n" ); + if ( $this->exitError ) { + throw $this->exitError; + } else { + throw $this->engine->newException( 'scribunto-luastandalone-gone' ); + } + } + } + + /** + * @throws ScribuntoException + */ + protected function handleIOError() { + $this->checkValid(); + + // Terminate, fetch the status, then close. proc_close()'s return + // value isn't helpful here because there's no way to differentiate a + // signal-kill from a normal exit. + proc_terminate( $this->proc ); + while ( true ) { + $status = proc_get_status( $this->proc ); + if ( $status === false ) { + // WTF? Let the caller throw an appropriate error. + return; + } + if ( !$status['running'] ) { + break; + } + usleep( 10000 ); // Give the killed process a chance to be scheduled + } + proc_close( $this->proc ); + $this->proc = false; + + // proc_open() sometimes uses a shell, check for shell-style signal reporting. + if ( !$status['signaled'] && ( $status['exitcode'] & 0x80 ) === 0x80 ) { + $status['signaled'] = true; + $status['termsig'] = $status['exitcode'] - 128; + } + + if ( $status['signaled'] ) { + if ( defined( 'SIGXCPU' ) && $status['termsig'] === SIGXCPU ) { + $this->exitError = $this->engine->newException( 'scribunto-common-timeout' ); + } else { + $this->exitError = $this->engine->newException( 'scribunto-luastandalone-signal', + [ 'args' => [ $status['termsig'] ] ] ); + } + } else { + $this->exitError = $this->engine->newException( 'scribunto-luastandalone-exited', + [ 'args' => [ $status['exitcode'] ] ] ); + } + throw $this->exitError; + } + + protected function debug( $msg ) { + if ( $this->enableDebug ) { + $this->logger->debug( "Lua: $msg\n" ); + } + } +} + +// @codingStandardsIgnoreLine Squiz.Classes.ValidClassName.NotCamelCaps +class Scribunto_LuaStandaloneInterpreterFunction { + public static $anyChunksDestroyed = []; + public static $activeChunkIds = []; + + /** + * @var int + */ + public $interpreterId; + + /** + * @var int + */ + public $id; + + /** + * @param int $interpreterId + * @param int $id + */ + function __construct( $interpreterId, $id ) { + $this->interpreterId = $interpreterId; + $this->id = $id; + $this->incrementRefCount(); + } + + function __clone() { + $this->incrementRefCount(); + } + + function __wakeup() { + $this->incrementRefCount(); + } + + function __destruct() { + $this->decrementRefCount(); + } + + private function incrementRefCount() { + if ( !isset( self::$activeChunkIds[$this->interpreterId] ) ) { + self::$activeChunkIds[$this->interpreterId] = [ $this->id => 1 ]; + } elseif ( !isset( self::$activeChunkIds[$this->interpreterId][$this->id] ) ) { + self::$activeChunkIds[$this->interpreterId][$this->id] = 1; + } else { + self::$activeChunkIds[$this->interpreterId][$this->id]++; + } + } + + private function decrementRefCount() { + if ( isset( self::$activeChunkIds[$this->interpreterId][$this->id] ) ) { + if ( --self::$activeChunkIds[$this->interpreterId][$this->id] <= 0 ) { + unset( self::$activeChunkIds[$this->interpreterId][$this->id] ); + self::$anyChunksDestroyed[$this->interpreterId] = true; + } + } else { + self::$anyChunksDestroyed[$this->interpreterId] = true; + } + } +} diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/MWServer.lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/MWServer.lua new file mode 100644 index 00000000..a8227bed --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/MWServer.lua @@ -0,0 +1,710 @@ +MWServer = {} + +--- Create a new MWServer object +function MWServer:new( interpreterId, intSize ) + interpreterId = tonumber( interpreterId ) + if not interpreterId then + error( "bad argument #1 to 'MWServer:new' (must be a number or convertible to a number)", 2 ) + end + intSize = tonumber( intSize ) + if intSize ~= 4 and intSize ~= 8 then + error( "bad argument #2 to 'MWServer:new' (must be 4 or 8)", 2 ) + end + + obj = { + interpreterId = interpreterId, + nextChunkId = 1, + chunks = {}, + xchunks = {}, + protectedFunctions = {}, + protectedEnvironments = {}, + baseEnv = {} + } + if intSize == 4 then + obj.intMax = 2147483648 + obj.intKeyMax = 2147483648 + else + -- Lua can't represent most larger integers, so they may as well be sent to PHP as floats. + obj.intMax = 9007199254740992 + obj.intKeyMax = 9223372036854775807 + end + setmetatable( obj, self ) + self.__index = self + + obj:init() + + return obj +end + +--- Initialise a new MWServer object +function MWServer:init() + self.baseEnv = self:newEnvironment() + for funcName, func in pairs( self ) do + if type(func) == 'function' then + self.protectedFunctions[func] = true + end + end + self.protectedEnvironments[_G] = true +end + +--- Serve requests until exit is requested +function MWServer:execute() + self:dispatch( nil ) + self:debug( 'MWServer:execute: returning' ) +end + +-- Convert a multiple-return-value or a ... into a count and a table +function MWServer:listToCountAndTable( ... ) + return select( '#', ... ), { ... } +end + +--- Call a PHP function +-- Raise an error if the PHP handler requests it. May return any number +-- of values. +-- +-- @param id The function ID, specified by a registerLibrary message +-- @param nargs Count of function arguments +-- @param args The function arguments +-- @return The return values from the PHP function +function MWServer:call( id, nargs, args ) + local result = self:dispatch( { + op = 'call', + id = id, + nargs = nargs, + args = args + } ) + if result.op == 'return' then + return unpack( result.values, 1, result.nvalues ) + elseif result.op == 'error' then + -- Raise an error in the actual user code that called the function + -- The level is 3 since our immediate caller is a closure + error( result.value, 3 ) + else + self:internalError( 'MWServer:call: unexpected result op' ) + end +end + +--- Handle a "call" message from PHP. Call the relevant function. +-- +-- @param message The message from PHP +-- @return A response message to send back to PHP +function MWServer:handleCall( message ) + if not self.chunks[message.id] then + return { + op = 'error', + value = 'function id ' .. message.id .. ' does not exist' + } + end + + local n, result = self:listToCountAndTable( xpcall( + function () + return self.chunks[message.id]( unpack( message.args, 1, message.nargs ) ) + end, + function ( err ) + return MWServer:attachTrace( err ) + end + ) ) + + if result[1] then + -- table.remove( result, 1 ) renumbers from 2 to #result. But #result + -- is not necessarily "right" if result contains nils. + result = { unpack( result, 2, n ) } + return { + op = 'return', + nvalues = n - 1, + values = result + } + else + if result[2].value and result[2].trace then + return { + op = 'error', + value = result[2].value, + trace = result[2].trace, + } + else + return { + op = 'error', + value = result[2] + } + end + end +end + +--- The xpcall() error handler for handleCall(). Modifies the error object +-- to include a structured backtrace +-- +-- @param err The error object +-- @return The new error object +function MWServer:attachTrace( err ) + return { + value = err, + trace = self:getStructuredTrace( 2 ) + } +end + +--- Handle a "loadString" message from PHP. +-- Load the function and return a chunk ID. +-- +-- @param message The message from PHP +-- @return A response message to send back to PHP +function MWServer:handleLoadString( message ) + if string.find( message.text, '\27Lua', 1, true ) then + return { + op = 'error', + value = 'cannot load code with a Lua binary chunk marker escape sequence in it' + } + end + local chunk, errorMsg = loadstring( message.text, message.chunkName ) + if chunk then + setfenv( chunk, self.baseEnv ) + local id = self:addChunk( chunk ) + return { + op = 'return', + nvalues = 1, + values = {id} + } + else + return { + op = 'error', + value = errorMsg + } + end +end + +--- Add a function value to the list of tracked chunks and return its associated ID. +-- Adding a chunk allows it to be referred to in messages from PHP. +-- +-- @param chunk The function value +-- @return The chunk ID +function MWServer:addChunk( chunk ) + local id = self.nextChunkId + self.nextChunkId = id + 1 + self.chunks[id] = chunk + self.xchunks[chunk] = id + return id +end + +--- Handle a "cleanupChunks" message from PHP. +-- Remove any chunks no longer referenced by PHP code. +-- +-- @param message The message from PHP +-- @return A response message to send back to PHP +function MWServer:handleCleanupChunks( message ) + for id, chunk in pairs( self.chunks ) do + if not message.ids[id] then + self.chunks[id] = nil + self.xchunks[chunk] = nil + end + end + + return { + op = 'return', + nvalues = 0, + values = {} + } +end + +--- Handle a "registerLibrary" message from PHP. +-- Add the relevant functions to the base environment. +-- +-- @param message The message from PHP +-- @return The response message +function MWServer:handleRegisterLibrary( message ) + local startPos = 1 + local component + if not self.baseEnv[message.name] then + self.baseEnv[message.name] = {} + end + local t = self.baseEnv[message.name] + + for name, id in pairs( message.functions ) do + t[name] = function( ... ) + return self:call( id, self:listToCountAndTable( ... ) ) + end + -- Protect the function against setfenv() + self.protectedFunctions[t[name]] = true + end + + return { + op = 'return', + nvalues = 0, + values = {} + } +end + +--- Handle a "wrapPhpFunction" message from PHP. +-- Create an anonymous function +-- +-- @param message The message from PHP +-- @return The response message +function MWServer:handleWrapPhpFunction( message ) + local id = message.id + local func = function( ... ) + return self:call( id, self:listToCountAndTable( ... ) ) + end + -- Protect the function against setfenv() + self.protectedFunctions[func] = true + + return { + op = 'return', + nvalues = 1, + values = { func } + } +end + +--- Handle a "getStatus" message from PHP +-- +-- @param message The request message +-- @return The response message +function MWServer:handleGetStatus( message ) + local nullRet = { + op = 'return', + nvalues = 0, + values = {} + } + local file = io.open( '/proc/self/stat' ) + if not file then + return nullRet + end + local s = file:read('*a') + file:close() + local t = {} + for token in string.gmatch(s, '[^ ]+') do + t[#t + 1] = token + end + if #t < 22 then + return nullRet + end + return { + op = 'return', + nvalues = 1, + values = {{ + pid = tonumber(t[1]), + time = tonumber(t[14]) + tonumber(t[15]) + tonumber(t[16]) + tonumber(t[17]), + vsize = tonumber(t[23]), + }} + } +end + +--- The main request/response loop +-- +-- Send a request message and return its matching reply message. Handle any +-- intervening requests (i.e. re-entrant calls) by dispatching them to the +-- relevant handler function. +-- +-- The request message may optionally be omitted, to listen for request messages +-- without first sending a request of its own. Such a dispatch() call will +-- continue running until termination is requested by PHP. Typically, PHP does +-- this with a SIGTERM signal. +-- +-- @param msgToPhp The message to send to PHP. Optional. +-- @return The matching response message +function MWServer:dispatch( msgToPhp ) + if msgToPhp then + self:sendMessage( msgToPhp, 'call' ) + end + while true do + local msgFromPhp = self:receiveMessage() + local msgToPhp + local op = msgFromPhp.op + if op == 'return' or op == 'error' then + return msgFromPhp + elseif op == 'call' then + msgToPhp = self:handleCall( msgFromPhp ) + self:sendMessage( msgToPhp, 'reply' ) + elseif op == 'loadString' then + msgToPhp = self:handleLoadString( msgFromPhp ) + self:sendMessage( msgToPhp, 'reply' ) + elseif op == 'registerLibrary' then + msgToPhp = self:handleRegisterLibrary( msgFromPhp ) + self:sendMessage( msgToPhp, 'reply' ) + elseif op == 'wrapPhpFunction' then + msgToPhp = self:handleWrapPhpFunction( msgFromPhp ) + self:sendMessage( msgToPhp, 'reply' ) + elseif op == 'cleanupChunks' then + msgToPhp = self:handleCleanupChunks( msgFromPhp ) + self:sendMessage( msgToPhp, 'reply' ) + elseif op == 'getStatus' then + msgToPhp = self:handleGetStatus( msgFromPhp ) + self:sendMessage( msgToPhp, 'reply' ) + elseif op == 'quit' then + self:debug( 'MWServer:dispatch: quit message received' ) + os.exit(0) + elseif op == 'testquit' then + self:debug( 'MWServer:dispatch: testquit message received' ) + os.exit(42) + else + self:internalError( "Invalid message operation" ) + end + end +end + +--- Write a message to the debug output stream. +-- Some day this may be configurable, currently it just unconditionally writes +-- the message to stderr. The PHP host will redirect those errors to /dev/null +-- by default, but it can be configured to send them to a file. +-- +-- @param s The message +function MWServer:debug( s ) + if ( type(s) == 'string' ) then + io.stderr:write( s .. '\n' ) + else + io.stderr:write( self:serialize( s ) .. '\n' ) + end +end + +--- Raise an internal error +-- Write a message to stderr and then exit with a failure status. This should +-- be called for errors which cannot be allowed to be caught with pcall(). +-- +-- This must be used for protocol errors, or indeed any error from a context +-- where a dispatch() call lies between the error source and a possible pcall() +-- handler. If dispatch() were terminated by a regular error() call, the +-- resulting protocol violation could lead to a deadlock. +-- +-- @param msg The error message +function MWServer:internalError( msg ) + io.stderr:write( debug.traceback( msg ) .. '\n' ) + os.exit( 1 ) +end + +--- Raise an I/O error +-- Helper function for errors from the io and file modules, which may optionally +-- return an informative error message as their second return value. +function MWServer:ioError( header, info ) + if type( info) == 'string' then + self:internalError( header .. ': ' .. info ) + else + self:internalError( header ) + end +end + +--- Send a message to PHP +-- @param msg The message table +-- @param direction 'call' or 'reply' +function MWServer:sendMessage( msg, direction ) + if not msg.op then + self:internalError( "MWServer:sendMessage: invalid message", 2 ) + end + self:debug('TX ==> ' .. msg.op) + + -- If we're making an outgoing call, let errors go to our caller. If we're + -- replying to a call from PHP, catch serialization errors and return them + -- to PHP. + local encMsg; + if direction == 'reply' then + local ok + ok, encMsg = pcall( self.encodeMessage, self, msg ) + if not ok then + self:debug('Serialization failed: ' .. encMsg) + self:debug('TX ==> error') + encMsg = self:encodeMessage( { op = 'error', value = encMsg } ) + end + else + encMsg = self:encodeMessage( msg ) + end + + local success, errorMsg = io.stdout:write( encMsg ) + if not success then + self:ioError( 'Write error', errorMsg ) + end + io.stdout:flush() +end + +--- Wait for a message from PHP and then decode and return it as a table +-- @return The received message +function MWServer:receiveMessage() + -- Read the header + local header, errorMsg = io.stdin:read( 16 ) + if header == nil and errorMsg == nil then + -- End of file on stdin, exit gracefully + os.exit(0) + end + + if not header or #header ~= 16 then + self:ioError( 'Read error', errorMsg ) + end + local length = self:decodeHeader( header ) + + -- Read the body + local body, errorMsg = io.stdin:read( length ) + if not body then + self:ioError( 'Read error', errorMsg ) + end + if #body ~= length then + self:ioError( 'Read error', errorMsg ) + end + + -- Unserialize it + msg = self:unserialize( body ) + self:debug('RX <== ' .. msg.op) + if msg.op == 'error' then + self:debug( 'Error: ' .. tostring( msg.value ) ) + end + return msg +end + +--- Encode a message for sending to PHP +function MWServer:encodeMessage( message ) + local serialized = self:serialize( message ) + local length = #serialized + local check = length * 2 - 1 + return string.format( '%08x%08x', length, check ) .. serialized +end + +-- Faster to create the table once than for each call to MWServer:serialize() +local serialize_replacements = { + ['\r'] = '\\r', + ['\n'] = '\\n', + ['\\'] = '\\\\', +} + +--- Convert a value to a string suitable for passing to PHP's unserialize(). +-- Note that the following replacements must be performed before calling +-- unserialize: +-- "\\r" => "\r" +-- "\\n" => "\n" +-- "\\\\" => "\\" +-- +-- @param var The value. +function MWServer:serialize( var ) + local done = {} + + local function isInteger( var, max ) + return type(var) == 'number' + and math.floor( var ) == var + and var >= -max + and var < max + end + + local function recursiveEncode( var, level ) + local t = type( var ) + if t == 'nil' then + return 'N;' + elseif t == 'number' then + if isInteger( var, self.intMax ) then + return 'i:' .. string.format( '%d', var ) .. ';' + elseif var < math.huge and var > -math.huge then + return 'd:' .. string.format( '%.17g', var ) .. ';' + elseif var == math.huge then + return 'd:INF;' + elseif var == -math.huge then + return 'd:-INF;' + else + return 'd:NAN;' + end + elseif t == 'string' then + return 's:' .. string.len( var ) .. ':"' .. var .. '";' + elseif t == 'boolean' then + if var then + return 'b:1;' + else + return 'b:0;' + end + elseif t == 'table' then + if done[var] then + error("Cannot pass circular reference to PHP") + end + done[var] = true + local buf = { '' } + local numElements = 0 + local seen = {} + for key, value in pairs(var) do + local k = key + local t = type( k ) + + -- Convert integers in range to look like standard integers. + -- Use tostring() for the rest. Reject all other non-strings. + if isInteger( k, self.intKeyMax ) then + k = string.format( '%d', k ) + elseif t == 'number' then + k = tostring( k ); + elseif t ~= 'string' then + error("Cannot use " .. t .. " as an array key when passing data from Lua to PHP"); + end + + -- Zend PHP doesn't really care whether integer keys are serialized + -- as ints or strings, it converts them correctly on unserialize. + -- But HHVM does depend on it, so keep doing it for now. + local n = nil + if k == '0' or k:match( '^-?[1-9]%d*$' ) then + n = tonumber( k ) + if n == -9223372036854775808 and k ~= '-9223372036854775808' then + -- Bad edge rounding + n = nil + end + end + if isInteger( n, self.intKeyMax ) then + buf[#buf + 1] = 'i:' .. k .. ';' + else + buf[#buf + 1] = recursiveEncode( k, level + 1 ) + end + + -- Detect collisions, e.g. { [0] = 'foo', ["0"] = 'bar' } + if seen[k] then + error( 'Collision for array key ' .. k .. ' when passing data from Lua to PHP' ); + end + seen[k] = true + + buf[#buf + 1] = recursiveEncode( value, level + 1 ) + numElements = numElements + 1 + end + buf[1] = 'a:' .. numElements .. ':{' + buf[#buf + 1] = '}' + return table.concat(buf) + elseif t == 'function' then + local id + if self.xchunks[var] then + id = self.xchunks[var] + else + id = self:addChunk(var) + end + return 'O:42:"Scribunto_LuaStandaloneInterpreterFunction":2:{s:13:"interpreterId";i:' .. + self.interpreterId .. ';s:2:"id";i:' .. id .. ';}' + elseif t == 'thread' then + error("Cannot pass thread to PHP") + elseif t == 'userdata' then + error("Cannot pass userdata to PHP") + else + error("Cannot pass unrecognised type to PHP") + end + end + + return recursiveEncode( var, 0 ):gsub( '[\r\n\\]', serialize_replacements ) +end + +--- Convert a Lua expression string to its corresponding value. +-- Convert any references of the form chunk[id] to the corresponding function +-- values. +function MWServer:unserialize( text ) + local func = loadstring( 'return ' .. text ) + if not func then + self:internalError( "MWServer:unserialize: invalid chunk" ) + end + -- Don't waste JIT cache space by storing every message in it + if jit then + jit.off( func ) + end + setfenv( func, { chunks = self.chunks } ) + return func() +end + +--- Decode a message header. +-- @param header The header string +-- @return The body length +function MWServer:decodeHeader( header ) + local length = string.sub( header, 1, 8 ) + local check = string.sub( header, 9, 16 ) + if not string.match( length, '^%x+$' ) or not string.match( check, '^%x+$' ) then + self:internalError( "Error decoding message header: " .. length .. '/' .. check ) + end + length = tonumber( length, 16 ) + check = tonumber( check, 16 ) + if length * 2 - 1 ~= check then + self:internalError( "Error decoding message header" ) + end + return length +end + +--- Get a traceback similar to the one from debug.traceback(), but as a table +-- rather than formatted as a string +-- +-- @param The level to start at: 1 for the function that called getStructuredTrace() +-- @return A table with the backtrace information +function MWServer:getStructuredTrace( level ) + level = level + 1 + local trace = {} + while true do + local rawInfo = debug.getinfo( level, 'nSl' ) + if rawInfo == nil then + break + end + local info = {} + for i, key in ipairs({'short_src', 'what', 'currentline', 'name', 'namewhat', 'linedefined'}) do + info[key] = rawInfo[key] + end + if string.match( info['short_src'], '/MWServer.lua$' ) then + info['short_src'] = 'MWServer.lua' + end + if string.match( rawInfo['short_src'], '/mw_main.lua$' ) then + info['short_src'] = 'mw_main.lua' + end + table.insert( trace, info ) + level = level + 1 + end + return trace +end + +--- Create a table to be used as a restricted environment, based on the current +-- global environment. +-- +-- @return The environment table +function MWServer:newEnvironment() + local allowedGlobals = { + -- base + "assert", + "error", + "getmetatable", + "ipairs", + "next", + "pairs", + "pcall", + "rawequal", + "rawget", + "rawset", + "select", + "setmetatable", + "tonumber", + "type", + "unpack", + "xpcall", + "_VERSION", + -- libs + "table", + "math" + } + + local env = {} + for i = 1, #allowedGlobals do + env[allowedGlobals[i]] = mw.clone( _G[allowedGlobals[i]] ) + end + + -- Cloning 'string' doesn't work right, because strings still use the old + -- 'string' as the metatable. So just copy it. + env.string = string + + env._G = env + env.tostring = function( val ) + return self:tostring( val ) + end + env.string.dump = nil + env.setfenv, env.getfenv = mw.makeProtectedEnvFuncs( + self.protectedEnvironments, self.protectedFunctions ) + env.debug = { + traceback = debug.traceback + } + env.os = { + date = os.date, + difftime = os.difftime, + time = os.time, + clock = os.clock + } + return env +end + +--- An implementation of tostring() which does not expose pointers. +function MWServer:tostring(val) + local mt = getmetatable( val ) + if mt and mt.__tostring then + return mt.__tostring(val) + end + local typeName = type(val) + local nonPointerTypes = {number = true, string = true, boolean = true, ['nil'] = true} + if nonPointerTypes[typeName] then + return tostring(val) + else + return typeName + end +end + +return MWServer diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/README b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/README new file mode 100644 index 00000000..1321af9b --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/README @@ -0,0 +1,39 @@ +The lua binaries were compiled from a manually patched 5.1.5 source tree, in order +to address security vulnerabilities reported in CVE-2014-5461. + +The Windows binaries were compiled statically and do not link against the Visual C++ +runtime libraries. They should function on Windows 7 and higher, and Windows Server +2008 and higher. + +The Linux binaries were compiled using "make generic". Lua does not use +autoconf, rather it encourages users to edit the makefile. The makefile patch +used to create the Linux binaries is in generic.patch. + +Compiling with "make generic" avoids introducing dynamic library dependencies +other than libc. This makes the binaries work on a far greater variety of Linux +distributions. + +More information on the patch process may be found in the corresponding Phabricator +issue: https://phabricator.wikimedia.org/T72541 + +The following copyright and license restrictions apply to these Lua binaries: + +Copyright © 2005-2011 Tecgraf/PUC-Rio and the Kepler Project. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/generic.patch b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/generic.patch new file mode 100644 index 00000000..b9262318 --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/generic.patch @@ -0,0 +1,22 @@ +diff -ru lua-5.1.5~/src/Makefile lua-5.1.5/src/Makefile +--- lua-5.1.5~/src/Makefile 2012-02-14 07:41:22.000000000 +1100 ++++ lua-5.1.5/src/Makefile 2012-04-11 16:13:04.548558332 +1000 +@@ -7,15 +7,15 @@ + # Your platform. See PLATS for possible values. + PLAT= none + +-CC= gcc +-CFLAGS= -O2 -Wall $(MYCFLAGS) ++CC=gcc ++CFLAGS=-fpie -O2 -Wall $(MYCFLAGS) + AR= ar rcu + RANLIB= ranlib + RM= rm -f + LIBS= -lm $(MYLIBS) + + MYCFLAGS= +-MYLDFLAGS= ++MYLDFLAGS=-pie + MYLIBS= + + # == END OF USER SETTINGS. NO NEED TO CHANGE ANYTHING BELOW THIS LINE ========= diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win32_bin/lua5.1.exe b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win32_bin/lua5.1.exe Binary files differnew file mode 100644 index 00000000..b75924f1 --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win32_bin/lua5.1.exe diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win64_bin/lua5.1.exe b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win64_bin/lua5.1.exe Binary files differnew file mode 100644 index 00000000..2bb611ec --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_Win64_bin/lua5.1.exe diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_32_generic/lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_32_generic/lua Binary files differnew file mode 100755 index 00000000..82656e64 --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_32_generic/lua diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/lua Binary files differnew file mode 100755 index 00000000..183f1a98 --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/lua diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_mac_lion_fat_generic/lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_mac_lion_fat_generic/lua Binary files differnew file mode 100755 index 00000000..5bcf7369 --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/binaries/lua5_1_5_mac_lion_fat_generic/lua diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/lua_ulimit.sh b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/lua_ulimit.sh new file mode 100755 index 00000000..a0b48ef0 --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/lua_ulimit.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +ulimit -St $1 +ulimit -Ht $2 +ulimit -v $3 +eval "exec $4" + diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/mw_main.lua b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/mw_main.lua new file mode 100644 index 00000000..651ab751 --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/mw_main.lua @@ -0,0 +1,8 @@ +package.path = arg[1] .. '/engines/LuaStandalone/?.lua;' .. + arg[1] .. '/engines/LuaCommon/lualib/?.lua' + +require('MWServer') +require('mwInit') +server = MWServer:new( arg[2], arg[3] ) +server:execute() + diff --git a/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/protocol.txt b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/protocol.txt new file mode 100644 index 00000000..ceb74d35 --- /dev/null +++ b/www/wiki/extensions/Scribunto/includes/engines/LuaStandalone/protocol.txt @@ -0,0 +1,176 @@ +The Lua standalone engine has a message-based protocol for communicating between +PHP and Lua. + +Messages start with a 16 byte header. The first 8 bytes are the body length in +hexadecimal. The second 8 bytes are (length * 2 - 1) also in hexadecimal. + +For messages passed from PHP to Lua, the body is encoded as a Lua expression. +The expression may reference a table in a variable called "chunks", which +contains an array of functions. + +Messages passed from Lua to PHP have their body encoded in PHP serialize() +format, and then "\\", "\r", and "\n" are replaced with "\\\\", "\\r", and +"\\n" to avoid issues with text-mode file handles. They may include instances +of function objects which have an "id" member for passing back to Lua as an +index in the chunk table. + +The expressions encoded into the message bodies are associative arrays. The "op" +member of the array gives the operation to be performed by the message. + +Every request message demands exactly one response message in reply. When a +request message is sent, the responder does not need to send the corresponding +response message as its next message. It may instead send its own request +message. In this way, a stack of pending requests can be accumulated. This +mechanism allows re-entrant and recursive calls. + +All numerically-indexed arrays should start from index 1 unless otherwise +specified. Note that the number of values in an array may not match what Lua's +'#' operator returns if the array contains nils. + +== Request messages sent from PHP to Lua == + +=== loadString === + +Load some executable Lua code (a "chunk") and return the resulting function ID. + +Message parameters: +* op: "loadString" +* text: The string to load +* chunkName: The name of the string, for use in error messages + +On success, the response message is: + +* op: "return" +* nvalues: 1 +* values: An array with a single element with the ID in it + +On failure, the response message is: + +* op: "error" +* value: The error message + +=== call === + +Call a Lua function. + +Message parameters: +* op: "call" +* id: The chunk ID +* nargs: Number of arguments, including nils +* args: The argument array + +On success, the response message is: + +* op: "return" +* nvalues: Number of return values, including nils +* values: All return values as an array + +On failure, the response message is: + +* op: "error" +* value: The value given to error(), usually an error message +* trace: A table giving a backtrace of the stack as it was when error() was + called, in a similar format to the one used by debug.getinfo(). Element 1 of + the table is the function that called error(), element 2 is the function that + called that, and so on. + +=== registerLibrary === + +Register a set of functions in the sandbox environment. + +Message parameters: +* op: "registerLibrary" +* name: The global variable name to register. May contain "." characters to + specify a global variable subtable. +* functions: An associative array mapping function name to ID + +On success, the response message is: + +* op: "return" +* nvalues: 0 +* values: An empty array + +On failure the response message is: + +* op: "error" +* value: The error message + +=== getStatus === + +Get status information about the Lua process. + +Message parameters: +* op: "getStatus" + +On success, the response message is: + +* op: "return" +* nvalues: 1 +* values: An array with a single element, which is an associative array mapping + status key to value. The status keys are: +** pid: The process identifier +** time: The amount of user and system time spent by the process, measured in clock ticks +** vsize: The virtual memory size in bytes +** rss: The resident set size in bytes + +On failure, the response message is: + +* op: "return" +* nvalues: 0 +* values: An empty array + +=== cleanupChunks === + +Tell Lua to release any chunks no longer referenced by PHP. + +Message parameters: +* op: "cleanupChunks" +* ids: Table with keys being the chunk IDs still referenced by PHP, and non-falsey values + +The response message is: + +* op: "return" +* nvalues: 0 +* values: An empty array + +=== quit === + +Request graceful shutdown. + +Message parameters: +* op: "quit" + +No return message will be sent. + +=== testquit === + +Request non-graceful shutdown, for testing. + +Message parameters: +* op: "testquit" + +No return message will be sent. + +== Request messages sent from Lua to PHP == + +=== call === + +Call a PHP function. + +Message parameters: +* op: "call" +* id: The function ID given by registerLibrary +* nargs: Number of arguments, including nils +* args: An array giving the function arguments + +On success, the response message is: + +* op: "return" +* nvalues: Number of return values, including nils +* values: All return values as an array + +On failure the response message is: + +* op: "error" +* value: The error message + |