Материал из Tkabber Wiki
В этой статье будет сделана попытка раскрыть волнующую многих тему написания
собственного плагина. Автор практического пособия (первых двух разделов статьи)
— ещё плохой охотник неопытный плагинописатель, зато его набитые шишки и
взгляд на вещи глазами новичка может помочь таким же начинающим. Впрочем,
поправки принимаются.
Содержание
- 1 С чего начать
- 2 Практические советы
- 3 Общие сведения о репозитории
- 4 Как получить доступ к репозиторию
- 5 В заключение
С чего начать
Прежде всего, вам необходимо желание написать плагин. Без этого никак. Если вы вообще тикль видите впервые, а плагин хочется (и никто его не пишет), то вам нужно огромное желание.
Учите матчасть
Выучите вы уже этот тикль, не бойтесь — он несложный
Первым практическим шагом станет изучение основ этого самого тикля. В статье
Ссылки есть немало линков на различные ресурсы (в том числе
и русскоязычные) по этому языку. В принципе, чтения книги самого Аустерхаута,
дополненное занятиями, предложенными на этом сайте, должно хватить, чтобы начать
мало-мальски в нём разбираться. Ещё один ценный источник информации, который
всегда под рукой — мануалы: например, man 3tcl if
и man 3tk frame
(маны
доступны и в интернете, но xterm всё же ближе). Могу вас успокоить — это вам не
си-плюс-плюс, не перл и не питон (не знаю ни одного из этих языков, но могу
голову дать на отсечение — тикль вы выучите на порядок быстрее любого из них).
Спорим, что двух недель вам хватит? ;-P
Изучите документацию и исходники
Очень рекомендуется прочитать файл README, который лежит в корне установочного каталога Ткаббера. Там описано буквально всё: начиная от процесса установки Ткаббера до описания всех его настроек, а также хуков и прочих интересных вещей, без знания о которых вам будет тяжело. Ваш покорный слуга свой первый плагин написал, не пользуясь никакими хуками (точнее, без них не обошлось, но они были благополучно скопипащены из другого плагина). Сейчас же, продолжая ковырять код Ткаббера, автор этого руководства в полной мере осознал ценность README и в ближайшие дни собирается его осилить, чего и вам желает.
В принципе, этого уже достаточно, но было бы совсем неплохо пробежаться глазами по списку наличествующих ткабберовских скриптов, а также бегло проглядеть их внутренности, обращая внимание на названия процедур — например, процедура
proc muc::join_group
обрабатывает вход в конференцию, а идущая за нейmuc::leave_group
— выход из неё. Не забудьте и про хуки.Не знаю, насколько это верно, но мне кажется, что почти любой плагин можно написать, надёргав кода из самого Ткаббера и/или других плагинов, развесив этот код по своему скрипту и связав его небольшим количеством продуктов своей мозговой деятельности, поэтому чем лучше вы ориентируетесь в исходниках, тем проще для вас. Если вы не пишете что-нибудь из ряда вон выходящее вроде поддержки jingle, скорее всего, так оно и будет. Более того, так и надо делать. Зачем изобретать свои детали к велосипеду, когда к вашим услугам целый склад? Но чтобы воспользоваться этими деталями, надо знать, где они лежат. Для этого надо твёрдо знать, что именно вы хотите сделать, каким будет ваш плагин.
Например, плагин Bldjid для забанивания пользователя по джиду во всех конференциях, где я админю, я хотел реализовать как команду в стиле IRC: /bldjid vasya@pupkin.ru. Если точнее, то сначала я хотел сделать диалог вроде того, который появляется, когда вы хотите послать кому-то приглашение в конференцию. Но, подумав, что это без подготовки будет чересчур геморройно, я выбрал более простой и более эффективный путь. Механизм работы IRC-style-команд я взял в плагине Urlcmd. Поскольку баны тесно связаны с работой в конференции, мне понадобилось довольно плотно изучить файл muc.tcl. Не обошлось и без заимствований оттуда.
Практические советы
Структура плагина
Структура каталога
Про это коротко, но ясно рассказано здесь.
Структура файла скрипта
Информация о плагине
В самом начале обычно пишутся некоторые данные о плагине и его авторе, в виде комментариев. Если вы не планируете делать развёрнутое описание плагина в отдельном файле, можете здесь же вкратце указать, как им пользоваться. Я поначалу так и сделал. Не забудьте дать свою контактную информацию (мыло, джаббер).
Задание пространства имён
"Во первы́х строка́х письма" © В. С. В.
желательно определить
пространство имён для процедур и переменных плагина, на случай, если вам
взбредёт в голову назвать одну из процедур, к примеру, join_group (в muc.tcl
есть такая процедура, и, чтобы не возникло путаницы и непонятно откуда
берущихся ошибок, лучше это пресечь уже сейчас). Более того, как советуют
знающие люди, если вы точно не знаете зачем вам поступать
по-другому, все процедуры и переменные вашего плагина должны быть в своём
пространстве имён. Также следует помнить, что загрузка плагинов делается кодом,
который выполняется внутри namespace eval ::plugins
, поэтому 1) собственное
пространство имён не должно быть абсолютным (без веской причины) 2) надо везде
использовать [namespace current]
, если требуется получить полное имя
переменной/процедуры. Чтобы не ходить далеко за примером, разберу свой плагин.
namespace eval bldjid {
::msgcat::mcload [file join [file dirname [info script]] msgs]
hook::add generate_completions_hook \
[namespace current]::command_comps
hook::add chat_send_message_hook \
[namespace current]::handle_commands 17
}
Как видите, для задания пространства имён нужна команда namespace. Внутри
фигурных скобок пишется то, что будет действовать в пределах этого
пространства. Например, можно задавать хуки, устанавливать переменные или
подгружать файлы сообщений, как во второй строчке примера. Немного подробнее о
хуках можно почитать тут. Обратите внимание, имена
хуков и их обработчиков я стырил из плагина-донора, но, чтобы мои обработчики
не перепутались с оригинальными, к ним спереди прибавляется текущее
пространство имён: при выполнении скрипта (см. объяснение ниже).[namespace current]
заместится
на bldjid и получится bldjid::command_comps
Строго говоря, в том плагине они тоже работают внутри своего пространства, поэтому с нашими не перепутаются, но лучше сделать всё по-человечески. С пространствами имён надо быть осторожнее и лучше перестраховываться, добавляя [namespace current]:: перед именем вызываемой процедуры. Дело в том, что пространство имён плагина само становится "отпрыском" пространства имён ::plugins — Это означает, что если вызывать процедуру вот так:
bldjid::command_comps
мы получим ошибку о неправильном имени этой команды. Так что если быть точнее,
то при выполнении скрипта [namespace current]
заместится
на ::plugins::bldjid. Или можно ещё вызывать процедуру безо всяких
неймспейсов, если вы уверены, что это имя уникально. Если же вы использовали
какую-то чужую процедуру и не поменяли ей имя, не забудьте про пространство
имён!
Настройки плагина
Если вы планируете делать плагин настраиваемым, после объявления неймспейса добавьте необходимые настройки. Вот пара настроек из плагина reversi:
custom::defgroup Plugins [::msgcat::mc "Plugins options."] -group Tkabber
custom::defgroup Reversi [::msgcat::mc "Reversi plugin options."] -group Plugins
custom::defvar options(theme) Checkers \
[::msgcat::mc "Reversi figures theme."] -group Reversi \
-type options -values $values \
-command [namespace current]::load_stored_theme
Смело копируйте эти или другие настройки себе и модифицируйте под свои нужды.
Тело плагина
Далее идут процедуры. Как минимум должны иметься процедуры-обработчики ваших хуков. Нужны ли вам другие — решать вам в зависимости от ваших потребностей, возможностей и стиля программирования. Вот для примера маленькая процедура-обработчик, которую я взял из urlcmd и адаптировал под себя:
proc bldjid::command_comps {chatid compsvar wordstart line} {
upvar 0 $compsvar comps
if {!$wordstart} {
lappend comps {/bldjid } {/unbldjid } {/banjid }
}
}
Обратите внимание на пространство имён bldjid, которое идёт перед именем процедуры (и отделяется от имени двумя двоеточиями). Его надо добавлять ко всем именам процедур скрипта, которые должны относиться к объявленному неймспейсу. Почувствуйте разницу: в utils.tcl имена процедур пишутся без пространства, потому что это базовые процедуры, и они выполняются в базовом пространстве Ткаббера — :: — и вызываются потом из других скриптов напрямую. Ещё обратите внимание на команды {/bldjid } {/unbldjid } {/banjid }, которые добавляются в список comps. Поначалу у меня были только две первые. Когда они заработали, я решил добавить бан по джиду в текущей комнате. Процедуру-то соответствующую дописал, а сюда команду добавить забыл и потом долго удивлялся, почему она не работает :)
Структура стандартного плагина примерно такова, но иногда (например, в плагине
custom-urls.tcl) в самом конце снова объявляется пространство имён, но уже с
другой начинкой.
Сделать: Уточнить и рассказать про это.
Анализируйте чужой код и не бойтесь экспериментировать
Закончим анализ разбиравшегося плагина
Вторая процедура — bldjid::handle_commands
— тоже обработчик хука (помните
объявление пространства имён?). Её начало я тоже слямзил из urlcmd.tcl, а
остальное — из muc.tcl. В процессе допиливания чужого кода выяснилось, что мне
нужны ещё процедуры. Для команды /unbldjid мне идеально подошла внешняя:
muc::unban
, и я просто вызываю её в цикле. Так что разбанивание у меня
заработало раньше забанивания :)
С баном вышло посложнее: я не знал, как лучше всё сделать, и поначалу пошёл по
неправильному пути — начал изучать процедуры запроса, получения и отсылки
чёрного списка (последняя из них — muc::send_list
). Процедуры большие и
выглядят довольно кошмарно (для новичка), но самое главное, что мне не
нравилось — это необходимость вытягивать весь список только для того, чтобы
прибавить к нему своего пациента и отправить весь список снова на сервер. Очень
нерационально! И тут мне случайно на глаза попался такой комментарий в
процедуре-обработчике muc::change_item_param
, который обрабатывает команды
Ткаббера /ban, /kick, /admin и другие: For unknown reason banning request MUST
be based on user's bare JID (which may be not known by admin). "Опа, — подумал
я, — так значит, серверу всё-таки шлётся реальный джид пользователя, а не
room@conference.jabber.ru/nick". Так что я просто забрал остатки кода после
этого комментария и сделал из него свою процедуру, выкинув ненужное. Также мне
пришлось наприсать свою процедуру проверки на ошибку, основывающуюся на
muc::test_error_res
, потому что оригинальная меня не устраивала. Процедура
отсылки бана вызывается один раз, если у нас команда /banjid (её я добавил
позже, когда всё заработало), и в цикле, если команда — /bldjid. Вот,
собственно, и всё.
Ну или почти всё. Плагин работает, но его можно чуть-чуть доделать. Сейчас он
шлёт запросы на бан тупо во все комнаты, где я нахожусь. Но админю-то я не во
всех. Это означает лишний трафик для меня и лишнюю нагрузку на сервер. Значит,
мне надо как-то отфильтровать те комнаты, где я имею права админа. Тут, как
может показаться, заколдованный круг: чтобы узнать, админю ли я в комнате, надо
спросить у сервера. А это как раз лишний трафик и лишний пинок серверу. Но если
подумать хорошо, то мы ведь уже знаем, что если я в ростере комнаты сижу в
группе "Модераторы", то значит, и права тут имею модераторские. Так что надо
проверить ростер каждой конференции и выбрать лишь нужные. Вот и обозначилось
направление для поисков — найти, где рисуется ростер конференции, и выдрать
оттуда нужный код :) На самом деле всё немного сложнее, потому что я могу быть
не админом, а модератором — я буду находиться в группе модеров, но банить не
смогу. Небольшие издержки, но соотношение "количество работы/результат"
получается неплохое. Ещё потребуется добавить исключение комнат вида
channel%irc.server1.ru@irc.server2.com
, чтобы запросы не слались в комнаты,
не поддерживающие стандарт MUC. Тут уже, скорее всего, придётся писать свой
парсер.
Отладка
Спешка нужна лишь при ловле блох
При отлове багов торопиться не надо. Вот вы получили сообщение об ошибке. Прочитайте его внимательно: там дана практически вся необходимая для отладки информация. Если виной всему опечатка в имени команды или аргумента, так и будет сказано (имя команды не существует или что-то в этом духе). Если вы позабыли где-то скобку, об этом тоже будет сказано начистоту. Вам дадут ссылку на ошибку вплоть до седьмого колена: в какой строке какой процедуры, вызванной кем и откуда находится источник проблем.
Сделать: Развить, если необходимо.
Сделать: Вкратце написать про ::plugins::debug.
Консоль Ткаббера — наше всё
Она сэкономит вам уйму времени. Чтобы не перегружать Ткаббер после каждого изменения кода (а поначалу ошибки будут сыпаться как осенние листья, да и потом не факт, что код будет работать правильно и как вам бы хотелось), откройте консоль Ткаббера и напишите там:
source "/путь/содержащий пробелы/к/вашему/плагину/coolplugin.tcl"
Плагин должен не отходя от кассы загрузиться (при этом в консоли вам ничего не будет сказано, что говорит о положительном результате). Если в пути ошибка, консоль изругается. Всё, можно тестировать. Если вам кажется, что он не загрузился (не делает того, что от него ожидается), проверьте, существует ли его пространство имён:
namespace exists ::bldjid
Если ответ — 1, всё должно быть в порядке. Если 0, где-то косяки (в ДНК
в коде, скорее всего — может, вы ошиблись в имени команды, и теперь Ткаббер "не
узнаёт" то, что вы ему подсовываете; при этом, кстати, команда выведется в
чате, как простое сообщение).
На первых порах очень неплохо быть в курсе того, что у вас попадает в переменные. Воспользуйтесь командой puts. Например, мы хотим узнать, действительно ли все конференции попали в список.
foreach tmpchatid [lsort [lfilter chat::is_groupchat [chat::opened $xlib]]] {
lappend groupjids [chat::get_jid $tmpchatid]
}
puts $groupjids
Результат действия этой команды будет выведен прямо в консоли. Ничто не мешает вам трассировать таким образом состояние переменных других скриптов Ткаббера. Просто добавьте печать нужной переменной в нужном месте, сохраните файл и не забудьте подгрузить его. Ещё одна удобная для тестирования команда — return stop. Если её поставить в вышеприведённом примере после puts, то дальнейшее выполнение кода прекратится. Полезно, когда сначала хочется отладить один кусок кода, не заморачиваясь на другом.
Внимание: Команда puts полезна, но не забудьте перед релизом их все убрать, иначе их вывод будет валиться в .xsession-errors пользователей плагина, что вряд ли кого-то обрадует.
Хоть консоль Ткаббера и является отличным отладочным инструментом, живого тестирования она заменить не может. Ваш покорный слуга погонял свой плагин, загруженный в Ткаббер через консоль, радостно выложил его сюда на вики, но когда через несколько дней дело дошло до реального масштабного бана, выскочила ошибка. В результате у автора плагина прибавилось ума, и он решил развить тему пространства имён (наверное, вы уже про это прочитали выше).
Сделать: Рассказать про return -code break
Побольше тестируйте
Прежде чем выкладывать плагин на общее обозрение, погоняйте его самостоятельно. Проверьте его работу в нескольких версиях Ткаббера (svn и хотя бы одной-двух из последних стабильных) и на разных тиклях (8.5, 8.4 как минимум). Возможно, вы воткнули где-то команду, которая есть в одной версии языка или Ткаббера и нет в других. Решайте сами, что вам нужно: совместимость с ними или более продвинутый программинг/функциональность. В случае сомнений посоветуйтесь со знающими людьми. Скорее всего, можно и рыбку съесть, и на ёлку влезть, то есть, разрулить ситуацию так, что и совместимость не пострадает, и работать всё будет хорошо.
Пробуйте вводить нестандартные параметры, левые символы, и вообще создавать
экстремальные условия для вашего скрипта. Например, для отсеивания каналов IRC
я в проверочном условии использовал поиск вхождения в имя символа процента,
поскольку такие комнаты работают через транспорт:
somechannel%someserver.ru@irc.server.org
. Однако потом я подумал (и это
подтвердилось на практике), что и обычная MUC-комната может иметь знак процента
в имени. Поэтому я доработал проверку таким образом:
foreach tmpchatid [lsort [lfilter chat::is_groupchat [chat::opened $xlib]]] {
set tmpgrp [chat::get_jid $tmpchatid]
if {([lindex [bldjid::whoami $xlib $tmpgrp] 1] == "admin" \
|| [lindex [bldjid::whoami $xlib $tmpgrp] 1] == "owner") \
&& ![string match *%*@*irc* $tmpgrp]} {
lappend groupjids $tmpgrp
}
}
Предполагается, что в имени транспорта обязательно должно присутствовать слово
irc (иначе мы просто не догадаемся, что это IRC-транспорт), то есть, в
теперешнем виде скрипт пропустит комнату mirc%room@conference.jabber.ru
и не
пропустит комнату channel%someserver.org@irc.putyourserverhere.ru
. Кстати,
скрипт в этом виде тормознёт и комнату mirc%room@conference.mirc.com
(представим себе на мгновение, что на этом сайте есть джаббер-сервер), поэтому
проверку следует изменить на *%*@irc* (точку после слова irc вылавливать не
надо, на случай, если транспорт называется irctransport.server.org). Теперь
максимум, что может случиться — если имя транспорта не начинается на "irc"
(что очень маловероятно), то тогда, отправив запрос на бан в эту комнату, мы
получим сообщение об ошибке.
Это лишь один пример. Напрягите фантазию, думайте за дурака-пользователя, предполагайте, что он может сморозить.
Научитесь пользоваться системой управления версиями
Не поленитесь поставить и освоить что-нибудь вроде subversion. Эта штука избавит вас от великой головной боли на тему "Блин, как уж я там позавчера эту хрень сделал? Сейчас бы мне этот кусок сюда, да потёр и не сохранил ту версию". Совсем не обязательно сходу просить допуск к репозиторию на xmpp.ru (особенно если у вас нет опыта работы с svn). Устройте себе локальный сервер: и от головной боли избавитесь, и работать с сабвершеном научитесь. Если вы считаете, что ваше творение должно находиться в общем репозитории svn.xmpp.ru, читайте дальше — там рассказано о его устройстве и о том, как получить к нему доступ. Врочем, ради одного плагина не стоит сильно заморачиваться. Выложите его на вики — делов-то.
(kostix замечает, что для локальной работы над проектом значительно удобнее Subversion использовать какого-нибудь представителя семейства распределённых систем управления версиями (DVCS), например, Bazaar, Mercurial, Git, Darcs, Monotone и т.п. — они позволяют лихо жонглировать кодом, не завися от центрального сервера.)
(bigote восторженно соглашается — не так давно освоенный им в минимальных количествах Git значительно упростил мышиную возню с кодом на десктопе. Но если вы планируете в конце концов поместить свой плагин в репозиторий tkabber-3rd-party-plugins, то лучше сразу вести разработку под svn или на крайний случай сделать это перед его публикацией туда, сначала экспортнув код из локального репозитория. Конечно, есть обвязки, позволяющие публиковать из git в svn, к примеру, но они не про нас, простых смертных начинающих плагинописателей ;). Вот, кстати, интересная притча (во всех переводах названная почему-то басней), простыми словами рассказывающая о мощи git.)
Тонкости
Здесь я попытаюсь рассказать о некоторых подводных камнях, на которые будет частенько находить ваша коса в процессе доведения плагина до ума. Моя, во всяком случае, на них натыкается преизрядно.
Хуки и return stop
Есть тонкая разница между командами return
и return stop
— первая не
прерывает выполнения хука, если процедура, в которой она встретилась, является
его обработчиком. Поясню на примере того же плагина Bldjid. Он разросся, и
теперь там есть процедура smart_enter_exit_message, которая обрабатывает хук
client_presence_hook (который срабатывает, когда кто-нибудь меняет своё
состояние либо заходит в комнату). Я по незнанию понаставил в некоторые
проверки внутри этой процедуры return stop
вместо return
, и в результате
при определённых условиях перестал показываться ростер конференции, да и другие
странности в работе Ткаббера появились. Дело было как раз в том, что вместо
того, чтобы мирно завершить свою работу и отдать бразды правления другим
обработчикам, эта процедура обрывала хук, и само собой, никто уже не заполнял
ростер группы.
Сложно дать готовый рецепт на все случаи жизни, но можно посоветовать подходить
с осторожностью к использованию return stop
в обработчиках хуков. В обычных
процедурах наоборот, лучше перестраховаться. Я только что попробовал позаменять
все стопы на обычные return в теле простых процедур — результат плачевный:
команды-то выполняются, но потом их текст выводится в общий чат, что нам
совершенно не нужно.
Делаем плагин динамически подключаемым
С появлением в Ткаббере Менеджера плагинов жизнь пользователя стала намного проще. Чтобы подключить нужный плагин, достаточно сделать несколько кликов (Настройки → Plugins Management → отметить галочкой нужный плагин → сохранить настройку). Естественно, сам плагин перед этим всё равно должен быть помещён в соответствующее место, но согласитесь, что это значительный шаг вперёд, тем более, что, сняв эту галочку, плагин можно частично из памяти выгрузить. Почему частично? Не буду забегать вперёд, начну по порядку.
Добавляем в namespace код для запуска процедур загрузки и выгрузки
if {![::plugins::is_registered bldjid]} {
::plugins::register bldjid \
-namespace [namespace current] \
-source [info script] \
-description [::msgcat::mc "Whether the Bldjid plugin is loaded."] \
-loadcommand [namespace code load] \
-unloadcommand [namespace code unload]
return
}
Помещать его следует в самом начале неймспейса, но после строки ::msgcat::mcload [file join [file dirname [info script]] msgs], если таковая у вас имеется, иначе не загрузится локализованный текст пояснительной строчки. Как видите, тут всё просто. Вам надо только поменять слово bldjid на название вашего плагина (должно совпадать с именем его каталога).
Создаём эти самые процедуры загрузки и выгрузки
Если вы взглянете на подобные плагины других авторов, сразу же заметите, что вышеприведённый код везде одинаков (за исключением имён), а вот процедуры везде разные. Впрочем, можно выделить общие черты:
- Почти наверняка ваш плагин использует хуки. Все хуки плагина (не только из неймспейса, но и из его тела тоже) должны быть собраны в процедуре загрузки.
- Скорее всего у вас есть и парочка глобальных переменных. Их тоже имеет смысл засунуть сюда.
- Если плагин использует сочетания клавиш (как, например, Ctrl-m в Bldjid для показа/скрытия монитора), их тоже стоит перенести сюда.
- Редко, но иногда может понадобиться выполнить какие-то действия. Смотрите пример чуть ниже.
Процедура загрузки
Рассмотрим разбираемый в этой статье плагин Bldjid:
proc bldjid::load {} {
# A variable where the last query result is stored.
variable user_list
# An array where all user entrances are stored.
global jids_by_chats
variable ent_cntr 1
event add <<ToggleMonitor>> <Control-m>
hook::add generate_completions_hook \
[namespace current]::visitors_compls
hook::add chat_send_message_hook \
[namespace current]::handle_commands 17
::hook::add open_chat_post_hook [namespace current]::setup_bindings
hook::add client_presence_hook \
[namespace current]::smart_enter_exit_message 69
hook::add room_nickname_changed_hook \
[namespace current]::smart_enter_exit_message 51
}
Как видите, здесь присутствуют все три упомянутых элемента: и хуки, и переменные, и привязка сочетания клавиш к событию.
Сделать: Пояснить, как всё это работает (надо мало-мало разобраться в этом самому).
Какие именно переменные переносить сюда — решать вам. Имеет смысл переносить глобальные (которые объявлены с помощью global и variable). Также можно грузить здесь переменные, находящиеся в процедуре объявления неймспейса, установленные командой set (чтобы потом при выгрузке прибить их командой unset, которую мы увидим ниже):
set verbosity [list min [::msgcat::mc "Minimum:\ Log each entrance or nick change of a unique pair nick/jid only once per room\ (less memory used)."]\ max [::msgcat::mc "Maximum: All entrances of each user will be logged."]] set matching_rules [list \ exact [::msgcat::mc "Exact match."]\ loose [::msgcat::mc "Loose match."]]
В моём плагине нет действий, которые имеет смысл делать во время процедуры загрузки, но такое дело есть в плагине attline, откуда я привожу следующий кусочек кода, идущий после объявления хуков:
foreach chatid [chat::opened] { setup_chat_win $chatid "" }
Теперь о процедуре выгрузки
proc bldjid::unload {} {
variable user_list
global jids_by_chats
variable ent_cntr
event delete <<ToggleMonitor>> <Control-m>
hook::remove generate_completions_hook \
[namespace current]::visitors_compls
hook::remove chat_send_message_hook \
[namespace current]::handle_commands 17
::hook::remove open_chat_post_hook [namespace current]::setup_bindings
hook::remove client_presence_hook \
[namespace current]::smart_enter_exit_message 69
hook::remove room_nickname_changed_hook \
[namespace current]::smart_enter_exit_message 51
catch {unset ent_cntr}
catch {unset user_list}
catch {unset jids_by_chats}
foreach chatid [chat::opened] {
clear_monitor $chatid
set ccw [::chat::winid $chatid]
if {![catch {pack info $ccw.mon} res]} {
pack forget $ccw.mon $ccw.vsb
}
}
}
В её начале, как и в начале любой процедуры, снова объявляются глобальные переменные. Нам их надо потом будет прибить, и если мы их не объявим, они не будут удалены командой unset, хотя и ошибки никакой не выскочит — можете сами в этом убедиться, закомментировав объявление variable ent_cntr и запустив Ткаббер. Убедитесь, что плагин включен, зайдите в какую-нибудь населённую комнату, откройте консоль Ткаббера и наберите в ней
puts $plugins::bldjid::ent_cntr
— вы увидите число участников, увеличенное на единицу. Теперь выгрузите плагин с помощью Менеджера и снова проверьте состояние переменной. Она содержит всё то же число. Если бы она была объявлена, вы получили бы сообщение об ошибке, что этой переменной не существует (что означало бы, что она стёрта из памяти). Я расписываю это так подробно потому, что не так давно в конференции как раз задавались вопросы на тему "зачем объявлять переменные, если мы сейчас их убьём".- Удаление некоторых переменных, возможно, большого выигрыша не даст (кроме, разве что, морального удовлетворения), но, например, переменная jids_by_chats при параноидальных настройках за несколько дней сидения в людных комнатах может изрядно распухнуть.
Затем я поместил удаление привязки горячих клавиш к событию (в данном примере: копируем строчку
event add <<ToggleMonitor>> <Control-m>
из процедуры загрузки и меняем add на delete).Далее идёт удаление хуков. Копируем хуки из процедуры load и заменяем add на remove.
После этого идёт удаление переменных, на всякий случай обёрнутое в catch {}
И в самом конце я делаю последнюю проверку на случай, если в какой-то конференции осталось открытым окошко монитора (его надо закрыть программно, ведь сочетание клавиш мы отключаем, и если где-нибудь останется болтаться окошко монитора, пользователь хороших слов о вас не подумает). Для этой проверки у нас есть цикл foreach, в котором мы сначала принудительно чистим окошки монитора (некоторые могут быть закрытыми, но содержать текст), а затем смотрим, есть ли открытый монитор, и если да — закрываем его. Финальные действия могут быть и сложнее. Например, в игровых плагинах процедура выгрузки закрывает окно с партией игры, если оно было открыто, и прибивает все загруженные в память элементы доски (всю графику) и делает много других подобных вещей.
Естественно, вам лучше знать, что ваш плагин делает и что нет. Соответственно, в зависимости от его сложности могут упроститься или усложниться процедуры загрузки и выгрузки.
Возможна ли полная выгрузка плагина из памяти?
На сегодняшний день выгрузка плагина из памяти ограничена лишь хуками, переменными и событиями, о чём я уже написал. Однако в памяти остаётся ещё немало:
- Настройки плагина (выключите какой-нибудь в Менеджере плагинов и сходите в раздел Plugins — его настройки продолжают болтаться там!
- Код плагина (если разработчик позаботился и запихал всё в свой неймспейс, задача его удаления переводится из разряда невозможных в разряд трудновыполнимых).
- На диске могут остаться временные файлы.
- Прочие ресурсы, созданные кодом во время выполнения (а не определённые статически в тексте плагина).
Из беседы с разработчиками мне удалось узнать следующее (цитирую):
- По-моему, выгрузка есть полное изничтожение плагина, включая
namespace delete ::plugins::pluginname
. - Переменные в настройках регистрируются через
custom::defvar
— Ткаббер же не святым духом про них узнаёт. - Прибить настройки, наверное, можно, если есть
custom::противоположное_defvar
:) - Тебе ничего не мешает убить свой неймспейс (как автору плагина), но грох неймспейса с живыми переменными кастомайза в нём может дать интересный эффект.
- Вообще, плагин должен знать, что он наделал, и уметь отнаделывать это обратно. Это ж стандартно. Например, плагин receipts создаёт несколько картинок (из файлов) и лепит их в окна. При выгрузке логично чтобы он сначала удалял свои картинки из всех чатов, а потом грохал их.
Так что если обозначить планы для дальнейших исследований, их можно свести к четырём рекомендациям:
- Изучить механизм
custom::defvar
и попробовать написатьcustom::противоположное_defvar
. - Если заниматься этим ломает, можно попробовать провести эксперимент: сделать
namespace delete ::plugins::pluginname
при живых переменных кастомайза и посмотреть, что получится; при этом подразумевается, что весь код плагина упрятан в его неймспейс; - Если плагин создавал какие-то свои временные файлы на диске, не забывать удалять их при выгрузке (не путайте это с логами, которые создаются для пользователя! если ему надо будет их убить, он убьёт их сам).
- Не забывать про открытые окна и прочие ресурсы, создаваемые плагином — я уже показал, как делается проверка на предмет забытых окошек монитора; кроме того, вы можете изучить процедуры выгрузки игровых плагинов: chess, reversi и т. д. — если выключить плагин, когда открыта партия, её окно закроется (сначала плагин удаляет из памяти всю свою графику, как об этом уже было сказано выше).
Наверняка я, реализуя выгрузку из памяти своего плагина, что-то не учёл, о чём-то позабыл и что-то сделал неправильно. Поэтому имеет смысл придумать универсальный механизм, позволяющий полностью удалять плагин из памяти, не заботясь о каждой мелочи. Возможно, когда-нибудь он будет реализован, но пока это не сделано, заботиться об этом нужно автору плагина.
Вот, собственно, и всё. Если у меня появятся результаты исследований описанных выше проблем, я непременно их здесь выложу. Призываю сделать то же самое и вас.
Не бойтесь задавать вопросы
Один вопрос специалисту сэкономил мне несколько часов напрасной работы. Камрад kostix подсказал мне простое решение проблемы, как узнать, в какой комнате я админю. А ещё через минуту и процедурку сваял:
proc whoami {xlib where} {
set chatid [chat::chatid $xlib $where]
set jid $where/[get_our_groupchat_nick $chatid]
list $muc::users(role,$xlib,$jid) $muc::users(affiliation,$xlib,$jid)
}
Конечно, задавая вопросы, следует помнить о простых правилах, изложенных в этой статье. На всякий случай напомню основные:
- Попробуйте решить проблему самостоятельно и в вопросе изложите шаги, вами предпринятые. Потенциальный помощник это оценит.
- Спешка нужна лишь при ловле блох. Это ведь не горящий проект, так? Да если и горящий, кому до этого какое дело? У всех свои заботы — кризис на дворе. От этого плагина не зависит судьба человечества? (Если зависит, какого чёрта вы сами столько с ним тянули?) Так что если вам не отвечают сразу, наберитесь терпения.
- В комментариях к коду не забудьте выразить благодарность этому человеку. Если он, помогая вам, написал половину или даже хотя бы треть плагина, можете смело включать его в соавторы.
Общие сведения о репозитории
Для разработки неофициальных проектов, имеющих отношение к Ткабберу (и в том числе — плагинов), был создан специальный репозиторий Subversion:
svn.xmpp.ru/repos/tkabber-3rd-party/
то есть он расположен "рядом" с официальным репозиторием Ткаббера и поддерживает такие же две схемы доступа: http и https.
Основная структура репозитория на данный момент такова:
svn.xmpp.ru/repos/tkabber-3rd-party/
trunk/
plugins/
tags/
branches/
то есть предполагается, что плагины будут складываться в
svn.xmpp.ru/repos/tkabber-3rd-party/trunk/plugins
а их "ветки" делаться где-то под
svn.xmpp.ru/repos/tkabber-3rd-party/branches/
Формат веток, в принципе, не важен. Например, юзер vasya может делать ветки в
svn.xmpp.ru/repos/tkabber-3rd-party/branches/vasya/mycoolplugin/testing-stuff
а может — в
svn.xmpp.ru/repos/tkabber-3rd-party/branches/plugins/mycoolplugin/testing-other-stuff
Можно и весь "куст" под trunk'ом копировать под branches — ветки в Subversion дёшевы и не зависят от формального объёма копируемой части дерева.
Идея состоит в том, чтобы не иметь "побочных" каталогов в каталогах с плагинами. Это позволит любому пользователю выполнить
svn co https://svn.xmpp.ru/repos/tkabber-3rd-party/trunk/plugins .
и получить себе полный набор "готовых к употреблению" плагинов.
Как получить доступ к репозиторию
- Заиметь некий готовый код или хотя бы внятную идею, которые можно будет предъявить в качестве доказательства стремления быть допущеным к репозиторию;
- Связаться с Kostix;
Выбрать себе имя пользователя и пароль. Пароль можно сообщить "открытым текстом" (как есть) или сгенерировать на нём "хэш для htpasswd". Сие делается либо при помощи программы htpasswd из комплекта Web-сервера Apache путём запуска её в виде
htpasswd -n USERNAME
или при помощи этой Web-формы;
Подождать, пока вас не допустят к телу репозитория, о чём Kostix вам сообщит;
Сделать, к примеру,
svn --username myusername mkdir https://svn.xmpp.ru/repos/tkabber-3rd-party/trunk/plugins/myplugin
и убедиться в том, что репозиторий позволил вам сделать изменение в нём, то есть аккаунт создан правильно.
В заключение
Вышенаписанное руководство никоим образом не претендует на полноту изложения и, скорее всего, упускает массу нюансов, а то и важных моментов. Впрочем, цель его не в том, чтобы на примере первого-в-жизни-плагина научить вас программировать, а в том, чтобы как раз на его примере показать, что написание своего плагина по плечу даже новичку.
Удачи вам! И ничего не бойтесь :)