LE Blog

Инженер с поэтической душой

10.03.2016 firtree_right Эзотерический джаваскрипт

Конечно, это уже не новая история, но меня настолько впечатлило, что я решил написать внеочередную статью. Тем более, что и ребята из ибэя тоже пропустили известие о том, что можно писать валидный джаваскрипт, не используя букв и цифр. Поскольку статья внеурочная, то и иллюстрации сегодня не будет. К тому же фиг проиллюстрируешь эти эзотерические языки программирования.

Что случилось?

Началось всё, я так понимаю, ещё в 2009 году с обсуждений на форумах sla.ckers.org потенциальных возможностей межсайтового скриптинга. Но там ещё был другой синтаксис. Впечатливший же меня пример написания джаваскрипта обнаруживается только в 2012 году. Были и раньше другие заходы. Например, джаваскрипт из японских смайликов. Я же узнал об этом от товарища, который прислал мне ссылку на the daily wtf.

Позволю себе немного пересказать, а дорогому читателю предлагаю прямо открыть консоль и попробовать. Вся эта штука основана на конвертации типов в джаваскрипте при сложении или приведении к булевым значениям. Программировать на джаваскрипте можно всего лишь с помощью восьми символов: [, ], (, ), {, }, ! и +. Вот, что выдаёт мне консоль в ответ на ввод:

> +[]
0
> !+[]
true
> !!+[]
false
> +![]
0
> +!![]
1
> +!![]+!![]
2
> +!![]+!![]+!![]
3

И вот у нас уже есть числа и булевы константы. Теперь нам нужны буквы:

> !![]
true
> !![]+[]
"true"
> ![]
false
> ![]+[]
"false"
> +!![]+!![]
2
> +!![]+!![]+[]
"2"
> ({})
Object {}
> []+{}
"[object Object]"
> ({}[+[]])
undefined
> ({}[+[]])+[]
"undefined"
> +{}
NaN
> +{}+[]
"NaN"

Далее все строковые значения можно получить, комбинируя вышеизложенное, но я буду писать строки в виде строк, чтобы облегчить (сделать возможным) прочтение кода. Используя уже имеющиеся буквы и цифры, можно ещё получить:

> +"1e1000"
Infinity
> +"1e1000"+[]
"Infinity"
> +"1e100"
1e+100
> +"1e100"+[]
"1e+100"

Все буквы (a, b, c, d, e, f, i, j, l, n, o, r, s, t, u, I, O, [, ], <пробел>, +) можно доставать из строк, с помощью квадратных скобок и чисел. Но мы уже давно можем сделать eval — исполнить произвольный код из строки:

> (![]+[])[3]
"s"
> (![]+[])[3]+([]+{})[1]
"so"
...
> []['sort']['constructor']
function Function() { [native code] }
> []['sort']['constructor']('return alert')()
function alert() { [native code] }
> []['sort']['constructor']('return alert')()(1)
/* должен выскочить алерт */

Дело остаётся за малым — получить все остальные символы, с помощью которых можно написать программу. Чтобы уже совсем не чувствовать стеснения. И тут нам на помощь должна прийти функция unescape, которая из знака «%» и аски-кода делает символ. Но у нас пока нет для этого символов «%» и «p». Где же их взять?

> []['sort']['constructor']('return location')()
Location {hash: "", search: "", pathname: "/blog/posts/48", port: "", hostname: "lonelyelk.ru"…}
> []['sort']['constructor']('return location')()+[]
"http://lonelyelk.ru/blog/posts/48"
> ([]['sort']['constructor']('return location')()+[])[3]
"p"
> ({}+[])[0]
"["
> []['sort']['constructor']('return escape')()('[')
"%5D"
> []['sort']['constructor']('return escape')()('[')[0]
"%"
> []['sort']['constructor']('return unescape')()('%'+'7a')
"z"

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

02.03.2016 firtree_right Лось

Сейчас меня уже почти не спрашивают: «Почему лось?» Отчасти потому, что я не очень продвигаю этого персонажа, а отчасти потому, что люди думают, что мне просто нравятся лоси, или я их коллекционирую. Второе приводит, например, к тому, что мне дарят разных игрушечных лосей. Это забавный результат, поэтому я считаю, что репутацию местного сумасшедшего нужно поддерживать. Как следствие я начал использовать лосей в повседневной работе.

Терминал

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

лось в терминале

При этом хотелось бы, чтобы у него было разное случайное выражение лица. Для этого нужно в ~/.bash_profile (или какой у вас файл для создания окружения) написать следующее:

eye=(o O @ . - \* \~ °)
let left=${RANDOM}%${#eye[*]}
let right=${RANDOM}%${#eye[*]}

cat << EOF

 _  _        _  _  _
| || | ____ | || || |
\    |/    \|      /
 \____ ${eye[$left]}  ${eye[$right]} ______/
      |    |________
      |             }=
      | __  ____  _ |
      ||  ||    || ||
      ||  ||    || ||
      ""  ""    "" ""

EOF

Базовая работа с массивами. Встроенная в баш функция случайного числа. Обязательно предварять косой чертой «*» и «~», чтобы они не стали списком файлов и домашней директорией. По-моему — красота!

Spec runner

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

elk-spec

Здесь нужно пользоваться эскейп кодами для управления положением каретки, то есть курсора. Мало кто помнит, но курсор можно двигать во все стороны на любое количество позиций. Данный пример я собрал для раннера тестов на жасмине. Да, да, я разрабатываю в том числе на node.js и тестирую с помощью jasmine. Ключевая функция для рисования лося вот:

function printElk() {
  if (specCount > 1) {
    print('\x1b[5A');
  }
  if (specCount % 2 === 0) {
    print(Array(specCount + 1).join(' ') + '     ^^' + eyes() + '^^\n');
    print(Array(specCount + 1).join(' ') + '  _____ U\n');
    print(specTrail + '~(  _  /\n');
    print(Array(specCount + 1).join(' ') + '  || ||\n');
    print(Array(specCount + 1).join(' ') + '  ^^ ^^\n');
  } else {
    print(Array(specCount + 1).join(' ') + '      ^^' + eyes() + '^^\n');
    print(Array(specCount + 1).join(' ') + '  _____ U\n');
    print(specTrail + '`(  _  /\n');
    print(Array(specCount + 1).join(' ') + '  // \\\\\n');
    print(Array(specCount + 1).join(' ') + ' ^^   ^^\n');
  }
}

Здесь specTrail и eyes следят за историей вопроса, а вся функция вместо того, чтобы как раньше, ставить точку или F, поднимается на пять строчек вверх и переписывает их полностью.

Бонус

Это не про лося, но тем не менее. Вообще, я считаю, что всегда лучше потратить немного времени, чтобы сделать всё удобно и интересно. Мелочи решают!

Эту вещь придумал не я. Скорее всего, до меня её придумали неоднократно, и я не могу найти первоисточник. Тем не менее, считаю, что это очень полезная вещь, и нужно ей поделиться. Речь идёт о том, чтобы показывать статус завершения предыдущей команды в строке запроса баша.

export PS1="\[\e]0;\u@\h: \W\a\`if [ \$? = 0 ];then echo \[\e[32m\]^_^\[\e[0m\];else echo \[\e[31m\]o_O\[\e[0m\];fi\`[\u@\h: \w][\$(rvm-prompt v)][\$(nvm_version)]\$(parse_git_branch)\$ "

Это моя строка запроса. В её начале мы видим или зелёный довольный смайлик, или красный удивлённый. Благодаря вот этой части:

\`if [ \$? = 0 ];then echo \[\e[32m\]^_^\[\e[0m\];else echo \[\e[31m\]o_O\[\e[0m\];fi\`

Когда сделал себе и привык, то теперь просто не понимаю, как другие без этого обходятся.

Материалы для самостоятельного изучения

  1. $RANDOM.
  2. elk_reporter.js.
  3. Управление курсором в баше.

17.02.2016 firtree_right Смена настроек /etc/hosts в одно касание

Зачем?

В какой-то момент я стал носить на работу ноутбук и отказался от двух компьютеров: домашнего и рабочего. В редких случаях приходится делать небольшие действия по работе, находясь дома. Для того, чтобы добраться до нужных машин внутри рабочей сети, я использую ssh-тоннель с пробрасыванием портов. Например, есть два сервиса: server1:8080 и server2:5000. Когда было два компьютера, то было всё просто. Рабочий компьютер находился внутри сети и видел оба сервера с их сервисами по правильным адресам, а домашний адресовал оба имени серверов на localhost, где сервисы оказывались на тех же портах после поднятия тоннеля. Но с ноутбуком нужно было как-то переключаться.

Для смены настроек я использовал самый простой, как мне кажется, способ: редактировал файл /etc/hosts. У меня было два набора строчек: для дома и для офиса. Один всегда закомментирован. Файл открывался с помощью sudo vi, и внутри можно использовать замену, используя номера строк, которые видны:

:2,7s/^#/
:9,13s/^/#/

Первая команда означает «со второй по седьмую строчку удалить „#“ в начале строки», а вторая — «с девятой по тринадцатую строчку поставить „#“ в начале строчки». Но когда делаешь одно и то же много раз, всегда хочется это автоматизировать.

Changesettings

Как?

Для начала мне хотелось избавиться от номеров строк (мало ли, какие добавятся или исчезнут строки). Поэтому я решил выделять зоны файла для офиса и дома комментариями «#officestart», «#officeend» и, соответственно, «#homestart» и «#homeend». Теперь интервал для замены можно было выделять через них:

:%s/#officestart\zs\_.\+\ze#officeend/smth_smth_smth/g
:%s/#homestart\zs\_.\+\ze#homeend/smth_smth_smth/g

В данных командах \zs и \ze означают начало и конец паттерна, который мы хотим заменить командой s в интервале %, то есть во всём файле. То есть мы меняем не всё, что нашли, а только часть. А сам паттерн — это _.+, что означает «один или более любых символов, включая конец строки». Буква g в конце означает, что может быть несколько таких блоков, что необязательно.

На что же мы будем заменять найденный паттерн между комментариями? Во-первых, нам совершенно точно понадобится замена внутри замены. А во-вторых, нам не поможет символ ^ для обозначения начала строчек, т.к. у найденного паттерна всего одно начало перед всеми строчками. Поэтому мы будем использовать знание структуры файла /etc/hosts: в случае IPv4 каждая незакомментированная рабочая строчка начинается с цифры, а закомментированная, как и положено, с «#». Для дома получаем команды:

:%s/#officestart\zs\_.\+\ze#officeend/\=substitute(submatch(0), '\n\(\d\)', '\n#\1', 'g')/g
:%s/#homestart\zs\_.\+\ze#homeend/\=substitute(submatch(0), '\n#\(\d\)', '\n\1', 'g')/g

Использование \= заставляет редактор выполнить выражение, то есть вызвать функцию substitute в таком виде. Тут, вроде бы, должно быть понятно, что мы передаём в функцию найденный паттерн, регулярное выражение с одной группой и на что его поменять в том паттерне.

От команд к скрипту

Осталось сделать из этого удобную штучку. Лично я оформил это следующим образом. В файле ~/.bash_profile:

alias imhome="sudo vim -u NONE -f -s $HOME/.vim/homehosts /etc/hosts"
alias imwork="sudo vim -u NONE -f -s $HOME/.vim/officehosts /etc/hosts"

Соответственно, файлы ~/.vim/homehosts:

:%s/#officestart\zs\_.\+\ze#officeend/\=substitute(submatch(0), '\n\(\d\)', '\n#\1', 'g')/g
:%s/#homestart\zs\_.\+\ze#homeend/\=substitute(submatch(0), '\n#\(\d\)', '\n\1', 'g')/g
:wq

~/.vim/officehosts

:%s/#officestart\zs\_.\+\ze#officeend/\=substitute(submatch(0), '\n#\(\d\)', '\n\1', 'g')/g
:%s/#homestart\zs\_.\+\ze#homeend/\=substitute(submatch(0), '\n\(\d\)', '\n#\1', 'g')/g
:wq

Таким образом, команды imhome и imwork спрашивают пароль и меняют настройки. Это иллюстрирует, почему был выбран редактор vim в качестве инструмента. Любые sed и awk будут потом требовать sudo tee для того, чтобы записать файл с нужными правами. А здесь мы запускаем всего одну команду.

Материалы для самостоятельного изучения

  1. Поиск и замена в vim.
  2. Использование выражений при поиске и замене в vim.

03.02.2016 firtree_right Вот это я называю «перерыв»!

Что было

Приветствую тебя, дорогой читатель! Каким-то образом ты остался подписан на этот блог или зашёл на него по неясной причине — это хорошо! В этом году я решил возобновить ведение этого самого блога. Посмотрим, что из этого выйдет.

Было странно копаться во внутренностях этого сайта снова. Все технологии устарели, версии не поддерживаются и тому подобное. Последний комментарий оставил мне читатель в 2014 году на статью 2009 года про тестирование оупенайди, авторизовавшись с помощью оупенайди, о том, что технология оупенайди мертва. Пришлось прикрутить на её место новую рекапчу от Гугла. Напишу как-нибудь об этом. Почувствовал себя, конечно, археологом.

Время, когда я начал вести этот блог, было временем увлечения руби он рэйлз. Было сделано много проектов. Потом влюблённость прошла, а с ней прошло о большое количество нового и интересного, о чём бы я мог здесь писать. Я стал немного залезать в другие области, но общей концепции так и не находилось. Даже прекрасные иллюстрации не спасли от наступившего затишья.

Wakeup

Что будет

Сейчас я не могу сказать, что являюсь увлечённым фанатом какого-либо языка, фреймворка или технологии, как не могу сказать, что являюсь профессионалом в оных. Что мне нравится делать — это изучать новые вещи и делать из них прикладные продукты. Прикладные они, конечно, не для всех, а в основном по линии моей работы. Но именно о простых, прикладных и — самое главное — интересных мне вещах из совершенно разных областей программирования я и буду писать. Думаю, получится делать по одной небольшой статье раз в две недели.

17.08.2014 firtree_right Жизнь в цифре

Форсквер окончательно разделился на два приложения, выкинув чекины в сворм. А так же отменил мои любимые бейджи. А ведь всё было ради них! Раньше мы в поездках им пользовались, потому что если нам нравится форсквер, то и места, которые нравятся его любителям, нам могут тоже понравиться. Так часто бывало. А теперь он будет подсказывать места, которые нравятся тем, кто пользуется свормом... Чувствую себя ужасным старпёром.

мои любимые бейджи

♯♯♯

Илья поделился совершенно прекрасной историей. Перескажу для обычных людей :) По ссылке — отчёт об ошибке. Чувак жалуется, что у него после обновлений не печатает принтер из ОпенОфиса. Другой подтверждает. Потом после другого обновления — печатает. Потом опять не печатает. Постит всякие логи, версии, короче, необходимые данные, чтобы те, кто разрабатывают ПО могли исправить. Никто не понимает, в чём дело, пока жена (!) этого чувака не жалуется ему на то, что принтер не печатает ПО ВТОРНИКАМ! Комментарий 28. И после этого он выясняет, что оказывается в середине процесса вывода на печать есть утилитка «file», которая определяет тип файла, и если она видит в определённом месте «Tue», то считает, что это программа на эрланге. Понимаете? Ни один программист не предположит, что проблема в дне недели! Услышав такое предположение — найти, подтвердить и исправить просто. Но вот предположить — только жена!

18.05.2011 firtree_right Использование руби программ в качестве фильтров для поиска

Предыстория

Не далее как прошлой осенью я писал о том, как изнутри процесса определить, запущен ли он, используя инструмент grep. В комментариях мне посоветовали использовать pidof, но мне не удалось заставить его работать для руби, т.к. поиск происходит по имени запускаемого файла, а в случае руби-скрипта это всегда ruby. Но мне существенно удалось сократить получение списка запущенных процессов с таким же именем. Вместо:

`ps ax | grep #{File.basename(__FILE__)} | grep -v grep`.split("\n").map{ |l| l.strip.split(/\s+/)[0].to_i }.reject{ |pid| pid == Process.pid }

получилось

`pgrep -f #{File.basename(__FILE__)}`.chomp.split(/\s+/).reject{ |pid| pid.to_i == Process.pid }

Довольно часто мне необходимо отфильтровать вывод или содержимое файла хитрее, чем просто поиск по регулярному выражению. Поскольку мне очень нравится руби, и, как неоднократно писалось в этом блоге, я пытаюсь использовать его везде, где можно, то почему бы снова так не поступить?

ascannerdarkly

Командная строка руби

Руби имеет умеренное количество ключей командной строки. Кратко они описаны в выводе:

ruby --help

Нас в большей степени интересуют ключи -n и -p, которые создают цикл вокруг чтения из пайпа. Ссылка на подробности — в конце статьи.

Например, мы хотим посчитать, сколько всего виртуальной памяти занимают все процессы браузера гугл-хром. В качестве источника информации будем использовать вывод команды:

ps axo "%p %z %c"

В которой собраны только необходимые данные (занимаемая виртуальная память и имя процесса без аргументов) и пид (ну а вдруг?). А теперь этот вывод отправим не грепу, а нашему родному руби:

ps axo "%p %z %c" | ruby -nae 'num ||= 0; num += $F[1].to_i if $F[2] =~ /chrome/; END{puts "total chrome virtual memory size #{num} Kbytes"}'

Что это означает? Ключ n означает, что вокруг нашего скрипта есть цикл вида:

while gets(); ... end

Ключ a означает, что вместо переменной $_, куда автоматически попадает результат gets, мы можем использовать $F, который есть суть $_.split. А END содержит блок, который выполняется после цикла.

Ту же магию можно использовать и внутри запускаемых руби-скриптов. Например, если мы хотим найти какое-то слово внутри файла, выделить его цветом и вывести строку с номером, где это слово нашлось, то наш скрипт будет выглядеть вот так (файл look_for):

#!/usr/bin/ruby -n

BEGIN {
  unless ARGV.size == 2
    puts "Usage: ./look_for <word> <path/to/file>"
    exit
  end
  str = ARGV.shift
}

next unless $_ =~ /#{str}/

printf "%6s%s", $., $_.gsub($&, "\e[31m#{$&}\e[0m")

Теперь, если сделать этот файл запускаемым и запустить его:

./look_for word /in/some/file

То можно увидеть неземную красоту. Кстати, обратите внимание на shift. Без него программа не работает, т.к. gets, который тут за кадром правит бал, пытается воспринимать все аргументы как пути к файлам, из которых непременно нужно что-нибудь прочитать.

Прочие прекрасные применения параметров командной строки руби я предлагаю пытливому читателю подсмотреть в ссылках ниже или найти самостоятельно.

Материалы для самостоятельного изучения

  1. Полный код статьи на гитхабе.
  2. Справочник по параметрам командной строки.
  3. То же, что и выше, но подробнее
  4. Множество прекрасных примеров (со ссылкой на источник).

23.03.2011 firtree_right Рекурсия в регулярных выражениях

Пролог

Что-то большие перерывы в написании статей входят в привычку. Способность некоторых коллег по цеху регулярно выдавать что-нибудь полезное и интересное вызывает уважение.

worm

Введение

С тех самых пор, как я только узнал про регулярные выражения, я слышал об их несовершенстве и моральном устаревании. Регулярные выражения продолжали использоваться, а недовольные теоретики — сетовать. Основной претензией было то, что регулярные выражения не позволяют исследовать вложенности паттернов в виду своей линейности. Действительно, соглашался я, невозможно проверить правильность открытия и закрытия тегов или получить выражение в самых внутренних скобках.

Однако, как оказалось, человечество шагнуло далеко вперёд в вопросе совершенствования регулярных выражений. Об одном из новшеств хочу сегодня рассказать.

mole_worm

Именованные группы

В регулярных выражениях руби 1.9 появились именованные группы. Вот, как выглядит их элементарное использование:

if /\A(?<first>[a-zA-Z]+)\s+(?<last>[a-zA-Z]+)\Z/ =~ "Vassily Poopkine"
  puts [first, last].inspect
end

if md = /\A(?<first>[a-zA-Z]+)\s+(?<last>[a-zA-Z]+)\Z/.match("Vassily Poopkine")
  puts [md[:first], md[:last]].inspect
end

То есть мы не только выделяем группу скобками, как обычно, назначая ей тем самым порядковый номер (по номеру открывающей скобки), но и даём имя. И использовать его можно не только в локальных переменных и объекте MatchData, но и в самом регулярном выражении.

Более того, обращение к объявленным группам внутри может быть рекурсивным. Мне сразу же захотелось написать давнишнюю мою задумку о функции, раскрывающей вложенные скобки. Вот так:

str = "1 + 2 * (3 - 4 / {5 + 6} + [7 - 8 * (9 + 10 * 11) + 12 * {13 - 14}] + 15) + 16 * (17 + 18)"

re = %r{
        (?<fill>[0-9+\-*/\s]+){0}
        (?<expression>\g<fill>*\g<brackets>\g<fill>*|\g<fill>){0}
        (?<braces>\{\g<expression>+\}){0}
        (?<squarebrackets>\[\g<expression>+\]){0}
        (?<parentheses>\(\g<expression>+\)){0}
        (?<brackets>\g<braces>|\g<squarebrackets>|\g<parentheses>)
}x

def calculator(str)
  if str =~ /\A[0-9+\-*\/\s]+\Z/
    eval str
  else
    raise "Invalid expression: #{str}"
  end
end

f =-> s do
  if $~[:expression] == $~[:fill]
    calculator($~[:fill])
  else
    calculator($~[:brackets][1..-2].gsub(re, &f))
  end
end

puts calculator(str.gsub(re, &f))
puts eval(str.gsub(/(?<left>\{|\[)|\}|\]/) { |s| $~[:left] ? "(" : ")" })

Итак, в регулярном выражении присутствует 6 именованных групп: fill (заполнения пространства между скобками), expression (выражение, содержащее одни или ни одних нераскрытых скобок), braces (фигурные скобки), squarebrackets (квадратные скобки), parentheses (круглые скобки), brackets (любые скобки). Как видите, выражение описывается через скобки, а скобки — через выражение.

Для проверки правильности расчёта, используем обычный eval, заменив все скобки на круглые.

mole

Сделав этот пример, я был доволен, как стадо слонов, но потом решил проверить, а что будет, если скобки расставлены неправильно?

str = "1 + 2 * (3 - 4 / {5 + 6} + [7 - 8 * (9 + 10 * 11) + 12 * {13 - 14]} + 15) + 16 * (17 + 18)"

re = %r{
        (?<fill>[0-9+\-*/\s]+){0}
        (?<expression>\g<fill>*\g<brackets>\g<fill>*|\g<fill>){0}
        (?<braces>\{\g<expression>+\}){0}
        (?<squarebrackets>\[\g<expression>+\]){0}
        (?<parentheses>\(\g<expression>+\)){0}
        (?<brackets>\g<braces>|\g<squarebrackets>|\g<parentheses>)
}x

str =~ re

И я не смог дождаться завершения работы оператора =~ для такого длинного выражения. Это, конечно, неприятно. В причины я вникал не особо, но похоже, это связано с поведением недетерминированной машины Тьюринга. По крайней мере вот ответ на похожую проблему. Для нас это всего лишь означает, что проверять правильность расстановки скобок нужно отдельно и другим способом. Чем я предлагаю заняться пытливому читателю самостоятельно.

Материалы для самостоятельного изучения

  1. Исходный код статьи.
  2. Новый синтаксис и прочие вкусняшки в руби 1.9. Для тех, кто заметил =->.
  3. Глобальные переменные с непонятными именами. Для тех, кто заметил $~.
  4. Ещё немного базовых приёмов в регулярных выражениях руби.

07.11.2010 firtree_right Отдых

В выходные был какой-то временной сдвиг. Я был уверен, что отдыхаю до вторника, а оказалось что 8-е — это понедельник.

В качестве отдыха приобрёл и прошёл сюжетные DLC (загружаемый контент) к Mass Effect 2. Получил удовольствие, конечно. Даже на минуточку захотелось создать нового героя и пройти первую и вторую часть от начала до конца. Задумался, почему им нужно делать прогрессирующую шкалу плохиша или кибальчиша. Сюжетно мне нравятся выборы положительного героя: больше персонажей остаётся в живых, больше потом приколов. Но многие диалоги у отрицательного героя лучше подходят. А иному персонажу с ноги заехать — самая адекватная реакция. Но если выбирать то так, то так, то потом недоступны ни те ни другие опции.

Ходил на день рождения к другу. Он сказал мне, что снова бросил учиться в семинарии. На вопрос: «Почему?» — показал вот это. Сказал: «Тот, Кто создал такое, не может быть в Своём уме». Я его понимаю.

Написал статью про ротацию логов в приложениях на rails. Ира к ней нарисовала картинку. Надеюсь на продолжение плодотворного сотрудничества. Хотя обычно я дольше ищу и исследую сюжет для статьи, чем Ира рисует картинку.

А как вы провели выходные?

05.11.2010 firtree_right Ротация логов рельсового приложения

hay roller

Введение

Это уже давно известная тема, и я не претендую на открытие Америки, но для себя зафиксирую это знание.

Даже если вы используете капистрано для выкладывания проекта в сеть, логи приложения хранятся в одном и том же месте (папка shared/log и разрастаются до огромных размеров. Можно, конечно, запускать после каждого обновления файлов проекта комманду:

rake log:clear

Но есть более цивилизованные методы. Тем более, после определённого времени код проекта начинает обновляться всё реже и реже.

С помощью системы

Существует прекрасный системный инструмент, который назвается logrotate. С его помощью архивируются логи апача, баз данных и даже менеджера пакетов.

Чтобы организовать это удовольствие для своего проекта нужно создать файл /etc/logrotate.d/my_project:

/path/to/my_project/shared/log/*.log {
  weekly
  missingok
  rotate 10
  nomail
  compress
  delaycompress
  sharedscripts
  postrotate
    touch /path/to/my_project/current/tmp/restart.txt
  endscript
}

Здесь написано:

  1. weekly — разбивать лог еженедельно;
  2. missingok — не выходить с ошибкой, если файла нет;
  3. rotate 10 — хранить 10 предыдущих томов;
  4. nomail — не высылать удаляемые тома на электронную почту;
  5. compress — архивировать;
  6. delaycompress — архивировать не сразу, т.к. после переименования файла и до перезапуска пэссенджера логи пишутся в тот же переименованный файл;
  7. sharedscripts — запускать скрипт один раз для всех логов по маске;
  8. postrotate...endscript — скрипт, который нужно запустить после ротации: в данном случае перезапустить пэссенджер.

Файлом должен владеть root:root. Теперь можно проверить и запустить принудительно, убедившись, что наш файл включается в общий список:

sudo logrotate -dv /etc/logrotate.conf
sudo logrotate -fv /etc/logrotate.conf

С помощью руби

В руби есть встроенный метод ротации логов. Достаточно в файе config/environment.rb написать внутри блока Rails::Initializer.run один из вариантов:

config.logger = Logger.new(config.log_path, "weekly")

или

config.logger = Logger.new(config.log_path, 10, 1.megabyte)

Первый вариант осуществляет ротацию раз в неделю, а второй — по достижении файлом размера в 1 мегабайт и хранит 10 старых томов. Только в данном случае архивацию, если она нужна, придётся организовывать отдельно.

Было бы интересно

Для логротейт можно написать такую маску, которая бы включала в себя все логи всех рельсовых проектов. Но мне неизвестен способ потом написать такой скрипт, который бы перезапускал именно те проекты, для которых была сделана ротация. Например, если логротэйт не нашёл нужного файла, то и скрипт не запустит. А если мы указываем путь типа /path/to/*/shared/.log, то и скрипт должен перебирать все эти проекты и создавать или просто менять дату редактирования файлов restart.txt. Или можно просто перезапускать апач.

Материалы для самостоятельного изучения

  1. Документация logrotate (по-русски)
  2. Что ещё можно делать с логами приложения на рельсах

20.10.2010 firtree_right Определение, запущен ли процесс

Пролог

Ого! Уже три месяца я ничего не писал в этот блог! Лето выдалось жаркое не только на погоду. Поскольку летом погода лучше, а световой день длиннее, было много работы. Причём работы связанной с поддержкой того, что уже и так нормально функционировало в прошлом сезоне. Ничего серьёзно нового не писалось активно, а значит и захватывающих сюжетов для статей не находилось.

Но теперь у меня появилась возможность писать кое-что новое. Поэтому есть, что рассказать.

to feed or not to feed

Введение

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

Баш в помощь

Предположим, что у нас есть простейший демон. Хорошо бы имя у него было уникальное, чтобы можно его потом было отыскать. Файл uniq_name_simple_daemon:

#!/usr/bin/env ruby

pid = fork do
  begin
    running = true
    Signal.trap("TERM") do
      running = false
    end
    while running
      sleep 0.01
    end
  rescue Exception => e
    puts e.to_s
    puts e.backtrace.join "\n"
  ensure
    exit!
  end
end

Мы всегда можем запускать с помощью другого скрипта, например на баше (simple_daemon_runner.sh):

#!/bin/bash

if ps ax | grep uniq_name_simple_daemon | grep -vq grep
then
  echo "uniq_name_simple_daemon is already running"
else
  echo "starting uniq_name_simple_daemon"
  ./uniq_name_simple_daemon
fi

На подобной команде будут базироваться все наши последующие методы. Тут, если кто не понял, мы фильтруем вывод ps ax сначала ища там имя нашего скрипта, а затем исключая из списка сам процесс поиска (команду grep). Ключ q позволяет нам получить код выхода, не выводя ничего на экран. То есть если строчка найдена, то запускаем первый блок, если нет, то второй.

Можно сделать такой же скрипт для остановки процесса (simple_daemon_stopper.sh):

#!/bin/bash

pid=$(ps ax | grep uniq_name_simple_daemon | grep -v grep | awk '{ print $1; }')

if [[ -n $pid ]]
then
  echo "stopping uniq_name_simple_daemon"
  kill -TERM $pid
else
  echo "nothing to stop"
fi

Конечно же, при таком раскладе всегда есть возможность запустить нашего демона без помощи скриптов. И тогда проверка делаться не будет. В таком случае полезно проверять, запущен ли процесс уже внутри самого руби, перед тем, как отпочковать дочерний процесс.

Сам себе хозяин

В данном случае задача сводится к проверке наличия в памяти ещё одного процесса с таким же именем кроме текущего. Так же нужно уметь останавливать процесс с помощью того же файла. Вот, какое решение получилось у меня (uniq_name_auto_daemon):

#!/usr/bin/env ruby

ps_ax = `ps ax | grep #{File.basename(__FILE__)} | grep -v grep`.split("\n").map{ |l| l.strip.split(/\s+/) }.reject{ |l| l[0].to_i == Process.pid }

if ps_ax.any?
  case ARGV[0]
    when /stop/i
      ps_ax.each do |l|
        system "kill -TERM #{l[0]}"
      end
    when /kill/i
      ps_ax.each do |l|
        system "kill -KILL #{l[0]}"
      end
    else
      puts "#{File.basename(__FILE__)} is already running. If you want to stop it, run './#{File.basename(__FILE__)} stop|kill'"
  end
else
  pid = fork do
    begin
      running = true
      Signal.trap("TERM") do
        running = false
      end
      while running
        sleep 0.01
      end
    rescue Exception => e
      puts e.to_s
      puts e.backtrace.join "\n"
    ensure
      exit!
    end
  end
end

Во-первых, обходимся одним файлом, который никак иначе не запустить. Во-вторых, нигде не нужно хардкодить его имя. По-моему, очень удобно.

Оффтопик

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

Внутри примеров кода — наоборот, удобнее писать комментарии и тексты по-английски, чтобы не переключать раскладку, но как-то это не очень соответствует русскоязычном блогу.

Что же делать? :)

Материалы для самостоятельного изучения

Полный код статьи на гитхабе.