Пришло время поэкспериментировать с более сложным и комплексным пет-проектом на D, желательно еще и полезным. Проблемы с дебаггером, постоянно отваливающийся плагин в vscode делает проект намного более интересным, сильно усложняя поиск багов и отладку. Но частично это можно компенсировать разными приёмами: иерархической архитектурой, которая позволяет комментированием участков кода быстро отсеивать связь бага с разными частями приложения, простейшим std.stdio, агрессивным логированием и средствами языка, например, контрактами (release mode отключает часть проверок, но их можно включить выборочно).
Сам по себе D очень мощный язык, а за счет поддержки объектной парадигмы позволяет организовать нужную архитектуру и относительно легко портировать код с него, так и на него, а это потеря лишь части времени в случае провала проекта. Ну а поскольку меня интересует синхронизация архитектуры и кодовых баз между разными объектными языками, то выбора особого и нет, ибо из системных языков этим требованиям соответствует только лишь Dlang.
Как всегда, статья (как и все в этом блоге) носит лишь исследовательско-экспериментальный характер, не более, а значит может содержать ошибки, упущения или же субъективизм. В первую очередь меня интересует работа программы под Linux, работу под Windows я не рассматриваю и некоторые вещи могут (и будут) различаться в зависимости от операционной системы. Но пару заметок о кроссплатформенности все же оставлю.
После перебора всех интересующих меня задач победителем оказался десктопный RSS-агрегатор: здесь нужен трей и потенциальная интеграция в систему, высокая производительность, нет специфической предметной логики, в библиотеки которой можно упереться.
В дикой природе существует огромное количество rss-агрегаторов на всякий вкус и цвет, посмотрев аналоги и почитав багтрекеры, я решил сместить акцент с просмотра и категоризации новостей на контроль ошибок и процессы парсинга.
Теоретически, прототип можно было бы написать на чём-то более высокоуровневом, например, на JavaFX. Но здесь планку требований к производительности и функционалу задают QuiteRSS, Liferea и аналоги, а у них эти показатели очень высоки. Получить агрегатор, который без браузера потребляет в несколько раз больше памяти чем у ближайших аналогов мне не позволяет самолюбие. Часть проблем из-за привязки программы к jvm можно решить, как и более тесно интегрировать приложение в систему, но "конкуренты" всё равно выглядят более удобными.
Отсюда неплохим кандидатом становится Flutter, учитывая ещё и возможность работы под мобайл. Однако gtkd с автоинжекцией контролов в поля через метапрограммирование ближе по архитектуре к классическим тулкитам, управление состояниями контролов автоматическое, что сильно упрощает и ускоряет разработку. Мутабельность контролов позволяет отделить код от структуры вью и активно использовать GUI-конструктор Glade. Здесь мне нужна очень высокая скорость работы пользователя с интерфейсом, классические контролы в gtk выглядят более подходящими и удобными. Ну и за счёт большой универсальности в Flutter-тулчейне увеличивается количество точек отказа и конфликтов инструментов друг с другом, чем в простых системных биндингах\байндингах. В случае последних, даже если они не заработают, то легко и просто сделать свои или вносить изменения в готовые.
Основной риск, конечно же, будет в расширении функционала программы. Если потребуется подключение сторонних сервисов или специфические библиотеки, то тут могут быть проблемы, ибо прикладные языки выигрывают в этом. С другой стороны, если расширения пойдёт к интеграции в систему, то системный язык не нуждается в большом количестве библиотек за счёт упрощения биндингов и подключения уже существующих.
Несмотря на относительно простой формат лент, есть много частных случаев, мне встречались: пустые даты публикации, появление новостей задним числом, неправильные кодировки, неэкранированные спецсимволы, разметка и тэги в текстовом блоке, отсутствие обязательных тэгов и замена их опциональными.
Выходит, что при большом количестве новостей за ними нужен дополнительный контроль, как за самим процессом обновления, так и за каждой конкретным источником. И тут мне хотелось бы видеть контроль ошибок, перенаправление лога программы в интерфейс и больший контроль за самими лентами: последние даты обновлений, возможность сброса даты и загрузки всех новостей в канале и т.п. Как показала практика, периодические ошибки по таймауту и отсутствие ответа от сервера для некоторых лент можно считать нормой, в статус баре программы хотелось бы видеть проблемные ленты, их количество и список.
Технически, rss-ленты могут иметь индивидуальные периоды обновлений, но я решил потестировать один глобальный таймер, им проще управлять, следить за обновлениями и отложить запуск при необходимости. Кроме того, агрессивная транзакционность SQLite (которую можно отключить, конечно же) может неплохо поднагрузить диск и сброс таймера может быть тут полезен, как и в случае неработоспособности сети и для прочих форс-мажорных случаев. С другой стороны, таймер может не успевать за слишком большой частотой обновления некоторых источников, особенно если в самой ленте старые новости хранятся очень мало и тогда какие-то из них могут потеряться. Ну и добавление в будущем индивидуальных периодов не особенно конфликтует с идеей глобального таймера, как наиболее затратной и долгой операции проверки всех лент сразу.
Наблюдения за QuiteRSS выявили некоторый риск порчи базы данных из-за большого количества обращений к бд при обновлении лент, поэтому я решил пойти иным путём: сначала получить все ленты, а потом все разом помещать их в бд.
Метками, категориями я не пользуюсь, как и встроенным в агрегаторы браузером: основной браузер более безопасен, обвешан плагинами и превратить новость в закладку мне удобнее именно там, хотя браузер легко добавить через тот же webkit2gtk. Импорт и экспорт лент можно сделать через формат OPML или аналогичный.
Усталость от повсеместного распространения бесцветных material-интерфейсов даёт о себе знать (старею) и захотелось ламповых и цветных кнопочек и контролов, в них были использованы gtk-эмодзи. Прототип получился таким:
Не слишком хорошая идея начинать пункты меню с одинаковых букв (Источники, Инструменты), что усложняет навигацию сочетаниями клавиш, но пункты меню пока не стабилизировались. Поскольку я не использовал css, то кое-где можно наблюдать двойные границы.
Что касается Windows, то LDC может сделать кросс-компиляцию на Linux-машине и exe-файл можно даже потестить под Wine. Для запуска под Windows (как и в Wine) нужны библиотеки gtk, которые можно получить несколькими способами: с официального сайта gtk через msys, с сайта самого gtkd или же сторонним инсталлятором, например, от tschoonj с гитхаба.
На первый взгляд, самый простой способ - это сайт gtkd. При установке он копирует в Program Files директорию Gtk-Runtime, добавляя её в PATH. В bin и будут основные библиотеки (кроме плагинов загрузки форматов изображений, которые в lib). Соответственно, эти директории можно поместить и в директорию с программой, сделав её относительно портабельной и выставлять переменные среды через скрипт запуска. Но под Wine возникает распространенная ошибка неверного формата изображений со стороны Pixbuf, плагины лоадеров на месте, на GDK_PIXBUF_MODULEDIR и GDK_PIXBUF_MODULE_FILE нет никакой реакции. Это ошибка исчезает где-то между 3.24.0 и 3.24.10 версиями инсталлятора от tschoonj.
Также в инсталляторе gtkd не все библиотеки. Если подключить gtkd как зависимость и не посмотреть на разделение на субпакеты, то к проекту подключается не только ядро тулкита gtk-d:gtkd, но и gtk-d:gstreamer, gtk-d:vte, gtk-d:peas и прочие. В gtkd инсталляторе есть libpeas*.dll, но потребуется libgirepository, которой там нет. Определенные проблемы могут быть с vte, эти библиотеки нужно доставать через msys, cygwin или еще как-то и далеко не факт, что это все сразу заработает. Т.е. инсталляторы между собой сильно различаются.
Без установки gtk-окружения в систему нужно выставить переменные среды, чтобы gtk начал подцеплять дефолтные ресурсы. Например, в текстовом поле Entry есть иконка, без XDG_DATA_DIRS на Gtk-Runtime\share там её не будет, не будет локализации меню и т.п. Так оно, теоретически, работает более-менее портабельно под Windows. Но все равно отличия будут, например, симпатичные цветные эмодзи будут заменены на черно-белые. Пишут, что это баг со стороны Cairo, возможно, его уже починили, а может быть и проблема с отсутствующими шрифтами вроде Noto Color Emoji.
Архитектурно, в программе за каждый сложный участок окна программы отвечает свой отдельный контроллер, который обменивается с остальными через делегаты-коллбэки, отдельные контроллеры на трей и на таймер обновления новостей. Управляет всем главный контроллер, без которого проблематично сделать lazy-загрузку программы без окна или же вообще убрать из неё gtk. Последнее может потребоваться для превращения программы из десктопной в серверную или же разделение на части клиент-сервер. Для некоторых новостных источников длительный пропуск обновлений может приводить к потере новостей и серверный вариант тут удобнее, с другой стороны, таких источников может быть не слишком много и усложнение себя не оправдает.
У этой иерархии контроллеров есть как и плюсы, так и минусы, скорее самый большой минус - усложнение взаимодействия между контроллерами и потенциальное размазывание логики: реализовать все на коллбэках-делегатах в общем случае очень сложно и бессмысленно, часть логики в любом случае будет в контроллерах, часть - в большом количестве делегирующих вызовов того же главного контроллера и между ними может начинаться путаница и конкуренция за ответственность. Но в условиях без полноценной IDE проблематично постепенно расслаивать контроллеры, а рано или поздно контроллер окна станет монструозным и неудобным в обращении, так что я сразу выбрал его разделение на части.
Описание интерфейса в xml через Glade. Здесь можно сблизить архитектуру с xml-тулкитами, автоматически заполняя поля и даже эмулировать вложенные контроллеры, связывая контроллер с xml файлом и загружая его содержимое в родительский виджет. Для обхода полей потребуется метапрограммирование, что возможно через allMembers\derivedMembers, поместив вызов через трейт в миксин. Без миксина приватных полей там может не быть.
Аналогичное метапрограммирование с помощью mixin template можно использовать для всяких полезных вспомогательных аннотаций, например, для генерации метода toString и прочих.
Сразу бросается в глаза возможное место изменений - наличие сайтов\сервисов, которые RSS напрямую не поддерживают. Кроме того, некоторые из них могут специально отключать поддержку новостной ленты для исключения парсинга через неё, стимуляции захода на сайт или каких-то своих целей. И это потенциальная точка расширения: если завязать код программы только лишь на rss, то при добавлении новых источников будут определенные проблемы, особенно на фоне отсутствия нормального рефакторинга. Отсюда есть смысл выбирать более универсальные понятия, например, "новостной источник", а не rss-канал. Хотя, на мой взгляд, такими ресурсами проще пожертвовать и найти замену, чем связываться с апи сервисов\соцсетей.
В самом D есть несколько поправок на архитект, которые нужно учитывать. При обращении к null и отключенной генерации дампа памяти будет молчаливый сегфолт. Судя по всему, такой подход был осознанным. После серии экспериментов более-менее сносно начал работать перехват сегфолта через хандлер на SIGSEGV и core.sys.linux.execinfo, сниппет был найден в репозитории DRuntime, опять-таки это не слишком кроссплатформенное решение. Немного позже в druntime был обнаружен модуль etc.linux.memoryerror с функцией registerMemoryErrorHandler. Есть, конечно же, std.typecons.Nullable, но за всем не уследишь. С другой стороны, в D нельзя получить проблемы с null на тех же строках, хотя это и может затирать баги, но программу не положит.
Здесь есть небольшой частный случай: я динамически загружал .so библиотеку, чтобы не линоваться с ней и позволить программе работать с отсутствующей либой, пытаясь этим немного повысить отказоустойчивость. Загрузка была через SharedLibLoader в DerelictUtil (на всякий случай напомню, что оно там уже легаси и повсеместно заменяется на BindBC), тоже самое можно получить через dlopen напрямую. И для хандлера выше, если ошибка произойдет после загрузки либы, то стектрейс может потеряться, уйдя в библиотеку.
Еще одна проблема - это переопределение параметризованных шаблонных методов в потомках, шаблонные методы не виртуальны и с ними могут быть проблемы. В общем случае, большая часть кода мапперов, конфига и прочего строится на этом и нужно выкручиваться как-то через std.variant или же делать сужающие касты, что такое себе занятие.
Сам D, как системный язык, не полностью объектный, конечно же, что влияет и на библиотеки: есть повышенный шанс, что рандомно взятая библиотека будет скорее всего на структурах и если в процессе интеграции на поздних этапах возникнет проблема, то будет очень больно её менять или пытаться настраивать. Поэтому для логики работы с бд я решил подстраховаться и пойти более трудным путем через встроенные D-биндинги etc.c.sqlite3 без сторонней либы, ибо работу с бд хотя и можно частично изолировать от остального кода, но какие-то там переделки или замены будут очень и очень болезненны. Маппер, конечно же, пришлось писать самому тоже.
Для борьбы с проблемами кодировок в D есть несколько вариантов перекодирования: через std.encoding.transcode или же в arsd есть characterencodings. В std.encoding есть и другие полезные функции, например, sanitize, которая может предотвратить попадание в парсер невалидных символов. В экспериментах оказалось затратна по памяти (хотя не факт, что она виновата), но это очень хорошая подстраховка.
Для парсинга дат в RSS есть из коробки parseRFC822DateTime в std.datetime.systime, у Atom формат RFC 3339, производный от ISO 8601 и, теоретически, должны сносно работать методы для парсинга последнего, тот же fromISOExtString. Для самого парсинга я не использовал сторонних библиотек, с ними возникли определенные проблемы. С другой стороны, хотя форматы лент между собой и различаются (RSS, Atom), но по структуре достаточно просты. Но такие библиотеки вроде как есть и еще субпакет rss в arsd.
Можно обработать xml ленты вручную, но std.xml считается устаревшим. Если последовать советам и использовать dxml, то не следует забывать проверку типа узла через EntityType перед получением из узла какой-либо информации, атрибутов, текста или же дочерних узлов. В некоторых случаях будут ошибки, а также парсер склонен молчаливо зависать, например, при получении текста из нетекстового узла со сложной иерархией вложенных дочерних узлов, так что нужно внимательно проверять нестандартные случаи, а иначе парсинг получается очень хрупким. В старых версиях dxml есть конфликт имен dxml.util.normalize с std.uni.normalize, гугл может запросто подсунуть старый вариант документации и при наличии импорта std.uni может быть конфуз: компилятор пропустит код, но normalize будет работать совсем не та, которая теперь decodeXML.
К самому gtkd\gtk-апи есть определенные вопросы. Например, интуитивно можно ожидать, что сигнал из TreeView на изменение выделения строк в TreeSelection.addOnChanged будет работать только в случае изменения выделения. Но не тут-то было: "Please note that this signal is mostly a hint. It may only be emitted once when a range of rows are selected, and it may occasionally be emitted when nothing has happened.". В апи TreeModelIF.addOnRowInserted: "Note that the row may still be empty at this point, since it is a common pattern to first insert an empty row, and then fill it with the desired values.". Это, конечно же, очень познавательно просветиться насчет хороших паттернов, но выглядит крайне странно, приводя к огромному количеству костылей и хаков. Еще что-то может просто не работать или сегфолтить, просто так или же из-за какого-нибудь установленного свойства и об этом нужно догадаться или же найти самому.
Есть небольшой нюанс в сортировке новостей. Многие агрегаторы позволяют вручную сортировать дерево RSS-лент в любом порядке. Это можно реализовать через отдельный столбец с индексом в бд, но программа должна делать переиндексацию в случае добавления новостей, как и удаление отдельной новости или же директории с ними, поддерживая корректность индекса сортировки. Это несколько осложняет работу с БД напрямую, в обход интерфейса программы, поэтому я решил попробовать сортировку по имени, как некий средний вариант, которым можно управлять. В TreeStore сортировка по столбцу отключает перетаскивание строк мышкой, но её можно включать и выключать в процессе перетаскивания. Но при сортировке по столбцу имени лент появляются частные случаи: перетаскивание уже сортированных детей в узле дерева, что по логике вещей не должно допускаться и активация перетаскивания не должна происходить, смена родителя, где после завершения события перетаскивания происходит автоматическая сортировка и узел может сместиться, прыгая на другую строку и занимая свое место среди остальных. Но при этом сортировка управляется целиком самим контролом, освобождая программу от лишней работы.
Огромной проблемой, как ни странно, оказался трей. GtkStatusIcon помечен устаревшим и из более поздних версий gtk просто выкинут на мороз без предложения какой-либо альтернативы, все же уведомления и трей выполняют разные задачи и одно другим полноценно не заменяется. Наиболее простой вариант - вызвать через extern библиотеку libappindicator, но она крайне упрощена и многих вещей не поддерживает или делает всё не то и не так. Еще спамит Gdk-CRITICAL и открывает контекстное меню в трее как по ПКМ, так и по ЛКМ, не делая особенных различий. После 100500 экспериментов это частично удалось обойти костылями: вытащить Window как родителя меню трея, управлять его прозрачностью и сразу скрывать после появления, чтобы по левой кнопке открывалась программа, а по правой - само меню. Конечно же, нужно подавлять другие слушатели, чтобы не было конфликтов между ними, в общем - костыли жуткие, почему трей настолько многострадальный... это тот еще вопрос.
Более правильным решением скорее всего будет xapp, но в разных дистрибутивах могут быть устаревшие версии, которые не поддерживают XAppStatusIcon или же он куда-то засунут, пока решил остановится на libappindicator. Но помимо трея, xapp имеет разные удобные функции, например, xapp_set_window_progress, куда можно передавать значения прогресса задачи (например, обновления rss-лент) от 1 до 100, что будет отображаться полоской прогресса в заголовке окна, по крайней мере в Cinnamon, что достаточно удобно.
В модуле gio есть апи для работы с D-Bus, что позволяет перехватывать события, например, от NetworkManager (подобный подход описан на хабре) и переводить программу в оффлайн-режим, чтобы не получать множественные ошибки обновления лент из-за отсутствия сети.
Что касается встроенного браузера, то биндинги к WebKitGTK есть, но мне не хотелось бы видеть браузер в читалке новостей, все-таки за обновлениями безопасности основных системных браузеров я слежу, а как там обновляются библиотеки с WebKit отследить уже проблематичнее. Но так как в новостях встречается разметка, то есть еще один путь - удалять теги. Есть несколько библиотек, но более-менее поддерживаемый htmltotext из arsd оставляет ссылки, пытается перевести некоторые тэги в markdown и особых настроек не имеет. Более чистый результат получился из Go-библиотеки bluemonday, простенькую обертку для нее с cgo можно вызвать через extern из D. Эта связка с Go выглядит особенно полезной, но пока ничего не могу сказать об утечках памяти в попытке подружить два языка с GC. В любом случае, браузер можно легко добавить.
Также изначально подумывал о Lua-плагинах, например, на них можно реализовать "фильтр" (на самом деле хоть этот функционал и называется фильтром в некоторых агрегаторах, но он ничего не фильтрует, а выполняет операции над новостью в зависимости от условий), который помечает новости важными или нет. Но его можно сделать и без плагинов, ибо с ними программа получается слишком системозависимой, возможно, позже я найду им более удобное применение. Lua можно интегрировать через bindbc-lua или аналогичные библиотеки.
В целом, gtkd мне понравился, если не считать некоторых неудобств со стороны самого gtk, но эксперимент можно признать успешным. В сравнении с нативным gtk может повышаться шанс утечек из-за неправильного использования апи и добавления ещё одного слоя абстракции.
С другой стороны, написать программу можно на чём угодно, основные проблемы будут с последующим сопровождением, особенно когда часть нюансов работы кода забывается. Ну а так как D приближает архитектуру к классическим тулкитам, то это сильно облегчает как разработку, навроде быстрой навигации и изучения объектного api, так и последующее сопровождение и быстрый поиск и фикс багов. В условиях одиночной разработки и пет-проекта это выглядит наиболее критичными требованиями. Как говорится, каждой задаче - свой инструмент.