private function collectProcessGarbage() { foreach ($this->processes as $key => $procHandle) { $info = proc_get_status($procHandle); if ($info["running"]) { continue; } $this->defunctProcessCount--; proc_close($procHandle); unset($this->processes[$key]); if ($this->expectedFailures > 0) { $this->expectedFailures--; continue; } if (!$this->stopPromisor) { $this->spawn(); } } // If we've reaped all known dead processes we can stop checking if (empty($this->defunctProcessCount)) { \Amp\disable($this->procGarbageWatcher); } if ($this->stopPromisor && empty($this->processes)) { \Amp\cancel($this->procGarbageWatcher); if ($this->stopPromisor !== true) { \Amp\immediately([$this->stopPromisor, "succeed"]); } $this->stopPromisor = true; } }
private function onWritable() { if ($this->isDead) { return; } if ($this->writeBuffer === "") { $this->writeBuffer = implode("", $this->writeQueue); $this->writeQueue = []; } $bytes = @fwrite($this->ipcSock, $this->writeBuffer); if ($bytes === false) { $this->onDeadIpcSock(); return; } if ($bytes !== \strlen($this->writeBuffer)) { $this->writeBuffer = substr($this->writeBuffer, $bytes); return; } if ($this->writeQueue) { $this->writeBuffer = implode("", $this->writeQueue); $this->writeQueue = []; return; } $this->writeBuffer = ""; \Amp\disable($this->writeWatcherId); }
private function onWritable() { if ($this->isDead) { return; } if ($this->writeBuffer === "") { $this->writeBuffer = implode("", $this->writeQueue); $this->writeQueue = []; } $bytes = @fwrite($this->ipcSock, $this->writeBuffer); if ($bytes === false) { $this->onDeadIpcSock(); return; } if ($bytes !== \strlen($this->writeBuffer)) { $this->writeBuffer = substr($this->writeBuffer, $bytes); return; } if ($this->writeQueue) { $this->writeBuffer = implode("", $this->writeQueue); $this->writeQueue = []; return; } $this->writeBuffer = ""; if ($this->stopPromisor) { \Amp\cancel($this->writeWatcherId); $promisor = $this->stopPromisor; $this->stopPromisor = null; $promisor->succeed(); } else { \Amp\disable($this->writeWatcherId); } }
private function checkoutExistingSocket($uri, $options) { if (empty($this->sockets[$uri])) { return null; } $needsRebind = false; foreach ($this->sockets[$uri] as $socketId => $poolStruct) { if (!$poolStruct->isAvailable) { continue; } elseif ($this->isSocketDead($poolStruct->resource)) { unset($this->sockets[$uri][$socketId]); } elseif (($bindToIp = @stream_context_get_options($poolStruct->resource)['socket']['bindto']) && $bindToIp == $options[self::OP_BINDTO]) { $poolStruct->isAvailable = false; \Amp\disable($poolStruct->idleWatcher); return $poolStruct->resource; } elseif ($bindToIp) { $needsRebind = true; } else { $poolStruct->isAvailable = false; \Amp\disable($poolStruct->idleWatcher); return $poolStruct->resource; } } $this->needsRebind = $needsRebind; return null; }
/** * 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]); }
private function onWritable() { $flush = \pg_flush($this->db); if ($flush) { // Write was fully flushed; we're finished and can disable the watcher. \Amp\disable($this->writeWatcher); return; } if ($flush === FALSE) { $this->failCurrentOperation(); } }
/** * @param int $buffer one of the self::BUFFER_* constants. Determines whether it will buffer the stdout and/or stderr data internally * @return Promise is updated with ["out", $data] or ["err", $data] for data received on stdout or stderr * That Promise will be resolved to a stdClass object with stdout, stderr (when $buffer is true), exit (holding exit code) and signal (only present when terminated via signal) properties */ public function exec($buffer = self::BUFFER_NONE) { if ($this->proc) { throw new \RuntimeException("Process was already launched"); } $cwd = isset($this->options["cwd"]) ? $this->options["cwd"] : NULL; $env = isset($this->options["env"]) ? $this->options["env"] : NULL; if (stripos(PHP_OS, "WIN") !== 0) { $fds = [["pipe", "r"], ["pipe", "w"], ["pipe", "w"], ["pipe", "w"]]; $this->proc = @proc_open("{$this->cmd}; echo \$? >&3", $fds, $pipes, $cwd, $env, $this->options); } else { $options = $this->options; $options["bypass_shell"] = true; $fds = [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]]; $this->proc = @proc_open($this->cmd, $fds, $pipes, $cwd, $env, $options); } if (!$this->proc) { return new Failure(new \RuntimeException("Failed executing command: {$this->cmd}")); } $this->writeBuf = ""; $this->writeTotal = 0; $this->writeCur = 0; stream_set_blocking($pipes[0], false); stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); if (isset($pipes[3])) { stream_set_blocking($pipes[3], false); $this->openPipes = 3; } else { $this->openPipes = 2; } $this->deferred = new Deferred(); $result = new \stdClass(); if ($buffer & self::BUFFER_STDOUT) { $result->stdout = ""; } if ($buffer & self::BUFFER_STDERR) { $result->stderr = ""; } $cleanup = function () use($result) { \Amp\cancel($this->stdin); $deferreds = $this->writeDeferreds; $this->writeDeferreds = []; $status = \proc_get_status($this->proc); if ($status["running"] === false && $status["signaled"]) { $result->signal = $status["termsig"]; $result->exit = $status["exitcode"]; } if (!isset($this->exit)) { $result->exit = proc_close($this->proc); } unset($this->proc); $this->deferred->succeed($result); foreach ($deferreds as $deferred) { $deferred->fail(new \Exception("Write could not be completed, process finished")); } }; $this->stdout = \Amp\onReadable($pipes[1], function ($watcher, $sock) use($result, $cleanup) { if ("" == ($data = @\fread($sock, 8192))) { \Amp\cancel($watcher); if (--$this->openPipes == 0) { \Amp\immediately($cleanup); } } else { if (isset($result->stdout)) { $result->stdout .= $data; } $this->deferred->update(["out", $data]); } }); $this->stderr = \Amp\onReadable($pipes[2], function ($watcher, $sock) use($result, $cleanup) { if ("" == ($data = @\fread($sock, 8192))) { \Amp\cancel($watcher); if (--$this->openPipes == 0) { \Amp\immediately($cleanup); } } else { if (isset($result->stderr)) { $result->stderr .= $data; } $this->deferred->update(["err", $data]); } }); $this->stdin = \Amp\onWritable($pipes[0], function ($watcher, $sock) { $this->writeCur += @\fwrite($sock, $this->writeBuf); if ($this->writeCur == $this->writeTotal) { \Amp\disable($watcher); } while (($next = key($this->writeDeferreds)) !== null && $next <= $this->writeCur) { $this->writeDeferreds[$next]->succeed($this->writeCur); unset($this->writeDeferreds[$next]); } }, ["enable" => false]); if (isset($pipes[3])) { $this->exit = \Amp\onReadable($pipes[3], function ($watcher, $sock) use($result, $cleanup) { stream_set_blocking($sock, true); // it should never matter, but just to be really 100% sure. $result->exit = (int) stream_get_contents($sock); \Amp\cancel($watcher); if (--$this->openPipes == 0) { \Amp\immediately($cleanup); } }); } return $this->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); } }
public function onWritable($watcherId, $socket, Rfc6455Client $client) { $bytes = @fwrite($socket, $client->writeBuffer); $client->bytesSent += $bytes; if ($bytes != \strlen($client->writeBuffer)) { $client->writeBuffer = substr($client->writeBuffer, $bytes); } elseif ($bytes == 0 && $client->closedAt && (!is_resource($socket) || @feof($socket))) { // usually read watcher cares about aborted TCP connections, but when // $client->closedAt is true, it might be the case that read watcher // is already cancelled and we need to ensure that our writing promise // is fulfilled in unloadClient() with a failure unset($this->closeTimeouts[$client->id]); $this->unloadClient($client); } else { $client->framesSent++; $client->writeDeferred->succeed(); if ($client->writeControlQueue) { $key = key($client->writeControlQueue); $client->writeBuffer = $client->writeControlQueue[$key]; $client->lastSentAt = $this->now; $client->writeDeferred = $client->writeDeferredControlQueue[$key]; unset($client->writeControlQueue[$key], $client->writeDeferredControlQueue[$key]); while (\strlen($client->writeBuffer) < 65536 && $client->writeControlQueue) { $key = key($client->writeControlQueue); $client->writeBuffer .= $client->writeControlQueue[$key]; $client->writeDeferredControlQueue[$key]->succeed($client->writeDeferred); unset($client->writeControlQueue[$key], $client->writeDeferredControlQueue[$key]); } while (\strlen($client->writeBuffer) < 65536 && $client->writeDataQueue) { $key = key($client->writeDataQueue); $client->writeBuffer .= $client->writeDataQueue[$key]; $client->writeDeferredDataQueue[$key]->succeed($client->writeDeferred); unset($client->writeDataQueue[$key], $client->writeDeferredDataQueue[$key]); } } elseif ($client->closedAt) { @stream_socket_shutdown($socket, STREAM_SHUT_WR); \Amp\cancel($watcherId); $client->writeWatcher = null; $client->writeDeferred = null; $client->writeBuffer = ""; } elseif ($client->writeDataQueue) { $key = key($client->writeDataQueue); $client->writeBuffer = $client->writeDataQueue[$key]; $client->lastDataSentAt = $this->now; $client->lastSentAt = $this->now; $client->writeDeferred = $client->writeDeferredDataQueue[$key]; unset($client->writeDataQueue[$key], $client->writeDeferredDataQueue[$key]); while (\strlen($client->writeBuffer) < 65536 && $client->writeDataQueue) { $key = key($client->writeDataQueue); $client->writeBuffer .= $client->writeDataQueue[$key]; $client->writeDeferredDataQueue[$key]->succeed($client->writeDeferred); unset($client->writeDataQueue[$key], $client->writeDeferredDataQueue[$key]); } } else { $client->writeDeferred = null; $client->writeBuffer = ""; \Amp\disable($watcherId); } } }
private function decrementPending() { if ($this->pending-- === 1) { \Amp\disable($this->watcher); } }
/** * @param $watcherId */ public function onWrite($watcherId) { if ($this->outputBufferLength === 0) { \Amp\disable($watcherId); return; } $bytes = fwrite($this->socket, $this->outputBuffer); if ($bytes === 0) { $this->state = self::STATE_DISCONNECTED; throw new ConnectException("Connection went away (write)", $code = 1); } else { $this->outputBuffer = (string) substr($this->outputBuffer, $bytes); $this->outputBufferLength -= $bytes; } }
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); } }
/** * @param int $buffer one of the self::BUFFER_* constants. Determines whether it will buffer the stdout and/or stderr data internally * @return Promise is updated with ["out", $data] or ["err", $data] for data received on stdout or stderr * That Promise will be resolved to a stdClass object with stdout, stderr (when $buffer is true), exit (holding exit code) and signal (only present when terminated via signal) properties */ public function exec($buffer = self::BUFFER_NONE) { if ($this->proc) { throw new \RuntimeException("Process was already launched"); } $fds = [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]]; $cwd = isset($this->options["cwd"]) ? $this->options["cwd"] : NULL; $env = isset($this->options["env"]) ? $this->options["env"] : NULL; if (!($this->proc = @proc_open($this->cmd, $fds, $pipes, $cwd, $env, $this->options))) { return new Failure(new \RuntimeException("Failed executing command: {$this->cmd}")); } $this->writeBuf = ""; $this->writeTotal = 0; $this->writeCur = 0; stream_set_blocking($pipes[0], false); stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); $this->deferred = new Deferred(); $result = new \stdClass(); if ($buffer & self::BUFFER_STDOUT) { $result->stdout = ""; } if ($buffer & self::BUFFER_STDERR) { $result->stderr = ""; } $this->stdout = \Amp\onReadable($pipes[1], function ($watcher, $sock) use($result) { if ("" == ($data = @fread($sock, 8192))) { \Amp\cancel($watcher); \Amp\cancel($this->stdin); \Amp\immediately(function () use($result) { $status = proc_get_status($this->proc); assert($status["running"] === false); if ($status["signaled"]) { $result->signal = $status["termsig"]; } $result->exit = $status["exitcode"]; $this->proc = NULL; $this->deferred->succeed($result); foreach ($this->writeDeferreds as $deferred) { $deferred->fail(new \Exception("Write could not be completed, process finished")); } $this->writeDeferreds = []; }); } else { isset($result->stdout) && ($result->stdout .= $data); $this->deferred->update(["out", $data]); } }); $this->stderr = \Amp\onReadable($pipes[2], function ($watcher, $sock) use($result) { if ("" == ($data = @fread($sock, 8192))) { \Amp\cancel($watcher); } else { isset($result->stderr) && ($result->stderr .= $data); $this->deferred->update(["err", $data]); } }); $this->stdin = \Amp\onWritable($pipes[0], function ($watcher, $sock) { $this->writeCur += @fwrite($sock, $this->writeBuf); if ($this->writeCur == $this->writeTotal) { \Amp\disable($watcher); } while (($next = key($this->writeDeferreds)) !== null && $next <= $this->writeCur) { $this->writeDeferreds[$next]->succeed($this->writeCur); unset($this->writeDeferreds[$next]); } }, ["enable" => false]); return $this->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; } } }