четверг, 24 апреля 2014 г.

nginx: эмуляция сложных операций в условии if с помощью регулярных выражений

Это продолжение статьи об эмуляции вложенных if в nginx с помощью регулярных выражений. На этот раз будем решать другую задачу: проверим, находится ли значение какого-нибудь объекта, например куки a, в некотором заданном списке. Пусть в этом списке, назовем его a_list, находятся значения, разделенные точкой с запятой. При равенстве куки a одному из значений в списке будем выполнять специальное действие, для простоты — выводить строку PASSED, в противном случае — выводить строку FAILED. Вот конфигурация nginx.
events {
    worker_connections  1024;
}

http {
    server {
        listen       80;
        server_name  localhost;

        set $a_list " foo; bar ;  1qwerty;a";

        location / {
            set $check_a $a_list::$cookie_a;
            if ($check_a ~* "(?:[^;]+\s*;\s*)*(?<=;|^)\s*([^;]+)\s*(?=;|::).*::\1$") {
                echo "PASSED";
                break;
            }
            echo "FAILED";
        }
    }
}
Я намеренно расставил разное количество пробелов вокруг точек с запятыми в списке a_list, чтобы показать, что это не будет являться проблемой при правильно составленном регулярном выражении в условии if. Как и в предыдущей статье, здесь создается проверочная переменная check_a, состоящая из двух частей — списка a_list и значения куки a, разделенных двумя двоеточиями. Внутри регулярного выражения в условии if два двоеточия соответствуют этому разделению двух переменных. В правой части выражения, которая соответствует значению куки a, находится обратная ссылка \1 на значение, выделенное круглыми скобками в левой части — в них мы ожидаем одно из значений из списка a_list. Таким образом, если значение куки a совпадет с одним из значений в списке a_list, то переменная check_a будет соответствовать этому регулярному выражению и условие if окажется верным. Самое сложное здесь — это составить выражение для левой части регулярного выражения в условии if. Его центральная часть — атом ([^;]+), который будет соответствовать обратной ссылке \1 из правой части. Этот атом может заканчиваться некоторым количеством пробельных символов (\s*) и точкой с запятой, либо двумя двоеточиями ((?=;|::)), если значение справа окажется равным последнему элементу из списка a_list. Перед центральным атомом могут находиться другие элементы, разделенные точкой с запятой ((?:[^;]+\s*;\s*)*), которые нас не интересуют, а также некоторое количество пробельных символов (\s*). Важно проверить, что перед центральным атомом в левой части, включая возможные пробельные символы вначале, стоит точка с запятой, либо это начало строки ((?<=;|^)), иначе хвосты элементов из списка a_list, такие как oo для foo или даже y для 1qwerty приведут к срабатыванию всего регулярного выражения. Давайте проверим конфигурацию.
curl -b 'a=foo' 'http://localhost:80/'
PASSED
curl -b 'a=oo' 'http://localhost:80/'
FAILED
curl -b 'a=foo1' 'http://localhost:80/'
FAILED
curl -b 'a=a' 'http://localhost:80/'
PASSED
curl -b 'a=' 'http://localhost:80/'
FAILED
curl -b 'a=qwert' 'http://localhost:80/'
FAILED
curl -b 'a=1qwerty' 'http://localhost:80/'
PASSED
Работает! А теперь усложним задачу. Будем сравнивать не на точное соответствие значения переменной справа, а на присутствие в ней слова из списка a_list. Это еще не всё. Пусть при этом значение некоторой другой переменной (например, cookie_b) будет равно SUCCESS — опять эмуляция вложенных if! Где это можно применить? Подставьте вместо cookie_b имя ssl_client_verify, а вместо cookie_a — имя ssl_client_i_dn. Получаем проверку клиентского SSL сертификата с дополнительной проверкой того, что SSL issuer входит в заготовленный нами список a_list (теперь мы можем назвать этот список более осмысленно, например valid_ssl_issuers). Итак, добавим в нашу конфигурацию новый локейшн.
        location /2 {
            set $check_a $cookie_b::$a_list::$cookie_a;
            if ($check_a ~* "^SUCCESS::(?:[^;]+\s*;\s*)*(?<=;|::)\s*([^;]+)\s*(?=;|::).*::.*\b\1(?:\b|$)") {
                echo "PASSED";
                break;
            }
            echo "FAILED";
        }
Что изменилось? Переменная check_a теперь состоит из трех частей — первой частью является значение куки b, которое мы будем проверять на соответствие значению SUCCESS. Оставшиеся две части, как и раньше — элементы списка a_list и значение куки a. Соответственно изменилось регулярное выражение в условии if. Теперь оно начинается с SUCCESS::. Вторая часть, за исключением look-behind атома (?<=;|::), который теперь проверяет, что центральный атом центральной части выражения начинается с точки с запятой либо с двух двоеточий, возможно дополненных пробельными символами, не изменилась. Наконец, последняя часть, которая соответствует значению куки a, претерпела небольшие изменения — в ней проверяется, что где-либо внутри значения куки a находится какое-либо слово из списка a_list (.*\b\1(?:\b|$)). Проверяем.
curl -b 'b=SUCCESS; a=foo' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCES; a=foo' 'http://localhost:80/2'
FAILED
curl -b 'b=SUCCESS; a=rty' 'http://localhost:80/2'
FAILED
curl -b 'b=SUCCESS; a=1qwerty' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCESS; a=--1qwerty' 'http://localhost:80/2'
PASSED
curl -b 'b=SUCCESS; a=7-1qwerty:7' 'http://localhost:80/2'
PASSED
Все правильно. Напомню, что символы - и : являются границами слова, поэтому два последних случая удовлетворяют нашему регулярному выражению.

воскресенье, 13 апреля 2014 г.

Несколько трюков с пакетом Latex Listings

Здесь я хочу показать три трюка с пакетом Listings, которые мне очень пригодились при определении “языка” для подсветки действий пользователя внутри командных оболочек и REPLов. Этот виртуальный язык, который я назвал shelloutput, может быть использован в плагине publish-helper. Некоторые сведения о реализации подсветки shelloutput в Listings приведены в разделе Highlighting shells and REPLs на главной странице проекта в github, там же приведен пример с картинками. Итак, определение псевдоязыка shelloutput — это и есть первый трюк, о котором я расскажу. Постановка задачи такова: обеспечить декоративное выделение блоков текста, изображающих действия пользователя внутри любых командных оболочек и REPLов, а также разделение текста с вводом пользователя (промпта) и выводом оболочки. Первая часть задачи решается без определения свойств языка и, соответственно, без участия пакета Listings. Простейший способ — определить новое окружение, внутри которого использовать какой-либо рамочный пакет, например пакет Framed.
\usepackage{framed}
\newenvironment{Leftbar}{
  \setlength\parskip{0pt}
  \setlength\OuterFrameSep{4pt}
  \setlength\partopsep{-\topsep}
  \begin{leftbar}
    \scriptsize
}{\end{leftbar}}
Это окружение, которое я назвал Leftbar, поскольку оно использует стиль leftbar из пакета Framed, должно находиться в преамбуле документа. Теперь, чтобы выделить блок текста с действиями пользователя в оболочке, нужно обернуть этот текст в тэги \begin{Leftbar} ... \end{Leftbar}. Перейдем к решению второй части задачи. Нам нужно определить новый язык, который мы сможем использовать в окружении lstlisting, которое, в свою очередь, мы будем помещать внутрь окружения Leftbar. Пусть основной стиль (basicstyle) языка shelloutput будет относиться к выводу оболочки, а стиль для ввода оболочки мы определим отдельно. Самое сложное здесь — это понять, как мы (и Listings) будем отличать ввод оболочки от вывода: промпты у всех оболочек разные и могут отсутствовать вообще. Поэтому мы не будем ориентироваться на промпт, а введем собственный виртуальный промпт, который будем вставлять в начало каждой строки, соответствующей вводу оболочки — ведь мы то знаем, где у нее ввод и вывод! В Listings есть опция moredelim, с помощью которой мы определим внешний вид ввода оболочки, передав ей наш виртуальный промпт (пусть это будет три бара и пробел — ‘||| ’) с флагами il, которые устанавливают, что Listings не должен показывать виртуальный промпт (i), и установки будут действовать в пределах всей строки (l). Вот как это может выглядеть в преамбуле документа:
\usepackage{listings}
\definecolor{shellpromptcolor}{HTML}{000000}
\definecolor{shelloutputcolor}{HTML}{666666}
\lstdefinelanguage{shelloutput}
  {basicstyle=\color{shelloutputcolor}
    \scriptsize
    \ttfamily\itshape,
   moredelim=[il][\color{shellpromptcolor}\upshape]{|||\ }}
В данном случае вывод оболочки будет подсвечен серым цветом и иметь наклонное начертание, в то время как ввод будет черным и прямым. Давайте добавим сюда небольшой, но красивый бонус — декоративные переносы строк. Для этого в преамбулу документа нужно добавить
\usepackage{MnSymbol}
\lstset{prebreak=\raisebox{0ex}[0ex][0ex]
  {\ensuremath{\rhookswarrow}}}
\lstset{postbreak=\raisebox{0ex}[0ex][0ex]
  {\ensuremath{\rcurvearrowse\space}}}
Пример использования подсветки виртуального языка shelloutput можно найти в разделе An example на странице разработки проекта vim-publish-helper. В этом примере документ, представленный на втором изображении, генерируется не напрямую из документа TeX, а опосредованно, через pandoc flavoured markdown, но результат будет совпадать независимо от языка исходного документа. Второй трюк. Listings преобразует символ переноса (-) в знак минус. Это разумно для многих языков, однако для нашего псевдоязыка shelloutput это выглядит некрасиво, во всяком случае в опциях команд оболочки, таких как ls -l --inode. Вернуть исходный символ переноса достаточно просто — определив опцию literate={-}{-}1 в преамбуле документа или внутри окружения lstlisting, однако этот простой подход ломает настройки alsoletter={-} и breakatwhitespace для символов переноса, что приводит к возможности обрыва строк при переносе на этих символах, а это выглядит очень некрасиво. Лучше всего определить макрос lst@CCPutMacro для символа переноса (фактически переопределить его, так как он уже определен — и в этом-то и проблема — в исходном коде listings.sty). Определение следует поместить в нашем окружении Leftbar, чтобы оно не повлияло на другие языки. Теперь Leftbar будет выглядеть так:
\makeatletter
\newenvironment{Leftbar}{
  \setlength\parskip{0pt}
  \setlength\OuterFrameSep{4pt}
  \setlength\partopsep{-\topsep}
  \lst@CCPutMacro\lst@ProcessLetter {"2D}{\lst@ttfamily{-{}}{-{}}}
  \@empty\z@\@empty
  \begin{leftbar}
    \scriptsize
}{\end{leftbar}}
\makeatother
Третий трюк. Русские символы (и вообще UTF-8) в выводе (и вводе) языка shelloutput. Оказывается, Listings до сих пор не поддерживает нормальную обработку национальных символов в формате UTF. Если в shelloutput встретятся русские символы, то они будут переставлены всевозможными способами с удалением пробелов и добавлением неожиданных новых строк. Проблема известная и есть вариант ее решения с помощью опции literate (см. здесь и здесь). Однако, этот способ очень verbose — лично мне он не нравится. Более простое и рабочее решение — использовать опцию escapeinside=|| в объявлении lstlisting, а внутри блока с текстом обрамлять участки с проблемными символами соответствующими эскейп-символами, в данном случае барами (|) и, возможно, внутри них тэгом \verb, который не позволит слипнуться множественным пробелам.

суббота, 5 апреля 2014 г.

Простой фильтр pandoc для преобразования параграфов HTML в блоки спанов

Вот исходный код фильтра (файл paraToSpanBlock.hs).
-- paraToSpanBlock.hs
import Text.Pandoc.JSON

paraToSpanBlock :: Maybe Format -> Block -> IO Block
paraToSpanBlock (Just (Format "html")) (Para contents) =
    return $ Plain [Span ("", [], [("style", style)]) contents]
    where style = "display: block; margin-bottom: 16px;\
                 \ font-family: Arial, Helvetica, sans-serif;"
paraToSpanBlock _ b = return b

main :: IO ()
main = toJSONFilter paraToSpanBlock
Фильтр преобразует блоки параграфов <p> ... </p> в блоки спанов <span style="STYLE"> ... </span>, где STYLE — значение, возвращаемое функцией style, объявленной внутри исходного кода фильтра и устанавливающей внешний вид этих блоков — величину отступа снизу и шрифт. Этот фильтр позволяет с легкостью генерировать код HTML для непосредственной вставки в блог на blogspot.com. Да, этот текст написан на pandoc flavoured markdown и преобразован в HTML с помощью команды
pandoc -thtml -FparaToSpanBlock -Fvimhl -S src.md
Здесь src.md — файл с этим исходным текстом, paraToSpanBlock и vimhl — фильтры paraToSpanBlock, о котором идет речь, и vimhl, о котором я писал здесь. Для компиляции фильтра paraToSpanBlock в командной строке нужно ввести
ghc --make paraToSpanBlock
Скомпилированную программу paraToSpanBlock следует поместить в директорию, объявленную в переменной окружения PATH. Плюсы в использовании простого текстового языка разметки для создания контента блога очевидны. Самое главное — это возможность редактирования контента в простом текстовом редакторе. Второе — возможность генерации документов в разных форматах, например в формате PDF, и здесь такой замечательный инструмент как Pandoc проявляет себя во всей красе. Третье (в том числе как следствие использования автоматических инструментов) — последовательное применение стилей к разным элементам текста, не допускающее случайных ошибок автора документа.

воскресенье, 2 февраля 2014 г.

nginx: почему if уровня server почти бесполезен при разработке модуля

Давайте посмотрим, как работает следующая конфигурация nginx.
worker_processes  1;
error_log  logs/error.log  notice; 

events {
    worker_connections  1024;
}

http {
    server {
        listen       80;
        server_name  localhost;

        if ($arg_a) {
            rewrite_log on;
            rewrite ^(.*)$ /catch$1;
        }

        location /catch {
            internal;
            rewrite ^/catch(.*)$ $1 last;
        }

        location /test {
            return 403;
        }
    }
}
Логика здесь простая: если в URI присутствует аргумент a, то мы хотим увидеть в логе трейс всех реврайтов (rewrite_log on). Сделаем запрос без аргумента a.
$ curl 'http://localhost/test'
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.5.9</center>
</body>
</html>
Ожидаемый ответ 403, ожидаемое отсутствие записей в логе. А теперь добавим в URI аргумент a.
$ curl 'http://localhost/test?a=1'
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.5.9</center>
</body>
</html>
Всё то же самое, в том числе и отсутствие записей в логе. Но последний факт - это уже неожиданность, ведь мы установили флаг rewrite_log внутри серверного if ($arg_a).

В чем же проблема? Давайте взглянем на определение директивы rewrite_log в исходном коде модуля rewrite в файле src/http/modules/ngx_http_rewrite_module.c.
    { ngx_string("rewrite_log"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_SIF_CONF|NGX_HTTP_LOC_CONF
                        |NGX_HTTP_LIF_CONF|NGX_CONF_FLAG,
      ngx_conf_set_flag_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_rewrite_loc_conf_t, log),
      NULL },
Из определения видно, что директива устанавливает флаг log, который определен в конфигурации уровня location. А теперь посмотрим, что делает директива if с конфигурацией уровня location (в том же файле в функции ngx_http_rewrite_if()).
    if (pclcf->name.len == 0) {
        if_code->loc_conf = NULL;
        cf->cmd_type = NGX_HTTP_SIF_CONF;

    } else {
        if_code->loc_conf = ctx->loc_conf;
        cf->cmd_type = NGX_HTTP_LIF_CONF;
    }
В случае, если директива if находится на уровне server, контекст уровня location отсутствует! Это объясняет, почему директивы, настраивающие данные конфигурации уровня location, такие как rewrite_log в модуле rewrite, не работают в серверном if. С другой стороны, директивы типа set и rewrite из того же модуля будут прекрасно работать внутри серверного if, поскольку они не связаны с изменением данных внутри конфигурации уровня location, а создают глобальные объекты для дальнейшей интерпретации во время исполнения программы.

Давайте посмотрим, что было бы, если бы флаг log был объявлен внутри конфигурации уровня server. В модуле rewrite нет конфигурации уровня server, мы ее создадим... В общем, я просто приведу патч относительно версии nginx 1.5.9.
--- src/http/modules/ngx_http_rewrite_module.c 2014-02-02 19:52:13.954829708 +0400
+++ src/http/modules/ngx_http_rewrite_module.c.new 2014-02-02 19:15:00.060722697 +0400
@@ -11,15 +11,22 @@
 
 
 typedef struct {
+    ngx_flag_t    log;
+} ngx_http_rewrite_srv_conf_t;
+
+
+typedef struct {
     ngx_array_t  *codes;        /* uintptr_t */
 
     ngx_uint_t    stack_size;
 
-    ngx_flag_t    log;
     ngx_flag_t    uninitialized_variable_warn;
 } ngx_http_rewrite_loc_conf_t;
 
 
+static void *ngx_http_rewrite_create_srv_conf(ngx_conf_t *cf);
+static char *ngx_http_rewrite_merge_srv_conf(ngx_conf_t *cf,
+    void *parent, void *child);
 static void *ngx_http_rewrite_create_loc_conf(ngx_conf_t *cf);
 static char *ngx_http_rewrite_merge_loc_conf(ngx_conf_t *cf,
     void *parent, void *child);
@@ -86,8 +93,8 @@
       NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_SIF_CONF|NGX_HTTP_LOC_CONF
                         |NGX_HTTP_LIF_CONF|NGX_CONF_FLAG,
       ngx_conf_set_flag_slot,
-      NGX_HTTP_LOC_CONF_OFFSET,
-      offsetof(ngx_http_rewrite_loc_conf_t, log),
+      NGX_HTTP_SRV_CONF_OFFSET,
+      offsetof(ngx_http_rewrite_srv_conf_t, log),
       NULL },
 
     { ngx_string("uninitialized_variable_warn"),
@@ -109,8 +116,8 @@
     NULL,                                  /* create main configuration */
     NULL,                                  /* init main configuration */
 
-    NULL,                                  /* create server configuration */
-    NULL,                                  /* merge server configuration */
+    ngx_http_rewrite_create_srv_conf,      /* create server configuration */
+    ngx_http_rewrite_merge_srv_conf,       /* merge server configuration */
 
     ngx_http_rewrite_create_loc_conf,      /* create location configuration */
     ngx_http_rewrite_merge_loc_conf        /* merge location configuration */
@@ -142,6 +149,7 @@
     ngx_http_core_srv_conf_t     *cscf;
     ngx_http_core_main_conf_t    *cmcf;
     ngx_http_rewrite_loc_conf_t  *rlcf;
+    ngx_http_rewrite_srv_conf_t  *rscf;
 
     cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
     cscf = ngx_http_get_module_srv_conf(r, ngx_http_core_module);
@@ -169,10 +177,12 @@
         return NGX_HTTP_INTERNAL_SERVER_ERROR;
     }
 
+    rscf = ngx_http_get_module_srv_conf(r, ngx_http_rewrite_module);
+
     e->ip = rlcf->codes->elts;
     e->request = r;
     e->quote = 1;
-    e->log = rlcf->log;
+    e->log = rscf->log;
     e->status = NGX_DECLINED;
 
     while (*(uintptr_t *) e->ip) {
@@ -227,6 +237,34 @@
 
 
 static void *
+ngx_http_rewrite_create_srv_conf(ngx_conf_t *cf)
+{
+    ngx_http_rewrite_srv_conf_t  *conf;
+
+    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_rewrite_srv_conf_t));
+    if (conf == NULL) {
+        return NULL;
+    }
+
+    conf->log = NGX_CONF_UNSET;
+
+    return conf;
+}
+
+
+static char *
+ngx_http_rewrite_merge_srv_conf(ngx_conf_t *cf, void *parent, void *child)
+{
+    ngx_http_rewrite_srv_conf_t *prev = parent;
+    ngx_http_rewrite_srv_conf_t *conf = child;
+
+    ngx_conf_merge_value(conf->log, prev->log, 0);
+
+    return NGX_CONF_OK;
+}
+
+
+static void *
 ngx_http_rewrite_create_loc_conf(ngx_conf_t *cf)
 {
     ngx_http_rewrite_loc_conf_t  *conf;
@@ -237,7 +275,6 @@
     }
 
     conf->stack_size = NGX_CONF_UNSET_UINT;
-    conf->log = NGX_CONF_UNSET;
     conf->uninitialized_variable_warn = NGX_CONF_UNSET;
 
     return conf;
@@ -252,7 +289,6 @@
 
     uintptr_t  *code;
 
-    ngx_conf_merge_value(conf->log, prev->log, 0);
     ngx_conf_merge_value(conf->uninitialized_variable_warn,
                          prev->uninitialized_variable_warn, 1);
     ngx_conf_merge_uint_value(conf->stack_size, prev->stack_size, 10);
В новой экспериментальной версии модуля rewrite мы перенесли флаг log из конфигурации уровня location в специально созданную конфигурацию уровня server, а также объявили новые хендлеры для создания и мерджа конфигураций уровня server, в которые перенесли все манипуляции с флагом log из соответствующих хендлеров конфигураций уровня location.

Компилируем nginx, перезапускаем сервер и посылаем те же тестовые запросы. Оба запроса возвращают ожидаемый 403 Forbidden, но теперь в случае, когда в URI запроса присутствует аргумент а, мы получаем следующую запись в логе:
2014/02/02 20:46:05 [notice] 7345#0: *2 "^(.*)$" matches "/test", client: 127.0.0.1, server: localhost, request: "GET /test?a=1 HTTP/1.1", host: "localhost"
2014/02/02 20:46:05 [notice] 7345#0: *2 rewritten data: "/catch/test", args: "a=1", client: 127.0.0.1, server: localhost, request: "GET /test?a=1 HTTP/1.1", host: "localhost"
2014/02/02 20:46:05 [notice] 7345#0: *2 "^/catch(.*)$" matches "/catch/test", client: 127.0.0.1, server: localhost, request: "GET /test?a=1 HTTP/1.1", host: "localhost"
2014/02/02 20:46:05 [notice] 7345#0: *2 rewritten data: "/test", args: "a=1", client: 127.0.0.1, server: localhost, request: "GET /test?a=1 HTTP/1.1", host: "localhost"
Здорово, мы добились того, к чему стремились! Однако потеряли несравненно большее. Теперь мы не сможем гибко настраивать флаг rewrite_log для каждого отдельного локейшна. Сервер просто не запустится, если этот флаг будет упомянут в одном блоке server более одного раза.

Давайте подытожим. Использование директивы, настраивающей данные в контексте location внутри директивы if уровня server бесполезно. Если бы это работало, то было бы несомненно полезно тем, что позволило бы записывать конфигурационный файл nginx в более выразительном и лаконичном стиле. С другой стороны, выразительный стиль все же доступен, если директива настраивает данные уровня server, однако в этом случае совершенно теряется возможность гибко настраивать конфигурацию для отдельных локейшнов сервера.

Я столкнулся с подобной проблемой при разработке кастомного модуля nginx. И мне пришлось пожертвовать гибкостью настройки внутри отдельных локейшнов ради возможности вызывать директиву из серверного if: в моем случае это казалось более обещающим, чем гибкость настройки. Естественно, перед тем как принять решение, я попытался изучить, как работают с серверным if модули, поставляемые вместе с nginx. Результат сначала показался удивительным: кроме модуля rewrite ни один стандартный модуль не работал с серверным if. Чтобы убедиться в этом, достаточно ввести команду
grep -r NGX_HTTP_SIF_CONF src/
внутри директории с исходниками nginx. Однако, если учесть только что описанный недостаток серверного if, то это уже не выглядит столь странно.

четверг, 9 января 2014 г.

vim: плагин publish_helper

Работу, начатую здесь и здесь, решил оформить в виде плагина vim (страница на гитхабе и страница на www.vim.org). Полная документация доступна по ссылкам, а также внутри плагина. Плагин состоит из двух частей. Первая часть - это собственно скрипт на viml, в котором определены команды MakeHtmlCodeHighlight (переименованная и улучшенная MakeBlogArticle) и MakeTexCodeHighlight (тоже улучшенная по сравнению с первой реализацией). Вторая часть написана на haskell и представляет собой фильтр для pandoc (фильтры доступны в pandoc начиная с версии 1.12), который позволяет подменять исходные блоки кода (CodeBlock) внутри абстрактного синтаксического дерева, представляющего документ, на неформатированные блоки (RawBlock), содержащие подсвеченный с помощью vim код.

Для того, чтобы показать как это работает, возьмем оригинальный пример из руководства пользователя pandoc, в котором приведена классическая реализация быстрой сортировки на haskell, и добавим туда еще пару образцов кода.
### Original example from [*Pandoc User's Guide*](http://johnmacfarlane.net/pandoc/README.html#fenced-code-blocks)

~~~~ {#mycode .haskell .numberLines hl="vim" startFrom="100"}
qsort []     = []
qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
               qsort (filter (>= x) xs)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

### Small C++ code sample

~~~~ {.cpp hl="vim"}
G4UIQt *  qtSession( dynamic_cast< G4UIQt * >( session ) );

if ( qtSession )
{
    qtSession->AddMenu( histoMenuHandle, histoMenuLabel );
    BuildMenuTree( qtSession, histoMenuHandle, gDirectory->GetList() );
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

### Content of my *~/.vimrc.pandoc*

~~~~ {#vimrc_pandoc .vim .numberLines hl="vim"}
syntax on

filetype on
filetype indent on
filetype plugin on
filetype plugin indent on

let g:lucius_style = 'light'
let g:lucius_contrast = 'high'
let g:lucius_contrast_bg = 'high'

set nocp    " for line breaks with backslashes
colorscheme lucius

let g:PhCtrlTrans = 0
let g:PhHtmlPreAttrs = 'style="white-space: pre-wrap;"'

runtime plugin/publish_helper.vim
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Это синтаксическая подсветка цветовой схемы lucius для типа файла pandoc, который будет доступен, если вы установите плагин для pandoc отсюда. Я вставил ее сюда из vim дословно с помощью команды MakeHtmlCodeHighlight.

Идем дальше. Сохраняем этот пример в файле example.md и генерируем HTML файл example.html с помощью pandoc и фильтра vimhl из нашего плагина.
pandoc --standalone -t html -F vimhl -o example.html example.md
Получаем файл example.html с таким содержимым:
 

Original example from Pandoc User's Guide

100  qsort []     = []
101  qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
102                 qsort (filter (>= x) xs)

Small C++ code sample

G4UIQt *  qtSession( dynamic_cast< G4UIQt * >( session ) );

if ( qtSession )
{
    qtSession->AddMenu( histoMenuHandle, histoMenuLabel );
    BuildMenuTree( qtSession, histoMenuHandle, gDirectory->GetList() );
}

Content of my ~/.vimrc.pandoc

 1  syntax on
 2  
 3  filetype on
 4  filetype indent on
 5  filetype plugin on
 6  filetype plugin indent on
 7  
 8  let g:lucius_style = 'light'
 9  let g:lucius_contrast = 'high'
10  let g:lucius_contrast_bg = 'high'
11  
12  set nocp    " for line breaks with backslashes
13  colorscheme lucius
14  
15  let g:PhCtrlTrans = 0
16  let g:PhHtmlPreAttrs = 'style="white-space: pre-wrap;"'
17  
18  runtime plugin/publish_helper.vim

Это цвета схемы lucius с правильной подсветкой синтаксиса всех образцов. А теперь создадим аналогичный PDF документ.
pandoc --standalone -t latex -F vimhl -o example.tex example.md
Открываем файл example.tex и вставляем в преамбулу строки
\usepackage{color}
\usepackage{xcolor}
\usepackage{fancyvrb}
\newcommand{\VerbBar}{|}
\newcommand{\VERB}{\Verb[commandchars=\\\{\}]}
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{commandchars=\\\{\}}
\usepackage{framed}
\newenvironment{Shaded}{
  \definecolor{shadecolor}{rgb}{1.0, 1.0, 0.9}
  \setlength\parskip{0cm}
  \setlength\partopsep{-\topsep}
  \addtolength\partopsep{0.2cm}
  \begin{shaded}
    \scriptsize
}{\end{shaded}}
Зачем это нужно, вы можете прочесть в документации к плагину. Завершаем генерацию example.pdf.
latexmk -pdf example
Вот скриншот получившегося документа:

вторник, 24 декабря 2013 г.

Что делать, если у вас маленький /, а обновить Федору очень хочется

В общем-то, не обязательно /, в общем случае это точка монтирования файловой системы, в которой находятся директории /var/lib/ и /var/tmp/. На моем рабочем компьютере эти директории находятся в корневой файловой системе, размер которой составляет всего 20 Гб. Это приводит к проблемам при штатном обновлении ядра через yum, что уж тогда говорить об обновлении всей системы.

Итак, обновляться я решил вчера, 23 декабря 2013 года, с Fedora 19 до Fedora 20, то есть уже после официального выхода Fedora 20. Инструмент для обновления - fedup-0.8.0-3, совсем недавно заменивший версию 0.7. Он запускается из командной строки и в процессе загрузки пакетов использует директории /var/lib/system-upgrade/ и /var/tmp/system-upgrade/. Опций для настройки других директорий нет (хотя в файле TODO.asciidoc, входящем в состав пакета fedup, такая возможность анонсируется в будущем). То есть, если запускать fedup без всяких предварительных настроек следующим образом:
fedup --network 20 --nogpgcheck
, то мой корневой раздел будет заполнен до отказа еще до начала реального обновления системы. Кстати, обратите внимание на опцию --nogpgcheck. Как я понял, она появилась в версии fedup 0.8, и без нее мой fedup не захотел работать.

Итак, что же делать? Самое очевидное решение - снести директории /var/lib/system-upgrade/ и /var/tmp/system-upgrade/ вместе с их содержимым, найти диск, на котором достаточно места для пары-тройки гигабайт обновлений, и создать символические ссылки на эти директории из /var/lib/system-upgrade и /var/tmp/system-upgrade. У меня нашлось достаточно места на диске, который монтируется в корневую файловую систему как /home. Поэтому, от имени суперпользователя я создал на нем две директории /home/fedup/cache/ и /home/fedup/packages/, а затем и упомянутые символические ссылки:
ln -s ../../home/fedup/cache /var/tmp/system-upgrade
ln -s ../../home/fedup/packages /var/lib/system-upgrade
Обратите внимание на то, что ссылки задаются не через абсолютные пути, а через относительные, это очень важно! Дело в том, что после перезагрузки системы для ее обновления все файловые системы, в том числе /home, будут смонтированы в /sysroot, а не в /, и загрузчик просто не сможет найти пакеты для обновления (см. об этом здесь).

В общем-то, на этом историю про обновление Fedora с небольшим дисковым пространством можно было бы и закончить. Но не тут-то было! Обновление Федоры - это почти всегда приключение, сопряженное с риском и опасностью, в которых не место домохозяйкам! После перезагрузки системы для дальнейшего обновления и загрузки нескольких сервисов systemd, запустился релэйблинг SELinux, как я понял на загруженные новые пакеты (хотя SELinux в системе у меня отключен). Помурыжив минут 15, компьютер перезагрузился, оставив опцию с апгрейдом системы в верхней строке загрузчика GRUB2. Я попробовал загрузить апгрейд снова: в итоге обычная загрузка GDM как ни в чем не бывало. То есть система не обновилась! Я перезагрузился в старое ядро от Fedora 19, снес Upgrade опцию из загрузчика:
fedup --resetbootloader
и запустил fedup--network 20 --nogpgcheck сначала. После серии таких попыток обновления я понял, что проблема не в новых пакетах, установленных неожиданным для fedup образом, а в чем-то другом. На правильный путь меня натолкнуло описание этого бага. В одном из комментариев было сказано про аргумент ядра systemd.unit=system-upgrade.target, который устанавливал старый fedup 0.7 для старта процесса обновления после перезагрузки системы. Я открыл /boot/grub2/grub.cfg, нашел секцию, соответствующую апгрейду системы, и вставил туда этот параметр, на авось, поскольку так и не понял, нужен ли он новому fedup 0.8 и установочному образу ядра. Кроме того, добавил туда же опцию selinux=0, чтобы не ждать релэйблинга новых пакетов (или чего-то там еще, не суть важно). Более того, новый fedup был так добр, что не установил в аргументы загрузки ядра опцию plymouth.splash=fedup: это в дальнейшем позволило наблюдать за реальным процессом установки пакетов в консоли, а не бестолковый графический прогрессбар. Если вы тоже захотите удалить эту опцию, то не пугайтесь, когда в процессе установки консоль заснет и дисплей погаснет: просто нажмите на клавиатуре Shift - это разбудит консоль.

Итак, как вы уже поняли, после добавления в аргументы загрузки ядра опции с systemd.unit, процесс пошел. Система обновилась, и после перезагрузки опция с апгрейдом системы ушла из меню GRUB2, что и должно было произойти. Однако, в момент загрузки новой Fedora 20 вернулась проблема с SELinux, с одной стороны неудивительно - я ведь устанавливал ее только для ядра, загружаемого для обновления системы. С другой стороны непонятно, почему fedup не вычистил эти пакеты сразу после обновления. После загрузки Fedora 20 и запуска fedup --clean все стало понятно: fedup сообщил, что он не в состоянии выполнить rmtree на символических ссылках. Пришлось вручную удалять все содержимое из директорий /home/fedup/cache/ и /home/fedup/packages/.

Осталось сказать, что проблему с нехваткой дискового пространства при обновлении ядра через yum решить просто - намного проще, чем с fedup. Создаем директорию /home/yum/cache/, открываем файл /etc/yum.conf и заменяем строку
cachedir=/var/cache/yum/$basearch/$releasever
на
cachedir=/home/yum/cache/$basearch/$releasever

воскресенье, 22 декабря 2013 г.

Интеграция гистограмм CERN ROOT в Qt GUI Geant4

Собственно, возможность открывать гистограммы из GUI Geant4 я прикрутил уже давно. Реализацию кооперации ROOT и Geant4 можно найти в исходном коде ChargeExchangeMC, который поставляется вместе с Geant4 как один из advanced examples. Эта реализация основана на использовании бэкенда Qt CERN ROOT, поэтому она доступна только при использовании плагина визуализации Qt Geant4.

Базовая реализация интеграции гистограмм в ChargeExchangeMC (входящая в состав последней версии Geant4) была основана исключительно на вводе пользователем команд в командной строке Geant4. Но недавно я подумал, а почему бы не создать специальное меню, в котором будут перечислены все имеющиеся гистограммы в естественном древовидном порядке. При клике на имени гистограммы в меню она будет отображаться в канвасе: это намного проще и естественнее, чем набирать вручную команду листинга гистограмм, затем выбирать из полученного списка нужную гистограмму и вводить новую команду для ее отображения.

На следующей картинке изображена сессия Qt Geant4 c одной из гистограмм, изображающей пучок, прошедший сквозь входной мониторный счетчик. Гистограмма была открыта из меню Histo, находящемся в верхней левой части окна справа от меню Viewer. Все картинки ниже кликабельны.


А это меню Histo в развернутом виде:


Одна из поддиректорий развернута, внутри нее находятся десять гистограмм. Для удобства восприятия перед именами двух- и трехмерных гистограмм стоят соответствующие значки 2: и 3:.

Самой главной технической сложностью здесь было построение подменю. Интерфейсы GUI Geant4 их просто не поддерживают. Один из вариантов реализации поддержки подменю - субклассинг G4UIQt - мне не понравился, так как из пушки по воробьям. В итоге я решил добавить новую функцию AddSubmenu() прямо в класс CexmcHistoManager. Вот реализация функции BuildMenuTree(), которая рекурсивно строит меню гистограмм, и функции AddSubmenu():
void  CexmcHistoManager::BuildMenuTree( G4UIQt *  session,
                                        const G4String &  menu, TList *  ls )
{
    TIter      objs( ls );
    TObject *  obj( NULL );

    while ( ( obj = ( TObject * )objs() ) )
    {
        G4String  name( obj->GetName() );
        G4String  title( obj->GetTitle() );

        if ( obj->IsFolder() )
        {
            AddSubmenu( session, menu, name, title );
            BuildMenuTree( session, name, ( ( TDirectory * )obj )->GetList() );
        }
        else
        {
            G4String  options( name );

            do
            {
                if ( obj->InheritsFrom( TH3::Class() ) &&
                     ! drawOptions3D.empty() )
                {
                    title = G4String( "3: " ) + title;
                    options += G4String( " " ) + drawOptions3D;
                    break;
                }
                if ( obj->InheritsFrom( TH2::Class() ) &&
                     ! drawOptions2D.empty() )
                {
                    title = G4String( "2: " ) + title;
                    options += G4String( " " ) + drawOptions2D;
                    break;
                }
                if ( obj->InheritsFrom( TH1::Class() ) &&
                     ! drawOptions1D.empty() )
                {
                    options += G4String( " " ) + drawOptions1D;
                    break;
                }
            } while ( false );

            G4String  cmd( CexmcMessenger::histoDirName + "draw " + options );
            session->AddButton( menu, title.c_str(), cmd );
        }
    }
}

void  CexmcHistoManager::AddSubmenu( G4UIQt *  session,
                                     const G4String &  parent,
                                     const G4String &  name,
                                     const G4String &  label )
{
  QMenu *  menu( new QMenu( label.c_str() ) );
  QMenu *  parentMenu( ( QMenu * )session->GetInteractor( parent ) );

  parentMenu->addMenu( menu );
  session->AddInteractor( name, ( G4Interactor )menu );
}
Предполагается, что BuildMenuTree() будет вызвана в момент инициализации менеджера гистограмм примерно таким образом:
    G4UIQt *  qtSession( dynamic_cast< G4UIQt * >( session ) );

    if ( qtSession )
    {
        qtSession->AddMenu( histoMenuHandle, histoMenuLabel );
        BuildMenuTree( qtSession, histoMenuHandle, gDirectory->GetList() );
    }
Здесь session - указатель на объект типа G4UIsession, histoMenuHandle и histoMenuLabel - строки, первая задается произвольно - это дескриптор нового главного меню в строке меню (menu bar), вторая - имя этого меню, в нашем случае она равна Histo.

Этого кода еще нет в составе ChargeExchangeMC, но я думаю добавить его к следующему релизу Geant4.