Се­год­ня мы по­го­во­рим о раз­ра­ботке сер­ви­са со­зда­ния скрин­шо­тов сайтов. Так по­лу­чи­лось, что я за­ни­мал­ся этим для од­но­го ин­тра­не­товско­го проек­та и, по­лу­чив благо­сло­ве­ние и раз­ре­ше­ние до­бро­душ­но­го за­каз­чи­ка из ле­си­стой Фин­лян­дии 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, у ме­ня еще много раз­но­об­разных ин­терес­но­стей в чер­но­ви­ках.