/** * We have to keep a static reference of eio event streams * because if we don't garbage collection can unload eio's * underlying pipe via a system close() call before it's * finished and generate a SIGPIPE. */ public function __construct() { if (empty(self::$stream)) { \eio_init(); self::$stream = \eio_get_event_stream(); } $this->callableDecrementor = function () { \call_user_func($this->incrementor, -1); }; $this->incrementor = function ($increment) { switch ($increment) { case 1: case -1: $this->pending += $increment; break; default: throw new FilesystemException("Invalid pending event increment; 1 or -1 required"); } if ($this->pending === 0) { \Amp\disable($this->watcher); } elseif ($this->pending === 1) { \Amp\enable($this->watcher); } }; $this->watcher = \Amp\onReadable(self::$stream, function () { while (\eio_npending()) { \eio_poll(); } }, $options = ["enable" => false]); }
protected function output(string $message) { if (empty($this->isDead)) { $this->writeQueue[] = pack("N", \strlen($message)); $this->writeQueue[] = $message; \Amp\enable($this->writeWatcherId); } }
private function processSendResult($sendResult) { if ($sendResult === 0) { // Send didn't complete in the initial pg_send_query() call; // enable the write watcher. \Amp\enable($this->writeWatcher); return; } if ($sendResult === FALSE) { $this->failCurrentOperation(); } }
/** * @return Promise which will succeed after $str was written. It will contain the total number of already written bytes to the process */ public function write($str) { if (strlen($str) === 0) { throw new \InvalidArgumentException("String to be written cannot be empty"); } if (!$this->proc) { throw new \RuntimeException("Process was not yet launched"); } $this->writeBuf .= $str; \Amp\enable($this->stdin); $this->writeTotal += strlen($str); $deferred = $this->writeDeferreds[$this->writeTotal] = new Deferred(); return $deferred->promise(); }
public function parser(Client $client) : \Generator { $maxHeaderSize = $client->options->maxHeaderSize; $maxBodySize = $client->options->maxBodySize; $bodyEmitSize = $client->options->ioGranularity; $buffer = ""; do { // break potential references unset($traceBuffer, $protocol, $method, $uri, $headers); $traceBuffer = null; $headers = []; $contentLength = null; $isChunked = false; $protocol = null; $uri = null; $method = null; $parseResult = ["id" => 0, "trace" => &$traceBuffer, "protocol" => &$protocol, "method" => &$method, "uri" => &$uri, "headers" => &$headers, "body" => ""]; if ($client->parserEmitLock) { do { if (\strlen($buffer) > $maxHeaderSize + $maxBodySize) { \Amp\disable($client->readWatcher); $client->parserEmitLock = false; } $buffer .= yield; } while ($client->parserEmitLock); \Amp\enable($client->readWatcher); } $client->parserEmitLock = true; while (1) { $buffer = \ltrim($buffer, "\r\n"); if ($headerPos = \strpos($buffer, "\r\n\r\n")) { $startLineAndHeaders = \substr($buffer, 0, $headerPos + 2); $buffer = (string) \substr($buffer, $headerPos + 4); break; } elseif ($maxHeaderSize > 0 && strlen($buffer) > $maxHeaderSize) { $error = "Bad Request: header size violation"; break 2; } $buffer .= yield; } $startLineEndPos = \strpos($startLineAndHeaders, "\n"); $startLine = \rtrim(substr($startLineAndHeaders, 0, $startLineEndPos), "\r\n"); $rawHeaders = \substr($startLineAndHeaders, $startLineEndPos + 1); $traceBuffer = $startLineAndHeaders; if (!($method = \strtok($startLine, " "))) { $error = "Bad Request: invalid request line"; break; } if (!($uri = \strtok(" "))) { $error = "Bad Request: invalid request line"; break; } $protocol = \strtok(" "); if (stripos($protocol, "HTTP/") !== 0) { $error = "Bad Request: invalid request line"; break; } $protocol = \substr($protocol, 5); if ($protocol != "1.1" && $protocol != "1.0") { // @TODO eventually add an option to disable HTTP/2.0 support??? if ($protocol == "2.0") { $client->httpDriver = $this->http2; $client->requestParser = $client->httpDriver->parser($client); $client->requestParser->send("{$startLineAndHeaders}\r\n{$buffer}"); return; } else { $error = HttpDriver::BAD_VERSION; break; } } if ($rawHeaders) { if (\strpos($rawHeaders, "\n ") || \strpos($rawHeaders, "\n\t")) { $error = "Bad Request: multi-line headers deprecated by RFC 7230"; break; } if (!\preg_match_all(self::HEADER_REGEX, $rawHeaders, $matches)) { $error = "Bad Request: header syntax violation"; break; } list(, $fields, $values) = $matches; $headers = []; foreach ($fields as $index => $field) { $headers[$field][] = $values[$index]; } if ($headers) { $headers = \array_change_key_case($headers); } $contentLength = $headers["content-length"][0] ?? null; if (isset($headers["transfer-encoding"])) { $value = $headers["transfer-encoding"][0]; $isChunked = (bool) \strcasecmp($value, "identity"); } // @TODO validate that the bytes in matched headers match the raw input. If not there is a syntax error. } if ($contentLength > $maxBodySize) { $error = "Bad request: entity too large"; break; } elseif ($method == "HEAD" || $method == "TRACE" || $method == "OPTIONS" || $contentLength === 0) { // No body allowed for these messages $hasBody = false; } else { $hasBody = $isChunked || $contentLength; } if (!$hasBody) { ($this->parseEmitter)([HttpDriver::RESULT, $parseResult, null], $client); continue; } ($this->parseEmitter)([HttpDriver::ENTITY_HEADERS, $parseResult, null], $client); $body = ""; if ($isChunked) { while (1) { while (false === ($lineEndPos = \strpos($buffer, "\r\n"))) { $buffer .= yield; } $line = \substr($buffer, 0, $lineEndPos); $buffer = \substr($buffer, $lineEndPos + 2); $hex = \trim(\ltrim($line, "0")) ?: 0; $chunkLenRemaining = \hexdec($hex); if ($lineEndPos === 0 || $hex != \dechex($chunkLenRemaining)) { $error = "Bad Request: hex chunk size expected"; break 2; } if ($chunkLenRemaining === 0) { while (!isset($buffer[1])) { $buffer .= yield; } $firstTwoBytes = \substr($buffer, 0, 2); if ($firstTwoBytes === "\r\n") { $buffer = \substr($buffer, 2); break; // finished ($is_chunked loop) } do { if ($trailerSize = \strpos($buffer, "\r\n\r\n")) { $trailers = \substr($buffer, 0, $trailerSize + 2); $buffer = \substr($buffer, $trailerSize + 4); } else { $buffer .= yield; $trailerSize = \strlen($buffer); $trailers = null; } if ($maxHeaderSize > 0 && $trailerSize > $maxHeaderSize) { $error = "Trailer headers too large"; break 3; } } while (!isset($trailers)); if (\strpos($trailers, "\n ") || \strpos($trailers, "\n\t")) { $error = "Bad Request: multi-line trailers deprecated by RFC 7230"; break 2; } if (!\preg_match_all(self::HEADER_REGEX, $trailers, $matches)) { $error = "Bad Request: trailer syntax violation"; break 2; } list(, $fields, $values) = $matches; $trailers = []; foreach ($fields as $index => $field) { $trailers[$field][] = $values[$index]; } if ($trailers) { $trailers = \array_change_key_case($trailers); foreach (["transfer-encoding", "content-length", "trailer"] as $remove) { unset($trailers[$remove]); } if ($trailers) { $headers = \array_merge($headers, $trailers); } } break; // finished ($is_chunked loop) } elseif ($chunkLenRemaining > $maxBodySize) { $error = "Bad Request: excessive chunk size"; break 2; } else { $bodyBufferSize = 0; while (1) { $bufferLen = \strlen($buffer); // These first two (extreme) edge cases prevent errors where the packet boundary ends after // the \r and before the \n at the end of a chunk. if ($bufferLen === $chunkLenRemaining || $bufferLen === $chunkLenRemaining + 1) { $buffer .= yield; continue; } elseif ($bufferLen >= $chunkLenRemaining + 2) { $body .= substr($buffer, 0, $chunkLenRemaining); $buffer = substr($buffer, $chunkLenRemaining + 2); $bodyBufferSize += $chunkLenRemaining; } else { $body .= $buffer; $bodyBufferSize += $bufferLen; $chunkLenRemaining -= $bufferLen; } if ($bodyBufferSize >= $bodyEmitSize) { ($this->parseEmitter)([HttpDriver::ENTITY_PART, ["id" => 0, "body" => $body], null], $client); $body = ''; $bodyBufferSize = 0; } if ($bufferLen >= $chunkLenRemaining + 2) { $chunkLenRemaining = null; continue 2; // next chunk ($is_chunked loop) } else { $buffer = yield; } } } } } else { $bufferDataSize = \strlen($buffer); while ($bufferDataSize < $contentLength) { if ($bufferDataSize >= $bodyEmitSize) { ($this->parseEmitter)([HttpDriver::ENTITY_PART, ["id" => 0, "body" => $buffer], null], $client); $buffer = ""; $contentLength -= $bufferDataSize; } $buffer .= yield; $bufferDataSize = \strlen($buffer); } if ($bufferDataSize === $contentLength) { $body = $buffer; $buffer = ""; } else { $body = substr($buffer, 0, $contentLength); $buffer = (string) \substr($buffer, $contentLength); } } if ($body != "") { ($this->parseEmitter)([HttpDriver::ENTITY_PART, ["id" => 0, "body" => $body], null], $client); } ($this->parseEmitter)([HttpDriver::ENTITY_RESULT, $parseResult, null], $client); } while (true); // An error occurred... // stop parsing here ... ($this->parseEmitter)([HttpDriver::ERROR, $parseResult, $error], $client); while (1) { yield; } }
private function onWritable(string $watcherId, $socket, $client) { $bytesWritten = @fwrite($socket, $client->writeBuffer); if ($bytesWritten === false) { if (!is_resource($socket) || @feof($socket)) { $client->isDead = true; $this->close($client); } } elseif ($bytesWritten === strlen($client->writeBuffer)) { $client->writeBuffer = ""; \Amp\disable($watcherId); if ($client->onWriteDrain) { ($client->onWriteDrain)($client); } } else { $client->writeBuffer = substr($client->writeBuffer, $bytesWritten); \Amp\enable($watcherId); } }
private function timeout() { $this->now = $now = time(); foreach ($this->closeTimeouts as $clientId => $expiryTime) { if ($expiryTime < $now) { $this->unloadClient($this->clients[$clientId]); unset($this->closeTimeouts[$clientId]); } else { break; } } foreach ($this->heartbeatTimeouts as $clientId => $expiryTime) { if ($expiryTime < $now) { $client = $this->clients[$clientId]; unset($this->heartbeatTimeouts[$clientId]); $this->heartbeatTimeouts[$clientId] = $now + $this->heartbeatPeriod; $this->sendHeartbeatPing($client); } else { break; } } foreach ($this->lowCapacityClients as $id => $client) { $client->capacity += $this->maxBytesPerMinute / 60; if ($client->capacity > $this->maxBytesPerMinute) { unset($this->lowCapacityClients[$id]); } if ($client->capacity > 8192 && !isset($this->highFramesPerSecondClients[$id])) { \Amp\enable($client->readWatcher); } } foreach ($this->highFramesPerSecondClients as $id => $client) { $client->framesLastSecond -= $this->maxFramesPerSecond; if ($client->framesLastSecond < $this->maxFramesPerSecond) { unset($this->highFramesPerSecondClients[$id]); if ($client->capacity > 8192) { \Amp\enable($client->readWatcher); } } } }
private function onDeadIpcClient(string $readWatcherId, $ipcClient) { \Amp\cancel($readWatcherId); @fclose($ipcClient); unset($this->ipcClients[$readWatcherId]); $this->defunctProcessCount++; \Amp\enable($this->procGarbageWatcher); }
private function write(Rfc6455Client $client, $frameInfo) : Promise { if ($client->closedAt) { return new Failure(new ClientException()); } $msg = $frameInfo["msg"]; $len = \strlen($msg); $w = chr($frameInfo["fin"] << 7 | $frameInfo["rsv"] << 4 | $frameInfo["opcode"]); if ($len > 0xffff) { $w .= "" . pack('J', $len); } elseif ($len > 0x7d) { $w .= "~" . pack('n', $len); } else { $w .= chr($len); } $w .= $msg; \Amp\enable($client->writeWatcher); if ($client->writeBuffer != "") { if ($frameInfo["opcode"] >= 0x8) { $client->writeControlQueue[] = $w; $deferred = $client->writeDeferredControlQueue[] = new Deferred(); } else { $client->writeDataQueue[] = $w; $deferred = $client->writeDeferredDataQueue[] = new Deferred(); } } else { $client->writeBuffer = $w; $deferred = $client->writeDeferred = new Deferred(); } return $deferred->promise(); }
private function incrementPending() { if ($this->pending++ === 0) { \Amp\enable($this->watcher); } }
private function initializeIdleTimeout(SocketPoolStruct $poolStruct) { if (isset($poolStruct->idleWatcher)) { \Amp\enable($poolStruct->idleWatcher); } else { $poolStruct->idleWatcher = \Amp\once(function () use($poolStruct) { $this->unloadSocket($poolStruct->uri, $poolStruct->id); }, $poolStruct->msIdleTimeout); } }
/** * @param array $strings * @return Promise */ public function send(array $strings) { return \Amp\pipe($this->connect(), function () use($strings) { $payload = $this->parsePayload($strings); $this->outputBuffer .= $payload; $this->outputBufferLength += strlen($payload); if ($this->reader !== null) { \Amp\enable($this->reader); } if ($this->writer !== null) { \Amp\enable($this->writer); } }); }
private function onWritable(string $watcherId, $socket, $client) { $bytesWritten = @\fwrite($socket, $client->writeBuffer); if ($bytesWritten === false) { if (!\is_resource($socket) || @\feof($socket)) { $client->isDead = true; $this->close($client); } } else { if ($bytesWritten === \strlen($client->writeBuffer)) { $client->writeBuffer = ""; \Amp\disable($watcherId); if ($client->onWriteDrain) { ($client->onWriteDrain)($client); } } else { $client->writeBuffer = \substr($client->writeBuffer, $bytesWritten); \Amp\enable($watcherId); } $client->bufferSize -= $bytesWritten; if ($client->bufferPromisor && $client->bufferSize <= $client->options->softStreamCap) { $client->bufferPromisor->succeed(); $client->bufferPromisor = null; } } }
function __finalizeResult($state, $serverId, $requestId, $error = null, $result = null) { if (empty($state->pendingRequests[$requestId])) { return; } list($promisor, $name) = $state->pendingRequests[$requestId]; $server = $state->serverIdMap[$serverId]; unset($state->pendingRequests[$requestId], $server->pendingRequests[$requestId]); if (empty($server->pendingRequests)) { $state->serverIdTimeoutMap[$server->id] = $state->now + IDLE_TIMEOUT; \Amp\disable($server->watcherId); \Amp\enable($state->serverTimeoutWatcher); } if ($error) { $promisor->fail($error); } else { foreach ($result as $type => $records) { $minttl = INF; foreach ($records as list(, $ttl)) { if ($ttl && $minttl > $ttl) { $minttl = $ttl; } } $state->arrayCache->set("{$name}#{$type}", $records, $minttl); } $promisor->succeed($result); } }
/** * @return Promise which will succeed after $str was written. It will contain the total number of already written bytes to the process */ public function write($str) { assert(strlen($str) > 0); if (!$this->proc) { throw new \RuntimeException("Process was not yet launched"); } $this->writeBuf .= $str; \Amp\enable($this->stdin); $this->writeTotal += strlen($str); $deferred = $this->writeDeferreds[$this->writeTotal] = new Deferred(); return $deferred->promise(); }
public static function reader($watcher, $socket, $info) { $buffer =& $info[0]; $data = @fread($socket, 8192); if ($data != "") { if ($buffer == "") { \Amp\enable($info[1]); } $buffer .= $data; if (\strlen($buffer) > self::MAX_INTERMEDIARY_BUFFER) { \Amp\disable($watcher); } } elseif (!is_resource($socket) || @feof($socket)) { \Amp\cancel($watcher); if ($buffer == "") { \Amp\cancel($info[1]); } else { $info[2] = true; } } }