Implement IPC

This commit is contained in:
Daniil Gentili 2020-07-11 20:01:54 +02:00
parent 7d29bd84bc
commit c8cdf328c0
Signed by: danog
GPG Key ID: 8C1BE3B34B230CA7
30 changed files with 1313 additions and 238 deletions

View File

@ -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;
if (isset($this->wrapper)) {
Tools::wait($this->wrapper->serialize());
if ($this->unlock) ($this->unlock)();
}
if ($this->unlock) {
($this->unlock)();
}
} else {
$this->logger->logger('Shutting down MadelineProto (old deserialized instance of API)');
}

View File

@ -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;

View File

@ -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);
}

View File

@ -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)

View File

@ -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;

View File

@ -0,0 +1,139 @@
<?php
/**
* API module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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!");
}
}

View File

@ -0,0 +1,163 @@
<?php
/**
* API wrapper module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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!");
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace danog\MadelineProto\Ipc;
use danog\MadelineProto\RPCErrorException;
use function Amp\Parallel\Sync\flattenThrowableBacktrace;
final class ExitFailure
{
/** @var string */
private $type;
/** @var string */
private $message;
/** @var int|string */
private $code;
/** @var string[] */
private $trace;
/** @var string */
private $tlTrace;
/** @var self|null */
private $previous;
/** @var self|null */
private $localized;
public function __construct(\Throwable $exception)
{
$this->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;
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace danog\MadelineProto\Ipc\Runner;
use Amp\Process\Process as BaseProcess;
use Amp\Promise;
final class ProcessRunner extends RunnerAbstract
{
/** @var string|null Cached path to located PHP binary. */
private static $binaryPath;
/** @var \Amp\Process\Process */
private $process;
/**
* Constructor.
*
* @param string $session Session path
*/
public function __construct(string $session)
{
if (\PHP_SAPI === "cli") {
$binary = \PHP_BINARY;
} else {
$binary = self::$binaryPath ?? self::locateBinary();
}
$options = [
"html_errors" => "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<int> Resolved with the PID
*/
public function start(): Promise
{
return $this->process->start();
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace danog\MadelineProto\Ipc\Runner;
use Amp\Promise;
abstract class RunnerAbstract
{
const SCRIPT_PATH = __DIR__."/entry.php";
/** @var string|null External version of SCRIPT_PATH if inside a PHAR. */
protected static $pharScriptPath;
/** @var string|null PHAR path with a '.phar' extension. */
protected static $pharCopy;
protected static function getScriptPath(string $alternateTmpDir = '')
{
/**
* If using madeline.php, simply return madeline.php path.
*/
if (\defined(\MADELINE_PHP::class)) {
return \MADELINE_PHP;
}
// Write process runner to external file if inside a PHAR different from madeline.phar,
// because PHP can't open files inside a PHAR directly except for the stub.
if (\strpos(self::SCRIPT_PATH, "phar://") === 0) {
$alternateTmpDir = $alternateTmpDir ?: \sys_get_temp_dir();
if (self::$pharScriptPath) {
$scriptPath = self::$pharScriptPath;
} else {
$path = \dirname(self::SCRIPT_PATH);
if (\substr(\Phar::running(false), -5) !== ".phar") {
self::$pharCopy = $alternateTmpDir."/phar-".\bin2hex(\random_bytes(10)).".phar";
\copy(\Phar::running(false), self::$pharCopy);
\register_shutdown_function(static function (): void {
@\unlink(self::$pharCopy);
});
$path = "phar://".self::$pharCopy."/".\substr($path, \strlen(\Phar::running(true)));
}
$contents = \file_get_contents(self::SCRIPT_PATH);
$contents = \str_replace("__DIR__", \var_export($path, true), $contents);
$suffix = \bin2hex(\random_bytes(10));
self::$pharScriptPath = $scriptPath = $alternateTmpDir."/madeline-ipc-".$suffix.".php";
\file_put_contents($scriptPath, $contents);
\register_shutdown_function(static function (): void {
@\unlink(self::$pharScriptPath);
});
}
} else {
$scriptPath = self::SCRIPT_PATH;
}
return $scriptPath;
}
/**
* Constructor.
*
* @param string $session Sessio path
*/
abstract public function __construct(string $session);
/**
* Starts the execution process.
*
* @return Promise<int> Resolves with the PID
*/
abstract public function start(): Promise;
}

View File

@ -0,0 +1,107 @@
<?php
namespace danog\MadelineProto\Ipc\Runner;
use Amp\ByteStream\ResourceOutputStream;
use Amp\Parallel\Context\ContextException;
use Amp\Promise;
final class WebRunner extends RunnerAbstract
{
/** @var string|null Cached path to the runner script. */
private static $runPath;
/**
* Initialization payload.
*
* @var array
*/
private $params;
/**
* Socket.
*
* @var ResourceOutputStream
*/
private $res;
/**
* Constructor.
*
* @param string $session Session path
*/
public function __construct(string $session)
{
if (!isset($_SERVER['SERVER_NAME'])) {
throw new ContextException("Could not initialize web runner!");
}
if (!self::$runPath) {
$uri = \parse_url('tcp://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (\substr($uri, -1) === '/') { // http://example.com/path/ (assumed index.php)
$uri .= 'index'; // Add fake file name
}
$uri = \str_replace('//', '/', $uri);
$rootDir = \debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$rootDir = \end($rootDir)['file'] ?? '';
if (!$rootDir) {
throw new ContextException('Could not get entry file!');
}
$rootDir = \dirname($rootDir);
$uriDir = \dirname($uri);
if (\substr($rootDir, -\strlen($uriDir)) !== $uriDir) {
throw new ContextException("Mismatch between absolute root dir ($rootDir) and URI dir ($uriDir)");
}
// Absolute root of (presumably) readable document root
$localRootDir = \substr($rootDir, 0, \strlen($rootDir)-\strlen($uriDir)).DIRECTORY_SEPARATOR;
$runPath = self::getScriptPath($localRootDir);
if (\substr($runPath, 0, \strlen($localRootDir)) === $localRootDir) { // Process runner is within readable document root
self::$runPath = \substr($runPath, \strlen($localRootDir)-1);
} else {
$contents = \file_get_contents(self::SCRIPT_PATH);
$contents = \str_replace("__DIR__", \var_export($localRootDir, true), $contents);
$suffix = \bin2hex(\random_bytes(10));
$runPath = $localRootDir."/madeline-ipc-".$suffix.".php";
\file_put_contents($runPath, $contents);
self::$runPath = \substr($runPath, \strlen($localRootDir)-1);
\register_shutdown_function(static function () use ($runPath): void {
@\unlink($runPath);
});
}
self::$runPath = \str_replace(DIRECTORY_SEPARATOR, '/', self::$runPath);
self::$runPath = \str_replace('//', '/', self::$runPath);
}
$this->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);
}
}

View File

@ -0,0 +1,87 @@
<?php
/**
* IPC server entry module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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');
}
}
}
})();

View File

@ -0,0 +1,133 @@
<?php
/**
* API wrapper module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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";
}
}

View File

@ -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);

View File

@ -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.
*

View File

@ -19,17 +19,13 @@
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
*
@ -45,7 +41,7 @@ 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
*
@ -70,7 +66,7 @@ trait AckHandler
return true;
}
/**
* Acknowledge incoming message ID
* Acknowledge incoming message ID.
*
* @param string|int $message_id Message ID
*

View File

@ -19,17 +19,13 @@
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

View File

@ -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);

View File

@ -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 = [];

View File

@ -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'])) {

View File

@ -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 [];

View File

@ -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,13 +222,63 @@ 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
{
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 (!self::$inited) {
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');
}
}
// Check if we're in a console, for colorful log output
try {
self::$isatty = \defined(\STDOUT::class) && \function_exists('posix_isatty') && \posix_isatty(\STDOUT);
} catch (\danog\MadelineProto\Exception $e) {
}
// Important, obtain root relative to caller script
$backtrace = \debug_backtrace(0);
self::$script_cwd = self::$cwd = \dirname(\end($backtrace)['file']);
try {
self::$cwd = \getcwd();
self::$can_getcwd = true;
} catch (\danog\MadelineProto\Exception $e) {
}
// 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 () {
Logger::log('Got sigint', Logger::FATAL_ERROR);
Magic::shutdown(1);
}));
Loop::unreference(Loop::onSignal(SIGTERM, static function () {
Logger::log('Got sigterm', Logger::FATAL_ERROR);
Magic::shutdown(1);
}));
} catch (\Throwable $e) {
}
}
self::$initedLight = true;
if ($light) {
\define('AMP_WORKER', true);
return;
}
}
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']);
}
@ -259,10 +311,6 @@ class Magic
self::$twoe2048 = new \tgseclib\Math\BigInteger('32317006071311007300714876688669951960444102669715484032130345427524655138867890893197201411522913463688717960921898019494119559150490921095088152386448283120630877367300996091750197750389652106796057638384067568276792218642619756161838094338476170470581645852036305042887575891541065808607552399123930385521914333389668342420684974786564569494856176035326322058077805659331026192708460314150258592864177116725943603718461857357598351152301645904403697613233287231227125684710820209725157101726931323469678542580656697935045997268352998638215525166389437335543602135433229604645318478604952148193555853611059596230656');
self::$twozerotwosixone = new \tgseclib\Math\BigInteger(20261);
self::$zeroeight = new \tgseclib\Math\BigInteger('2147483648');
try {
self::$isatty = \defined('STDOUT') && \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;
@ -299,47 +347,6 @@ class Magic
if (!self::$can_parallel && !\defined('AMP_WORKER')) {
\define('AMP_WORKER', 1);
}
$backtrace = \debug_backtrace(0);
self::$script_cwd = self::$cwd = \dirname(\end($backtrace)['file']);
try {
self::$cwd = \getcwd();
self::$can_getcwd = true;
} catch (\danog\MadelineProto\Exception $e) {
}
// Even an empty handler is enough to catch ctrl+c
if (\defined('SIGINT')) {
//if (function_exists('pcntl_async_signals')) pcntl_async_signals(true);
try {
Loop::unreference(Loop::onSignal(SIGINT, static function () {
Logger::log('Got sigint', Logger::FATAL_ERROR);
Magic::shutdown(1);
}));
Loop::unreference(Loop::onSignal(SIGTERM, static function () {
Logger::log('Got sigterm', Logger::FATAL_ERROR);
Magic::shutdown(1);
}));
} 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');
}
}
$res = \json_decode(@\file_get_contents('https://rpc.madelineproto.xyz/v3.json'), true);
if (isset($res['ok']) && $res['ok']) {
RPCErrorException::$errorMethodMap = $res['result'];
@ -347,7 +354,6 @@ class Magic
}
self::$inited = true;
}
}
/**
* Check if this is a POSIX fork of the main PHP process.
*

View File

@ -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,16 +53,38 @@ 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, '<br>'.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);
if (\is_string($caller)) {
$this->prettifyTL($caller);
$this->caller = $caller;
$additional = [];
@ -73,6 +95,7 @@ class RPCErrorException extends \Exception
$additional = $level['args'];
}
}
}
/*
if (\in_array($this->rpc, ['CHANNEL_PRIVATE', -404, -429, 'USERNAME_NOT_OCCUPIED', 'ACCESS_TOKEN_INVALID', 'AUTH_KEY_UNREGISTERED', 'SESSION_PASSWORD_NEEDED', 'PHONE_NUMBER_UNOCCUPIED', 'PEER_ID_INVALID', 'CHAT_ID_INVALID', 'USERNAME_INVALID', 'CHAT_WRITE_FORBIDDEN', 'CHAT_ADMIN_REQUIRED', 'PEER_FLOOD'])) {
return;

View File

@ -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();
}

View File

@ -0,0 +1,120 @@
<?php
/**
* Session paths module.
*
* This file is part of MadelineProto.
* MadelineProto is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* MadelineProto is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with MadelineProto.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2020 Daniil Gentili <daniil@daniil.it>
* @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;
}
}

View File

@ -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");
}
}

View File

@ -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 = '<br>'.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;
}
}
}

View File

@ -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;

View File

@ -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();
}

View File

@ -1,10 +1,23 @@
<?php
\define('MADELINE_PHP', __FILE__);
function ___install_madeline()
{
if (\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 {
$arguments = [];
}
if (isset($arguments[1]) && $arguments[1] === 'madeline-ipc') {
\define(\MADELINE_WORKER_START::class, $arguments[2]);
} else {
die('You must include this file in another PHP script'.PHP_EOL);
}
}
$old = false;
if (PHP_MAJOR_VERSION === 5) {
if (PHP_MINOR_VERSION < 6) {