diff --git a/src/danog/MadelineProto/API.php b/src/danog/MadelineProto/API.php index e9218a33..c0b7a8f5 100644 --- a/src/danog/MadelineProto/API.php +++ b/src/danog/MadelineProto/API.php @@ -92,7 +92,7 @@ class API extends InternalDoc private $wrapper; /** - * Global session unlock callback + * Global session unlock callback. * * @var callback */ @@ -146,6 +146,7 @@ class API extends InternalDoc unset($unserialized); + $this->API->wrapper = $this->wrapper; yield from $this->API->initAsynchronously(); $this->APIFactory(); $this->logger->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE); @@ -163,6 +164,7 @@ class API extends InternalDoc $settings['app_info']['api_hash'] = $app['api_hash']; } $this->API = new MTProto($settings); + $this->API->wrapper = $this->wrapper; yield from $this->API->initAsynchronously(); $this->APIFactory(); $this->logger->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE); @@ -186,14 +188,18 @@ class API extends InternalDoc { $this->init(); if (!$this->oldInstance) { - $this->logger->logger('Shutting down MadelineProto (API)'); + $this->logger->logger('Shutting down MadelineProto ('.\explode('\\', \get_class($this))[2].')'); if ($this->API) { $this->API->destructing = true; $this->API->unreference(); } $this->destructing = true; - Tools::wait($this->wrapper->serialize()); - if ($this->unlock) ($this->unlock)(); + if (isset($this->wrapper)) { + Tools::wait($this->wrapper->serialize()); + } + if ($this->unlock) { + ($this->unlock)(); + } } else { $this->logger->logger('Shutting down MadelineProto (old deserialized instance of API)'); } diff --git a/src/danog/MadelineProto/APIWrapper.php b/src/danog/MadelineProto/APIWrapper.php index dfb05f93..85f23d68 100644 --- a/src/danog/MadelineProto/APIWrapper.php +++ b/src/danog/MadelineProto/APIWrapper.php @@ -160,6 +160,18 @@ final class APIWrapper return $this->factory; } + /** + * Get IPC path. + * + * @internal + * + * @return string + */ + public function getIpcPath(): string + { + return (new SessionPaths($this->session))->getIpcPath(); + } + /** * Serialize session. * @@ -171,6 +183,10 @@ final class APIWrapper Logger::log("Not serializing, no session"); return new Success(); } + if ($this->API instanceof FastAPI) { + Logger::log("Not serializing, IPC client"); + return new Success(); + } return Tools::callFork((function (): \Generator { if (isset($this->API->flushSettings) && $this->API->flushSettings) { $this->API->flushSettings = false; @@ -183,9 +199,9 @@ final class APIWrapper yield from $this->API->initAsynchronously(); } $this->serialized = \time(); - $realpaths = Serialization::realpaths($this->session); + $realpaths = new SessionPaths($this->session); Logger::log('Waiting for exclusive lock of serialization lockfile...'); - $unlock = yield Tools::flock($realpaths['lockfile'], LOCK_EX); + $unlock = yield Tools::flock($realpaths->getLockPath(), LOCK_EX); Logger::log('Lock acquired, serializing'); try { if (!$this->gettingApiId) { @@ -198,8 +214,8 @@ final class APIWrapper $this->API->settings['logger']['logger_param'] = [$this->API, 'noop']; } } - $wrote = yield put($realpaths['tempfile'], \serialize($this)); - yield renameAsync($realpaths['tempfile'], $realpaths['file']); + $wrote = yield put($realpaths->getTempPath(), \serialize($this)); + yield renameAsync($realpaths->getTempPath(), $realpaths->getSessionPath()); } finally { if (!$this->gettingApiId) { $this->API->settings['updates']['callback'] = $update_closure; diff --git a/src/danog/MadelineProto/AbstractAPIFactory.php b/src/danog/MadelineProto/AbstractAPIFactory.php index b0f5270c..66df9683 100644 --- a/src/danog/MadelineProto/AbstractAPIFactory.php +++ b/src/danog/MadelineProto/AbstractAPIFactory.php @@ -168,7 +168,6 @@ abstract class AbstractAPIFactory extends AsyncConstruct $name = $this->namespace.$name; $aargs = isset($arguments[1]) && \is_array($arguments[1]) ? $arguments[1] : []; $aargs['apifactory'] = true; - $aargs['datacenter'] = $this->API->datacenter->curdc; $args = isset($arguments[0]) && \is_array($arguments[0]) ? $arguments[0] : []; return yield from $this->API->methodCallAsyncRead($name, $args, $aargs); } diff --git a/src/danog/MadelineProto/ApiWrappers/Start.php b/src/danog/MadelineProto/ApiWrappers/Start.php index fc9a5514..7c32c520 100644 --- a/src/danog/MadelineProto/ApiWrappers/Start.php +++ b/src/danog/MadelineProto/ApiWrappers/Start.php @@ -37,6 +37,9 @@ trait Start */ private function APIStart(array $settings): \Generator { + if (\defined(\MADELINE_WORKER::class)) { + throw new \danog\MadelineProto\Exception('Not inited!'); + } if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { $stdout = getStdout(); yield $stdout->write('You did not define a valid API ID/API hash. Do you want to define it now manually, or automatically? (m/a) diff --git a/src/danog/MadelineProto/CombinedAPI.php b/src/danog/MadelineProto/CombinedAPI.php index cf1d589b..9adab900 100644 --- a/src/danog/MadelineProto/CombinedAPI.php +++ b/src/danog/MadelineProto/CombinedAPI.php @@ -36,25 +36,25 @@ class CombinedAPI { \set_error_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionErrorHandler']); \danog\MadelineProto\Magic::classExists(); - $realpaths = Serialization::realpaths($session); - $this->session = $realpaths['file']; + $realpaths = new SessionPaths($session); + $this->session = $realpaths->getSessionPath(); foreach ($paths as $path => $settings) { $this->addInstance($path, $settings); } - if (\file_exists($realpaths['file'])) { - if (!\file_exists($realpaths['lockfile'])) { - \touch($realpaths['lockfile']); + if (\file_exists($realpaths->getSessionPath())) { + if (!\file_exists($realpaths->getLockPath())) { + \touch($realpaths->getLockPath()); \clearstatcache(); } - $realpaths['lockfile'] = \fopen($realpaths['lockfile'], 'r'); + $lock = \fopen($realpaths->getLockPath(), 'r'); \danog\MadelineProto\Logger::log('Waiting for shared lock of serialization lockfile...'); - \flock($realpaths['lockfile'], LOCK_SH); + \flock($lock, LOCK_SH); \danog\MadelineProto\Logger::log('Shared lock acquired, deserializing...'); try { - $tounserialize = \file_get_contents($realpaths['file']); + $tounserialize = \file_get_contents($realpaths->getSessionPath()); } finally { - \flock($realpaths['lockfile'], LOCK_UN); - \fclose($realpaths['lockfile']); + \flock($lock, LOCK_UN); + \fclose($lock); } $deserialized = \unserialize($tounserialize); /*foreach ($deserialized['instance_paths'] as $path) { @@ -115,21 +115,21 @@ class CombinedAPI $filename = $this->session; } Logger::log(\danog\MadelineProto\Lang::$current_lang['serializing_madelineproto']); - $realpaths = Serialization::realpaths($filename); - if (!\file_exists($realpaths['lockfile'])) { - \touch($realpaths['lockfile']); + $realpaths = new SessionPaths($filename); + if (!\file_exists($realpaths->getLockPath())) { + \touch($realpaths->getLockPath()); \clearstatcache(); } - $realpaths['lockfile'] = \fopen($realpaths['lockfile'], 'w'); + $lock = \fopen($realpaths->getLockPath(), 'w'); \danog\MadelineProto\Logger::log('Waiting for exclusive lock of serialization lockfile...'); - \flock($realpaths['lockfile'], LOCK_EX); + \flock($lock, LOCK_EX); \danog\MadelineProto\Logger::log('Lock acquired, serializing'); try { - $wrote = \file_put_contents($realpaths['tempfile'], \serialize(['event_handler' => $this->event_handler, 'event_handler_instance' => $this->event_handler_instance, 'instance_paths' => $this->instance_paths])); - \rename($realpaths['tempfile'], $realpaths['file']); + $wrote = \file_put_contents($realpaths->getTempPath(), \serialize(['event_handler' => $this->event_handler, 'event_handler_instance' => $this->event_handler_instance, 'instance_paths' => $this->instance_paths])); + \rename($realpaths->getTempPath(), $realpaths->getSessionPath()); } finally { - \flock($realpaths['lockfile'], LOCK_UN); - \fclose($realpaths['lockfile']); + \flock($lock, LOCK_UN); + \fclose($lock); } $this->serialized = \time(); return $wrote; diff --git a/src/danog/MadelineProto/FastAPI.php b/src/danog/MadelineProto/FastAPI.php new file mode 100644 index 00000000..2d3ac095 --- /dev/null +++ b/src/danog/MadelineProto/FastAPI.php @@ -0,0 +1,139 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto; + +use Amp\File\StatCache; +use danog\MadelineProto\Ipc\Client; +use danog\MadelineProto\Ipc\Server; + +use function Amp\File\exists; +use function Amp\File\get; +use function Amp\File\isfile; + +/** + * IPC API wrapper for MadelineProto. + */ +class FastAPI extends API +{ + /** + * Constructor function. + * + * @param string $session Session name + * @param array $settings Settings + * + * @return void + */ + public function __magic_construct(string $session, array $settings = []): void + { + Magic::classExists(true); + $this->setInitPromise($this->__construct_async($session, $settings)); + foreach (\get_class_vars(APIFactory::class) as $key => $var) { + if (\in_array($key, ['namespace', 'API', 'lua', 'async', 'asyncAPIPromise', 'methods'])) { + continue; + } + if (!$this->{$key}) { + $this->{$key} = $this->exportNamespace($key); + } + } + } + /** + * Async constructor function. + * + * @param string $session Session name + * @param array $settings Settings + * + * @return \Generator + */ + public function __construct_async(string $session, array $settings = []): \Generator + { + $this->logger = Logger::constructorFromSettings($settings); + $session = new SessionPaths($session); + yield from $this->checkInit($session, $settings); + if (!(yield exists($session->getIpcPath()))) { + yield from Server::startMe($session); + $inited = false; + for ($x = 0; $x < 3; $x++) { + $this->logger->logger("Waiting for IPC server to start..."); + yield Tools::sleep(0.1); + if (yield from $this->checkInit($session, $settings)) { + $inited = true; + break; + } + yield from Server::startMe($session); + } + if (!$inited) { + throw new Exception("The IPC server isn't running, please check logs!"); + } + } + $this->API = new Client($session->getIpcPath(), $this->logger); + yield from $this->API->initAsynchronously(); + $this->logger->logger(Lang::$current_lang['madelineproto_ready'], Logger::NOTICE); + } + /** + * Try initializing session. + * + * @param SessionPaths $session Session paths + * @param array $settings Settings + * + * @return \Generator + */ + private function checkInit(SessionPaths $session, array $settings): \Generator + { + StatCache::clear($session->getIpcPath()); + StatCache::clear($session->getSessionPath()); + if (!(yield exists($session->getSessionPath())) + || (yield exists($session->getIpcPath()) + && yield isfile($session->getIpcPath()) + && yield get($session->getIpcPath()) === 'not inited') + ) { // Should init API ID|session + Logger::log("Session not initialized, initializing it now..."); + $API = new API($session->getSessionPath(), $settings); + yield from $API->initAsynchronously(); + unset($API); + return false; // Should start IPC server + } + return true; // All good, IPC server is running + } + /** + * Start MadelineProto and the event handler (enables async). + * + * Also initializes error reporting, catching and reporting all errors surfacing from the event loop. + * + * @param string $eventHandler Event handler class name + * + * @return void + */ + public function startAndLoop(string $eventHandler): void + { + throw new Exception("Can't use ".__FUNCTION__." in an IPC client instance, please use a full ".API::class." instance, instead!"); + } + /** + * Start multiple instances of MadelineProto and the event handlers (enables async). + * + * @param API[] $instances Instances of madeline + * @param string[]|string $eventHandler Event handler(s) + * + * @return Promise + */ + public static function startAndLoopMulti(array $instances, $eventHandler): void + { + throw new Exception("Can't use ".__FUNCTION__." in an IPC client instance, please use a full ".API::class." instance, instead!"); + } +} diff --git a/src/danog/MadelineProto/Ipc/Client.php b/src/danog/MadelineProto/Ipc/Client.php new file mode 100644 index 00000000..62ccd071 --- /dev/null +++ b/src/danog/MadelineProto/Ipc/Client.php @@ -0,0 +1,163 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto\Ipc; + +use Amp\Deferred; +use Amp\Ipc\Sync\ChannelledSocket; +use danog\MadelineProto\API; +use danog\MadelineProto\Async\AsyncConstruct; +use danog\MadelineProto\Exception; +use danog\MadelineProto\Logger; +use danog\MadelineProto\Tools; + +use function Amp\Ipc\connect; + +/** + * IPC client. + */ +class Client extends AsyncConstruct +{ + use \danog\MadelineProto\Wrappers\Start; + use \danog\MadelineProto\Wrappers\Templates; + + /** + * IPC server socket. + */ + private ChannelledSocket $server; + /** + * Requests promise array. + */ + private array $requests = []; + /** + * Logger instance. + */ + public Logger $logger; + /** + * Constructor function. + * + * @param string $ipcPath IPC socket path + * @param Logger $logger Logger + */ + public function __construct(string $ipcPath, Logger $logger) + { + $this->setInitPromise($this->__construct_async($ipcPath, $logger)); + } + /** + * Constructor function. + * + * @param string $ipcPath IPC socket path + * @param Logger $logger Logger + * + * @return \Generator + */ + public function __construct_async(string $ipcPath, Logger $logger): \Generator + { + $this->logger = $logger; + $this->logger("Connecting to IPC server..."); + $this->server = yield connect($ipcPath); + $this->logger("Connected to IPC server!"); + Tools::callFork($this->loop()); + } + /** + * Logger. + * + * @param string $param Parameter + * @param int $level Logging level + * @param string $file File where the message originated + * + * @return void + */ + public function logger($param, int $level = Logger::NOTICE, string $file = ''): void + { + if ($file === null) { + $file = \basename(\debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'], '.php'); + } + isset($this->logger) ? $this->logger->logger($param, $level, $file) : Logger::$default->logger($param, $level, $file); + } + /** + * Main loop. + * + * @return \Generator + */ + private function loop(): \Generator + { + while ($payload = yield $this->server->receive()) { + [$id, $payload] = $payload; + if (!isset($this->requests[$id])) { + Logger::log("Got response for non-existing ID $id!"); + } else { + $promise = $this->requests[$id]; + unset($this->requests[$id]); + if ($payload instanceof ExitFailure) { + $promise->fail($payload->getException()); + } else { + $promise->resolve($payload); + } + unset($promise); + } + } + } + /** + * Unreference. + * + * @return void + */ + public function unreference(): void + { + if (isset($this->server)) { + Tools::wait($this->server->disconnect()); + } + } + /** + * Call function. + * + * @param string $function Function name + * @param array $arguments Arguments + * + * @return \Generator + */ + public function __call(string $function, array $arguments): \Generator + { + $this->requests []= $deferred = new Deferred; + yield $this->server->send([$function, $arguments]); + return $deferred->promise(); + } + /** + * Placeholder. + * + * @param mixed ...$params Params + * + * @return void + */ + public function setEventHandler(...$params): void + { + throw new Exception("Can't use ".__FUNCTION__." in an IPC client instance, please use a full ".API::class." instance, instead!"); + } + /** + * Placeholder. + * + * @param mixed ...$params Params + * + * @return void + */ + public function getEventHandler(...$params): void + { + throw new Exception("Can't use ".__FUNCTION__." in an IPC client instance, please use a full ".API::class." instance, instead!"); + } +} diff --git a/src/danog/MadelineProto/Ipc/ExitFailure.php b/src/danog/MadelineProto/Ipc/ExitFailure.php new file mode 100644 index 00000000..761a81b1 --- /dev/null +++ b/src/danog/MadelineProto/Ipc/ExitFailure.php @@ -0,0 +1,64 @@ +type = \get_class($exception); + $this->message = $exception->getMessage(); + $this->code = $exception->getCode(); + $this->trace = flattenThrowableBacktrace($exception); + if (method_exists($exception, 'getTLTrace')) { + $this->tlTrace = $exception->getTLTrace(); + } + + if ($exception instanceof RPCErrorException) { + $this->localized = $exception->getLocalization(); + } + + if ($previous = $exception->getPrevious()) { + $this->previous = new self($previous); + } + } + + public function getException() + { + $previous = $this->previous ? $this->previous->getException() : null; + + $exception = new $this->type($this->message, $this->code, $previous); + if ($this->tlTrace) { + $exception->setTLTrace($this->tlTrace); + } + if ($this->localized) { + $exception->setLocalization($this->localized); + } + return $exception; + } +} diff --git a/src/danog/MadelineProto/Ipc/Runner/ProcessRunner.php b/src/danog/MadelineProto/Ipc/Runner/ProcessRunner.php new file mode 100644 index 00000000..f3818b01 --- /dev/null +++ b/src/danog/MadelineProto/Ipc/Runner/ProcessRunner.php @@ -0,0 +1,87 @@ + "0", + "display_errors" => "0", + "log_errors" => "1", + ]; + + $runner = self::getScriptPath(); + + $command = \implode(" ", [ + 'nohup', + \escapeshellarg($binary), + self::formatOptions($options), + $runner, + 'madeline-ipc', + \escapeshellarg($session), + '&' + ]); + \var_dumP($command); + + $this->process = new BaseProcess($command); + } + private static function locateBinary(): string + { + $executable = \strncasecmp(\PHP_OS, "WIN", 3) === 0 ? "php.exe" : "php"; + + $paths = \array_filter(\explode(\PATH_SEPARATOR, \getenv("PATH"))); + $paths[] = \PHP_BINDIR; + $paths = \array_unique($paths); + + foreach ($paths as $path) { + $path .= \DIRECTORY_SEPARATOR.$executable; + if (\is_executable($path)) { + return self::$binaryPath = $path; + } + } + + throw new \Error("Could not locate PHP executable binary"); + } + + private static function formatOptions(array $options): string + { + $result = []; + + foreach ($options as $option => $value) { + $result[] = \sprintf("-d%s=%s", $option, $value); + } + + return \implode(" ", $result); + } + + /** + * Starts the process. + * + * @return Promise Resolved with the PID + */ + public function start(): Promise + { + return $this->process->start(); + } +} diff --git a/src/danog/MadelineProto/Ipc/Runner/RunnerAbstract.php b/src/danog/MadelineProto/Ipc/Runner/RunnerAbstract.php new file mode 100644 index 00000000..9e5c9afd --- /dev/null +++ b/src/danog/MadelineProto/Ipc/Runner/RunnerAbstract.php @@ -0,0 +1,74 @@ + Resolves with the PID + */ + abstract public function start(): Promise; +} diff --git a/src/danog/MadelineProto/Ipc/Runner/WebRunner.php b/src/danog/MadelineProto/Ipc/Runner/WebRunner.php new file mode 100644 index 00000000..bbf7fdb8 --- /dev/null +++ b/src/danog/MadelineProto/Ipc/Runner/WebRunner.php @@ -0,0 +1,107 @@ +params = [ + 'argv' => ['pony', 'madeline-ipc', $session] + ]; + } + + /** + * Start process. + * + * @return Promise + */ + public function start(): Promise + { + $params = \http_build_query($this->params); + + $address = ($_SERVER['HTTPS'] ?? false ? 'tls' : 'tcp').'://'.$_SERVER['SERVER_NAME']; + $port = $_SERVER['SERVER_PORT']; + + $uri = self::$runPath.'?'.$params; + + $payload = "GET $uri HTTP/1.1\r\nHost: ${_SERVER['SERVER_NAME']}\r\n\r\n"; + + // We don't care for results or timeouts here, PHP doesn't count IOwait time as execution time anyway + // Technically should use amphp/socket, but I guess it's OK to not introduce another dependency just for a socket that will be used once. + $this->res = new ResourceOutputStream(\fsockopen($address, $port)); + return $this->res->write($payload); + } +} diff --git a/src/danog/MadelineProto/Ipc/Runner/entry.php b/src/danog/MadelineProto/Ipc/Runner/entry.php new file mode 100644 index 00000000..2f9b4cde --- /dev/null +++ b/src/danog/MadelineProto/Ipc/Runner/entry.php @@ -0,0 +1,87 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +use Amp\Deferred; +use danog\MadelineProto\API; +use danog\MadelineProto\Logger; +use danog\MadelineProto\SessionPaths; +use danog\MadelineProto\Tools; + +(static function () use (&$argv): void { + $ipcPath = null; + if (\defined(\MADELINE_WORKER_START::class)) { + $ipcPath = \MADELINE_WORKER_START; + } elseif (\count(\debug_backtrace(0)) === 1) { + if (isset($GLOBALS['argv']) && !empty($GLOBALS['argv'])) { + $arguments = $GLOBALS['argv']; + } elseif (isset($_GET['argv']) && !empty($_GET['argv'])) { + $arguments = $_GET['argv']; + } else { + return; + } + if (isset($arguments[1]) && $arguments[1] === 'madeline-ipc') { + $ipcPath = $arguments[2]; + } else { + return; + } + $paths = [ + \dirname(__DIR__, 7)."/autoload.php", + \dirname(__DIR__, 5)."/vendor/autoload.php", + ]; + + foreach ($paths as $path) { + if (\file_exists($path)) { + $autoloadPath = $path; + break; + } + } + + if (!isset($autoloadPath)) { + \trigger_error("Could not locate autoload.php in any of the following files: ".\implode(", ", $paths), E_USER_ERROR); + exit(1); + } + + include $autoloadPath; + } + + if ($ipcPath) { + if (!\file_exists($ipcPath)) { + \trigger_error("IPC session $ipcPath does not exist!", E_USER_ERROR); + exit(1); + } + \define(\MADELINE_WORKER::class, 1); + + try { + $API = new API($ipcPath); + if ($API->hasEventHandler()) { + $API->startAndLoop(\get_class($API->getEventHandler())); + } else { + $API->initSelfRestart(); + Tools::wait((new Deferred)->promise()); + } + } catch (\Throwable $e) { + Logger::log("Got exception $e in IPC server, exiting...", Logger::FATAL_ERROR); + \trigger_error("Got exception $e in IPC server, exiting...", E_USER_ERROR); + if ($e->getMessage() === 'Not inited!') { + $ipc = (new SessionPaths($ipcPath))->getIpcPath(); + @\unlink($ipc); + \file_put_contents($ipc, 'not inited'); + } + } + } +})(); diff --git a/src/danog/MadelineProto/Ipc/Server.php b/src/danog/MadelineProto/Ipc/Server.php new file mode 100644 index 00000000..bdc067cd --- /dev/null +++ b/src/danog/MadelineProto/Ipc/Server.php @@ -0,0 +1,133 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto\Ipc; + +use Amp\Ipc\IpcServer; +use Amp\Ipc\Sync\ChannelledSocket; +use danog\MadelineProto\Ipc\Runner\ProcessRunner; +use danog\MadelineProto\Ipc\Runner\WebRunner; +use danog\MadelineProto\Logger; +use danog\MadelineProto\Loop\Impl\SignalLoop; +use danog\MadelineProto\Tools; + +/** + * IPC server. + */ +class Server extends SignalLoop +{ + /** + * IPC server. + */ + private IpcServer $server; + /** + * Set IPC path. + * + * @param string $path IPC path + * + * @return void + */ + public function setIpcPath(string $path): void + { + $this->server = new IpcServer($path); + } + /** + * Start IPC server in background. + * + * @param string $session Session path + * + * @return \Generator + */ + public static function startMe(string $session): \Generator + { + try { + Logger::log("Starting IPC server $session (process)"); + yield (new ProcessRunner($session))->start(); + return; + } catch (\Throwable $e) { + Logger::log($e); + } + Logger::log("Starting IPC server $session (web)"); + yield (new WebRunner($session))->start(); + } + /** + * Main loop. + * + * @return \Generator + */ + public function loop(): \Generator + { + while ($socket = yield $this->waitSignal($this->server->accept())) { + Tools::callFork($this->clientLoop($socket)); + } + $this->server->close(); + } + /** + * Client handler loop. + * + * @param ChannelledSocket $socket Client + * + * @return \Generator + */ + private function clientLoop(ChannelledSocket $socket): \Generator + { + $this->API->logger("Accepted IPC client connection!"); + + $id = 0; + while ($payload = yield $socket->receive()) { + Tools::callFork($this->clientRequest($socket, $id++, $payload)); + } + } + /** + * Handle client request. + * + * @param ChannelledSocket $socket Socket + * @param integer $id Request ID + * @param array $payload Payload + * + * @return \Generator + */ + public function clientRequest(ChannelledSocket $socket, int $id, $payload): \Generator + { + try { + $result = $this->API->{$payload[0]}(...$payload[1]); + $result = $result instanceof \Generator ? yield from $result : yield $result; + } catch (\Throwable $e) { + $result = new ExitFailure($e); + } + try { + yield $socket->send([$id, $result]); + } catch (\Throwable $e) { + $this->API->logger("Got error while trying to send result of ${payload[0]}: $e", Logger::ERROR); + try { + yield $socket->send([$id, new ExitFailure($e)]); + } catch (\Throwable $e) { + $this->API->logger("Got error while trying to send error of error of ${payload[0]}: $e", Logger::ERROR); + } + } + } + /** + * Get the name of the loop. + * + * @return string + */ + public function __toString(): string + { + return "IPC server"; + } +} diff --git a/src/danog/MadelineProto/Logger.php b/src/danog/MadelineProto/Logger.php index 6b113676..8f944b08 100644 --- a/src/danog/MadelineProto/Logger.php +++ b/src/danog/MadelineProto/Logger.php @@ -185,6 +185,9 @@ class Logger if ($mode === self::NO_LOGGER) { $mode = (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') ? Logger::ECHO_LOGGER : Logger::FILE_LOGGER; } + if (\defined(\MADELINE_WORKER::class)) { + $mode = Logger::FILE_LOGGER; + } $level = \max($level, self::NOTICE); $max_size = \max($max_size, 100 * 1024); diff --git a/src/danog/MadelineProto/MTProto.php b/src/danog/MadelineProto/MTProto.php index ac39cf38..d6e2b3ee 100644 --- a/src/danog/MadelineProto/MTProto.php +++ b/src/danog/MadelineProto/MTProto.php @@ -28,6 +28,7 @@ use danog\MadelineProto\Db\DbArray; use danog\MadelineProto\Db\DbPropertiesFabric; use danog\MadelineProto\Db\DbPropertiesTrait; use danog\MadelineProto\Db\Mysql; +use danog\MadelineProto\Ipc\Server; use danog\MadelineProto\Loop\Generic\PeriodicLoop; use danog\MadelineProto\Loop\Update\FeedLoop; use danog\MadelineProto\Loop\Update\SeqLoop; @@ -384,6 +385,12 @@ class MTProto extends AsyncConstruct implements TLCallback * @var PeriodicLoop */ private $rpcLoop; + /** + * IPC server. + * + * @var Server + */ + private $ipcServer; /** * Feeder loops. * @@ -678,6 +685,15 @@ class MTProto extends AsyncConstruct implements TLCallback { return $this->datacenter->getDataCenterConnections(); } + /** + * Get main DC ID. + * + * @return int + */ + public function getDataCenterId(): int + { + return $this->datacenter->curdc; + } /** * Prompt serialization of instance. * @@ -716,12 +732,17 @@ class MTProto extends AsyncConstruct implements TLCallback if (!$this->rpcLoop) { $this->rpcLoop = new PeriodicLoop($this, [$this, 'rpcReport'], 'config', 60); } + if (!$this->ipcServer) { + $this->ipcServer = new Server($this); + $this->ipcServer->setIpcPath($this->wrapper->getIpcPath()); + } $this->callCheckerLoop->start(); $this->serializeLoop->start(); $this->phoneConfigLoop->start(); $this->configLoop->start(); $this->checkTosLoop->start(); $this->rpcLoop->start(); + $this->ipcServer->start(); } /** * Stop all internal loops. @@ -754,6 +775,10 @@ class MTProto extends AsyncConstruct implements TLCallback $this->rpcLoop->signal(true); $this->rpcLoop = null; } + if ($this->ipcServer) { + $this->ipcServer->signal(null); + $this->ipcServer = null; + } } /** * Report RPC errors. @@ -1056,7 +1081,7 @@ class MTProto extends AsyncConstruct implements TLCallback */ public static function parseSettings(array $settings, array $previousSettings = []): array { - Magic::classExists(); + //Magic::classExists(); $settings = \array_replace_recursive($previousSettings, $settings); if (isset($previousSettings['connection_settings']['default_dc'])) { $settings['connection_settings']['default_dc'] = $previousSettings['connection_settings']['default_dc']; @@ -1787,6 +1812,15 @@ class MTProto extends AsyncConstruct implements TLCallback } return $this->authorization['user']; } + /** + * Get authorization info. + * + * @return int + */ + public function getAuthorization(): int + { + return $this->authorized; + } /** * IDs of peers where to report errors. * diff --git a/src/danog/MadelineProto/MTProtoSession/AckHandler.php b/src/danog/MadelineProto/MTProtoSession/AckHandler.php index 224ad793..88905b5e 100644 --- a/src/danog/MadelineProto/MTProtoSession/AckHandler.php +++ b/src/danog/MadelineProto/MTProtoSession/AckHandler.php @@ -19,20 +19,16 @@ namespace danog\MadelineProto\MTProtoSession; -use Amp\Deferred; -use Amp\Promise; -use danog\MadelineProto\Loop\Connection\WriteLoop; - /** * Manages acknowledgement of messages. */ trait AckHandler { /** - * Acknowledge outgoing message ID + * Acknowledge outgoing message ID. * * @param string|int $message_id Message Id - * + * * @return boolean */ public function ackOutgoingMessageId($message_id): bool @@ -45,10 +41,10 @@ trait AckHandler return true; } /** - * We have gotten response for outgoing message ID + * We have gotten response for outgoing message ID. * * @param string|int $message_id Message ID - * + * * @return boolean */ public function gotResponseForOutgoingMessageId($message_id): bool @@ -70,10 +66,10 @@ trait AckHandler return true; } /** - * Acknowledge incoming message ID + * Acknowledge incoming message ID. * * @param string|int $message_id Message ID - * + * * @return boolean */ public function ackIncomingMessageId($message_id): bool diff --git a/src/danog/MadelineProto/MTProtoSession/Reliable.php b/src/danog/MadelineProto/MTProtoSession/Reliable.php index cc267c06..6d923b79 100644 --- a/src/danog/MadelineProto/MTProtoSession/Reliable.php +++ b/src/danog/MadelineProto/MTProtoSession/Reliable.php @@ -19,21 +19,17 @@ namespace danog\MadelineProto\MTProtoSession; -use Amp\Loop; -use danog\MadelineProto\Logger; -use danog\MadelineProto\MTProto; - /** * Manages responses. */ trait Reliable { /** - * Send state info for message IDs + * Send state info for message IDs. * * @param string|int $req_msg_id Message ID of msgs_state_req that initiated this * @param array $msg_ids Message IDs to send info about - * + * * @return \Generator */ public function sendMsgsStateInfo($req_msg_id, array $msg_ids): \Generator @@ -62,4 +58,4 @@ trait Reliable } $this->outgoing_messages[yield from $this->objectCall('msgs_state_info', ['req_msg_id' => $req_msg_id, 'info' => $info], ['postpone' => true])]['response'] = $req_msg_id; } -} \ No newline at end of file +} diff --git a/src/danog/MadelineProto/MTProtoSession/ResponseHandler.php b/src/danog/MadelineProto/MTProtoSession/ResponseHandler.php index 4250d745..2d71765e 100644 --- a/src/danog/MadelineProto/MTProtoSession/ResponseHandler.php +++ b/src/danog/MadelineProto/MTProtoSession/ResponseHandler.php @@ -475,7 +475,7 @@ trait ResponseHandler } $botAPI = isset($request['botAPI']) && $request['botAPI']; if (isset($response['_']) && !$this->isCdn() && $this->API->getTL()->getConstructors()->findByPredicate($response['_'])['type'] === 'Updates') { - $response['request'] = $request; + $response['request'] = ['_' => $request['_'], 'body' => $request['body'] ?? []]; \danog\MadelineProto\Tools::callForkDefer($this->API->handleUpdates($response)); } unset($request); diff --git a/src/danog/MadelineProto/MTProtoTools/MinDatabase.php b/src/danog/MadelineProto/MTProtoTools/MinDatabase.php index a7ad3b22..3927876b 100644 --- a/src/danog/MadelineProto/MTProtoTools/MinDatabase.php +++ b/src/danog/MadelineProto/MTProtoTools/MinDatabase.php @@ -128,11 +128,11 @@ class MinDatabase implements TLCallback } } $frames = \array_reverse($frames); - $tl_trace = \array_shift($frames); + $tlTrace = \array_shift($frames); foreach ($frames as $frame) { - $tl_trace .= "['".$frame."']"; + $tlTrace .= "['".$frame."']"; } - $this->API->logger->logger($tl_trace, \danog\MadelineProto\Logger::ERROR); + $this->API->logger->logger($tlTrace, \danog\MadelineProto\Logger::ERROR); return false; } $peers = []; diff --git a/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php b/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php index b877a57c..061cd3d2 100644 --- a/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php +++ b/src/danog/MadelineProto/MTProtoTools/ReferenceDatabase.php @@ -160,11 +160,11 @@ class ReferenceDatabase implements TLCallback } } $frames = \array_reverse($frames); - $tl_trace = \array_shift($frames); + $tlTrace = \array_shift($frames); foreach ($frames as $frame) { - $tl_trace .= "['".$frame."']"; + $tlTrace .= "['".$frame."']"; } - $this->API->logger->logger($tl_trace, \danog\MadelineProto\Logger::ERROR); + $this->API->logger->logger($tlTrace, \danog\MadelineProto\Logger::ERROR); return false; } if (!isset($location['file_reference'])) { diff --git a/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php b/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php index 942e4d7c..5ddf1f62 100644 --- a/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php +++ b/src/danog/MadelineProto/MTProtoTools/UpdateHandler.php @@ -95,7 +95,7 @@ trait UpdateHandler if (!$params['timeout']) { $params['timeout'] = 0.001; } - yield \danog\MadelineProto\Tools::first([$this->waitUpdate(), \danog\MadelineProto\Tools::sleep($params['timeout'])]); + yield $this->waitUpdate(); } if (empty($this->updates)) { return []; diff --git a/src/danog/MadelineProto/Magic.php b/src/danog/MadelineProto/Magic.php index e8bb8f69..30fa9816 100644 --- a/src/danog/MadelineProto/Magic.php +++ b/src/danog/MadelineProto/Magic.php @@ -19,14 +19,10 @@ namespace danog\MadelineProto; -use Amp\DoH\DoHConfig; -use Amp\DoH\Nameserver; -use Amp\DoH\Rfc8484StubResolver; use Amp\Loop; use Amp\Loop\Driver; use ReflectionClass; use function Amp\ByteStream\getStdin; -use function Amp\Dns\resolver; use function Amp\Promise\wait; class Magic @@ -103,12 +99,18 @@ class Magic * @var int */ public static $pid; + /** + * Whether we've inited all light constants. + * + * @var boolean + */ + private static $initedLight = false; /** * Whether we've inited all static constants. * * @var boolean */ - public static $inited = false; + private static $inited = false; /** * Bigint zero. * @@ -220,85 +222,35 @@ class Magic /** * Initialize magic constants. * + * @param bool $light Use lightweight initialization routine + * * @return void */ - public static function classExists() + public static function classExists(bool $light = false): void { - \set_error_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionErrorHandler']); - \set_exception_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionHandler']); - if (!self::$inited) { - if (!\defined('\\tgseclib\\Crypt\\Common\\SymmetricKey::MODE_IGE') || \tgseclib\Crypt\Common\SymmetricKey::MODE_IGE !== 7) { - throw new Exception(\danog\MadelineProto\Lang::$current_lang['phpseclib_fork']); - } - foreach (['xml', 'fileinfo', 'json', 'mbstring'] as $extension) { - if (!\extension_loaded($extension)) { - throw Exception::extension($extension); + if (self::$inited || (self::$initedLight && $light)) { + return; + } + if (!self::$initedLight) { + // Setup error reporting + \set_error_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionErrorHandler']); + \set_exception_handler(['\\danog\\MadelineProto\\Exception', 'ExceptionHandler']); + if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') { + try { + \error_reporting(E_ALL); + \ini_set('log_errors', 1); + \ini_set('error_log', Magic::$script_cwd.'/MadelineProto.log'); + \error_log('Enabled PHP logging'); + } catch (\danog\MadelineProto\Exception $e) { + //$this->logger->logger('Could not enable PHP logging'); } } - self::$has_thread = \class_exists(\Thread::class) && \method_exists(\Thread::class, 'getCurrentThread'); - self::$BIG_ENDIAN = \pack('L', 1) === \pack('N', 1); - self::$bigint = PHP_INT_SIZE < 8; - self::$ipv6 = (bool) \strlen(@\file_get_contents('http://ipv6.google.com', false, \stream_context_create(['http' => ['timeout' => 1]]))) > 0; - \preg_match('/const V = (\\d+);/', @\file_get_contents('https://raw.githubusercontent.com/danog/MadelineProto/master/src/danog/MadelineProto/MTProto.php'), $matches); - if (isset($matches[1]) && \danog\MadelineProto\MTProto::V < (int) $matches[1]) { - throw new \danog\MadelineProto\Exception(\hex2bin(\danog\MadelineProto\Lang::$current_lang['v_error']), 0, null, 'MadelineProto', 1); - } - if (\class_exists('\\danog\\MadelineProto\\VoIP')) { - if (!\defined('\\danog\\MadelineProto\\VoIP::PHP_LIBTGVOIP_VERSION') || !\in_array(\danog\MadelineProto\VoIP::PHP_LIBTGVOIP_VERSION, ['1.5.0'])) { - throw new \danog\MadelineProto\Exception(\hex2bin(\danog\MadelineProto\Lang::$current_lang['v_tgerror']), 0, null, 'MadelineProto', 1); - } - } - self::$emojis = \json_decode(self::JSON_EMOJIS); - self::$zero = new \tgseclib\Math\BigInteger(0); - self::$one = new \tgseclib\Math\BigInteger(1); - self::$two = new \tgseclib\Math\BigInteger(2); - self::$three = new \tgseclib\Math\BigInteger(3); - self::$four = new \tgseclib\Math\BigInteger(4); - self::$twoe1984 = new \tgseclib\Math\BigInteger('1751908409537131537220509645351687597690304110853111572994449976845956819751541616602568796259317428464425605223064365804210081422215355425149431390635151955247955156636234741221447435733643262808668929902091770092492911737768377135426590363166295684370498604708288556044687341394398676292971255828404734517580702346564613427770683056761383955397564338690628093211465848244049196353703022640400205739093118270803778352768276670202698397214556629204420309965547056893233608758387329699097930255380715679250799950923553703740673620901978370802540218870279314810722790539899334271514365444369275682816'); - self::$twoe2047 = new \tgseclib\Math\BigInteger('16158503035655503650357438344334975980222051334857742016065172713762327569433945446598600705761456731844358980460949009747059779575245460547544076193224141560315438683650498045875098875194826053398028819192033784138396109321309878080919047169238085235290822926018152521443787945770532904303776199561965192760957166694834171210342487393282284747428088017663161029038902829665513096354230157075129296432088558362971801859230928678799175576150822952201848806616643615613562842355410104862578550863465661734839271290328348967522998634176499319107762583194718667771801067716614802322659239302476074096777926805529798115328'); - self::$twoe2048 = new \tgseclib\Math\BigInteger('32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521914333389668342420684974786564569494856176035326322058077805659331026192708460314150258592864177116725943603718461857357598351152301645904403697613233287231227125684710820209725157101726931323469678542580656697935045997268352998638215525166389437335543602135433229604645318478604952148193555853611059596230656'); - self::$twozerotwosixone = new \tgseclib\Math\BigInteger(20261); - self::$zeroeight = new \tgseclib\Math\BigInteger('2147483648'); + // Check if we're in a console, for colorful log output try { - self::$isatty = \defined('STDOUT') && \function_exists('posix_isatty') && \posix_isatty(STDOUT); + self::$isatty = \defined(\STDOUT::class) && \function_exists('posix_isatty') && \posix_isatty(\STDOUT); } catch (\danog\MadelineProto\Exception $e) { } - self::$altervista = isset($_SERVER['SERVER_ADMIN']) && \strpos($_SERVER['SERVER_ADMIN'], 'altervista.org'); - self::$zerowebhost = isset($_SERVER['SERVER_ADMIN']) && \strpos($_SERVER['SERVER_ADMIN'], '000webhost.io'); - self::$can_getmypid = !self::$altervista && !self::$zerowebhost; - self::$revision = @\file_get_contents(__DIR__.'/../../../.git/refs/heads/master'); - if (self::$revision) { - self::$revision = \trim(self::$revision); - $latest = @\file_get_contents('https://phar.madelineproto.xyz/release'); - if ($latest) { - $latest = self::$revision === \trim($latest) ? '' : ' (AN UPDATE IS REQUIRED)'; - } - self::$revision = 'Revision: '.self::$revision.$latest; - } - self::$can_parallel = false; - if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && !(\class_exists(\Phar::class) && \Phar::running())) { - try { - $back = \debug_backtrace(0); - \define('AMP_WORKER', 1); - $promise = \Amp\File\get(\end($back)['file']); - do { - try { - if (wait($promise)) { - self::$can_parallel = true; - break; - } - } catch (\Throwable $e) { - if ($e->getMessage() !== 'Loop stopped without resolving the promise') { - throw $e; - } - } - } while (true); - } catch (\Throwable $e) { - } - } - if (!self::$can_parallel && !\defined('AMP_WORKER')) { - \define('AMP_WORKER', 1); - } + // Important, obtain root relative to caller script $backtrace = \debug_backtrace(0); self::$script_cwd = self::$cwd = \dirname(\end($backtrace)['file']); try { @@ -306,8 +258,8 @@ class Magic self::$can_getcwd = true; } catch (\danog\MadelineProto\Exception $e) { } - // Even an empty handler is enough to catch ctrl+c - if (\defined('SIGINT')) { + // Define signal handlers + if (\defined(\SIGINT::class)) { //if (function_exists('pcntl_async_signals')) pcntl_async_signals(true); try { Loop::unreference(Loop::onSignal(SIGINT, static function () { @@ -321,32 +273,86 @@ class Magic } catch (\Throwable $e) { } } - /*if (!self::$altervista && !self::$zerowebhost) { - $DohConfig = new DoHConfig( - [ - new Nameserver('https://mozilla.cloudflare-dns.com/dns-query'), - new Nameserver('https://dns.google/resolve'), - ] - ); - resolver(new Rfc8484StubResolver($DohConfig)); - }*/ - if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') { - try { - \error_reporting(E_ALL); - \ini_set('log_errors', 1); - \ini_set('error_log', Magic::$script_cwd.'/MadelineProto.log'); - \error_log('Enabled PHP logging'); - } catch (\danog\MadelineProto\Exception $e) { - //$this->logger->logger('Could not enable PHP logging'); - } + self::$initedLight = true; + if ($light) { + \define('AMP_WORKER', true); + return; } - $res = \json_decode(@\file_get_contents('https://rpc.madelineproto.xyz/v3.json'), true); - if (isset($res['ok']) && $res['ok']) { - RPCErrorException::$errorMethodMap = $res['result']; - RPCErrorException::$descriptions += $res['human_result']; - } - self::$inited = true; } + if (!\defined('\\tgseclib\\Crypt\\Common\\SymmetricKey::MODE_IGE') || \tgseclib\Crypt\Common\SymmetricKey::MODE_IGE !== 7) { + throw new Exception(\danog\MadelineProto\Lang::$current_lang['phpseclib_fork']); + } + foreach (['xml', 'fileinfo', 'json', 'mbstring'] as $extension) { + if (!\extension_loaded($extension)) { + throw Exception::extension($extension); + } + } + self::$has_thread = \class_exists(\Thread::class) && \method_exists(\Thread::class, 'getCurrentThread'); + self::$BIG_ENDIAN = \pack('L', 1) === \pack('N', 1); + self::$bigint = PHP_INT_SIZE < 8; + self::$ipv6 = (bool) \strlen(@\file_get_contents('http://ipv6.google.com', false, \stream_context_create(['http' => ['timeout' => 1]]))) > 0; + \preg_match('/const V = (\\d+);/', @\file_get_contents('https://raw.githubusercontent.com/danog/MadelineProto/master/src/danog/MadelineProto/MTProto.php'), $matches); + if (isset($matches[1]) && \danog\MadelineProto\MTProto::V < (int) $matches[1]) { + throw new \danog\MadelineProto\Exception(\hex2bin(\danog\MadelineProto\Lang::$current_lang['v_error']), 0, null, 'MadelineProto', 1); + } + if (\class_exists('\\danog\\MadelineProto\\VoIP')) { + if (!\defined('\\danog\\MadelineProto\\VoIP::PHP_LIBTGVOIP_VERSION') || !\in_array(\danog\MadelineProto\VoIP::PHP_LIBTGVOIP_VERSION, ['1.5.0'])) { + throw new \danog\MadelineProto\Exception(\hex2bin(\danog\MadelineProto\Lang::$current_lang['v_tgerror']), 0, null, 'MadelineProto', 1); + } + } + self::$emojis = \json_decode(self::JSON_EMOJIS); + self::$zero = new \tgseclib\Math\BigInteger(0); + self::$one = new \tgseclib\Math\BigInteger(1); + self::$two = new \tgseclib\Math\BigInteger(2); + self::$three = new \tgseclib\Math\BigInteger(3); + self::$four = new \tgseclib\Math\BigInteger(4); + self::$twoe1984 = new \tgseclib\Math\BigInteger('1751908409537131537220509645351687597690304110853111572994449976845956819751541616602568796259317428464425605223064365804210081422215355425149431390635151955247955156636234741221447435733643262808668929902091770092492911737768377135426590363166295684370498604708288556044687341394398676292971255828404734517580702346564613427770683056761383955397564338690628093211465848244049196353703022640400205739093118270803778352768276670202698397214556629204420309965547056893233608758387329699097930255380715679250799950923553703740673620901978370802540218870279314810722790539899334271514365444369275682816'); + self::$twoe2047 = new \tgseclib\Math\BigInteger('16158503035655503650357438344334975980222051334857742016065172713762327569433945446598600705761456731844358980460949009747059779575245460547544076193224141560315438683650498045875098875194826053398028819192033784138396109321309878080919047169238085235290822926018152521443787945770532904303776199561965192760957166694834171210342487393282284747428088017663161029038902829665513096354230157075129296432088558362971801859230928678799175576150822952201848806616643615613562842355410104862578550863465661734839271290328348967522998634176499319107762583194718667771801067716614802322659239302476074096777926805529798115328'); + self::$twoe2048 = new \tgseclib\Math\BigInteger('32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521914333389668342420684974786564569494856176035326322058077805659331026192708460314150258592864177116725943603718461857357598351152301645904403697613233287231227125684710820209725157101726931323469678542580656697935045997268352998638215525166389437335543602135433229604645318478604952148193555853611059596230656'); + self::$twozerotwosixone = new \tgseclib\Math\BigInteger(20261); + self::$zeroeight = new \tgseclib\Math\BigInteger('2147483648'); + self::$altervista = isset($_SERVER['SERVER_ADMIN']) && \strpos($_SERVER['SERVER_ADMIN'], 'altervista.org'); + self::$zerowebhost = isset($_SERVER['SERVER_ADMIN']) && \strpos($_SERVER['SERVER_ADMIN'], '000webhost.io'); + self::$can_getmypid = !self::$altervista && !self::$zerowebhost; + self::$revision = @\file_get_contents(__DIR__.'/../../../.git/refs/heads/master'); + if (self::$revision) { + self::$revision = \trim(self::$revision); + $latest = @\file_get_contents('https://phar.madelineproto.xyz/release'); + if ($latest) { + $latest = self::$revision === \trim($latest) ? '' : ' (AN UPDATE IS REQUIRED)'; + } + self::$revision = 'Revision: '.self::$revision.$latest; + } + self::$can_parallel = false; + if ((PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') && !(\class_exists(\Phar::class) && \Phar::running())) { + try { + $back = \debug_backtrace(0); + \define('AMP_WORKER', 1); + $promise = \Amp\File\get(\end($back)['file']); + do { + try { + if (wait($promise)) { + self::$can_parallel = true; + break; + } + } catch (\Throwable $e) { + if ($e->getMessage() !== 'Loop stopped without resolving the promise') { + throw $e; + } + } + } while (true); + } catch (\Throwable $e) { + } + } + if (!self::$can_parallel && !\defined('AMP_WORKER')) { + \define('AMP_WORKER', 1); + } + $res = \json_decode(@\file_get_contents('https://rpc.madelineproto.xyz/v3.json'), true); + if (isset($res['ok']) && $res['ok']) { + RPCErrorException::$errorMethodMap = $res['result']; + RPCErrorException::$descriptions += $res['human_result']; + } + self::$inited = true; } /** * Check if this is a POSIX fork of the main PHP process. diff --git a/src/danog/MadelineProto/RPCErrorException.php b/src/danog/MadelineProto/RPCErrorException.php index d5deab2f..19238a71 100644 --- a/src/danog/MadelineProto/RPCErrorException.php +++ b/src/danog/MadelineProto/RPCErrorException.php @@ -43,7 +43,7 @@ class RPCErrorException extends \Exception self::$toReport = \array_slice(self::$toReport, -100); } self::$toReport []= [ - $method, $code, $error, time() + $method, $code, $error, \time() ]; } if (!$description) { @@ -53,24 +53,47 @@ class RPCErrorException extends \Exception } public function __toString() { - $result = \sprintf(\danog\MadelineProto\Lang::$current_lang['rpc_tg_error'], self::localizeMessage($this->caller, $this->code, $this->message)." ({$this->code})", $this->rpc, $this->file, $this->line.PHP_EOL, \danog\MadelineProto\Magic::$revision.PHP_EOL.PHP_EOL).PHP_EOL.$this->getTLTrace().PHP_EOL; + $this->localized ??= self::localizeMessage($this->caller, $this->code, $this->message); + $result = \sprintf(\danog\MadelineProto\Lang::$current_lang['rpc_tg_error'], $this->localized." ({$this->code})", $this->rpc, $this->file, $this->line.PHP_EOL, \danog\MadelineProto\Magic::$revision.PHP_EOL.PHP_EOL).PHP_EOL.$this->getTLTrace().PHP_EOL; if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') { $result = \str_replace(PHP_EOL, '
'.PHP_EOL, $result); } return $result; } + /** + * Get localized error name + * + * @return string + */ + public function getLocalization(): string + { + $this->localized ??= self::localizeMessage($this->caller, $this->code, $this->message); + return $this->localized; + } + /** + * Set localized error name + * + * @param string $localization + * @return void + */ + public function setLocalization(string $localization): void + { + $this->localized = $localization; + } public function __construct($message = null, $code = 0, $caller = '', Exception $previous = null) { $this->rpc = $message; parent::__construct($message, $code, $previous); - $this->prettifyTL($caller); - $this->caller = $caller; - $additional = []; - foreach ($this->getTrace() as $level) { - if (isset($level['function']) && $level['function'] === 'methodCall') { - $this->line = $level['line']; - $this->file = $level['file']; - $additional = $level['args']; + if (\is_string($caller)) { + $this->prettifyTL($caller); + $this->caller = $caller; + $additional = []; + foreach ($this->getTrace() as $level) { + if (isset($level['function']) && $level['function'] === 'methodCall') { + $this->line = $level['line']; + $this->file = $level['file']; + $additional = $level['args']; + } } } /* diff --git a/src/danog/MadelineProto/Serialization.php b/src/danog/MadelineProto/Serialization.php index ce071f9f..666632c6 100644 --- a/src/danog/MadelineProto/Serialization.php +++ b/src/danog/MadelineProto/Serialization.php @@ -29,24 +29,6 @@ use function Amp\File\get; */ class Serialization { - /** - * List of session paths. - * - * @var array - */ - private $paths = []; - /** - * Extract path components for serialization. - * - * @param string $file Session path - * - * @return array - */ - public static function realpaths(string $file): array - { - $file = Tools::absolute($file); - return ['file' => $file, 'lockfile' => $file.'.lock', 'tempfile' => $file.'.temp.session', 'session_lockfile' => $file.'.slock', ]; - } /** * Unserialize legacy session. * @@ -58,8 +40,8 @@ class Serialization */ public static function legacyUnserialize(string $session): \Generator { - $realpaths = self::realpaths($session); - if (yield exists($realpaths['file'])) { + $realpaths = new SessionPaths($session); + if (yield exists($realpaths->getSessionPath())) { Logger::log('Waiting for exclusive session lock...'); $warningId = Loop::delay(1000, static function () use (&$warningId) { Logger::log("It seems like the session is busy."); @@ -68,7 +50,7 @@ class Serialization Loop::unreference($warningId); }); Loop::unreference($warningId); - $unlockGlobal = yield Tools::flock($realpaths['session_lockfile'], LOCK_EX, 1); + $unlockGlobal = yield Tools::flock($realpaths->getSessionLockPath(), LOCK_EX, 1); Loop::cancel($warningId); $tempId = Shutdown::addCallback($unlockGlobal = static function () use ($unlockGlobal) { Logger::log("Unlocking exclusive session lock!"); @@ -78,10 +60,10 @@ class Serialization Logger::log("Got exclusive session lock!"); Logger::log('Waiting for shared lock of serialization lockfile...'); - $unlock = yield Tools::flock($realpaths['lockfile'], LOCK_SH); + $unlock = yield Tools::flock($realpaths->getLockPath(), LOCK_SH); Logger::log('Shared lock acquired, deserializing...'); try { - $tounserialize = yield get($realpaths['file']); + $tounserialize = yield get($realpaths->getSessionPath()); } finally { $unlock(); } diff --git a/src/danog/MadelineProto/SessionPaths.php b/src/danog/MadelineProto/SessionPaths.php new file mode 100644 index 00000000..b646240b --- /dev/null +++ b/src/danog/MadelineProto/SessionPaths.php @@ -0,0 +1,120 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2020 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * + * @link https://docs.madelineproto.xyz MadelineProto documentation + */ + +namespace danog\MadelineProto; + +/** + * Session path information. + */ +class SessionPaths +{ + /** + * Session path. + */ + private string $sessionPath; + /** + * Global session lock path. + */ + private string $slockPath; + /** + * Session lock path. + */ + private string $lockPath; + /** + * IPC socket path. + */ + private string $ipcPath; + /** + * Temporary serialization path. + */ + private string $tempPath; + /** + * Construct session info from session name. + * + * @param string $session Session name + */ + public function __construct(string $session) + { + $session = Tools::absolute($session); + $this->sessionPath = $session; + $this->slockPath = "$session.slock"; + $this->lockPath = "$session.lock"; + $this->ipcPath = "$session.ipc"; + $this->tempPath = "$session.temp.session"; + } + /** + * Get session path. + * + * @return string + */ + public function __toString(): string + { + return $this->sessionPath; + } + + /** + * Get session path. + * + * @return string + */ + public function getSessionPath(): string + { + return $this->sessionPath; + } + + /** + * Get global session lock path. + * + * @return string + */ + public function getSessionLockPath(): string + { + return $this->slockPath; + } + + /** + * Get lock path. + * + * @return string + */ + public function getLockPath(): string + { + return $this->lockPath; + } + + /** + * Get IPC socket path. + * + * @return string + */ + public function getIpcPath(): string + { + return $this->ipcPath; + } + + /** + * Get temporary serialization path. + * + * @return string + */ + public function getTempPath(): string + { + return $this->tempPath; + } +} diff --git a/src/danog/MadelineProto/Snitch.php b/src/danog/MadelineProto/Snitch.php index de1d6ea5..79dd34fd 100644 --- a/src/danog/MadelineProto/Snitch.php +++ b/src/danog/MadelineProto/Snitch.php @@ -108,6 +108,9 @@ class Snitch */ private function die(): void { - die('Please do not remove madeline.phar, madeline.php and MadelineProto.log, or else MadelineProto will crash. If you have any problem with MadelineProto, report it to https://github.com/danog/MadelineProto or https://t.me/pwrtelegramgroup'); + Shutdown::removeCallback('restarter'); + $message = "Please do not remove madeline.phar, madeline.php and MadelineProto.log, or else MadelineProto will crash. If you have any problem with MadelineProto, report it to https://github.com/danog/MadelineProto or https://t.me/pwrtelegramgroup"; + Logger::log($message, Logger::FATAL_ERROR); + die("$message\n"); } } diff --git a/src/danog/MadelineProto/TL/PrettyException.php b/src/danog/MadelineProto/TL/PrettyException.php index 3df2e114..ab17d9df 100644 --- a/src/danog/MadelineProto/TL/PrettyException.php +++ b/src/danog/MadelineProto/TL/PrettyException.php @@ -29,7 +29,7 @@ trait PrettyException * * @var string */ - public $tl_trace = ''; + public $tlTrace = ''; /** * Method name. * @@ -63,7 +63,18 @@ trait PrettyException */ public function getTLTrace(): string { - return $this->tl_trace; + return $this->tlTrace; + } + /** + * Set TL trace. + * + * @param string $tlTrace TL trace + * + * @return void + */ + public function setTLTrace(string $tlTrace): void + { + $this->tlTrace = $tlTrace; } /** * Generate async trace. @@ -76,8 +87,8 @@ trait PrettyException public function prettifyTL(string $init = '', array $trace = null) { $this->method = $init; - $previous_trace = $this->tl_trace; - $this->tl_trace = ''; + $previous_trace = $this->tlTrace; + $this->tlTrace = ''; $eol = PHP_EOL; if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg') { $eol = '
'.PHP_EOL; @@ -86,30 +97,30 @@ trait PrettyException foreach (\array_reverse($trace ?? $this->getTrace()) as $k => $frame) { if (isset($frame['function']) && \in_array($frame['function'], ['serializeParams', 'serializeObject'])) { if (($frame['args'][2] ?? '') !== '') { - $this->tl_trace .= $tl ? "['".$frame['args'][2]."']" : "While serializing: \t".$frame['args'][2]; + $this->tlTrace .= $tl ? "['".$frame['args'][2]."']" : "While serializing: \t".$frame['args'][2]; $tl = true; } } else { if ($tl) { - $this->tl_trace .= $eol; + $this->tlTrace .= $eol; } if (isset($frame['function']) && ($frame['function'] === 'handle_rpc_error' && $k === \count($this->getTrace()) - 1) || $frame['function'] === 'unserialize') { continue; } - $this->tl_trace .= isset($frame['file']) ? \str_pad(\basename($frame['file']).'('.$frame['line'].'):', 20)."\t" : ''; - $this->tl_trace .= isset($frame['function']) ? $frame['function'].'(' : ''; - $this->tl_trace .= isset($frame['args']) ? \substr(\json_encode($frame['args']), 1, -1) : ''; - $this->tl_trace .= ')'; - $this->tl_trace .= $eol; + $this->tlTrace .= isset($frame['file']) ? \str_pad(\basename($frame['file']).'('.$frame['line'].'):', 20)."\t" : ''; + $this->tlTrace .= isset($frame['function']) ? $frame['function'].'(' : ''; + $this->tlTrace .= isset($frame['args']) ? \substr(\json_encode($frame['args']), 1, -1) : ''; + $this->tlTrace .= ')'; + $this->tlTrace .= $eol; $tl = false; } } - $this->tl_trace .= $init !== '' ? "['".$init."']" : ''; - $this->tl_trace = \implode($eol, \array_reverse(\explode($eol, $this->tl_trace))); + $this->tlTrace .= $init !== '' ? "['".$init."']" : ''; + $this->tlTrace = \implode($eol, \array_reverse(\explode($eol, $this->tlTrace))); if ($previous_trace) { - $this->tl_trace .= $eol.$eol; - $this->tl_trace .= "Previous TL trace:{$eol}"; - $this->tl_trace .= $previous_trace; + $this->tlTrace .= $eol.$eol; + $this->tlTrace .= "Previous TL trace:{$eol}"; + $this->tlTrace .= $previous_trace; } } } diff --git a/src/danog/MadelineProto/Wrappers/Loop.php b/src/danog/MadelineProto/Wrappers/Loop.php index 84c375d2..df4e9ce7 100644 --- a/src/danog/MadelineProto/Wrappers/Loop.php +++ b/src/danog/MadelineProto/Wrappers/Loop.php @@ -36,34 +36,12 @@ trait Loop */ private $stopLoop = false; /** - * Start MadelineProto's update handling loop, or run the provided async callable. + * Initialize self-restart hack. * - * @param callable $callback Async callable to run - * - * @return mixed + * @return void */ - public function loop($callback = null): \Generator + public function initSelfRestart(): void { - if (\is_callable($callback)) { - $this->logger->logger('Running async callable'); - return (yield $callback()); - } - if ($callback instanceof Promise) { - $this->logger->logger('Resolving async promise'); - return (yield $callback); - } - if (!$this->authorized) { - $this->logger->logger('Not authorized, not starting event loop', \danog\MadelineProto\Logger::FATAL_ERROR); - return false; - } - if (\in_array($this->settings['updates']['callback'], [['danog\\MadelineProto\\API', 'getUpdatesUpdateHandler'], 'getUpdatesUpdateHandler'])) { - $this->logger->logger('Getupdates event handler is enabled, exiting from loop', \danog\MadelineProto\Logger::FATAL_ERROR); - return false; - } - $this->logger->logger('Starting event loop'); - if (!\is_callable($this->loop_callback)) { - $this->loop_callback = null; - } static $inited = false; if (PHP_SAPI !== 'cli' && PHP_SAPI !== 'phpdbg' && !$inited) { $needs_restart = true; @@ -104,12 +82,43 @@ trait Loop $this->closeConnection('Bot was started'); $inited = true; } + } + /** + * Start MadelineProto's update handling loop, or run the provided async callable. + * + * @param callable $callback Async callable to run + * + * @return mixed + */ + public function loop($callback = null): \Generator + { + if (\is_callable($callback)) { + $this->logger->logger('Running async callable'); + return (yield $callback()); + } + if ($callback instanceof Promise) { + $this->logger->logger('Resolving async promise'); + return (yield $callback); + } + if (!$this->authorized) { + $this->logger->logger('Not authorized, not starting event loop', \danog\MadelineProto\Logger::FATAL_ERROR); + return false; + } + if (\in_array($this->settings['updates']['callback'], [['danog\\MadelineProto\\API', 'getUpdatesUpdateHandler'], 'getUpdatesUpdateHandler'])) { + $this->logger->logger('Getupdates event handler is enabled, exiting from loop', \danog\MadelineProto\Logger::FATAL_ERROR); + return false; + } + $this->logger->logger('Starting event loop'); + if (!\is_callable($this->loop_callback)) { + $this->loop_callback = null; + } if (!$this->settings['updates']['handle_updates']) { $this->settings['updates']['handle_updates'] = true; } if (!$this->settings['updates']['run_callback']) { $this->settings['updates']['run_callback'] = true; } + $this->initSelfRestart(); $this->startUpdateSystem(); $this->logger->logger('Started update loop', \danog\MadelineProto\Logger::NOTICE); $this->stopLoop = false; diff --git a/src/danog/MadelineProto/Wrappers/Start.php b/src/danog/MadelineProto/Wrappers/Start.php index 646c432f..d5b213e6 100644 --- a/src/danog/MadelineProto/Wrappers/Start.php +++ b/src/danog/MadelineProto/Wrappers/Start.php @@ -19,6 +19,7 @@ namespace danog\MadelineProto\Wrappers; +use danog\MadelineProto\MTProto; use danog\MadelineProto\Tools; /** @@ -33,7 +34,7 @@ trait Start */ public function start(): \Generator { - if ($this->authorized === self::LOGGED_IN) { + if (yield $this->getAuthorization() === MTProto::LOGGED_IN) { return yield from $this->fullGetSelf(); } if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { @@ -52,7 +53,7 @@ trait Start $this->serialize(); return yield from $this->fullGetSelf(); } - if ($this->authorized === self::NOT_LOGGED_IN) { + if ($this->authorized === MTProto::NOT_LOGGED_IN) { if (isset($_POST['phone_number'])) { yield from $this->webPhoneLogin(); } elseif (isset($_POST['token'])) { @@ -60,26 +61,26 @@ trait Start } else { yield from $this->webEcho(); } - } elseif ($this->authorized === self::WAITING_CODE) { + } elseif ($this->authorized === MTProto::WAITING_CODE) { if (isset($_POST['phone_code'])) { yield from $this->webCompletePhoneLogin(); } else { yield from $this->webEcho("You didn't provide a phone code!"); } - } elseif ($this->authorized === self::WAITING_PASSWORD) { + } elseif ($this->authorized === MTProto::WAITING_PASSWORD) { if (isset($_POST['password'])) { yield from $this->webComplete2faLogin(); } else { yield from $this->webEcho("You didn't provide the password!"); } - } elseif ($this->authorized === self::WAITING_SIGNUP) { + } elseif ($this->authorized === MTProto::WAITING_SIGNUP) { if (isset($_POST['first_name'])) { yield from $this->webCompleteSignup(); } else { yield from $this->webEcho("You didn't provide the first name!"); } } - if ($this->authorized === self::LOGGED_IN) { + if ($this->authorized === MTProto::LOGGED_IN) { $this->serialize(); return yield from $this->fullGetSelf(); } diff --git a/tools/phar.php b/tools/phar.php index 0c81a763..3a2ca171 100644 --- a/tools/phar.php +++ b/tools/phar.php @@ -1,9 +1,22 @@