NodeJS + SocketIO + Linux = сервер генерации скриншотов сайтов
пост написан и отправлен в печать 2011-07-29 примерно в 17:10
Сегодня мы поговорим о разработке сервиса создания скриншотов сайтов. Так получилось, что я занимался этим для одного интранетовского проекта и, получив благословение и разрешение добродушного заказчика из лесистой Финляндии c аватаркой чрезвычайной усатости, публикую тут подробное описание процесса и технологий, возможно, это кого-то заинтересует. Использовать мы будем следущий инструментарий: NodeJS, SocketIO, Xvnc, xwd, Chromium и еще кучу всего другого. Вкратце постановка задачи выглядела подобным образом: нужен API, возвращающий скриншот сайта в PNG, проект сам по себе - внутренний сервис компании юзабилити тестирования. Только нужно было управиться без всех этих хитрожопых и кривых command-line конверсий по типу khtml2png, а грабить настоящий живой вывод современных браузеров. Я, почесав репу, решил выделить три уровня разработки и автоматизации: serverside, middleware и конечно же clientside, которые я вам сейчас и опишу, дорогие мои.

Начнем с самого глубокого серверсайда. В распоряжении имелся старенький слабенький сервачок с ненавистным CentOS-ом, ну делать было нечего. Прежде всего, нужно понять, что без иксов нам абсолютно никак не обойтись. Они нужны для виртуальных дисплеев, куда мы будем высаживать наши браузеры. Варианта два: мы можем использовать xorg-x11-server-Xvfb либо же vncserver, который представляет собой perl обертку вокруг Xvnc. Я советую остановиться на втором варианте преимущественно по причине меньшего количества dependencies, возможности удобного удаленного подключения для настройки того же браузера и отсутствия проблем с битностью цвета. К тому же, на старике-центосе Xvfb почему-то постоянно отваливался, а ядро было собрано без поддержки framebuffer, поэтому устройство /dev/fb[0-9] отсутствовало, сводя на нет все преимущества утилит fbdump и fbgrab. После установки vncserver, протестируем его работоспособность, вызвав vncserver :11 -geometry 1920х1200 -depth 24. У вас спросят пароль при первом запуске и, если все прошло без ошибок, то вы счастливчик. Иксы на виртуальном дисплее localhost:11 у вас уже есть. Можете рассказать это своему environment (ненавижу слово "окружение") c помощью export DISPLAY=:11. А еще вы можете запустить xterm и сконектиться через VNC клиент (для виндовса есть отличный бесплатный от RealVNC) и в очередной раз убедиться, что все работает. Теперь нам нужно как-то воровать экран и сохранять его в изображение. В интернетах советуют использовать утилиту import, входящую в набор ImageMagic, но это как перевозить две горошины на грузовом самолете. Вместо нее мы будем использовать xwd из xorg-x11-apps для снятия слепка с виртуального дисплея, и xwdtopnm плюс pnmtopng из netpbm-progs для конвертации его в PNG формат. Тут все еще проще, для получения скриншота вам нужно просто выполнить xwd -root -display localhost:11 | xwdtopnm 2>/dev/null | pnmtopng > screenshot.png. Большая часть серверной магии окончена, осталось сделать небольшой скрипт автоматизации и защиты от дураков. Оговорюсь сразу, что процесс генерации сриншотов будет последователен на основе очереди задач, никакого распараллеливания. О причинах этого мы поговорим чуточку позже. После установки Хромиума, что для ебучего CentOS-а, который сыпется пылью из всех щелей, тоже нихуя не тривиальная задача, мы сделаем небольшой скрипт для облегчения рутины создания скриншотов. Хромиум запускается в инкогнито режиме в фоне, мы ждем открытия страницы 6 секунд (для многих страниц этого времени не хватает, но для тестов сойдет), снимаем скришот и жестко тушим все открытые процессы. Если вам не нужны элементы оформления браузера аля адресная строка или панель табов, то можете добавить ключ --kiosk при запуске хрома. Все пропадет, останется лишь окно с отрендериной страницей, но это выглядит как-то менее эстетично ;). Таким образом, мы собрали все необходимое для создания скриншотов, осталось написать обертку и обертку над оберткой. Пару слов про безопасность: создайте отдельного пользователя из-под которого будете запускать браузер и иксы, отключите все плагины на внутренней странице about:plugins. Клиентская и серверная валидация ссылок в добавок к жесткому ограничению времени исполнения (у нас это 6 секунд) защищает от умников, которые мечтают о stack overflow и arbitrary code execution или пытаются банально загрузить html файл размером с пару гигабайт. От запуска нескольких инстансов браузера для многопоточной генерации пришлось отказаться по этой же самой причине. И да, все настройки хромиума хранятся в JSON формате в файле ~/.config/chromium/Default/Preferences, изменить вам придется параметры размеров окна, потому что даже с ключом --start-maximized у браузера развернуть окно на весь виртуальный экран почему-то не получается.

Часть вторая - middleware или, другими словами, прослойка между сервером-клиентом и своеобразный примитивный менеджер задач. Писать мы ее будем на NodeJS и SocketIO, оба решения мне полюбились event-based моделью. В стандартный набор ноде входит функция spawn объекта child_process для асинхронного запуска дочерних процессов и получения их stdout потоков, которая и будет работать с нашим небольшим bash скриптом. Для создания последовательной модели исполнения, нам нужен какой-то неблокирующий алгоритм task queue и именно поэтому нам не подходит метод Array.forEach. Я, признаться, не стал ебать себе мозг академическими решениями и просто сделал рекурсивную функцию, которая банально вызывала Array.push при добавлении новой задачи и Array.shift при завершении выполнения и переходе на новый цикл итерации с проверкой блокирующей переменной. Решение не идеальное и при больших нагрузках могут возникать проблемы с выпадающими из стека заданиями, но никто больших нагрузок и не ждет - идеальная отговорка для лентяя, который поленился сделать асинхронную модель обработки с помощью setInterval. Чтобы не быть голословным - вот вам код прослойки. Судя по коллекции сохраненных скриншотов, некоторые куллхакеры, думающие, что, выполнив в консоле isURL = function() {return true;}, они обойдут все проверки и удалят мне /etc/passwd, соснули хуйцов и посмотрели веселые картинки на сайте gay.ru. Кекеке!

Последний и самый верхний уровень - clientside не представляет собой ничего особо интересного. Хочу лишь заметить, что при всех преимуществах охуительнейшей библиотеки SocketIO, у нее нету нормальной внятной документации или хотя бы описания API. Лично мне найти не удалось, на гитхабе только описание в примерах, но это не удобно и противоречит законам мироздания. Справедливости ради скажу, что все, описанное мной в этом посте, пахнет влагой и сыростью. Нету ни обработок исключений, ни проверок результата генерации, ни нормальной валидации ссылок, но я лишь даю болванку, а ее обтачивание - дело дорчитателей, которым это необходимо.

Куда ж мы без живых примеров? Никто в наше время тексту не верит. Пришлось перенести все на свой старенький сервачок в далекой Фрицландии, сделать симпатичную обертку на скорую руку и теперь вы можете проверить работу теории на практике в разрастающейся секретной экспериментальной лаборатории интернетовских опытов имени Коли Цискаридзе. Ссылка на ScreenShooter, который старательной описывался в этой статье, работает до последнего посетителя. В качестве бонуса всем, кто дочитал достаточно сложную для формата блога статью, я дарю самодельный уникальный инструмент по раскрутке вашего сайта, известный подписчикам моего твиттера и гуглоплюса. А если серьезно, спасибо всем, кто активно плюсует гуглокнопкой мои посты. Это достаточно приятно и позволяет понять направление дальнейшего развития тематик. Продолжайте в том же духе, нажимайте +1, у меня еще много разнообразных интересностей в черновиках.