C++ инжект модов в Electron - эволюция решения

Summary

Если совсем коротко: я хочу уйти от переделывания файлов Яндекс Музыки и прийти к отдельному лаунчеру. Пользователь запускает не модифицированный клиент, а маленькую программу-обертку. Она открывает обычную Яндекс Музыку, аккуратно подключает моды во время запуска и хранит сами моды отдельно от файлов приложения.

YMusic Bootstrap - эволюция подходов.excalidraw

О чем вообще речь

Яндекс Музыка на Windows сделана как Electron-приложение. Это значит, что внутри обычного .exe живет веб-интерфейс: JavaScript, CSS, HTML и немного нативной обвязки. Поэтому многие визуальные доработки технически похожи не на “взлом программы”, а на расширение сайта: добавить стили, изменить поведение кнопок, повесить наблюдатель на появление элементов, открыть свою панель настроек.

Проблема начинается там, где лежат эти файлы. В Electron они часто упакованы в архив app.asar. Раньше самый прямой способ моддинга был такой: распаковали архив, добавили свои файлы, собрали обратно. Для первых экспериментов это удобно. Для нормальной жизни - так себе.

Почему:

  • после обновления клиента все приходится проверять заново;
  • структура файлов внутри app.asar может поменяться;
  • Electron может заметить, что архив уже не тот;
  • моды оказываются перемешаны с официальными файлами приложения;
  • пользователю сложнее объяснить, что именно было изменено и как это откатить.

Идеальная цель звучит проще: официальный клиент лежит как лежал, а моды подключаются снаружи.

Как менялась идея

Сначала решение было довольно грубым: поменять app.asar и положить моды прямо внутрь клиента. Это работало, но каждый апдейт превращался в маленькую лотерею. Сегодня файл называется так, завтра иначе, послезавтра Electron ругается на целостность.

Потом появилась мысль обойти проверку целостности. То есть не спорить с тем, что архив изменен, а заставить Electron не падать из-за этого. Для исследования полезно, но для обычного пользователя выглядит слишком тяжело: патчить .exe, зависеть от версии Electron, надеяться, что очередное обновление не сдвинуло нужные байты.

Следующая идея была красивее: не трогать файлы на диске вообще, а подменять их “на лету” через DLL. Программа думает, что читает свой обычный файл, а мы в нужный момент отдаём другой JavaScript. Но тут выяснилась важная деталь: Electron не открывает main/index.js внутри app.asar как отдельный файл.

Он открывает сам архив app.asar, смотрит таблицу внутри него, находит нужный диапазон байтов и читает именно этот кусок. Поэтому простой перехват по имени main/index.js почти бесполезен: такого открытия на уровне Windows может вообще не быть.

YMusic Bootstrap - как Electron читает app.asar.excalidraw

После этого появился более умный вариант: ASAR-aware hook. Он уже понимает, где внутри архива лежит нужный файл, и подменяет не “путь к файлу”, а конкретный диапазон байтов. Инженерно это интересно, но для первой версии слишком капризно. Нужно правильно учитывать смещения, ReadFile, ReadFileEx, SetFilePointerEx, OVERLAPPED, закрытие и дублирование handles. Ошибка в такой логике может сломать запуск клиента очень неприятным способом.

Поэтому финальное направление сейчас другое: отдельный bootstrap launcher.

Что такое bootstrap launcher по-человечески

Представь обычный ярлык, только чуть умнее. Ты нажимаешь “запустить модифицированную Музыку”, а дальше происходит примерно следующее:

  1. Лаунчер находит установленную Яндекс Музыку.
  2. Проверяет, что рядом есть свежий набор модов.
  3. Запускает обычный клиент.
  4. Дожидается нужного процесса Electron.
  5. Подключает маленький нативный bootstrap.
  6. Bootstrap уже в окне клиента запускает JS/CSS-моды.

Главная разница с патчем app.asar: моды живут отдельно. Например, в папке вроде:

%AppData%/YMusicBootstrap/payload/

А внутри нее уже лежит понятная структура:

payload/
  manifest.json
  runtime/
    bootstrap.js
    bootstrap.css
  mods/
    some-mod/
      index.js
      style.css

То есть сам клиент не превращается в “особую сборку”. Он остается клиентом, а моды становятся внешним слоем поверх него.

YMusic Bootstrap - архитектура запуска.excalidraw

Что сможет первая версия

Первая версия не должна обещать все на свете. Самый здоровый вариант - начать с того, что реально можно поддерживать стабильно:

  • визуальные правки интерфейса;
  • CSS-темы;
  • небольшие JS-моды для renderer-части;
  • наблюдение за изменениями страницы;
  • реакция на переходы внутри приложения;
  • простая панель настроек для модов;
  • включение и отключение модов без пересборки клиента.

Это значит, что V1 больше похожа на систему внешних UI-модов, а не на полную замену старого app.asar-патча.

Минимальный SDK может выглядеть примерно так:

window.__YMBootstrap = {
  register(mod),
  injectStyle(id, css),
  onRouteChange(callback),
  observe(selector, callback),
  getConfig(modId),
  logger
}

Для автора мода смысл простой: не нужно думать, куда встроиться в main/index.js или как пережить очередной апдейт архива. Мод регистрируется через общий API, добавляет стили или поведение и работает в renderer-окне.

Что переносить из старого ModYandexClient

Из старого проекта стоит брать не все подряд, а только то, что нормально живет в интерфейсе:

  • DOM/UI helpers;
  • CSS injection;
  • повторное применение правок через MutationObserver;
  • обработчики переходов в SPA;
  • overlay для настроек;
  • чистые функции, которые меняют внешний вид или поведение страницы.

А вот это лучше не тащить в первую версию:

  • прямую работу с electron и ipcRenderer;
  • main-process hooks;
  • deep links;
  • FFmpeg updater;
  • старый modUpdater;
  • функции, которым нужен измененный preload.js.

Не потому что это невозможно навсегда. Просто если сразу пытаться перенести весь старый мир, первая версия утонет в совместимости. Лучше сначала сделать небольшой, понятный и надежный слой для UI-модов.

Чем это будет лучше для обычного пользователя

Пользователю не нужно знать, что такое ASAR, preload или Electron fuse. В идеале весь опыт должен быть таким:

  1. Установил bootstrap.
  2. Запустил Яндекс Музыку через новый ярлык.
  3. Включил нужные моды.
  4. Если что-то сломалось - выключил мод или запустил обычный клиент без bootstrap.

Это важный момент. Старый подход менял внутренности клиента, и из-за этого было сложнее понять, где заканчивается официальная программа и начинается мод. Новый подход делает границу видимой: вот клиент, вот внешний слой модов, вот папка с payload.

Таблица решений

ВерсияИдеяЧто далаПочему не финал
V0Патчить app.asarБыстро заработали первые модыЛомается на обновлениях
V1Отключать integrity/fuseМожно обойти проверку архиваСлишком зависит от Electron и .exe
V2Перехватывать файловые APIНе надо менять файлы на дискеНаивный hook не видит файлы внутри ASAR
V3Понимать ASAR и подменять байтыТехнически точнееСложно и рискованно для первой версии
V4Внешний launcher + payloadМоды отдельно от клиентаЭто выбранное направление

Планы

План я бы разделил на несколько спокойных этапов.

Сначала - собрать минимальный launcher: найти клиент, запустить его, дождаться процесса, подключить bootstrap и вывести понятный лог. Без магии и лишнего UI.

Потом - сделать payload-слой: manifest.json, загрузку bootstrap.js, подключение CSS, регистрацию модов и безопасное выключение проблемного мода.

После этого - нормальный SDK для авторов модов: события роутов, наблюдение за DOM, настройки, логгер, версионирование API.

Дальше - менеджер модов: установка, обновление, включение/выключение, совместимость с версиями клиента, понятные ошибки для пользователя.

И только после этого есть смысл возвращаться к более глубоким вещам: интеграции с preload/main, ASAR-aware hook как fallback, расширенным возможностям, которые нельзя сделать только через renderer.

YMusic Bootstrap - дорожная карта.excalidraw

Итог

Самое правильное направление сейчас - не пытаться бесконечно чинить патчинг app.asar. Это был хороший этап для исследования, но плохая основа для удобного инструмента.

Новая архитектура должна быть проще для пользователя и честнее по устройству: официальный клиент отдельно, моды отдельно, запуск через launcher, вся логика модов в payload. Первая версия пусть будет скромной, зато устойчивой: UI, JS, CSS, настройки и нормальный способ отключить то, что сломалось.

А ASAR-aware hook я бы оставил как инженерный запасной путь. Он полезен для понимания Electron и может пригодиться позже, но не должен быть фундаментом первой публичной версии.