tag:blogger.com,1999:blog-54118197542912921052024-03-24T23:09:47.056-07:00archimagarchimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.comBlogger192125tag:blogger.com,1999:blog-5411819754291292105.post-76622517790209319712012-03-06T09:47:00.001-08:002012-03-06T09:53:05.916-08:00Про выборыЯ некоторое время сомневался не пойти ли мне наблюдателем на выборы, но в итоге решил, что ввиду свойств характера и патологической нелюбви к большим скоплениям людей, вряд ли смогу сколько-нибудь качественно исполнять эту роль. Но, зато когда мне предложили поработать в штабе наблюдателей, согласился без особых раздумий. "Яблоко" не могло посылать своих наблюдателей (но имеет некоторое количество людей в УИКах), зато могло посылать представителей прессы, благо местный филиал "Яблока" имеют свою газету и с оформлением наблюдателей как представителей прессы нет никаких проблем. Методика наблюдений была предоставлена "Голосом", которому и скидывались в итоге результаты. <br /><br />Вообще, первое впечатление за день до выборов было довольно удручающим. В местном подразделение "Яблоко" работают хорошие люди, но на мой взгляд им катастрофически не хватает опыта организационной деятельности. Не буду вдаваться в причины этого, но идейные люди с опытом и пониманием проектной деятельности им бы совершенно точно не помешали. В любом случае, для меня понятно, что не то что повлиять на результаты выборов, но даже провести качественного наблюдения с подобной организацией нельзя.<br /><br />Суть моей работы заключалась в том, что я должен был проводить анкетирование наблюдателей и вносить их в некую систему (которая оскорбляет меня как разработчика, но не суть). Первое анкетирование должно было проводиться с 10 до 11 часов, второе с 19 до 20, а третье после получения итогового протокола. Наблюдателей мне досталось немного, всего 11. Двое из них так и не вышли на связь (один телефон был выключен, другой не отвечал). Ещё двое людей перестали отвечать после первого анкетирования и больше на связь не выходили. Дополнительная сложность заключалась в том, что наши наблюдатели должны были иметь на руках специальный блокнот с анкетами, которые они должны были заполнить самостоятельно, а мне просто продиктовать ответы, но у большинства моих наблюдателей такого блокнота не оказалось. В итоге, мне приходилось зачитывать им вопросы (а ответы на некоторые из них требовали подготовки), а главной моей фразой стало "ДА или НЕТ?".<br /><br />В течении всего дня по моим участкам всё было в полном порядке, нарушений особых замечено не было. На некоторых других участках было хуже. В частности, было обнаружено два вброса. Одну женщину, члена "Единой России" поймали в буквальном смысле за руку в момент вброса. Было ещё несколько историй с карусельщиками. При этом каждый раз в момент вброса кто-то отключал веб-камеру, следящую за урной. По вборосам были написаны заявления в прокуратуру, которая должна была прибыть на участки и не допустить дальнейшее использование скомпрометированных урн, но до конца дня они так туда и не приехали (хотя всё время утверждали, что уже едут). Ну этого из того что я слышал, ибо сбор анкет потребовала неожиданно много времени и внимания. Но всяком случае, когда в середине дня <a href="http://ru.wikipedia.org/wiki/%D0%A0%D1%83%D0%B4%D0%BE%D0%BC%D0%B0%D1%85%D0%B0,_%D0%90%D0%BD%D0%B4%D1%80%D0%B5%D0%B9_%D0%92%D0%BB%D0%B0%D0%B4%D0%B8%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B8%D1%87">Рудомаха</a> сел писать гневный пресс-релиз мне показалось, что оснований для подобных выводов и формулировок пока нет (ну может у него была дополнительная информация, но с моей колокольни пока ничего особо страшного не происходило).<br /><br />Да, небольшой комментарий по выездные голосования. В целом оно проходило нормально, если на него выезжали наши наблюдатели, но тогда они не могли контролировать ситуацию на участке. Я знаю один случай, когда на выездное голосование уехала урна без наблюдателя и в итоге мы получили там 300 бюллетеней за 2 часа, что просто физически совершенно не реально. От другого наблюдателя я узнал, что в списках выездного голосования оказывались давно умершие люди, они физически не могли написать заявления! Так же, во время анкетирования получал информацию, что заявления на выездные голосования подавали не люди, а организации (чего делать нельзя).<br /><br />После второго анкетирования количество закреплённых за мной наблюдателей, остававшихся на связи, сократилось до 7, но полученная от них картина была вполне положительная. Участки закрылись и мы стали ждать протоколов.<br /><br />Вот тут картина выборов начинает резко меняться. В офисе начинается телефонный ад, ибо постоянно идут попытки удаления наблюдателей. Большинство наших людей шли как представители прессы и не могли сами как-либо вмешиваться в происходящее. Видя нарушение закона они пытались как-то реагировать и это могло использоваться для их удаления, как мешающих процессу подсчёта голосов. На моих участках сначала вроде всё было спокойно, но потом началось. Один из наблюдателей приехал на участок на своей машине и в процессе подсчёта голосов у него сработала сигнализация, он вышел и не смог вернуть назад. Это полностью в рамках закона, но... Я опросил его по третьей анкете и оказалось, что процесс подсчёта голосов на этом участке был нарушен чуть более, чем полностью, но получить итоговый протокол стало невозможно. Следующая ситуация - звонит сотрудничающий с нами наблюдатель от Прохорова, говорит подсчёт окончен(за Путина у них было около 45%), но вот данные не входят с систему ГАС-выборы, что-то не так и тут вот совещаются что делать, но ей завтра рано на работу и стоит ли ей ждать завершения ситуации? Вообще, это грубое нарушение закона, ибо сначала составляется протокол, подписывается и заверяется и только после этого данные сообщаются в ТИК. Но на всех участках данные сначала отправлялись в ТИК и только после этого составлялся протокол. Как легко догадаться, это требуется для согласования результатов. Если ТИК даёт добро, то глава УИК даёт команду составлять протокол и т.п. По трём участкам я быстро получил протоколы, на которых за Путина было за 60%. По остальным случилось светопреставление. Та девушка от Прохорова больше не выходила на связь, я ей советовал держаться до тех пор, пока она не получит протокол на руки, он полагаю что ей действительно надо было утром на работу и она просто ушла. По двум участкам мне звонят в 3.00 ночи и сообщают, что главе УИК из ТИК дана команда не составлять протокол вообще! Кафка нервно курит в сторонке. Ещё бы, там ведь за Путина только 40%. Глава местного "Голоса" тока покачал головой "Ой-ой-ой, кого-то уволят" (не за нарушение, а за 40%). Как это не составлять протокол? Мы точно знаем, что Чуров дал команду посчитать всё до 9.00, об этом и говорим наблюдателям, терпите, ждите, ситуация кто кого пересидит. Я не дождался завершения, ибо ушёл домой около 5.00. <br /><br />Но главное украшение ночи, это, конечно, история Артема Агеева. Он был членом УИК с правом решающего голоса. Началось с его звонка, что он снял на видео три пачки вброшенных бюллетеней, у него много интересного видео и его пытаются удалить. Вокруг это удаления развивается вся дальнейшая история. Мы напрягаем все силы, звоним во все возможные инстанции (ТИК, крайизбирком), шлём факсы с заявлениями, ситуация развивается часа полтора или может два (мне сейчас трудно оценить время), но в конце концов его всё таки удаляют с участка. Но дело в том, что члена УИК вообще нельзя удалять с участка! Глава УИК может принять решение об отстранении его, но его нельзя физически удалять из помещения, где происходит подсчёт голосов. Даже если он в порыве гнева убьёт там кого-нибудь, полиция должна обеспечить, что бы он не мешал работе, но ни в коем случае не удалять. Полицейский, который его удалял, отказался даже представится. И не мудрено. За несколько дней до выборов Нургалиев собирал специальную пресс-конференцию, где презентовал инструкцию для полицейских, якобы впервые полицейские больше не будут подчиняться главе УИК, а будут следить соблюдением законности. Мне рассказали, что этот вопрос специально обсуждался на совещании у Ткачёва и полицейские чины были в полном замешательстве, как это они могут обеспечить соблюдение законности? как это не подчиняться главе УИК? В итоге, ничего и не поменялось, как оказалось полицейские по прежнему подчиняются главе УИК и непосредственно участвуют в нарушении закона. Но лучше об этой истории узнать непосредственно от Артёма, <a href="https://twitter.com/#aaageev">https://twitter.com/#aaageev</a> - здесь его твиттер, где выложены ссылки на снятое им видео (включая видео его удаления), <a href="http://www.itsec.pro/">http://www.itsec.pro/</a> - его блог, там уже есть рассказ про его приключения от первого лица. <br /><br />Итого. Мы знаем, что были вбросы. Мы знаем, что были карусели (наши люди пытались поучаствовать в одной из них, но лавочку вовремя прикрыли). Мы знаем, что во время выездного голосования, если оно проводилось без участия наблюдателя, в выездных урнах могло появиться до нескольких сотен бюллетеней за сами понимаете кого. Мы знаем, что если всё это не помогало или не могло быть осуществлено и на участке получалась плохая ситуация за Пу, то УИК всячески затягивал оформление протокола, надеясь, что наблюдатель не выдержит и пойдёт спать, а если это не помогало, то привлекали полицию. Мы не может оценить объём фальсификаций, но может заявлять, что голосование сопровождалось вопиющими нарушениями избирательного законодательства. Лично моя оценка на основе тех данных, что я видел, это 40-45% за Путина в Краснодаре. При этом явка в Краснодаре около 50% (Ткачев в твиттер отчитался о явке в 70% по краю).<br /><br />Пара наблюдателей, сообщая мне протоколы, были явно разочарованы. Я знаю несколько таких случаев, вроде и нарушений человек серьёзных не заметил, а на участке 65% за Путина, это может серьёзно подорвать моральных дух. Но мне сейчас совершенно ясно, что один человек в большинстве случаев не может что-либо предотвратить или заметить, если "плохие люди" действуют осторожно (тем более, на их стороне опыт). Как оказалось, при всей плохой организации "Яблока", именно яблочные наблюдатели пытались что-то сделать и фиксировали нарушения. Наблюдатели от Прохорова/Зюганова/Миронова вроде бы как и есть, но в основном занимают совершенно пассивную позицию. Я не знаю, откуда они берутся, возможно они просто получают деньги за участия в этих мероприятиях, а другой мотивации не имеют. Часть наших наблюдателей, кстати, вызвала у меня большие подозрения, ещё когда я их видел на обучении. Один наблюдатель, во время первого анкетирования мне стал заявлять, что вот он видит, что действительно много людей голосуют за Путина и наверное мы проиграем. Не знаю зачем он мне это рассказывал, цель наблюдателей - честные выборы, а не свалить Путина. В общем, у меня возникло подозрение, что некоторые наблюдатели совершенно не стремятся к честным выборам, а преследуют какие-то другие цели. <br /><br />Из всего вышесказанного естественным образом рождается ответ на традиционный русский вопрос "Что делать?" - готовится к следующим выборам. Насколько я сейчас вижу, такая простая мысль приходит в голову не только мне. Сегодня на "Дожде" показывали одного человека (поздно включил, не понял кто он), который рассказывал о том, как его группе удалось обеспечить действительно чистые выборы на 5-ти участках, но для этого им потребовалось 40 человек и 7 дней подготовки. Ещё раз, яблочные наблюдатели, которые считаются самыми эффективными (видимо за счёт того, что не получают денег, а действуют исключительно из идейных соображений) - организованны очень плохо и имеют очень ограниченные возможности юридической помощи (людей знающих закон очень мало и они были порваны на множество маленьких частей). Кстати, "Голос" тоже организован плохо и мне не очень понравились люди, которые там работают. Т.е. сейчас в обществе нет структуры, способной качественно проконтролировать ход голосования. Как бы не развивалась ситуация в стране, но впереди у нас ещё много разных выборов и сейчас в России просто физически невозможно провести по-настоящему честные выборы. Необходимо создавать организацию, способную обеспечить качественный контроль за ходом голосования. Эта организация должна не только контролировать ход голосования, но и оказывать давлению на выборную систему, добиваясь реакции на обнаруженные замечания и шаг за шагом приучать людей из ТИКов и УИКов соблюдать закон. Даже если через месяц Навальный захватит Манежку, а Удальцов Кремль - потребность в честных выборах своей актуальности не потеряет. Вопрос лишь в том, найдутся ли люди, готовые заниматься такими проблемами.<br /><br />В своё время меня сильно удивило то, что основную часть работы перед выборами и во время выборов в США делают волонтёры. Может ли хорошая работа быть сделана бесплатно? Главным итогом этих выборов для меня стало появление понимания, что только хорошо организованные и подготовленные волонтёры-активисты способные обеспечить честность выборов. Для Краснодара таких надо несколько тысяч. Ну да только мы всё любим понимать не любим что-либо делать :(archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com2tag:blogger.com,1999:blog-5411819754291292105.post-33461126695588915102011-04-26T08:33:00.000-07:002011-04-26T08:34:02.700-07:00Блог переехалБлог переехал, теперь я буду жить тут: http://archimag.lisper.ru/archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com1tag:blogger.com,1999:blog-5411819754291292105.post-30527204291505324982011-03-25T03:19:00.000-07:002011-03-25T03:50:40.545-07:00cl-sanitizeНаписал <a href="https://github.com/archimag/cl-sanitize">cl-sanitize</a> - библиотеку для очистки HTML от нежелательного содержания. Точнее, не написал, а портирова <a href="https://github.com/rgrove/sanitize">Sanitize</a> (Ruby). Хотя "портировал" тоже не совсем верное слово, ибо реализация алгоритма очистки тривиальна и его я написал сам (это просто быстрее, чем разбирать код на Ruby), но вот самое важное - конфигурационные данные и набор тестов взял из <a href="https://github.com/rgrove/sanitize">Sanitize</a>. <br /><br />cl-sanitize основана на cl-libxml2. Я сомневался некоторое время, что взять за основу cxml или cl-libxml2 (сомнения тем более оправданны, что cxml и cl-libxml2 плохо совместимы между собой из-за конфликта имён), но всё таки решил использовать cl-libxml2. Во-первых, оригинальная версия основанна на libxml2, так что это позволило легко получить идентичные результаты на тестах. Во-вторых, я не имею такого доверия к "Closure HTML", как к libxml2. А это очень важный момент, ибо после просмотра <a href="https://github.com/rgrove/sanitize/blob/master/test/test_sanitize.rb">тестов</a> мне стало немного страшно ходить в интерент. <br /><br />В процессе пришлось немного допилить cl-libxml2 - как то мне сейчас несколько страшновато заглядывать внутрь, всё таки это был фактически мой первый серьёзный опыт использования CL, отрефракторить бы её основательно, да боюсь, что до этого руки у меня не скоро дойдут.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com2tag:blogger.com,1999:blog-5411819754291292105.post-47816600271042769162011-03-15T16:25:00.000-07:002011-03-15T18:30:10.982-07:00Новый парсер для cl-closure-templateПолностью переписал парсер для <a href="https://github.com/archimag/cl-closure-template">cl-closure-template</a> на базе <a href="https://github.com/nikodemus/esrap">esrap</a>. Я вообще давно хотел переписать его на какой-нибудь более серьёзной базе, но те решения для парсинга на CL, которые мне попадались, меня совершенно не устраивали - запутанно и слишком громоздко. <a href="https://github.com/nikodemus/esrap">esrap</a> понравилась мне сразу, во-первых это <a href="http://en.wikipedia.org/wiki/Parsing_expression_grammar">PEG</a>, во-вторых описание грамматики можно разбивать на множество отдельных кусков, а не загонять их в один большой макрос, в-третьих - описание грамматики объединяется с кодом по обработке, так что никаких раздельных стадий лексического и синтаксического анализа - всё объединено. <br /><br />Вообще, <a href="https://github.com/nikodemus/esrap">esrap</a> принципиально очень сильно похожа на моё старое решение (которое я подсмотрел в коде dokuwiki, а туда оно попало из какого-то решения на Perl), но гораздо совершенней и основано на более выразительных инструментах. Благодаря принципиальной схожести переписывание оказалось довольно простым и заняло 2 дня. При этом, не сказать, что размер код сильно уменьшился, но вот соответствие <a href="http://code.google.com/intl/ru/closure/templates/docs/overview.html">оригинальной спецификации</a> стало значительно более точным. Например, появилась возможность использовать экспоненциальную форму записи для чисел с плавающей точкой, целые в шестнадцатеричной форме, или напрямую писать коды Unicode в строковых литералах.<br /><br />Самое главное изменение - теперь неверно записанный шаблон не будет компилироваться и будет вываливаться сообщение об ошибке, по которому можно примерно установить место ошибки. Точной информации там нет, но локализовать конкретный template можно. <br /><br />Мне пришлось исправить несколько своих шаблонов по одной причине - старая версия дозволяла использовать символы { и } достаточно свободным образом (что противоречит спецификации) и пришлось завернуть соответствующие куски в <a href="http://code.google.com/intl/ru/closure/templates/docs/commands.html#literal">literal</a> секции. Плюс, было несколько мелких опечаток, которые проглатывались старой версией. Ещё в старой версии можно было записывать имена переменных как $foo-bar-x, что противоречит спецификации и я это запретил, но возможность записывать имена шаблонов в "лисп-стиле" пока всё таки оставил (может тоже запретить?).<br /><br />Полный код парсера <a href="https://github.com/archimag/cl-closure-template/blob/master/src/parser.lisp">здесь</a>.<br /><br />P.S. Если придётся что-нибудь ещё парсить, то однозначно буду использовать <a href="https://github.com/nikodemus/esrap">esrap</a>.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com3tag:blogger.com,1999:blog-5411819754291292105.post-54557425033750078302011-03-03T15:12:00.000-08:002011-03-03T16:45:40.932-08:00Ещё пара полезных декораторовВ веб-приложениях для отдачи статических файлов принято использовать возможности веб-серверов (Apache, Nginx), поскольку традиционные языки веб-разработки (Python, Ruby, PHP, Perl) справляются с этой задачей очень плохо. Однако, это связано с рядом ограничений. Во-первых, нельзя автоматически генерировать ссылки на основе информации о маршрутах. Но если это ещё можно пережить (хотя, честное слово, очень неудобно), то есть куда более серьёзная проблема - часто нужен "управляемый" доступ к статическим файлам. Например, скрипт должен проверить права доступа. Для решения этой дилеммы была придумана техника, когда скрипт выполняет свою логику, а реальную передачу файла делегирует веб-серверу путем настройки специальных заголовков ответа. Для nginx это <a href="http://wiki.nginx.org/XSendfile">X-Accel-Redirect</a>, а для Apache - <a href="https://tn123.org/mod_xsendfile/">X-Sendfile</a>.<br /><br />При разработке на Common Lisp проблема отдачи статических файлов стоит значительнее менее остро, ибо язык сам по себе намного быстрее и Hunchentoot справляется с этой работой достаточно быстро. Я для публикации статических файлов пользуюсь restas-directory-publisher - модулем для RESTAS. Однако, следует признать, что возможны ситуации, когда отдавать статику средствами CL окажется не очень разумно.<br /><br />Тут вот и возникла идея обеспечить возможность использования механизма XSendfile прозрачным для основной логики образом. В итоге, я добавил в RESTAS соответствующие декораторы для Apache и Nginx.<br /><br />Ниже я приведу возможные примеры использования этих декораторов в пакете restas-doc, обеспечивающего работу http://restas.lisper.ru/.<br /><br />для публикации файлов я использую такой код: <pre>(restas:mount-submodule -publisher- (#:restas.directory-publisher))</pre> При этом, сами файлы с документацией лежат в каталоге /usr/share/doc/restas/.<br /><br /><span style="font-weight:bold;">@apache-xsendfile</span><br />При работе с Apache всё просто: <pre>(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@apache-xsendfile))</pre> а в конфиге Apache необходимо установить значение параметра XSendFilePath таким образом, что бы публикуемые файлы оказались внутри указанного каталога. Самый простой путь <pre>XSendFilePath /</pre> Но это может оказаться не очень разумным с точки зрения администрирования, так что в данном случае можно и так <pre>XSendFilePath /usr/share/doc/restas/</pre> Более подробная информация о настройке сервера есть в <a href="https://tn123.org/mod_xsendfile/">официальной документации</a>.<br /><br /><span style="font-weight:bold;">@nginx-accel-redirect</span><br />Настройка nginx является более сложной. Необходимо создать internal-секцию и дальше есть два варианта: либо установить root, либо задать alias. В зависимости от выбранного варианта необходимо различным образом формировать заголовок X-Accel-Redirect. Для поддержки этих вариантов в RESTAS добавлено три переменных: <pre>(defvar *nginx-internal-location* nil)<br />(defvar *nginx-internal-alias* nil)<br />(defvar *nginx-internal-root* nil)</pre> и их надо настроить точно в соответствии с настройками nginx. При этом, переменные *nginx-internal-alias* и *nginx-internal-root* являются взаимоисключающими и одна из них должна быть установлена в NIL. В принципе, их можно выставить глобально, но RESTAS позволяет сделать лучше. Например, для такого конфига nginx: <pre><br />location /restas {<br /> internal;<br /> root /usr/share/doc;<br />}</pre> Код на CL может быть таким: <pre>(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@nginx-accel-redirect)<br /> (restas:*nginx-internal-location* #P"/restas/")<br /> (restas:*nginx-internal-root* #P"/usr/share/doc/"))</pre> Настройка с root представляется мне довольно неудобной и если nginx поднят только для CL, то гораздо проще делать так <pre>location /protected/ {<br /> internal;<br /> alias /usr/share/doc/restas/;<br />}</pre> и <pre>(restas:mount-submodule -publisher- (#:restas.directory-publisher restas:@nginx-accel-redirect)<br /> (restas:*nginx-internal-location* "/protected/")<br /> (restas:*nginx-internal-alias* "/usr/share/doc/restas/"))</pre> Вообще, описанный функционал обеспечивает очень важную возможность работать со статическими файлами средствами CL, например, создавать повторно используемые компоненты, а в случае возникновения необходимости легко подключить механизм XSendFile с помощью подключения нужного декоратора. <br /><br />Я пока разбирался с этими вопрос успел посмотреть советы по использованию XSendFile с RoR и Django - там это носит вид хаков, жёстко прибивающих код к конкретной конфигурации веб-сервера. Этот факт подкрепил моё мнение, что модель декораторов RESTAS вкупе с возможностями CL (такими, как CLOS) обеспечивает более гибкие и мощные возможности, чем более традиционная схема с middleware-компонентами.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com7tag:blogger.com,1999:blog-5411819754291292105.post-85281052046592293292011-03-03T07:52:00.000-08:002011-03-03T07:56:10.556-08:00Прозрачное использование ParenscriptЯ использую <a href="http://common-lisp.net/project/parenscript/">Parenscript</a> в реализации <a href="https://github.com/archimag/cl-closure-template">cl-closure-template</a>. Но обычный код предпочитаю писать на JavaScript. На то есть несколько причин и одна из самых существенных - возникающие инфраструктурные трудности. На самом деле я бы хотел просто размещать файлы с кодом на Parenscript среди других статических файлов и больше ни о чём не думать, а оно пусть там как-нибудь само обрабатывается. После добавления в RESTAS декораторов реализовать такое поведение оказалось очень просто: <pre>(defclass ps-compile-route (routes:proxy-route) ())<br /><br />(defmethod restas:process-route ((route ps-compile-route) bindings)<br /> (let ((result (call-next-method)))<br /> (cond<br /> ((and (pathnamep result)<br /> (string= (pathname-type result) "parenscript"))<br /> (setf (hunchentoot:content-type*)<br /> "text/javascript")<br /> (let ((*package* (find-package '#:ps)))<br /> (ps:ps-compile-file result)))<br /> (t result))))<br /><br />(defun @ps-compile (origin)<br /> (make-instance 'ps-compile-route :target origin))</pre> В случае если основной обработчик маршрута возвращает pathname с расширением ".parenscript", то данный декоратор компилирует его содержимое в код на JavaScript и отправляет клиенту, иначе возвращает результат без какой-либо обработки. Теперь я просто публикую статические файлы <pre>(restas:mount-submodule -static- (#:restas.directory-publisher @ps-compile)<br /> (restas.directory-publisher:*directory* #P"/path/to/static/")</pre> И если мой Parenscript-код находится в файле "/path/to/static/js/core.parenscript", то в HTML он подключается как <pre><script src="/js/test.parenscript"></script></pre>archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com2tag:blogger.com,1999:blog-5411819754291292105.post-22203385788116987792011-03-02T11:33:00.000-08:002011-03-02T11:49:47.089-08:00Интернет-магазина на Common LispЕсли вы мечтали купить что-нибудь в интернет-магазине, написанном на Common Lisp, то с недавних пор имеете такую счастливую возможность (особенно, если живёте в Питере) - <a href="http://www.320-8080.ru/">www.320-8080.ru</a>, разрабатывается <a href="http://axiger.livejournal.com/">rigidus</a>-ом на основе <a href="http://restas.lisper.ru/">RESTAS</a> и <a href="https://github.com/archimag/cl-closure-template">cl-closure-template</a>!archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com9tag:blogger.com,1999:blog-5411819754291292105.post-56827033299928347982011-02-28T15:18:00.000-08:002011-02-28T21:58:19.744-08:00ДекораторыВ <a href="http://archimag-dev.blogspot.com/2011/02/restas-middleware.html">предыдущем сообщении</a> я использовал термин "middleware", который показался мне в итоге не очень удачным. Всё же middleware это больше про WSGI или про тот же <a href="http://fukamachi.github.com/clack/">Clack</a>. А в контексте <a href="http://restas.lisper.ru/">RESTAS</a> более точным и адекватным видимо является термин "декоратор". Я изменил это в коде, а также добавил несколько новых возможностей. <br /><br />Теперь <a href="http://restas.lisper.ru/ru/ref/index.html#restas-define-module">restas:define-module</a> и <a href="http://restas.lisper.ru/ru/ref/index.html#restas-define-route">restas:define-rout</a>e имеют keyword аргумент :decorators, в котором можно указать список декораторов, используемых для преобразования маршрутов. Вместо :decorators в restas:define-module также можно использовать переменную *decorators*, создаваемую при определении модуля. Т.е. теперь указывать декораторы можно при определении модуля, определении маршрута и подключении субмодуля. При этом, декораторы, указанные на различных уровнях, не замещают друг друга, а применяются последовательно. <br /><br />В качестве примера декоратора, а также как просто удобную возможность, добавил restas:no-cache-decorator: <pre>(defclass no-cache-route (routes:proxy-route) ())<br /><br />(defmethod process-route :before ((route no-cache-route) bindings)<br /> (setf (hunchentoot:header-out :expires)<br /> (hunchentoot:rfc-1123-date)) <br /> (setf (hunchentoot:header-out :cache-control)<br /> "max-age=0, no-store, no-cache, must-revalidate"))<br /><br />(defun no-cache-decorator (route)<br /> (make-instance 'no-cache-route :target route))</pre> Кстати, функция no-cache из Hunchentoot, делает какую-то ерунду.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com2tag:blogger.com,1999:blog-5411819754291292105.post-61411319874392489672011-02-24T07:41:00.000-08:002011-02-24T08:34:44.160-08:00RESTAS middlewareЯ сегодня рассматривал <a href="http://fukamachi.github.com/clack/">Clack</a> и должен сказать, что мне это определённо не нравится. Впрочем WSGI у меня тоже никогда больших восторгов не вызывал. Слишком универсальный протокол и слишком слабый API. Но, сама концепция middleware (в том виде, как она есть, например, в Pylons) не так уж и плоха. Просто, на мой вкус, её надо сделать более конкретной. Кое какие мысли у меня на эту тему уже крутились и я даже <a href="http://archimag-dev.blogspot.com/2011/02/restas.html">показывал</a> недавно, как можно тонко влиять на процесс обработки запроса. И вот сегодня идея начала приобретать конкретную форму. В частности, изменился макрос restas:mount-submodule, теперь можно писать примерно так: <pre>(restas:mount-submodule -mysubmodule- (#:mymodule middleware1 middleawre2)<br /> ..)</pre> Здесь middleware1 и middleware2 это функции, которые должны принимать маршрут и возвращать другой. <br /><br />В момент построения дерева маршрутов (а это происходит каждый раз при вызове функции restas:recconect-all-routes) для конкретного субмодуля строиться список обрабатываемых им маршрутов и пропускается через цепочку middleware-вызовов. middleware-функция может как угодно изменить маршрут или вообще вернуть другой. Специально для поддержки этого я добавил в cl-routes новый класс routes:proxy-route (который реализует известный паттерн proxy).<br /><br />Скажем, с помощью модуля restas-directory-publisher можно публиковать директории со статикой: <pre>(restas:mount-submodule -tmp- (#:restas.directory-publisher)<br /> (restas.directory-publisher:*baseurl* '("tmp"))<br /> (restas.directory-publisher:*directory* #P"/tmp/)<br /> (restas.directory-publisher:*autoindex* t)) </pre> Такой код позволит любому просматривать содержимое директории /tmp на сервере, но кто знает что там может быть. Теперь с помощью middleware можно защитить содержимое этого каталога, требуя от посетителей пройти HTTP-авторизацию: <pre>(defclass http-auth-route (routes:proxy-route) () )<br /><br />(defmethod routes:route-check-conditions ((route http-auth-route) bindings)<br /> (if (call-next-method)<br /> (multiple-value-bind (user password) (hunchentoot:authorization)<br /> (or (and (string= user "hello")<br /> (string= password "world"))<br /> (hunchentoot:require-authorization)))))<br /><br />(defun http-auth-middleware (route)<br /> (make-instance 'http-auth-route :target route))<br /><br />(restas:mount-submodule -tmp- (#:restas.directory-publisher http-auth-middleware)<br /> (restas.directory-publisher:*baseurl* '("tmp"))<br /> (restas.directory-publisher:*directory* #P"/tmp/)<br /> (restas.directory-publisher:*autoindex* t))</pre> Здесь определяется новый класс http-auth-route, наследующий от routes:proxy-route, и для него специализируется метод routes:route-check-conditions, который вызывается для проверки соответствия маршрута условиям запроса. Если маршрут проходит все проверки, то проверяется прошёл ли пользователь HTTP-авторизацию. Функция http-auth-middleware используется для создания таких маршрутов и указывается в списке middlewares макроса restas:mount-submodule.<br /><br />Для маршрутов, наследующих от routes:proxy-route, имеет смысл определять специализации методов routes:route-check-conditions и/или restas:process-route. <br /><br />Чуть более сложный пример использования описанных возможностей можно найти <a href="https://github.com/archimag/restas/blob/master/example/publish-rst.lisp">здесь</a>.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com10tag:blogger.com,1999:blog-5411819754291292105.post-65983655488832916232011-02-20T14:24:00.000-08:002011-02-20T15:00:01.913-08:00Крякнем, плюнем и надёжно скрепим скотчем (с)Итак, мне удалось запустить локальную версию сайта lisper.ru на своей машине под управлением Mongrel2 без Hunchentoot. В наличии имеется некоторое количество костылей, жуткого оверхеда и ещё не решённых вопросов, но это работает, включая обработку POST-запросов (как application/x-www-form-urlencoded, так и multipart/form-data), работу с cookie и отдачу статики. <br /><br />Изменение в коде компонентов lisper.ru были самыми минимальными и заключались в переключении с использования Hunchentoot API на библиотеку <a href="https://github.com/archimag/cl-wsal">cl-wsal</a>.<br /><br /><strong>Common Lisp Web servers abstraction layer</strong> <br /><br />Идея cl-wsal зрела у меня уже давно и нужен был лишь маленький толчок, который и был сделан в <a href="http://archimag-dev.blogspot.com/2011/02/restas-mongrel2.html">предыдущем обсуждении</a>. Я вынес из Hunchentoot значительную часть кода, которая может быть полезной в любых веб-серверах для CL в эту библиотеку и с её помощью довольно легко довёл <a href="https://github.com/archimag/cl-mongrel2">cl-mongrel2</a> до необходимого состояния. <br /><br />cl-wsal определяет <a href="https://github.com/archimag/cl-wsal/blob/master/src/protocol.lisp">протокол</a>, с помощью которого строится унифицированный <a href="https://github.com/archimag/cl-wsal/blob/master/src/interface.lisp">интерфейс</a> для клиентского кода. Также имеется набор <a href="https://github.com/archimag/cl-wsal/blob/master/src/util.lisp">утилит</a>, который могу использовать разработчики веб-сервера.<br /><br />Весь этот код хорошо протестирован, поскольку просто взят из Hunchentoot. Правда, кое-что я изменил. Использование flexi-streams я заменил на babel. Пришлось вырвать кусок из chunga. А также скопировать и несколько отредактировать rfc2388.lisp из одноимённого пакета.<br /><br />Вся работа над соответствующими изменениями в RESTAS ведётся в бранче Mongrel2. Следующий релиз RESTAS не будет включать этих изменений, посколькуо будет увязан с новым релизом Hunchentoot. А вот после этого я буду полностью переключаться на использование cl-wsal.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com4tag:blogger.com,1999:blog-5411819754291292105.post-83189875693480636812011-02-17T08:20:00.000-08:002011-02-17T08:58:05.412-08:00RESTAS и Mongrel2Hunchentoot предоставляет очень удобный интерфейс, но есть некоторые сомнения о возможности использования его под высокой нагрузкой, всё таки схема поток на соединения имеет достаточно понятные пределы для масштабирования. Правда, судя по патчам ITA они таки используют Hunchentoot под достаточно высокой нагрузкой, но хотелось бы всё таки иметь и другое решение. Писать асинхронный веб-сервер на CL, который бы предоставлял уровень сервиса сопоставимый с Hunchentoot, меня сейчас не прельщает. Я достаточно долго хотел просто форкнуть Hunchentoot, переделать его на базе iolib с использованием epoll и т.п., но сейчас уже отказался от этой идеи. Отказался после того, как узнал о существовании <a href="http://mongrel2.org/">Mongrel2</a>, который обещает все плюшки асинхронного веб-сервера и при этом не зависит от языка. <br /><br />Сейчас моя самая большая цель - научить RESTAS работать с Mongrel2 (сохранив при этом возможность работать и с Hunchentoot). Но есть проблема, что Mongrel2 не предоставляет такого интерфейса, как Hunchentoot, он только умеет обрабатывать HTTP и всё. Соответственно, задача сводится фактически к вырыванию довольно больших кусков кода из Hunchentoot и адаптации их для Mongrel2. Но в данный момент я не имею достаточных временных ресурсов для выполнения этой работы. Отсюда и вопрос. Нет ли желающих помочь в этом (достойном) деле? ;)archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com27tag:blogger.com,1999:blog-5411819754291292105.post-60279664258039792902011-02-15T05:05:00.000-08:002011-02-18T06:47:28.239-08:00Управление обработкой запросов в RESTASДобавил в <a href="http://restas.lisper.ru/">RESTAS</a> экспериментальную возможность: переменные restas:*before-dispatch-request-hook* и restas:*after-dispatch-request-hook*. В эти переменные можно складывать (pushnew) функции, которые будут вызываться соответственно до и после обработки запроса. restas:*before-dispatch-request-hook* может быть полезен, например, для безусловного требования авторизации от пользователей - не-авторизованных пользователей можно перенаправлять на страницу входа или запрашивать HTTP-авторизацию с помощью hunchentoot:require-authorization. А restas:*after-dispatch-request-hook* можно использовать, например, для сбора статистики обращения к различным URL, или можно делать косметическую пост-обработку результата, скажем, добавлять заголовки ответа.<br /><br />Для иллюстрации использования restas:*before-dispatch-request-hook* я написал тривиальный пример модуля для сбора статистики, посмотреть его можно <a href="https://github.com/archimag/restas/blob/master/example/statistics-collection.lisp">здесь</a>.<br /><br />restas:*before-dispatch-request-hook* и restas:*after-dispatch-request-hook* не позволяют серьёзным образом влиять на обработку запроса (хотя с их помощью и можно вносить небольшие изменения), кроме того - они применяются для всех запросов, обрабатываемых веб-сервером.<br /><br />Есть другая, более тонкая возможность влиять на ход обработки запросов. Например, если необходимо ограничить доступ к маршрутам из какого-либо модуля или проводить тонкую настройку обработки маршрутов из конкретного модуля. <pre>(in-package #:mymodule)<br /><br />(defclass myroute (restas:route) ())<br /><br />(defmethod restas:module-routes ((module (eql #.*package*)) submodule)<br /> (let ((routes (call-next-method)))<br /> (iter (for route in routes)<br /> (change-class route 'myroute))<br /> routes))</pre> Здесь объявляется свой класс маршрутов myroute и для конкретного модуля специализируется метод restas:module-routes, в котором основная работа делается с помощью call-next-method, а затем у полученных маршрутов изменяется класс с restas:route на myroute. Теперь можно определить собственные специализации для методов обработки маршрутов и определить произвольную логику обработки любым удобным способом. Для класса myroute имеет смысл специализировать следующие методы: <ul><li>routes:route-check-conditions (route bindings) - проверяется соответствует ли запрос требования маршрута</li><li>restas:process-route (route bindings) - здесь происходит вызов обработчика маршрута и, соответственно, генерация контента</li></ul>archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com1tag:blogger.com,1999:blog-5411819754291292105.post-58343649502170660612011-02-09T05:53:00.000-08:002011-02-09T06:41:05.650-08:00swank-js - удивительное рядомПоскольку мне приходится писать и отлаживать много JavaScript кода, то я уже давно мечтал о возможность изменять исходный код при работе в Emacs и сразу же отправлять изменения в браузер. Так же очень здорово было бы иметь JavaScript-консоль в Emacs, которая бы реально взаимодействовал с открытой веб-страницей. Или очень часто нужно немного подправить CSS и заставить браузер применить эти изменения без перезагрузки страницы. Звучит несколько фантастически, но сейчас это совершенно реально благодаря проекту <a href="https://github.com/ivan4th/swank-js">swank-js</a>.<div><br /></div><div>Мне, правда, пришлось внести небольшое изменение в оригинальный код, что бы это заработало для меня. Суть отличий в том, что в веб-страницу надо дополнительно включать такой JavaScript-код: </div> <pre> SwankJS.setup("localhost", {port: 8009});</pre> <div>Мои изменения <a href="https://github.com/archimag/swank-js">здесь</a>.</div>archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com2tag:blogger.com,1999:blog-5411819754291292105.post-49183120140446088772011-02-08T01:19:00.000-08:002011-02-08T02:40:33.934-08:00RESTAS и JavaScriptПопулярность JavaScript в качестве серверного языка стремительно увеличивается, чему способствует в том числе и свойства самого языка - JavaScript это удивительно гибкий и пластичный язык, он даже мягче, чем лисп. Кроме того, в современных веб-приложениях часто логика перемешивается между клиентом и сервером и очень удобно использовать и там и там один и тот же код. cl-closure-template решает многие проблемы разделения логики, но не все. Поэтому, у меня зародилась мысль дать возможность создавать <a href="http://restas.lisper.ru/ru/manual/modules.html">модули</a> для RESTAS на JavaScript. Тем более, что существует <a href="http://marijnhaverbeke.nl/cl-javascript/">CL-JavaScript</a> - достаточно качественная реализация JavaScript на Common Lisp.<div><br /></div><div><a href="https://github.com/archimag/restas-javascript">restas-javascript</a> - проект, который должен дать возможность смешивать код на Common Lisp и JavaScript при разработке web-приложений на базе RESTAS. Кое-что уже работает. В частности, я уже смог переписать на JavaScript примеры из статьи <a href="http://restas.lisper.ru/ru/tutorial/hello-world.html">Hello World</a> - смотрите код в файле <a href="https://github.com/archimag/restas-javascript/blob/master/example/demo.js">demo.js</a>.</div><div><br /></div><div>Структура этого кода полность аналогична соответствующему кода на Common Lisp, только использует идиомы JavaScript. Доступ к объектам request и reply (которые в CL оформлены в виде специальных переменных) в обработчиках маршрутом осуществляется через this.request и this.reply. Интерфейсы request и reply повторяет функции из документации к Hunchentoot, но именуются в стиле CamelCase, плюс некоторые методы оформлены в виде "активных свойств".</div><div><br /></div><div>Загрузить данный файл можно следующим образом: <pre>(restas.javascript:execute #P"/path/to/demo.js")</pre> после чего уже можно будет идти в браузер смотреть результат. </div><div><br /></div><div>С помощью <pre>(restas.javascript:repl)</pre> можно запустить примитивную JavaScript-консоль и поиграться с маршрутами. Что бы несколько упростить это я добавил простейшую реализацию console.log.</div><div><br /></div><div>Кстати, дизайн интерфейса для JavaScript мне нравится в некоторых аспектах больше, чем для CL.</div>archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com7tag:blogger.com,1999:blog-5411819754291292105.post-11406306518721454422011-02-02T02:12:00.000-08:002011-02-02T02:30:12.684-08:00Ещё раз про cl-uglify-jsЯ уже <a href="http://archimag-dev.blogspot.com/2010/11/cl-uglify-js.html">писал</a> про <a href="https://github.com/mishoo/cl-uglify-js">cl-uglify-js</a>, но тут случилось примечательное событие, которое на мой взгляд стоит отметить отдельно. <a href="https://github.com/mishoo/cl-uglify-js">cl-uglify-js</a> и <a href="https://github.com/mishoo/UglifyJS">UglifyJS</a> суть библиотеки-близнецы, от одного автора, развивающиеся синхронно и делающие совершенно одно и то же, просто написанные на разных языках - одна на Common Lisp, а другая на JavaScript (для Node.js). Так вот, теперь система сборки <a href="http://jquery.com/">jquery</a> использует именно <a href="https://github.com/mishoo/UglifyJS">UglifyJS</a> (вместо <a href="http://code.google.com/intl/ru-RU/closure/compiler/">Google Closure Compiler</a>!), подтверждение чему можно найти <a href="https://github.com/jquery/jquery/pull/173">здесь</a> или <a href="http://blog.jquery.com/2011/01/31/jquery-15-released/">здесь</a> (раздел про BUILD SYSTEM).<br /><br />В общем, сейчас можно смело утверждать, что <a href="https://github.com/mishoo/cl-uglify-js">cl-uglify-js</a> (наряду с <a href="https://github.com/mishoo/UglifyJS">UglifyJS</a>) претендует на роль одного из лучших (или даже лучшего) решений в своей области.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com0tag:blogger.com,1999:blog-5411819754291292105.post-69928959240428756762011-02-02T01:45:00.000-08:002011-02-02T02:05:43.292-08:00IE 9 - я удивлёнЯ, конечно, слышал, что IE 9 это чудо техники и поддерживает все современные стандарты почти полностью и т.п. Слышал, но не верил. Но вот стало любопытно, попросил админа установить на его машине сей продукт и решил попробовать запустить на нём свой <a href="http://archimag-dev.blogspot.com/2010/07/made-with-common-lisp-2-screencast.html">мерчендайзинг</a>. Тут должен заметить, что я никогда не планировал использовать данное приложение под IE. Мои пользователи используют Firefox, разработку я веду в основном с помощью Chromium, время от времени тестирую под Opera. Приложение основано на XHTML + SVG, использует достаточно нетривиальную обработку XML (DOMParser и т.п.). Под этими тремя (Firefox, Chrome, Opera) браузерами приложение заработало не сразу, был целый ряд различий в поведении, так что пришлось искать код, который одинаково работает во всех этих браузерах. <br /><br />И вот, просто ради любопытства запускаю под IE 9 - и, о чудо, оно просто работает. Вообще без какой-либо адаптации. У меня, честно говоря, культурный шок.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com1tag:blogger.com,1999:blog-5411819754291292105.post-30539144213983667312011-01-21T07:12:00.000-08:002011-01-21T08:33:41.676-08:00Автоматическая генерация ссылок в RESTASОдной из типовых проблем веб-разработки является генерация ссылок. В некоторых простых случаях на неё можно просто закрыть глаза и создавать ссылки "вручную". Например, недавно <a href="http://rigidus.livejournal.com/">rigidus</a> опубликовал на хабре <a href="http://habrahabr.ru/blogs/webdev/112051/">статью</a>, в которой рассказал про создание <a href="http://rigidus.ru/">простого сайта</a> на Common Lisp (с использование <a href="http://restas.lisper.ru/">RESTAS</a>). В данном примере для генерации главного меню используется такой код: <pre>(defun menu ()<br /> (list (list :link "/" :title "Главная")<br /> (list :link "/about" :title "About")<br /> (list :link "/articles" :title "Статьи")<br /> (list :link "/resourses" :title "Ресурсы")<br /> (list :link "/contacts" :title "Контакты")))<br /></pre> Т.е. ссылки жёстко задаются в теле программы. Поскольку здесь их всего 5 и они очень простые, то в данном случае это не создаёт больших проблем.<br /><br />Однако, по мере роста приложения, а также в процессе изменения его структуры, проблема сохранения актуальности ссылок приобретает серьёзный характер.<br /><br />Самый лучший способ решения данной проблемы использовать автоматическую генерацию ссылок. В <a href="http://restas.lisper.ru/">RESTAS</a> для этого есть специальная поддержка на базе функции <a href="http://restas.lisper.ru/ru/ref/index.html#restas-genurl">restas:genurl</a>. Например, с использованием данной функции вышеприведённый код можно было бы переписать следующим образом: <pre>(defparameter *mainmenu*<br /> '((main . "Главная")<br /> (about . "About")<br /> (articles . "Статьи")<br /> (resources . "Ресурсы")<br /> (contacts . "Контакты")))<br /><br />(defun menu ()<br /> (iter (for (route . title) in *mainmenu*)<br /> (collect (list :link (restas:genurl route)<br /> :title title))))<br /></pre> Здесь для генерации ссылок используется символ, связанный с конкретным маршрутом, а получающиеся ссылки будут учитывать базовый url, по которому подключается разрабатываемый модуль.<br /><br />Это очень простая ситуация, которая решается совершенно тривиальным образом. На сайте <a href="http://lisper.ru/">lisper.ru</a> имеет место более сложный случай. Исходный код данного ресурса разбит на несколько совершенно независимых пакетов, которые объединяются в один сайт на основе механизма <a href="http://restas.lisper.ru/ru/manual/modules.html">модулей</a>. Простое использование <a href="http://restas.lisper.ru/ru/ref/index.html#restas-genurl">restas:genurl</a> здесь не подходит, поскольку маршруты, на которые ссылается главное меню, находятся в разных модулях. Для определения состава главного меню используется такое объявление: <pre>(defparameter *mainmenu* `(("Главная" nil main)<br /> ("Статьи" rulisp-articles restas.wiki:main-wiki-page)<br /> ("Планета" rulisp-planet restas.planet:planet-main)<br /> ("Форум" rulisp-forum restas.forum:list-forums)<br /> ("Сервисы" nil tools-list)<br /> ("Practical Common Lisp" rulisp-pcl rulisp.pcl:pcl-main)<br /> ("Wiki" rulisp-wiki restas.wiki:main-wiki-page)<br /> ("Файлы" rulisp-files restas.directory-publisher:route :path "")<br /> ("Поиск" nil google-search)))<br /></pre> Здесь каждому элементу меню соответствует список, содержащий следующие элементы: заголовок, субмодуль (символ, который используется при вызове <a href="http://restas.lisper.ru/ru/ref/index.html#restas-mount-submodule">restas:mount-submodule</a>), символ маршрута (указанный в <a href="http://restas.lisper.ru/ru/ref/index.html#restas-define-route">restas:define-route</a>) и возможно несколько ключевых параметров (параметров маршрута). А для непосредственной генерации ссылок используется такой код: <pre>(in-package #:rulisp)<br /><br />(restas:with-submodule (restas:find-upper-submodule #.*package*)<br /> (iter (for item in *mainmenu*)<br /> (collect (list :href (apply #'restas:genurl-submodule<br /> (second item)<br /> (if (cdddr item)<br /> (cddr item)<br /> (last item)))<br /> :name (first item))))) </pre> Наиболее интересной в данном коде является строка <pre>(restas:with-submodule (restas:find-upper-submodule #.*package*)</pre> Дело в том, что генерация меню происходит каждый раз при генерации HTML-страницы для всех маршрутов, которые находятся в разных модулях и имеют различный контекст выполнения, а параметр *mainmenu* составлен с точки зрения самого верхнего модуля :rulisp, который используется для запуска приложения с помощью <a href="http://restas.lisper.ru/ru/ref/index.html#restas-start">start</a>.<br /><br />Структура субмодулей в <a href="http://restas.lisper.ru/">RESTAS</a> образует иерархию и restas:find-upper-submodule позволяет найти нужный модуль выше по дереву, а макрос restas:with-submodule выполнить код в контексте найденного модуля. Таким образом, генерация ссылок работает всегда одинаково, не зависимо от контекста выполнения этого кода.<br /><br />restas:find-upper-submodule и restas:with-submodule я добавил только сегодня, так что они пока есть только в <a href="https://github.com/archimag/restas">git-версии</a> <a href="http://restas.lisper.ru/">RESTAS</a>.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com0tag:blogger.com,1999:blog-5411819754291292105.post-56103661854980810172010-12-20T04:50:00.000-08:002010-12-20T04:51:47.067-08:00Колличество процессоровЕсли вам вдруг потребуется узнать количество процессоров в коде на Common Lisp, то сделать это с помощью iolib можно так: <pre>(iolib.syscalls:sysconf iolib.syscalls:sc-nprocessors-onln)</pre>archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com1tag:blogger.com,1999:blog-5411819754291292105.post-65248867668986840162010-12-04T14:41:00.000-08:002010-12-04T14:59:34.883-08:0010 тысяч запросов в секундуНакидал очень простой (100 строк кода) прототип асинхронного веб-сервера на базе iolib. Он умеет принимать GET-запрос и не обращая на него внимание отдавать одну и ту же страницу. Практической пользы от него никакой, но для исследования вопроса вполне сгодится. Так вот, планка в 10 000 запросов в секунду (тестировал через ab) на моей машине была уверенно взята без каких-либо оптимизаций - временами скорость доходила до 11 000 запросов в секунду. Код не полностью корректен, но от него это и не требуется. Всё обработка ведётся в одном потоке. Собственно, код: <pre>(asdf:operate 'asdf:load-op '#:iolib)<br />(asdf:operate 'asdf:load-op '#:iterate)<br /><br />(defpackage #:http.test<br /> (:use #:cl #:iter)<br /> (:export #:start #:stop))<br /><br />(in-package #:http.test)<br /><br />(defparameter *event-base* nil)<br /><br />(defparameter *reply*<br /> (let ((endl #.(babel:octets-to-string (coerce #(13 10) '(vector (unsigned-byte 8)))))<br /> (content "<html><br /> <head><br /> <title>Hello world</title><br /> </head><br /> <body><br /> <h1>Hello world</h1><br /> </body><br /></html>"))<br /> (babel:string-to-octets <br /> (with-output-to-string (out)<br /> (write-string "HTTP/1.0 200 OK" out)<br /> (write-string endl out)<br /> (format out "Content-Length: ~A" (length content))<br /> (write-string endl out)<br /> (write-string "Content-Type: text/html" out)<br /> (write-string endl out)<br /> (write-string endl out)<br /> (write-string content out))<br /> :encoding :latin1)))<br /><br />(defvar *bucket-pool* nil)<br /><br />(defun get-bucket ()<br /> (or (pop *bucket-pool*)<br /> (make-array 4096 :element-type '(unsigned-byte 8))))<br /><br />(defun free-bucket (bucket)<br /> (push bucket *bucket-pool*))<br /><br />(defun read-http-headers (socket callback)<br /> (let ((headers (get-bucket))<br /> (size 0))<br /> (flet ((read-handler (fd event errorp)<br /> (declare (ignore event errorp fd))<br /> (multiple-value-bind (buffer count) (iolib.sockets:receive-from socket :buffer headers)<br /> (declare (ignore buffer))<br /> (incf size count))<br /><br /> (when (and (> size 4)<br /> (equal '(13 10 13 10)<br /> (coerce (subseq headers (- size 4) size) 'list)))<br /> (iolib.multiplex:remove-fd-handlers *event-base*<br /> (iolib.sockets:socket-os-fd socket)<br /> :read t)<br /> (free-bucket headers)<br /> (funcall callback))))<br /> (iolib.multiplex:set-io-handler *event-base*<br /> (iolib.sockets:socket-os-fd socket)<br /> :read #'read-handler))))<br /><br />(defun send-http-reply (socket data callback)<br /> (let ((curpos 0)<br /> (total-length (length data)))<br /> (flet ((write-handler (fd event errorp)<br /> (declare (ignore fd event errorp))<br /> (cond<br /> ((= curpos total-length)<br /> (iolib.multiplex:remove-fd-handlers *event-base*<br /> (iolib.sockets:socket-os-fd socket)<br /> :write t)<br /> (funcall callback))<br /> (t (incf curpos<br /> (iolib.sockets:send-to socket data :start curpos))))))<br /> (iolib.multiplex:set-io-handler *event-base*<br /> (iolib.sockets:socket-os-fd socket)<br /> :write #'write-handler))))<br /><br />(defun accept-connection (passive-socket)<br /> (let ((active-socket (iolib.sockets:accept-connection passive-socket)))<br /> (read-http-headers active-socket<br /> (lambda ()<br /> (send-http-reply active-socket<br /> *reply* <br /> (lambda ()<br /> (close active-socket)))))))<br /><br />(defun start (&optional (port 8080))<br /> (setf *event-base* (make-instance 'iolib.multiplex:event-base))<br /> (flet ((impl ()<br /> (iolib.sockets:with-open-socket (acceptor :connect :passive<br /> :address-family :internet<br /> :type :stream<br /> :external-format '(:utf-8 :eol-style :crlf)<br /> :ipv6 nil)<br /> (iolib.sockets:bind-address acceptor<br /> iolib.sockets:+ipv4-unspecified+<br /> :port port<br /> :reuse-addr t)<br /> (iolib.sockets:listen-on acceptor :backlog 5)<br /><br /> (flet ((accept (fd event errorp)<br /> (declare (ignore fd event errorp))<br /> (accept-connection acceptor)))<br /> (iolib.multiplex:set-io-handler *event-base*<br /> (iolib.sockets:socket-os-fd acceptor)<br /> :read #'accept))<br /> <br /> (iolib.multiplex:event-dispatch *event-base*)<br /> (close *event-base*))))<br /> (bordeaux-threads:make-thread #'impl<br /> :name "*http-server*")))<br /> <br />(defun stop ()<br /> (iolib.multiplex:exit-event-loop *event-base*))</pre>archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com3tag:blogger.com,1999:blog-5411819754291292105.post-62465888170866240722010-11-25T03:36:00.001-08:002010-11-25T08:25:52.610-08:00Обработка PNG-изображений на Common LispДля своего текущего приложения я использую интерфейс, который откровенно содрал <a href="http://mugtug.com/sketchpad/">отсюда</a>, при чём, необходимые для таких красивых панелек png-файлы взял как есть (но несколько изменил способ их использования в html-разметке). Всё получается относительно <a href="http://www.youtube.com/watch?v=f6b0sQpDGVM">неплохо</a>, но мне захотелось посмотреть как будет выглядеть это приложение в других цветовых схемах.<br /><br />А вот с этим проблема, поскольку украденный мной набор png-файлов сделан только в чёрном исполнении. Сам я в дизайне полный ноль, всякими Gimp-ами владею очень слабо и вообще, как создаются <a href="http://mugtug.com/sketchpad/media/gui/win_LB.png">подобные</a> изображения понятия не имею: я пробовал создать такое просто кодом с помощью градиентов, закруглений и т.п., но так хорошо никак не получается.<br /><br />И я решил просто по-пиксельно заменить все цвета оригинальных изображений на новые, которые будут вычисляться на основе базового цвета. В оригинальных файлах основным цветом является rgb(30, 30, 30), но для создания эффекта тени используется переход данного цвета в чёрный. Функция translate-color вычисляет новый цвет на основе базового и опирается на rgb(30, 30, 30) как на основу старого изображения: <div class="code"><span class="paren1">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/m_defun.htm" class="symbol"><i><span class="symbol">defun</span></i></a> translate-color <span class="paren2">(<span class="">orig base-color</span>)</span><br> <span class="paren2">(<span class="">iter <span class="paren3">(<span class="">for i in orig</span>)</span><br> <span class="paren3">(<span class="">for j in base-color</span>)</span><br> <span class="paren3">(<span class="">collect <span class="paren4">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_max_m.htm" class="symbol">min</a> <span class="paren5">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_max_m.htm" class="symbol">max</a> <span class="paren6">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/a_pl.htm" class="symbol">+</a> i j -30</span>)</span> 0</span>)</span><br> 255</span>)</span></span>)</span></span>)</span></span>)</span> </div> Для создания png-файлов есть известное и хорошее решение - <a href="http://www.xach.com/lisp/zpng/">ZPNG</a>, а вот библиотеки для разбора png-файлов я не знал и кажется такая библиотека не освещалось широко где-либо, по крайней мере, я не видел. Однако, быстрый поиск в гугл сразу показал мне библиотеку <a href="http://github.com/Ramarren/png-read">png-read</a>. Я опробовал её на нескольких примерах и кажется она "просто работает". Таким образом, я смог записать такой код по изменению цвета нужных мне изображений: <div class="code"><span class="paren1">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/m_defun.htm" class="symbol"><i><span class="symbol">defun</span></i></a> make-other-png <span class="paren2">(<span class="">orig dest base-color</span>)</span><br> <span class="paren2">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/s_let_l.htm" class="symbol"><i><span class="symbol">let*</span></i></a> <span class="paren3">(<span class=""><span class="paren4">(<span class="">orig-png <span class="paren5">(<span class="">png-read:read-png-file orig</span>)</span></span>)</span><br> <span class="paren4">(<span class="">orig-image <span class="paren5">(<span class="">png-read:image-data orig-png</span>)</span></span>)</span><br> <span class="paren4">(<span class="">png <span class="paren5">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_mk_ins.htm" class="symbol">make-instance</a> 'zpng:png<br> <span class="keyword">:color-type</span> <span class="keyword">:truecolor-alpha</span><br> <span class="keyword">:width</span> <span class="paren6">(<span class="">png-read:width orig-png</span>)</span><br> <span class="keyword">:height</span> <span class="paren6">(<span class="">png-read:height orig-png</span>)</span></span>)</span></span>)</span><br> <span class="paren4">(<span class="">image <span class="paren5">(<span class="">zpng:data-array png</span>)</span></span>)</span></span>)</span><br> <span class="paren3">(<span class="">iter <span class="paren4">(<span class="">for w from 0 below <span class="paren5">(<span class="">png-read:width orig-png</span>)</span></span>)</span><br> <span class="paren4">(<span class="">iter <span class="paren5">(<span class="">for h from 0 below <span class="paren6">(<span class="">png-read:height orig-png</span>)</span></span>)</span><br> <span class="paren5">(<span class="">iter <span class="paren6">(<span class="">for c in <span class="paren1">(<span class="">translate-color <span class="paren2">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/a_list.htm" class="symbol">list</a> <span class="paren3">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_aref.htm" class="symbol">aref</a> orig-image w h 0</span>)</span><br> <span class="paren3">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_aref.htm" class="symbol">aref</a> orig-image w h 1</span>)</span><br> <span class="paren3">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_aref.htm" class="symbol">aref</a> orig-image w h 2</span>)</span></span>)</span><br> base-color</span>)</span></span>)</span><br> <span class="paren6">(<span class="">for i from 0</span>)</span><br> <span class="paren6">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/a_setf.htm" class="symbol">setf</a> <span class="paren1">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_aref.htm" class="symbol">aref</a> image h w i</span>)</span> c</span>)</span></span>)</span><br> <span class="paren5">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/a_setf.htm" class="symbol">setf</a> <span class="paren6">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_aref.htm" class="symbol">aref</a> image h w 3</span>)</span><br> <span class="paren6">(<span class=""><a href="http://www.lispworks.com/reference/HyperSpec/Body/f_aref.htm" class="symbol">aref</a> orig-image w h 3</span>)</span></span>)</span></span>)</span></span>)</span><br> <span class="paren3">(<span class="">zpng:write-png png dest</span>)</span></span>)</span></span>)</span></div> Функция make-other-png принимает путь к оригинальному файлу, путь для сохранения нового изображения и цвет, который должен являться базовым для нового изображения.<br /><br />Опробовал данный код и остался очень доволен результатом. Вот что получается в результате вызова <div class="code"><span class="paren1">(<span class="">make-other-png <span class="string">"win_LB.png"</span> <span class="string">"out.png"</span> '<span class="paren2">(<span class="">0 192 0</span>)</span></span>)</span> </div><a href="http://4.bp.blogspot.com/_g61JPQ_dZng/TO5Rhix9wJI/AAAAAAAAAFY/3YjC_BEfeOU/s1600/win_LB.png"><img src="http://4.bp.blogspot.com/_g61JPQ_dZng/TO5Rhix9wJI/AAAAAAAAAFY/3YjC_BEfeOU/s400/win_LB.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5543457828053565586" style="margin-top: 0px; margin-right: 10px; margin-bottom: 10px; margin-left: 0px; cursor: pointer; width: 16px; height: 44px; " /></a><a href="http://4.bp.blogspot.com/_g61JPQ_dZng/TO5RvwUoM5I/AAAAAAAAAFg/iUS7nistiWY/s1600/out.png"><img src="http://4.bp.blogspot.com/_g61JPQ_dZng/TO5RvwUoM5I/AAAAAAAAAFg/iUS7nistiWY/s400/out.png" border="0" alt="" id="BLOGGER_PHOTO_ID_5543458072206783378" style="margin-top: 0px; margin-right: 10px; margin-bottom: 10px; margin-left: 0px; cursor: pointer; width: 16px; height: 44px; " /></a><br />Слева оригинальное изображение, а с права получившееся в результате преобразования.<br /><br />P.S. Ebuild для <a href="http://github.com/Ramarren/png-read">png-read</a> я добавил в свой форк gentoo-lisp-overlay.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com1tag:blogger.com,1999:blog-5411819754291292105.post-90646418842442505292010-11-24T07:53:00.000-08:002010-11-24T08:08:45.923-08:00Переделал свой форк cl-pdfФорк cl-pdf я сделал довольно давно и тогда я ещё плохо ориентировался как в CL, так и в git, в итоге форк был оформлен очень топорно, без истории изменений. Сейчас дошли руки полностью его переделать используя git svn, так что в него попала полная история изменения. Все свои изменения также внёс одно за другим. Так что стало намного лучше и можно теперь нормально синхронизироваться с основным репозиторием, если там вдруг будут изменения, а они там бывают, хоть и реже чем раз в год.<br /><br /><div>От оригинальной версии мой форк отличается следующим:</div><div><ul><li>Почищен разный мусор, типа каких-то левых патчей для поддержки CMUCL, различных вариаций на тему zlib и т.п., которые предлагалось как-то загружать руками</li><li>Для сжатия используется salza2 и только она.</li><li>Поддерживается загрузка и использования ttf шрифтов с помощью zpb-ttf</li><li>У функций draw-centered-text, draw-left-text и draw-right-text имеется дополнительный опциональный параметр max-height (параметр max-width уже был в оригинальной версии)</li><li>Добавлена функция append-child-ouline, а также экспортируется функция outline-root</li></ul><div>Вообще надо немного привести в порядок код для генерации PDF, который я использую на работе, а также код для генерации PDF-версии PCL, который используется на lisper.ru и в соответствии с этим также внести ряд небольших изменений. </div><div><br /></div><div>Плюс, есть желание выкинуть из cl-pdf код для парсинга PNG-файлов и использовать для этого библиотеку <a href="http://github.com/Ramarren/png-read">png-read</a> (которую я обнаружил на днях) и сделать возможным использование PNG-изображений с прозрачностью (сейчас мне приходиться насильственно добавлять к таким изображениям фон). </div></div><div><br /></div><div>Мой форк: <a href="http://github.com/archimag/cl-pdf">http://github.com/archimag/cl-pdf</a></div>archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com0tag:blogger.com,1999:blog-5411819754291292105.post-84131004999471348982010-11-18T13:52:00.000-08:002010-11-18T14:22:35.428-08:00Необычное использование restas-directory-publisherМодуль <a href="https://github.com/archimag/restas-directory-publisher/">restas-directory-publisher</a> по начальной задумке предназначался для простой публикации директорий, содержащих статические файлы. Но в последнее время я использовал его сразу несколькими способами, которые я никак не ожидал в момент разработки и которые показались мне довольно любопытными. Так что решил немного об этом рассказать.<br /><br />Сейчас у меня возникла необходимость показывать на странице пользователю диалог, в котором о мог бы выбрать файл, находящийся на файловой системе сервера. Немного погуглив нашёл несколько решений и примеров для <a href="http://jquery.com/">jquery</a>, которые показались мне просто ужасными и я решил, что сделать собственное решение будет значительно проще и быстрее. Как оказалось, делается оно почти тривиально.<br /><br />Полученное мною решение состоит из трёх частей: шаблон cl-closure-template для генерации контента на стороне клиента, несколько строк кода на JavaScript для управления и серверная часть, которая возвращает информацию о файловой системе. <br /><br />Модуль <a href="https://github.com/archimag/restas-directory-publisher/">restas-directory-publisher</a> умеет собирать информацию о файловой системе, но по умолчанию возвращаёт её в формате html, а мне для данной задачи нужно в формате JSON. Исправить этот недостаток можно так: <pre>(defun encode-json (obj)<br /> (flet ((encode-json-list (list stream)<br /> (if (keywordp (car list))<br /> (json:encode-json-plist list stream)<br /> (json::encode-json-list-guessing-encoder list stream))))<br /> (let ((json::*json-list-encoder-fn* #'encode-json-list))<br /> (json:encode-json-to-string obj))))<br /><br />(restas:mount-submodule -file-system- (#:restas.directory-publisher)<br /> (restas.directory-publisher:*baseurl* '("api"))<br /> (restas.directory-publisher:*directory* #P"/")<br /> (restas.directory-publisher:*autoindex* t)<br /> (restas.directory-publisher:*autoindex-template* #'encode-json))</pre> Здесь производится настройка подключения субмодуля и с переменной restas.directory-publisher:*autoindex-template*, используемой для генерации контента, связывается функция #'encode-json (реализацию данной функции я уже приводил <a href="http://archimag-dev.blogspot.com/2010/06/cl-json-plists.html">ранее</a>).<br /><br />Шаблон для генерации разметки: <pre>{template directoryBrowse}<br /> <table summary="Directory Listing" cellpadding="0" cellspacing="0"><br /> <thead><br /> <tr><br /> <th class="n">Name</th><br /> <th class="m">Last Modified</th><br /> <th class="s">Size</th><br /> <th class="t">Type</th></tr><br /> </thead><br /><br /> <tbody><br /> {if $parent}<br /> <tr><br /> <td class="n"><br /> <span class="directory" href="{$parent}">Parent Directory</span><br /> </td><br /> <td class="m"> </td><br /> <td class="s">- </td><br /> <td class="t">Directory</td><br /> </tr><br /> {/if}<br /><br /> {foreach $path in $paths}<br /> <tr><br /> <td class="n"><br /> <span class="{$path.type == 'Directory' ? 'directory' : 'file'}" href="{$path.href}"><br /> {$path.name}<br /> </span><br /> {nil}<br /> {if $path.type == 'Directory'}/{/if}<br /> </td><br /> <td class="m">{$path.lastModified}</td><br /> <td class="s">{$path.size ? $path.size : '- ' |noAutoescape}</td><br /> <td class="t">{$path.type}</td><br /> </tr><br /> {/foreach}<br /> </tbody><br /> </table><br />{/template} </pre> Я лишь немного модифицировал шаблон, используемый в <a href="https://github.com/archimag/restas-directory-publisher/">restas-directory-publisher</a><br /><br />Управлящий код на JavaScript совсем прост: <pre>$(document).ready( function () { browse("/api/"); } );<br /><br />function browse (url) {<br /> function directoryClick (evt) {<br /> browse($(evt.currentTarget).attr("href"));<br /> }<br /><br /> function fileClick (evt) {<br /> $("h1").html(decodeURI($(evt.currentTarget).attr("href")));<br /> }<br /><br /> function handler (data) {<br /> $("#content").html(restas.jsBrowser.view.directoryBrowse(data));<br /> $("#content .directory").click(directoryClick);<br /> $("#content .file").click(fileClick);<br /> }<br /><br /> $.getJSON(url, handler);<br />}</pre> Я организовал этот код в виде отдельного законченного примера jsBrowser, который включил в состав <a href="https://github.com/archimag/restas-directory-publisher/">restas-directory-publisher</a>, посмотреть исходный код можно <a href="https://github.com/archimag/restas-directory-publisher/tree/master/example/jsBrowser/">здесь</a>archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com0tag:blogger.com,1999:blog-5411819754291292105.post-1199599989202359572010-11-16T05:07:00.001-08:002010-11-16T05:19:11.907-08:00cl-mssql и FreeTDS-0.82Обновил у себя <a href="http://www.freetds.org/">FreeTDS</a> до версии 0.82 и обнаружил проблемы с кодировками. Я использую <a href="http://code.google.com/p/cl-mssql/">cl-mssql</a> для взаимодействия с 1С, данные там лежат в кодировке cp1251, а у меня в системе используется utf-8. Версия FreeTDS-0.62 кажется вообще никак не учитывала кодировки, поэтому в cl-mssql есть параметр соединения :external-format, который использовался для настройки переменной cffi:*default-foreign-encoding* - я устанавливал его в :cp1251 и спокойно работал. Версия FreeTDS-0.82 уже относится к этому не так просто и, вероятно, самостоятельно занимается перекодированием строк (а может как-то по другому взаимодействует с сервером, я не спец в этом вопросе). Теперь приходиться настраивать кодировку в /etc/freetds.conf: <pre>[global]<br /> client charset = utf8</pre> Кодировка, указанная в /etc/freetds.conf, должна совпадать с кодировкой, которая указывается в mssql:connect (по-умолчанию - :utf-8).archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com2tag:blogger.com,1999:blog-5411819754291292105.post-40643322069623664482010-11-15T06:16:00.000-08:002010-11-15T06:21:21.533-08:00cl-popenПереименовал свою либу iolib.process в <a href="https://github.com/archimag/cl-popen">cl-popen</a> и несколько изменил интерфейс. Использовать для создания и взаимодействия с дочерними потоками через стандартные потоки ввода/вывода можно, например, так: <pre>(popen:with-popen2 ("cat | grep good" conveyer pin pout)<br /> (write-line "Java is bad" pin)<br /> (write-line "Python is bad" pin)<br /> (write-line "Common Lisp is good" pin)<br /> (write-line "imho" pin)<br /> (close pin)<br /> (read-line pout))</pre> Поскольку стал использовать эту либу по работе, то добавил ebuild в <a href="https://github.com/archimag/archimag-lisp-overlay">свой оверлей</a>.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com3tag:blogger.com,1999:blog-5411819754291292105.post-80797080466783031702010-11-14T11:47:00.000-08:002010-11-14T11:49:02.615-08:00ПредупреждениеСегодня ночью ожидается недоступность lisper.ru в связи с техническими работа в датацентре.archimaghttp://www.blogger.com/profile/07997791035847047137noreply@blogger.com0