Компактная и портабельная программа, четко выполняющая свое предназначение — редкая для современного мира красота и услада для глаз опытного разработчика. Именно такие проекты, реализующие серверы и клиенты для веба вы найдете в этой статье.
Проект "Sandbird' в действии.
Ода миниатюризации
Есть много причин, по которым миниатюрные девушк.. ээ реализации программ заслуживают внимания:
обучение — врядли получится разобраться как устроен вебсервер, перелопачивая исходники монстров вроде Apache или Nginx, спасет только миниатюрная реализация;
основа для собственных проектов — большой объем чужого исходного кода под капотом вашего проекта будет висеть гирей и отвлекать ресурсы на поддержку, в отличие от чего-то маленького и простого;
борьба с энтропией — популярные библиотеки постоянно растут и раздуваются, при этом объем используемого функционала не особо меняется. Таким образом, большая часть кода в современном проекте с кучей внешних библиотек не используется никогда.
Разумеется есть определенные риски использования таких «наколенных» библиотек, связанные с неполной реализацией, безопасностью, работой под нагрузкой и так далее.
Но говоря откровенно, всего этого хватает с головой и в больших, известных реализациях, которые вы используете каждый день на работе.
Еще с опытом приходит понимание, что любая программа — не более чем инструмент а реальную опасность всегда представляют живые люди, а не тупые машины.
Тестовое окружение
Не стал опять заморачиваться с *BSD, чтобы в третий раз не описывать специальную прослойку epoll-shim, позволяющую быстро и более-менее безболезненно портировать серверный софт с линукса. На этот раз в качестве тестового окружения выступает обычная Ubuntu Linux 25.10, хотя и с немного нестандартным ядром.
Компилятором выступит штатный же GCC, без изысков:
A tiny (~800sloc) embeddable HTTP server written in C89, compatible with Linux, OSX and Windows.
800 строк на чистом С, причем наиболее портабельного стандарта С89, c поддержкой Windows, Linux и MacOS — отличный набор для реального применения, например в качестве встроенного вебсервера для WiFi-роутера.
Один из примеров, демонстрирующих работу этой библиотеки — на заглавном скриншоте к статье, причем показана обработка вебформы.
Вот так выглядит сборка примера:
cd example gcc hello.c ../src/*.c -I../src -std=c89 -pedantic -Wall -Wextra -o hello
Так выглядит код тестового приложения с использованием этой библиотеки:
К сожалению этот интересный проект по большей части прототип, иллюстрирующий как можно на коленке без внешних библиотек реализовать серверную сторону вебсокетов на чистом С.
В случае реального использования, столь вольное использование malloc для обработки входящих пакетов быстро превратится в проблему:
.. /* deal with normal frames (non-fragmented) */ if (WEBSFR_GET_OPCODE(frm.info) != 0x0) { /* read data */ if (data) free(data); data = malloc(frm.length + 1); ..
Так выглядит сокращенная версия тестового сервера вебсокетов, c минимумом обработчиков:
#include "../webs.h"
int myFuncZ(webs_client* self) { printf("server %ld: (id %ld) connected!\n", self->srv->id, self->id); webs_send(self, "greetings, salutations!"); return 0; } int myFunc2(webs_client* self) { printf("server %ld: (id %ld) disconnected!\n", self->srv->id, self->id); return 0; } int main(void) { webs_server* server1 = webs_start(7754); if (!server1) { printf("failed to initialise a server.\n"); return 1; } server1->events.on_open = myFuncZ; server1->events.on_close = myFunc2;
Как видно из кода выше, все интересное происходит именно в обработчиках, где собственно и будет находиться ваша собственная логика, если вдруг решитесь использовать эту штуку.
Повторюсь, что весь этот проект, несмотря на всю свою интересность — сырой прототип и тащить в прод подобный код без переработки, "as-is" точно не стоит.
Собирается только для Linux и только с помощью gcc.
Реализуют весьма продвинутый HTTP/HTTPS сервер и клиент, причем фактически в одном файле. Есть поддержка сборки как на Linux так и Windows, причем для второй есть готовый проект для Visual Studio.
Разумеется это не мейнстрим и к качеству есть вопросы, зато все уместилось в очень небольшом коде, без особых ухищрений по миниатюризации и потому вполне читаемому.
Так выглядит сборка тестового сервера, использующего эту библиотеку:
Всего 1.5к строк на С++11 от безвестного китайского автора реализуют полнофункциональный FTP/FTPS‑сервер — с chroot, поддержкой локальных и анонимных юзеров и всем прочим.
FTPS это довольно редко встречающееся расширение протокола FTP, с поддержкой шифрования передачи данных. Не набравшее большой популярности ввиду появления SFTP и органичений самого FTP и потому мало известное широкой админской публике.
В работе:
Проект использует известную библиотеку libnet, о чем автор забыл сообщить и которую необходимо установить до сборки:
apt install libnet1-dev
Сама сборка происходит с помощью обычного cmake:
mkdir build && cd build cmake ..
Реализация FTP-сервера тут максимально классическая — используется chroot а использование 21 и 20 портов зашито в код:
Все это означает, что запускать сервер придется от суперпользователя:
sudo ./tiny_ftpserver ../tiny_ftpserver.conf
У этого проекта есть один неожиданный нюанс:
на самом деле это.. студенческая работа.
И подобныхпроектов на Github оказалось великоемножество. Так что далеко не везде в 2026м году забыли что такое высшее техническое образование, что не может не радовать.
FineFTP is a minimal FTP server library for Windows and Unix flavors.
~2k строк на С++14 реализуют FTP-сервер в виде.. библиотеки!
В этом и есть основная фишка этого проекта — возможность встроить FTP (причем сервер а не клиент) в ваше собственное приложение.
Так выглядит код примера, с внедрением FTP-сервера:
#include <fineftp/server.h> #include <thread>
int main() { // Create an FTP Server on port 2121. We use 2121 instead of the default port // 21, as your application would need root privileges to open port 21. fineftp::FtpServer ftp_server(2121);
// Add the well known anonymous user. Clients can log in using username // "anonymous" or "ftp" with any password. The user will be able to access // your C:\ drive and upload, download, create or delete files. On Linux just // replace "C:\\" with any valid path. FineFTP is designed to be cross-platform. ftp_server.addUserAnonymous("C:\\", fineftp::Permission::All);
// Start the FTP Server with a thread-pool size of 4. ftp_server.start(4);
// Prevent the application from exiting immediately for (;;) std::this_thread::sleep_for(std::chrono::milliseconds(100)); return 0; }
Зачем и для чего такое может быть нужно — другой вопрос, все же обычно работу с файлами в конечном приложении (например обновление прошивки) стараются реализовать в виде клиента и через HTTP/HTTPS.
Но с точки зрения использования все отлично работает:
Это хорошая, взрослая библиотека, с несколькими коммитерами, с историей разработки, которая активно поддерживается и развивается.
Старого цирка с chroot и 21м портом тут нет, поэтому все работает без привилегий суперпользователя — на скриншоте выше как раз видно использование нестандартного порта для работы.
Поддерживается сборка для Linux, MacOS и Windows c приоритетом для последней.
Отличный проект, но далеко не последний в сегодняшней подборке.
Этот проект — привет из далекого и славного прошлого:
This is a modern re-implementation of rcp (remote copy protocol) daemon, originally part berkeley r-commands.
Да, это самый настоящий сервер RCP, всего 300 строк на Golang от знаменитого в узких кругах компьютерных реконструкторов автора Tenox.
RCP, если кто вдруг не знает, это такой устаревший протокол передачи файлов между компьютерами с UNIX, мало пригодный для использования в современных реалиях, ввиду отсутствия какого-либо шифрования и авторизации.
Но крайне актуальный, если имеете дело с устаревшим оборудованием или встраиваемыми системами, которые до сих пор используют rcp для например загрузки обновлений прошивок по сети.
Возвращаясь к проекту:
rcpd от Tenox реализует серверную сторону — сервер RCP
К этому серверу подключаются клиенты для загрузки или скачивания файлов. Так выглядит процесс копирования файла, в качестве клиента тут rcp из пакета GNU Inetutils:
Как видите и клиент и сервер запускаются от суперпользователя — такие были времена, RCP использует 514 порт, который нельзя занять без привилегий.
Сборка:
go build -o rcpd .
Кстати в Makefile проекта есть поддержка кроссплатформенных сборок, с весьма богатым выбором:
Стоит добавить, что протокол RCP сам по себе очень простой и не менялся весь период своего существования, что позволяет подключаться к этому RCP-серверу 21 века даже с помощью клиентских программ из 80х и 90х.
За кадром
Детально разобрать удалось лишь малую часть интересных находок, поэтому ниже буквально одной строкой про интересные миниатюрные, точно заслуживающие внимания.
На свете есть не так много вещей, способных выбесить программиста. И лишь одна делает это с гарантией: оборзевшая машина, возомнившая себя умнее человека.
А значит снова пришло время карать и патчить!
Видите эти повторяющиеся записи справа? Так будет выглядеть буфер сообщений ядра (dmesg), если вам не повезло нарваться на этот баг.
Direct Rendering Manager (DRM)
Развитие современных видеокарт пошло по весьма.. сюрреалистичному пути:
производители страстно желают, чтобы выпускаемые устройства имели максимальную совместимость с модными открытыми ОС, но одновременно были закрытыми и неповторимыми, защищенными разнообразными патентами.
То что звучит как чистая шизофрения для человека с техническим образованием, будучи спущенным сверху в виде директивы, еще и подкрепленной серьезным бюджетом, породило на свет такую «хтонь» как binary blob.
Это когда основная часть управляющей устройством логики реализуется в виде специальной прошивки с защитой и обфрускацией — того самого «блоба».
Затем вокруг создается открытая часть ПО, которая линкуется с открытым ядром и/или окружением. А все это вместе и без смущения объявляется как частично беременна открытое решение.
The Direct Rendering Manager (DRM) is a subsystem of the Linux kernel responsible for interfacing with GPUs of modern video cards
Если кратко и цензурно, то это такая специальная дыра в ядре Linux, для максимально быстрой коммуникации между видеокартой и клиентским ПО, создаваемая в первую очередь ради видеоиг.. скажем так «медийных приложений, требующих аппаратного ускорения графики».
В угаре оптимизации «ядростроители» дошли до использования 3D-ускорения (GPU) даже для отрисовки терминала текстовой консоли (тот самый KMS):
И теперь мы все имеем клевый терминал в разрешении 1920×1280 со сглаживанием шрифтов, на котором что-то разобрать возможно лишь с лупой прищурившись.
Угар портирования
Под натиском волн малолетних специалистов, желающих «гонять игоры» на серверной ОС, не выдержали даже разработчики FreeBSD:
подсистема DRM и KMS, вместе с проприетарными блобами была перенесена из Linux и теперь вовсю используется в FreeBSD.
По-умолчанию, да.
Кстати с тех лет и до сих пор разработка DRM синхронизируется с апстримом (а это внезапно отдельный проект) и ядром Linux, заезжая в сборки FreeBSD вместе со всеми свежими багами в виде срезов исходников. Основная разработка при этом едет дальше.
Нетрудно догадаться, что отправлять багрепорты в проект drm-kmod, курируемый FreeBSD или апстрим ядра Linux при таком подходе равнозначно отправке в «Спортлото».
Заодно во FreeBSD были фактически похоронены альтернативные варианты:
старый текстовый TTY и Xorg-драйвер для видеокарт Intel пока еще существуют и собираются в виде пакетов, но вот их настройку и главное — все сопутствующие сбои в клиентском ПО вы теперь вынуждены отлавливать и исправлять самостоятельно.
«Интерес сообщества» к таким технологиям оказался утрачен.
В качестве вишенки на этом интересном торте из говна, добавлю что всенародно любимый браузер Google Chrome плохо работает без модуля KMS, поэтому заводить весь этот цирк с DRM и KMS приходится даже на откровенно винтажных машинах — ради работающего браузера.
И других вариантов уже фактически нет.
Как-то так выглядит процесс попадания обновлений DRM в FreeBSD. FreeBSD — крайний справа, увы.
Оборзевшая техника
Вся эта технологическая «многоножка» из разработчиков Intel с блобами, апстрима ядра Linux с любовью к тотальным переделкам на Rust, на практике приводит к тому, что в бедную FreeBSD постоянно попадают ломающие обновления, отследить и исправить которые «в моменте» не получается.
«Думали что работает» — говорят в таких случаях разработчики FreeBSD и советуют обновиться до -CURRENT версии.
Что для обычных людей и рабочего окружения на машине равносильно старому японскому ритуалу сеппуку, поскольку упадет и сломается вообще все.
В какой-то момент обновление пакета drm-kmod, содержащего тот самый порт подсистемы DRM из ядра Linux принесло мне такое:
drmn0: [drm] ERROR Fault errors on pipe A: 0x00000080
Ну принесло и принесло, ошибок много разных, исправляются далеко не все, дело это небыстрое и так далее.
Но только эта забивала собой весь буфер сообщений ядра!
Сообщений генерировалось настолько много, что dmesg — команда для отображения этого самого буфера, показывала только одну эту бесконечно повторяющуюся ошибку, как на скриншоте в шапке статьи.
Но самое веселое, что баг оказался с рогами и копытами совсем не специфичным и само сообщение означает буквально.. ничего.
Просто «что-то сломалось», в переводе с языка Intel.
Ну что, вы все еще любите проприетарные драйвера от уважаемых производителей?
Беглый поиск в репозитории, в котором ведется разработка подсистемы DRM (напоминаю, она ведется отдельно от ядра Linux) показал, что сообщений с этой ошибкой навалом:
88 репортов, только в этом довольно новом репозитории.
Положение дел
В итоге есть некий баг, с гнездом в районе прошивки видекарты Intel, который (судя по репортам) зверствует проявляет себя самым феерическим образом:
от спама сообщениями до мигания экрана и полного зависания системы.
Исправить своими силами этот баг нельзя, все «workaround» в сети по накалу дичи в рекомендациях напоминают известное шоу «Битва Экстрасенсов»:
нарисуйте ночью в поле пентаграмму из соли, разложите по углам загрузочные диски FreeBSD с 5й по 10ю и громко читайте Changelog задом наперед.
Первая заключается в том, что в свежих прошивках баг вроде бы исправили на стороне Intel, но чтобы эту прошивку поставить — нужна сборка модуля DRM для FreeBSD 15, которая еще не была выпущена на момент написания статьи:
Еще стоит рассказать, что прямо сейчас идет серьезная работа по перепиливанию DRM как в его основном проекте так и на стороне команды FreeBSD (в -CURRENT), поэтому даже пытаться бекпортировать текущую версию drm-kmod в 14.х ветку не стоит.
Также (к сожалению) не стоит рассматривать переезд на 15ю версию как гарантированное решение, поскольку есть уже открытые багрепорты и оттуда:
Хотя тут явно использовалась 6.1 версия.
Вторая хорошая новость заключается в том, что на двух моих ноутбуках, где проявляется данный баг все работает без критических сбоев — без мигания и зависаний системы. А само сообщение до недавних пор глушилось настройкой:
compat.linuxkpi.i915_disable_power_well="0"
Так что конкретно в моем случае, проблема заключается лишь в бесконечном затирании буфера сообщений ядра этой дурацкой и бессмысленной ошибкой:
drmn0: [drm] ERROR Fault errors on pipe A: 0x00000080
Которую мы и будем сейчас цинично глушить.
Аморальный патч
Разумеется так делать нельзя:
глушить сообщения об ошибках в ваших реальных проектах не стоит.
Хотя если в вашем проекте возникает ситуация, когда сообщение об ошибке генерируется по сотне раз за секунду — ну наверное вопрос уже к вам как к разработчику, допустившему такую дичь.
К счастью FreeBSD проект не мой, прямого доступа к разработчикам нет, а сам процесс разработки подсистемы DRM сильно напоминает известный фильм «Human Centipede».
Где (а главное — зачем) искать концы в такой ситуации не очень понятно, для примера багрепорты по ошибкам DRM в трекере ядра Linux сразу просят перенаправлять в отдельный проект, не разбираясь:
Так что было решено применить военную хитрость просто заглушить сообщение об ошибке в исходниках, пересобрав DRM из портов.
Так выглядит конечный результат после патча:
Как видите буфер ядра не забит этими дурацкими сообщениями.
Поскольку новая 15я по счету версия FreeBSD еще официально не вышла на момент написания статьи, я все еще использую 14.3, где максимально доступная версия DRM для этой ветки — 6.1.
Ее мы и будем жестоко патчить.
В портах она находится в этом каталоге:
/usr/ports/graphics/drm-61-kmod
Поиск в исходном коде по тексту сообщения об ошибке показал, что генерируется она всего в одном месте, в файле i915_irq.c:
if (fault_errors) drm_err(&dev_priv->drm, "Fault errors on pipe %c: 0x%08x\n", pipe_name(pipe), fault_errors); ..
Все что нужно сделать — закомментировать этот блок, начиная с условия.
И все, больше вы это проклятое сообщение не увидите, ура.
Для уменьшения работы мозгом у дорогих читателей, подготовил на этот раз готовый патч, который достаточно положить в специальный каталог и он автоматически применится при сборке drm-kmod.
Надо создать файл patch-i915_irq.c в каталоге /usr/ports/graphics/drm-61-kmod/files, затем вставить туда вот такое содержимое:
*** drivers/gpu/drm/i915/i915_irq.c.orig Tue Nov 18 17:17:39 2025 --- drivers/gpu/drm/i915/i915_irq.c Tue Nov 18 17:17:52 2025 *************** *** 2571,2585 ****
if (iir & gen8_de_pipe_underrun_mask(dev_priv)) intel_cpu_fifo_underrun_irq_handler(dev_priv, pipe);
fault_errors = iir & gen8_de_pipe_fault_mask(dev_priv); ! if (fault_errors) drm_err(&dev_priv->drm, "Fault errors on pipe %c: 0x%08x\n", pipe_name(pipe), ! fault_errors); }
if (HAS_PCH_SPLIT(dev_priv) && !HAS_PCH_NOP(dev_priv) && master_ctl & GEN8_DE_PCH_IRQ) { /*
Дальше просто перезапускаем сборку:
make clean make
Проверяем что изменения в файле i915_irq.cприменились и запускаем установку собранного пакета:
make reinstall
После чего перезагружаем систему.
Если все прошло удачно — дурацкое сообщение вас больше беспокоить не будет, если нет — компьютер взорвется система скорее всего зависнет при запуске, либо вас будет ожидать черный экран.
Но главное это разумеется хорошее настроение и поставленная на место машина, вернувшаяся к своей основной задаче служить людям.
P.S.
Ошибка действительно дурацкая, еще и наведенная — если дойдет до серьезных сбоев вроде мигания экрана (screen flickering), вы увидите дополнительные сообщения в логах, по которым можно продолжать искать реальную проблему.
Так что вопрос удаления этого сообщения чисто косметический и частично — лишней нагрузки, поскольку генерация сотен таких сообщений в секунду разумеется нагружала систему.
Сейчас будет еще одна «трешевая» история из мира открытого ПО, из тех что не рассказывают детям, дамам и сотрудникам ППС.
С добрым утром!
XFCE
Xfce это такое очень популярное рабочее окружение (Desktop Environment), яркий представитель опенсорса, который вы неоднократно могли видеть в моих статьях и скриншотах.
Одно время им пользовался даже сам Линус Торвальдс, мотивировав переезд чрезмерным ожирением KDE и уходом в лунатизм разработчиков Gnome.
Штука популярная и известная, некий баланс разумности и последний барьер, разделяющий откровенную гиковскую дичь вроде тайловых менеджеров и тяжеловесные современные среды, разрабатываемые большими командами при поддержке мировых корпораций.
БАГ
Разумеется в Xfce есть баги и недоработки, не такие феерические как например в KDE («Плазма не падает» ага), но тоже временами доставляющие и долгоживущие. Как раз об одном таком «долгожителе» и пойдет наш сегодняшний рассказ.
Как это выглядит в действии:
ноутбук засыпает, ноутбук просыпается и на экране внезапно появляется.. диалог «Display Settings».
Казалось бы мелочь, ну появляется и появляется (причем не на всех ОС и ноутбуках), б‑г с ним и без того проблем навалом.
Но во‑первых временами появляется несколько копий этого диалога, что огорчает куда сильнее, поскольку приходится их каждый раз закрывать. А во‑вторых автор не просто так два десятка лет занимается разработкой, чтобы позволить какой‑то программе творить беспредел.
Так что я полез за боевым топором компилятором.
Ресерч
Первый же беглый поиск дал понять, что это не осеннее обострение проблема есть далеко не только у меня, вот например сообщение с официального форума Xfce от 2021 года:
А вот об этой проблеме пишут на форуме Linux Mint:
Так что проблема действительно есть, причем именно в Xfce а не в конкретной ОС, дистрибутиве или настройках окружения.
Дальнейшие изыскания привели в багтрекер Xfce, к этому багу из 2013 (!) года:
Я насчитал двадцать (20) дублей, закрытых за все время жизни этого тикета, но поскольку с 2013го года сам трекер успел переехать с BugZilla на Gitlab (оригинальный тикет находится тут) — гарантированно что-то успело потеряться.
Несмотря на сильно отличающееся описание, если посмотреть переписку под тикетом — можно увидеть знакомые рога и копыта очертания бага с открытием диалога:
А вот это сообщение в обсуждении навело на возможное место в исходном коде, ответственное за проблему:
Разумеется с 2021го года логика в исходниках успела измениться до неузнаваемости и в актуальной на момент написания статьи версии 4.20 это место выглядит иначе:
Суть происходящего
При восстановлении из сна, происходит повторное включение монитора (какой сюрприз), которое вызывает отработку логики, отвечающей за поиск новых устройств вывода:
Из-за данной логики и происходит отображение диалога «Display Settings», по замыслу авторов — как приглашение к настройке нового устройства, когда например подключается второй монитор или проектор.
"Благими намерениями", вы правильно поняли.
Обратите внимание на вот такую проверку:
/* Start the display dialog according to the user preferences */ if (action == ACTION_ON_NEW_OUTPUT_SHOW_DIALOG) { ..
Чуть выше по коду в этом же файле displays-x11.c происходит чтение из настроек:
Таким образом по идее создателей, если в этом диалоге в поле «When display is connected» выставлено значение «Do nothing»:
Тогда никакого диалога при просыпании ноутбука появляться не будет.
Но есть нюанс.
Нюанс
У Xfce традиционно не очень хорошо с настройками — она их регулярно теряет и сбрасывает при обновлениях, перезаписывает новыми опциями и вообще всячески ломает.
Из‑за чего диалог «Display Settings» появляется снова и снова.
Я решил что хватит это терпеть видеть сей проклятый диалог при просыпании ноутбука более не желаю, поэтому применил спецсредства.
Решение
Думаю нетрудно догадаться, что окончательное решение диалогового вопроса оказалось очень простым:
/* Start the display dialog according to the user preferences */ if (action == ACTION_ON_NEW_OUTPUT_SHOW_DIALOG) { // const gchar *cmd = helper->outputs->len <= 2 ? "xfce4-display-settings -m" : "xfce4-display-settings"; // xfce_spawn_command_line (NULL, cmd, FALSE, FALSE, TRUE, NULL); g_print("Ignored 'Display Settings' dialog \n"); }
Да, весь блок (он такой один), отвечающий за запуск диалога «Display Settings» был самым банальным образом закомментирован в файле displays-x11.c. И нет, мне не стыдно.
Wayland
Несмотря на то что автор не пользуется Wayland, Xfce его поддерживает и в файле displays-wayland.c есть полностью аналогичное место, где также зашит вызов диалога «Display Settings».
Так что если вы используете Wayland и столкнулись с описываемой проблемой — теперь знаете где именно искать и что делать.
Однако поправить код в таком проекте это лишь начало, ключевая проблема — как это потом собрать и запустить.
Сборка
На самом деле мне сильно повезло, что исправление выше находится в небольшом и автономном консольном приложении xfsettingsd, исходники которого в свою очередь находятся в отдельном репозитории xfce4-settings.
Так что экстремальных приключений в виде сборки всего Xfce из исходников с последующей установкой удастся избежать.
Переключаемся на релизную ветку той версии Xfce, которая у вас уже установлена — не забываем, что мы правим лишь один небольшой сервис, а все остальное остается из пакетной версии:
git checkout origin/xfce-4.20 -b xfce-4.20
Вытаскиваем зависимые репозитории:
git submodule update --init --recursive
Запускаем сборку:
./autogen ./configure gmake
В каталоге xfsettingsd появится одноименный бинарник с измененной логикой.
Я не стал заморачиваться с отдельным каталогом в PATH и изучать доступные в Xfce механизмы переопределения путей, вместо этого просто подменив бинарник:
cp ./xfsettingsd/xfsettingsd /usr/local/bin/
И все, больше никаких надоедливых диалогов.
Так выглядит проверочное сообщение, добавленное вместо закомментированных вызовов:
Эпилог
Разумеется такое исправление никогда не примут в апстрим Xfce (занятый переездом на meson) и без навыков программирования вы обречены на вечные муки и страдания наблюдать этот диалог до скончания времен — пока при очередном обновлении не слетит конфигурация.
Добавлю, что сам этот функционал принудительного открытия диалога настроек при изменении оборудования — откровенно дурацкий, из серии «хотели как лучше» и точно нуждается в пересмотре.
Но повлиять «с моей галерки» на авторов Xfce разумеется никакой возможности нет.
Сейчас вы снова убедитесь, что знание языка С сопоставимо с навыками самообороны, поскольку в современном мире мега-корпораций и победившего киберпанка на простых пользователей всем давно плевать.
Картина "Хром шатает батарею цифрового Ильича".
Отдыхаем хорошо
Как и все нормальные люди, я смотрю фильмы, сериалы и длинные видео на ноутбуке и чтобы тот случайно не ушел в сон во время просмотра — включаю на нем «режим презентации».
Поскольку за последние годы все кино переместилось в веб, большую часть времени теперь используется не программа-видеоплеер, а обычный браузер Chrome/Chromium.
Однако после одного из обновлений, стал замечать, что браузер-то оборзел научился самостоятельно перехватывать засыпание ноутбука, блокировку экрана и запуск скринсейвера во время просмотра видео, без всяких «режимов презентации».
Затем такое поведение появилось и на обычных сайтах — без видимого видео или аудио-контента.
Без каких-либо сообщений, запросов и подтверждений на подобные действия. Затем ноутбук впервые разрядился в ноль, будучи оставленным с открытой страницей какого-то левого сайта, таймеры для suspend и hibernate внезапно не сработали.
После пятой по счету подставы с перехватом управления, автор окончально огорчился с такой наглости, достал любимый топор компилятор и стал изучать проблему в деталях.
Изучение проблемы
Первым делом я полез в поисковик, убедиться что проблема — не результат осеннего обострения или моего специфического окружения, в котором половина системы это разнообразные компиляторы и средства разработки, а другая — горы исходного кода.
Разумеется тут не про порно, человек просто собирал ядро из исходников. "Sensitive situation".
Хотя в статье речь пойдет о Xfce, аналогичным обазом ведут себя все «большие» окружения — KDE, Gnome, Cinnamon и так далее:
Отдельная «шутка юмора» — попытка втащить поддержку такого поведения в.. Sway:
Monitor dbus and inhibit swayidle when Firefox or Chromium request it
Хотя любители тайловых менеджеров — особая раса сверхлюдей, понять мотивы которых обывателю не дано, так что не буду даже пытаться.
Разумеется по этой проблеме есть давно заведенный тикет в трекере, с длиннющей перепиской, с приложенными дампами памяти и техническими деталями, открытый уже 11 лет:
Как видите тут стоит низкий приоритет и не назначен ответственный, ниже станет понятно почему.
Помимо означенного тикета в трекере Ubuntu, где тусуются в основном простые пользователи, нашелся еще один эпичный тикет в трекере уже самого Chromium, где обитают в основном разработчики, висящий аж с 2013 года:
Если прокрутить в самый низ страницы, можно заметить статус «Fixed» и битую ссылку на коммит (поскольку трекер переехал), суть которого — легализацияспециального API для управления блокировкой экрана и засыпанием.. прямо из кода на странице!
Примерно такого:
// The wake lock sentinel. let wakeLock = null;
// Function that attempts to request a screen wake lock. const requestWakeLock = async () => { try { wakeLock = await navigator.wakeLock.request(); wakeLock.addEventListener('release', () => { console.log('Screen Wake Lock released:', wakeLock.released); }); console.log('Screen Wake Lock released:', wakeLock.released); } catch (err) { console.error(`${err.name}, ${err.message}`); } };
// Request a screen wake lock… await requestWakeLock(); // …and release it again after 5s. window.setTimeout(() => { wakeLock.release(); wakeLock = null; }, 5000);
Как тебе такое, Илон Маск?
Повторяю для тех, кто еще не понял и не осознал:
любая веб-макака, верстающая на полставки порносайты, ныне может с помощью специального кода на странице заставить браузер Chrome заблокировать засыпание вашего ноутбука.
И защитить от такого может лишь знание языка С и эта замечательная статья.
Механизм работы
Прежде чем «карать и патчить» очередную оборзевшую программу, стоит рассказать широкой аудитории как вся эта кухня вообще работает, хотя‑бы для осознания печальных реалий.
Есть одна неведомая штука в Linux-системах, под названием systemd:
systemd is a suite of basic building blocks for a Linux system. It provides a system and service manager that runs as PID 1 and starts the rest of the system.
Описание, взятое с официального сайта столь расплывчато не потому, что это перевод с языка рептилоидов, просто такого рода системные сервисы крайне непросто описать простыми словами, доступными обывателю.
Так вот, у этого замечательного systemd с недавних пор появился «сказочный» функционал, созданный для перехвата управления процессами засыпания и выключения системы:
systemd 183 and newer include a logic to inhibit system shutdowns and sleep states. This is implemented as part of systemd-logind.daemon(8)
Разумеется создан он был с самыми лучшими пожеланиями, ради блага и добра, но как и бывает при реальном использовании - все очень быстро скатилось в треш-угар и рекламу онлайн-курсов.
Да, этот функционал отключаемый, но с последствиями, вроде сломанного процесса автоматического засыпания из менеджеров управления питанием. И риском загнуть систему целиком при обновлении, поскольку например менеджеры пакетов выставляют подобную блокировку при установке пакета.
Можете конечно попробовать заблокировать механизм inhibit в systemd, но все последствия — на вас и вашей совести.
Мы же пойдем немного другим, менее радикальным путем.
Менеджер управления питанием Xfce
Этой командой можно посмотреть список запущенных перехватчиков:
systemd-inhibit --list
В моей системе (Linux Manjaro) вывод выглядит следующим образом:
Обратите внимание, что самого браузера Chrome в списке нет, зато есть xfce4-power-manager — менеджер управления питанием из Xfce, который принимает входящие запросы на перехват и решает что делать дальше.
Остальные сервисы обрабатывают только события засыпания (sleep).
Так что наша цель это xfce4-power-manager , именно туда мы сейчас и залезем, для нанесения правок.
xfce4-power-manager — по большей части фоновое приложение, автоматически запускаемое при старте среды Xfce. Но в отличие от прошлого поциента, тут есть некоторый интерфейс и взаимодействие с пользователем, которое происходит с помощью иконки в трее:
По нажатию правой кнопки мыши, появится меню со списком дерзких приложений, которые в данный момент перехватывают управление питанием:
Так что ответственный за весь этот электронный беспредел был наконец четко определен.
Кровавый патчинг
Исходный код менеджера управления питанием находится в основном репозитории Xfce, исправляемая версия должна совпадать с установленной локально, чтобы не словить феерические проблемы совместимости.
Автор использовал версию 4.20, установленную на момент написания статьи.
Место предстоящей правки — файл xfpm-inhibit.c, в который вынесена вся логика по обработке перехватов (inhibit).
Нас интересует метод xfpm_inhibit_inhibit,строка 370, где начинается обработка входящего запроса на перехват управления.
Код метода небольшой, поэтому привожу его целиком:
Обратите внимание на вызов метода XFPM_DEBUG, содержащего текст отладочного сообщения. Все подобные сообщения становятся видны только если запустить xfce4-power-manager с ключом --debug.
Именно так и было найдено место будущей правки, после сообщения в консоли:
xfpm_inhibit_inhibit(): Inhibit send application name=/usr/lib/chromium/chromium reason=Video Wake Lock sender=:1.459
Что мы имеем в итоге:
есть единственная точка входа (метод) в менеджере управления питанием, с которой начинается регистрация перехватчика управления;
метод принимает на вход название приложения (полный путь), посягнувшего на такой перехват.
Думаю не надо иметь высшее техническое образование, чтобы догадаться как будет выглядеть финальное решение этой проблемы:
метод strstr проверяет на входжение слова «chrom» в названии дерзкого приложения, которое отправило запрос на перехват управления питанием, если оно там есть — происходит немедленный выход из этого метода, а запрос игнорируется.
Вот так, всего лишь 4 строчки на С, вставленные в нужном месте, сразу после проверки на пустоту обламывают рога оборзевшему браузеру, созданному мировой корпорацией, которая решила, что «мы знаем как лучше».
Так это выглядит в действии после наложения моего «кровавого» патча:
В этот знаменательный день браузер Chrome.. пошел лесом.
Сборка
Теперь поговорим о печальном — о сборке всего этого цирка с конями.
Проект xfce4-power-manager это уже существенная часть Xfce, чтобы собрать его из исходников и заставить работать — придется постараться.
Во-первых, не стоит забирать исходники непосредственно из репозитория, поскольку в проекте используется кодогенерация и в этом случае придется заниматься еще и ей, устанавливая дополнительные пакеты в систему.
Куда проще скачать готовый архив со специально подготовленными исходниками релизной версии.
Напоминаю что мы патчим версию 4.20.
Во-вторых, придется установить в систему довольно много библиотек и утилит для разработки:
Gtk+ and Glib headers, in some distributions called the -devel packages
Xfce 4.20 requires Gtk+ 3.24 and Glib-2.0 >= 2.72 (See also: 4.20 dependencies)
Same version for gmodule-2.0, gobject-2.0, gthread-2.0, gio-2.0 and gdbus
gdk-pixbuf-2.0 >= 2.42.8
gobject-introspection >= 1.72
gtk-layer-shell 0.7.0
pkgconfig
Это не весь список, тут внизу страницы находится специальная таблица с описанием зависимостей между компонентами Xfce, часть из которых также придется установить.
Таков путь.
Распаковываем скачанный архив с исходниками и запускаем скрипт configure:
Поскольку сам автор не использует Wayland — тут отключена зависимость от него при сборке, но если вам оно актуально, придется установить дополнительные библиотеки:
wayland 1.20
wayland-protocols 1.25
Добавляем описанный выше блок в файл src/xfpm-inhibit.c и наконец запускаем сборку:
make
Если сборка завершится успешно, в каталоге src будет готовый бинарник с патчем, проверить который можно так:
Дальше открываем в Chrome любую страницу с видео и смотрим выдаваемые сообщения:
Победа.
Разумеется, после такого патча вам придется вручную включать и отключать режим презентации (Presentation mode) при просмотре длинных роликов в браузере, зато процесс будет полностью контролируемым.
Также подобным образом можно «обламывать рога» и другим интересным приложениям, дерзнувшим покуситься на управление питанием, например с недавних пор за подобным неблаговидным делом был замечен Firefox.
Эпилог
Грустно наблюдать (в который уж раз), как некогда хороший софт, по мере роста популярности скатывается в полное УГ отрицание пользовательского опыта и начинает считать своих пользователей полными идиотами:
неотключаемые опции, неизменяемое поведение и просто наглая ложь — все это стало, к великому сожалению, постоянными спутниками самого популярного браузера на планете.
И лишь владение языком С и навыки системного программирования все еще позволяют ставить на место оборзевшие программы — изучайте же инженерное дело настоящим образом!
Рассказываю про еще одну коварную подлость, встроенную в современные технологии беспроводной связи — WiFi. Про это знают все приличные сетевые инженеры, но почему-то не рассказывают простым пользователям.
Проблема
Временами во время разъездов, когда нахожусь в дороге, включаю мобильную точку WiFi на своем телефоне — чтобы подключиться к ней с ноутбука и попасть в интернет.
Работает надежнее и быстрее, чем использование публичных сетей, даже в поезде или гостинице.
Однако после одного из недавних обновлений, WiFi-точка на телефоне стала работать нестабильно и подключиться получалось не с первой попытки.
Временами мобильная точка пропадала из выдачи — ее не было видно в списке доступных сетей, даже когда телефон лежал рядом. Причем проблемы с подключением были далеко не со всем клиентским оборудованием и не со всеми ОС, так что дело было явно не в ней самой.
Я довольно долго не мог понять в чем дело и забивал на исправление, пока однажды это не стало проблемой:
в нужный момент не смог подключиться и отправить важное письмо.
Тут стоит указать, что хотя все действия происходили на FreeBSD, сама проблема актуальна для любых ОС, включая внезапно встроенные.
Были скопированы настройки сети с «соседнего устройства», где все работало, после чего попытался подключиться полностью вручную — указав название точки, пароль, SSID и номер канала.
Мобильная точка выбирает номер канала случайным образом при каждом включении и на момент отладки выпал номер 13.
Внезапно, при попытке указать канал с этим «чертовым» номером появилась ошибка:
unknown/undefined channel number 13 flags 0x0
Которая немедленно была забита в поисковик и выдала кучу сообщений с похожими проблемами:
FreeBSD's net80211 stack has basic regulatory domain support, enforcing restrictions on frequency, operating modes, transmission power and general behavior.
Та самая сказочная хтонь, про которую вы точно слышали, если имеете отношение к беспроводным сетям и админству, но слабо представляли как оно может влиять на просторах нашей необъятной.
While the USA restricts 2.4 GHz Wi-Fi to eleven channels, channels 12 through 14 are available elsewhere in the world. You might even be able to activate them by changing your router settings, although you should not do so. Channel 14 is the most tempting to people, as it would have even less interference---but it's illegal to operate your router on this channel in the USA.
Круто?
Как думаете, что произойдет если при установке системы (любой) будет выбрана страна по-умолчанию — США?
Помимо очевидной английской локали, форматов дат и времени, будет применен еще и этот самый «regulatory domain» для Wifi — для США.
И вы получите описанную проблему с подключением и каналами. Ну разве 21 век это не чудо?
Решение
Как уже писал в самом начале, про сам «regulatory domain» знает любой более-менее опытный сисадмин, но вот как его неправильный выбор влияет на работу WiFi-карты — не знает почему-то никто (проверено).
Поэтому вполне допускаю, что описанное окажется сюрпризом и для вас.
К счастью для исправления ситуации, на этот раз не надо патчить драйвера или пересобирать ядро, достаточно указать в /etc/rc.conf правильный regulatory domain:
create_args_wlan0="country RU"
Затем перезагрузить всю систему, либо поддержку сети:
/etc/rc.d/netif restart
Для того чтобы убедиться в правильности выбора и что описанная проблема вас не коснется, существует команда:
ifconfig wlan0 list regdomain
После перенастройки regulatory domain, проблема с мобильной WiFi-точкой исчезла как по волшебству — удивительно какого размера свиней временами подкладывают разработчики стандартов и оборудования простым пользователям.
Еще один удивительный момент:
ни одна нейросеть не смогла найти связь между проблемами с подключением к WiFi и выбором regulatory domain, ни для одной ОС.
Хотя по идее это старая и широко известная история.
Стоит еще добавить, что на самом деле проблема касается двух каналов: 12 и 13. А еще есть 14, использование которого разрешено только в Японии, поэтому для его использования придется переключиться на их regulatory domain.
Бывает что на руках есть лишь «бинарная» сборка сайта на модном фреймворке вроде Angular или React, в которой «срочно надо что‑то поправить». А исходного кода нет.
Есть лишь вы, «бандл» с обфрусцированным JavaScript внутри и горящие сроки. Рассказываю что с этим можно cделать кроме увольнения.
Процесс восстановления исходников из «source map» как есть.
Проблема
Как-то так получилось, что связка из Typescript и упаковщиков вроде Webpack захватила современную веб-разработку практически целиком, а модель построения веб-приложений под названием «Single Page Application» (SPA) стала применяться для всего вообще — от простейших лендингов и сайтов-визиток до сложных CRM-систем с динамической подгрузкой данных.
Из-за того что Typescript является компилируемым языком, в котором существует разделение на исходный код и конечный код, обфрусцированный и упакованный в специальные «бандлы» — произошло некое смешение смыслов:
Многие заказчики теперь понятия не имеют, что даже у статичных лендингов и сайтов-визиток могут быть исходники.
Особенно если пришли из веб-разработки начала 2000х, когда был кругом статичный HTML, а весь JavaScript-код был очень простым и вставлялся прямо на страницу.
Более того, поскольку результат сборки с помощью Webpack это внезапно тоже код, некоторые особо хитрые джентельмены умудрялись сдавать проекты в виде конечной сборки и набора бандлов, без предоставления реальных исходников, мотивируя тем что «это и есть рабочий исходный код».
И с точки зрения пунктов договора на разработку они вообщем-то были правы.
Вот вам небольшой пример такой сборки, взятый с сайта JHipster, чтобы было понятно о чем речь:
А вот так выглядит этот же код после частичного восстановления декомпилятором (который на самом деле больше де-обфрускатор):
(() => { "use strict"; var e, v = {}, m = {}; function r(e) { var i = m[e]; if (void 0 !== i) return i.exports; var t = m[e] = { exports: {} }; return v[e](t, t.exports, r), t.exports } r.m = v, e = [], r.O = (i, t, f, o) => { if (!t) { var a = 1 / 0; for (n = 0; n < e.length; n++) { for (var [t, f, o] = e[n], c = !0, u = 0; u < t.length; u++)(!1 & o || a >= o) && Object.keys(r.O).every(p => r.O[p](t[u])) ? t.splice(u--, 1) : (c = !1, o < a && (a = o)); if (c) { e.splice(n--, 1); var l = f(); void 0 !== l && (i = l) } } return i } o = o || 0; for (var n = e.length; n > 0 && e[n - 1][2] > o; n--) e[n] = e[n - 1]; e[n] = [t, f, o] }, r.d = (e, i) => { for (var t in i) r.o(i, t) && !r.o(e, t) && Object.defineProperty(e, t, { enumerable: !0, get: i[t] }) }, r.f = {}, r.e = e => Promise.all(Object.keys(r.f).reduce((i, t) => (r.f[t](e, i), i), [])), r.u = e => (592 === e ? "common" : e) + "." + { 127: "3939584411b29233", 146: "9e6e63e24ba057f5", 462: "3c011262c4aafd67", 592: "1a0b39952c0d48d5", 679: "dc91bdcb440d5d58", 792: "f4d5b583a515fb1b", 848: "b617a814d1d8d8d1", 905: "e873b5c1adf6b3c3", 920: "a0a741fb2015a1e4", 972: "bd8f95f56699519f", 994: "4c7d5415c98549f2" } [e] + ".js", r.miniCssF = e => {}, r.o = (e, i) => Object.prototype.hasOwnProperty.call(e, i), (() => { var e = {}, i = "jhonline:"; r.l = (t, f, o, n) => { if (e[t]) e[t].push(f); else { var a, c; if (void 0 !== o) for (var u = document.getElementsByTagName("script"), l = 0; l < u.length; l++) { var d = u[l]; if (d.getAttribute("src") == t || d.getAttribute("data-webpack") == i + o) { a = d; break } } a || (c = !0, (a = document.createElement("script")).type = "module", a.charset = "utf-8", a.timeout = 120, r.nc && a.setAttribute("nonce", r.nc), a.setAttribute("data-webpack", i + o), a.src = r.tu(t)), e[t] = [f]; var b = (g, p) => { a.onerror = a.onload = null, clearTimeout(s); var h = e[t]; if (delete e[t], a.parentNode && a.parentNode.removeChild(a), h && h.forEach(y => y(p)), g) return g(p) }, s = setTimeout(b.bind(null, void 0, { type: "timeout", target: a }), 12e4); a.onerror = b.bind(null, a.onerror), a.onload = b.bind(null, a.onload), c && document.head.appendChild(a) } } })(), r.r = e => { typeof Symbol < "u" && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, (() => { var e; r.tt = () => (void 0 === e && (e = { createScriptURL: i => i }, typeof trustedTypes < "u" && trustedTypes.createPolicy && (e = trustedTypes.createPolicy("angular#bundler", e))), e) })(), r.tu = e => r.tt().createScriptURL(e), r.p = "", (() => { var e = { 666: 0 }; r.f.j = (f, o) => { var n = r.o(e, f) ? e[f] : void 0; if (0 !== n) if (n) o.push(n[2]); elseif (666 != f) { var a = newPromise((d, b) => n = e[f] = [d, b]); o.push(n[2] = a); var c = r.p + r.u(f), u = newError; r.l(c, d => { if (r.o(e, f) && (0 !== (n = e[f]) && (e[f] = void 0), n)) { var b = d && ("load" === d.type ? "missing" : d.type), s = d && d.target && d.target.src; u.message = "Loading chunk " + f + " failed.\n(" + b + ": " + s + ")", u.name = "ChunkLoadError", u.type = b, u.request = s, n[1](u) } }, "chunk-" + f, f) } else e[f] = 0 }, r.O.j = f => 0 === e[f]; var i = (f, o) => { var u, l, [n, a, c] = o, d = 0; if (n.some(s => 0 !== e[s])) { for (u in a) r.o(a, u) && (r.m[u] = a[u]); if (c) var b = c(r) } for (f && f(o); d < n.length; d++) r.o(e, l = n[d]) && e[l] && e[l][0](), e[l] = 0; return r.O(b) }, t = self.webpackChunkjhonline = self.webpackChunkjhonline || []; t.forEach(i.bind(null, 0)), t.push = i.bind(null, t.push.bind(t)) })() })();
Думаю очевидно что попытка сопровождать такой код вашими хилыми силами это примерно как попытка участия 100кг туши в балете — и то и другое хотя и теоретически возможно, но врядли продлится долго.
Другой пример
Допустим вы сделали самый обычный «сайт‑визитку» для стартапа, через полгода стартап внезапно выстреливает и идет волна заказов.
Теперь сайт по-хорошему надо делать заново, но времени нет, а старый (он же текущий) при этом отключать нельзя — надо туда постоянно вносить мелкие правки текста: новые контакты, правила, адреса, ссылки и так далее.
И правок этих будет миллион.
А исходников нет. Забыли, потеряли, пролюбили в хаосе начинающей компании — кто работал в стартапах тот поймет.
И вот вы уже в позиции «вечной Золушки», вынуждены снова и снова «добавлять мелкие правки» в некогда статичный сайт прямо на ходу.
Тестовый пример
К сожалению не получится использовать один из наших рабочих проектов в качестве примера для этой статьи — Webpack генерирует чудовищного размера сборки для более-менее объемного проекта, разбираться в которых будет слишком уж долго.
Поэтому был взят шаблон лендинга на React, посвежее и более менее похожий на то что бывает в реальности. И сейчас я покажу на нем что и как можно сделать в столь печальной ситуации.
Выглядит оригинальный шаблон не без изысков вот так:
Цвет фона медленно меняется а звезды двигаются — автор хотел показать свое мастерство.
Сам проект технически вообщем-то тривиален, а его cборка максимально упрощена:
В папке build будет релизная сборка, а в build/js — те самые бандлы.
Каталог build со всем содержимым и будет выступать нашим тестовым стендом, на котором я буду показывать все чудеса эквилибристики с декомпиляторами и деобфрускаторами.
Но начнем мы все же с немного другого, поскольку самый короткий путь — часто самый лучший.
Восстановление исходников из .git
Очень и очень многие современные разработчики — скажем прямо "не отличаются умом и сообразительностью", поэтому не могут натворить бед работодателю даже когда им очень этого хочется.
Поэтому действительно на практике случаются ситуации экстремального дебилизма, хорошо показанные в фильмах Гая Ричи.
Например история вот этих парней:
Нет, эти даже поумнее некоторых моих коллег по отрасли, если честно.
Да, вы правильно поняли — все это про сохранение каталога .git одновременно с попыткой удаления файлов исходников, например с целью шантажа работодателя. Такое тоже бывает в жизни, причем чаще чем вы думаете.
Если кто вдруг еще не знает то сообщаю:
в каталоге .git хранится полная копия всего вашего исходного кода, еще и с историей всех изменений
Потому что это часть системы контроля версий, так она работает.
Было:
Я взял и удалил все папки с исходниками, оставив лишь финальную сборку и каталог .git
Запускаем восстановление:
git reset --hard
Стало:
Вуаля! Все вернулось из небытия.
Так что если видите папку .git на сервере или в архиве с вашим сайтом — скорее всего жизнь не так плоха и печальна.
«Скорее всего» тут по той простой причине, что бывают варианты, когда git используется не по прямому назначению, а например для передачи готовых сборок на сервер через сервис CI.
В этом случае фиксироваться будут только изменения в бандлах, а исходники останутся на уровне CI. Но это уже история не про лендинги и сайты-визитки, а про что-то большое, где полная утеря исходников маловероятна.
Восстановление из файлов «source maps»
Следующий рабочий вариант как вытащить исходники React-приложения из небытия — попытаться восстановить их из файлов «source maps».
Подробно про технологию «source map» можно почитать вот тут в оригинале или тут на русском в переводе Гоблина.
Если кратко, то это такие специальные файлы, которые генерируются при сборке проекта и содержат метаданные по исходному коду. Эти самые медатанные во время работы приложения позволяют формировать корректную трассировку исключений: с номерами строк и читаемыми названиями методов и классов.
Вот так выглядит небольшая часть «source map» файла:
Технически это просто большой JSON, с кучей вложенных объектов и закодированных частей.
Оказалось что метаданных из файлов «source maps» вполне достаточно для восстановления оригинального исходного кода.
Сейчас покажу как это работает.
Инструментов для восстановления исходников из «source map» файлов многоразных, я использовал вот такой, в первую очередь из‑за того что он сохраняет сразу на диск все найденное. По этой ссылке находится статья от автора, с детальным описанием работы.
Я использовал NPM-пакет http-server для эмуляции отдачи статики, поскольку он не связан с отладочным режимом Webpack (когда работает перекомпиляция на лету) и может отдавать только статический контент — получается полная симуляция старого доброго HTTP-сервера для отдачи статики, вроде Apache или Nginx.
Запускается вот так:
http-server ./build
По-умолчанию отдает контент на порту 8081 и использует путь «/», а аргумент «./build» это указание на каталог со статикой, все просто.
Теперь посмотрим что же удалось вытащить из source maps:
Как видите вытащить получилось дофига, настолько дофига, что некоторые коллеги после демонстрации такого восстановления хватаются за сердце и срочно убирают генерацию файлов «source maps» из своих продуктовых сборок.
Вот вам для сравнения оригинальный каталог с исходниками:
Нет лишь статики (картинок и стилей оформления), которая просто выносится при сборке в отдельный каталог:
Теперь посмотрим внимательно на востановленный исходный код — что именно и до какой степени в нем восстановилось.
Вот восстановленная копия стартового скрипта нашего тестового веб-приложения на React:
А вот так выглядит оригинальный файл:
Что тоже в легком удивлении?
В заголовке окна показывается полный путь к файлу, если вы вдруг подумали что я ошибся и дваджы открыл один и тот же файл ;)
Но это еще не все, следующий файл будет с JSX-шаблоном компонента — специально прокрутил до места где он начинается.
Восстановленная копия:
А теперь оригинал для сравнения:
Как видите восстановилось фактически вообще все.
Все исходные файлы, вся структура каталогов, весь исходный код включая даже комментарии и форматирование — спокойно восстанавливается из файлов source maps.
Прекрасный новый мир современных веб-технологий!
Ложка дегтя
К сожалению кое-что все же не восстанавливается: скрипты сборки и внешние пакеты. И то и другое придется собирать по крупицам и создавать заново, что в случае большого проекта легко может стать экстремально сложной задачей.
Тем не менее, такое восстановление исходников из файлов «source maps» это отлично работающий вариант для мелких лендингов и сайтов-визиток, по чьей-то безумной прихоти реализованных на компилируемом языке и столь сложном фреймворке.
Теперь наконец переходим к настоящей жести.
Работа с обфрусцированным бандлом
Да, это именно тот самый вариант, на который меня и подобных товарищей обычно и зовут. Каталога .git нет, никаких «source maps» тоже нет, есть лишь статика в виде набора файлов, а index.html выглядит как-то так:
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="shortcut icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link href="https://use.fontawesome.com/releases/v5.4.1/css/all.css" rel="stylesheet"/><meta name="theme-color" content="#000000"/><meta name="description" content="My name is Hashir Shoaib. I’m a graduate of 2020 from National University of Sciences and Technology at Islamabad with a degree in Computer Engineering. I'm most passionate about giving back to the community, and my goal is to pursue this passion within the field of software engineering. In my free time I like working on open source projects."/><link rel="apple-touch-icon" href="logo192.png"/><link rel="manifest" href="/manifest.json"/><meta property="twitter:image" content="/social-image.png"/><meta property="og:image" content="/social-image.png"/><title>Hashir Shoaib</title><script defer="defer" src="/static/js/main.2911d091.js"></script><link href="/static/css/main.fe927caf.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
Ну и в наличии сами бандлы на JavaScript, прошедшие через Tree Shaking, минификацию и обфрускацию.
Задачи
Давайте начнем с типичного набора задач, которые ставились лично мне в таких случаях:
изменить текст в нужном месте,
добавить пункт меню в шапке,
добавить кнопку или изменить поведение существующей,
скрыть существующий или добавить блок данных.
Все это напоминаю надо проделать в обфрусцированном бандле, сгенерированным с помощью упаковщика Webpack и без доступа к исходникам.
Есть ряд вещей, которые необходимо знать о внутреннем устройстве «упакованной» версии веб-приложения на React прежде чем мы продолжим.
Первое и самое главное:
весь контент, все что вы видите на странице это один сплошной JavaScript, никакого HTML кроме стартового index.html в таком веб-приложении нет.
Для иллюстрации возьмем вот эту красивую кнопку:
Вот так выглядит восстановленный код (прогнанный через несколько разных деобфрускаторов), который ее создает и показывает:
Как видите к нормальному HTML это отношения не имеет, а правка такой дичи — не имеет ничего общего с обычной версткой.
Второе:
веб-приложение на React максимально изолировано от внешнего окружения.
Отправлять и получать события из кода вне React хоть и возможно но очень сложно и требует определенных действий в самом приложении.
Помимо этого, приложение на React поддерживает внутреннее состояние всех компонентов и не реагирует (по-умолчанию) на изменения в DOM-дереве страницы, произведенные снаружи приложения.
Это одновременно и хорошо и плохо.
Хорошо, потому что дает возможность производить манипуляции c DOM-деревом не влезая внутрь самого приложения.
Плохо, потому что ваши изменения могут быть легко затерты приложением, когда оно решит что «время пришло» и надо обновлять компонент. А обновлять его оно будет разумеется из своего внутреннего состояния.
Так что налучший подход это минимальное вмешательство во внутренности таких приложений и решение задачи малой кровью — снаружи.
Что я сейчас и продемонстрирую.
Начнем с самого простой, но самой частой задачи — с удаления элемента на странице.
Задача первая: "С глаз долой, из сердца вон"
Разумеется физически удалять из DOM-дерева ничего не стоит (помним про внутренее состояние React-приложения), благо есть вариант проще — скрытие ненужного элемента через CSS-стили.
Допустим, нам надо убрать пункт меню «About» из шапки страницы нашего тестового проекта.
Открываем index.html и добавляем новый блок <style></style> внутрь тега <head>, пишем :
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="shortcut icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> <link href="https://use.fontawesome.com/releases/v5.4.1/css/all.css" rel="stylesheet" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="My name is Hashir Shoaib. I’m a graduate of 2020 from National University of Sciences and Technology at Islamabad with a degree in Computer Engineering. I'm most passionate about giving back to the community, and my goal is to pursue this passion within the field of software engineering. In my free time I like working on open source projects." /> <link rel="apple-touch-icon" href="logo192.png" /> <link rel="manifest" href="/manifest.json" /> <meta property="twitter:image" content="/social-image.png" /> <meta property="og:image" content="/social-image.png" /> <title>Hashir Shoaib</title> <script defer="defer" src="/static/js/main.2911d091.js"></script> <link href="/static/css/main.fe927caf.css" rel="stylesheet"> <!-- наша коварная вставка --> <style> #basic-navbar-nav > div.navbar-nav > a:nth-of-type(3) { display: none; } </style> </head> <body><noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>
Результат:
Внимание на пункты меню вверху, раздел "About" пропал
Теперь рассказываю как и почему это работает.
Если открыть «Developer Tools» в любимом браузере Chrome (клавиша F12) и перейти на вкладку «Elements», можно увидеть что внутри пустого тега
<div id="root"></div>
внезапно появилась жизнь — все что вы визуально можете наблюдать на странице в браузере находится внутри этого самого тега:
Рабочие будни
То что вы наблюдаете есть результат работы рендера React, который «отрисовывает» каждый компонент приложения внутри корневого элемента согласно его текущему состоянию.
И до тех пор пока в каком-то из скрываемых нами компонентов не произойдет изменения свойства «display» — он так и будет невидимым.
Что касается самого CSS, то используется достаточно сложный селектор, где происходит одновременно выборка по id элемента:
#basic-navbar-nav
затем обращение по цепочке ко вложенным элементам:
> div.navbar-nav > a
а потом еще и обращение по порядковому номеру:
a:nth-of-type(3)
Селектор a:nth-of-type(3) означает что запрашивается третий по порядку элемент <a>. Обращение по уникальному id работает поскольку он был указан в исходном компоненте React:
<Navbar.Collapse id="basic-navbar-nav">
Который после стадии рендеринга попадает и в конечный DOM-элемент:
Код выше скроет самую большую надпись на странице с именем автора:
Огромной надписи выше строки "Passionate about.." больше нет.
Думаю этого примера будет достаточно и читателям теперь понятно как и почему оно работает.
Замечу что сей метод и на практике (на больших приложениях) работает «на ура», честно говоря большая часть работы с подобными задачами это и есть столь банальное скрытие элементов: ссылок, кнопок, пунктов меню или просто разделов с данными.
Но это еще не конец статьи, поэтому мы погружаемся глубже в дикий треш.
Задача вторая: "немного поправить текст на странице"
Следущая стадия это скромная просьба «немного изменить текст на странице», напоминаю что речь про обфрусцированный и минимизированный бандл, созданный с помощью упаковщика, о чем разумеется просящие скромно умалчивают.
Тут уже потребуется немного кода на JavaScript, поэтому добавляем тег <script></script> и помолясь начинаем ваять:
let intervalID;
function makeWhenReady() { let el3 = document.querySelector('span.react-loading-skeleton'); if (!el3) { let el = document.querySelector('#home > div.container > div.text-center > h1.display-1'); el.innerHTML = "Да здраствует хардкор!"; el.style.display='block'; clearInterval(intervalID); } } window.addEventListener('load', function() { intervalID = setInterval(makeWhenReady, 500); });
Вот так выглядит результат работы:
Результат правки
Теперь немного расскажу о том как и почему это работает.
Попытка поработать с DOM-деревом непосредственно из такого обработчика приведет к тому что вместо данных вы увидите фигу шаблон их загрузки — в проекте используется модуль react-loading-skeleton для создания таких шаблонов.
Поэтому мы поступаем хитрее: устанавливаем свой собственный обработчик на событие load у «окна» браузера:
Внутри обработчика уставливаем еще и фоновую обработку — функцию, которая будет вызываться с определенной переодичностью (в нашем случае раз в 500 миллисекунд):
intervalID = setInterval(makeWhenReady, 500);
intervalID как нетрудно догадаться это ее идентификатор, который будет нужен далее для остановки этой фоновой функции.
Внутри этой фоновой функции мы делаем проверку на наличие в DOM-дереве элементов <span> с классом react-loading-skeleton — сие означает что еще не все компоненты загружены:
function makeWhenReady() { let el3 = document.querySelector('span.react-loading-skeleton'); if (!el3) { .. } }
Если таких элементов не было найдено — считаем что все компоненты загрузились, производим наши манипуляции с данными и останавливаем фоновую функцию:
if (!el3) { let el = document.querySelector('#home > div.container > div.text-center > h1.display-1'); el.innerHTML = "Да здравствует хардкор!"; el.style.display='block'; clearInterval(intervalID); }
Поскольку между рендером оригинала и нашими изменениями есть видимая задержка — изменяемый текст будет «моргать»:
сначала будет отображен оригинал, который через какое-то время изменится на нашу версию.
И пользователи это увидят и обязательно огорчатся. Чтобы их не смущать, необходимо спрятать изменяемый блок через CSS:
Причем на вставленный таким образом блок будут распространяться и все эффекты оригинальных элементов:
тень и «подпрыгивание» блока при наведении мыши.
Ну что ж, с этой задачей разобрались, самое время повысить накал жести.
Задача третья: "поправить валидацию формы"
Разумеется есть и нормальныеспособы организовать взаимодействие с React-приложением в обе стороны — из стороннего JavaScript-кода вызывать React-компонент и из такого компонента вызывать сторонний JavaScript-код.
Но к сожалению все они требуют внутренних изменений в приложении, вроде регистрации компонента в контексте window, которые очевидно никто для вас вносить не станет.
Поэтому стандартные способы, о которых вы при желании сможете прочитать в документации и других статьях — для наших специфических задач к сожалению не применимы.
Поэтому будем применять нестандартные, как обычно.
Для лучшей иллюстрации я добавил в проект простую форму с логикой валидации:
Визуально это что-то вроде формы регистрации, ключевой компонент React с реализацией формы был немного изменен по сравнению с оригиналом, выглядит так:
import { FC } from 'react'; import { useForm } from '../hooks/useForm'; import './Registration.scss'; import { Container, } from "react-bootstrap";
const Registration: FC = () => { const { handleSubmit, handleChange, data: user, errors } = useForm<User>({ validations: { name: { pattern: { value: '^[A-Za-z]*#x27;, message: "You're not allowed to use special characters or numbers in your name.", }, }, age: { custom: { isValid: (value) => parseInt(value, 10) > 17, message: 'You have to be at least 18 years old.', }, }, password: { custom: { isValid: (value) => value?.length > 6, message: 'The password needs to be at least 6 characters long.', }, }, }, onSubmit: () => alert('User submitted!'), });
Помещен он был рядом с другими комонентами, с сохранением принципов их именования. Сами стили при этом не менялись и были взяты из оригинального проекта «как есть»:
EventTarget.prototype.addEventListener = function (a, b, c) { if (c == undefined) c = false; if (a == 'submit') { console.log('hijack listener ', a); if (!this.eventListenerList) this.eventListenerList = {}; if (!this.eventListenerList[a]) this.eventListenerList[a] = []; this.eventListenerList[a].push({ listener: b, options: c }); } this._addEventListener(a, b, c); };
EventTarget.prototype._getEventListeners = function (a) { if (!this.eventListenerList) this.eventListenerList = {}; if (a == undefined) { return this.eventListenerList; } return this.eventListenerList[a]; };
В результате его работы, вы увидите в консоли браузера все регистрации обработчиков на отправку формы:
Но главное что у всех DOM-элементов, в которые происходили добавления обработчиков появится новая функция _getEventListeners(), которая будет содержать ссылки на все обработчики.
Зачем это надо?
Например для того чтобы эти обработчики было можно легко удалить:
let r = document.querySelector('#root'); console.log('root element:', r);
let rEvents = r._getEventListeners(); console.log('events:', rEvents);
for (let evt of Object.keys(dvevents)) { console.log('evt:',evt); for (let i = 0; i < dvevents[evt].length; i++) { dv.removeEventListener(evt,dvevents[evt][i].listener); } }
Код выше необходимо вставить все в ту же функцию makeWhenReady(), которая как вы помните вызывается после полной инициализации React-приложения.
Пару слов про обработчики в React.
Оказалось что все обработчики React регистрируются в родительском DOM-элементе, внутри которого происходит отрисовка всех компонентов React:
<body><noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body>
Очевидно что при таком подходе обработчиков там будет очень много — имейте это ввиду.
Наконец для того чтобы добавить наш собственный обработчик для отправки формы, вставляем вот такой код (все также в функцию makeWhenReady()):
let elForm = document.querySelector('form.registration-wrapper'); elForm.addEventListener("submit", function (e) { e.preventDefault(); alert('Hi there!'); });
Результат:
Вместо отправки на сервер вызывается наш обработчик.
Эпилог
В общем случае действительно стоит отказаться от попыток доработки обфрусцированных бандлов — это «путь в никуда», решение которое невозможно поддерживать долго и рано или поздно оно все равно сломается, похоронив под собой и все ваши доработки.
Только в исключительных случаях, когда действительно горит и надо «поправить вчера» на такое имеет смысл идти.
Ноутбук засыпает, ноутбук просыпается, батарея «зависает» — более не отдает ни уровень заряда ни другие показатели, вне зависимости от подключения к сети.
Патч ядра Linux и три года изысканий, рассказываю как это было.
Божественные Вайнона Райдер и Натали Портман, работы нейросети. Ну и пропатченное ядро.
Вводная
Автор очень давно использует самые разнообразные версии и вариации Linux и UNIX‑систем для работы и диких развлечений, в том числе на ноутбуках, поэтому старается решать все найденные проблемы, по мере сил.
Временами проблема возвращается заново в новых версиях ядра, будучи решенной в прошлом, временами происходит наоборот и проблема проявляется только в самых свежих версиях Linux.
Бывает приходится приостановить поиски решения и вернуться к изысканиям спустя много лет, когда случайно обнаруживается новый вариант решения.
Описываемая история — как раз из последних.
Проблема
Вкратце проблема заключалась в абзаце, вынесенном в заголовок:
ноутбук засыпает, ноутбук просыпается, после чего батарея «зависает» — более не отдает свой актуальный уровень заряда и все показатели, вне зависимости от подключения к электросети.
Естественно в Windows все работало правильно и стабильно, в любых режимах засыпания.
Ноутбук редкий, ноутбук старый, от вендора, который в гробу видал никогда не любил альтернативные ОС и тем более в страшном сне не мог представить, что одна из его топовых моделей (на свое время) вместо подсчета прибылей успешному бизнесмену, стала бы использоваться для компиляции ядра из исходников и прочих гиковских непотребств.
Никакая отладка ядра и никакие отладочные сообщения не помогли, что неудивительно:
Работа с ACPI — традиционно самая замороченная область ядра Linux, а процессы засыпания и возвращения к работе — сложны и нестабильны по своей сути.
Так что оно глючило, глючит и будет глючить, в любой ОС и на любом оборудовании при любой погоде.
Процесс отлова ошибок связанных с ACPI усложнен тем, что такие ошибки чаще всего «плавающие» — могут появиться не через один цикл «засыпания‑пробуждения» а например через десять, т. е. вам надо десять раз подряд погрузить ноутбук в сон и затем пробудить чтобы отловить ошибку.
Правда ведь отладка это весело?
Решение
Далеко не сразу (ушло примерно три года), путем хитрых запросов к поисковикам и изучения исходников ядра, автор все же смог отыскать концы этой проблемы.
Description : I tracked the issue ,I discovered that all was working good until the 4.19.86 kernel.
So I checked DIFF and After many tries maybe 50 or more.
I found the cause : It was (if statement was added in 4.19.86 COMMIT) ,particularly checking if spaceid == 0 [ACPI_ADR_SPACE_SYSTEM_MEMORY]. Commit Link
Разумеется патч китайского автора оказался нерабочий, сообщение было написано про другую модель ноутбука, с другим поведением при сбое и с неверной фиксацией на типе батареи в качестве источника проблемы.
Еще речь шла про совсем уж древнюю версию ядра (4.19, релиз в 2018м году) а само сообщение датировалось 2022 годом.
Но вы ведь и не думали, что все будет настолько просто, верно?
/* * These address spaces do not need a call to _REG, since the ACPI * specification defines them as: "must always be accessible". Since * they never change state (never become unavailable), no need to ever * call _REG on them. Also, a data_table is not a "real" address space, * so do not call _REG. September 2018. */
Обратите внимание на дату — 2018й год, год выпуска версии ядра 4.19, в которой и проявилась данная проблема.
По сути самого комментария и исходя из логики, один из коммитеров ядра Linux в далеком 2018 году хотел «сделать как лучше»:
Полагая что строгое соотвествие спецификации ACPI в данном случае бывает всегда — коммитер вставил заглушку, убирающую вызов _REG метода для вроде как системных частей ACPI‑прошивки.
Чем и поломал восстановление статуса батареи при пробуждении ноутбука.
Благими намерениями выстелена дорога в Ад. (ц)
Исправление
Все что нужно сделать для исправления ситуации, это убрать из условия проверки константу ACPI_ADR_SPACE_SYSTEM_MEMORY:
Ну и опционально добавить отладку, чтобы убедиться в правильности работы:
if (space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { printk(KERN_DEBUG "PRO HEHE : Bypassing for battery is done"); }
Отладка была включена в текущем ядре, само сообщение можно наблюдать на стартовой картинке к статье.
Для того чтобы убедиться в правильности работы, достаточно усыпить ноутбук, пробудить и подергать состояние батареи несколько раз:
Эпилог
Такой патч никогда не примут в аппстрим ядра, потому что он неправилен с точки зрения спецификации ACPI.
Собственно сама проблема с зависшей батареей появилась из-за того что некоторые вендоры класть хотели на стандарты и спецификации.
К сожалению такого рода проблемы в новых версиях ядра Linux появляются все чаще, поэтому и ситуация и ее решение — уже можно сказать типовые и подобным образом решаются ныне и многие другие проблемы с оборудованием.
Так что если на вашем ноутбуке также проявляется описанная проблема с «зависанием» батареи — теперь будете знать в какую сторону копать.
P.S.
Если вы внимательно смотрели на заглавную картинку к статье, могли заметить что автор использовал нестандартное ядро:
XanMod is a general-purpose Linux kernel distribution with custom settings and new features. Built to provide a stable, smooth and solid system experience.
The real-time version is recommended for critical runtime applications such as Linux gaming server / client for eSports, streaming, live productions and ultra-low latency enthusiasts.
Этот набор патчей ядра Linux действительно сильно ускоряет работу, что особенно заметно на некрожелезе времен молодости Брежнева, которое автор регулярно использует для укрепления стойкости духа.
P.P.S.
На данный момент проблема все также актуальна и вряд ли будет решена в апстриме ядра в обозримом будущем. Поэтому автор продолжает накладывать описанный патч (и еще несколько) при каждой пересборке ядра.
Если вам неудержимо хочется использовать оборудование из музея для современной разработки — статья специально для вас.
Машины должны служить а не требовать ресурсы. И автор патча l9 об этом знает.
Эпический баг
Сейчас наверное некоторые читатели сильно удивятся:
с 2007 года в ядре Linux живет серьезный баг, приводящий к полному зависанию системы при работе под большой нагрузкой на память.
На дворе на момент написания статьи май 2025 года, так что баг успел отпраздновать совершеннолетие и открыть первую бутылку пива.
Оригинальный репорт выглядит так:
Разумеется разработчики ядра в курсе проблемы, но по ряду причин.. не считают этот баг важным.
Да, вы правильно прочитали:
«полное зависание системы под нагрузкой» и «разработчики не считают важным исправлять» — как вам такие реалии Linux?
Более того, недавно тикет с описанием этого бага вообще закрыли с эпической формулировкой «just become obsolete»:
С легким намеком, что некоторым стоит перестать собирать себе компьютеры по помойкам:
but now I don't bother with less than 32Gb of RAM for a desktop.
Теперь прокрутите обсуждение бага в трекере вниз и посмотрите на последнее сообщение о проблеме:
Специально сохранил картинкой для истории, вдруг не поверите.
Оно конечно все замечательно и у самого автора этой статьи давно 64Гб на одной из рабочих машин, а некоторые коллеги успели впихнуть даже 128Гб, причем в ноутбук — чтобы мы наконец увидели SUSE Linux, которая не тормозит.
Но к сожалению одними любителями компьютерного антиквариата данная проблема не ограничивается — на нынешние облачные времена типичное рабочее окружение Linux это виртуальная машина, с ограниченным обьемом памяти. Скорее всего даже ваш корпоративный сайт крутится на виртуальной машине с 4Гб памяти.
Так что на самом деле проблема касается практически всех пользователей Linux, а не только идейных нищебродов энтузиастов, собирающих себе оборудование по музеям.
Как так получилось
Если вы хоть немного понимаете в компьютерах, прочитав абзац выше и сопоставив масштаб проблемы и отношение к ней разработчиков Linux, думаю уже сделали определенные выводы:
либо команда разработки ядра Linux — поголовно некомпетентны, либо у автора контракт с рептилоидами в описании выше был упущен ряд важных нюансов.
Правда как обычно где‑то между — «особенных» среди современных разработчиков Linux действительно хватает, но ряд нюансов я все же намеренно упустил.
Опишу в какой момент проявляется этот баг:
надо долго и упорно увеличивать нагрузку на использование памяти, причем маленькими порциями и обязательно из нескольких разных процессов — чтобы OOM Killer не успел отработать.
На практике надо либо заниматься тренировкой нейросетей, либо непрерывно гонять тяжелые приложения на Java/Node (в первую очередь IDE) и постоянно запускать сборку больших проектов.
И все это на неподготовленном офисном оборудовании с 4-6 Гб памяти, представляющем историческую ценность, либо в виртуальной машине.
Патч l9ec
Уже давно существует неофициальный патч, решающий описанную проблему с зависанием квадратно-гнездовым радикальным способом:
The kernel does not provide a way to protect the working set under memory pressure. A certain amount of anonymous and clean file pages is required by the userspace for normal operation. First of all, the userspace needs a cache of shared libraries and executable binaries. If the amount of the clean file pages falls below a certain level, then thrashing and even livelock can take place.
По сути этим патчем формируется небольшой объем памяти (тот самый working set), которую запрещается перегружать даже самым хитрым приложениям, откусывающим память по килобайтам.
Разумеется патч заметили и тут находится архив эпической переписки в рассылке Linux Kernel длиною в год, где автор патча пытается объяснить окружающим что он не верблюд и проблема действительно есть.
Однако патч в мейнстрим так и не попал, что наводит на определенные нехорошие мысли.
История с Xanmod
Помимо основной версии ядра т. н. «vanilla», исходники которого выкладываются на широко известном kernel.org, существуют «васянские сборки» — наборы патчей ядра, собранные энтузиастами под конкретную задачу.
Одна из таких сборок называется Xanmod и посвящена работе современного ядра на desktop-системе с минимальными визуальными задержками:
XanMod is a general-purpose Linux kernel distribution with custom settings and new features. Built to provide a stable, smooth and solid system experience.
Так вот на момент появления l9ec патча, он был включен в сборку Xanmod:
Но в последних 6.х версиях Xanmod его уже нет, на что есть формальная причина — появление вот этого патча, вроде как окончательно решающего проблему c зависанием:
На данный момент MGLRU в mainline и скорее всего работает прямо сейчас и у вас в системе, если конечно у вас современный линукс и MGLRU не отключен вручную.
К сожалению принцип работы MGLRU другой (см. комментарий выше про 32Гб памяти на десктопе) и тестировался его функционал тоже в другом месте:
On Android, our most advanced simulation that generates memory pressure from realistic user behavior shows 18% fewer low-memory kills, which in turn reduces cold starts by 16%.
Как нетрудно догадаться, «realistic user behavior» на мобильном Android несколько отличается от тотальной перегрузки тяжелыми средствами разработки на дохлом десктопе или еще более слабой виртуальной машине.
Поэтому «продвинутым пользователям Linux» в очередной раз придется заботиться о себе и своих проблемах самостоятельно.
Эта история — еще одна причина, по которой стоит использовать *BSD. Реклама.
Портирование на 6.х ядро
К сожалению автор патча l9 видимо устав бодаться с идиотами, не стал переносить свой замечательный патч в 6.х ветку ядра, решив что раз более умные ребята из Google выкатили MGLRU — от его решения толку больше не будет.
Как ни странно, но это не так и l9 патч куда более предсказуем и надежен как удар ломом, в отличие от цирка с аж 14 патчами MGLRU:
These initial multi-generational LRU patches amount to 14 patches at the moment and in a patched kernel can be enabled via the LRU_GEN Kconfig switch
Собственно эта статья появилась на свет после того как автор опять словил зависание под нагрузкой во время работы над большим проектом, из-за чего и решил откопать дедовский пулемет портировать известный патч в 6.х ядро.
За основу был взят последний патч для 5.х ветки без учета MGLRU: le9ec-5.15.patch а его логика добавлялась в Xanmod-версию ядра 6.14.5.
Ниже по шагам объясняю как выполнить перенос логики патча, чтобы процедуру можно было повторить и на более новых ядрах и на «vanilla» версиях.
Скачиваем архив с Xanmod ядром и l9-патч по ссылкам выше и распаковываем.
Стоит сразу предупредить, что размер текущей версии ядра Linux в распакованном виде ~1.8 Гигабайт, а для сборки понадобится еще ~28 Гигабайт.
Вот такие нынче ядра.
Разумеется применить готовый diff автоматически для ветки 6.х не получится, так что будем переносить логику патча по шагам.
Всего в рамках патча изменения происходят в пяти файлах:
Поскольку исправлять документацию нам не очень актуально, первый файл можно пропустить. Таким образом первое актуальное исправление находится в файле include/linux/mm.h, куда добавляются глобальные переменные, отвечающие за настраиваемые лимиты:
Все что нужно сделать — вставить строки в файл include/linux/mm.h:
/* * Force-scan anon if clean file pages is under vm.clean_low_kbytes * or vm.clean_min_kbytes. */ if (sc->clean_below_low || sc->clean_below_min) { scan_balance = SCAN_ANON; goto out; }
Следующая правка в этом же файле должна быть вставлена в этот же метод get_scan_count, но ниже по коду — ориентируйтесь на строку nr[lru] = scan;благо она такая одна:
Я вставил логику проверки сразу над ней:
/* * Hard protection of the working set. */ if (file) { /* * Don't reclaim file pages when the amount of * clean file pages is below vm.clean_min_kbytes. */ if (sc->clean_below_min) scan = 0; } else { /* * Don't reclaim anonymous pages when their * amount is below vm.anon_min_kbytes. */ if (sc->anon_below_min) scan = 0; } nr[lru] = scan;
Следующей правкой добавляется новая функция prepare_workingset_protection, которая должна вызываться из существующего метода shrink_node_memcgs:
Так что вам надо найти функцию shrink_node_memcgs (она такая одна) и вставить новую функцию prepare_workingset_protection над ней:
static void prepare_workingset_protection(pg_data_t*pgdat, struct scan_control *sc) { /* * Check the number of anonymous pages to protect them from * reclaiming if their amount is below the specified. */ if (sysctl_anon_min_kbytes) { unsignedlong reclaimable_anon;
/* * Check the number of clean file pages to protect them from * reclaiming if their amount is below the specified. */ if (sysctl_clean_low_kbytes || sysctl_clean_min_kbytes) { unsignedlong reclaimable_file, dirty, clean;
reclaimable_file = node_page_state(pgdat, NR_ACTIVE_FILE) + node_page_state(pgdat, NR_INACTIVE_FILE) + node_page_state(pgdat, NR_ISOLATED_FILE); dirty = node_page_state(pgdat, NR_FILE_DIRTY); /* * node_page_state() sum can go out of sync since * all the values are not read at once. */ if (likely(reclaimable_file > dirty)) clean = (reclaimable_file - dirty) << (PAGE_SHIFT - 10); else clean = 0;
Собственно последняя правка это вызов новой функции из существующей shrink_node_memcgs:
После внесения всех этих исправлений, запускаем один из вариантов настройки ядра:
make xconfig
И наблюдаем новые поля настройки:
Цепочка сборки и установки ядра совершенно стандартная:
make && make modules && make modules_install && make install
К сожалению это еще не все и прежде чем патч заработает надо будет отключить MGLRU, который как я уже описывал — успели внести в основную ветку ядра:
cat /sys/kernel/mm/lru_gen/enabled
Должен показать 0x0007 если MGLRU включен, отключить можно командой:
echo 0 | sudo tee /sys/kernel/mm/lru_gen/enabled
Вот тут у автора патча лежат готовые скрипты для автоматизации всего этого цирка. Я же просто добавил строчку с отключением в /etc/rc.local.
Пруфы
Для тестов портированного патча, был взят один из моих боевых ноутбуков Lenovo Z580 2012го года выпуска, с 8Гб памяти:
На нем постоянно творится всевозможная дичь — тут пять разных операционных систем и куча проектов и инструментов для разработки на каждой.
Поэтому без особого труда были одновременно запущены:
PostgreSQL с реальной базой
MySQL тоже с реальной базой
Intellij Idea
VSCode
Сборка проекта на Node.js с Webpack и hot reload
Сборка достаточно крупного Java-проекта (~3000 исходных файлов)
Chromium с 20 вкладками
Напоминаю что все это на 8Гб реальной памяти и на ноутбукe. Причем в качестве ОС в этот раз была обычная Ubuntu:
Как-то так это выглядит в действии:
Через неделю после публикации я решил пойти еще дальше и поставил пропатченное с l9 ядро на ноутбук 2007 года с 3Гб памяти. И повторил тесты с нагрузкой. Видео тут.
Все более чем работает и пропатченное ядро замечательно отрабатывает свою пайку.
Эпилог
Можно сколько угодно стебаться с пожеланиями «купи себе наконец нормальный компьютер», скажу что намеренно и давно использую старое железо — в первую очередь для оценки производительности создаваемого ПО.
И это одна из причин, по которой у нас получаются технические чудеса вроде Телепорты.
Если вы пока не дошли до столь глубокой стадии просвещения в разработке — все равно стоит знать, что мы ловили подобные зависания и в виртуальных машинах с Linux, например на CI‑сервере при сборке нескольких проектов одновременно.
Так что актуальность описанного все же высокая и как получилось, что столь простой и очевидный патч, который гарантированно решает проблему до сих пор не используют активно — ума не приложу. Ну и разумеется автору патча лучи респекта, благо это лучший представитель отечественной инженерной школы.
Статья была опубликована на Хабре, оригинал, в котором автор статьи себя не сдерживал и в красках рассказал все что думает о разработчиках ядра Linux как обычно можно найти в нашем блоге.