<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Блог компании ТомскСофт]]></title><description><![CDATA[Мысли, идеи и истории.]]></description><link>https://blog.tomsksoft.ru/</link><image><url>https://blog.tomsksoft.ru/favicon.png</url><title>Блог компании ТомскСофт</title><link>https://blog.tomsksoft.ru/</link></image><generator>Ghost 2.23</generator><lastBuildDate>Sun, 05 Jan 2025 10:27:53 GMT</lastBuildDate><atom:link href="https://blog.tomsksoft.ru/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[libvideo - библиотека кодирования и декодирования видео для Windows и Android]]></title><description><![CDATA[<p><strong><a href="https://github.com/tomsksoft/libvideo">libvideo on GitHub</a></strong></p><h2 id="-">Введение</h2><p>В современном мире человечество окружает огромное количество различного цифрового контента, основной частью этого контента является видео.</p><p>Видео используется в стриминговых сервисах, видеохостингах, хостингах видео связи и т.д. На данный момент пропускные способности современных сетей не позволяют передавать видео в исходном разрешении, поэтому необходимо использовать специальные</p>]]></description><link>https://blog.tomsksoft.ru/libvideo/</link><guid isPermaLink="false">63e1434671488d6f5e98aaaf</guid><category><![CDATA[android]]></category><category><![CDATA[development]]></category><category><![CDATA[crossplatform]]></category><category><![CDATA[C++]]></category><category><![CDATA[Desktop]]></category><dc:creator><![CDATA[Vasily Dmitriev]]></dc:creator><pubDate>Wed, 08 Feb 2023 03:04:10 GMT</pubDate><content:encoded><![CDATA[<p><strong><a href="https://github.com/tomsksoft/libvideo">libvideo on GitHub</a></strong></p><h2 id="-">Введение</h2><p>В современном мире человечество окружает огромное количество различного цифрового контента, основной частью этого контента является видео.</p><p>Видео используется в стриминговых сервисах, видеохостингах, хостингах видео связи и т.д. На данный момент пропускные способности современных сетей не позволяют передавать видео в исходном разрешении, поэтому необходимо использовать специальные технологии для сжатия видео и передачи их по сети, затем нужно разархивировать сжатые видео данные для отображения их на устройстве пользователя.</p><p>Существует множество различных программных продуктов, предоставляющие подобные возможности, однако их ключевым элементом является кодек, кодирующий и декодирующий видео поток.</p><p>В статье будет приведено описание написанной кроссплатформенной (Windows/Android) библиотеки кодирования и декодирования видео с возможностью масштабировать, кадрировать кадры.</p><h2 id="--1">Стек</h2><ul><li>языки программирования: C++ 17, Java11.</li><li>системы сборки: CMake, Gradle.</li><li>пакетный менеджер: Conan.</li><li>библиотека unit-тестирования: <a href="https://github.com/google/googletest">Google C++ Testing Framework</a> (gtest).</li></ul><h2 id="--2">Предметная область</h2><p>Как было описано выше видео с исходным разрешением невозможно передавать по сети из-за большого веса передаваемых данных. Поэтому на стороне отправителя видео специальным образом подготавливается с помощью специальных алгоритмов перед отправкой, т.е. кодируется. На стороне пользователя для того чтобы воспроизвести видео оно декодируется. За кодирование/декодирование отвечает кодек (co/dec) - специальное аппаратное устройство или программа.</p><h3 id="-h-264">Кодек H.264</h3><p>Внутри библиотека построена c использованием кодека H.264 реализованным с помощью <a href="https://www.videolan.org/developers/x264.html">libx264</a>. </p><p>Кодек H.264 является очень известным, поэтому не будем останавливаться на его описании, лишь уточним, что используется именно он, потому что его использование (в т.ч. libx264) является бесплатным и потому что H.264 один из самых распространненых кодеков.</p><h2 id="--3">Описание работы</h2><h3 id="windows">Windows</h3><p>На Windows реализовано:</p><ul><li>Создание программного кодера, аппаратного кодера, программного декодера и аппаратного декодера</li><li>Вывод кодированных данных в заданный пользователем callback, вывод декодированного кадра в заданный пользователем callback, вывод декодированного кадра на Windows окно</li><li>Фильтры масштабирования и кадрирования кадра</li><li>Логирование (encoded size, resolution, framerate и т.д.) в консоль и на Windows окно при программном декодировании</li></ul><p>Общий алгоритм работы с библиотекой:</p><p>1) Создание кодера и декодера. На этом этапе возможно воспользоваться встроенным методом для поиска существующих на системе кодеров и декодеров, если в системе нет аппаратных кодеров или декодеров, будут использоваться программные.<br>2) Добавление callback-а кодеру или декодеру для вывода данных.<br>3) Добавление Windows окон декодеру для вывода кадров.<br>4) Добавление фильтров масштабирования и кадрирования к кодеру и декодеру.<br>5) Включение/выключение логирования.<br>6) Кодирование и декодирование.</p><p>Описанный выше алгоритм является общим и не все его части необходимы для работы. Например, добавление фильтров необязательно, создавать кодер и декодер в "одном месте" необязательно и т.д.</p><p>Для создания кодера необходимо задать параметры width, height, bit_rate, framerate и H.264 preset. Для создания декодера необходимо задать параметры width и height.</p><h3 id="android">Android</h3><p>На Android реализовано:</p><ul><li>Создание программного кодера, программного декодера и аппаратного декодера</li><li>Вывод кодированных данных в Java byte array</li><li>Вывод декодированного кадра на Android Surface при аппаратном декодировании</li><li>Логирование (encoded size, resolution, framerate и т.д.) в Android Logcat</li></ul><p>Общий алгоритм работы с библиотекой:</p><p>1) Создание кодера и декодера. На этом этапе возможно создать аппаратный декодер, в него нужно передать ссылку на Android Surface.<br>2) Включение/выключение логирования.<br>3) Кодирование и декодирование.</p><p>Для создания кодера необходимо задать параметры width, height, bit_rate, framerate и H.264 preset. Для создания декодера необходимо задать параметры width, height и extradata. Параметр extradata является опциональным, по сути представляет собой SPS и PPS данные, которые необходимые для запуска аппаратного декодера, но если пользователь не задал extradata, декодер попытается получить эти данные из входящего потока от кодера.</p><h2 id="--4">Тестирование</h2><h3 id="windows-1">Windows</h3><p>Для Windows написано тестовое приложение, которое реализует работу со всеми основными функциями библиотеки.</p><p>При запуске тестового приложения с программным кодированием и декодированием с разрешением 1280x720 и фильтром масштабирования на 0.5x выводное Windows окно выглядит так, как представлено на рис. 1.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2023/02/--------------2023-02-07-171934.png" class="kg-image"><figcaption>Рис. 1 - Декодированный кадр 1280x720 с фильтром масштабирования 0.5x</figcaption></figure><p>При этом в консоли будет следующий вывод, представленный на рис. 2.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2023/02/console.png" class="kg-image"><figcaption>Рис. 2 - Логированная информация о кодированном и декодированном кадре</figcaption></figure><p>При использовании аппаратного кодирования и декодирования производительность повышается. Например, кодирование с помощью AMD AMF сжимало размер <a href="https://en.wikipedia.org/wiki/SMPTE_color_bars">SMPTE</a> кадра с разрешением 320x240 и изначальным размером 307200 байт до размера 17 байт при повторной отправке того же кадра. Программный кодер libx264 в таких условиях сжимал кодированные данные кадра до 750 байт.</p><h3 id="android-1">Android</h3><p>Для Android написано два тестовых приложения использующих камеру телефона, как входной поток для кодирования. В первом используется программное кодирование и декодирование, во втором программное кодирование и аппаратное декодирование.</p><p>На рис. 3 представлена работа приложения с программным декодированием. На нем отображается сверху поток с камеры, снизу декодированное изображение.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2023/02/android1-1.png" class="kg-image"><figcaption>Рис. 3 - Android тестовое приложение с программным кодированием декодированием</figcaption></figure><p>Второе приложение имеет одно окно, созданное как Android Surface, на которое выводит аппаратно декодированный кадр. На рис. 4 представлен результат работы тестового приложения.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2023/02/android2.png" class="kg-image"><figcaption>Рис. 4 – Android тестовое приложение с аппаратным декодированием</figcaption></figure><h3 id="unit-">Unit-тесты</h3><p>Для unit-тестирования используется gtest.</p><p>Создано 9 unit-тестов проверяющие корректность работы кодирования, декодирования и фильтров.</p><p>На рис. 5 представлено сообщение, выведенное gtest о успешном завершении всех тестов.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2023/02/tests.png" class="kg-image"><figcaption>Рис. 5 - Успешное завершение всех тестов</figcaption></figure><h2 id="--5">Вывод</h2><p>В статье описана разработанная библиотека под Windows и Android, позволяющая кодировать, декодировать, масштабировать, кадрировать кадры.</p><p>Для библиотеки созданы тестовые приложения под Windows и Android. Созданы 9 unit-тестов.</p><p>Для удобной сборки библиотеки написан скрипт, подробнее в <a href="https://github.com/tomsksoft/libvideo/blob/main/README.md">README</a> библиотеки на GitHub.</p><h2 id="--6">Лицензия</h2><p>Файл <a href="https://github.com/tomsksoft/libvideo/blob/main/LICENSE">LICENSE на GitHub</a></p><p>Используемые библиотеки</p><ul><li><a href="https://www.videolan.org/developers/x264.html">libx264</a></li><li><a href="http://ffmpeg.org/">FFmpeg</a></li><li><a href="https://github.com/google/googletest">Google C++ Testing Framework</a></li></ul>]]></content:encoded></item><item><title><![CDATA[Обмен данными приложения и системного расширения-камеры (camera extension) на macOS]]></title><description><![CDATA[macos camera extension, sink stream, custom properties]]></description><link>https://blog.tomsksoft.ru/exchange-data-app-camera-extension-macos/</link><guid isPermaLink="false">63a18d8471488d6f5e98aa93</guid><dc:creator><![CDATA[Egor O. Ivanov]]></dc:creator><pubDate>Wed, 21 Dec 2022 03:34:33 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2022/12/1584613489_698d51a19d8a121ce581499d7b701668-4.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.tomsksoft.ru/content/images/2022/12/1584613489_698d51a19d8a121ce581499d7b701668-4.png" alt="Обмен данными приложения и системного расширения-камеры (camera extension) на macOS"><p><strong>Введение</strong></p><p>Начиная с macOS 12.3 Apple наконец-то предоставляет новую возможность для создания  виртуальной веб-камеры - с помощью Camera Extension. Это такой новый тип системных расширений (System Extensions) - механизма, появившегося в macOS 10.15 (Catalina). Системные расширения призваны заменить расширения ядра (Kernel Extensions, KEXTs), выполняя те же самые функции по расширению возможностей macOS на низком уровне, но при этом работая в пространстве пользователя (user space). Camera Extensions также были созданы для замены старого механизма создания виртуальных камер - через плагины CoreMedia I/O DAL Plug-ins. Одним из главных недостатков этих плагинов является то, что они, как и любой плагин, должны загружаться в каждый процесс (клиентского приложения), который хочет использовать камеру, представляемую этим плагином. С этой загрузкой в последнее время начали возникать проблемы из-за различных мер по обеспечению безопасности, которые Apple стала планомерно внедрять в свою ОС. Но даже когда ограничения позволяют загрузить сторонний плагин в процесс клиентского приложения, если в этом приложении включён sandboxing, то плагин становится полностью изолированным и не может получать исходные данные для камеры откуда бы ни было. Именно эти недостатки и призван преодолеть Camera Extension, который запускается системой в своём собственном процессе, а его взаимодействие и отправку видеоданных клиентским приложениям ОС также берёт на себя.</p><p>В 32-минутном <a href="https://developer.apple.com/videos/play/wwdc2022/10022/">ролике</a> сотрудник Apple лихо демонстрирует, как создать такое расширение, а также приложение, которое передает в него некие данные. Само расширение создается из шаблона, нужно подправить пару деталей и вуаля - все готово. Конечно же, на самом деле все гораздо сложней. Кроме того, что опущены различные детали безопасности, без которых ничего не заработает, сам механизм передачи данных рассмотрен настолько бегло, что можно говорить о том, что он практически не документирован.</p><p>Итак, у нас есть рабочий прототип системного расширения с камерой, осталось приделать к нему передачу видео потока и управляющих сигналов из нашего приложения. Естественным решением выглядит использование XPC - механизма <a href="https://developer.apple.com/documentation/xpc?language=objc">межпроцессной коммуникации</a>. Все указывает на то, что по аналогии с другой технологией виртуальной камеры (DAL) нужно соединяться через XPC с сервисом Mach Service, в роли которого выступает наш Camera Extension. Для создания такого XPC-соединения в Objective-C классе NSXPCConnection есть метод initWithMachServiceName:. Но выяснилось, что установить такое XPC-соединение невозможно, по крайней мере, обычным способом, без использования хаков. Скорее всего, это связано с тем, что процесс Camera Extension запускается под другим юзером (_cmiodalassistants) и поэтому не виден из процесса нашего приложения. Во всяком случае, при попытке установить соединение (вызов метода remoteObjectProxyWithErrorHandler: у объекта NSXPCConnection) получаем ошибку с кодом 4099 из NSCocoaErrorDomain ("Couldn’t communicate with a helper application."), с описанием:                                                                                       </p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">{
    <span class="hljs-built_in">NSDebugDescription</span> = <span class="hljs-string" style="color: rgb(106, 135, 89);">"The connection to service named<br>        XXXXXXXXX.com.company.appName.cameraExtension was invalidated: failed at <br>        lookup with error 3 - No such process."</span>;
}</pre><p>Как выясняется, XPC-соединения - не тот способ, которыми можно обмениваться данными между приложением и Camera Extension. Именно такие рекомендации дают на девелоперском форуме Apple:<a href="https://developer.apple.com/forums/thread/706184?answerId=723807022#723807022"> https://developer.apple.com/forums/thread/706184?answerId=723807022#723807022</a>. Вместо них для передачи кадров в Camera Extension предлагают использовать sink stream, т.е. объявить в виртуальной камере возможность ввода данных в неё. Давайте реализуем этот вариант.</p><p><strong>Входной поток (Sink Stream)</strong></p><p>Отличие создания объекта sink stream от source stream это указание направления этого потока CMIOExtensionStreamDirectionSink. В расширении:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">stream = [[CMIOExtensionStream alloc] 
    initWithLocalizedName:localizedName 
    streamID:streamID 
    direction:CMIOExtensionStreamDirectionSink
    clockType:CMIOExtensionStreamClockTypeHostTime
    source:<span class="hljs-keyword" style="color: rgb(204, 120, 50);">self</span>];</pre><p>Затем созданный экземпляр объекта добавляется как второй stream в объект CMIOExtensionDevice по аналогии с Source Stream</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">[device addStream:stream error:&amp;error]</pre><p>Теперь давайте разберемся, как подключиться к расширению из приложения.</p><p>Для того, чтобы подключиться к запущенному расширению для передачи стрима и чтения/записи его параметров необходимо:</p><p>     1. Получить список девайсов и найти нужный нам (например по имени device.localizedName):</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">auto discoverySession = [<span class="hljs-built_in">AVCaptureDeviceDiscoverySession</span>
    discoverySessionWithDeviceTypes: @[<span class="hljs-built_in">AVCaptureDeviceTypeExternalUnknown</span>]
    mediaType: <span class="hljs-built_in">AVMediaTypeVideo</span>
    position: <span class="hljs-built_in">AVCaptureDevicePositionUnspecified</span>];
devices = discoverySession.devices;</pre><p>     2. Получить значение CMIOObjectID по свойству uniqueId класса AVCaptureDevice, для этого получаем список девайсов, но уже используя другое CMIO API:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;"><span class="hljs-built_in">UInt32</span> dataSize = <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>;
<span class="hljs-built_in">UInt32</span> dataUsed = <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>;
CMIOObjectPropertyAddress opa = {
    CMIOObjectPropertySelector(kCMIOHardwarePropertyDevices),
    CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal),
    CMIOObjectPropertyElement(kCMIOObjectPropertyElementMain)
};

CMIOObjectGetPropertyDataSize(CMIOObjectPropertySelector(kCMIOObjectSystemObject),
    &amp;opa, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, &amp;dataSize);

auto devicesCount = <span class="hljs-keyword" style="color: rgb(204, 120, 50);">int</span>(dataSize) / <span class="hljs-keyword" style="color: rgb(204, 120, 50);">sizeof</span>(CMIOObjectID);
std::vector&lt;CMIOObjectID&gt; devices(devicesCount);

CMIOObjectGetPropertyData(CMIOObjectPropertySelector(kCMIOObjectSystemObject),
    &amp;opa, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, dataSize, &amp;dataUsed, devices.data());</pre><p>     3. Найти среди всех девайсов устройство с UID равным uniqueId, из найденного AVCaptureDevice через свойство kCMIODevicePropertyDeviceUID:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">opa.mSelector = CMIOObjectPropertySelector(kCMIODevicePropertyDeviceUID);
CMIOObjectGetPropertyDataSize(devices[deviceObjectIndex],
    &amp;opa, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, &amp;dataSize);

<span class="hljs-built_in">CFStringRef</span> cfUID = <span class="hljs-literal" style="color: rgb(104, 151, 187);">NULL</span>;
CMIOObjectGetPropertyData(devices[deviceObjectIndex],
    &amp;opa, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, dataSize, &amp;dataUsed, &amp;cfUID);</pre><p>    4. В результате мы имеем значение CMIOObjectID устройства, которое поможет получить доступ к списку его потоков. Для получения списка потоков также необходимо воспользоваться CMIO API по этому device id:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;"><span class="hljs-built_in">UInt32</span> dataSize = <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>;
<span class="hljs-built_in">UInt32</span> dataUsed = <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>;
CMIOObjectPropertyAddress opa = {
    CMIOObjectPropertySelector(kCMIODevicePropertyStreams),
    CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal),
    CMIOObjectPropertyElement(kCMIOObjectPropertyElementMain)
};

CMIOObjectGetPropertyDataSize(deviceId, &amp;opa, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, &amp;dataSize);

<span class="hljs-keyword" style="color: rgb(204, 120, 50);">int</span> streamsCount = <span class="hljs-keyword" style="color: rgb(204, 120, 50);">int</span>(dataSize) / <span class="hljs-keyword" style="color: rgb(204, 120, 50);">sizeof</span>(CMIOStreamID);
std::vector&lt;CMIOStreamID&gt; streamIds(streamsCount);

CMIOObjectGetPropertyData(deviceId, &amp;opa, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, dataSize, 
    &amp;dataUsed, streamIds.data());</pre><p>   В случае успеха этих операций у нас будет список потоков, зарегистрированных в расширении. Мы выбираем поток, работающий в направлении CMIOExtensionStreamDirectionSink - он был добавлен вторым. Значение CMIOStreamID как раз таки и характеризует этот поток. Далее необходимо просто начать поток следующим методом:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">OSStatus CMIODeviceStartStream(CMIODeviceID  deviceID, CMIOStreamID streamID)</pre><p>Однако простого создания потока будет мало, так как мы хотим отправлять в него видео, мы должны создать объект CMSimpleQueue методом CMSimpleQueueCreate, а также "привязать" эту очередь к самому потоку методом CMIOStreamCopyBufferQueue</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">CMSimpleQueueCreate(kCFAllocatorDefault, <span class="hljs-number" style="color: rgb(104, 151, 187);">5</span>, &amp;sinkQueue);

CMIODeviceStreamQueueAlteredProc handler 
    = [](CMIOStreamID streamID, <span class="hljs-keyword" style="color: rgb(204, 120, 50);">void</span>* token, <span class="hljs-keyword" style="color: rgb(204, 120, 50);">void</span>* refCon) {
            auto <span class="hljs-keyword" style="color: rgb(204, 120, 50);">self</span> = static_cast&lt;TSClassName*&gt;(refCon);
            <span class="hljs-keyword" style="color: rgb(204, 120, 50);">self</span>-&gt;_readyToEnqueue = <span class="hljs-literal" style="color: rgb(104, 151, 187);">true</span>;
    };

CMIOStreamCopyBufferQueue(sinkStream, handler, <span class="hljs-keyword" style="color: rgb(204, 120, 50);">this</span>, &amp;sinkQueue);</pre><p>Где sinkQueue определена как поле класса типа CMSimpleQueueRef. В дальнейшем это поле используется для отправки изображения в этот поток методом CMSimpleQueueEnqueue.</p><p><strong>Пользовательские свойства (Custom Properties)</strong></p><p>Другой способ передачи данных - custom properties. Мы использовали эти свойства для передачи управляющей информации, то есть небольших (относительно видео) объемов данных. Использование свойств для передачи видео-потока представляется возможным, но не исследовалось на предмет производительности.</p><p>Добавить свойство можно в любой из объектов, CMIOExtensionDeviceSource или CMIOExtensionStreamSource, но давайте сделаем это в устройство. Просто добавим несколько новых строк в нужном формате в уже существующий метод availableProperties.</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;"><span class="hljs-class"><span class="hljs-keyword" style="color: rgb(204, 120, 50);">@implementation</span> <span class="hljs-title" style="color: rgb(255, 198, 109);">DeviceSource</span></span>

- (<span class="hljs-built_in">NSSet</span>&lt;CMIOExtensionProperty&gt; *)availableProperties
{
    <span class="hljs-keyword" style="color: rgb(204, 120, 50);">return</span> [<span class="hljs-built_in">NSSet</span> setWithObjects:
        	CMIOExtensionPropertyDeviceTransportType,
        	CMIOExtensionPropertyDeviceModel,
        	<span class="hljs-string" style="color: rgb(106, 135, 89);">@"4cc_clie_glob_0000"</span>, <span class="hljs-comment" style="color: grey;">// кастомное свойство, положите в константу</span>
        	<span class="hljs-string" style="color: rgb(106, 135, 89);">@"4cc_reso_glob_0000"</span>, <span class="hljs-comment" style="color: grey;">// кастомное свойство</span>
        	<span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>];
}</pre><p>Мы добавили 2 новых свойства, clie - clients (список клиентов), reso - resolution. Имя свойства задается в формате: 4cc_selector_scope_element. Где “4cc” константа, затем 4-хзначный селектор (4сс == 4 character code), затем scope и element, все разделены подчеркиванием. В наших примерах мы всегда будем использовать scope = global, а element 0000, что значит main, но можно использовать любое число. Таким образом, мы будем задавать разные свойства, меняя только 4х-символьный селектор.</p><p>Добавленные таким образом свойства будут доступны из приложения посредством такой структуры:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">CMIOObjectPropertyAddress propertyAddress = {
	CMIOObjectPropertySelector(FOUR_CHAR_CODE(<span class="hljs-string" style="color: rgb(106, 135, 89);">'clie'</span>)), // clie
	CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal), // glob
	CMIOObjectPropertyElement(kCMIOObjectPropertyElementMain) // 0000
};</pre><p>Где макрос FOUR_CHAR_CODE создаст селектор с нужным 4х-символьным кодом.</p><p>Давайте попробуем передать произвольную структуру данных из расширения в приложение. Для обмена удобнее всего объявить в общем для расширения и приложения заголовочном файле структуру, например:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;"><span class="hljs-keyword" style="color: rgb(204, 120, 50);">typedef</span> <span class="hljs-keyword" style="color: rgb(204, 120, 50);">struct</span> TSResolution {
    uint32_t  width;
    uint32_t  height;
} TSResolution;</pre><p>В существующий метод нашего устройства, добавим отдачу нового свойства:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">- (nullable CMIOExtensionDeviceProperties *)
    devicePropertiesForProperties:(<span class="hljs-built_in">NSSet</span>&lt;CMIOExtensionProperty&gt; *)properties
    error:(<span class="hljs-built_in">NSError</span> * _Nullable *)outError
{
    CMIOExtensionDeviceProperties *deviceProperties 
        = [CMIOExtensionDeviceProperties devicePropertiesWithDictionary:@{}];
<span class="hljs-comment" style="color: grey;">// …</span>
<span class="hljs-keyword" style="color: rgb(204, 120, 50);">if</span> ([properties containsObject:<span class="hljs-string" style="color: rgb(106, 135, 89);">@"4cc_reso_glob_0000"</span>]) {
    	TSResolution res = {.width = <span class="hljs-number" style="color: rgb(104, 151, 187);">1920</span>, .height = <span class="hljs-number" style="color: rgb(104, 151, 187);">1080</span>};
    	<span class="hljs-built_in">NSData</span> * resData = [<span class="hljs-built_in">NSData</span> dataWithBytes:&amp;res length:<span class="hljs-keyword" style="color: rgb(204, 120, 50);">sizeof</span>(res)];
    	CMIOExtensionPropertyState* propertyState
           = [CMIOExtensionPropertyState propertyStateWithValue:resData];
    	[deviceProperties setPropertyState:propertyState 
             forProperty:<span class="hljs-string" style="color: rgb(106, 135, 89);">@"4cc_reso_glob_0000"</span>];
}
    <span class="hljs-keyword" style="color: rgb(204, 120, 50);">return</span> deviceProperties;
}</pre><p>Как видим, мы просто создаем объект NSData, содержащий копию данных нашей структуры и выставляем этот объект как состояние соответствующего свойства. В качестве данных можно использовать другие объекты, например строки NSString.</p><p>Теперь можно прочитать эти данные обратно из приложения. Для этого нужен буфер, например std::vector или NSMutableData. Подключаемся к нашему объекту (см. выше п.2), здесь и далее CMIOObjectID objectID. Пример (проверки пропущены):</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">CMIOObjectPropertyAddress propertyAddress = {
    CMIOObjectPropertySelector(FOUR_CHAR_CODE(<span class="hljs-string" style="color: rgb(106, 135, 89);">'reso'</span>)),
    CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal),
    CMIOObjectPropertyElement(kCMIOObjectPropertyElementMain)
};

<span class="hljs-built_in">UInt32</span> dataSize = <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>;
<span class="hljs-built_in">UInt32</span> dataUsed = <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>;
OSStatus status = CMIOObjectGetPropertyDataSize(objectID,
     &amp;propertyAddress, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, &amp;dataSize);

<span class="hljs-keyword" style="color: rgb(204, 120, 50);">if</span> (status != <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>) {
    <span class="hljs-keyword" style="color: rgb(204, 120, 50);">return</span> status;
}

std::vector&lt;<span class="hljs-keyword" style="color: rgb(204, 120, 50);">char</span>&gt; buffer(dataSize);
status = CMIOObjectGetPropertyData(objectID,
    &amp;propertyAddress, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, dataSize, &amp;dataUsed,
    (<span class="hljs-keyword" style="color: rgb(204, 120, 50);">void</span> *)buffer.data());

TSResolution * res = (TSResolution *)buffer.data();
<span class="hljs-comment" style="color: grey;">// res-&gt;width; res-&gt;height</span></pre><p>Все хорошо, однако мы заметили, что повторный вызов получения данных свойства через CMIOObjectGetPropertyData не приводит к повторному чтению данных из метода devicePropertiesForProperties. Нам просто вернутся те же самые данные, что и при первом вызове, а сам вызов не дойдет до нашего кода. Дело в том, что если данные кастомного свойства поменялись, то мы должны явно об этом уведомить.</p><p>Для того чтобы вычитывать каждый раз новое значение, мы будем перед чтением вызывать запись свойства, чтобы сбросить его в расширении.      </p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">CMIOObjectSetPropertyData(objectID,
    &amp;propertyAddress,
    <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>,
    <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>,
    dataSize,
    (<span class="hljs-keyword" style="color: rgb(204, 120, 50);">void</span> *)buffer.data());
<span class="hljs-comment" style="color: grey;">// CMIOObjectGetPropertyData // потом читаем</span></pre><p>При этом в расширении саму операцию записи можно игнорировать, просто уведомлять об изменении:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">- (<span class="hljs-built_in">BOOL</span>)setDeviceProperties:(CMIOExtensionDeviceProperties *)deviceProperties error:(<span class="hljs-built_in">NSError</span> * _Nullable *)outError
{
    <span class="hljs-built_in">NSDictionary</span>&lt;CMIOExtensionProperty, CMIOExtensionPropertyState *&gt;* dictionary
        = [deviceProperties propertiesDictionary];
    [<span class="hljs-keyword" style="color: rgb(204, 120, 50);">self</span>.device notifyPropertiesChanged:dictionary]; <span class="hljs-comment" style="color: grey;">// уведомляем об изменении</span>

    <span class="hljs-keyword" style="color: rgb(204, 120, 50);">return</span> <span class="hljs-literal" style="color: rgb(104, 151, 187);">YES</span>;
}
</pre><p>Теперь перед чтением мы будем сбрасывать кеш значения и каждый раз получать новое состояние.</p><p>Однако, если нам все же нужно прочитать переданное значение из приложения? То в расширении в метод setDeviceProperties можно добавить:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">CMIOExtensionPropertyState* resProperty
    = [dictionary objectForKey:<span class="hljs-string" style="color: rgb(106, 135, 89);">@"4cc_reso_glob_0000"</span>];
<span class="hljs-keyword" style="color: rgb(204, 120, 50);">if</span> (resProperty &amp;&amp; resProperty.value) {
    <span class="hljs-built_in">NSData</span> * resData = (<span class="hljs-built_in">NSData</span> *)resProperty.value;
    TSResolution * res = (TSResolution *)[resData bytes];
}</pre><p>Передача строки осуществляется несколько проще. В расширении:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;">CMIOExtensionPropertyState* propertyState 
    = [CMIOExtensionPropertyState propertyStateWithValue:<span class="hljs-string" style="color: rgb(106, 135, 89);">@"String"</span>];
[deviceProperties 
    setPropertyState:propertyState 
    forProperty:<span class="hljs-string" style="color: rgb(106, 135, 89);">@"4cc_prop_glob_0000"</span>];</pre><p>В приложении:</p><pre class="hljs" style="color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;"><span class="hljs-built_in">UInt32</span> dataSize = <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>;
<span class="hljs-built_in">UInt32</span> dataUsed = <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>;
<span class="hljs-built_in">CFStringRef</span> cfStrData = <span class="hljs-literal" style="color: rgb(104, 151, 187);">NULL</span>;

CMIOObjectGetPropertyDataSize(objectID,
    &amp;propertyAddress, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, &amp;dataSize);

CMIOObjectGetPropertyData(objectID,
    &amp;propertyAddress, <span class="hljs-number" style="color: rgb(104, 151, 187);">0</span>, <span class="hljs-literal" style="color: rgb(104, 151, 187);">nil</span>, dataSize, &amp;dataUsed, &amp;cfStrData);

<span class="hljs-built_in">NSString</span> * data = (__bridge <span class="hljs-built_in">NSString</span> *)cfStrData;</pre>]]></content:encoded></item><item><title><![CDATA[Современные стандарты авторизации в Desktop приложениях]]></title><description><![CDATA[Использование технологии OAuth 2.0 и Firebase C++ SDK для авторизации пользователей в Desktop приложениях для Windows, Linux и MacOS с применением Qt]]></description><link>https://blog.tomsksoft.ru/sovriemiennyie-standarty-avtorizatsii-v-desktop-prilozhieniiakh/</link><guid isPermaLink="false">625ce8b671488d6f5e98aa81</guid><category><![CDATA[C++]]></category><category><![CDATA[Qt]]></category><category><![CDATA[Firebase]]></category><category><![CDATA[OAuth 2.0]]></category><category><![CDATA[Desktop]]></category><dc:creator><![CDATA[Aleksey Verkhoglyad]]></dc:creator><pubDate>Wed, 20 Apr 2022 05:29:00 GMT</pubDate><content:encoded><![CDATA[<h1 id="-"><strong><strong>Введение</strong></strong></h1><p>В текущий момент в разработке прослеживается общая тенденция упрощения механизмов авторизации и отказ от длинных регистрационных форм с большим количеством вопросов. Стандартной практикой является добавление возможности зарегистрироваться с помощью уже существующих профилей в социальных сетях. Это позволяет упростить процесс регистрации всего до пары кликов. </p><p>Внедрение механизма авторизации через соцсети, помимо улучшения пользовательского опыта, позволяет разработчикам сервисов получить доступ к уже существующим данным пользователей и обеспечить защиту от авторегистраций, накруток и других действий потенциальных злоумышленников.</p><p>Современные приложения используют стандарт OAuth 2.0, позволяющий получить ограниченный доступ к ресурсам соцсетей с помощью технологий Web. В данной статье изложены сведения о внедрении данного механизма в Desktop приложение Qt с применением службы Firebase Authentication и C++ SDK.</p><h1 id="--1"><strong>Пользовательский опыт регистрации через социальную сеть</strong></h1><p>С продуктовой точки зрения, пользовательский опыт можно описать следующим образом. На экране входа (рисунок 1), помимо полей ввода имени и пароля, также содержится набор кнопок с пиктограммами социальных сетей.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2022/04/1.png" class="kg-image"><figcaption>Рисунок 1 - экран входа в приложение</figcaption></figure><p>При нажатии на кнопку происходит открытие системного браузера и переход на специальную страницу авторизации в выбранной социальной сети (рисунок 2). На экране отображается информация об учетной записи пользователя и данных, доступ к которым будет предоставлен приложению. От пользователя требуется подтвердить предоставление доступа нажатием на соответствующую кнопку. </p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2022/04/2-1.png" class="kg-image"><figcaption>Рисунок 2 - предоставление доступа к Google аккаунту в браузере</figcaption></figure><h1 id="oauth-2-0"><strong>OAuth 2.0</strong></h1><p>Спецификация OAuth 2.0 определяет протокол делегирования, который предоставляет клиентам безопасный доступ к ресурсам пользователя на сервисе-провайдере. Такой подход избавляет пользователя от необходимости вводить пароль за пределами сервиса-провайдера: весь процесс сводится к нажатию кнопки «Согласен предоставить доступ к ...». Идея в том, что имея один хорошо защищенный аккаунт, пользователь может использовать его для аутентификации на других сервисах, не раскрывая при этом своего пароля.</p><figure class="kg-image-card kg-width-wide"><img src="https://blog.tomsksoft.ru/content/images/2022/04/3.png" class="kg-image"><figcaption>Рисунок 3 - общая схема OAuth 2.0</figcaption></figure><p>Общая схема взаимодействия (рисунок 3) предусматривает следующие шаги:</p><ol><li>Клиент запрашивает авторизацию у владельца ресурса.</li><li>Клиент получает грант авторизации.</li><li>Клиент запрашивает токен доступа путем аутентификации с помощью сервера авторизации и предоставление гранта авторизации.</li><li>Сервер авторизации аутентифицирует клиента, проверяя грант авторизации и, если он действителен, выдает токен доступа (access token) и рефреш токен (refresh token).</li><li>Клиент запрашивает защищенный ресурс у провайдера и аутентифицируется, предоставляя токен доступа.</li><li>Провайдер проверяет токен доступа и, если он действителен, обслуживает запрос.</li></ol><h1 id="firebase-authentication"><strong><strong>Firebase Authentication</strong></strong></h1><p>Аутентификация Firebase - это функция аутентификации пользователей, предоставляемая Firebase в качестве ее серверных служб. Это система аутентификации на основе токенов, которая обеспечивает легкую интеграцию с большинством платформ.</p><p>Firebase предоставляет бэкенд, простой в использовании SDK и готовые библиотеки пользовательского интерфейса для реализации аутентификации пользователей в приложении. Он поддерживает аутентификацию как с помощью email и пароля, так и с помощью таких популярных поставщиков идентификации, как Google, Facebook, Twitter и GitHub. Доступна версия C++ SDK c поддержкой Desktop платформ Windows и Mac OS.</p><h1 id="-oauth-2-0-google-cloud-platform"><strong>Конфигурация OAuth 2.0 в Google Cloud Platform </strong></h1><p>Конфигурация осуществляется в консоли, доступной по адресу <a href="https://console.cloud.google.com/apis/dashboard?project=c-auth-test">https://console.cloud.google.com/apis/dashboard</a> и включает следующие шаги:</p><ol><li>Создать новый или выбрать существующий проект.</li><li>На вкладке Credentials создать Web client id.</li><li>Установить Redirect URI <a href="http://127.0.0.1:8080/">http://127.0.0.1:8080/</a></li><li>Скачать файл конфигурации json.</li></ol><h1 id="-firebase-authentication"><strong>Конфигурация <strong>Firebase Authentication</strong></strong></h1><p>Конфигурация осуществляется в консоли, доступной по адресу <a href="https://console.firebase.google.com/">https://console.firebase.google.com/</a> и включает следующие шаги:</p><ol><li>Создать новый или выбрать существующий Android проект.</li><li>На вкладке Authentication – Sign In method добавить необходимые провайдеры, в том числе Google.</li><li>Перейти в Project Settings и скачать google-services.json</li></ol><h1 id="-oauth-2-0-qt"><strong>Поддержка OAuth 2.0 в Qt</strong></h1><p>Qt предоставляет модуль <strong>Qt Network Authorization</strong>, который обеспечивает поддержку OAuth 2.0. С помощью удобного API можно получить Access Token.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2022/04/4.png" class="kg-image"><figcaption>Рисунок 4 - листинг работы с Qt Network Authorization</figcaption></figure><p>Авторизация (рисунок 4) включает в себя следующие шаги:</p><ol><li>Парсинг JSON файла конфигурации, полученного из консоли Google cloud platform.</li><li>Создание экземпляра <strong>QOAuth2AuthorizationCodeFlow</strong> и привязка его сигнала <strong>authorizeWithBrowser</strong> к <strong>QDesktopServices::openUrl</strong>.</li><li>Настройка параметров <strong>QOAuth2AuthorizationCodeFlow</strong> значениями из файла конфигурации.</li><li>Создание экземпляра <strong>QOAuthHttpServerReplyHandler </strong>для обработки ответа авторизации.</li><li>Привязка к сигналу <strong>QOAuth2AuthorizationCodeFlow::granted</strong> для обработчики полученного токена.</li></ol><h1 id="firebase-c-sdk"><strong><strong>Firebase C++ SDK</strong></strong></h1><p>Firebase C++ SDK предоставляет API аутентификации пользователей, доступа к информации из аккаунтов социальных сетей и получения Firebase Access Token, необходимого для передачи на бэкенд приложения, при наличии такового. </p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2022/04/5.png" class="kg-image"><figcaption>Рисунок 5 - листинг Firebase Google authentication</figcaption></figure><p>Работа с SDK (рисунок 5) включает следующие шаги:</p><ol><li>Создание экземпляра <strong>firebase::App</strong>.</li><li>Создание <strong>firebase::Auth</strong>.</li><li>Создание <strong>firebase::Credential</strong> с помощью токена id_token, полученного через OAuth 2.0.</li><li>Вызов <strong>firebase::Auth::SignInWithCredential</strong>.</li><li>Обработка результата запроса, содержащего токен и сведения об аккаунте в социальной сети, такие как аватар, имя и почтовый адрес пользователя.</li></ol><h1 id="--2"><strong><strong>Вывод</strong></strong></h1><p>Текущий уровень развития средств разработки и библиотек обеспечивает возможность внедрения современных способов авторизации в Desktop приложения. </p><p>Описанный подход позволяет улучшить пользовательский опыт благодаря отказу от Web View и использованию нативных экранных форм окна входа. Другим важным преимуществом является снижение риска возникновения интеграционных ошибок, так как применяется более простая схема с меньшим количеством программных модулей и команд, вовлеченных в разработку продуктового функционала.</p>]]></content:encoded></item><item><title><![CDATA[Android accessibility]]></title><description><![CDATA[Инструменты, этапы и основные требования при разработке Android приложений с учетом Android accessibility]]></description><link>https://blog.tomsksoft.ru/android-accessibility/</link><guid isPermaLink="false">623be90a71488d6f5e98aa75</guid><dc:creator><![CDATA[Dmitry A]]></dc:creator><pubDate>Mon, 28 Mar 2022 04:04:54 GMT</pubDate><content:encoded><![CDATA[<h1 id="">Введение</h1>
<p>Многие разработчики программных продуктов заинтересованы в расширении аудитории приложения, которое они разрабатывают. Если вы только начинаете задумываться о том, насколько удобно будет пользоваться вашим приложением людям с ограниченными возможностями, и вам трудно понять, каковы требования к<br>
<a rel="nofollow" href="https://developer.android.com/guide/topics/ui/accessibility">accessibility</a> и как начать работу, то эта статья для вас. Она позволит вам начать изучение распространенных проблем с <strong>accessibility</strong> в приложениях <strong>Android</strong>. А их устранение поможет многим пользователям вашего приложения.</p>
<p>Операционная система <strong>Android</strong> предоставляет <a rel="nofollow" href="https://play.google.com/store/apps/details?id=com.google.android.marvin.talkback&hl=en&gl=US">Android Accessibility Suite</a> - это набор различных приложений <strong>Google</strong>, предназначенных для людей с ограниченными возможностями зрения, слуха, речи и другими физическими недостатками. Эти приложения упрощают использование смартфона и позволяют использовать устройство в полной мере, поскольку предлагают различные способы чтения текстов и веб-страниц, открытия приложений и т.д.</p>
<h1 id="">Инструменты</h1>
<p>Основные инструменты, используемые для тестирования <strong>Android Accessibility</strong>, с которых вам можно начать, являются:</p>
<ul>
<li><strong>TalkBack</strong> (<a rel="nofollow" href="https://support.google.com/accessibility/android/topic/3529932?hl=en&ref_topic=9078845">документация</a>, <a rel="nofollow" href="https://github.com/google/talkback">исходный код</a>)</li>
<li><a rel="nofollow" href="https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor">Accessibility Scanner</a></li>
<li>Различные онлайн ресурсы для вычисления contrast ratio:<br>
<a rel="nofollow" href="https://material.io/resources/color/#!/?view.left=1&view.right=0">Color tool</a>, <a rel="nofollow" href="https://webaim.org/resources/contrastchecker">Contrast Checker</a>, <a rel="nofollow" href="https://color-contrast-checker.deque.com">Color Palette Contrast Checker</a></li>
<li>Инструменты для автоматизации тестирования: <a rel="nofollow" href="https://developer.android.com/guide/topics/ui/accessibility/testing#espresso">Espresso</a>, <a rel="nofollow" href="https://developer.android.com/guide/topics/ui/accessibility/testing#robolectric">Robolectric</a></li>
</ul>
<h2 id="talkback">TalkBack</h2>
<p>На большинстве устройств приложение <strong>TalkBack</strong>  уже предустановлено. Это программа чтения с экрана позволяет озвучивать элементы и управлять устройством с помощью жестов, а также печатать с помощью экранной клавиатуры Брайля.</p>
<blockquote>
<p>Включение <strong>TalkBack</strong> может отличаться в зависимости от производителя устройства и версии Android, например, так:<br>
<strong>Settings -&gt; Accessibility -&gt; Vision -&gt; Talkback -&gt; Включить switch</strong></p>
</blockquote>
<p>При разработке необходимо привыкнуть к особенностям взаимодействия с устройством, когда <strong>TalkBack</strong> включен. Например, чтобы произвести нажатие на элемент на экране, необходимо выбрать элемент касанием и выполнить двойное нажатие на экран, а чтобы прокрутить экран (<strong>swipe</strong>) необходимо использовать два пальца вместо одного.</p>
<blockquote>
<p><strong>TalkBack</strong> поддерживает два основных вида навигации по устройству:<br>
<strong>Линейный обход.</strong> Для перемещения между элементами экрана необходимо проводить по экрану вправо или влево. При этом <strong>TalkBack</strong> будет озвучивать каждый объект, которого вы выбираете.<br>
<strong>Изучение касанием.</strong> Для этого нужно коснуться экрана пальцем и водить по экрану, пока не найдете нужный элемент.</p>
</blockquote>
<h2 id="accessibilityscanner">Accessibility Scanner</h2>
<p>Это приложение созданное <strong>Google</strong> специально для поиска распространенных проблем с <strong>accessibility</strong>. После его установки любой экран в любом приложении можно проверить на наличие проблем с <strong>accessibility</strong>.</p>
<h2 id="">Области для улучшения</h2>
<p>Когда разработчики слышат об <strong>accessibility</strong> в приложениях, они часто думают о нарушениях зрения, включая его потерю. Но следует помнить также о нарушениях подвижности, которые могут затруднить выбор элементов управления и навигацию. Можно выделить пять самых главных областей с наиболее распространенными проблемами с <strong>accessibility</strong>:</p>
<ul>
<li>Описание содержимого элементов</li>
<li>Навигация</li>
<li>Размеры шрифтов</li>
<li>Контраст цветов</li>
<li>Области элементов для нажатия</li>
</ul>
<h2 id="">Описание содержимого элементов</h2>
<p>Попробуйте включить <strong>Talkback</strong> и начните проверять, как <a rel="nofollow" href="https://github.com/google/talkback/blob/master/compositor/src/main/res/values/strings.xml">озвучиваются</a> различные элементы, когда вы их выбираете. Вот вопросы, которые следует задавать себе при такой проверке:</p>
<ul>
<li>Есть ли у элементов описания содержимого?</li>
<li>Соответствует ли озвучивание тому, что на самом деле показано на экране?</li>
<li>Совпадает ли озвучивание действий, которые выполняют элементы экрана?</li>
<li>При изменении состояния элемента обновляется ли описание содержимого?</li>
<li>Есть ли какие-то элементы, которые являются только декоративными и могут быть исключены из озвучивания?</li>
</ul>
<p>Вышеупомянутый <strong>Accessibility Scanner</strong> выявит многие из этих проблем и даже подскажет возможные их решения. В основном потребуется добавить атрибуты конкретным элементам, такие как <strong>contentDescription</strong>, <strong>importantForAccessibility</strong>, <strong>labelFor</strong> и др. <strong>Android Lint</strong> указывает на отсутствие атрибута <strong>contentDescription</strong> у <strong>ImageView</strong>:<br>
<img src="https://blog.tomsksoft.ru/content/images/2022/03/screenshot_from_2021-11-22_17-59-55.png" alt="screenshot_from_2021-11-22_17-59-55"><br>
Описание элемента экрана может быть добавлено и обновлено. Декоративные элементы могут быть помечены как не важные для <strong>accessibility</strong> и будут пропущены (<strong>android:importantForAccessibility = &quot;no&quot;</strong>).</p>
<p><strong>TalkBack</strong> озвучивает текст вместе с информацией о типе элемента, его состоянии и возможных действиях с ним. Это означает, что разработчикам приложений нет необходимости повторять эту информацию, не перегружая тем самым пользователя:</p>
<ul>
<li>Если на экране есть элемент <a rel="nofollow" href="https://developer.android.google.cn/reference/android/widget/Button">Button</a> с текстом <strong>&quot;Log In&quot;</strong>, <strong>TalkBack</strong> сам добавляет тип элемента <a rel="nofollow" href="https://github.com/google/talkback/blob/master/compositor/src/main/res/values/strings.xml#L224">&quot;Button&quot;</a> после текста: <strong>&quot;Log In, button&quot;</strong>. Если же разработчик решит включить тип элемента  в <strong>contentDescription</strong>, то тип элемента озвучится дважды: <strong>&quot;Log In, button button&quot;</strong>).</li>
<li>Если на экране есть элемент <a rel="nofollow" href="https://developer.android.google.cn/reference/android/widget/CheckBox">CheckBox</a> с текстом, <strong>TalkBack</strong> озвучивает его состояние <strong>&quot;checked&quot;</strong> или <strong>&quot;not checked&quot;</strong> вместе с текстом. Если, например, разработчик попробует добавить к <strong>CheckBox'у</strong> информацию о состоянии, то пользователь может услышать эту информацию дважды.</li>
</ul>
<p>Бывает и так, что тип элемента не соответствует его предназначению. Например, на <strong>Camfrog Android 7.13</strong> кнопка открытия экрана логина была не <strong>Button</strong>, а <a rel="nofollow" href="https://developer.android.google.cn/reference/kotlin/android/widget/TextView?hl=en">TextView</a>, стилизованный под кнопку:<br>
<img src="https://blog.tomsksoft.ru/content/images/2022/03/screenshot_2021-11-22-08-31-49.png" alt="screenshot_2021-11-22-08-31-49"><br>
В результате того, что для кнопки использовался элемент <strong>TextView</strong>, <strong>TalkBack</strong> озвучивал только его текст (<strong>&quot;Log In&quot;</strong>), не озвучивая, что это кнопка. Соответственно пользователь также этого не знал, и это вводило его в заблуждение. Решение было простое: заменить <strong>TextView</strong> на <a rel="nofollow" href="https://developer.android.com/reference/com/google/android/material/button/MaterialButton">MaterialButton</a>.</p>
<blockquote>
<p>Для упрощения разработки можно показывать текст озвучки на экране: <strong>Settings -&gt; Accessibility -&gt; Vision -&gt; Talkback -&gt; Settings -&gt; Advance settings -&gt; Developer settings -&gt; Display speech output set to ON.</strong> Эту настройку можно также использовать, если есть необходимость продемонстрировать коллеге текст озвучки, отправив ему скриншот экрана.</p>
</blockquote>
<h2 id="">Навигация</h2>
<p>Некоторые вопросы, которые требуют рассмотрения:</p>
<ul>
<li>Когда вы перемещаетесь по приложению, логичен ли порядок изменения фокуса элементов?</li>
<li>Можно ли сгруппировать некоторые элементы для упрощения линейного обхода <strong>Talkback</strong>?</li>
</ul>
<p>Исправление навигации может быть <a rel="nofollow" href="https://material.io/design/usability/accessibility.html#hierarchy">трудной задачей</a>. Некоторые проблемы можно решить добавлением атрибутов <strong>accessibilityTraversalBefore</strong>, <strong>accessibilityTraversalAfter</strong>, <strong>nextFocusForward</strong> и др. Здесь стоит задуматься о порядке линейного обхода <strong>Talkback</strong>, например, элементы <strong>display name</strong> могут быть сгруппированы в один и пройдены, и озвучены единожды, не делая это четыре раза для каждого элемента:<br>
<img src="https://blog.tomsksoft.ru/content/images/2022/03/screenshot_2021-11-23-09-12-55.png" alt="screenshot_2021-11-23-09-12-55"></p>
<blockquote>
<p>Попробуйте провести увлекательный эксперимент: включите <strong>Talkback</strong> на устройстве, закройте глаза и попробуйте выполнить простые действия в вашем приложении. Это будет ценный и незабываемый опыт, который поможет определить наиболее проблемные области в вашем приложении.</p>
</blockquote>
<h2 id="">Размеры шрифта</h2>
<p>Изменение шрифта на больший размер в настройках устройства поможет проверить наличие проблем в этой области. Вещи, которые нужно иметь в виду:</p>
<ul>
<li>Соответствует ли весь текст в приложении размеру шрифта, определенный устройством?</li>
<li>Накладываются ли какие-либо элементы друг на друга?</li>
<li>Если ли какой-либо текст, который может быть обрезан и больше не может быть прочитан?</li>
</ul>
<p>Чтобы устранить проблемы в этой области, замените все <strong>dp</strong> на <strong>sp</strong> для определения размера текста. Убедитесь, что элементы масштабируются в соответствии с размерами текста: атрибуты <strong>minWidth</strong>, <strong>minHeight</strong> и <strong>ellipsize</strong>, а также использование <strong>wrap_content</strong> для высоты и ширины помогут с этим.</p>
<h2 id="">Контраст</h2>
<p>Некоторые вопросы, которые требуют внимания:</p>
<ul>
<li>Какие цвета фона и текста/изображения на нем можно использовать в приложении, чтобы упростить их просмотр?</li>
<li>Можно ли легко различить элементы, которые можно нажать?</li>
</ul>
<p>Общее требование - минимальное соотношение контрастности текста к фону <a rel="nofollow" href="https://material.io/design/usability/accessibility.html#color-and-contrast"><strong>4,5:1</strong> и <strong>3:1</strong></a> для элементов управления. В некоторых случаях добавление контуров поможет различить элементы на экране. Проверить контрастность текста и изображений на определенном фоне можно с помощью <strong>Accessibility Scanner'a</strong>:</p>
<table>
    <tr>
        <td>
            <img align="”left”" src="https://blog.tomsksoft.ru/content/images/2022/03/screenshot_2021-11-22-08-28-12.png" width="288" height="512">
        </td>
        <td>
            <img src="https://blog.tomsksoft.ru/content/images/2022/03/screenshot_2021-11-23-14-34-48.png" width="288" height="512"> 
        </td>
    </tr>
</table>
<h2 id="">Область элемента для нажатия</h2>
<p>Если вы когда-либо безуспешно пытались нажать кнопку несколько раз, вы столкнулись с этой проблемой. Некоторые вещи, которые следует иметь в виду:</p>
<ul>
<li>Достаточных ли размеров область вокруг элемента, которая реагирует на нажатие?</li>
<li>Можно ли ее расширить?</li>
<li>Можно ли коснуться всего элемента или область касания ограничена небольшой его частью?</li>
<li>Есть ли много мелких элементов рядом друг с другом, так что можно легко нажать на соседний элемент вместо желаемого?</li>
</ul>
<p>Рекомендация <strong>Android</strong> - <a rel="nofollow" href="https://material.io/design/usability/accessibility.html#layout-and-typography">минимум 48x48dp</a> для доступных областей. Выставление <strong>margin'ов</strong> и <strong>padding'ов</strong> между элементами, где это возможно, может облегчить нажатие на нужный элемент. <strong>Accessibility Scanner</strong> умеет определять области, которые слишком малы для точного нажатия и дает рекомендации по их увеличению:<br>
<img src="https://blog.tomsksoft.ru/content/images/2022/03/screenshot_2021-11-22-08-28-35.png" width="288" height="512"></p>
<h2 id="customviews">Custom views</h2>
<p>Особое внимание следует обратить на <strong>custom views</strong>. Если вы создаете их на основе классов <a rel="nofollow" href="https://material.io/components?platform=android">Material Components</a>, то возможно их <a rel="nofollow" href="https://material.io/design/usability/accessibility.html#understanding-accessibility">встроенной поддержки accessibility</a> будет достаточно. Но бывает так, что <strong>custom views</strong> наследует и использует <strong>Android framework/appcompat widget'ы</strong>, в которых поддержки <strong>accessibility</strong> <a rel="nofollow" href="https://developer.android.com/guide/topics/ui/accessibility/custom-views">может не хватать</a>. Это можно исправить путем добавления <a rel="nofollow" href="https://developer.android.google.cn/reference/kotlin/androidx/core/view/AccessibilityDelegateCompat">AccessibilityDelegateCompat</a> с указанием нужных текстов для озвучивания:</p>
<pre><code>open class AuthInputView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
	FrameLayout(context, attrs, defStyleAttr) {
	...

	init {
  	...
		ViewCompat.setAccessibilityDelegate(viewHolder.input, AccessibilityDelegate(viewHolder))
	}
  
	class AccessibilityDelegate(val viewHolder: ViewHolder) : AccessibilityDelegateCompat() {
		override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
			super.onInitializeAccessibilityNodeInfo(host, info)
			val inputText = viewHolder.input.text
			val helperText = viewHolder.helperText.text
			info.text = &quot;$inputText, $helperText&quot;

			if (viewHolder.error.isVisible) {
				info.error = viewHolder.error.text
			}
		}
	}
}
</code></pre>
<p>Разработчикам стоит помнить, что избыток информации иногда вредит больше, чем ее недостаток. Особенно когда она явно лишняя и к делу не относится. Речевые сообщения требуют времени на восприятие, поэтому их объем следует сокращать везде, где это возможно, не жертвуя при этом полезной информацией.</p>
<blockquote>
<p>В некоторых случаях, функционала <a rel="nofollow" href="https://developer.android.google.cn/reference/kotlin/androidx/core/view/AccessibilityDelegateCompat">AccessibilityDelegateCompat</a> может не хватать, и придется писать свой <a rel="nofollow" href="https://developer.android.com/guide/topics/ui/accessibility/service">Accessibility service</a> для логики вашего приложения.</p>
</blockquote>
<blockquote>
<p>Есть еще несколько ресурсов по <a rel="nofollow" href="https://stuff.mit.edu/afs/sipb/project/android/docs/design/patterns/accessibility.html">дизайну</a>, <a rel="nofollow" href="https://stuff.mit.edu/afs/sipb/project/android/docs/guide/topics/ui/accessibility/checklist.html">разработке</a> и <a rel="nofollow" href="https://stuff.mit.edu/afs/sipb/project/android/docs/tools/testing/testing_accessibility.html">тестированию</a> <strong>accessibility</strong> приложений, которые не были упомянуты в этой статье.</p>
</blockquote>
<h1 id="">Заключение</h1>
<p>Улучшение <strong>accessibility</strong> вашего приложения - это возможность взглянуть на него с другой стороны, усовершенствовать общее качество вашего приложения и обеспечить всем вашим пользователям приятный опыт взаимодействия с ним. Хорошей новостью является то, что путем несложных телодвижений можно порой существенно улучшить ситуацию с <strong>accessibility</strong> без глобальных изменений вашего приложения.</p>
]]></content:encoded></item><item><title><![CDATA[TomskSoft's open source NATter service]]></title><description><![CDATA[NATer - система с открытым кодом, разрабатываемая компанией ТомскСофт и написанная на языке Golang, задача которой – проксировать сообщения, между брокером NATs и веб-сайтом на php]]></description><link>https://blog.tomsksoft.ru/natter/</link><guid isPermaLink="false">61496ce645f9e45e49789714</guid><category><![CDATA[go]]></category><category><![CDATA[nats]]></category><category><![CDATA[message broker]]></category><category><![CDATA[microservice]]></category><dc:creator><![CDATA[Maksim Kuznetsov]]></dc:creator><pubDate>Wed, 22 Sep 2021 11:22:35 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2021/09/cover-5.png" medium="image"/><content:encoded><![CDATA[<h2 id="-">Введение</h2><img src="https://blog.tomsksoft.ru/content/images/2021/09/cover-5.png" alt="TomskSoft's open source NATter service"><p>В связи с массовым переходом систем, разрабатываемых и поддерживаемых ТомскСофтом, как, например Camfrog, на микросервисную архитектуру необходим инструмент, который позволит сделать этот переход плавным и безболезненным, не прерывая текущую их работу. Кроме того, так как интеграция микросервисов, как правило, осуществляется с помощью вспомогательных систем, таких как, например, брокер сообщений, а изначально монолитная система функционирует, скажем, на HTTP, то этот инструмент должен обеспечить надёжную и стабильную коммуникацию между сервисами, работающими на разных протоколах, будь то сеансовых или с динамической маршрутизацией.</p><h2 id="-natter-">Что такое NATter и зачем он нужен?</h2><p>NATter – это система с открытым кодом, разрабатываемая ТомскСофтом и написанная на языке Golang, задача которой – проксировать сообщения, между брокером и веб-сайтом. Смысл в том, что новоиспечённый микросервис декомпозируемой монолитной системы использует, например, NATS для обмена сообщениями, а сама система, делегировавшая какую-то часть функционала этому сервису, использует HTTP, что затрудняет коммуникацию между ними. Задача NATter’а как раз и состоит в том, чтобы возобновить общение между монолитом и отколовшимся от микросервисом.</p><h2 id="--1">Пример использования</h2><p>Допустим, клиентам какой-либо системы нужно проверять валидность email’а при регистрации или его изменении. Один из способов реализации – это валидация непосредственно на веб-сервере.</p><p>Другой способ основан на микросерверном подоходе к архитектуре: выделяется отдельный сервис, занимающийся валидацией email’а и работающий, например, с NATS. Так как запросы продолжают приходить на веб, то возникает проблема его коммуникации с сервисом.</p><p>NATter в данном случае с одной стороны предоставляет HTTP-интерфейс для веба, с другой – имеет доступ к очереди сообщений, по ту сторону которой находится сервис, производящий валидацию email’а. Кто именно в конечном итоге обработает запрос и вернёт ответ, для NATter’а и, следовательно, для веба не имеет значения.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/NATter-deploy.drawio-1.png" class="kg-image" alt="TomskSoft's open source NATter service"></figure><p>Таким образом можно задавать NATter'у неограниченное количество маршрутов, являющихся связующим звеном между вебом и микросервисами.</p><p>Хотя NATter и был создан для использования в веб-разработке, ему можно найти и другие применения.</p><h2 id="--2">Батчинг</h2><p>NATter поддерживает отправку сообщений пакетами, то есть каждый отдельный маршрут возможно настроить таким образом, чтобы каждое новое сообщение, пришедшее от отправителя, отдавалось получателю не сразу, а при условиях, определяемых параметрами, такими как таймаут и размер пакета, измеряемый в количестве сообщений. Для сериализации используется Protocol Buffers. Общий алгоритм работы батчинга:</p><ol>
<li>заводится таймер, обнуляется счётчик сообщений, определяющий размер пакета, и ожидается приход очередного сообщения;</li>
<li>от отправителя приходит сообщение, которое удерживается в памяти до тех пор пока не выполнится одно из следующих условий:
<ul>
<li>таймер истёк;</li>
<li>размер пакета достиг определённого максимума;</li>
<li>сервис завершает работу;</li>
</ul>
</li>
<li>после выполнения одного из условий выше каждое удерживаемое сообщение конвертируется в <strong>google.protobuf.Any</strong>, после чего массив полученных структур сериализуется в бинарник, отправляемый получателю;</li>
<li>если сервис не завершает работу, возврат к шагу 1.</li>
</ol>
<h2 id="--3">Архитектура</h2><p>Основой NATter’а является пакет <strong>driver</strong>, предоставляющий интерфейсы для работы с протоколом:</p><pre><code>type Conn interface {
        Serve(context.Context) error
        Close() error
        Receiver(*entity.Route) Receiver
        Sender(*entity.Route) Sender
}

type Receiver interface {
        Listen(Sender) error
        ListenRequest(Sender) error
}

type Sender interface {
        Send(payload []byte) error
        Request(payload []byte) ([]byte, error)
}
</code></pre>
<p>Эти интерфейсы в совокупности реализуют шаблон проектирования "Абстрактная фабрика". <strong>Conn</strong> представляет собой соединение по протоколу, которое можно контролировать с помощью основных методов <strong>Serve()</strong> и <strong>Close()</strong>. Также есть два вспомогательных фабричных метода: <strong>Receiver()</strong> и <strong>Sender()</strong> – создающих и возвращающих сущности соответственно для приёма и отправки сообщений по протоколу, определяемому сущностью <strong>Conn</strong>. Настройка получателя и отправителя осуществляется на основе экземпляра структуры <strong>entity.Route</strong>, передаваемой в фабричный метод:</p><pre><code>type Route struct {
        Mode     RouteMode
        Async    bool
        Topic    string
        Endpoint string
        URI      string
}
</code></pre>
<p>Здесь <strong>Mode</strong> – это режим маршрута вида “отправитель-получатель-направленность”, <strong>Async</strong> – флаг асинхронности маршрута, <strong>Topic</strong> – топик-источник или топик-назначение сообщения, <strong>Endpoint</strong> – адрес-назначение сообщения, <strong>URI</strong> – путь-источник сообщения.</p><p>Поле <strong>Mode</strong> имеет вид “отправитель-получатель-направленность”, где отправитель – это имя протокола-источника сообщения, получатель – имя протокола-назначения сообщения, а направленность – одно из двух значений: <strong>oneway</strong> (запрос) и <strong>twoway</strong> (запрос-ответ). Например, <strong>http-broker-oneway</strong> означает одностороннее проксирование сообщения от HTTP-клиента к брокеру сообщений, а <strong>broker-http-twoway</strong> – проксирование запроса от брокера к HTTP-серверу и обратное проксирование ответа.</p><p>Сами интерфейсы <strong>Receiver</strong> и <strong>Sender</strong> предоставляют методы как для односторонней обработки сообщений (<strong>Listen()</strong> и <strong>Send()</strong>), так и для обработки запросов-ответов(<strong>ListenRequest()</strong> и <strong>Request()</strong>). При этом <strong>Receiver</strong> требует передачи <strong>Sender</strong> в свои методы, так как после приёма сообщения <strong>Receiver</strong>’ом оно сразу направляется <strong>Sender</strong>’у.</p><p>Простой пример использования интерфейсов для создания маршрута:</p><pre><code>var (
        httpConn driver.Conn = // ...
        natsConn driver.Conn = // ...
)

route := &amp;entity.Route{
        Topic:    &quot;hello&quot;,
        Endpoint: &quot;https://www.world.com/&quot;,
}

natsReceiver := natsConn.Receiver(route)
httpSender := httpConn.Sender(route)

natsReceiver.Listen(httpSender)

ctx, _ := context.WithTimeout(context.Background(), time.Minute)

go func() {
        httpConn.Serve(ctx)
        httpConn.Close()
}

go func() {
        natsConn.Serve(ctx)
        natsConn.Close()
}

&lt;-ctx.Done()
</code></pre>
<p>В данном примере создаются сущности <strong>Conn</strong>, относящиеся к протоколам “HTTP” (<strong>httpConn</strong>) и “NATS” (<strong>natsConn</strong>). После этого создаются <strong>natsReceiver</strong> (с топиком <strong>hello</strong>) и <strong>httpSender</strong> (с адресом <strong>https://www.world.com/</strong>). Строка</p><p><code>natsReceiver.Listen(httpSender)</code></p>
<p>буквально означает следующее: “отправить сообщение, принятое <strong>natsReceiver</strong>’ом (т.е. из топика <strong>hello</strong>), <strong>httpSender</strong>’у (т.е. по адресу <strong>https://www.world.com/</strong>)”. Далее <strong>httpConn</strong> и <strong>natsConn</strong> приводятся в готовность вызовом блокирующего метода <strong>Serve()</strong> и закрываются по завершении контекста <strong>ctx</strong> вызовом <strong>Close()</strong>.</p><p>Регистрация маршрутов, описанная выше в общих чертах, выполняется сущностью <strong>service.Router</strong>. Конструктор <strong>Router</strong>’а принимает массив маршрутов и карту <strong>&lt;string, driver.Conn&gt;</strong>, где <strong>string</strong> – это имя соединения; эта карта при обработке режима маршрута (<strong>entity.Route.Mode</strong>), указываемого в конфиге, позволяет определить, по каким именно протоколам требуются соединения для регистрации маршрута. Например, если имеется следующая карта:</p><pre><code>map[string]driver.Conn{
        &quot;nats&quot;:      natsConn,
        &quot;kafka&quot;:     kafkaConn,
        &quot;websocket&quot;: websocketConn,
}
</code></pre>
<p>то поле <strong>Route.Mode</strong> может быть <strong>nats-websocket-oneway</strong>, <strong>kafka-nats-oneway</strong>, <strong>websocket-kafka-twoway</strong> и т.д.</p><p>Инициализация соединений и создание на их основе карты осуществляется в методе <strong>(*cmd.NATter) setupConns() error</strong>.</p><h2 id="-open-source">Выпуск в open source</h2><p>Изначально проект разрабатывался только для внутреннего использования и назывался (и, по крайней мере, пока продолжает называться внутри ТомскСофта) “NATter v2.0”. Название было унаследовано от менее универсальной системы “NATter”, ныне уже не используемой, на которой и основана данная разработка. Решение о выпуске проекта в open source поставило перед нами задачу разработать такую архитектуру, которая позволит дополнять функционал NATter'а собственными реализациями работы с протоколами помимо тех, что уже “вшиты” в базовую реализацию сервиса (HTTP, NATS, Kafka), не изменяя заложенную концепцию, а также “вытянуть” все зависимости из нашей общей библиотеки, написанной специально для сервисов Camshare, непосредственно в NATter.</p><h2 id="--4">Используемые библиотеки</h2><p>Реализация необходимых модулей внутренней библиотеки для сервисов Camshare была вытянута в почти неизменном виде, а значит, что и внешние Go-библиотеки для реализации и модульного тестирования были взяты те же, что и в нашем самописном фреймворке. Упоминания стоят следующие библиотеки, фундаментально повлиявшие на ход разработки как Camshare-сервисов в целом, так и NATter’а в частности:</p><ul><li><em>nats-io/nats.go</em> – клиент, используемый для реализации работы с NATS;</li><li><em>Shopify/sarama</em> – клиент, используемый для реализации работы с Apache Kafka;</li><li><em>go-chi/chi</em> – HTTP-роутер, используемый для реализации REST API;</li><li><em>golang/protobuf</em> – библиотека для работы с Protocol Buffers, используемая для сериализации сообщений в пакеты;</li><li><em>stretchr/testify</em> – библиотека, предоставляющая инструменты для тестирования кода: assert’ы, mock’и, suite’ы. </li></ul><p>Несмотря на то что модули внутренней библиотеки уже были готовы к использованию, их перенос непосредственно в исходный код NATter’а требовал склеивания логики пакетов из библиотеки, реализующих работу с протоколами, с логикой соответствующих пакетов в самом сервисе во избежание наличия лишних уровней абстракции.</p><h2 id="--5">Заключение</h2><p>NATter позволяет относительно легко интегрировать системы, работающие с разными сетевыми протоколами, что очень полезно при перестройке проектов на микросервисную архитектуру, когда отдельные сервисы разрабатываются постепенно и требуют интеграции по мере своей готовности.</p><p>Исходный код NATter’а уже загружен в <a href="https://github.com/tomsksoft-llc/NATter">публичный репозиторий</a>, и в скором времени ожидается первый релиз.</p>]]></content:encoded></item><item><title><![CDATA[Тонкости проектирования SDK на C++ при помощи динамических библиотек]]></title><description><![CDATA[Проектирование C++ SDK в виде - лучшие практики, проблемы совместимости (API vs ABI), построение переносимых интерфейсов, поддержка бинарной совместимости.]]></description><link>https://blog.tomsksoft.ru/cpp_sdk_creation/</link><guid isPermaLink="false">612f645845f9e45e497896f5</guid><category><![CDATA[C++]]></category><category><![CDATA[SDK]]></category><category><![CDATA[DLL]]></category><dc:creator><![CDATA[Roman Meita]]></dc:creator><pubDate>Fri, 10 Sep 2021 10:29:34 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2021/09/toolbox-1.jpg" medium="image"/><content:encoded><![CDATA[<h1 id="">Введение</h1>
<img src="https://blog.tomsksoft.ru/content/images/2021/09/toolbox-1.jpg" alt="Тонкости проектирования SDK на C++ при помощи динамических библиотек"><p>Иногда случается так, что в рамках разработки программного продукта становится очевидно, что часть его функционала самодостаточна и может распространяться отдельно. В этот момент появляется необходимость создания <a href="https://en.wikipedia.org/wiki/Software_development_kit">SDK</a> инкапсулирующего в себе отдельные технологические наработки. В контексте разработки на языке C++, решение этой задачи часто приводит к появлению динамической библиотеки, предоставляющей доступ к реализации определенных функциональных возможностей, и распространяющейся как отдельный продукт. В этой статье мы рассмотрим тонкости реализации описанного сценария.</p>
<h1 id="">О библиотеках и местах их обитания</h1>
<p>Для того, чтобы понять как распространять обособленный набор функций в виде отдельного продукта, сначала нужно разобраться с тем, как работают библиотеки. Не зависимо от целевой операционной системы, существуют два основных типа библиотек: <strong>статические</strong> и <strong>динамические</strong>.</p>
<figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/0.png" class="kg-image" alt="Тонкости проектирования SDK на C++ при помощи динамических библиотек"></figure><h2 id="">Статические библиотеки</h2>
<p>Статические библиотеки по своей сути являются самым прямым воплощением идеи библиотек. В процессе сборки компилятор, проходя по всем файлам проекта, создает объектные файлы, содержащие код и данные для каждой <a href="https://en.wikipedia.org/wiki/Translation_unit_(programming)">единицы трансляции</a>. Статические библиотеки появились из идеи, что можно разделять код путем повторного использования общих объектных файлов. Такие библиотеки иногда называют «архивами», поскольку фактически это просто набор упакованных объектных файлов. Так в UNIX-подобных операционных системах они обладают расширением *.a от слова archive. В Windows статические библиотеки обычно имеют расширение *.lib, хотя такое же расширение можно заметить и у файлов с информацией об экспортируемых символах динамических библиотек, что вносит некоторую путаницу.</p>
<p>В процессе сборки программы компоновщик объединяет переданные ему компилятором объектные файлы, ведя список <a href="https://developer.ibm.com/articles/au-aix-symbol-visibility/">символов</a>, для которых пока не нашлось части кода с реализацией. Когда предоставленный ему компилятором перечень объектных файлов заканчивается, компоновщик начинает поиск недостающих символов в переданных на линковку библиотеках. Если еще не определенный символ находится в одном из объектов статической библиотеки, тогда этот объект добавляется к исполняемому файлу. Следует обратить внимание на то, что найденный объектный файл добавляется в программу целиком, что может повлечь за собой пополнение списка неразрешенных символов. Процесс сопоставления символа и кода его реализации на этапе компиляции также называется <strong>ранним связыванием</strong>.</p>
<figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/1-2.png" class="kg-image" alt="Тонкости проектирования SDK на C++ при помощи динамических библиотек"></figure><p>Преимуществом использования статических библиотек является простота распространения приложения - на выходе получается один исполняемый файл с нужным кодом внутри. Причем, в процессе линковки из статической библиотеки будут подключены только те объектные файлы, код либо данные которых используются в приложении.</p>
<p>Однако, использование статических библиотек налагает на разработчиков определенные ограничения, часто не подходящие для реализации SDK. В том числе:</p>
<ol>
<li>При обновлении версии библиотеки необходима перекомпиляция приложения.</li>
<li>Статическая библиотека, написанная на C++, не может свободно распространяться в виде скомпилированного файла из-за проблемы отсутствия стандартизированного <a href="https://en.wikipedia.org/wiki/Application_binary_interface">ABI</a> (о чем будет рассказано ниже). Распространение библиотеки в виде исходных файлов не всегда желательно.</li>
<li>Поскольку код статической библиотеки включается напрямую в исполняемый файл, он не может быть разделен с другими приложениями, использующими ту же библиотеку. Это может приводить к ситуациям, когда один и тот же код загружается в память многократно.</li>
</ol>
<h2 id="">Динамические библиотеки</h2>
<p>Развитием идеи разделения общего кода между независимыми приложениями стало появление динамических библиотек. Например, реализация системных либо широко используемых библиотек, таких как стандартная библиотека C++, в виде статических библиотек чрезвычайно накладна — массовое дублирование одинаковых частей кода среди всех приложений в системе привело бы к сокращению доступного дискового пространства и оперативной памяти. Кроме того, необходимость исправления реализации одной из функций в такой библиотеке означала бы неизбежную пересборку всех зависимых приложений.</p>
<p>Динамические библиотеки спроектированы для решения таких проблем. Обычно они имеют расширение *.dll в Windows, *.so в UNIX, *.dylib в macOS. В процессе сборки программы компоновщик, обнаруживая символ из разделяемой библиотеки, не включает его определение в исполняемый файл. Вместо этого, фиксируется имя символа и библиотеки, в которой он должен быть реализован. При работе процесса, операционная система просматривает список зависимостей, подгружая в память либо используя уже загруженный код нужных динамических библиотек. Разрешение символов в процессе работы приложения называется <strong>поздним связыванием</strong>.</p>
<figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/2-1.png" class="kg-image" alt="Тонкости проектирования SDK на C++ при помощи динамических библиотек"></figure><p>Это означает, что ни один исполняемый файл, зависящий от динамической библиотеки, не содержит объектных файлов с кодом этой библиотеки. При необходимости обновить или исправить реализацию, например, функции, предоставляемой библиотекой, необходимо заменить лишь файл библиотеки без перекомпиляции зависимых приложений.</p>
<p>Другие преимущества использования динамических библиотек:</p>
<ol>
<li>Экономия как дисковой, так и оперативной памяти за счет использования одного и того же кода.</li>
<li>Доступ к общим ресурсам, разделяемым между всеми приложениями (строки, константы, изображения).</li>
<li>Ускорение сборки приложения. Нет необходимости включать код динамической библиотеки в исполняемый файл.</li>
</ol>
<p>Основные недостатки:</p>
<ol>
<li>Появляется необходимость поддержки обратной совместимости и версионирования библиотек.</li>
<li>Необходимость предоставлять динамические библиотеки при развертывании приложения. Приложение не запустится, если в системе нет нужной библиотеки.</li>
<li>Многочисленные проблемы связности библиотек, которые иногда обобщенно называют <a href="https://en.wikipedia.org/wiki/Dependency_hell">Dependency Hell</a>. Зачастую проявляются в виде перекрестных зависимостей от разных версий одной и той же библиотеки.</li>
</ol>
<p>Есть два способа подключения динамических библиотек к пользовательскому приложению:</p>
<ol>
<li>Динамическая линковка</li>
<li>Динамическая загрузка</li>
</ol>
<p>При <strong>динамической линковке</strong> библиотека инициализируется в момент старта процесса. Всю работу по сопоставлению символов с адресами в памяти берет на себя операционная система, а точнее специальная программа-загрузчик.</p>
<p>В случае <strong>динамической загрузки</strong> процесс во время своей работы явно вызывает загрузку библиотеки при помощи вызовов API операционной системы, таких как <strong><a href="https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibrarya">LoadLibrary()</a></strong> в Windows и <strong><a href="https://man7.org/linux/man-pages/man3/dlopen.3.html">dlopen()</a></strong> в Unix. В этом случае не происходит автоматического связывания символов с фактическими адресами в коде загруженной библиотеки. Процесс должен сам осуществить поиск нужного символа в памяти при помощи таких функций как <strong><a href="https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getprocaddress">GetProcAddress()</a></strong> или <strong><a href="https://man7.org/linux/man-pages/man3/dlsym.3.html">dlsym()</a></strong>.</p>
<p>Динамическая загрузка — повторяющийся паттерн в приложениях, которые поддерживают плагинную систему.</p>
<h2 id="">Плагины</h2>
<p>По своей сути плагины являются технологией, позволяющий в определенных пределах изменять функциональные возможности приложения без необходимости внесения изменений в его программный код. Например, практически все современные веб-браузеры используют плагины для реализации поддержки разных форматов мультимедии, в том числе видео, аудио и графических изображений. Поскольку при динамической загрузке библиотеки нет необходимости ее наличия на этапе компиляции, а интерфейс плагина может быть стандартизирован, становится достаточно легко создать подсистему приложения, которая была бы способна подключать любое количество динамических библиотек из определенного каталога, реализующих дополнительную функциональность.</p>
<p>На этом этапе мы подходим к двум важным темам — программный и двоичный интерфейс приложений, называемые так же API и ABI.</p>
<h1 id="apiabic">Об API, ABI и языке C</h1>
<p>Чтобы понять, как наши библиотеки взаимодействуют с кодом пользовательского приложения, нужно разобраться, что такое API и ABI.</p>
<h2 id="apiapplicationprograminginterface">API - Application Programing Interface</h2>
<p>В широком смысле API – это протокол, который описывает, как можно использовать данный программный код. В контексте C++ API – это набор публичных типов, функций, переменных, открытых для внешнего кода из приложения или библиотеки. Вместе с библиотекой обычно поставляется заголовочные файлы *.h, которые и являются описанием её API.</p>
<h2 id="abiapplicationbinaryinterface">ABI - Application Binary Interface</h2>
<p>В C++ под термином ABI понимают детали реализации языковых конструкций на двоичном уровне, не специфицированных в стандарте языка. Стандарт C++ описывает общую модель языка, но не указывает на то, какие подходы использовать для её реализации. В качестве примера можно привести виртуальные функции. В стандарте есть описание того, какое поведение ожидается от виртуальных функций, но нет информации о том, как его реализовать. Расположение классов и их виртуальных таблиц в памяти, способ передачи параметров в функции (через стек или регистр), реализация возврата значения из функции, механизм исключений — все это описывается в ABI. Если два компилятора на одной платформе реализуют разные ABI, то их код будет несовместим на бинарном уровне. Иногда ABI изменяется даже между разными версиями одного компилятора.</p>
<h3 id="namemangling">Name Mangling</h3>
<p>Стоит отдельно упомянуть такую особенность ABI, как <a href="https://www.ibm.com/docs/en/i/7.1?topic=linkage-name-mangling-c-only">Name Mangling</a>. Методы и данные в приложениях на языке C++ имеют внутренние или декорированные имена, отличные от их имен в исходном коде. <strong>Декорированное имя</strong> — это символьная строка, в которой вместе с именем объекта закодирована дополнительная информация о типе, параметрах, соглашении о вызовах и других полезных для компилятора подсказках, помогающих найти правильный код функции.</p>
<p>Поскольку для языка C++ нет общепринятого ABI, при динамической загрузке библиотеки может возникнуть проблема поиска нужных символов из-за декорирования имен. Однако здесь на помощь приходит <strong>язык C</strong>. Директива препроцессора extern &quot;C&quot; позволяет отключить декорирование имени для экспортируемых интерфейсов библиотеки, так как если бы они были написаны на C.</p>
<p>Пример Name Mangling:</p>
<figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/mangling.jpg" class="kg-image" alt="Тонкости проектирования SDK на C++ при помощи динамических библиотек"></figure><h3 id="callingconvention">Calling Convention</h3>
<p>Чтобы иметь возможность вызвать функцию из нашего SDK не достаточно знать одно лишь её имя. Такие важные особенности работы функций, как способ передачи параметров (через стек или через регистры), порядок их размещения и механизм реализации возвращенаемого значения, могут отличатся между разными компиляторами. В общем случае эти детали объединяют под термином <a href="https://software.intel.com/content/www/us/en/develop/documentation/cpp-compiler-developer-guide-and-reference/top/compiler-reference/c-c-calling-conventions.html">Calling Convention</a>.</p>
<p>К примеру, вот некоторые известные соглашения о вызове функций и их краткое описание:</p>
<table>
		<tbody><tr>
			<th>Название </th>
			<th>Кто очищает стек </th>
			<th>Передача параметров </th>
		</tr>
		<tr>
			<td> __stdcall </td>
			<td> Вызывающий код </td>
			<td> Через стек в обратном порядке (слева направо) </td>
		</tr>
        <tr>
			<td> __fastcall </td>
			<td> Вызывающий код </td>
			<td> Первые два через регистры, остальные - в стеке. Все слева направо </td>
		</tr>
        <tr>
			<td> __cdecl </td>
			<td> Вызывающий код </td>
			<td> Через стек в обратном порядке (слева направо) </td>
		</tr>
        <tr>
			<td> __thiscall </td>
			<td> Вызывающий код </td>
			<td> Через стек справа налево. Указатель this сохраняется в регистре ecx. </td>
		</tr>
	</tbody>
</table><h3 id="">Заметка об исключениях</h3>
<p>Исключения, при их наличии, не должны покидать кода библиотеки. Реализация механизма исключения отличается между компиляторами и так же является частью ABI C++. Если функция в процессе своей работы должна сообщить об ошибке, она должна сделать это через возвращаемое значение.</p>
<h1 id="">Об экспортируемых и импортируемых символах</h1>
<p>Общие принципы работы разделяемых библиотек одинаковы вне зависимости от платформы (Windows, Linux, macOS). Однако так же существуют и некоторые существенные различия.</p>
<h2 id="">Экспортируемые символы</h2>
<p>Основное отличие заключается в том факте, что в библиотеках, собранных в UNIX-подобных системах, все символы всех объектных файлов библиотеки экспортируются автоматически, если они не запрещены для экспорта путем использования модификатора static. В Windows символы не экспортируются, если они не открыты для доступа явно.</p>
<p>Существует три способа экспортировать символ из *.dll:</p>
<ol>
<li>При помощи директивы <a href="https://docs.microsoft.com/en-us/cpp/build/exporting-from-a-dll-using-declspec-dllexport?view=msvc-160"><strong>__declspec(dllexport)</strong></a></li>
<li>При помощи опции компоновщика <a href="https://docs.microsoft.com/en-us/cpp/build/reference/export-exports-a-function?view=msvc-160"><strong>export:symbol</strong></a></li>
<li>При помощи специального <a href="https://docs.microsoft.com/en-us/cpp/build/exporting-from-a-dll-using-def-files?view=msvc-160"><strong>*.def</strong></a> файла, содержащего символы для экспортирования.</li>
</ol>
<h2 id="">Импортируемые символы</h2>
<p>Кроме необходимости для динамических библиотек в Windows явно определять экспортируемые символы, разрешается так же явно указывать символы, подлежащие импорту в приложении. Это не обязательная процедура, но она указана как способствующая оптимизации скорости работы с DLL.</p>
<p>Распространенный подход решения проблем с экпортом/импортов нужных символов —  использование макросов препроцессора. Пример:</p>
<figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/defines-2.jpg" class="kg-image" alt="Тонкости проектирования SDK на C++ при помощи динамических библиотек"></figure><p>При помощи введенной нами переменной препроцессора <strong>SDK_EXPORT_SYMBOLS</strong>, определенной в исходном коде библиотеки, гарантируется, что нужные символы будут экспортированы. В клиентском коде, включающем этот заголовочный файл, символы будут отмечены как импортируемые. Переменная <strong>SDK_CALL</strong> здесь используется для указания соглашения о вызовах.</p>
<h2 id="dll">Дополнительные файлы DLL</h2>
<p>Еще один уровень сложности при работе с библиотеками Windows связан с тем фактом, что дополнительная информация о DLL (как, например, информация об экспортируемых символах) хранится в отдельных файлах. Среди них:</p>
<ul>
<li>*.lib – файл импорта библиотеки, описывающий какой и где символ хранится в DLL. Создается только если библиотека экспортирует символы. При динамической линковке исполняемый файл, использующий DLL, обращается *.lib файлу для поиска символов.</li>
<li>*.exp -  файл с информацией, необходимой для разрешения циклических зависимостей библиотек.</li>
<li>*.ilk — файл с информацией о статусе инкрементной компоновки.</li>
<li>*.def -  файл определений, позволяющий управлять деталями скомпонованной библиотеки, включая экспорт символов.</li>
<li>*.res - файл ресурсов, используемых библиотекой.</li>
</ul>
<p>В этом DLL отличнается от динамических библиотек в системах UNIX, где вся перечисленная информация обычно добавляется в сам файл библиотеки.</p>
<p>В случае плагинной реализации SDK мы избавляемся от необходимости использования дополнительных зависимостей при помощи динамической загрузки и явного поиска символов.</p>
<h1 id="">О стандартном подходе к построению переносимых интерфейсов</h1>
<p>Как мы уже успели увидеть, отсутствие общепринятого ABI делает неудобным использование компонентов стандартной библиотеки C++ в открытых интерфейсах. В случае различий в реализации ABI для компилятора, используемого для сборки клиентского приложения, при использовании библиотеки может возникнуть неопределенное поведение.</p>
<p>Классическим подходом к построению переносимых интерфейсов является проектирование API библиотеки при помощи функций и типов языка C. В этом случае при необходимости произведения инициализации/деинициализации библиотеки вызываются соотвествующие функции. Еще одной необязательной особенностью подхода в стиле C является добавление к именам функций префикса, ассоциируемого с названием библиотеки. Это происходит из-за отсутствия возможности реализации namespace без декорирования имен в стиле C++. При необходимости оперирования объектами, создаваемыми библиотекой и хранящими в себе какое-то состояние, используется подход передачи непрозрачного указателя (хендлера) или структуры в вызовы функций из библиотеки.</p>
<p>Пример простого интерфейса:</p>
<figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/simple_example-1.jpg" class="kg-image" alt="Тонкости проектирования SDK на C++ при помощи динамических библиотек"></figure><p>Такой подход позволяет не заботиться о совместимости ABI между приложением и библиотекой. Важным моментом здесь является выделение из кодовой базы небольших и самодостаточных процедур в интерфейсные функции библиотеки, что сделает удобным как использование её внешним кодом, так и возможное расширение списка доступных функций в будущем.</p>
<h2 id="">Преимущества и недостатки</h2>
<p>К преимуществам использования внешнего интерфейса SDK в стиле C можно отнести следующие пункты:</p>
<ul>
<li>Множество современных языков программирования поддерживают работу с библиотеками языка C</li>
<li>Зависимости от реализации стандартной библиотеки для SDK и клиентского кода разделены. Поскольку выделение и освобождение памяти происходит непосредственно в самом SDK, нет возможности утечек либо неправильного освобождения памяти программы.</li>
</ul>
<p>В недостатки можно записать следующее:</p>
<ul>
<li>Клиентский код сам отвечает за правильную последовательность вызовов функций SDK. Например, если для работы библиотеки необходима первоначальная инициализация, нужная функция должная быть вызвана во внешнем коде. То же самое относится к освобождению ресурсов при окончании работы с библиотекой.</li>
<li>Необходимо явно вызывать функции создания и удаления объектов.</li>
</ul>
<h1 id="">О расширенном подходе к построению переносимых интерфейсов</h1>
<p>Из-за своих ограничений, подход в стиле C при проектировании интерфейса SDK может оказаться недостаточно гибким и удобным, как при работе с объектно-ориентированными компонентами стандартной библиотеки C++. Альтернативой может являться использование интерфейсных классов в составе API.</p>
<p>Интерфейсный класс — это класс, не имеющий данных и состоящий из чисто виртуальных функций. В случае использования интерфейсных классов может быть реализована следующая схема: библиотека предоставляет открытую функцию, которая создает и возвращает указатель на интерфейсный класс-фабрику, с помощью которого создаются экземпляры других классов. В коде библиотеки классы-реализации наследуются от интерфейсов, оставаясь полностью скрытыми и не зависимыми от клиентского кода. В данном подходе необходимо явно объявить только одну точку экспорта — функцию-фабрику. Весть остальной API библиотеки становится доступным за счет таблиц виртуальных функций. Такая схема помогает реализовать гибкие интерфейсы, менее ограниченные, чем при классическом подходе с использованием функций в стиле C. Тут нужно сделать замечание, что использование интерфейсных классов не освобождает от необходимости избегать применения в их методах компонентов стандартной библиотеки C++.</p>
<figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/modern_example-2.jpg" class="kg-image" alt="Тонкости проектирования SDK на C++ при помощи динамических библиотек"></figure><p>Несмотря на то, что применение интерфейсных классов позволяет использовать преимущества объектно-ориентированного подхода, для корректного проектирования SDK нужно помнить о еще одном важном ограничении: память, выделенная библиотекой под один из своих объектов, должна быть симметрично освобождена при помощи соответствующего кода из библиотеки. Это связано с тем фактом, что реализация работы операторов <strong>new()</strong> и <strong>delete()</strong> относится к ABI C++. В общем случае это правило можно сформулировать следующим образом: произведение любых операций в клиентском коде над объектом, созданным библиотекой, безопасно до тех пор, пока эти действия не зависят от реализации стандартной библиотеки.</p>
<p>Таким образом, проблема заключается в необходимости контролировать удаление объектов SDK. Здесь обычно используется один из двух подходов:</p>
<ol>
<li>В интерфейсные классы всех объектов библиотеки добавляется метод для явного удаления объекта (например, <strong>release()</strong>)</li>
<li>Указатель на объект должен быть передан специальной функции или методу библиотеки, отвечающей за освобождение памяти соответствующих объектов.</li>
</ol>
<p>При выборе первого подхода, в клиентском приложении можно избежать дополнительного контроля над освобождением памяти при помощи оборачивания указателей на объекты SDK в умные указатели с указанием функции для освобождения памяти, ссылающейся на методы <strong>release()</strong>.</p>
<h2 id="">Почему это работает</h2>
<p>Интерфейсный класс, не содержащий в себе данных и функций, представляет собой всего лишь виртуальную таблицу, то есть набор указателей на функции. Этим указателям присваиваются адреса реальных функций из кода библиотеки. Внешний код, работая с интерфейсным классом, вызывает соответствующую имплементацию функции из SDK. Почему такой подход работает для кода всех наиболее значимых компиляторов? Следует напомнить, что таблица виртуальных функций не является единственно возможным вариантом реализации работы виртуальных функций для языка C++.</p>
<p>Ответом является история распространения поддержки технологии <a href="https://en.wikipedia.org/wiki/Component_Object_Model">COM</a>. Если не вдаваться в детали, спецификация технологии COM хорошо подходила для реализации при помощи таблицы виртуальных функций. Популяризация технологии заставила разработчиков компиляторов добавить её поддержку, сделав использование таблицы виртуальных функций фактически стандартом.</p>
<h2 id="">Преимущества и недостатки</h2>
<p>Использование интерфейсных классов в SDK дает следующие преимущества:</p>
<ul>
<li>Объектно-ориентированный подход к работе с абстракциями SDK.</li>
<li>При следовании правилу ограничения работы с памятью внутри SDK, библиотека и клиентский код независимы друг от друга и от релизации стандартной библиотеки C++.</li>
</ul>
<p>Основные недостатки:</p>
<ul>
<li>Необходимо тщательно контролировать удаление объектов, созданных библиотекой.</li>
<li>Методы интерфейсных классов все еще не могут работать со стандартными типами и коллекциями языка С++.</li>
</ul>
<h1 id="">Поддержка бинарной совместимости</h1>
<p>В заключении рассмотрим те изменения в коде библиотеки SDK, которые требуют или не требуют пересборку клиентского кода.</p>
<p>Что можно менять в SDK без потери бинарной совместимости с клиентским кодом:</p>
<ul>
<li>Добавлять новые функции.</li>
<li>Добавлять новые виртуальные методы в конец объявления класса, не имеющего наследников.</li>
<li>Добавлять новые интерфейсные классы.</li>
<li>Как угодно изменять классы и функции, не являющимися частью API (то есть не экспортируемыми во внешний код).</li>
</ul>
<p>Какие изменения приведут к перекомпиляции с клиентским приложением:</p>
<ul>
<li>Удаление существующих классов и функций, являющихся частью API.</li>
<li>Удаление существующих виртуальных методов.</li>
<li>Изменение иерархии интерфейсных классов (добавление или удаление наследования).</li>
<li>Изменение сигнатуры функций и методов.</li>
<li>Добавление новых виртуальных методов в середину класса.</li>
<li>Добавление виртуальных методов в класс, имеющий наследников.</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Введение в стэк ELK с использованием Laravel]]></title><description><![CDATA[Начало работы с ELK из Laravel-я, пример с отображением общей статистики с сайта/приложения. ]]></description><link>https://blog.tomsksoft.ru/vviedieniie-v-stek-elk-s-ispolzovaniiem-laravel/</link><guid isPermaLink="false">613064a945f9e45e49789703</guid><dc:creator><![CDATA[Dmitrii Pleshanov]]></dc:creator><pubDate>Thu, 02 Sep 2021 05:47:17 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2021/09/a2d09b19-how-to-install-elasticsearch-and-kibana-on-ubuntu-18.04.jpg" medium="image"/><content:encoded><![CDATA[<h2 id="-">Введение</h2><img src="https://blog.tomsksoft.ru/content/images/2021/09/a2d09b19-how-to-install-elasticsearch-and-kibana-on-ubuntu-18.04.jpg" alt="Введение в стэк ELK с использованием Laravel"><p>ELK — это аббревиатура из названий трех продуктов: Elasticsearch, Logstash и Kibana. </p><p>Logstash — инструмент, который необходим для приема первичных данных из .log файлов и иных представлений. Посредством его работы информацию можно преобразовать и отправить в общее хранилище. В наших примерах и микросервисах данные модифицируются посредство базового кода, поэтому не требовалось использование этого сервиса.  </p><p>Elasticsearch — это механизм индексирования и хранения полученной информации, а также полнотекстового поиска по ней. Он является NoSQL решением, главная задача которого — организация быстрого и гибкого поиска по полученным данным. Работа с информацией происходит с помощью REST API, который позволяет добавлять, просматривать, модифицировать и удалять данные.</p><p>Можно выделить основное устройство Elasticsearch и провести косвенную аналогию с реляционными бд:</p><ul><li>Индекс - база данных</li><li>Документ - таблица базы данных</li><li>Элемент - запись в таблице</li></ul><p>Структуру каждого индекса и содержимое его документов задается с помощью мэппинга.</p><p>Kibana — это интерфейс для Elasticsearch, который имеет большое количество возможностей по поиску данных в индексах Elasticsearch и отображению этих данных в удобочитаемых видах таблиц, графиков и диаграмм, карт.</p><h2 id="-elasticseach">Работа с Elasticseach</h2><p>Elasticsearch может быть развернут на локальном сервере с помощью системных администраторов или настроен в автоматическом режиме на одном из серверов https://www.elastic.co/ </p><p>После базовой настройки можно приступить к написанию мэппинга и созданию необходимых индексов.</p><p>Эту задачу можно выполнить несколькими путями: </p><ul><li>написание команд из консоли Elasticsearch - документация <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html</a></li><li>использование php библиотеки для написание собственного API - документация<br><a href="https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/installation.html">https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/installation.html</a></li></ul><p>В данной статьей рассмотрим второй вариант. После установки пакета </p><pre><code>composer install elasticsearch/elasticsearch</code></pre><p>можно приступить к написанию кода с использованием основ, описанных в документации.</p><p>Например, с помощью данного сурдокода будет создан индекс, после чего к нему будет применен описанный мэппинг:</p><pre><code>$params = [
    'index' =&gt; $index_name
];

$response = $this-&gt;es_api-&gt;indices()-&gt;create($params);

-------------------------------------------------------

$params = [
    'index' =&gt; $index_name,
    'body' =&gt; [
        'properties' =&gt; $properties
    ]
];

return $this-&gt;es_api-&gt;indices()-&gt;putMapping($params);
Мэппинг может быть описан как массив с указанием необходимых полей внутри документа:</code></pre><p>Мэппинг может быть описан как массив с указанием необходимых полей внутри документа:</p><pre><code>"properties" =&gt; [
    "appVersion" =&gt;  [
        "type" =&gt; "keyword",
    ],
    "clientApplication" =&gt;  [
        "type" =&gt; "keyword",
    ],
    "uid" =&gt;  [
        "type" =&gt; "keyword",
    ],
    "ip" =&gt;  [
        "type" =&gt; "ip",
    ],
    "usage_date" =&gt; [
        "type" =&gt; "date",
        "format"  =&gt; "yyyy-MM-dd HH:mm:ss"
    ],
    "os" =&gt;  [
        "type" =&gt; "keyword",
    ]
]</code></pre><p>Таким образом мы создали индекс и описали структуру его документа.<br>После можно приступить к отправке данных и созданию элементов в elasticsearch.  </p><p>Пример кода:</p><pre><code>$this-&gt;elastic_search
     -&gt;createBulk(ElasticSearch::MAIN_INFO_INDEX, $main_info);
-----------------------------------------------------------------
public function createBulk($index_name, $elements) {

    $params = [];

    foreach ($elements as $element) {
        $params['body'][] = [
            'index' =&gt; [
                '_index' =&gt; $index_name,
            ]
        ];

        $params['body'][]   = $element;
    }

    if (!empty($params)) {
        try {
            $responses = $this-&gt;es_api-&gt;bulk($params);
            return $responses;
        } catch (Exception $e) {
            Log::channel("elastic_search_errors")-&gt;error("Exception when calling es_api-&gt;bulk: ". $e-&gt;getMessage());
        }
    }
}
</code></pre><p>Данные в соответствии с мэппингом будут отправлены в Elasticsearch и готовы для дальнейшей работы с ними.</p><h2 id="-kibana">Введение в работу Kibana</h2><p>Из личного кабинета <a href="https://www.elastic.co/">https://www.elastic.co/</a> вы сможете перейти в интерфейс Kibana.<br>Для создания необходимых визуализаций необходимо привязать сервис к индексам из Elasticsearch.</p><p>Для этого из соответствующего раздела настроек потребуется создать индекс-паттерн, который и будет отслеживать механизм Kibana.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/image-1.png" class="kg-image" alt="Введение в стэк ELK с использованием Laravel"><figcaption>Созданный индекс-паттерн main_info</figcaption></figure><p>В разделе визуализаций возможно построение разных представлений данных.Самое распространенное Lens (содержит множество различных диаграмм и графиков) и Maps (карты и наложенные на них данные). </p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/image-2.png" class="kg-image" alt="Введение в стэк ELK с использованием Laravel"><figcaption>Выбор типа визуализации</figcaption></figure><p>Интерфейс Kibana дружественен к пользователю и предлагает интуитивно понятное взаимодействие с данными. </p><p>В левой части содержится выбранный индекс и структура его документа. Центральная и правая части отводятся для отображения представлений информации и различных форм её агрегирования. </p><p>В верхней панели находится строка ввода для написания простых KQL (kibana query language) запросов, которая автоматически подсказывает доступные варианты.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/09/image-3.png" class="kg-image" alt="Введение в стэк ELK с использованием Laravel"><figcaption>Интерфейс создания визуализации</figcaption></figure><p>После создания визуализации, её можно сохранить и разместить с другими на одном из дашбордов для удобного просмотра всех представлений данных.</p><h2 id="--1">Заключение</h2><p>Таким образом, посредством изучения документации Elasticsearch PHP API и сервису, написанному с использованием фреймворка Laravel, возможен перенос большого количества данных в систему Elasticsearch, которая представит высокую скорость работы в сравнении с реляционной бд.</p><p>Kibana в свою очередь поможет представить информацию в удобном и понятно виде для последующего анализа.</p>]]></content:encoded></item><item><title><![CDATA[Базовая настройка докер для развертывания веб-приложений на примере фреймворка Laravel]]></title><description><![CDATA[Docker упрощает запуск и развертывание приложений, в основе образы и виртуальные контейнеры. Быстрый старт для локальной разработки проекта в Docker.]]></description><link>https://blog.tomsksoft.ru/bazovaia-nastroika-dokier-dlia-razviertyvaniia-vieb-prilozhienii-na-primierie-frieimvorka-laravel/</link><guid isPermaLink="false">6130613e45f9e45e49789701</guid><dc:creator><![CDATA[Dmitrii Pleshanov]]></dc:creator><pubDate>Thu, 02 Sep 2021 05:42:21 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2021/09/docker-banner2.jpg" medium="image"/><content:encoded><![CDATA[<h2 id="-">Введение</h2><img src="https://blog.tomsksoft.ru/content/images/2021/09/docker-banner2.jpg" alt="Базовая настройка докер для развертывания веб-приложений на примере фреймворка Laravel"><p>Docker упрощает запуск и развертывание приложений, основой его работы являются образы и виртуальные контейнеры.</p><p>Разверните свежее приложение Laravel и настройте его для базовой работы.</p><p>Для запуска его в виртуальном контейнере нам потребуется установленный Docker на рабочей машине. </p><p>После его установки приступим к настройке. </p><h2 id="dockerfile">Dockerfile</h2><p>Docker позволяет задавать описание настройки среды внутри отдельных контейнеров с помощью файла Dockerfile.</p><p>Создайте файл Dockerfile в корне проекта laravel. Данная конфигурация будет использоваться для создания образа приложения, установки всех необходимых зависимостей приложения. </p><p>Пример Dockerfile для локальной разработки проекта:</p><pre><code>FROM php:7.4-fpm

# Set working directory
WORKDIR /var/www

# Install dependencies
RUN apt-get update &amp;&amp; apt-get install -y \
    build-essential \
    libpng-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    locales \
    zip \
    jpegoptim optipng pngquant gifsicle \
    vim \
    unzip \
    git \
    curl \
    libonig-dev \
    locales \
    nodejs \
    npm \
    zlib1g-dev \
    libicu-dev \
    supervisor \
    g++ \
    --no-install-recommends \
    &amp;&amp; rm -r /var/lib/apt/lists/* \
    &amp;&amp; sed -i 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen \
    &amp;&amp; locale-gen

# Clear cache
RUN apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

# Install extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl intl bcmath gd
RUN docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/
RUN docker-php-ext-install gd

# Install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Install NPM
RUN curl https://www.npmjs.com/install.sh | sh

# Add user for laravel application
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www

# Change current user to www
USER www

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]</code></pre><p>Данная конфигурация не потребует много времени на выполнение, но так же и не сделает вещи, которые необходимы для стабильной работы в режиме production.</p><p>Поэтому так же рассмотрим вторую версию. </p><p>Пример Dockerfile для развертывания проекта на боевом сервере:</p><pre><code>FROM php:7.4-fpm

# Set working directory
WORKDIR /var/www

# Install dependencies
RUN apt-get update &amp;&amp; apt-get install -y \
    wget \
    cmake \
    gcc \
    build-essential \
    libpng-dev \
    libjpeg62-turbo-dev \
    libfreetype6-dev \
    locales \
    zip \
    jpegoptim optipng pngquant gifsicle \
    vim \
    unzip \
    git \
    curl \
    libonig-dev \
    locales \
    nodejs \
    npm \
    zlib1g-dev \
    libicu-dev \
    supervisor \
    g++ \
    --no-install-recommends \
    &amp;&amp; rm -r /var/lib/apt/lists/* \
    &amp;&amp; sed -i 's/# ru_RU.UTF-8 UTF-8/ru_RU.UTF-8 UTF-8/' /etc/locale.gen \
    &amp;&amp; locale-gen

# Clear cache
RUN apt-get clean &amp;&amp; rm -rf /var/lib/apt/lists/*

# Install extensions
RUN docker-php-ext-install pdo_mysql mbstring exif pcntl intl bcmath gd
RUN docker-php-ext-configure gd --with-freetype=/usr/include/ --with-jpeg=/usr/include/
RUN docker-php-ext-install gd

# Install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Install NPM
RUN curl https://www.npmjs.com/install.sh | sh

# Add user for laravel application
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www

# Copy existing application directory permissions
COPY --chown=www:www . /var/www

RUN npm install &amp;&amp; \
    composer install  &amp;&amp; \
    composer dumpautoload -o

#laravel cache and optimization
RUN php artisan optimize &amp;&amp; \
    php artisan route:cache &amp;&amp; \
    php artisan config:cache &amp;&amp; \
    php artisan scribe:generate --force

#building frontend assets
RUN npm run development

# Change current user to www
USER www

# Expose port 9000 and start php-fpm server
EXPOSE 9000
CMD ["php-fpm"]</code></pre><p>Данная конфигурация смонтирует образ из базового Docker образа <strong>php:7.4-fpm</strong>.</p><p>Так же установятся указанные пакеты wget, cmake, gcc и прочие. Команда <strong>WORKDIR</strong> укажет рабочий каталог нашего приложения. </p><p>Директива <strong>RUN</strong> помогает описывать команды, которые должны быть исполнены для установки и настройки различных параметров. </p><p>Контейнеры в  Docker по умолчанию запускаются с привилегиями root, поэтому мы создаем пользователя www с ограниченными правами и указываем запуск контейнера от его имени.</p><p>Команда <strong>EXPOSE</strong> открывает порт в контейнере для php-fpm, а <strong>CMD</strong> указывает команду для запуска сервера.</p><h2 id="docker-compose">Docker-compose</h2><p>Как Dockerfile будет готов, создайте файл  docker-compose.yml с инструкциями для запуска контейнера по образу Dockerfile.</p><p>Пример docker-compose.yml:</p><pre><code>version: '3.9'
services:
    php:
        build:
           context: .
           dockerfile: Dockerfile
        image: "${ENV}-laravel-app:latest"
        container_name: "${ENV}-laravel-app"
        volumes:
            - laravel-app-volume:/var/www
            - ./storage:/var/www/storage:rw
        command: /bin/bash -c "php artisan migrate --force &amp;&amp; php-fpm"
        links:
            - mysql
    mysql:
        image: mysql:latest
        container_name: "${ENV}-laravel-mysql"
        expose:
            - "3306"
        environment:
            - MYSQL_ROOT_PASSWORD
            - MYSQL_DATABASE
            - MYSQL_USER
            - MYSQL_PASSWORD
        volumes:
            - ./mysql:/var/lib/mysql

    nginx:
        image: nginx:latest
        container_name: "${ENV}-laravel-nginx"
        volumes:
            - ./nginx/conf.d:/etc/nginx/conf.d
            - laravel-app-volume:/var/www/
            - ./storage/app/public:/var/www/public/storage:ro
            - ./nginx/logs:/var/log/nginx/
        environment:
            - PROJECT_URL
            - PROJECT_ROOT
            - DIRTY_HACK
            - VIRTUAL_HOST
            - VIRTUAL_PORT
            - LETSENCRYPT_HOST
            - LETSENCRYPT_EMAIL
        expose:
            - $VIRTUAL_PORT
        ports:
            - $VIRTUAL_PORT:$VIRTUAL_PORT

        command: /bin/bash -c "envsubst &lt; /etc/nginx/conf.d/mysite.template &gt; /etc/nginx/conf.d/my.conf &amp;&amp; nginx -g 'daemon off;'"
        links:
            - php

volumes:
    laravel-app-volume:
        name: "${ENV}-laravel-app-volume"
        external: false</code></pre><p>В приведенном коде задаются базовые элементы, запускаемые внутри контейнеров. Так это сервис nginx, mysql, php и прочее.</p><p>В каждом блоке требуется указать необходимую информацию для корректной работы сервисов. Их имена, название образов, из которых будут созданы, конфигурации, а так же связи между разделами контейнеров.</p><p><strong>Context</strong> параметра <strong>build</strong> в блоке <strong>php</strong> сообщает о том, какой каталог относительно docker-compose.yml будет использован как рабочая директория проекта. То есть мы указываем путь до нашего приложения. </p><p><strong>Dockerfile</strong> указывает название Dockerfile'а, который будет использован для создания образа. <strong>Image</strong> используется для определения  образа для запуска контейнера, если такой dockerfile не определен или образ по нему уже был смонтирован.</p><p><strong>Containername</strong> задает имя создаваемого контейнера. <strong>Command</strong> позволяет запустить указанные команда после запуска контейнера, а <strong>links</strong> используется для указания алиасов, посредством которых контейнеры могут свободно общаться между друг-другом.</p><p>С помощью <strong>environment</strong> определяются переменные из файла .env, которые будут использоваться при запуске контейнеров.</p><p>И, наконец, <strong>volumes</strong> необходим для создания тома, который обеспечит постоянное сохранения файлов, даже если работа контейнера будет остановлена. А так же указания монтируемого образа, который свяжет наши конфигурационные файлы с сервисами контейнеров.</p><h3 id="-env">.ENV </h3><p>Для работы с динамическими данными, а так же данными, которые должны храниться на сервере и не использоваться внутри Git репозиториев используются переменные из файла .env.</p><p>Его пример:</p><pre><code>ENV=dev
MYSQL_ROOT_PASSWORD=password
MYSQL_DATABASE=laravel_database
MYSQL_USER=laravel_user
MYSQL_PASSWORD=laravel_password
PROJECT_URL=localhost
PROJECT_ROOT=/var/www/public
DIRTY_HACK=$
VIRTUAL_HOST=localhost
VIRTUAL_PORT=8000</code></pre><h3 id="-nginx">Конфигурация nginx</h3><p>Так же в docker-compose.yml используются файлы для конфигурации ngnix сервиса.</p><p>Создайте в проекте папку nginx и подпапку conf.d с двумя файлами: laravel-conf.template и laravel.conf. </p><p>В первом файле мы опишем базовую структуру конфигурации ngnix и использованием переменных из .env, после чего во время создания контейнера сервис создаст файл с итоговой конфигурацией и будет его использовать для своей работы.</p><p>Пример laravel-conf.template:</p><pre><code>server {
    listen       ${VIRTUAL_PORT};

    client_max_body_size 100M;

    server_name  ${PROJECT_URL};

    access_log /var/log/nginx/${PROJECT_URL}-access.log;
    error_log /var/log/nginx/${PROJECT_URL}-error.log;

    root ${PROJECT_ROOT};

    index index.php index.html index.htm;

    location ~* ^.+\.(jpg|jpeg|gif|png|ico|css|pdf|ppt|txt|bmp|rtf|js)$ {
        #access_log off;
        #expirs 3d;
        #add_header Cache-Control "public, max-age=259200";
        add_header Cache-Control "s-maxage:259200";
    }

    location / {
        try_files ${DIRTY_HACK}uri ${DIRTY_HACK}uri/ /index.php${DIRTY_HACK}is_args${DIRTY_HACK}args;
    }

   location ~ \.php$ {
         fastcgi_param HTTPS on;
                   fastcgi_param DEVEL_SITE 0;
         try_files ${DIRTY_HACK}uri =404;
         fastcgi_split_path_info ^(.+\.php)(/.+)$;
         fastcgi_pass php:9000;
         fastcgi_index index.php;
         fastcgi_param SCRIPT_FILENAME ${DIRTY_HACK}document_root${DIRTY_HACK}fastcgi_script_name;
         include fastcgi_params;
   }
}</code></pre><h2 id="--1">Заключение</h2><p>Таким образом мы подготовили все необходимое для создания образов и контейнеров. </p><p>Выполните команду: </p><pre><code>docker-compose up -d</code></pre><p>Docker посредством содержимого docker-compose.yml создаст контейнеры из указанных в файле образов, после чего автоматически их запустит.</p><p>Флаг -d преобразует процесс в демона, с которым контейнеры остаются запущенными в фоновом режиме.</p><p>Для остановки контейнеров возможно использование команды:</p><pre><code> docker-compose down -v</code></pre>]]></content:encoded></item><item><title><![CDATA[Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах]]></title><description><![CDATA[Виртуальный фон - современные подходы, доступные датасеты, оптимизация скорости, уменьшение артефактов сегментации и дрожания, запуск на всех платформах.]]></description><link>https://blog.tomsksoft.ru/cross-platfrom-virtual-backgrund-functionality/</link><guid isPermaLink="false">6123723545f9e45e497896e7</guid><dc:creator><![CDATA[Egor Gribkov]]></dc:creator><pubDate>Mon, 23 Aug 2021 14:12:53 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2021/08/vb.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.tomsksoft.ru/content/images/2021/08/vb.jpg" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"><p><strong><strong>Введение</strong></strong></p><p>	В связи с пандемией и масштабным переходом многих сотрудников на удаленную работу резко возросла популярность мессенджеров, платформ для видеозвонков, стриминговых сервисов которые помогают организовать эффективную удаленную работу.  </p><p>	Сотрудникам пришлось осваивать технологии и подстраиваться под работу из дома, где, зачастую нет хорошего освещения (для качественного видео стриминга), красивого одноцветного фона, дети и родственники регулярно появляющиеся в кадре и отвлекающие внимание. Появилась функциональность виртуального фона (также известная как замена или размытие фона в видеозвонках, или перерождение хромакея в умный хромакей, который не требует однотонного фона), это когда автоматически в режиме реального времени в видео заменяется фон на заранее выбранную картинку или размывается чтобы не было видно что происходит за человеком. Одни из первых такую функциональность ввели компании Zoom, Skype, Google.</p><p>	Популярность росла, пользователи уже не просто интересовались, а требовали наличия виртуального фона во всех программах связанных с видеозвонкам. Собственная разработка - долго  и дорого, а также требует определенных компетенций команды, а готовых решений на рынке просто не было. В этой статье мы расскажем про путь создания собственного кроссплатформенного решения позволяющего заменять/вырезать фон в видео с веб камер в режиме реального времени.</p><p><strong><strong>Существующие подходы</strong></strong></p><p>	Традиционно для удаления фона применяли технику «зеленого экрана», когда пользователь ставит позади себя однотонное полотно, цвет которого считается фоновым. В таком случае достаточно вырезать пиксели, соответствующие основному цвету, и некоторый объем вокруг этой точки, чтобы исключить «протекание» фона из-за неравномерности освещения экрана или шумов камеры. Такое решение позволяет получить хорошую картинку, но требует от пользователя определенных усилий для подготовки места съемки. Это не будет большой проблемой для людей, регулярно ведущих трансляции, но для повседневного или рабочего общения в чатах хотелось бы обойтись вообще без подготовки.</p><p>	При удалении фона нам необходимо классифицировать каждый пиксель как “человек” или “фон”, что полностью совпадает с постановкой задачи сегментации изображения. И тут на сцену выходят свёрточные нейронные сети. С момента их дебюта на соревновании по классификации изображений ImageNet Large Scale Visual Recognition Challenge прошло много времени и им покорились многие задачи компьютерного зрения. Именно методы на основе нейронных сетей уверенно держат верхние позиции во всех бенчмарках по сегментации изображений, что делает их очевидным кандидатом для решения задачи определения фона.</p><p>	Перед нами стояло две основных задачи. Во-первых, подготовить набор данных для решения именно нашей проблемы. Во-вторых, разработать архитектуру, пригодную для запуска на достаточно слабом железе, включая ограниченные по мощности процессоры ноутбуков и мобильные устройства на Android и iOS.</p><p><strong><strong>Существующие наборы данных</strong></strong></p><p>	Для того, чтобы лучше понять требования к обучающим данным, мы изучили существующие датасеты для проблем, похожих на нашу.</p><p><strong><em>MS COCO</em></strong></p><p>	Стандартный датасет, использующийся как бенчмарк в работах по детекции и семантической сегментации изображений. Они содержит достаточно большое количество примеров с людьми, но эти изображения далеки от тех, что обычно встречаются при использовании веб-камер. Качество масок тоже оставляет желать лучшего, особенно на границах контура человека.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/08/image.png" class="kg-image" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"><figcaption>MS COCO dataset example</figcaption></figure><p><strong><em>Supervisely Person</em></strong></p><p>	Набор данных, созданный специально для определения людей на изображениях. Источником изображений послужили фотографии с фотостоков. Хотя визуальное качество масок в этом датасете достаточно высокое, исходные изображения слишком далеки от того, что обычно можно увидеть на кадрах с веб-камер: фон размыт и фокус сделан на человеке, сцена хорошо освещена, разрешение снимка высокое, шума практически нет. Обученные на таком датасете модели не подходят для решения нашей задачи.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/08/image-1.png" class="kg-image" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"><figcaption>Supervisely Person dataset example</figcaption></figure><p><strong><em>AISegment.com - Matting Human Datasets</em></strong></p><p>	Датасет для решения очень похожей на нашу проблемы - portrait matting. Основных отличий от нашей постановки два. Во-первых, вместо жесткой классификации тут требуется определить значение прозрачности в диапазоне [0; 1], что позволяет более качественно разделять фон и человека в сложных ситуациях. Например, четко определить границы волос довольно трудно и использование мягкого несколько улучшает визуальные характеристики результата. Во-вторых, авторы датасета ограничились портретными фотографиями с характерным ограничением разнообразия поз в них. В частности, обученные на этом датасете модели не могут корректно определять руки.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/08/image-2.png" class="kg-image" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"></figure><p><strong><strong>Сбор собственного набора данных</strong></strong></p><p>	После изучения датасетов и нескольких безуспешных попыток обучить на них модель с приемлемым качеством мы решили, что лучшим способом добиться результата будет обучить модель на картинках напрямую из целевого домена. Хотелось получить датасет напрямую с веб-камер, чтобы визуальные характеристики изображений, которые модель увидит во время обучения, совпадали с кадрами, на которых будет производиться вывод. Первыми “жертвами” стали сотрудники нашей компании, которых мы попросили на добровольной основе записать для нас небольшие ролики с демонстрацией различных положений.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/08/image-4.png" class="kg-image" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"></figure><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/08/image-5.png" class="kg-image" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"></figure><p>	Количества и разнообразия собственных данных не хватило для того, чтобы значительно улучшить результаты модели. Погрустив над тем, что расширить датасет путем сбора данных с реальных людей не представляется экономически возможным, мы решили обратиться к самой большой коллекции видео на Земле – YouTube.</p><p>	Мы собрали около 300 роликов из видео-блогов и записей стримов, достаточно похожих по качеству съемки на целевой домен. При сборе мы старались больше внимания уделять роликам, снятым в плохих условиях на плохие камеры, и следили, чтобы профессионально настроенные сетапы были в меньшинстве. Сбор данных и их разметка шла итеративно по мере улучшения модели, и каждый новый раунд разметки улучшал результат даже без изменения модели. На данный момент датасет состоит из примерно 12000 картинок с качественными масками.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/08/image-6.png" class="kg-image" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"></figure><p>	Кроме самого набора данных, важную роль в улучшении результатов сыграл подбор существующих и разработка собственных аугментаций. Для построения пайплайна мы использовали библиотеку Albumentations. В ней реализован большой набор аугментаций и предлагаются абстракции для построения цепочек аугментаций (с возможностью ветвления), каждая из которых активируется с определенной вероятностью. Были использованы цветовые (смещение цветов в RGB, гауссов аддитивный и мультипликативный шумы, перестановка каналов, симуляция шума сенсора камеры, изменения яркости и насыщенности, блюр и т.д.) и пространственные (поворот, отражение) преобразования. Мы самостоятельно реализовали аугментации для замены фона и наложения синусоидального шума. Последний помогал при наличии мерцания на изображении из-за особенностей поведения сенсора камеры при включенном искусственном освещении.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/08/image-7.png" class="kg-image" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"><figcaption>Синусоидальный шум</figcaption></figure><p>Аугментации не отразились существенно на метриках, зато значительным образом улучшили стабильность поведения модели «в естественной среде».</p><p><strong><strong>Модель</strong></strong></p><p><strong>	</strong>Хотя данные в нашем случае были ключом к получению качественной сегментации, модель все еще играет важную роль. Особенно когда важна максимальная производительность и просто взять готовую сеть целиком из статьи нельзя. В основу архитектуры мы положили схему“encoder-decoder”. Первая часть сети (encoder) поэтапно извлекает признаки из исходного изображения, снижая пространственное разрешение карты признаков в 2 раза на каждом этапе обработки. Итоговое разрешение карты признаков может быть в 32 раза меньше исходного и предсказать достаточно мелкие детали на таком маленьком разрешении затруднительно. Поэтому вторая часть сети использует признаки из исходного изображения и поэтапно восстанавливает разрешение до исходного.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/08/image-8.png" class="kg-image" alt="Функциональность виртуального фона в виде кроссплатформенного SDK: исследования, разработка, запуск на разных платформах"></figure><p><strong>	</strong>Первая версия архитектуры представляла из себя энкодер-декодер с использованием миниатюрной DenseNet в качестве энкодера и последовательности слоёв билинейной интерполяции. Хотя производительность сети нас устраивала, модель могла предсказывать только общий контур сидящей смирно фигуры. Сложный фон или поднятые руки сбивали модель с толку.</p><p>	Эксперименты с сетью UNet показали высокую точность, однако скорость вывода даже на GPU была слишком низкой. Мы попробовали разные варианты наивного масштабирования: уменьшение разрешения входа, уменьшение глубины карт признаков, удаление стадий декодирования, но качество падало быстрее, чем увеличивалась скорость модели. В итоге мы заменили обычные свертки на depthwise-separable варианты. Это увеличило скорость до приемлемых показателей без существенной потери в качестве. Такая модель первой преодолела рубеж качества, достаточного для деплоя и демонстрации.</p><p>	Параллельно мы оценивали легковесные модели на основе “dilated”свертки, однако получить быстрые модели с достаточно высоким качеством в короткие сроки не получалось, и мы забросили это направление исследований полностью.</p><p>	Вдохновением для последующих версий модели послужила работа по<a href="https://arxiv.org/abs/1806.11430"> оценке глубины с помощью UNet-подобной сети</a>, в которой авторы предложили ассиметричную архитектуру с легким энкодером и тяжелым декодером. Еще одной важной идеей было получение предсказания в меньшем разрешении по сравнению с входной картинкой. Мы взяли эти две идеи и реализовали свою собственную модель, использовав легковесные боттлнеки и несколько упростив декодер. От расчета ошибки на нескольких разных масштабах отказались, так как не наблюдали его влияния на качество. Вместо него мы добавили в сеть дополнительную ветку, которая использовала последнюю карту признаков для расчета ошибки только на границах маски. Она позволила немного улучшить стабильность предсказания на границах.</p><p><strong><strong>Деплой модели</strong></strong></p><p>	После обучения модели её необходимо подготовить к разворачиванию в продакшене. На первых этапах предполагалось развертывать модель в составе нативных приложений на платформе Windows с возможность запуска нейронки на CPU и на GPU. К сожалению, деплоить модель напрямую с помощью PyTorch не представляется возможным из-за маленькой скорости инференса на CPU. Кроме того, поддержка GPU Windows ограничена карточками nvidia, бинарь библиотеки весит около 3 Гб и на стороне пользователя требуется установка CuDNN, так как её нельзя распространять в составе собственных решений.</p><p><strong>	</strong>Приняв этот печальный факт, мы обратились к другим решениям, нацеленных только на эффективный инференс сетей. Благо, что накопилось достаточное количество зрелых альтернатив.</p><p><strong><em>WinML</em></strong></p><p><strong>	</strong>В первую очередь наш взгляд упал на WinML – библиотека для вывода нейронок от Microsoft, интегрированная в Windows 10 начиная с версии 2004. Оно позволяет запускать модели на CPU и GPU, причем любых вендоров. Библиотека понимает только сериализованные в ONNX модельки. Благо, что в PyTorch конвертация модели делается в пару строчек.</p><p><strong>	</strong>С помощью WinML мы смогли быстро получить первые рабочие прототипы и продолжить исследование доступных средств для запуска нейронных сетей.</p><p><strong><em>OpenVINO</em></strong></p><p><strong>	</strong>Фреймворк от Intel для оптимизации и вывода нейронок на процессорах x86 и интегрированных в процессоры GPU. OpenVINO реализует большое количество операторов, особенно необходимых для компьютерного зрения, так что напороться на несовместимость шансы не так велики. Все это великолепие сопровождается отличной документацией, тулами для тестирования и бенчмаркинга моделей, большим зоопарком уже готовых для использования моделей. Напрямую из PyTorch модель экспортировать нельзя, сначала нужно сконвертировать её в ONNX, Разумеется, поддерживается интеграция в C++ проекты.</p><p>	После демонстрации десктопного SDK мы получили запрос на добавление возможности запуска модели на мобильных устройствах и в веб-браузерах.</p><p><strong><em>MediaPipe</em></strong></p><p>	Для начала мы попробовали библиотеку MediaPipe, созданную в недрах Google. Основные сценарий её использования это реализация пайплайнов потоковой обработки видео и аудио в мультимедийных приложениях. Вы описываете источники данных (камера, микрофон), последовательность манипуляций над данными в виде графа, а фреймворк берет на себя заботы по обслуживанию процесса обработки. За вывод нейронок отвечает TensorFlow Lite. Библиотека поддерживает десктопные и мобильные платформы, но с оговорками, о которых мы поговорим ниже.</p><p><strong>	</strong>Так как этот фреймворк не поддерживает импорт из ONNX и PyTorch, сначала нужно решить вопрос перегонки модели в понятный ему формат. После изучения опыта хождения по граблям других людей, мы пошли по не самому простому пути, но с наиболее предсказуемым результатом – повторной реализации модели на TensorFlow и импорта весов из PyTorch. Процесс этот несложный и достаточно механический. Требуется только помнить о том, что TF по умолчанию использует NHWC формат тензоров, а PyTorch – NCHW. Эта несовместимость лечится простым транспонированием весов сверточных слоёв при импорте. Портировав таким образом модель на TensorFlow и убедившись в правильности процесса путем сравнения ответов сети, мы преобразовали сеть в формат TensorFlow Lite и принялись за запуск MediaPipe на мобильных девайсах.</p><p><strong>	</strong>На Android нам пришлось самостоятельно реализовать несколько операций (вроде склейки предыдущей маски с текущим изображением) для обработки изображений на OpenGL, но в целом запуск модели прошел гладко и занял около двух дней. На не самых продвинутых устройствах мы смогли получить уверенные 25-30 FPS, что нас несомненно обрадовало. К тому же, к тому времени у нас были пути дальнейшей оптимизации модели.</p><p><strong>	</strong>К сожалению, с iOS все пошло не так гладко. Главной проблемой стала политика Apple, из-за которой мобильные устройства так и не получили поддержку вычислительных шейдеров OpenGL и пришлось экспериментировать с конвертацией текстур из OpenGL в Metal и обратно. Быстро реализовать это не получилось, поэтому мы отказались от использования MediaPipe на iOS.</p><p><strong><em>CoreML</em></strong></p><p><strong>	</strong>CoreML является родным для экосистемы Apple фреймворком для запуска нейронных сетей. Он поддерживает все железки этой компании, включая встроенные в SoC NPU (Neural Processing Unit). Процесс портирования оказался гладким, так как Apple позаботилась о путях прямой конвертации модели из PyTorch в CoreML. В итоге портированная модель показала около 30 FPS.</p><p><strong><em>Web</em></strong></p><p><strong>	</strong>Многие приложения сейчас с самого начала делаются как веб-ориентированные (тем более есть замечательный WebRTC), поэтому упускать возможность предложить им нашу модель не хочется.  Мы попробовали три опции: ONNX.js, TensorFlow.js и TensorFlow Lite. Опыт, к сожалению, был довольно болезненный и удовлетворительно на данный момент работает только одно решение.</p><p><strong>	</strong>Самый простой путь к запуску модели, в теории, представляет ONNX.js, который позволяет подгружать ONNX-модели и исполнять их на GPU или CPU. Картину портит только то, что библиотека не так активно поддерживается и поддерживает импорт ограниченного набора операторов и только из старых версий ONNX IR. Из-за этого могут быть проблемы при экспорте моделей из новых версий PyTorch. Например, в нашем случае проблемой стали слои транспонированной свертки и апсемплинга. Быстро побороть их не получилось мы пошли исследовать другие варианты.</p><p><strong>	</strong>TensorFlow.js представляет собой полноценный фреймворк для обучения и запуска нейронных сетей в JavaScript. В частности, можно импортировать графы из обычного Tensorflow. Мы взяли модель, подготовленную для мобильной версии, и без проблем смогли запустить модель на GPU через WebGL. Проблемы возникли с выводом на CPU при использовании XNNPACK – операция транспонированной свертки давала неверные результаты.</p><p><strong>	</strong>Последним исследованным вариантом был TensorFlow Lite, скомпилированный в WebAssembly с помощью emscripten. WebAssembly – это стандарт, определяющий независимый от платформы байткод, который может исполняться в песочнице. Он был создан в первую очередь для ускорения программ, которым недостаточно производительности JavaScript в браузерном окружении. Тулчейн emscripten позволяет компилировать программы на C/C++ в WebAssembly. К нашему счастью, TensorFlow Lite как раз написан на C++, что позволяет запустить его напрямую в браузере. Скомпилированная версия TensorFlow Lite быстро завелась и показала достаточную для деплоя производительность. Кроме того, такой способ деплоя открывает возможность использовать другие библиотеки, например, OpenCV для пре- и постпроцессинга изображений, чем мы с радостью воспользовались.</p><p><strong>	</strong>Главным препятствием на пути интеграции стало отсутствие встроенных средств шифрования моделей во всех рассмотренных нами фреймворках. Именно по этой причине мы остановились на варианте с компиляцией TensorFlow Lite, так как он позволяет реализовать шифрование модели самостоятельно.</p><p><strong><strong>Что получилось</strong></strong></p><p>Сейчас у нас подготовлены четыре версии SDK для всех основных платформ с быстрой, стабильной и качественной сегментацией фона. Есть интеграции с коммерческими проектами. Мы продолжаем развивать это направление. Вот сайт продукта: <a href="https://tomsksoft.com/solutions/cross-platform-virtual-backgroung-sdk">https://tomsksoft.com/solutions/cross-platform-virtual-backgroung-sdk</a></p><p>А вот пример работы самой сегментации на десктопе:</p><figure class="kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/qg3uGmgFQEw?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Специфика разработки нового UI для Paltalk Android]]></title><description><![CDATA[<h2 id="road-to-design-system">Road to design system</h2><p>Работа с UI раньше:</p><ol><li>Тикет с независимыми ссылками figma/zeplin;</li><li>Имплементация.</li></ol><p>В чем проблема? Работая по такой схеме мы получили:</p><ol><li>Большое число близких цветов;</li><li>Бесконечное количество текстовых стилей;</li><li>Похожие картинки и иконки по всему проекту;</li><li>Отсутствие какой-либо структуризации в дизайне.</li></ol><p>Проблема заключается в том, что отсутствует</p>]]></description><link>https://blog.tomsksoft.ru/spietsifika-razrabotki-novogho-ui-dlia-paltalk-android/</link><guid isPermaLink="false">60f6648245f9e45e497896da</guid><dc:creator><![CDATA[Vyacheslav Vorozheykin]]></dc:creator><pubDate>Thu, 22 Jul 2021 01:42:39 GMT</pubDate><content:encoded><![CDATA[<h2 id="road-to-design-system">Road to design system</h2><p>Работа с UI раньше:</p><ol><li>Тикет с независимыми ссылками figma/zeplin;</li><li>Имплементация.</li></ol><p>В чем проблема? Работая по такой схеме мы получили:</p><ol><li>Большое число близких цветов;</li><li>Бесконечное количество текстовых стилей;</li><li>Похожие картинки и иконки по всему проекту;</li><li>Отсутствие какой-либо структуризации в дизайне.</li></ol><p>Проблема заключается в том, что отсутствует гибкость решения. Темизация Android приложения в таких условиях потребует огромных ресурсов. Но все можно исправить!</p><p>На самом деле – все просто. Отсутствие гибкости – это прямое следствие отсутствия дизайн системы на проекте. </p><p>В какой-то момент стало известно, что планируется редизайн. Это могло стать отличным поводом надавить на дизайнеров и запросить дизайн систему. Собственно, так оно и произошло. Что-то может и откидывалось на первых порах, но по итогу мы все же дошли до того максимума, который нам был нужен.</p><p>На данный момент дизайн система представлена в виде:</p><ol><li>States &amp; Components. Состояния различных компонент;</li><li>Typography. Перечень всех текстовых стилей, используемых в приложении;</li><li>Colors. Все цвета приложения;</li><li>Icons. Все иконки.</li></ol><p>Эти составляющие - как кубики Lego. Нам же, как разработчикам, для реализации теперь необходимо просто правильно сочетать эти кубики между собой и как по инструкции собирать то, что отрисовал дизайнер. </p><h2 id="-">О техническом решении</h2><p>В арсенале есть два приложения. Весь их новый ui разрабатывается через отдельные ui модули (общие модули). Таким образом, что-то переиспользуется. Например, auth-ui. Для того, чтобы покрасить конкретный модуль в нужные цвета, мы что-то связанное с внешним видом прячем за атрибуты. Это могут быть текстовые стили, цвета, бэкграунды, картинки, иконки и так далее.</p><p>Вот атрибуты одного из таких модулей.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2021/07/image-4.png" class="kg-image"></figure><p> И они мне не совсем нравятся по следующей причине: не для всех них очевидно, что и когда применять. </p><p>Усложняется все тем, что мы не можем подставить конкретику. Например, мы не можем поменять название roomAdminUiColor1 на что-нибудь с Primary из палитры. Все из-за того, что проекты отличаются дизайн системами.</p><p>Нужно как-то абстрагироваться от устройства дизайн системы каждого из приложений.</p><p>Решение:</p><ol><li>Единственный способ изменить внешний вид компонента – стиль;</li><li>Чтобы не было пересечений с другой частью приложения, а также с дефолтными стилями из Material Design Components, мы временно, пока полностью не избавимся от старого дизайна, добавляем префиксы / постфиксы;</li><li>Названия стилей не должны раскрывать его внешний вид и должны базироваться на том, как компонент назван в дизайн системе (в идеале);</li><li>Все ресурсы, включая имплементации стилей, находятся внутри каждого из проектов.</li></ol><p>Как итог:</p><ol><li>Получаем абстрактные стили. Проекты независимы в области палитр, текстовых стилей и любых других составляющих внешнего вида;</li><li>UI модули не содержат никаких ресурсов;</li><li>Пересечение именований компонентов старого и нового ui исключено вследствие префикса-постфикса.</li></ol><p>При имплементации стиля в Paltalk цвета, текстовые стили и иконки не используются напрямую. Они также спрятаны за атрибуты. Такой подход дает преимущества в случае появления новых тем:</p><ol><li>Темная (или любая другая, отличная от оригинальной по палитре) тема. В новой теме просто меняем значения атрибутов цветов на нужные;</li><li>"Тематические" темы (Halloween, Christmas, Easter и так далее). Переопределяем иконки и шрифты под саму тематику.</li></ol><p>Более детальная информация, картинки, код, примеры, представлены в выступлении и источниках ниже. </p><h2 id="--1">Выступление:</h2><figure class="kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/QeDTORDr3qY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p></p><h2 id="--2">Источники:</h2><ol><li><a href="https://habr.com/ru/post/552486/">Стилизуя нестандартно</a></li><li><a href="https://www.figma.com/community/file/778763161265841481">Material Baseline Design Ki</a></li></ol>]]></content:encoded></item><item><title><![CDATA[Бесплатный курс обучения языку программирования Python]]></title><description><![CDATA[Основы программирования на Python и Jupyter Notebook. Компиляция, интерпретация, типы данных, переменные, ссылки, ООП и другие понятия для введения в Python]]></description><link>https://blog.tomsksoft.ru/kurs-obucheniya-yazyku-programmirovaniya-python/</link><guid isPermaLink="false">5feab13445f9e45e497896c6</guid><dc:creator><![CDATA[Anton A. Pantyukhin]]></dc:creator><pubDate>Tue, 29 Dec 2020 05:08:55 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2021/01/Python-Programming.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.tomsksoft.ru/content/images/2021/01/Python-Programming.png" alt="Бесплатный курс обучения языку программирования Python"><p></p><p>Данный курс представляет собой введение в язык программирования Python и среду разработки Jupyter Notebook и ориентирован на студентов физико-математических специальностей. Мы постарались сделать курс максимально простым и не требующим предварительной подготовки в области программирования, поэтому особое внимание уделяем объяснению основополагающих понятий: компиляция/интерпретация, типы данных, переменные, ссылки, объектно-ориентированное программирование (ООП) и т.д. Заключительная глава посвящена краткому рассмотрению основных библиотек языка Python для решения задач технических вычислений: NumPy, Mathplotlib и Pandas.</p>
<p>Курс был разработан для студентов Томского центра университета <a href="https://hwtpu.info/">Heriott-Watt</a>, занимающихся таким популярным в наши дни направлением, как Machine Learning. Университет Heriott-Watt специализируется на подготовке специалистов нефтегазового дела и занимается развитием методов использования алгоритмов Machine Learning для анализа месторождений.</p>
<p>Пройти курс онлайн можно, перейдя по этой <a href="https://nbviewer.jupyter.org/github/pananton/python_course/blob/master/00_Overview.ipynb">ссылке</a>. Скачать курс можно <a href="https://github.com/pananton/python_course">здесь</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Обзор практик и инструментов Site Reliability Engineering]]></title><description><![CDATA[Личный опыт инженера о работе в Site Reliability Engineering: Роль SR-инженера; Подходы в SRE; В чем польза SRE для разработки; Что такое SLO, SLI, SLA; SRE — разработчик или DevOps?]]></description><link>https://blog.tomsksoft.ru/sre-overview/</link><guid isPermaLink="false">5f8ff9d445f9e45e497896aa</guid><dc:creator><![CDATA[Innokentii Mokin]]></dc:creator><pubDate>Fri, 30 Oct 2020 11:17:39 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2020/10/Screenshot-from-2020-10-30-18-16-33.png" medium="image"/><content:encoded><![CDATA[<figure class="kg-embed-card"><iframe width="480" height="270" src="https://www.youtube.com/embed/HNRVVXd4_B4?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Simple autodeploy tool, или весь деплой в одном bash скрипте]]></title><description><![CDATA[Простой инструмент для развертывания (деплоя) сайтов и сервисов на Linux системы. Автоматизация развертывания приложений в одном bash скрипте.]]></description><link>https://blog.tomsksoft.ru/simple-autodeploy-tool/</link><guid isPermaLink="false">5f66b7e345f9e45e49789696</guid><category><![CDATA[Deploy]]></category><category><![CDATA[DevOps]]></category><category><![CDATA[CI]]></category><category><![CDATA[Gitlab CI]]></category><category><![CDATA[Jenkins]]></category><category><![CDATA["Deploy tool]]></category><dc:creator><![CDATA[Ilya Bezkhodarnov]]></dc:creator><pubDate>Wed, 23 Sep 2020 05:39:00 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2020/09/DeployPostImage-8.jpg" medium="image"/><content:encoded><![CDATA[<aside class="toc"></aside><img src="https://blog.tomsksoft.ru/content/images/2020/09/DeployPostImage-8.jpg" alt="Simple autodeploy tool, или весь деплой в одном bash скрипте"><p></p><h2 id="-">Инструмент для автоматизации развертывания веб-приложений</h2><p></p><p>Речь пойдет про простой инструмент для развертывания (деплоя) сайтов и сервисов на Linux системы. Сделали сами, пользуемся несколько лет, при определенных обстоятельствах может пригодится и вам, пользуйтесь.</p><p>Состоит из пары bash скриптов (интегрируются в исходный код проекта), а для их работы нужны только стандартные утилиты Linux (наличие git на целевых серверах не требуется).</p><p><strong>Обеспечивает:</strong><br>- развертывание вашего проекта на любое количество серверов<br>- проверка корректности среды для развертывания<br>- детектирование локальных изменений файлов<br>- возможность полностью автоматически последовательно вернуться на три предыдущие версии<br>- очистку сервера от старых, неиспользуемых версий<br>- легкую интеграцию в проект<br>- совместимость с любой системой CI, которая поддерживает запуск bash скриптов</p><p><strong>Подходит для:</strong><br>- веб сайтов, работающих под управлением ngnix, apache, и вообще, любого веб-сервера, у которого понятие «сайт» - это каталог где расположен корень сайта (doc_root)<br>- сервисов, представляющих собой запускаемый модуль (бинарный или скриптовый)<br>- проекта, которые развертывается на целевую систему в виде конкретного каталога или файла (при этом он может иметь взаимосвязи с другими каталогами системы без ограничений) и у проекта есть «точка входа» которая полностью идентифицируется этим каталогом или файлом</p><p>На картинке это можно представить вот так:</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/09/DeployToolsSchema.jpg" class="kg-image" alt="Simple autodeploy tool, или весь деплой в одном bash скрипте"></figure><p>	Вы интегрируете в код проекта пару скриптов, настраиваете на своей системе CI их запуск в том workflow которое у вас принято, после чего CI занимается тем, что копирует по scp установочные пакеты и скрипт для развертывания на удаленные сервера, и запускает его там, а скрипт выполняет всю работу, при этом обеспечивает диагностический вывод, возврат статуса завершения, перед деплоем выполняет диагностику окружения для уменьшения вероятности сбоев.</p><p></p><h2 id="-docker-git-">Зачем это надо, если есть Docker и git и другие инструменты?</h2><p></p><p>	Главная фишка тут – почти полная независимость от внешней среды. Из внешних зависимостей тут только Linux, bash и их стандартные утилиты и команды. А они как известно поддерживают крайне стабильную совместимость между системами и поколениями. Это может сыграть ключевую роль там, где необходимо поставлять проект на самые разные системы/сервера, когда у вас нет возможности обеспечить на них совместимую среду для более сложных пакетов (даже python скрипты не будут работать где угодно!), и там, где нет ресурсов (сил, желания) тянуть проект на постоянно развивающихся (и не всегда заботящихся об обратной совместимости) системах CI и DevOps. </p><p>	Второе, но не менее важное – это отсутствие backdoor для доступа к вашему репозитарию извне, т.к. система не использует git на серверах, куда деплоит проект.</p><p>	Ну и третье, если Вам просто невозможно использовать что-то другое (заказчик запретил, система не поддерживает, и другие, экзотические случаи).</p><p>	Мы сами являемся сторонниками перехода на доставку с помощью Docker-а и сейчас переводим наши основные проекты на него, но вот старые, те что живут на одной поддержке и не планируются к развитию, оставляем на этом инструменте, который был разработан и внедрен пару лет назад, и успешно эксплуатируется все это время в связке со всеми перечисленными в статье инструментами и технологиями.</p><p></p><h2 id="--1">Этапы внедрения автоматического деплоя сайтов и сервисов</h2><p></p><p>	Для того, чтобы это полностью заработало, нужно сделать следующие шаги:</p><ol><li>Включить скрипты сборки установочного пакета и деплоя в репозитарий вашего проекта (0.5-10 минут)</li><li>Настроить эти скрипты для работы с инфраструктурой проекта (для первого раза 30-60 минут, со второго минут 10-15)</li><li>Подготовить на серверах для деплоя структуру каталогов для работы скрипта (для первого раза с полчаса, дальше не более нескольких минут).<br><br>И с этого момента оно уже работает, но для полноценной интеграции в CI конечно еще нужно:<br></li><li>Если проект имеет сложную инфраструктуру (внешние зависимости, создает временные файлы внутри себя, и т.д. и т.п.), потребуется чуть более тонкая настройка – делаем ее и отлаживаем скрипты до полной работоспособности</li><li>Включить запуск скриптов сборки и деплоя в вашу CI в соответствии с вашим workflow (это может быть любая система, которая поддерживает запуск bash скриптов, например, Gitlab CI или Jenkins)</li><li>И написать краткую инструкцию для разработчиков как этим пользоваться (полчаса, шаблон типовой инструкции приведен в конце статьи)</li></ol><p></p><h2 id="--2">Пошаговая инструкция по автоматизации развертывания веб-приложений в проекте</h2><p></p><p><strong>Шаг 1, добавляем в проект скрипты для подготовки установочного пакета и деплоя:</strong></p><p>	Создаем в корне своего проекта каталог CI, скачиваем в него два скрипта: <em>deploy.sh</em> и <em>build-inst.sh</em> вот отсюда <a href="https://github.com/tomsksoft-llc/ci-deploy-scripts-bash.git">https://github.com/tomsksoft-llc/ci-deploy-scripts-bash.git</a></p><p><em>	Не нравится имя каталога CI? Используйте любое другое, только учитывайте это при дальнейшей настройке.</em></p><p><strong>Шаг 2, настраиваем скрипты под наш проект:</strong></p><p><em>	build-inst.sh </em>– скрипт создания установочного пакета, запускается только на билд-сервере, на удаленных серверах не нужен.</p><p>	В самом верху скрипта строчка:</p><pre><code>INSTALL_PCKG_SUFFIX="my_project"</code></pre><p>	Меняем «<em>my_project</em>» на что подходит для вашего проекта. Это будет использовано как часть имени файла установочного пакета.</p><p>	Дальше ищем:</p><pre><code>tar -czf "${BUILD_VERSION}_${INSTALL_PCKG_SUFFIX}.tgz" --exclude="CI" ./* </code></pre><p>	Создание пакета – это просто архивирование всех нужных файлов в один файл, при этом исключаются не нужные, в данном случае каталог с нашими скриптами. Что бы исключить еще что-то, просто пишем следующий –<em>exclude=”bla…..”</em> и так сколько надо. <strong>Внимание!</strong> Некоторые версии <em>tar </em>имеют чуть другой синтаксис, проверьте что на вашем билд-сервере оно работает как вам надо. Этот скрипт будет запускаться только там и нигде более.<br>	Можете переписать скрипт как угодно вам, интегрировать его в ваш процесс сборки, или просто взять из него одну строчку для получения архива – как вам удобнее. <br>	Задача – получить файл архива, который можно перенести на удаленный сервер, развернуть и запустить из полученных файлов ваш проект.</p><p>	<em>deploy.sh</em> – он выполняет все функции, перечисленные в начале статьи, работает на удаленных серверах.</p><p>	Верхняя часть файла, константы, задаем их:</p><pre><code>OWNER="myusername"</code></pre><p>	Здесь надо указать имя того пользователя, под которым ваш CI будет заходить на удаленные сервера по ssh.</p><pre><code>TARGET_DIR_STATE="drwxr-xr-x${OWNER}mygroupname"</code></pre><p>	Вместо mygroupname укажите имя группы, которой принадлежит каталог в который мы будем выполнять деплой.<br>	В начале строки указаны права на этот каталог, их можно поменять, главное, чтобы они соответствовали действительности, скрипт будет это проверять при деплое и вываливаться с ошибкой если найдет отличия. </p><pre><code>BUILDS_DIR_STATE="drwxr-xr-x${OWNER}mygroupname"</code></pre><p>	Все тоже самое, но для каталога, который находится уже внутри предыдущего. В этом каталоге хранятся версии вашего проекта. Обычно все должно совпадать с предыдущим каталогом, но мало ли…. Кстати, он называется revs. Если хотите поменять это имя – меняйте, в предыдущей строке:</p><pre><code>BUILDS_DIR="revs"</code></pre><p>и обеспечьте соответствие названия с реальностью, когда будете готовить файловую систему на сервере.</p><pre><code>SITE_ROOT_SYMLINK="site" 
SITE_ROOT_SYMLINK_STATE="lrwxrwxrwx${OWNER}mygroupname"</code></pre><p>	Это название и права доступа к файлу сим-линку, который будет указывать на текущую рабочую версию. Именно имя этого сим-линка вы должны использовать в качестве «<em>doc_root</em>» при настройке веб сервера. Здесь также задайте имя группы, которой будет принадлежать файл, и если нужно поменяйте его имя и права доступа.</p><p><strong>Шаг 3, подготовка структуры каталогов на сервере:</strong></p><p>	На сервере(ах) куда будем деплоить нужен локальный пользователь и группа, которые мы задали в конфигурации на предыдущем шаге.<br>	Создаем каталог в который будем все заливать-деплоить, назначаем ему владельцев и права которые задали в конфигурации.<br>	Внутри этого каталога создаем подкаталог <em>revs</em> (или как вы его назвали на предыдущем шаге) и назначаем ему владельцев и права которые указали в конфигурации.<br>	Создаем файл сим-линк (указывающий куда угодно, хоть на несуществующий файл), назначаем ему владельца и права, которые задали в конфигурации.<br>	Если у нас уже развернута живая система на этом сервере, то, есть хитрость легко переехать на данную систему без даунтайма. Но об этом позже, давайте сначала убедимся, что у нас все заработало.</p><p>Вот пример как можно это сделать:</p><pre><code>$ mkdir myproject
$ chmod 755 myproject/
$ mkdir myproject/revs
$ chmod 755 myproject/revs/
$ ln -s foo myproject/site</code></pre><p>Вот что примерно получится:</p><pre><code>drwxr-xr-x 3 myuser mygroup 4096 Sep 19 11:51 myproject:
  drwxr-xr-x 2 myuser mygroup  4096 Sep 19 11:51 revs
  lrwxrwxrwx 1 myuser mygroup 3 Sep 19 11:51 site -&gt; foo</code></pre><p><strong>Шаг 4, приступаем к использованию, тонкая настройка</strong></p><p>	Давайте попробуем получить установочный пакет, для этого переходим в корень репозитария нашего проекта и запускаем:</p><pre><code>$ ./CI/build-inst.sh 1
Try to build for 1:
OK: Build successfully.</code></pre><p>В результате получим файл <em>1_myproject.tgz</em>, в текущем каталоге.</p><p>	Вот так его можно задеплоить на наш удаленный сервер (где мы уже настроили инфраструктуру):</p><pre><code>$ scp CI/deploy.sh myuser@myserver:~
$ scp 1_myproject.tgz myuser@myserver:/home/myuser/myproject/revs/
$ ssh myuser@myserver bash ~/deploy.sh /home/myuser/myproject /home/myuser/myproject/revs/1_myproject.tgz</code></pre><p>	Каталоги надо указывать полностью от корня, т.к. скрипт работает с сим-линками, а нам не надо, чтобы получились относительные сим-линки (тогда уже при запуске самого приложения точно будут проблемы).</p><p>	Я здесь не останавливаюсь на том, как обеспечить доступ по ssh, но проще всего сделать это с помощью RSA ключей. На билд сервере будет лежать закрытая часть ключа, а на удаленном, открытая в каталоге пользователя, под которым мы идем (тогда для команд scp и ssh надо будет указать ключ: -i с именем файла ключа в качестве первого аргумента команды). <br>	Инструкции как это сделать можно легко найти в интернете.</p><p>	Если у вас получилось, то скрипт выполнится успешно, и на удаленном сервере вы увидите такое состояние каталогов:</p><pre><code>myproject:
lrwxrwxrwx  1 myuser mygroup   23 Sep 19 12:16 backup1 -&gt; /home/myuser/myproject/foo
drwxr-xr-x  3 myuser mygroup 4096 Sep 19 12:16 revs
lrwxrwxrwx  1 myuser mygroup   36 Sep 19 12:16 site -&gt; /home/myuser/myproject/revs/1_myproject</code></pre><p>В каталоге <em>/home/myuser/myproject/revs/1_myproject</em> будет развернуто содержимое файла архива, который мы передали в качестве инсталляционного пакета. <br>	При этом сам файл архива после успешного деплоя удаляется.</p><p>	В процессе выполнения скрипта он выведет кучу диагностики – это нормально. В случае, если что-то пошло не так, скорее всего она вам поможет найти ошибку. Если же все было ОК, то завершающий вывод будет примерно таким:</p><pre><code>OK...............: Deploy script deploy.sh
    on myserver
    for /home/myuser/myproject
    from  /home/myuser/myproject/revs/1_myproject
    successfully done.</code></pre><p>	Теперь надо учесть внешние зависимости нашего проекта. Это могут быть постоянные файлы/каталоги, которые должны сохранятся от деплоя к деплою (простейший пример – конфигурационные файлы и файлы локальных настроек).<br>	Принцип похож на тот, что используется в докере – мы просто храним такие каталоги и файлы локально на удаленном сервере в отдельном месте выше каталога проекта (можно и даже нужно их хранить в каталоге куда мы выполняем деплой, т.е. в нашем случае в <em>/home/myuser/myproject</em>, но это не обязательно, делайте как вам удобнее), а при деплое, линкуем их в нужные места в каталоге где развернута конкретная версия (если таких зависимостей в вашем проекте нет, то переходите сразу к пункту «Правила использования скрипта deploy.sh»).</p><p>	В скрипте deploy.sh заготовлено несколько мест, куда нужно прописать команды линковки, и если есть желание, можно прописать и команды проверки наличия этих файлов на удаленной системе, тогда скрипт будет перед деплоем их проверять и не запуститься если проверки обнаружат ошибку.</p><p><strong>Функция <em>deploy_project_environment()</em> </strong>– именно сюда добавляем действия, которые надо выполнить между тем моментом, когда архив уже развернут (т.е. все новые файлы на месте) и тем, когда рабочий сим-линк будет переключен на каталог с новой версией. Если вы вернете из э той функции НЕ ноль, то деплой будет остановлен, и переключения на новую версию не произойдет (старая версия останется работать как ни в чем не бывало).<br>	Например там может быть создание линка внутри только что развернутой версии проекта на файл или каталог который лежит выше и является перманентным (сохраняется между сменами версий). Можно сделать это так:</p><pre><code>if ! create_symlink "$_target_dir/.env" "$_build_dir/.env"
then
    return 1
fi</code></pre><p>Функция <em>create_symlink</em> объявлена внутри нашего скрипта, можете использовать ее просто для удобства, она работает сразу и с каталогами и с файлами и выводит стандартное диагностическое сообщение.<br>	Этот и еще один пример в закомментированном виде есть в самом скрипте, пользуйтесь. Ну и вообще, делайте что нужно, а в случае ошибки возвращайте НЕ ноль чтобы прервать деплой в тот момент, когда на живой системе еще нет никаких изменений.</p><p><strong>Функция <em>test_env_cons()</em></strong> – сюда можно вставить любые проверки, они будут выполнены еще до попытки распаковать файл. Если функция возвращает НЕ ноль, то деплой будет остановлен. В этой функции уже есть три проверки – на существование и правильность прав доступа к основному каталогу, каталогу с ревизиями и сим-линку на рабочую версию. Можете расширять по образу и подобию своими проверками, закомментированный пример есть в коде скрипта. Функция <em>test_dst,</em> которая используется в примерах, определена внутри скрипта, можете пользоваться ей просто для удобства.</p><p><strong>Функция <em>test_content()</em></strong> – она ищет файлы, которые были изменены в рабочей версии уже после того, как она стала основной рабочей. Для того, чтобы избежать реакции на файлы или имена каталоги, которые могут меняться исключите их таким образом:</p><p>ищем строчку </p><pre><code>if [ "$(find "$link_src" -newer "$link" | wc -l)" -gt 0  ]</code></pre><p>и для исключения, например, файла <em>.env</em>, добавляем его вот таким образом:</p><pre><code>if [ "$(find "$link_src" -newer "$link" | grep -v .env | wc -l)" -gt 0  ]</code></pre><p>а для того, чтобы убрать его и из диагностического вывода, добавляем его таким же образом и на пару строк ниже, вот сюда:</p><pre><code>find "$link_src" -newer "$link" | grep -v .env</code></pre><p><strong>Правила использования скрипта deploy.sh</strong></p><p>	Получить справку по использованию можно запустив скрипт без аргументов:</p><pre><code>deploy.sh [-f|--force] TARGET_DIR   BUILD      - to deploy BUILD into TARGET_DIR
TARGET_DIR - directory where all environment will be deployed.
BUILD can be .tgz archive or a directory within TARGET_DIR/revs.
force: Deploy anyway (the script will delete existing directory and ignore local changes).

deploy.sh [-t|--test] TARGET_DIR             - to test environment consistent in TARGET_DIR
deploy.sh [-r|--restore] TARGET_DIR          - to restore live from backup1 link if it exist in TARGET_DIR
deploy.sh [-с|--content] TARGET_DIR          - to check if any file(s) was changed after deploy in TARGET_DIR
deploy.sh [-s|--scavenge] TARGET_DIR         - delete old not used revision in TARGET_DIR
deploy.sh [-h|--help]                        - to print this message</code></pre><p>Основная функция - это деплой, ее синтаксис указан в самом начале описания. Немножко пояснений по поводу флажка force – по умолчанию скрипт если наткнется на любую проблему сразу же прервет деплой (например, уже существует версия с таким же именем как вы хотите задеплоить, или в текущей активной ревизии есть файлы с локальными изменениями). Если вы укажете ключ -f, то такие проблемы будут проигнорированы, но резервная копия при этом все равно будет создана (путем переименования старого каталога).<br>	Функция очистки -s удаляет только те ревизии, которые не являются бекапной копией или текущей рабочей системой, и при этом каталог создан не менее чем за 5 дней до момента на который производится очистка. <br>	Функция restore возвращает систему на одну версию обратно, т.е. текущая рабочая версия заменяется версией из backup1, backup1 заменяется версией из backup2 и т.д. Всего хранится три бекапных линка, но если надо больше, то просто поменяйте код скрипта в функции <em>link_to_site()</em> (увеличьте счетчик цикла, который создает бекапные линки до нужного вам значения).</p><p><strong>Отлаживаем и запускаем</strong></p><p>	Остается теперь перенастроить ваш сервер так, чтобы он брал ваш сайт по нашему сим-линку <em>site</em>. Например, для веб серверов достаточно просто указать его в качестве <em>doc_root</em>. Бывают специфические настройки, которые влияют на поведение веб серверов при таком деплое, например, для ngnix желательно использовать директиву <code>$realpath_root</code>.</p><p>	Проще и надежнее всего поднимать новую систему рядом с уже существующей, с другим именем хоста, там все отлаживать и проверять, и уже потом менять имя хоста в конфигурационном файле веб-сервера.<br>	И только если вам позволяет опыт, цена ошибки не высока и вообще, все звезды сложились, то можно попробовать сделать так: укажите текущему сим-линку целевой каталог где на данный момент находится ваша система, перенастройте сервер и выполните рестарт. Дальше разберитесь с внешними зависимостями, например, все что нужно оставьте в каталоге вашей текущей системы (ведь она сейчас работает), настройте скрип деплоя чтобы линки делались туда, и пробуйте деплой. Если с первого раза не получилось, просто выполняйте restore, он будет возвращать сим-линк на ваш прежний каталог. </p><p><strong>Шаг 5, как это интегрировать в инфраструктуру проекта и его workflow:</strong></p><p>	Для интеграции деплоя в ваш CI, нужно просто вставить запуск наших скриптов в нужные места. Тут все зависит от вашего workflow и инфраструктуры. Например, для формирования инсталляционного пакета используете команду:</p><pre><code>./CI/build-inst.sh &lt;$Version_id&gt;</code></pre><p>Где в качестве <code>Version_id</code><em> </em>можете использовать номер сборки, хеш ревизии из git, или их комбинацию. Это позволит кроме всего прочего легко идентифицировать какая ревизия залита на вашу систему (конкретный сервер).<br>	Не забывайте в CI обрабатывать код возврата. Скрипты в случае ошибки возвращают НЕ ноль, реагируйте на это (хотя, например, Jenkins и Gitlab CI отреагируют на это сами и прервут выполнение своей Job-ы).</p><p>Для деплоя используйте комбинацию команд:</p><pre><code>$ scp -i id_rsa_file_name CI/deploy.sh myuser@myserver:~
$ scp -i id_rsa_file_name &lt;install_pkg_name&gt;.tgz myuser@myserver:/home/myuser/revs/
$ ssh -i id_rsa_file_name myuser@myserver bash ~/deploy.sh /home/myuser/myproject /home/myuser/myproject/revs/&lt;install_pkg_name&gt;.tgz</code></pre><p>Если серверов несколько, то сначала копируйте инсталляционный пакет на все, и если все получилось, только тогда на каждом из них, последовательно, запускайте скрипт деплоя.<br>	Если на одном сервере несколько проектов, то просто переименуйте скрипт <em>deploy.sh </em>прямо в репозитарии проекта (добавьте к имени файла идентификатор проекта) и тогда вы избежите перетирания файлов при деплое разных проектов на один сервер.<br>	Если после деплоя нужны дополнительные действия для вступления изменений в силу – вставляйте эти команды сразу после скрипта деплоя (не забывая обрабатывать его код возврата!). Это могут быть, например, рестарт php-fpm, рестарт вашего сервиса и т.д. и т.п. </p><p>	Кроме этого, можно еще по крону раз в день запускать на билд-сервере скрипт примерно такого содержания:</p><pre><code>$ ssh -i id_rsa_file_name myuser@myserver "/bin/bash ~/deploy.sh -t /home/myuser/myproject"
$ ssh -i id_rsa_file_name myuser@myserver "/bin/bash ~/deploy.sh -c /home/myuser/myproject"
$ ssh -i id_rsa_file_name myuser@myserver "/bin/bash ~/deploy.sh -s /home/myuser/myproject"</code></pre><p>Выводить это в лог и отслеживая код завершения каждой из команд, например, в случае ошибки присылать на почту.<br>	Такой подход позволит вам обнаружить ошибки в инфраструктуре сразу в день их появления (иначе, можно обнаружить ее через полгода, когда срочно надо залить какое-то изменение в проект, а тут выясняется, что у вашего пользователя больше нет доступа к серверу…). <br>	Хотя это на ваше усмотрение, мы такой подход используем и он себя оправдывает (например, смена IP адреса на билд-сервере, в результате чего тот теряет доступ к серверам, ну потом что там настроен firewall, и не пускает кого попало – вы об этом узнаете в тот же день и исправляете по горячим следам).</p><p><strong>Шаг 6, пишем инструкцию по использованию для разработчиков на основании шаблона:</strong></p><p>	Просто приведу пример такой инструкции, основано на одном из наши workflow для конкретной группы проектов. Если будете делать такую инструкцию для ваших инженеров по данному шаблону, это займет несколько минут 30 (вместе с перерывом на чай/кофе):</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/09/deployDoc.jpg" class="kg-image" alt="Simple autodeploy tool, или весь деплой в одном bash скрипте"></figure><p>Если решите использовать данные скрипты, помните – мы не несем ответственности за последствия их применения в первозданном или измененном виде.</p><p>Если решились использовать – удачи вам, и пожалуйста, поделитесь с нами своим успешным опытом!</p>]]></content:encoded></item><item><title><![CDATA[Опыт применения системы непрерывной интеграции - CIS]]></title><description><![CDATA[CIS - система непрерывной интеграции и предназначен для  автоматизации рутинных операций, таких как: сборка, развертывание, автотестирование  и т.д.]]></description><link>https://blog.tomsksoft.ru/cis/</link><guid isPermaLink="false">5f54f55f45f9e45e49789687</guid><dc:creator><![CDATA[Innokentii Mokin]]></dc:creator><pubDate>Mon, 14 Sep 2020 08:12:01 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2020/09/head.png" medium="image"/><content:encoded><![CDATA[<h1 id="-">Введение</h1><img src="https://blog.tomsksoft.ru/content/images/2020/09/head.png" alt="Опыт применения системы непрерывной интеграции - CIS"><p>CIS - это инструмент для разработчиков программного обеспечения.</p><p>	CIS является системой непрерывной интеграции и предназначен для  автоматизации рутинных операций, которые присутствуют в жизненном цикле  ПО таких как: сборка, развертывание, автотестирование, диагностика  состояния систем и т.д.</p><p>	В отличии от других систем непрерывной интеграции ядро CIS можно  запускать локально без остальных компонентов, что может быть удобно для  отладки разрабатываемых скриптов автоматизации. Все логи, настройки проектов  и переменных доступны в виде обычных  текстовых файлов, что позволяет хранить конфигурацию CI в системах  контроля версий, а так же переносить проекты вместе со всей историей при  помощи обычного копирования. Веб интерфейс позволяет создавать задачи, изменять их, следить за их  исполнением в реальном времени, просматривать логи и скачивать  полученные артефакты. Так как CIS использует исполняемые файлы в качестве скриптов, то  возможные сценарии использования ограничены лишь возможностями ОС,  правами пользователя и фантазией разработчика. Например пользователь  может использовать docker или другие средства контейнеризации для  сборки. Механизм задач, в свою очередь, позволяет реализовать набор  базовых компонентов для переиспользования подобных приемов.</p><p>Скриншоты работы системы: </p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/09/photo_2020-08-25_11-30-43--4-.jpg" class="kg-image" alt="Опыт применения системы непрерывной интеграции - CIS"><figcaption>Страница со списком проектов</figcaption></figure><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/09/photo_2020-08-25_11-30-43--3-.jpg" class="kg-image" alt="Опыт применения системы непрерывной интеграции - CIS"><figcaption>Добавление нового проекта</figcaption></figure><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/09/photo_2020-08-25_11-30-43--2-.jpg" class="kg-image" alt="Опыт применения системы непрерывной интеграции - CIS"><figcaption>Страница задачи</figcaption></figure><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/09/photo_2020-08-25_11-30-43.jpg" class="kg-image" alt="Опыт применения системы непрерывной интеграции - CIS"><figcaption>Редактирование скрипта в веб-интерфейсе</figcaption></figure><p>Подробное описание системы и документация представлены в <a href="https://github.com/tomsksoft-llc/cis1-docs">отдельном</a> репозитории.</p><h1 id="--1">Описание системы</h1><p>	Разрабатываемая система состоит из трех основных частей:</p><ul><li>FrontEnd</li><li>WebUI</li><li>Core</li></ul><p>Схема их взаимодействия представлена ниже.</p><pre><code>┌────────┐ &lt;─[exec]─ ┌───────┐          ┌──────────┐
│        │ &lt;─[env]── │       │ &lt;[HTTP]&gt; │          │
│  Core  │           │ WebUI │          │ FrontEnd │
│        │ ─[TCP]──&gt; │       │ &lt;─[WS]─&gt; │          │
└────────┘ &lt;[files]&gt; └───────┘          └──────────┘</code></pre><p>	Ядро является независимой от остальной системы частью системы и может  работать отдельно. В данный момент для корректной работы необходимо устанавливать все  компоненты на один сервер или в один контейнер. При этом существует  возможность разработки небольшой прослойки, которая позволит запускать  ядро и webui на отдельных серверах.</p><h2 id="core">Core</h2><p>	Ядро является ключевым компонентом системы и содержит в себе логику  запуска задач, работы со сборками, логами и планировщиком. Ядро не  зависит от остальных компонентов и может работать отдельно, как из  терминала, так и из других систем, например задачи ядра могут  запускаться напрямую из GitLab CI Runner'а.</p><h3 id="--2">Задачи</h3><p>	Базовой единицей в CIS является задача. Каждая задача представлена  отдельной директорией, содержащей конфигурацию и исполняемый файл  (которой не обязательно является скриптом). У каждой задачи присутствуют  параметры, которые считываются интерактивно, или, могут быть заданы с  помощью аргументов. В рамках сессии параметры задачи могут быть  установлены и считаны с помощью утилит <code>setparam</code> и <code>getparam</code>.</p><h3 id="--3">Сессии</h3><p>	При старте первой задачи создается новая сессия, в рамках сессии все  задачи выполняются синхронно, образуя дерево задач. При этом существует  возможность в задаче создать новую сессию. Такую сессию можно выполнять  как синхронно, так и асинхронно, но в асинхронном варианте все задачи  синхронизации ложатся на пользователя. Вся информация о событиях в  сессии логируется в соответствующий файл, и, если webui доступен, то  также отправляется в него посредством TCP. Служебная информация, такая  как пользователь, внутренний адрес webui сервера, имя самой задачи,  номер сборки и другие параметры передаются посредством переменных среды.  Так же существует механизм пользовательских глобальных переменных,  которые видны в рамках одной сессии и могут быть установлены с помощью <code>setvalue</code> и получены с помощью <code>getvalue</code>.</p><p>	Пример выполнения задач в сессии. Сессия начинается в <code>(а)</code> и завершается в <code>(б)</code>:</p><pre><code>───────────────────────────────────────────────
 WebUI или терминал
───────────────────────────────────────────────
 ↓ (а)                                ↑ (б)
 ┌────────────────────────────────────┐
 | startjob                           |
 └────────────────────────────────────┘
 ↓                                    ↑
 ┌────────────────────────────────────┐
 | job                                |
 └────────────────────────────────────┘
 ↓          ↑  ↓                      ↑
 ┌──────────┐  ┌──────────────────────┐
 | startjob |  | startjob             |
 └──────────┘  └──────────────────────┘
 ↓          ↑  ↓                      ↑
 ┌──────────┐  ┌──────────────────────┐
 | job      |  | job                  |
 └──────────┘  └──────────────────────┘
                     ↓            ↑
                     ┌────────────┐
                     | startjob   |
                     └────────────┘
                     ↓            ↑
                     ┌────────────┐
                     | job        |
                     └────────────┘</code></pre><h3 id="--4">Логи</h3><p>Ядро ведет несколько логов:</p><ul><li><code>cis.log</code> - служебные события CIS, в основном ошибки.</li><li><code>${session_id}.${n}.log</code> - события в рамках сессии.</li><li><code>output.txt</code> в директории каждой сборки - вывод самого скрипта сборки.</li><li><code>${session_id}.combined.log</code> - полный лог всех событий сессии, информация идентична той, что отправляется в WebUI посредством TCP.</li></ul><h3 id="--5">Прочее</h3><p>	В задачи ядра входит очистка директории задачи после сборки, если это  необходимо, так же очистку задачи можно провести вручную при помощи  утилиты <code>maintenance</code>.</p><p>	Помимо этого ядро позволяет запускать задачи по расписанию аналогично утилите <code>cron</code>.</p><p>	Существует репозиторий с вспомогательными скриптами используемых для упрощения написания задач - <a href="https://github.com/tomsksoft-llc/ci-py-lib">ci-py-lib</a>.</p><h2 id="webui">WebUI</h2><p>	Предоставляет API для удаленного управления ядром, просмотра процесса  исполнения задач. Также добавляет функционал управления пользователями,  правами, аутентификации и авторизации. Основной функционал реализован  посредством собственного протокола поверх WebSocket. Дополнительно часть  функций, а именно загрузка и скачивание файлов и вебхуки используют  HTTP. Помимо управления ядром так же реализовано управление файловой  системой, т.к. именно на неё завязано много функций в ядре.</p><h2 id="frontend">FrontEnd</h2><p>	Реализует интерфейс для API WebUI. Даёт возможность создавать пользовательские сценарии.</p><h1 id="--6">Техническая реализация</h1><h2 id="--7">Зависимости</h2><h3 id="core-1">core</h3><ul><li>boost 1.69</li><li>sqlite_orm 1.3</li><li>croncpp 1.0</li><li><a href="https://github.com/tomsksoft-llc/sc-logger">sc_logger</a> 1.0.3</li></ul><h3 id="webui-1">webui</h3><ul><li>boost 1.69</li><li>rapidjson 1.1.0</li><li>sqlite_orm 1.3</li><li>croncpp 1.0</li><li><a href="https://github.com/tomsksoft-llc/sc-logger">sc_logger</a> 1.0.3</li></ul><h2 id="-webui-api">Протокол WebUI API</h2><p>	Для взаимодействия с фронтендом или другими внешними системами, а так  же для получения логов из ядра был реализован протокол похожий на  JSON-RPC или WAMP.</p><p>	Каждое сообщение содержит поле <code>event</code> являющееся уникальным идентификатором события.</p><p>	Каждое сообщение содержит поле <code>transactionId</code> -  идентификатор транзакции, все ответы сервера на какой-либо запрос  устанавливают значение данного поля равное установленному в запросе.  Позволяет асинхронно взаимодействовать с API по модели RequestReply.</p><p>	Сообщение может содержать поле <code>errorMessage</code> - отправляется только сервером и содержит дополнительное текстовое описание ошибки.</p><p>	Поле <code>data</code> содержит произвольные данные специфичные для каждого отдельного события.</p><h2 id="--8">Сериализация и десериализация</h2><p>	Для реализации протоколов были реализованы вспомогательные  библиотеки, позволяющие сереализовать и десериализовать JSON на основе  добавления метаописания к C++-типам.</p><p>	Пример типа с метаописанием:</p><pre><code class="language-cpp">struct auth_logout
{
    std::string token;
    static constexpr auto get_converter()
    {
        using namespace reflect;
        return make_meta_converter&lt;auth_logout&gt;()
            .set_name(
                CT_STRING(&quot;auth&quot;),
                CT_STRING(&quot;logout&quot;))
            .add_field(
                CT_STRING(&quot;token&quot;), 
                ptr_v&lt;&amp;auth_logout::token&gt;{}) 
            .done();
    }
};
</code></pre>
<p>И соотвествующего ему JSON объекта:</p><pre><code class="language-javascript">{
    &quot;event&quot;: &quot;auth.logout&quot;,
    &quot;data&quot;: {
        &quot;token&quot;: string
    }
}
</code></pre>
<p>	При этом при десериализации происходит автоматическая валидация типа,  так же присуствует возможность задать дополнительные функции валидаторы  для каждого типа.</p><p>	Т.к. при данном подходе описание типа отделено от описания протокола, присутствует возможность перехода на бинарный протокол.</p><h2 id="-webui-core">Транспортный протокол между WebUI и Core</h2><p>	Для передачи информации о активной сессии в реально времени был реализован простой бинарный ltv-протокол поверх TCP. В нём присутствует всего 3 типа сообщений - <code>ping</code>, <code>pong</code> и <code>regular</code>.</p><p>	Первые два используются для проверки того, что соединение активно (в  случае если вторая сторона не отвечает на ping соединение просто  обрывается). Сообщение типа <code>regular</code> может иметь произвольное содержимое.</p><p>	Данный протокол реализован отдельный библиотекой, с использованием Boost.Asio.</p><h2 id="core-2">Core</h2><p>	Ядро состоит из нескольких исполняемых файлов, каждый из которых  выполняет отдельную задачу. Для запуска исполняемых файлов используется  библиотека Boost.Process.</p><p>	Если задача является корневой для сессии, то она считывает параметры  интерактивно, в ином случае она считывает их из job.params в папке  сборки, который формируется в момент подготовки сборки из параметров  заданных ранее при помощи setparam и параметров задачи по-умолчанию.</p><p>	Планировщик задач использует два исполняемых файла - один для  непосредственно запуска задач, второй для добавления, удаления и  изменения запланированных задач. Для оповещения процесса о изменении  списка задач используется системная условная переменная из  Boost.Interprocess.</p><p>	Логирование реализовано при помощи библиотеки scf::logger.</p><p>	Ядро покрыто модульными тестами с применением библиотеки GTest.</p><h2 id="webui-2">WebUI</h2><p>	Событийный цикл приложения и сетевая часть реализованы при помощи библиотеки Boost.Asio.</p><p>	Публичный API представлен WebSocket с протоколом описанным выше, а  так же дополнительным HTTP интерфейсом для загрузки и скачивания файлов,  запуска задач посредством вебхук или curl. Данный API реализован с  помощью Boost.Beast.</p><p>	Для работы с ядром используется библиотека Boost.Process.</p><p>	Для хранения информации о пользователях и правах доступа использована СУБД SQLite и библиотека sqlite_orm.</p><h1 id="--9">Результат</h1><p>	Система успешно запущена в эксплуатацию на сайтах tomsksoft.</p>]]></content:encoded></item><item><title><![CDATA[Пробуем запустить свой проект на Qt WebAssembly за час.]]></title><description><![CDATA[<p>Да, столько времени у меня занял запуск простой демки на своем pet-project (другое дело, что демка вышла бесполезной, но об этом позже). Имеем: linux со всякими стандартными штуками для разработки и небольшой проект для доступа к БД, написанный на Qt (проект собирается как на CMake, так и на QMake, не</p>]]></description><link>https://blog.tomsksoft.ru/qt-webassembly-demo-within-hour/</link><guid isPermaLink="false">5f4ce8d845f9e45e49789679</guid><category><![CDATA[Qt]]></category><category><![CDATA[WebAssembly]]></category><dc:creator><![CDATA[Egor O. Ivanov]]></dc:creator><pubDate>Wed, 02 Sep 2020 08:49:50 GMT</pubDate><media:content url="https://blog.tomsksoft.ru/content/images/2020/09/bg_post.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.tomsksoft.ru/content/images/2020/09/bg_post.jpg" alt="Пробуем запустить свой проект на Qt WebAssembly за час."><p>Да, столько времени у меня занял запуск простой демки на своем pet-project (другое дело, что демка вышла бесполезной, но об этом позже). Имеем: linux со всякими стандартными штуками для разработки и небольшой проект для доступа к БД, написанный на Qt (проект собирается как на CMake, так и на QMake, не знаю зачем так). Делаем все максимально лениво, поэтому вместо сборки чего-то руками, тупо скачаем Qt for WebAssembly c помощью online-установщика (поди вспомни еще свой аккаунт в Qt). Я взял последний Qt 5.15, ставим на скачивание, идем читать доку.</p><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/08/wasm_image1.png" class="kg-image" alt="Пробуем запустить свой проект на Qt WebAssembly за час."></figure><p>Сразу же напрягло отсутствие Widgets в списке поддерживаемых модулей на <a href="https://wiki.qt.io/Qt_for_WebAssembly">wiki-странице</a>, но и в списке не поддерживаемых я тоже его не нашел, так что будем надеяться, что оно заработает (спойлер: оно заработает). Список поддерживаемых модулей:<br>* qtbase<br>* qtdeclarative<br>* qtquickcontrols2<br>* qtwebsockets<br>* qtsvg<br>* qtcharts<br>* qtmqtt</p><p>На той же вики я прочитал, что для Qt 5.15 лучше всего подходит Emscripten SDK версии 1.39.8. Что ж, его и возьмем =)<br>Вскользь прочитал, что там какая-то беда с многопоточностью, но потоки я завести еще не успел (лень-матушка), с довольной ухмылкой идем дальше.</p><p>Давайте поставим уже этот Emscripten SDK, чем бы он ни был:</p><pre>$ git clone https://github.com/emscripten-core/emsdk.git
$ cd emsdk/
$ ./emsdk install 1.39.8
$ ./emsdk activate 1.39.8</pre><p>Видим такую подсказку в выводе:</p><pre>- To conveniently access emsdk tools from the command line,
  consider adding the following directories to your PATH:
    /home/ieo/Documents/projects/emsdk
    /home/ieo/Documents/projects/emsdk/node/12.18.1_64bit/bin
    /home/ieo/Documents/projects/emsdk/upstream/emscripten
- This can be done for the current shell by running:
    source "/home/ieo/Documents/projects/emsdk/emsdk_env.sh"
- Configure emsdk in your bash profile by running:
    echo 'source "/home/ieo/Documents/projects/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile
    </pre><p>Выберем первый способ (хватит же на попробовать):</p><pre>$ source ./emsdk_env.sh</pre><p>Проверим, что все встало:</p><pre>$ em++ --version
<...>
emcc (Emscripten gcc/clang-like replacement) 1.39.8 (commit 60e3a51c7ba8bb96a47d06280b0b1f8ef711608b)
</...></pre><p>Видим нужную нам версию в выводе, как-то даже слишком просто, чувство незаслуженной гордости требует двигать дальше.</p><p>Давайте сперва проверим, что Qt вообще запустится в браузере (ага, онлайн демку с их сайта то нельзя запустить). Отрубаем в проекте через опции сборки все, кроме UI на Qt (хорошо, что эти опции есть), что также исключит из сборки кучу библиотек, с ними разберемся позже (нет).<br>Дока по <a href="https://emscripten.org/docs/compiling/Building-Projects.html">emscripten</a> предлагает использовать ./emconfigure и ./emmake вместо сами поняли чего, однако Qt предлагает вызывать qmake из папочки, куда мы поставили Qt WebAssembly. Разбираться со сборкой через CMake что-то лень, да и отпуск у меня, QMake так QMake =)</p><pre>$ cd .. && mkdir build && cd build
$ ~/Qt/5.15.0/wasm_32/bin/qmake ../project_name
</pre><p>Собираем:</p><pre>$ make -j4</pre><p>Собралось! Запускаем встроенный в emsdk простецкий веб-сервер, который засунули в папочку emsdk, указываем ему путь до проекта:</p><pre>$ emrun --no_browser --port 8080 ~/Documents/projects/project_name/project_name.html</pre><p>Открываем в FireFox ссылочку: <a href="http://localhost:8080/project_name.html">http://localhost:8080/project_name.html</a> (вы можете открыть демку <a href="https://ragnar-lodbrok.github.io/meow-sql/meow-sql.html">здесь</a>)</p><p><strong>It works!</strong> (помните, так apache говорил?) Скрин диалога из лисицы и нативное окно для сравнения:</p>
<figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/08/wasm_image2.png" class="kg-image" alt="Пробуем запустить свой проект на Qt WebAssembly за час."><figcaption>Qt Widgets WebAssembly</figcaption></figure><figure class="kg-image-card"><img src="https://blog.tomsksoft.ru/content/images/2020/08/wasm_image3.png" class="kg-image" alt="Пробуем запустить свой проект на Qt WebAssembly за час."><figcaption>Qt Widgets Native</figcaption></figure><p>Как видим, работают стандартные виджеты QDialog, QComboBox, QSpinBox и тд и все это без изменения Qt кода вообще (хотя чего-то не сохраняется в QSettings).</p><p>Давайте поглядим что получилось на выходе по размеру (жЫр, но я ждал под сотню метров):</p><p>2,8K project_name.html<br>298K project_name.js<br>13M  project_name.wasm<br>22K  qtloader.js</p><p>Раскупориваем либу для доступа к БД (-lmysqlclient) и ... что-то не собирается. Значит, настало время читать инструкции.</p><p>В документации я прочитал про следующие ограничения/особенности WASM:</p><h3 id="0-">0. Библиотеки.</h3><p>Нужно собрать либы самому в bitcode. Нельзя просто так взять <strike>и войти в Мордор</strike> готовую либу (*.a. *.so) с вашей нативной системы (ну, логично), нужно собрать ее в байткод, либо воспользоваться портом (если вам повезло и он есть).</p><p>Спортированные либы, кт нам понадобятся (нет):</p><ul><li>libc/libc++</li><li>zlib</li></ul><p>Список всех портов <a href="https://github.com/emscripten-ports">https://github.com/emscripten-ports</a></p><p>Еще я нашел такие (интересные для меня) порты:</p><ul><li>ffmpeg <a href="https://github.com/Kagami/ffmpeg.js">https://github.com/Kagami/ffmpeg.js</a></li><li>FANN <a href="https://github.com/louisstow/fann.js">https://github.com/louisstow/fann.js</a></li><li>OpenCV <a href="https://docs.opencv.org/3.4/d5/d10/tutorial_js_root.html">https://docs.opencv.org/3.4/d5/d10/tutorial_js_root.html</a></li></ul><h3 id="1-">1. Динамическое связывание</h3><p>Динамические библиотеки (.so) поддерживаются (конечно же собранные в байткод!), но на финальном этапе сборки при генерации JS они будут слинкованы как <a href="https://emscripten.org/docs/compiling/Building-Projects.html#dynamic-linking">статические</a>. Более того, компилятор emcc игнорирует комманды по линковке динамических либ при линковке биткода (т.е. не на финальной стадии), чтобы избежать злосчастной проблемы с дубликатами символов. (на этом моменте я с ужасом вспоминаю, как когда-то давно на iOS из-за ограничений App Store приходилось статически линковать кучу библиотек и однажды возник конфликт BoringSSL и OpenSSL).</p><h3 id="2-tcp">2. Неподдержка TCP</h3><p>И тут внезапно (нет) выяснилось, что браузеры не поддерживают прямой доступ к TCP сокетам, нужный для работы mysqlclient, да и других сетевых клиентов для БД, да и много для чего еще. Emscripten пытается решить эту проблему <a href="https://emscripten.org/docs/porting/networking.html">эмулированием</a> работы Posix Sockets API через WebSocket (они-то конечно же в браузере работают), но это означает, что на стороне сервера нужно делать мост в обратном направлении (делать из WebSocket TCP), что конечно же крайне непоходящее решение.</p><p>Можно конечно же взять и попробовать chrome.sockets.tcp и <a href="https://github.com/yoichiro/mysql_js_driver">какой-нибудь</a> драйвер поверх него, но это доступно только в расширениях Chrome и то со специальными пермишнами (FIXME). Облом! UPD: уже после заметил, что "сырые" сокеты прямо в браузере сейчас <a href="https://www.opennet.ru/opennews/art.shtml?num=53582">под обсуждением</a>.</p><h3 id="-">Выводы</h3><p>Итак, Qt в браузере через WebAssembly заработало, но сделать полезное приложение у меня не получилось из-за ограничений браузера. Веб-разработчики героически пытаются писать полезные приложения, но нативщина никуда не делась и надеюсь не денется.</p>]]></content:encoded></item></channel></rss>