Показываю на конкретном примере как выглядит идеальный «вкат в ИТ» — с почетом и уважением от старших коллег и карьерой, сразу улетающей ракетой в космос.
Без нейронок и римминга руководству.
Наш герой.
После ряда прошлых публикаций, многим читателям предсказуемо поплохело от глубины разрыва между больными фантазиями о будущей сказочной жизни в ИТ и жестокой реальностью.
Для примера самый частый отзыв на статью «Как на самом деле стать программистом» — «так не бывает», это какая-то фантастика и дурной прикол, а автор в лучшем случае сильно преувеличивает, в худшем — стебет и троллит почтенную публику.
Чтобы окончательно закрыть вопрос перспективности «вката в ИТ по видеокурсам», показываю почтенной публике эталонного джуна, из палаты мер и весов. Именно таких берут на работу без собеседования, за такими охотятся серьезные рекрутеры и таких переманивают конкурирующие компании.
Известный лозунг «Дорогу молодым» — тоже именно про них.
Знакомтесь:
Коннор Крукоски (Connor Krukosky), The "Mainframe Kid"
Коннор с раннего детства увлекался старыми компьютерами и успел собрать немалую коллекцию в подвале родительского дома. В 2016 году, когда ему было лишь 18 лет, он смог купить на интернет-аукционе списанный.. мейнфрейм IBM z890 за жалкие 200 баксов.
Настоящий "Big Iron", оригинальной ценой от $300 000
Так выглядел его первый пост на Reddit сразу после покупки:
So I saw someone post about this system for sale on the auction site 'GovDeals' on a mailing list I am a part of. They had no interest in the machine they just were wondering if anyone else had interest.
I was wondering what ridiculous price they were starting it at, 100 dollars, not the ridiculous I was thinking of...
Being that it was only about 2 hours from me I bid and ended up winning it at $237.
Затем эту адскую древнюю железку весом в полторы тонны храбрый подросток смог утащить в свой подвал, для чего папе пришлось расширять фундамент и немного поработать экскаватором.
Процесс перемещения свежего лута в любимый подвал. Когда родители действительно тебя поддерживают.
Затем было еще немало приключений с поиском недостающих частей, лицензионных ключей для специального ПО и кабелей — даже просто запустить эту «шайтан‑арбу» храбрый подросток очень далеко не сразу. Тут находится целая галерея с фотографиями эпического процесса перевозки и монтажа мейнфрейма подручными средствами.
Комната жениха
Коннор стал знаменитостью, сюжет о нем показывали по местному (американскому) телевидению и неоднократно брали интервью разные издания. Журналисты же придумали ему ник «Mainframe Kid», для примера одно из его более свежих интервью.
А затем его взяла на работу корпорация IBM, несмотря на учебу в колледже.
Причем и колледж и сам Коннор — из каких-то совсем далеких американских пердей в лесной глуши.
На момент написания статьи, Коннору 28 лет и он все также трудится в IBM, успев получить 10 лет непрерывного стажа в известной мировой корпорации.
Фактически его карьера теперь обеспечена и застрахована навсегда — с такими вводными его с радостью возьмут в любую компанию на планете, даже если там нет мейнфреймов, продукции IBM или компьютеров вообще.
Недостижимый идеал
Теперь разберем историю Коннора с точки зрения работодателя, чтобы вы понимали насколько такой человек идеален в качестве сотрудника. Коннор интересовался с детских лет не фентанилом или фурри-оргиями а винтажными компьютерами и уже к 15ти годам смог собрать внушительную коллекцию.
Обратите внимание на полки на заднем плане.
Чтобы вы понимали, процесс даже пользовательского взаимодействия (не говоря о разработке) с винтажными компьютерами для ребенка 21 века очень непрост:
надо постоянно рыть интернет в поисках документации, доставать редкие запчасти и носители данных.
А еще постоянно общаться с не самыми приятными людьми, из тех кто либо сам застал «те времена» либо является фанатом винтажных железок.
Со всеми этими «избранными» от мира компьютерных задротов надо еще уметь договориться, поскольку помогать просто так эти товарищи обычно не очень любят — Коннор натурально пробивался чтобы вся эта история с мейнфреймом получилась.
Ну и сам процесс работы с компьютером даже из начала 90х для современного ребенка, привыкшего к ноутбукам и планшетам покажется невероятно сложным.
А Коннор ковырялся с железками из 80х причем еще в 15-летнем возрасте.
Это вам не Майнкрафт с Роблоксами.
Даже по репозиторию заметно, что Коннор неплохо разбирается в матчасти компьютерного железа — эмуляторы, FPGA и комлекты «железячника», видно что человек учился с малых лет сложной работе инженера. Учился сам, по собственному желанию и следуя собственным интересам.
Теперь что касается всей истории с мейнфреймом.
Фактически будучи 18-летним студентом колледжа, Коннор повторил рабочие обязанности полевого инженера IBM:
демонтаж, перевозка, монтаж и подключение сверхсложного компьютерного оборудования — мейнфрейма.
Причем проявил недюженную смекалку, решив все возникшие проблемы, связанные с перевозкой полторы тонны железа на обычной легковой машине с прицепом и последующим монтажем в неподготовленном помещении.
Кабель питания мейнфрейма в тонкой детской ручке.
Уверяю если автору в его взрослые годы выдать такой мейнфрейм — далеко не факт что он сможет повторить подвиг и такую железку хотя-бы запустить.
А Коннор смог, причем в 18 лет.
Презентация
Серьезная подготовка, мотивация и интеллект в столь юные годы это замечательно, но еще не все. У Коннора оказалось в наличии еще одно важное качество, которого в свое время сильно не хватало автору — хорошее воспитание.
Посмотрите его эпическую презентацию и обратите внимание как он выглядит и что говорит:
В столь юные годы Коннор выглядит и говорит как настоящий высокооплачиваемый сотрудник IBM:
у него нет пирсинга и татуировок на лице, нет разноцветных волос или золотых насадок на зубах;
одет в рубашку классического покроя а не в разноцветный прикид с золотыми цепями или черный балахон с пауком на спине;
не хамит, не ржет в голос, не использует жаргон, не матерится и не размахивает руками;
во время презентации он с большим уважением рассказывает о своих родителях и благодарит их за помощь с проектом;
несмотря на глубокие знания в столь юном возрасте, он не заносчив и не считает собравшуюся аудиторию идиотами.
Коннор четко осознает куда он пришел и зачем, ему не надо объяснять зачем нужен деловой костюм и как себя вести в приличном обществе. Сама презентация вызвала серьезный интерес у публики — на видео выше видно, что народ в зале смотрел не отрываясь, а в конце — аплодировал.
Собравшиеся в зале высоклассные инженеры с многими годами стажа аплодировали стоя 18-летнему подростку.
Уверяю, далеко не каждый убеленный сединами менеджер способен так увлечь аудиторию как это сделал Коннор, причем сложную аудиторию из технарей и инженеров.
Что в итоге увидел работодатель:
увлеченный и мотивированный начинающий специалист, с уже сформированным багажем профильных знаний и компетенций;
с опытом решения сложных технических и производственных проблем — не социальный калека, который будет валить все на "беды с башкой" или «тревожность»;
достаточно умный и организованный, чтобы суметь во всем этом разобраться, причем с минимальным бюджетом;
без раздутого самомнения, позиции «мне все должны» и сказочных ожиданий от будущей работы;
смог повторить дорогостоящий рабочий процесс в домашних условиях и без специального начального обучения.
Собственно корпорации IBM оставалось только нанять его официально, чтобы «легализовать» деятельность и немедленно получать профит:
Коннор как сотрудник окупился сразу с первых дней своей работы в IBM, поскольку фактически еще до официального трудоустройства был полностью готов к будущей работе.
Компания IBM просто выдала ему бейдж с именем и отправила на площадку к первому клиенту.
Эпилог
Пример Коннора — эталон того каким должен быть подающий надежды «джун» и как должен выглядеть правильный «вкат в ИТ».
Именно к такому стоит стремиться «вкатунам», если не хотите общаться с недалекими рекрутерами, недалекими коллегами и вообще миновать все посредственные компании, чтобы заниматься настоящим делом в приличной компании.
Молодость спишет многое, это время экспериментов и первых побед — не надо тратить ее на алкогольные трипы и разрисовывание лица.
Если вы хотите попасть с порога в Газпром — сделайте (например) симулятор нефтедобывающей вышки, со сложными расчетами и оптимизацией процессов. Если хотите работать в Росатоме — создайте (навскидку) модель реактора из Чернобыля с расчетом причин аварии и симуляцией.
Даже если это окажется в итоге полной херней — вас запомнят и вполне возможно возьмут на хорошую работу.
Собственно изначальная идея дипломной работы — демонстрация ваших компетенций, уникальных талантов и уровня знания и автору известны случаи, когда особо эпическая дипломная работа открывала путь в светлое будущее.
PS
Это не последняя статья про уникальную и талантливую молодежь, действительно заслуживающую внимания широкой аудитории и почета с уважением от коллег по отрасли — автор и дальше будет целенаправленно показывать как выглядят приличные люди в нашей индустрии и чего стоит их опыт и талант.
А вы дорогой читатель, сможете сравить и более адекватно оценивать собственные перспективы.
Статья была опубликована на Хабре, менее цензурный оригинал как обычно в нашем блоге. Добавлю что мы пытались связаться и с самим Коннором, но ответа не поступило.
Продолжаю тему отбитого ретрогейминга для "особенных" пользователей, показываю только что собранную из исходников первую Diablo, запущенную на моей домашней FreeBSD:
Буднично рассказываю как локализовать обычное корпоративное приложение на нечеловеческие языки: Клингонский и Р’льех.
На этом скриншоте куда больше реального приложения чем кажется на первый взгляд.
Эээ.. думаю стоит начать с демонстрации результата — той самой нереальной локализации, ради которой все это и затевалось, чтобы всем сразу "все стало понятно".
Так выглядит версия на клингонском:
Обратите внимание на даты — это настоящий Stardate.
А вот так выглядит версия на Р'льех:
«Cthulhu fhtagn!» на JSF, CDI и JPA. Сложно сказать какая часть предложения напугает сильнее.
Ну и наконец банальный английский:
Вот так выглядит в работе переключение локализации:
Да, это самое обычное веб-приложение на Java, работающее в обычном браузере.
Но только с локализацией на клингонский и Р'льех.
Матчасть
Чтобы вы смогли оценить сложность задачи «локализации на язык которого нет», стоит для начала рассказать как происходит обычная локализация — на обычные человеческие языки.
Возьмем для примера классику в виде русско‑английской локализации, вот что необходимо реализовать в этом случае:
Определение текущей локали
Переключение локали
Хранение локализованных строк
Отображение локализованных данных
Данный функционал подразумевается как минимальный, когда речь заходит о локализации ПО, причем большая часть всей этой логики уже реализована в любом современном инструментарии и все что нужно сделать для поддерживаемых языков — «включить и использовать».
Вот так например выглядит хранение локализованных строк:
Это абсолютно стандартный способ, поддерживаемый как самим JDK так и всем прикладным ПО на Java
Также легко и просто оперировать обычным человеческим языком со стороны прикладного кода, например вот так выглядит получение локали из кодового названия:
Locale locale = Locale.forLanguageTag("en_US");
Где en — это указание на английский а US — на страну США.
Не менее легко происходит и переключение между языками (в данном случае в Jakarta Faces):
Но вся эта благодать быстро заканчивается, стоит только выйти за границу реальности поддерживаемых локалей и попытаться использовать «то чего нет».
Язык которого нет
Символы несуществующих фантастических языков предсказуемо отсутствуют в официальной таблице символов Unicode, их нет в списке поддерживаемых средствами разработки и нет в браузере.
Что означает невозможность какой-либо работы «из коробки» с таким языком — без специальных шагов.
Но прежде хотелось бы немного рассказать о самих фантастических языках, выбранных для локализации — чтобы у вас появилось некоторое представление куда может завести фанатизм и любовь к хардкору.
В мире где настоящие человеческие языки отмирают по сотне в день по мере ухода из жизни последних носителей, кто-то специально учит вымышленный!
Поскольку большинство фанатов клингонского — самые разнообразные гики, хорошо дружащие с техникой и матчастью, было и есть множество попыток протащить вымышленный язык куда только можно.
Например в ядро Linux:
In September 1997, Michael Everson made a proposal for encoding KLI pIqaD in Unicode, based on the Linux kernel source code. The Unicode Technical Committee rejected the Klingon proposal in May 2001
September 1997: first Unicode proposal for pIqaD.1 May 2001: Rick McGowan submits Proposal to Reject Klingon May 2001: Proposal to reject Klingon adopted by UTC (minutes) November 2016: New Proposal for Encoding Klingon, showing lots of examples of usage July 2020: Another New Proposal for Encoding Klingon. This one uses the correct “Klingon” names for the letters. August 2021: Request to Remove Klingon from Non-Approval List, made in accordance with Ken Whistler’s suggestion from 2016, linked above.
Как видите фанаты "Star Trek" крайне упертые товарищи, которые уже второй десяток лет продолжают упорно осаждать двери офиса по адресу:
611 Gateway Blvd. Suite 120
в Сан‑Франциско CA 94 080, где и располагается «The Unicode Consortium». Кстати вы также можете позвонить в консорциум Unicode на их офисный номер:
+1-408-401-8915
и поинтересоваться почему клингонский до сих пор не включен в официальный набор символов — дело же важное.
Удивительно (или нет), но в Microsoft тоже любят клингонский, настолько что добавили его поддержку в свой онлайн-переводчик:
Именно его я использовал для клингонского перевода.
При таком интересе технически продвинутой общественности, очень быстро появились готовые TTF-шрифты, использующие PUA область:
Since then several fonts using that encoding have appeared, and software for typing in pIqaD has become available
Это важный момент, поскольку такой шрифт позволяет комбинировать символы клингонского со всеми остальными, например одним шрифтом можно отобразить и английский и клингонский.
Вот так выглядит клингонский алфавит:
Обратите внимание на соответствие одного глифа клингонского сразу нескольким на английском — это влияет на реализацию транслятора (см. ниже).
Р'льех
С языком древнихР’льех все обстоит куда проще — этот также полностью выдуманный язык, приверженцы которого живут под водой и к счастью мало интересуются продвижением своего фантастического языка в широкие массы.
С названием есть небольшая неточность:
Cthuvian, which is also called R'lyehian, is a fictional language created by H. P. Lovecraft in "The Call of Cthulhu" and expanded upon by various authors.
Дословный перевод — «ктулхский» или «р'льехский», что (да простят меня подводные боги) показалось не очень благозвучным.
Поэтому я использовал термин Р'льех, который на самом деле означает иное:
Н'ЯРЛАФОТЕП — отличное название для нового проекта, не находите?
Доступные TTF-шрифты для Р'льех не используют PUA-область Unicode, поэтому применение такого шрифта превратит все символы в месиво:
Обратите внимание на поле ввода - текст в нем визуально на Р'льех, хотя введены символы английского.
Но если переключиться на клингонский, будет виден ввод символов на нормальных языках:
В этом и заключается главная сила PUA-области и ее главная фишка.
Будете создавать локализацию на древнеегипетский или руническое письмо викингов — обязательно используйте шрифт с PUA-областью.
Так выглядит проект из среды разработки.
Тестовый проект
Для статьи был специально выбран самый «тру‑энтерпрайз» стек, чтобы показать насколько далеко продвинулись технологии локализации. Это не какие-то околонаучные экспериментальные языки или малоизвестные специализированные фреймворки и не дикий «low level» с песьеголовыми программистами на С, это самый настоящий технологический мейнстрим — тот вид разработки и набор технологий, с которыми вы (если занимаетесь разработкой) сталкиваетесь каждый день:
представьте любимый клиент-банк с локализацией на клингонском.
Разумеется будет много специфики именно для Java и выбранных технологий, но описанные идеи и подходы очень даже применимы и для большинства других языков и решений.
Вот что в меню:
JakartaEE 10, который в девичестве назывался JavaEE а в далеком детстве J2EE.
В качестве сервера приложений был взят IBM OpenLiberty — современный открытый потомок большой IBM Websphere Application Server, который IBM ныне продвигает в светлое корпоративное будущее как платформу для разработки микросервисов.
Технически тестовый проект представляет собой веб‑приложение (WAR), которое разворачивается на сервере приложений и по полной использует его ресурсы — все как в золотые годы JavaEE.
Но чтобы не загонять читателей в классические мытарства с установкой и развертыванием — был добавлен автозапуск приложения с автоматическим развертыванием (как в Spring Boot).
Внутри классика корпоративной разработки:
JPA, CDI, JSF и новое Servlet API 6 — уже полностью на аннотациях.
Все прямо как на настоящей работе в банке, где деньги платят.
И сейчас мы будем локализовывать все это на выдуманный язык из фантастического сериала 1970х.
Но прежде опишу стандатное — сборку и запуск.
Сборка
Для сборки используется обычный Apache Maven и последняя версия JDK (22+), забираем проект из репозитория:
Готовое приложение будет находиться в каталоге target:
В каталоге liberty находится распакованный сервер приложений Open Liberty, с установленным внутрь нашим приложением — за все эти радости отвечает специальный плагин (см. ниже).
Запуск
Как уже упоминалось выше, наш замечательный проект предназначен для запуска и работы на сервере приложений IBM Open Liberty.
Разумеется вы можете сходить по ссылке выше, прокрутить страницу вниз до раздела Releases, скачать версию 24.0.0.6+ с профилем Jakarta EE 10, развернуть и затем установить туда наше приложение.
Для настоящего развертывания в корпоративной среде обычно и делают. По крайней мере делали до эры докера.
Но поскольку у нас тут технологическое демо, я посчитал что все эти шаги по развертыванию будут слишком сложными и добавил в сборку специальный плагин для автоматического развертывания и запуска.
Одной командой:
mvn liberty:dev
Произойдет скачивание IBM Open Liberty, распаковка, настройка, установка внутрь нашего приложения и немедленный запуск.
Вот так это выглядит из среды разработки Intellj Idea:
Начну с самого главного вопроса — с отображения символов несуществующего фантастического языка. Взгляните:
Нет это не галлюцинации или фотошоп, это установленный правильный TTF-шрифт клингонского в системе.
На скриншоте выше стандартный gedit, в настройках которого был задан клингонский шрифт для отображения основной части. Как видите использование PUA‑области Unicode в шрифте позволяет неплохо дружить символы обычного и фантастического языков.
Если приглядитесь — увидите сглаживание, работающее даже для глифов клингонского.
К сожалению для Р'льех не нашлось шрифта, использующего PUA‑область Unicode, поэтому при отображении происходит замена всех символов глифами Р'льех:
Тут все служат подводным богам, без исключений.
К сожалению нехватило времени для разработки с нуля шрифтов двух несуществующих языков, поэтому были взяты готовые.
Для клингонского:
Klingon pIqaD Mandel takes the Klinzhai or Mandel font glyphs (really a different alphabet from the KLI’s Standard pIqaD) and refits them for use as pIqaD.
I created this font based on the description by H.P. Lovecraft. Click here to download the Rlyehian font package, which includes two version of the font and a guide to understanding its use.
Но для полноты картины, все же расскажу как происходит разработка новых шрифтов, если вдруг вам понадобится локализовать проект скажем на дотракийский.
FontForge
Уже достаточно давно и успешно существует отличный открытый редактор шрифтов:
FontForge is a FOSSfont editor which supports many common font formats. Developed primarily by George Williams until 2012, FontForge is free software and is distributed under a mix of the GNU General Public License Version 3 and the 3-clause BSD license.[2] It is available for operating systems including Linux, Windows,[3] and macOS,[4] and is localized into 12 languages
Редактор мощный и доступный практически для любых ОС — его возможностей точно хватит с запасом, по крайней мере для стадии прототипирования и любительской работы со шрифтами.
Вот так выглядит клингонский шрифт, открытый в этом редакторе:
Обратите внимание на фразу «Private Use Area» — она означает что глифы клингонского расположены именно в PUA‑области.
Вот так выглядит процесс редактирования отдельного символа:
Имейте ввиду что это долгий и утомительный процесс, особенно если речь про разработку шрифта с нуля.
А вот так для сравнения выглядит шрифт для Р'льех:
Как видите тут не используется PUA и заменяются символы ASCII, с самого начала таблицы.
Для полного погружения, вот так выглядит редактирование одного из этих стильных глифов:
И ведь кто-то сидел и рисовал это. Воистину воля подводных богов безгранична.
Разумеется, можно было потратить какое‑то время и перенести глифы Р'льех в PAU‑область, что позволило бы использование шрифта по аналогии с клингонским — параллельно с другими языками.
Но к сожалению я не верю в Ктулху обладаю достаточным запасом времени и сил, так что оставил как есть.
На самом деле есть еще одна важная причина — показать вам два подхода к локализации, а не один:
второй вариант реализации шрифта с полной заменой всех символов на безумные иероглифы чем-то фантастическим (без использования PAU-области) встречается куда чаще.
Его точно стоит учитывать, поскольку скорее всего именно с таким шрифтом вы и столкнетесь, пытаясь работать с фантастическими языками.
Отображение в браузере
Отдельно опишу как происходит отображение этих фантастических языков в браузере — поскольку мы используем веб, а не отдельное десктоп-приложение.
Все современные браузеры поддерживают регистрацию и использование пользовательских шрифтов на странице — это мягко говоря не новость.
Регистрация TTF‑шрифта происходит путем использования CSS‑стиля и специальной директивы font‑face:
Сложно выглядящая директива #resource[''] на самом деле уже часть парсера страниц JSF — EL-выражение, преобразующее относительный путь к указанному ресурсу в полный.
А вот так выглядит задание отдельных стилей для использования наших фантастических шрифтов:
.klingon { font-family: 'Klingon'; }
Эти стили применяются выборочно, для включения фантастического шрифта при включенной перекодировке у сообщения:
Если сообщение было написано на клингонском pIqaD — оно будет пропущено через транслятор (см. ниже) и при отображении будет использован клингонский TTF‑шрифт.
Таким образом сохраняется обратная совместимость с другими языками и остается возможность ввода на обычном английском.
Но это решение только для отдельных блоков сообщений, ведь есть еще глобальное переключение выбранной локали:
Для решения этой задачи, используется вот такая логика:
Звездочка (*) означает что указанный шрифт должен быть применен ко всем элементам на странице, что и дает вот такой эффект глобальной локализации всего:
Также тут задается фоновая картинка в немного странном формате:
klingon.jpg.xhtml
На самом деле файл называется klingon.jpg и находится в каталоге webapp/resources, а постфикс .xhtml — особенность работы ресурсов в JSF, он нужен для правильной работы, хотя и выглядит полной дичью.
Переходим к следующей важной теме.
Транслятор
При локализации на несуществующий и неподдерживаемый язык существует еще одна проблема:
необходимо как-то работать с локализованным на такой язык текстом из стандарного окружения.
Конечно можно попробовать ставить шрифты, поддерживающие ваш фантастический язык в каждый используемый редактор, каждый терминал и среду разработки — да, это будет работать (см. ниже).
Но с точки зрения промышленной разработки это плохой путь — любая ошибка приведет к тому что вы не сможете увидеть локализованный текст вообще, либо он будет отображаться неправильно.
Если очень повезет, то пойдя этим путем можно получить что-то такое:
Круто, но слишком сложно и не подходит для массовой разработки — когда задействовано много разработчиков.
Есть способ лучше. Дело в том что ни один, даже трижды фантастический язык не существует в вакууме — для него в обязательном порядке создается:
Транслитера́ция (лат. trans- «через; пере-» + littera — «буква») — точная передача знаков одной письменности знаками другой письменности[1][2], при которой каждый знак (или последовательность знаков) одной системы письма передаётся соответствующим знаком (или последовательностью знаков) другой системы письма.
Даже если речь про например дотракийский — выдуманный сценаристами язык кхала Дрого из «Игры Престолов», к нему все равно в качестве приложения идет транслитерация на английском — актерам надо как-то учить произношение.
Более того, такая транслитерация существует и для самих человеческих языков, причем видимо для всех (исключений пока не встречал).
Например есть широко известный вариант написания кириллицы с помощью символов латиницы:
Нет людей в рунете старше 30ти, которые бы его никогда не видели.
Собственно транслит встречается до сих пор — стоит только сломаться мультиязычному вводу на вашем компьютере или телефоне и все — вам тоже придется его использовать.
Именно транслитерацию в латинские символы мы и будем использовать.
Да это «Гамлет» на клингонском — а что вы знаете о фанатизме?
pIqaD
Вариант написания клингонского латинскими символами называется pIqaD, конечно же он куда более широко распространен и популярен чем те сложные клингонские иероглифы, которые я с таким трудом отображал выше.
Думаю не стоит упоминать, что при такой популярности есть и устоявшиеся правила транслитерации и (что куда более важно) — готовые наработки. Очень быстро были найдены и готовые трансляторы, самый популярный (из открытых) выглядит вот так:
На основе его исходного кода (на Javascript) была написана моя реализация на Java, с помощью которой вот такие строковые ресурсы:
Превращаются во время работы приложения в те самые фантастические иероглифы:
Как видите тут происходит достаточно простая замена символов согласно таблице подстановки, с латинских на Unicode из PAU‑области — все внешне сложное, на самом деле устроено очень просто.
Один из немногих оригиналов документов на Р'льех.
К сожалению (или к счастью — в зависимости от контекста), фантастический язык Р’льех из миров Лавкрафта куда менее популярен, поэтому получилось найти всего один рабочий транслятор:
Using the digital serpent's package, you can translate english to the language of the "old ones" Spread aimgr'luh
Занимается им некий китайский DevOps-инженер (надеюсь не в рамках должностных обязанностей), сам транслятор и написан на Python:
Важным моментом является другой принцип работы — вместо транслитерации символов происходит подстановка слов или даже целых фраз:
Вся логика была портирована в мой проект, мою реализацию транслятора для Р'льех можно посмотреть вот тут. Разумеется с таким подходом в виде зашитого и очень небольшого словаря, нет возможности реализовать перевод технических терминов:
у меня честно нет идей как могут выглядеть слова «Авторизация», «Назад» или «Сохранить» на языке древних.
Поэтому транслятор Р’льех используется только для ввода текста — чтобы найти истинных последователей показать как это работает.
Но перейдем к следующей интересной теме.
Нереальная локаль
Следующей проблемой при работе с фантастическими языками является их регистрация в системе — в том языке, платформе или фреймворке, который вы используете.
Это нужно в первую очередь для того, чтобы как‑то сигнализировать внутри приложения о том что используется такой фантастический язык и проводить соответствующую подстройку — например вызывать тот самый транслятор, описанный выше.
Тут может быть огромное количество вариантов, проблем и подводных камней, поскольку такой разработкой мы выходим за рамки обыденного поддерживаемого. И при возникающих проблемах вам скорее всего никто не поможет — кроме нас разумеется.
Но для Java весь процесс более-менее отработан, описан и предсказуем:
в Java у локалей есть поддержка т. н. «variant» — специальной вариации языка, которая может быть сколь угодно нестандартной.
Сама локаль остается системной (в данном случае — английской), но при этом к ней добавляется специальный постфикс, означающий что используется «вариация»:
Поскольку такие variants являются частью официального API, они поддерживаются всем прикладным ПО и библиотеками (за редкими исключениями).
В том числе они используются в механизме работы ResourceBundle:
Если включить «variant» в название файла с ресурсами — он будет найден и загружен при выборе локали с таким «variant». К сожалению стандартной реализации ResourceBundle оказалось недостаточно — хотелось получить перекодированный клингонский сразу из ресурсов, поэтому я сделал свою:
package com.Ox08.experiments.kligon; import jakarta.annotation.Nonnull; import jakarta.faces.context.FacesContext; import java.util.Enumeration; import java.util.Locale; import java.util.ResourceBundle; import java.util.logging.Level; import java.util.logging.Logger; /** * Extended resource bundle, used to inject Klingon glyphs if Klingon locale * used * * @Author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a> */ publicclass KlingonedResourceBundle extends ResourceBundle { public KlingonedResourceBundle() { setParent(ResourceBundle.getBundle("i18n.messages", FacesContext.getCurrentInstance().getViewRoot().getLocale())); } @override publicfinalvoid setParent(ResourceBundle parent) { super.setParent(parent); } @override protectedObject handleGetObject(@Nonnull String key) { // here will be extracted and substituted value finalObject v = parent.getObject(key); if (!(v instanceofString vstring)) { return v; } LOG.log(Level.INFO, "handleGetObject : {0}", vstring); // current locale final Locale l = FacesContext.getCurrentInstance().getViewRoot().getLocale(); // check if its Klingon and transliterate to glyphs if ("KLINGON".equals(l.getVariant())) return KlingonTranslator.transliterate(vstring); // .. and for Rlyeh if ("RLYEH".equals(l.getVariant())) return RlyehTranslator.translate(vstring);
Основное действие происходит в методе handleGetObject() ,сейчас разберу логику этого метода по шагам, благо она будет повторяться и в других местах.
Первым шагом происходит вызов такого же метода, но из родительского класса — для получения еще не перекодированного текстового шаблона:
final Object v = parent.getObject(key);
Затем происходит отбраковка по возвращаемому типу — мы работаем только со строками и все остальные варианты пропускаем:
if (!(v instanceofString vstring)) { return v; }
Дальше происходит получение текущей локали пользователя:
final Locale l = FacesContext.getCurrentInstance() .getViewRoot().getLocale();
Что несколько неправильно с точки зрения архитектуры большой системы, но достаточно для демо проекта.
Затем в зависимости от значения «variant» вызывается перекодировщик для клингонского:
if ("KLINGON".equals(l.getVariant())) return KlingonTranslator.transliterate(vstring);
или для Р'льех:
if ("RLYEH".equals(l.getVariant())) return RlyehTranslator.translate(vstring);
Регистрация кастомной реализации ResourceBundle задается в файле с настройками Jakarta Faces (webapp/WEB-INF/faces-config.xml):
.. <resource-bundle> <!-- Note that 'base name' points to specific class, not to .properties file --> <base-name>com.Ox08.experiments.kligon.KlingonedResourceBundle</base-name> <var>msgs</var> </resource-bundle> ..
Там же указывается список поддерживаемых локалей, с учетом «variants»:
Но это еще не все интесное и необычное, что хотелось бы раскрыть в рамках статьи.
Валидация данных
Как каша без масла протеина или водка без закуски — не бывает корпоративных приложений без валидации данных.
В Jakarta EE (как и в ее предшественнике JavaEE) для автоматической валидации входных данных используются механизмы из спецификации JSR 303 «Bean Validation».
В самом простом случае это выглядит как аннотирование полей класса:
.. @size(min = 3, max = 255) privateString title; // a title @NotBlank(message = "{validation.message.not-blank}") @Lob @column(length = Integer.MAX_VALUE) privateString message; // message, stored as CLOB in database, //so size is almost unlimited @size(min = 3, max = 30) @email privateString author; // author's email ..
Когда такой класс попадает в качестве входящего аргумента метода класса, управляемого CDI‑окружением, срабатывает автоматическая валидация и в интерфейсе появляются сообщения об ошибках:
Если ошибка имеет привязку к конкретному полю, за ее отображение отвечает отдельный блок:
<h:message for="f_message" errorClass="msg" />
если нет — она отображается через «глобальную свалку»:
Вместо текста сообщения, тут указан некий код, который автоматически заменяется на текст из специального ResourceBundle:
Согласно спецификации JSR303 название для бандла должно быть именно ValidationMessages.
Вся эта логика является частью спецификации JSR303 и вообщем-то отлично работает без вашего участия — до тех пор пока не появляется необходимость сотворить какую-нибудь дичь.
К сожалению поддержка несуществующих языков в текстах сообщений об ошибках является именно такой дичью:
Текст красненьким — та самая валидация JSR303. На клингонском.
Поэтому придется немного подумать.
После долгих поисков и изучения документации, все же был найден способ вклиниться в процесс получения текстов сообщений с ошибками валидации:
Message interpolators are used by the validation engine to create user readable error messages from constraint message descriptors.
В итоге была написана собственная реализация такого «интерполятора»:
package com.Ox08.experiments.kligon; import jakarta.validation.MessageInterpolator; import jakarta.validation.Validation; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; /** * Custom JSR 303 Message Interpolator, used to inject Klingon glyphs * into JSR303 validation * * @author <a href="mailto:alex3.145@gmail.com">Alex Chernyshev</a> */ publicclass JSR303KlingonMessageInterpolator implements MessageInterpolator { // we need to have existing MessageInterpolator, // to being used as parent privatefinal MessageInterpolator delegate; public JSR303KlingonMessageInterpolator() { // take default implementation from JSR303 configuration this.delegate = Validation.byDefaultProvider() .configure().getDefaultMessageInterpolator(); } @Override publicString interpolate(String string, Context cntxt) { LOG.log(Level.INFO, "interpolating {0}", string); // without specified locale - just pass interpolation to delegate return delegate.interpolate(string, cntxt); } @Override publicString interpolate(String string, Context cntxt, Locale locale) { LOG.log(Level.INFO, "interpolating {0} with locale: {1}", newObject[]{string, locale.toLanguageTag()}); // here will be extracted and substituted value finalString result = delegate.interpolate(string, cntxt, locale); // check for Klingon locale and transliterate to glyphs if ("KLINGON".equals(locale.getVariant())) return KlingonTranslator.transliterate(result);
if ("RLYEH".equals(locale.getVariant())) return RlyehTranslator.translate(result);
Основная магия логика заключается вот в этих строках:
.. finalString result = delegate.interpolate(string, cntxt, locale); // check for Klingon locale and transliterate to glyphs if ("KLINGON".equals(locale.getVariant())) return KlingonTranslator.transliterate(result);
if ("RLYEH".equals(locale.getVariant())) return RlyehTranslator.translate(result);
return result;
Как видите, локаль поступает на вход метода в готовом виде — ее не надо определять из контекста JSF, а вот в этом месте происходит получение оригинальной строки из файла с текстовыми строками:
final String result = delegate.interpolate(string, cntxt, locale);
Дальше в зависимости от наличия «variant» у локали, текст либо пропускается через транслятор либо отдается «как есть».
Регистрация кастомного интерполятора также имеет свою специфику — она происходит в отдельном XML-файле:
Который находится в файле src/main/resources/META-INF/validation.xml
Последней интересной темой, достойной освещения в рамках статьи про локализацию будут фантастические даты.
Заметьте — не просто фантастический формат отображения а целый календарь!
Фантастические даты
Никогда не задумывались какой смысл закладывается в дату?
Что такое на самом деле 2024й год?
Фактически это означает что прошло 2024 года с рождения Иисуса Христа (по новому летоисчислению), что возможно не очевидно некоторым представителям молодого поколения, но вполне достаточно для жизни и работы цивилизации.
А что если вам надо использовать альтернативную систему расчета времени?
Миллион лет от последнего динозавра?
40 000 лет бесконечной войны?
Озадачившись данным вопросом, я решил что неплохо было бы реализовать для фантастического языка еще и фантастическое летоисчисление. И использовать его для обычного корпоративного приложения, да.
A stardate is a fictional system of time measurement developed for the television and film series Star Trek. In the series, use of this date system is commonly heard at the beginning of a voice-over log entry, such as "Captain's log, stardate 41153.7.
Именно ее поддержку я и решил реализовать:
За основу был взят фанатский проект с реализацией StarDate на куче разных языков, оригинальный код был сильно уменьшен и почищен.
Но одной только реализации кастомного календаря оказалось мало — нужен еще один класс-конвертер, реализующий непосредственно конвертацию дат с этим календарем:
package com.Ox08.experiments.kligon; import jakarta.faces.component.UIComponent; import jakarta.faces.context.FacesContext; import jakarta.faces.convert.Converter; import jakarta.faces.convert.FacesConverter; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; importjava.util.Date; import java.util.Locale; /** * A custom converter for StarDate * @author alex0x08 */ @FacesConverter(value = "stardateConverter") publicclass StarDateConverter implements Converter<Date> { @Override public Date getAsObject(FacesContext fc, UIComponent uic, String string) { final Locale l = fc.getViewRoot().getLocale(); if ("KLINGON".equals(l.getVariant())) return StarDate.parseStarDate(string).getDate();
return Date.from(ZonedDateTime.parse(string, DateTimeFormatter.ISO_DATE_TIME .withZone(ZoneId.systemDefault())).toInstant()); } @Override publicString getAsString(FacesContext fc, UIComponent uic, Date t) { final Locale l = fc.getViewRoot().getLocale(); if ("KLINGON".equals(l.getVariant())) return StarDate.newInstance(t).toString(); return DateTimeFormatter.ISO_DATE_TIME .withZone(ZoneId.systemDefault()) .format(t.toInstant()); } }
Активируется этот конвертер автоматически благодаря наличию аннотации:
@FacesConverter(value = "stardateConverter")
И автоматически же применяется для всех полей с типом Date, проходящих через бины, управляемые CDI.
Внутри уже традиционная логика получения текущей локали:
final Locale l = fc.getViewRoot().getLocale();
Затем при наличии клингонского «variant» происходит либо преобразование из объекта в строку с учетом кастомного календаря:
if ("KLINGON".equals(l.getVariant())) return StarDate.parseStarDate(string).getDate();
либо из строки в объект (также с учетом StarDate):
if ("KLINGON".equals(l.getVariant())) return StarDate.newInstance(t).toString();
На этом красивая история о нереальном подходит к концу, подведем итоги.
Итоги и выводы
В современных реалиях и с использованием современных инструментов нет серьезных препятствий для локализации на любые неведомые языки — искусственные или настоящие.
Отсутствие официальной поддержки «из коробки» в инструментах разработки и даже отсутствие символов в таблице символов Unicode — не является проблемой для настоящего джедая.
Первым шагом необходимо разработать или найти готовый TTF‑шрифт для вашего языка и проверить его отображение в системе и браузере — если планируется веб‑разработка.
Следующим шагом необходимо реализовать либо взять готовые правила транслитерации вашего фантастического языка символами существующего — кириллицей, латиницей и так далее. И написать соответствующий транслятор символов.
Вся дальнейшая работа сведется к включению транслятора в ключевых местах проекта.
На свете есть не так много вещей, способных выбесить программиста. И лишь одна делает это с гарантией: оборзевшая машина, возомнившая себя умнее человека.
А значит снова пришло время карать и патчить!
Видите эти повторяющиеся записи справа? Так будет выглядеть буфер сообщений ядра (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 разумеется никакой возможности нет.
Бывает что на руках есть лишь «бинарная» сборка сайта на модном фреймворке вроде 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!'); });
Результат:
Вместо отправки на сервер вызывается наш обработчик.
Эпилог
В общем случае действительно стоит отказаться от попыток доработки обфрусцированных бандлов — это «путь в никуда», решение которое невозможно поддерживать долго и рано или поздно оно все равно сломается, похоронив под собой и все ваши доработки.
Только в исключительных случаях, когда действительно горит и надо «поправить вчера» на такое имеет смысл идти.