# Обзор

## Архитектура LeechCraft

LeechCraft основан на плагинах: ядро только обеспечивает корректную загрузку и инициализацию плагинов в правильном порядке, соединяет их между собой и управляет сообщениями между ними, в то время как вся реальная работа типа BitTorrent-клиента выполняется плагинами. Ядро также выполняет некоторые общие задачие, такие как показ GUI пользователю, отображение настроек из разных плагинов в одном диалоге или обеспечение общей базы cookies и кеша.

При запуске LeechCraft ищет плагины в директориях, соответствующих платформе, под которой он работает. Плагины, по сути, являются динамическими библиотеками (`libleechcraft_*.so` на *NIX и `libleechcraft_*.dll` на Windows), которые загружаются ядром LeechCraft при помощи `dlopen` на *NIX и `LoadLibrary` на Windows во время выполнения. Однако, плагин («виртуальный плагин») может быть предоставлен другим плагином («плагин-адаптор»), вместо того, чтобы быть загруженным из динамической библиотеки ядром LeechCraft. Виртуальные плагины должны следовать тем же правилам поведения, определенным через пару параграфов.

Каждый плагин должен иметь класс, объект которого экспортируется из библиотеки при помощи макроса Qt `Q_EXPORT_PLUGIN2`. Соответствующий объект этого класса создается Qt внутри этого макроса и может быть получен при помощи метода `QPluginLoader::instance`, если для загрузки используется `QPluginLoader` (и это внутри LeechCraft). В дальнейшем этот объект будет называться иинстанс-объектом плагина или просто инстанс-объектом.

Экспортированный класс, очевидно, должен быть `QObject`'ом и также реализовывать, по крайней мере, интерфейс `IInfo`, чтобы LeechCraft распознал и загрузил этот плагин.

При запуске LeechCraft инициализирует плагины, которые он нашел. Инициализация происходит в два этапа. Сначала вызывается `IInfo::Init` на каждом найденном плагине, затем, после того, как этот метод был вызван на всех плагинах, аналогично вызывается `IInfo::SecondInit`. Как следствие, внутри плагина нельзя использовать другие плагины до начала `IInfo::SecondInit`, в частности, в `IInfo::Init`. Поэтому нельзя запрашивать список плагинов, посылать сообщения и так далее до того, как вызвана `IInfo::SecondInit`. Однако, после `IInfo::Init` плагин должен быть в корректном и подразумевающем использование состоянии, так как остальные плагины могут начать его использовать. По сути это означает, что необходимо инициализировать все члены в `IInfo::Init`.

## Архитектура LeechCraft

LeechCraft основан на плагинах. Ядро LeechCraft только обеспечивает корректный порядок загрузки/выгрузки плагинов, инициализирует их, соединяет их между собой и обеспечивает доставку сообщений между ними. Ядро также выполняет некоторые общие задачи типа показывания общего GUI пользователю, объединения настроек от различных плагинов в один диалог или поддержания сетевого кеша и базы cookies.

Плагины могут взаимодействовать между собой либо через набор интерфейсов (основанный, но не ограниченный общими для LeechCraft интерфейсами, находящимися в директории `/src/interfaces/`), либо используя механизм сообщений.

Рассмотрим оба этих метода.

### Механизм сообщений

Этот метод больше подходит, когда вы не знаете конкретный плагин, либо не знаете, как этот плагин устроен, и взаимодействие через сообщения вполне вас устраивает. Диаметрально противоположный случай — когда необходимо обеспечить взаимодействие двух (или более) конкретных плагинов, а вводить для этого еще один интерфейс не хочется.

Взаимодействие обеспечивается при помощи структуры LeechCraft::Entity (находящейся в `/src/interfaces/structures.h`), которую можно рассматривать как некий пакет или сообщение — иными словами, Entity соответствует некоторой сущности. Плагины шлют эти Entity-сообщения, ядро LeechCraft ловит их, и... Здесь начинается самое интересное.

Итак, есть два типа сообщений. Одинаковая Entity используется для каждого из этих типов, и эти сообщения различаются только методом отправки их из плагина.

Первый тип сообщений — уведомление. Уведомления используются для уведомления ядра и других плагинов о том, что появилась новая сущность, но плагин не заботится о том, что произойдет с ней дальше.

В качестве примера рассмотрим BitTorrent-клиент, который только что завершил закачку. Он уведомляет остальные части LeechCraft о файлах, которые он только что закончил скачивать, но ему совершенно не важно, что с ними произойдет далее.

Так как последствия такого сообщения не интересуют плагин прямо в точке создания уведомления, сигнал, используемый для отправки этого уведомления не блокирующий и возвращается сразу. Он помещает сообщение в главный цикл сообщений, и таким образом обработка сообщения произойдет только тогда, когда управление снова перейдет циклу сообщений.

Для отправки уведомления используется

    						void gotEntity
    						LeechCraft::Entity *entity

сигнал. Достаточно просто определить его в главном классе вашего плагина (это тот, который экспортируется при помощи макроса `Q_EXPORT_PLUGIN2`), и ядро LeechCraft автоматически найдет его.

Второй тип сообщений — запрос на делегирование. Он используется для делегирования некой задачи другому плагину в надежде, что найдется плагин, способный обработaть такой запрос. При использовании такого типа сообщений возможно получить доступ к плагину, который был выбран для обработки запроса, и, соответственно, можно в дальнейшем взаимодействовать с ним. Очевидно, соответствующий сигнал блокирующий.

Например, рассмотрим Aggregator — клиент для чтения новостей. Он не скачивает ленты сам, вместо этого он делегирует это какому-нибудь другому плагину, отправляя запрос на делегирование. После возврата из соответствующего сигнала, плагин-отправитель будет знать, делегирована ли сущность, и если она делегирована успешно, какой плагин ее обрабатывает.

Для отправки запроса на делегирование используется

    						void delegateEntity
    						LeechCraft::Entity *entity
    						int *id = 0
    						QObject **handler = 0

сигнал. Здесь, если делегирование прошло успешно, `handler` будет указывать на главный объект плагина, который обрабатывает этот запрос, и `id` будет равен идентификатору задачи в этом обработчике — некоторому неотрицательному числу. Для проверки, было ли делегирование успешным, часто используется следующий финт: перед вызовом сигнала `delegateEntity`, `id` устанавливается в некоторое отрицательное значение, например, -1. Если после вызова сигнала оно так и осталось этим отрицательным значением, то запрос на делегирование не удался.

#### Делегирование и проверка результата

В этом примере отправляется запрос на делегирование для закачивания данного `url` в заданное место (`location`) с параметрами задачи, часто используемыми для внутренних задач в LeechCraft (таких как скачивание ленты новостей: пользователю необязательно знать о самом процессе скачивания). Мы проверяем, успешен ли был запрос, в случае неуспеха мы используем `LeechCraft::Util::MakeNotification` для создания пользовательского уведомления (структуры Entity, описывающей некоторое сообщение для пользователя, не путать с сообщением-уведомлением в смысле типа сообщения) с некоторым понятным пользователю заголовком, описанием ошибки и приоритетом. Мы излучаем это пользовательское сообщение как уведомление при помощи сигнала `gotEntity`. Кстати, это реальный кусок кода из плагина LackMan — пакетного менеджера.

    Entity e = Util::MakeEntity (url,
    		location,
    		LeechCraft::Internal |
    			LeechCraft::DoNotNotifyUser |
    			LeechCraft::DoNotSaveInHistory |
    			LeechCraft::NotPersistent |
    			LeechCraft::DoNotAnnounceEntity);
    int id = -1;
    QObject *pr;
    emit delegateEntity (e, &id, &pr);
    if (id == -1)
    {
    	emit gotEntity (Util::MakeNotification (tr ("Error fetching repository"),
    			tr ("Could not find plugin to fetch repository information for %1.")
    				.arg (url.toString ()),
    			PCritical_));
    	return;
    }

В некоторых случаях необходимо знать, способен ли вообще LeechCraft обработать некоторую сущность здесь и сейчас, но не обрабатывать ее. Например, плагин Poshuku — веб-браузер, при завершении загрузки страницы проверяет, какие ссылки на внешние ресурсы (типа RSS-лент или OpenSearch-описаний) могут быть обработаны. Для тех ссылок, которые могут быть обработаны, Poshuku дает возможность их обработать.

Для проверки возможности обработки сущности, используется сигнал

    						void couldHandle
    						const LeechCraft::Entity& entity
    						bool *could

Этот сигнал блокирующий. Он возвращает либо после того, как ядро найдет первый способный обработать сущность `entity` плагин, либо если все опрошенные плагины отказываются ее обработать. Результат сохраняется в переменной, на которую указывает `could`.

В общшем случае лучше не хранить результат этого сигнала долго, так как плагины могут динамически во время выполнения решать, обрабатывать ту или иную сущность или нет. Однако, обычно вполне безопасно исходить из предположения, что результат не изменится до возврата в основной цикл событий.

### Использование интерфейсов

Это в некотором смысле гораздо более простой подход, но он также гораздо более «статичный». Имея указатель на `QObject` — главный объект плагина (объект класса, экспортированного из плагина при помощи `Q_EXPORT_PLUGIN2`), с которым вы хотите взаимодействовать, вы можете попробовать преобразовать его при помощи `qobject_cast` в интересующий вас интерфейс.

Есть три основных способа получить указатель на другой плагин:

- Он может быть установлен ядром LeechCraft в обработчике сигнала `delegateEntity`: третий параметр этого сигнала — указатель на указатель на объект-обработчик сущности. предыдущая секция содержит больше информации на эту тему.

- Он может быть получен плагином в функции `IInfo::SetProvider`, если он вернул соответствующий список зависимостей из `IInfo::Needs` или `IInfo::Uses`.

- Вы также можете получить список всех плагинов через объект-менеджер плагинов, который можно получить через `ICoreProxy::GetPluginsManager`. Соответствующий `ICoreProxy` передается в `IInfo::Init` плагина.

Рассмотрим пару простых примеров, иллюстрирующих основные идеи взаимодействия с другими плагинами.

#### Получение любого плагина, реализующего интерфейс `IWebBrowser`.

Это реальный пример из плагина Aggregator. Полагается, что `ICoreProxy`, переданный в `IInfo::Init` вашего плагина, сохранен как `Proxy_`.

    IWebBrowser* Core::GetWebBrowser () const
    {
        IPluginsManager *pm = Proxy_->GetPluginsManager ();
        QObjectList browsers = pm->Filter<IWebBrowser*> (pm->GetAllPlugins ());
        return browsers.size () ?
            qobject_cast<IWebBrowser*> (browsers.at (0)) :
            0;
    }

#### Более продвинутое делегирование и обработка ошибок.

Это другая часть кода, взятая из Aggregator. Вот как он делегирует скачивание свежих версий лент новостей другому плагину.

    void Core::UpdateFeed (const QString& url)
    {
        QList<int> keys = PendingJobs_.keys ();
        Q_FOREACH (int key, keys)
            if (PendingJobs_ [key].URL_ == url)
            {
                QObject *provider = ID2Downloader_ [key];
                IDownload *downloader = qobject_cast<IDownload*> (provider);
                if (downloader)
                {
                    qWarning () << Q_FUNC_INFO
                        << "stalled task detected from"
                        << downloader
                        << "trying to kill...";
                    downloader->KillTask (key);
                    ID2Downloader_.remove (key);
                    PendingJobs_.remove (key);
                    qWarning () << Q_FUNC_INFO
                        << "killed!";
                }
                else
                    qWarning () << Q_FUNC_INFO
                        << "provider is not a downloader:"
                        << provider
                        << "; cannot kill the task";
                return;
            }

        QString filename = LeechCraft::Util::GetTemporaryName ();

        LeechCraft::Entity e = LeechCraft::Util::MakeEntity (QUrl (url),
                filename,
                LeechCraft::Internal |
                    LeechCraft::DoNotNotifyUser |
                    LeechCraft::DoNotSaveInHistory |
                    LeechCraft::NotPersistent |
                    LeechCraft::DoNotAnnounceEntity);

        PendingJob pj =
        {
            PendingJob::RFeedUpdated,
            url,
            filename,
            QStringList ()
        };

        int id = -1;
        QObject *pr;
        emit delegateEntity (e, &id, &pr);
        if (id == -1)
        {
            qWarning () << Q_FUNC_INFO << url << "wasn't delegated";
            return;
        }

        HandleProvider (pr, id);
        PendingJobs_ [id] = pj;
        Updates_ [url] = QDateTime::currentDateTime ();
    }

    void Core::HandleProvider (QObject *provider, int id)
    {
        if (Downloaders_.contains (provider))
            return;

        Downloaders_ << provider;
        connect (provider,
                SIGNAL (jobFinished (int)),
                this,
                SLOT (handleJobFinished (int)));
        connect (provider,
                SIGNAL (jobRemoved (int)),
                this,
                SLOT (handleJobRemoved (int)));
        connect (provider,
                SIGNAL (jobError (int, IDownload::Error)),
                this,
                SLOT (handleJobError (int, IDownload::Error)));

        ID2Downloader_ [id] = provider;
    }