16 марта 2016, 23:25

Как я встраивал reCAPTCHA v2 в свой антикварный бложек

Темы: javascript, ruby, rails, ruby1.9, future, elk, antique

Предпосылки

Мы живём в интересное время. Когда я увлекался довольно серьёзно руби он рэйлз, лет шесть-семь назад, была версия руби 1.8.7 и версия рельсов 2.3. Все неспешно переходили на руби 1.9.1 и рэйлз 3.0. Сегодня, пять лет спустя, стабильная версия руби — 2.2.3, а про рельсы уже агитируют переходить на 5.0, хоть и бета. Когда же я начал свой первый проект на ноде — три года назад, — версия node.js была что-то типа 0.22. А сегодня уже 5.8!

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

Антиквариат

До последнего перерыва, рассказ о начинке моего блога мог бы легко заполнить парочку в меру интересных статей, а сейчас это представляет интерес только для археологов. Что касается движков для блога вообще, то про когда-то популярный ЖЖ все уже забыли. Стремительно набирает моду вести канал в Телеграме. А товарищ мой — Илья — в когда-то давно в качестве платформы для блога сделал самый правильный, как мне сегодня кажется, выбор — генератор статического сайта.

Что ещё? Технология оупенайди, на которой у меня были прикручены комментарии, умерла. Рекапча, которую я прикручивал в комментарии Ире, была куплена Гуглом и ещё пока жива, но уже выпустили вторую версию, и я боюсь, как бы они таки не закрыли первую, как Гугл это умеет делать. Но даже тогда джем, который я использовал, чтобы встроить капчу, волшебным образом исчез из библиотек, и мне пришлось таскать его с собой в папочке vendor. Про вёрстку я даже не говорю.

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

DIY

reCAPTCHA v2

Когда я решил использовать новую капчу, я даже не стал искать библиотек. С вероятностью 80% они не заработают на моей старинной системе, и с вероятностью 30% перестанут поддерживаться очень скоро. Просто читаем документацию, встраиваем капчу в форму отправки комментария, а на сервере прямо в методе создания комментария пишем, например:

url = URI.parse('https://www.google.com/recaptcha/api/siteverify')
req = Net::HTTP::Post.new(url.path)
req.set_form_data 'secret' => 'SECRET_KEY',
                  'response' => params['g-recaptcha-response'],
                  'remoteip' => request.remote_ip
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
res = JSON.parse(https.start { |p| p.request req }.body)
if res['success'] ...

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

Да, это противоречит паттерну MVC, да, это противоречит ООП. Но посмотрите на саму капчу: она не учитывает положения формы на странице, когда открывает своё окно. А также не работает задокументированная фича data-tabindex. Полно хороших библиотек и поделок увядают так и не исправив своих ошибок. Такова реальность программиста сегодня. Красивая библиотека для встраивания в проект на рельсах не сделает эту капчу лучше, но исправлять описанные выше ошибки можно тоже обезьяньими заплатками.

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

Комментарии 0 >>

18 мая 2011, 22:54

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

Темы: ruby, ruby1.9, regexp, syntax, bash, mistakes

Предыстория

Не далее как прошлой осенью я писал о том, как изнутри процесса определить, запущен ли он, используя инструмент 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. Множество прекрасных примеров (со ссылкой на источник).

Комментарии 0 >>

23 марта 2011, 18:58

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

Темы: ruby, ruby1.9, regexp, syntax

Пролог

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

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. Ещё немного базовых приёмов в регулярных выражениях руби.

Комментарии 4 >>

05 ноября 2010, 10:18

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

Темы: ruby, rails, syntax, deploy, automation

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. Что ещё можно делать с логами приложения на рельсах

Комментарии 2 >>

20 октября 2010, 16:20

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

Темы: daemon, ruby, bash, syntax

Пролог

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

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

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

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

Оффтопик

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

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

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

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

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

Комментарии 10 >>

01 июня 2010, 23:40

Работа над ошибками

Темы: ruby, git, automation, air, mistakes, syntax

Mistakes

Введение

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

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

git hooks

Недостатков скрипта для удаления пробелов в концах строк нашёл два:

  1. Скрипт без нужды дёргает ни в чём не повинные файлы, потому что \s соответствует и символу конца строки, который там всегда есть.
  2. Скрипт не содержит решения для выбора всех текстовых файлов проекта.

Вот хороший скрипт:

#!/usr/bin/env ruby
`git grep -I --name-only -e ""`.split("\n").each do |p|
  lines = File.readlines(p).map(&:chomp)
  if lines.inject(false) { |memo, l| l.gsub!(/\s+$/, "") || memo }
    File.open(p, "w") do |f|
      f.puts lines.join("\n")
    end
    puts "Removed trailing spaced from '#{p}'"
    system "git add #{p}"
  end
end

Так же по совету Дмитрия в комментариях добавил скрипт для проверки счастливого коммита.

Работа с версией в (ai)rake

Совершенно очевидная ошибка в примере про работу с версиями air-приложения в rake. Когда увеличивается более старшая часть версии, то все младшие должны обнуляться:

namespace :version do

  [:major, :minor, :patch].each_with_index do |subv, index|
    desc "Bump #{subv} in version"
    task :"bump_#{subv}" do

      unless `git status` =~ /nothing to commit/
        raise "There are uncommitted changes. Failed to proceed."
      end

      appxml = YAML.load_file('airake.yml')["appxml_path"]
      str = File.read(appxml)

      msg = nil
      new_version = nil

      if str.gsub! /<version>(.*)<\/version>/ do |matched|
          old_version = $1
          major, minor, patch = old_version.split(".").map(&:to_i)
          eval("#{subv} += 1")
          new_version = [major, minor, patch].fill(0, index+1).join(".")
          msg = "Version bump #{old_version} => #{new_version}"
          puts msg
          "<version>#{new_version}</version>"
        end.nil?
        raise "Cannot detect current version.\nMake sure appxml file contains <version>X.X.X</version> tag."
      else
        File.open(appxml, "w") do |f|
          f.write str
        end

        puts `git commit -am "#{msg}"`
        puts `git tag v#{new_version}`
      end
    end
  end
end

Теперь rake version:bump_minor делает из 0.1.6 не 0.2.6, а 0.2.0, как и должно быть.

Мимоходом

Тем временем я сменил тарифный план у своего провайдера на (ve). И незаметно перенёс сайт. Посмотрим, как работает на собственном опыте. Работа по ssh, как была, так и осталась основным способом администрирования, а необходимость лазить в plesk пропала, потому что его теперь нет :)

Комментарии 0 >>

26 мая 2010, 21:42

Использование git hooks на руби в корыстных целях

Темы: ruby, automation, git

Organizers

Введение

Как-то раз мне попался очень злой git-репозиторий, который отказывался работать, если я оставлял пробелы в конце строк. Есть такая версия, что git заточен под отправку патчей по почте, и что пробелы в концах строк могут навредить в таком процессе.

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

git hooks

Для множества различных целей у git есть хуки. (Как бы их перевести нормально?) Они находятся в каждом репозитории в папке:

.git/hooks

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

Использование pre-commit для удаления пробелов на концах строк

Поскольку я, опять же, фанат руби, то и скрипты — благо есть такая возможность — напишу на руби. Создаём файлик .git/hooks/pre-commit:

#!/usr/bin/env ruby

Dir.glob("*.{txt,rb}").each do |p|
  lines = File.readlines(p)
  if lines.inject(false) { |memo, line| line.gsub!(/\s+$/, "") || memo }
    File.open(p, "w") do |f|
      f.puts lines.join("\n")
    end
    system "git add #{p}"
  end
end

Как видно, этот скрипт ищет файлы *.txt и *.rb в корневом каталоге репозитория, и если в них есть пробелы в конце строк, перезаписывает их и добавляет в индекс для коммита.

Не забыть сделать его запускаемым:

chmod +x .git/hooks/pre-commit

Теперь у нас в распоряжении автоматический помощник-редактор, который удаляет пробелы в конце строк.

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

Документация по git hooks

Комментарии 6 >>

28 апреля 2010, 15:21

Использование Airake под Kubuntu

Темы: tdd, actionscript, ruby, flex, automation, air

simple things

Введение

Уже некоторое время назад обнаружил гениальный инструмент. Правда только недавно опробовал его на своих рабочих проектах и зафанател ещё больше. Подготовка к докладу на секции Яндекса про панорамы на РИФе не позволила мне поделиться этим ранее. Исправляю ошибку.

Я люблю руби. И, естественно, rake, как инструмент, продолжающий славные традиции make в руби и с помощью руби. Так же я питаю нежные чувства к ActionScript. Мне нравится AIR, который позволяет писать действительно кросс-платформенные приложения довольно быстро. Так же я неплохо отношусь к TDD, как к одному из способов разработки.

Какова же была моя радость найти инструмент, который всё это объединяет! Хотя ему уже пара лет, он по-прежнему прекрасен.

Установка составляющих

Предполагаю, что ruby, rubygems и rake уже установлены у тех, кто читает этот блог.

Далее, качаем и разархивируем куда-нибудь Adobe AIR SDK и Adobe Flex SDK (или предыдущая версия, если вы консерватор), а так же устанавливаем Adobe AIR Runtime. Чтобы установить последний, после загрузки bin-файла нужно:

chmod +x AdobeAIRInstaller.bin
sudo ./AdobeAIRInstaller.bin

Теперь добавим в PATH пути к исполняемым файлам загруженных SDK. В .bashrc добавляем:

export PATH="/path/to/air_sdk/bin:$PATH"
export PATH="/path/to/flex_sdk_4/bin:$PATH"

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

sudo apt-get install sun-java6-jre

После установки, независимо от того, используете вы Flex3 или Flex4, нужно переписать содержимое AIR SDK поверх Flex SDK. Мне не совсем понятен сакральный смысл этих действий, но иначе ничего не работает.

Привѣтъ, Мiръ!

Создание пустого air-приложения теперь просто:

airake airake_hello_world

Чтобы запустить его, однако, следует исправить в src/AirakeHelloWorld-app.xml и test/Test-app.xml:

...
xmlns="http://ns.adobe.com/air/application/1.5"
...

Если вы решили использовать Flex4, то вам необходимо отредактировать сгенерированное приложение, чтобы запустить его. Это связано с изменениями в стилях. Поэтому проще просто удалить всё содержимое тэга WindowedApplication в файле src/AirakeHelloWorld.mxml.

Про использование TDD в ActionScript я уже писал, поэтому подробно останавливаться не буду. Для примера в код на github включён тривиальный тест. Запуск тестирования происходит привычным образом:

rake test

Документация, если вы пишете правильные комментарии ASDoc, тоже запускается привычным образом:

rake docs

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

rake -T

Использование rake

Конечно, вся прелесть rake не только в привычных и коротких командах для разработки, но и в том, что можно создавать свои сценарии. Например, вот как могла бы выглядеть работа с версиями приложения. Добавим в файл raketasks/version.rake следующий код:

require 'yaml'

desc "Print out current version"
task :version do
  if md = File.read(YAML.load_file('airake.yml')["appxml_path"]).match(/<version>(.*)<\/version>/)
    puts "Current version is #{md[1]}"
  else
    raise "Cannot detect current version.\nMake sure appxml file contains <version>X.X.X</version> tag."
  end
end

namespace :version do

  [:major, :minor, :patch].each do |subv|
    desc "Bump #{subv} in version"
    task :"bump_#{subv}" do

      unless `git status` =~ /nothing to commit/
        raise "There are uncommitted changes. Failed to proceed."
      end 

      appxml = YAML.load_file('airake.yml')["appxml_path"]
      str = File.read(appxml)

      msg = nil
      new_version = nil

      if str.gsub! /<version>(.*)<\/version>/ do |matched|
        old_version = $1
        major, minor, patch = old_version.split(".").map(&:to_i)
        eval("#{subv} += 1")
        new_version = [major, minor, patch].join(".")
        msg = "Version bump #{old_version} => #{new_version}"
        puts msg
        "<version>#{new_version}<\/version>"
      end.nil?
        raise "Cannot detect current version.\nMake sure appxml file contains <version>X.X.X</version> tag."
      else
        File.open(appxml, "w") do |f|
          f.write str
        end

        puts `git commit -am "#{msg}"`
        puts `git tag v#{new_version}`
      end
    end
  end
end

А в Rakefile соответственно:

# Custom rake tasks
Dir.glob("raketasks/*.rake").each { |rf| load rf }

Теперь мы можем привычным образом работать с версиями приложения (а версии эти потом будут распознаваться установщиком обновлений):

rake version
rake version:bump_major
rake version:bump_minor
rake version:bump_patch

И это не предел!

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

  1. Полный код статьи на github
  2. Инструкция по работе с airake, которая во многом повторена в этой статье с добавлением манипуляций, чтобы всё заработало.
  3. Документация по FlexUnit. Не уверен, что в поставке airake идёт самая последняя версия, но ничего не мешает написать rake task для обновления версии FlexUnit :)
  4. Документация по rake

Комментарии 0 >>

07 апреля 2010, 12:59

Немного о $SAFE

Темы: ruby, ruby1.9, syntax, security

secure code

Введение

Совершенно не по работе заинтересовался переменной $SAFE и её ролью в жизни современного разработчика. Оказалось, что всё нужно проверять самому.

Нежная безопасность

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

print "child: "
child = gets.chomp
puts "child tainted: #{child.tainted?}"
(0..4).to_a.each do |i|
  puts "SAFE: #{i}"
  $a = "safe"
  th = Thread.new do
    $SAFE = i
    child_copy = child.dup
    Thread.current[:out] = ""

    begin
      load child_copy
      Thread.current[:out] += "1. Child loaded\n"
    rescue SecurityError => e
      Thread.current[:out] += "1. Security error: #{e.to_s}\n"
      begin
        child_copy.untaint
        load child_copy
        Thread.current[:out] += "2. Child untainted and loaded\n"
      rescue SecurityError => e
        Thread.current[:out] += "2. Security error: #{e.to_s}\n"
        begin
         Thread.current[:out] += "3. Read from file '#{child_copy}': '#{File.read(child_copy)}'\n"
        rescue SecurityError => e
          Thread.current[:out] += "3. Security error: #{e.to_s}\n"
          begin
           Thread.current[:out] += "4. Read from untainted file: '#{File.read("child.rb")}'\n"
          rescue SecurityError => e
            Thread.current[:out] += "4. Security error: #{e.to_s}\n"
          end
        end
      end
    end

    begin
      $a = "modified"
      Thread.current[:out] +=  "5. Global variable modified: $a = '#{$a}'\n"
    rescue SecurityError => e
      Thread.current[:out] += "5. Security error: #{e.to_s}\n"
    end

    begin
      Dir.mkdir "test"
      Thread.current[:out] += "6. Created directory 'test': #{File.exist?("test")}\n"
      Dir.rmdir "test"
    rescue SecurityError => e
      Thread.current[:out] += "6. Security error: #{e.to_s}\n"
    end

    begin
      Thread.current[:out] +=  "7. Dir glob: #{Dir.glob(File.join("..", "*")).inspect}\n"
    rescue SecurityError => e
      Thread.current[:out] += "7. Security error: #{e.to_s}\n"
    end

    begin
      Thread.current[:out] +=  "8. System ls output: '#{`ls`.chomp}'"
    rescue SecurityError => e
      Thread.current[:out] += "8. Security error: #{e.to_s}\n"
    end
  end
  th.join
  puts "Global variable: $a = '#{$a}'"
  puts th[:out] if th[:out]
end

Конструкция со Thread.current[:out] используется потому, что для $SAFE >= 4 нельзя ничего писать ни в какие устройства вывода.

Вроде бы всё логично. Первый уровень годится для умеренного карантина внешних данных. При желании их можно и расколдовать. Второй уровень запрещает изменения в файловой системе. Третий уровень похож на осаду с постоянным подозрением на шпионаж. Все созданные объекты считаются небезопасными. А четвёртый уровень — это самое близкое к песочнице (sandbox) в руби, что что есть.

Кстати, когда ещё github работал как репозиторий библиотек, спецификация gemspec выполнялась там под $SAFE = 3. Для разработчиков это выливалось в то, что нужно было перечислять все файлы своей библиотеки вручную вместо использования какого-нибудь листинга.

Суровый гайдлайн

Конечно же, только использование $SAFE не убережёт от действительно настойчивой атаки или блокирующего кода. Например:

Thread.new do
  $SAFE = 2
  class String
    def ==(other_string)
      true
    end
  end
end.join
puts "string modified: #{'a' == 'b'}"

И это на втором уровне! А на третьем открыть класс тоже можно, но вызов перегруженного оператора будет вызывать SecurityError.

На сегодняшний момент эту концепцию безопасности можно считать сырой. Актуальное поведение руби 1.8 слегка отклоняется от описаний, что я нашёл. Поведение в 1.9 изменилось, но подробно нигде не описано (я не нашёл).

Это не значит, что этой переменной нет применения в жизни прогрессивного человечества. Адекватное текущему состоянию применение — это гайдлайн при разработке. Руководство для программистов, которое само следит за своим исполнением. Жестковато, но зато действенно. :)

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

  1. Код примеров в статье на github
  2. Старая, но самая подробная документация по $SAFE
  3. Просто дополнительно: шпаргалка по руби

Комментарии 0 >>

15 декабря 2009, 13:46

Одновременное использование двух версий руби на одной системе

Темы: ruby, ruby1.9, bash

Введение

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

Поэтому необходимо найти удобный для себя способ (а лучше несколько) чтобы начать использовать руби 1.9.

Постановка задачи

Сейчас практически панацеей для использования более одной версии руби является rvm. Очень удобно в использовании, полностью прозрачно, и позволяет иметь разные версии руби в разных окнах терминала.

Но недавно мне понадобилось скомпилировать wxRuby под свою систему (kubuntu 9.10 amd64), и rvm не справилась с этой задачей. По какой-то причине в момент компилляции были недоступны заголовки руби. Поэтому я решил поставить две версии руби более явно: одна системная (1.8.7) и одна в папке /opt (1.9.1). Причем все команды, связанные с руби 1.9 будут вызываться с суффиксом: ruby1.9, irb1.9, gem1.9, rake1.9.

Возможно, подобных инструкций уже полно, но мне будет удобнее, если я точно буду знать, где находится одна из них :) При всём этом, конечно, rvm продолжает работать. Мы никак ему не помешаем.

Решение

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

sudo apt-get install libreadline5-dev

Теперь хорошо бы вписать пути в наше окружение. В конце ~/.bashrc добавим:

export PATH=$PATH:/opt/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/lib

Теперь следует скачать и разархивировать последнюю версию руби. Зайдя в папку скомпилировать и установить:

./configure --prefix=/opt --enable-shared --program-suffix=1.9
make
sudo make install

Теперь у нас есть две отдельных установки руби и сопутствующих инструментов. Единственное, что обе установки используют общие конфигурационные файлы: ~/.gemrc, ~/.irbrc и т.п., что вполне удобно. Также для обеих систем общей директорией джемов будет ~/.gem, куда будут устанавливаться библиотеки, запусти мы их установку без sudo (в случае с sudo, конечно же, директории установки различаются).

Так же я не нашёл быстрого способа добавить /opt/bin в переменную PATH для sudo. Поэтому в таких случаях пока использую полный путь. Например, первая команда, которую следует выполнить:

sudo /opt/bin/gem1.9 update --system

Потому что в пакете с руби идёт версия 1.3.1, а настоящие пацаны уже во всю используют 1.3.5.

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

  1. Всё об установке нескольких версий руби на одной системе
  2. Проект «Используй руби 1.9 или вали!»

Комментарии 8 >>

09 декабря 2009, 16:43

Забота о неблокировании потоков в руби

Темы: ruby, ruby1.9, syntax, extension

Введение

Сегодня будет блиц-молния, которой место, скорее в твиттере. Однако, мне необходимо развеять тучи, которые я сам же и нагнал. :)

Постановка задачи

Не так давно я писал о том, что при создании руби-оболочки вокруг библиотеки на си, легко получить код, который блокирует все потоки приложения. Можно ли с этим что-то сделать?

Решение

Если мы имеем чуть больше контроля над участками кода на си, которые выполняются долго (обычно в цикле), то можно добавить спасительный код в тело цикла:

if (!rb_thread_alone()) rb_thread_schedule();

Это позволит каждый раз при очередном витке передавать управление соседним потокам, если они есть, и сделает приложение более отзывчивым.

То есть, если мы сделаем точную копию функции из той статьи и добавим наш спасительный код внутрь for, то в выводе такой программы:

# coding: utf-8
require "block_thread.so"

t1 = Thread.new do
  10.times { |i| puts i; sleep 0.1 }
end

t2 = Thread.new do
  puts "Блокируем"
  BlockThread.cycle
  puts "Разблокируем"
end

t3 = Thread.new do
  puts "Стараемся не блокировать"
  BlockThread.cycle_with_schedule
  puts "Закончили стараться"
end
t1.join
t2.join
t3.join

Будет картина гораздо приятнее:

Блокируем
0
Стараемся не блокировать
Разблокируем
1
2
3
4
5
Закончили стараться
6
7
8
9

Заметьте, кстати, что два лишних вывода между «Блокируем» и «Разблокируем» благодаря передаче управления соседнимпотокам между cycle и puts.

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

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

Полный код статьи на github

Комментарии 0 >>

18 ноября 2009, 23:16

Сборка руби-билиотеки в заданной среде

Темы: ruby, extension, syntax, bash

Постановка задачи

Как разработчику gphoto4ruby мне приходится сталкиваться с особыми задачами. Связано это с тем, что этот gem является оболочкой поверх ещё одной библиотеки. И как у всякой более-менее развитой сторонней библиотеки, у libgphoto2 есть версия, распространяемая через системные репозитории и порты и есть, так сказать, последний писк моды (bleeding edge).

    Отсюда вытекает необходимость:
  1. Иметь разные версии библиотеки не конфликтующие между собой, установленные не одной системе,
  2. Компилировать свою руби-библиотеку под любую из версий.

Установка двух gphoto2 :)

Проделаю весь путь с самого начала. Для пущей целостности. Для начала установка из системного репозитория:

sudo apt-get install libgphoto2-2-dev gphoto2
gphoto2 --version

Теперь можно скачать нужную версию и установить её отдельно. Поскольку я в основном использую две версии, то версию из исходников нужно установить в /opt. Предположим, что исходники libgphoto2 и gphoto2 скачаны:

tar zxvf libgphoto2-x.x.x.tar.gz
cd libgphoto2-x.x.x.tar.gz
./configure --prefix=/opt
make
sudo make install
tar zxvf gphoto2-x.x.x.tar.gz
cd gphoto2-x.x.x.tar.gz
./configure --prefix=/opt --with-libgphoto2=/opt
make
sudo make install
/opt/bin/gphoto2 --version

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

Компиляция джема

Если скачать исходник библиотеки, то можно проделать руками то, что делает команда gem install. Для создания Makefile используется утилита mkmf, которая входит в ruby-dev и с которой работает файл extconf.rb. В моём случае последовательность действий установщика такая:

cd ext
ruby extconf.rb
make

Теперь в папке ext мы имеем скомпилированную библиотеку (*.so или *.bundle в зависимости от системы). Установщик потом копирует её в папку lib, но мы пока остановимся. Мы можем посмотреть, какие другие библиотеки использует эта:

ldd gphoto4ruby.so

По выводу этой команды видно, что используется библиотека установленная из центрального репозитория. Теперь попробуем скомпилировать под версию «по последней моде». Поскольку я написал в extconf.rb

dir_config("gphoto2")

То это означает, что пользователю будет доступен целый ряд опций, позволяющих сказать компилятору, где искать libgphoto2. Попробуем:

ruby extconf.rb --with-gphoto2-dir=/opt
make
ldd gphoto4ruby.so

Но что это? Вывод показывает нам, что библиотека привязалась опять к тому, что установлено из репозиториев, а не тому, что в /opt. То есть компилятор, конечно, находит нужные ему заголовки (*.h), но ничего в них не говорит о том, где искать соответствующие им библиотеки. Об этом ему должны сказать мы:

ruby extconf.rb --with-gphoto2-dir=/opt --with-dldflags="-Wl,-rpath,/opt/lib"
make
ldd gphoto4ruby.so

Вуаля!

Теперь, собственно, главное. Как это сделать при установке джема. Чтобы передать ключи для extconf нужно задать их после дополнительного «--»:

sudo gem i gphoto4ruby -- --with-gphoto2-dir=/opt --with-dldflags="-Wl,-rpath,/opt/lib"

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

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

Руководство по расширению руби с помощью C (см. главу про extconf.rb)

Комментарии 0 >>

11 ноября 2009, 20:46

Сравнения и неравенства в руби

Темы: ruby, ruby1.9, syntax, regexp

Постановка задачи

Собрать в одном месте важные, на мой взгляд, особенности сравнений и неравенств в руби.

Основа неравенств в руби

Основным методом сравнения является <=>. Определив его, мы определяем все остальные операции, включив модуль Comparable:

class MyComp
  attr :value
  include Comparable
  def initialize(val)
    @value = val
  end

  def <=>(other)
    @value <=> other.value
  end
end

v1 = MyComp.new(1)
v2 = MyComp.new(2)

puts v1 < v2  # > true
puts v1 <= v2 # > true
puts v1 > v2  # > false
puts v1 >= v2 # > false
puts v1 == v2 # > false

Сам метод можно было бы описать как «возвращает -1, 0 или 1 в зависимости от того, меньше равен или больше объект, чей метод вызывается в сравнении с объектом переданным в качестве параметра». Но на самом деле, скорее, наоборот понятия «больше», «меньше» и «равен» определяются исходя из работы <=>.

Далее всё понятно и более ли менее очевидно для чисел, массивов и строк. Но есть и интересная особенность.

Сравнение модулей и классов

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

module T1
end
module T2
  include T1
end
T3 = T1

class C1
end
class C2 < C1
end
C3 = C1

puts "T1 <=> T2: #{(T1 <=> T2).inspect}" # > 1
puts "T1 <=> T3: #{(T1 <=> T3).inspect}" # > 0
puts "C1 <=> C2: #{(C1 <=> C2).inspect}" # > 1
puts "C1 <=> C3: #{(C1 <=> C3).inspect}" # > 0
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > nil
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > nil

C3.send(:include, T1)

puts "после включения"
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > -1
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > 1

Наследник или модуль, который включает другой модуль, меньше, чем родитель или включаемый модуль. Это видно даже из синтаксиса наследования.

Равенство

Существует три метода равенства: ==, eql?, equal?. Последний из которых никогда не следует переопределять, т.к. он отвечает за идентичность. Первые же два обычно работают одинаково. Канонический пример различия из документации:

3 == 3.0   # > true
3.eql? 3.0 # > false

Что лишь свидетельствует о том, что == проводит конвертацию чисел перед сравнением. Обычно == соответствует случаю, когда <=> возвращает 0.

Сравнение case...when

Все мы знаем, что в case...when оператор сравнения — это ===. В большинстве случаев он эквивалентен равенству из предыдущего параграфа. Но если равенство симметрично

(a.==(b)) == (b.==(a))

И если это не так, то это можно считать ошибкой. То === вовсе не обязано таковым быть. Нужно помнить, что в конструкции case...when вызывается метод сравнения объекта, стоящего после when, а в качестве параметра ему передаётся объект, стоящий после case:

puts String === "строка" # > true
puts "строка" === String # > false
puts /ок/ === "строка"   # > true
puts "строка" === /ок/   # > false
puts (1..10) === 5       # > true
puts 5 === (1..10)       # > false

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

  1. Полный код статьи на github
  2. Что нужно помнить, создавая свой объект руби

Комментарии 0 >>

28 октября 2009, 13:03

Удалённые вызовы через систему распределённых объектов в руби (dRuby)

Темы: ruby, daemon, ruby1.9

Введение

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

Постановка задачи

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

Решение: DRb

Для удалённого обращения с объектами существует стандартная руби-библиотека dRuby, в которой находится модуль DRb, который мы и будем использовать. Ничего устанавливать не нужно. Согласно документации, совершенно прозрачным образом можно вызвать методы на удалённом объекте даже на другой машине. Объекты и ссылки на них передаются в формате Marshal.

Ну, довольно теории! Перейдём к практике. Для эмуляции параллельных процессов (возможно на разных машинах (!)) мы будем использовать два окна терминала. В одном запустим server.rb:

# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"

class RemoteObject
  def remote_method_with_param(param)
    puts "вызван метод на сервере с параметром #{param.inspect}"
    case param.class.to_s
    when "String"
      puts "параметр типа строка"
      param.reverse!
    when "Array"
      puts "параметр типа массив"
      param.shift
    else
      puts "параметр оставшегося типа"
      param.do_smth
    end
  end
end

$SAFE = 1 # Запретить eval() и eval-оподобные вызовы

DRb.start_service("druby://localhost:45678", RemoteObject.new)
DRb.thread.join

Здесь мы используем банальный Thread#join, чтобы при необходимости просто прервать выполнение. Но те, кто читал предыдущую статью, знают, что в это время можно делать что угодно и следить за потоком dRuby отдельно.

В другом терминале запустим клиентский код client.rb:

# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"

class MyString
  def initialize(str)
    @string = str
  end

  def do_smth
    @string.reverse!
  end

  def inspect
    "<#{@string}>"
  end
end

rem_o = DRbObject.new_with_uri("druby://localhost:45678")

["строка", ["котик", "пёсик", "слоник"], MyString.new("суперстрока")].each do |obj|
  puts "Вызов метода вернул: #{rem_o.remote_method_with_param(obj).inspect}"
  puts "Параметр после вызова: #{obj.inspect}"
end

Вывод в терминалы будет следующий (я использую вывод для версии руби 1.9.1, потому что он нормально переворачивает кириллическую строку без колдовства) для сервера:

вызван метод на сервере с параметром "строка"
параметр типа строка
вызван метод на сервере с параметром ["котик", "пёсик", "слоник"]
параметр типа массив
вызван метод на сервере с параметром #<DRb::DRbUnknown:0x00000001248910 @name="MyString", @buf="\x04\bo:\rMyString\x06:\f@stringI\"\e\xD1\x81\xD1\x83\xD0\xBF\xD0\xB5\xD1\x80\xD1\x81\xD1\x82\xD1\x80\xD0\xBE\xD0\xBA\xD0\xB0\x06:\rencoding\"\nUTF-8">
параметр оставшегося типа

Клиент же упадёт с ошибкой:

Вызов метода вернул: "акортс"
Параметр после вызова: "строка"
Вызов метода вернул: "котик"
Параметр после вызова: ["котик", "пёсик", "слоник"]
(druby://localhost:45678) server.rb:17:in `remote_method_with_param': undefined method `do_smth' for #<DRb::DRbUnknown:0x00000001248910> (NoMethodError)
     .....

Что, безусловно, прекрасно. Прекрасно, что упал не сервер. :) Понятно, что он не знает ничего про этот объект и не знает, как с ним обращаться.

Как видно из вывода, объекты передаются в виде копий. Нашим же третьим, самодельным объектом, мы можем исследовать две возможности: таки передавать копию объекта или передавать лишь ссылку на него, чтобы вызовы выполнялись на клиентской копии. Для первой возможности достаточно вынести определение класса в общедоступное для клиента и сервера место — common.rb:

# coding: utf-8
$KCODE = "utf-8" if RUBY_VERSION < "1.9.0"
require "drb/drb"

REM_URI = "druby://localhost:45678"

class MyStringCopied
  def initialize(str)
    @string = str
  end

  def do_smth
    @string.reverse!
    self
  end

  def inspect
    "<<#{@string}>>"
  end
end

class MyStringSingle
  include DRb::DRbUndumped # это ключ :)
  def initialize(str)
    @string = str
  end

  def do_smth
    @string.reverse!
    self
  end

  def inspect
    "<#{@string}>"
  end
end

Добавим require "common.rb" в серверный код, а клиентский преобразится до такого:

# coding: utf-8
require "common"

rem_o = DRbObject.new_with_uri(REM_URI)

DRb.start_service # Это нужно для объекта, который не копируется при передаче

["строка",
  ["котик", "пёсик", "слоник"],
  MyStringCopied.new("суперстрока"),
  MyStringSingle.new("суперстрока без копий")].each do |obj|
  puts "Вызов метода вернул: #{rem_o.remote_method_with_param(obj).inspect}"
  puts "Параметр после вызова: #{obj.inspect}"
end

Как видно, мы сразу позаботились и о второй возможности, создав для неё ещё один класс. Секрет заключается во включении модуля DRb::DRbUndumped и старте ещё одного серверного процесса на клиенте (для вызовов методов объектов клиента удалённо) Клиентский вывод теперь выглядит так:

Вызов метода вернул: "акортс"
Параметр после вызова: "строка"
Вызов метода вернул: "котик"
Параметр после вызова: ["котик", "пёсик", "слоник"]
Вызов метода вернул: <<акортсрепус>>
Параметр после вызова: <<суперстрока>>
Вызов метода вернул: #<DRb::DRbObject:0x000000012588c8 @uri="druby://127.0.1.1:43998", @ref=9631244>
Параметр после вызова: <йипок зеб акортсрепус>

Если немножко почитать, и разобраться, какие объекты можно и нужно «маршализировать», а какие нельзя или не нужно, то получается вполне себе прекрасный инструмент. Который, повторюсь, входит в стандартную библиотеку и не требует никаких внешних зависимостей.

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

  1. Полный код статьи на github
  2. Документация по DRb (rdoc)
  3. Документация по Marshal

Комментарии 2 >>

22 октября 2009, 00:48

Работа с потоками (Thread) в руби

Темы: ruby, extension, syntax, daemon, ruby1.9

Введение

Сначала я расскажу, почему на сегодняшний день я не очень много работаю с подпроцессами на базе Thread, предпочитая им Kernel.fork. А потом покажу простой способ следить за потоками при работе приложения.

На текущий момент, основная проблема потоков — это «ненастоящее» распределение ресурсов. Все потоки руби на самом деле находятся в одном системном потоке, который по очереди передаёт им управление. Это влечёт за собой полтора следствия.

Зависание

Когда имеешь дело с внешним оборудованием, сторонними библиотеками и серийными портами, зависание потока может случиться на самом низком уровне. Это можно симулировать небольшой программой на си — block_thread.c:

#include <ruby.h>

VALUE rb_mBlockThread;

/*
 * call-seq:
 *   BlockThread::cycle(interval=5)
 *
 * Блокирует текущий поток на <code>interval</code> секунд.
 *
 */

VALUE bt_cycle(int argc, VALUE *argv, VALUE self) {
  int i, max;
  max = 5;

  if (argc == 1) {
    max = FIX2INT(argv[0]);
  } else if (argc > 1) {
    rb_raise(rb_eArgError, "Неправильное количество аргументов (%d вместо 0 или 1)");
  }

  for (i=0; i<max; i++) {
    sleep(1);
  }
  return Qnil;
}

void Init_block_thread() {
  /*
   * Модуль содержит методы для демонстрации работы потока
   */
  rb_mBlockThread = rb_define_module("BlockThread");
  rb_define_module_function(rb_mBlockThread, "cycle", bt_cycle, -1);
}

Если вы никогда не расширяли руби с помощью си, поясню, что в этой программе мы создаём модуль BlockTread, в котором создаём метода класса cycle, который указанное число раз (по умолчанию 5) в цикле ждёт одну секунду. Напишем extconf.rb:

require "mkmf"
create_makefile("block_thread")

И программу на руби, в которой будут два потока, один из которых мы заблокируем на низком уровне block_threads.rb:

# coding: utf-8
require "block_thread.so"

t1 = Thread.new do
  10.times { |i| puts i; sleep 0.1 }
end

t2 = Thread.new do
  puts "Блокируем"
  BlockThread.cycle
  puts "Разблокируем"
end

t1.join
t2.join

Скомпилируем и запустим:

ruby extconf.rb
make
ruby block_threads.rb

И что же мы видим? Мы видим, как все потоки, включая основной, блокируются на пять секунд (или любое число секунд, которое мы укажем) И даже ctrl + c не в силах нам помочь. Помогает только ctrl + z и потом killall ...

В случае же с Kernel.fork, процессы действительно равномерно делят между собой ресурсы, и один подпроцесс не способен заблокировать всё.

Синхронизация

Я говорил про полторы проблемы. Об одной уже рассказал, а вторая известна давно — попробуйте выполнить следующий код:

# coding: utf-8
$cnt = 0

t1 = Thread.new do
  100000.times { $cnt += 1 }
end

t2 = Thread.new do
  100000.times { $cnt += 1 }
end

t1.join
t2.join

puts "Without sync: #{$cnt}"

Если вы не используете руби 1.9, то вы получите неожиданный и каждый раз разный результат. Всё дело в том, что переключение между потоками происходит между элементарными операциями, а += состоит из трёх элементарных операций: достать значение, прибавить к нему число, записать значение. Чтобы этого не произошло, нужно либо использовать синхронизацию с помощью Mutex, либо руби 1.9. Ссылка на полный код для этой статьи в конце, т.к. я спешу перейти к более интересной части. :)

Слежение за потоками с помощью менеджера ThreadsWait

Совершенно недавно открыл для себя интересный способ следить за статусом пакетов в блокирующей и неблокирующей манере:

# coding: utf-8
require "thwait"

t1 = Thread.new do
  10.times { |i| puts "поток 1 тик #{i}"; sleep 0.5 }
end

t2 = Thread.new do
  10.times { |i| puts "поток 2 тик #{i}"; sleep 0.7 }
end

tw = ThreadsWait.new t1, t2

t3 = Thread.new do
  10.times { |i| puts "поток 3 тик #{i}"; sleep 0.3 }
end

run = true
tw.join_nowait t3

while run do
  begin
    # Неблокирующее ожидание
    puts "Закончил работу #{tw.next_wait(true).inspect }"
    run = false
  rescue ThreadsWait::ErrNoFinishedThread
    puts "Ожидаем окончания работы одного из потоков"
    sleep 0.5
  end
end

# Блокирующее ожидание
tw.all_waits do |t|
  puts "Закончил работу #{t.inspect}"
end

По-моему, весьма удобно, если вам нужно не просто ожидать окончания работы потоков, но ещё и делать что-то при этом.

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

  1. Полный код статьи на github
  2. Документация ThreadsWait
  3. Толковая статья о многопотоковости и процессах в руби

Комментарии 0 >>

07 октября 2009, 21:33

Некоторые тонкости стыковки ruby и bash

Темы: ruby, bash

Введение

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

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

Условное выполнение

В bash кроме разделителей команд «&» и «;», существует ещё и условное выполнение списка команд с помощью «&&» и «||». Их работа зависит от кода, с которым произошёл выход.
exit0:

#!/usr/bin/env ruby
puts "Выход с кодом 0"
exit 0

exit1:

#!/usr/bin/env ruby
puts "Выход с кодом 1"
exit 1

Теперь если запустить скрипты в следующем сочетании:

./exit0 && ./exit0 && ./exit1 && ./exit0

То вывод будет следующий:

Выход с кодом 0
Выход с кодом 0
Выход с кодом 1

А если запустить скрипты в следующем сочетании:

./exit1 || ./exit1 || ./exit0 || ./exit1

То вывод будет следующий:

Выход с кодом 1
Выход с кодом 1
Выход с кодом 0

То есть, «&&» выполняет следующую команду, если предыдущая вышла с кодом 0, а «||» выполняет следующую команду, если предыдущая вернула ненулевой код. Условно говоря, первый список выполняется пока всё срабатывает, а второй — пока не срабатывает.

Перенаправление вывода с помощью pipeline

Тут всё просто. Если в bash команды разделены с помощью «|», то вывод первой команды будет перенаправлен на вход второй.

Простая демонстрация из трёх файлов.
show:

#!/usr/bin/env ruby
$KCODE = "utf8"
$stdout.puts "stdin содержит: #{$stdin.read.inspect}"

out:

#!/usr/bin/env ruby
$stdout.puts "Привет из stdout!"

err:

#!/usr/bin/env ruby
$stderr.puts "[*stderr*] Привет из stderr!"
$stdout.puts "Привет из stdout!"

Думаю, не составит труда выяснить, что выводят запущенные отдельно out и err, но вот, что из этого можно сделать с помощью pipeline:

./out | ./show

выводит

stdin содержит: "Привет из stdout!\n"

Когда же есть обращение к другому каналу вывода, то

./err | ./show

выводит

[*stderr*] Привет из stderr!
stdin содержит: "Привет из stdout!\n"

Вывод stderr можно перенаправить в stdout:

./err 2>&1 | ./show

выводит

stdin содержит: "[*stderr*] Привет из stderr!\nПривет из stdout!\n"

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

Документация по bash

Комментарии 0 >>

23 сентября 2009, 15:59

Получение irb-консоли в среде приложения

Темы: ruby, automation, rails

Задача

Допустим, есть приложение, написанное на руби. У него есть основной процесс и есть тесты, которые так или иначе загружают объекты приложения.

Хочется получить доступ в среду приложения в виде irb-консоли, чтобы вручную взаимодействовать с объектами и изменять данные. По типу script/console в rails.

Решение

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

irb
require ...
require ...
...

Раз уж мы захотели как в рельсах, то следует предположить, что окружение нашего приложения загружается одним файлом. Например, config/environment.rb. Это будет первым упрощением многократно повторённого процесса.

Теперь сам файл script/console:

#!/usr/bin/env ruby

APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))

libs = " -r irb/completion"
libs << " -r pp" # специально для консоли автоподставнока и pretty print
libs << %( -r "#{APP_ROOT}/config/environment.rb")

ENV["APP_ENV"] = "console" # пример того, как сообщить приложению, что оно в консоли

exec "irb #{libs} --simple=prompt"
    Здесь стоит сделать два акцента:
  1. Для сообщения приложению, что оно запущено в консоли, мы использовали волшебный хэш ENV. К нему потом можно обратиться внутри config/environment.rb и сделать что-то по-другому.
  2. Для запуска консоли мы использовали Kernel.exec, который не просто выполняет системную команду, но и передаёт ей управление, заменяя текущий процесс.

Теперь, если сделать наш файл запускаемым, будет как в сказке:

chmod +x script/console

Я хочу, чтоб эта песня, эта песня не кончааалась

Если есть приятные библиотеки, которые хочется подключать при каждом запуске irb. А так же, если хочется сохранять историю команд консоли при выходе. То следует воспользоваться мощью ~/.irbrc. Создайте этот файл («~» — это $HOME, на всякий случай) и напишите в него:

require 'pp' # pretty print
require 'irb/completion' # автоподстановка
require 'irb/ext/save-history' # сохранение истории

ARGV.concat [ "--readline", # не пробовал без readline
              "--prompt-mode", "simple" ] # --simple-prompt

IRB.conf[:SAVE_HISTORY] = 25 # сколько сохранять

IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb-history" # куда сохранять

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

  1. Подробная документация irb;
  2. Занятный способ получить irb-консоль прямо в запущенном работающем приложении.

Комментарии 0 >>

16 сентября 2009, 01:26

Обработка исключений в руби

Темы: ruby, syntax

Введение

Сегодняшняя запись — это первая запись по заявкам читателей. :) Я напоминаю, что если у вас включен JavaScript, и если вы читаете эту запись на сайте, а не в rss-читалке, то вдоль левой границы окна будет оранжевая кнопка. Можно написать своё предложение либо нажав на неё, либо непосредственно на странице проекта. За предложения можно так же голосовать.

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

Базовые блоки

Я не буду слишком подробно останавливаться на каждом составляющем, а просто приведу пример, сделав некоторые пояснения:

class RetryException < StandardError
  attr :can_retry
  def initialize(rtr = true)
    @can_retry = rtr
  end
end

def exception_handling
  begin # Если бы не было else, то возвращалось бы последнее значение begin
    yield if block_given? 
  rescue ZeroDivisionError # Если програму запускает не Чак Норрис
    "На ноль делить нельзя"
  rescue RetryException => re # Возврат перезапускает блок begin
    puts "Не получилось, но мы попробуем снова"
    if re.can_retry
      retry
    else
      "Теперь точно не получилось"
    end
  rescue # Здесь ловится всё остальное
    puts "Случилось непредвиденное: #{$!.inspect}"
    raise
  else # Если всё прошло без ошибок
    "Всё прошло без ошибок"
  ensure # Этот блок выполняется в любом случае
    puts "Процесс окончен, но эта часть ничего не возвращает"
    "В любом случае" # Этой строки мы нигде не увидим
  end
end

blocks = [] # Массив с блоками

blocks.push(lambda {})
blocks.push(lambda { 1/0 })
blocks.push(lambda do
  @retry ||= 3 # Пробуем ещё раз не более трёх раз
  @retry -= 1
  raise RetryException.new(@retry > 0), "Временная ошибка"
end)
blocks.push(lambda { raise "Неведомая ошибка" })

blocks.each do |block|
  puts "Возвратилось: #{exception_handling(&block) rescue "Ошибка!"}"
  puts "------------------------"
end

Результат выполнения программы выглядит следующим образом:

Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Всё прошло без ошибок
------------------------
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: На ноль делить нельзя
------------------------
Не получилось, но мы попробуем снова
Не получилось, но мы попробуем снова
Не получилось, но мы попробуем снова
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Теперь точно не получилось
------------------------
Случилось непредвиденное: #<RuntimeError: Неведомая ошибка>
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Ошибка!
------------------------
    Что мы отсюда почерпнули:
  1. Блок else (необязательный) выполняется в случае удачно выполненного begin и возвращает значение вместо него.
  2. Выполняется первый из блоков rescue, который соответствует ошибке
  3. Метод raise без параметров выбрасывает текущее исключение: то, которое находится в глобальной переменной $!
  4. Чтобы создать свой класс исключений, наследовать нужно от StandardError или от его потомков, иначе не сработает rescue без указания класса (попробуйте сами проверить)

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

a = some_call rescue "some value"

эквивалентна

a = begin
  some_call
rescue
  "some value"
end

Особенности работы с потоками

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

Во-первых, без специальных указаний, никто даже не узнает о том, что в каком-то из потоков произошла ошибка:

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end
rescue
  puts "Спасены!"
end

puts "Поехали!"

sleep 7

puts "Как-то слишком тихо"

Производит вывод:

Поехали!
Первый поток
Последний поток
Как-то слишком тихо

Первый способ засечь исключения — это вызвать join на потоке. Тогда исключение будет переброшено в сам join. Но этот метод блокирует программу, пока поток не завершится. Что конечно же не очень удобно. Наше «поехали» перемещается ближе к концу:

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end.join
rescue Exception => e
  puts "Спасены!"
  puts e.backtrace.join("\n")
end

puts "Поехали!"

sleep 7

Производит вывод:

Первый поток
Спасены!
thr.rb:16
thr.rb:14:in `join'
thr.rb:14
Поехали!
Последний поток

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

Thread.abort_on_exception = true

begin
  Thread.new do
    sleep 1
    puts  "Первый поток"
  end

  Thread.new do
    sleep 3
    puts "Последний поток"
  end

  Thread.new do
    sleep 2
    raise "Ошибка внутри потока"
  end
rescue Exception => e
  puts "Спасены!"
end

puts "Поехали!"

sleep 7

Тогда наша программа оборвётся в момент ошибки и выведет:

Поехали!
Первый поток
thr.rb:16: Ошибка внутри потока (RuntimeError)
        from thr.rb:14:in `initialize'
        from thr.rb:14:in `new'
        from thr.rb:14

И никакие rescue нас не спасут.

Для самостоятельного изучения

  1. Вопросы-задачи-загадки, на которые полезно ответить не пиша программы
  2. Иерархия классов исключений в руби
  3. Более подробно про азы, в которые я не стал здесь спускаться

Комментарии 0 >>

10 сентября 2009, 17:43

Создание init-скриптов с помощью руби

Темы: ruby, bash

Предисловие

Сегодня у меня кроме всего прочего появился повод похвастаться! :) Когда вы смотрите широко обсуждаемые Яндекс-Панорамы, знайте, что их производством в составе моей любимой компании neq4 занимался и я.

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

Так же я, безусловно, планирую раскрывать некоторые секреты. Кстати, практически все записи в этом блоге основаны на работе над этим проектом. Например, программа, которая управляет камерами во время движения, а так же получает данные от приборов, была так же написана мной. И теперь вы знаете, что написана она на руби. :) За исключением библиотеки для работы с камерами, о которой я уже писал.

Это и подводит нас вплотную к теме сегодняшней записи.

Введение

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

Скрипты эти располагаются в папке /etc/init.d и запускаются при старте в определённых условиях. Они должны принимать параметры start, stop, restart и status. Как правило, они служат для запуска и остановки служб.

Решение

Написать программу, которая принимает текстовый ключ на входе, не составляет труда (/etc/init.d/my_script):

#!/usr/bin/env ruby

APP_NAME = "my_script"

case ARGV.first
  when "status"
    if...
      ...
      exit 0
    elsif ...
      ...
      exit 1
    else
      exit ...
    end
  when "start"
    ...
  when "stop"
    ...
  when "restart"
    ...
  else
    puts "Usage: #{APP_NAME} {start|stop|restart|status}"
end

Круто, но до настоящего init-скрипта не дотягивает. Когда система проходит свой цикл жизни от запуска до перезагрузки или выключения, она проходит через шесть стадий. Нужно указать, в каких стадиях наш скрипт следует запустить с параметром start, а когда — с параметром stop. Для этого используется спецификация LSB (Linux Standard Base). Вот так:

#!/usr/bin/env ruby
# Start/stop my daemon
#
### BEGIN INIT INFO
# Provides:          my_script
# Required-Start:    $all
# Required-Stop:     $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Initializes my daemon at boot time
# Description:       In a Galaxy far, far away...
### END INIT INFO

APP_NAME = "my_script"

case ARGV.first
.....

Теперь лежащий в /etc/init.d скрипт нужно подрегулировать и зарегистрировать:

sudo chown root:root /etc/init.d/my_script
sudo chmod 755 /etc/init.d/my_script
sudo update-rc.d my_script defaults

Таким образом, вовсе необязательно изучать bash-скрипт. Вот, кстати, оболочка-альтернатива всяким башам, использующая синтаксис руби. Для таких, как я. :)

Для самостоятельного изучения

Спецификация LSB в части запуска и остановки скриптов

Комментарии 0 >>

02 сентября 2009, 23:27

Автоподстановка задач rake в терминале с помощью ruby

Темы: ruby, bash

Введение

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

Задача

Найти решение для автоподстановки задач rake с помощью bash несложно. Для этого используется complete. Огромное число примеров можно посмотреть у себя же в системе в /etc/bash_completion.

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

Можно ли из этого извлечь пользу?

Решение

Итак, чтобы подключить наш скрипт, нужно добавить в файл ~/.bashrc такую строчку.

complete -C ~/.bash/autocomplete/rake_complete -o default rake

Для начала положим по адресу ~/.bash/autocomplete/rake_complete элементарную реализацию автоподстановки, которая будет запускать rake и фильтровать результат.

    Следует знать два момента:
  1. Введенная строка, после которой была нажата табуляция, находится в ENV["COMP_LINE"].
  2. В задачах могут попасться пространства имён. Поэтому из окончательного результата нужно убрать часть введенной строки, содержащей двоеточие.

Элементарная реализация выглядит следующим образом:

#!/usr/bin/env ruby

class RakeComplete
  def initialize(cmd)
    @command = cmd[ /\s(.+)$/, 1 ] || ""
  end

  def search
    `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }.select{ |cmd| cmd[0, @command.length] == @command }.map{ |cmd| cmd.gsub(cmd_before_column, "") }
  end
private
  def cmd_before_column
    @command[ /.+\:/ ] || ""
  end
end

puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0

Вроде бы всё понятно: из результата вывода rake -T , отбросив паразитную первую строчку, вытаскиваем только названия задач, подбираем те, начало который совпадает с введенным названием задачи, выводим массив, предварительно удалив у элементов введённую до двоеточия часть. Но уж больно медленно работает. Конечно же, встает вопрос о кешировании.

Кеширование

Приведу сразу результат:

#!/usr/bin/env ruby

class RakeComplete
  CACHE_NAME = ".rake_complete~"

  def initialize(cmd)
    @command = cmd[ /\s(.+)$/, 1 ] || ""
  end

  def search
    exit 0 if rake_file.nil?
    selected_tasks.map do |cmd|
      cmd.gsub(cmd_before_column, "")
    end
  end

private

  def cmd_before_column
    @command[ /.+\:/ ] || ""
  end

  def rake_file
    ["Rakefile", "Rakefile.rb", "rakefile", "rakefile.rb"].detect do |name|
      File.file?(File.join(Dir.pwd, name))
    end
  end

  def cache_file
    File.join(Dir.pwd, CACHE_NAME)
  end

  def generate_tasks
    tasks = `rake -T`.split("\n")[1..-1].map{ |l| l.split(/\s+/)[1] }
    File.open(cache_file, "w") do |f|
      tasks.each do |task|
        f.puts task
      end
    end
    tasks
  end

  def cached_tasks
    File.read(cache_file).split("\n")
  end

  def cache_valid?
    File.exist?(cache_file) && (File.mtime(cache_file) >= File.mtime(rake_file))
  end

  def tasks
    cache_valid? ? cached_tasks : generate_tasks
  end

  def selected_tasks
    tasks.select do |cmd|
      cmd[0, @command.length] == @command
    end
  end
end

puts RakeComplete.new(ENV["COMP_LINE"]).search
exit 0

Для самостоятельного изучения

Думаю, теперь не составит труда написать подобное для задач capistrano.

Комментарии 2 >>

27 августа 2009, 17:07

Приёмы работы с массивами и блоками в качестве аргументов в руби

Темы: ruby, syntax

Введение

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

Массивы

О том, как принять неограниченное число аргументов, знают все:

$KCODE = "utf-8"

def my_args(*args)
  puts args.inspect
end

my_args(1, 2, "собачка", "котик")  # => [1, 2, "собачка", "котик"]

Но что, если сами объекты, которые необходимо передать в качестве аргументов, уже находятся в массиве по какой-то причине?

arr = ["собачка", "котик", "ёжик", "медвежонок"]
selection = arr.select{ |a| a.chars.to_a.length > 5 }

my_args(selection)  # => [["собачка", "медвежонок"]]

Внезапно массив (args) стал двумерным, что неудивительно, т.к. метод интерпретировал переданный ему массив как первый аргумент типа Array. В данном случае, чтобы передать массив как список аргументов, нужно использовать ту же звезду:

my_args(*selection)  # => ["собачка", "медвежонок"]

Ура!

Кстати, звезду можно использовать и при получении значения от функции. Например, в таком случае:

def get_values
  ["красный", "зелёный", "синий"]
end

r, g, b = get_values

puts r.inspect   # => "красный"
puts g.inspect   # => "зелёный"
puts b.inspect   # => "синий"

r, gb = get_values

puts r.inspect   # => "красный"
puts gb.inspect  # => "зелёный"

r, *gb = get_values

puts r.inspect   # => "красный"
puts gb.inspect  # => ["зелёный", "синий"]

По-моему, здорово!

Блоки

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

class Button
  def on_click(&block)
    if block_given?
      @stored_block = block
    else
      puts "Блока нет, но жизнь продолжается"
    end
  end

  def click(*args)
    @stored_block.call(*args)
  end
end

my_btn = Button.new

my_btn.on_click  # => Блока нет, но жизнь продолжается

my_btn.on_click do |*args|
  puts "Произошёл клик!"
end

my_btn.click     # => Произошёл клик!

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

handler = lambda{ |*args| puts "Вот аргументы: #{args.inspect}" }

my_btn.on_click(handler)  # => wrong number of arguments (1 for 0) (ArgumentError)

То есть наш блок превратился в аргумент типа Proc. А метод совсем этого не ожидал. Можно модифицировать сам метод, чтобы он выдерживал подобные условия, но не для этого я всё это пишу :) Если мы чему-нибудь научились в предыдущей главке, то решение по аналогии приходит само:

my_btn.on_click(&handler)

my_btn.click("усы", "хвост")  # => Вот аргументы: ["усы", "хвост"]

Надеюсь, что было полезно.

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

  1. Аргументы в руби
  2. Ещё про аргументы в руби

Комментарии 0 >>

06 июля 2009, 17:56

Упрощение работы с путями в руби

Темы: ruby, tdd

Введение

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

Текущий путь

Довольно часто встречающаяся конструкция, после объединения путей, в моём случае — это:

File.dirname(__FILE__)

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

File.open(filepath)

А также должен уметь определять путь файла, в котором инициализируется или вызывается.

Начнём, конечно же, с тестов. Кроме всего прочего, я предпочитаю оперировать с развёрнутыми путями, т.к. если загружать библиотеку из разных мест, то пути могут не опознаваться как одинаковые, и интерпретатор ругается, например, на повторную инициализацию констант. Итак, тест с использованием RSpec:

describe FilePath do
  it "should show correct current path" do
    FilePath.current.should == File.expand_path(__FILE__)
  end
end

Если использовать __FILE__ внутри класса FilePath, то там окажется путь к файлу, в котором определяется класс.

Использование $0 так же не подходит, т.к. выдает путь только главного файла. В случае запуска теста $0 будет где-то в библиотеках.

Нам бы пригодилось что-нибудь вида:

eval("__FILE__", binding_of_caller)

Но binding_of_caller работало с помощью бага, который уже давно исправлен, а Binding.of_caller выглядит очень громоздко (можно там кликнуть на ссылочку Source). Мало того, что он ломает trace_func, так он требует, чтобы метод, в котором он используется, вызывали только внутри метода.

Можно ещё передавать внутрь метода пустой Proc, вытаскивая его binding, но требовать это от человека, использующего библиотеку для упрощения жизни, как-то нелепо.

Решение

На помощь спешит Kernel.caller, знакомый нам по трейсам ошибок. Если разобраться, как он работает, то решение приходит сразу:

caller(1)[0].split(":")[0]

Остальное можно посмотреть в исходниках file_path@github. Когда соберётся джем-библиотека, я обновлю инструкции и опубликую rdoc. Вдруг кому пригодится!

Комментарии 2 >>

16 июня 2009, 21:07

Управление фотокамерой с помощью руби

Темы: ruby, extension

Введение

По заказу neq4 в рамках одного из проектов сделал библиотеку для работы с камерами. Точнее, это оболочка вокруг библиотеки на C (libgphoto2) для использования в руби.

Пришлось, конечно, изучать тему расширения руби с помощью C, но сегодня я хотел написать не об этом, а наоборот о том, как пользоваться библиотекой на руби. Что называется, программировал на C, чтобы другим не пришлось :)

Оболочка собрана в виде gem и работает на Linux и Mac OS. То есть везде, где работает gphoto2.

Установка

Для того, чтобы собрать gem, кроме средств сборки и версии руби для разработчиков, нужна установденная версия libgphoto2 для разработчиков:

sudo apt-get install ruby-dev build-essential libgphoto2-dev

Теперь можно ставить gem:

sudo gem i gphoto4ruby

Инициализация и настройки камеры

О том, какие камеры совместимы, и как их подключать для работы достаточно много написано у создателей gphoto2. Скажу лишь, что те камеры, с которыми я работал, для подключения нужно было переводить в PTP-mode. После подключения:

require "rubygems"
require "gphoto4ruby"

c = GPhoto2::Camera.new

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

cfg = c.config      #{"capturemode" => "Single Shot",
                    # "exptime" => "0.125",
                    # "f-number" => "f/3.2", ...}
cfg.keys            # возможные настройки
c["f-number", :all] # ["f/2.8", "f/3.2", "f/3.5", ...]
                    # возможные значения
c["f-number"]       # текущее значение
c["f-number"] = "f/8" # вуаля! настройка на камере изменена

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

c["focusmode", :no_cache] # на Nikon D80 меняется переключателем

Съёмка

c.capture
c.capture.save.delete # сохранить снятую фотографию в текущую
                      # папку и удалить её с камеры
c.save :file => :last, :to_folder => "/home/sweet/home"
                      # сохранить последний из файлов в папку
c.save :file => c.files[3], :type => :preview, :new_name => "PREVIEW.JPG"
                      # загрузить превью фотографии под номером 4
                      # в списке файлов на карточке

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

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

Библиотека libgphoto2 и инструмент для работы с камерами из командной строки gphoto2: есть FAQ.
Документация оболочки gphoto4ruby: полный список классов и методов с примерами.
Исходный код gphoto4ruby
Расширение руби с помощью C

Комментарии 0 >>

09 июня 2009, 00:51

Ruby daemon, или как сделать демона на руби

Темы: daemon, ruby

Задача

Иногда в процессе работы с разными сторонними библиотеками, которые содержат тяжелые блокирующие методы, не обойтись обычным Thread. В таком случае на помощь приходят демоны :) Вот несколько приемов, которые я освоил в работе с нимим.

Базовый механизм

Запустить демона можно с помощью метода Kernel.fork, передав ему блок. Метод возвращает pid процесса, который можно записать в файл и просто использовать в дальнейшем.

pid = fork do
  puts "from daemon"
  exit!(1)
end

Определение статуса

Я не нашёл удобного способа определения методами руби, запущен ли процесс. Есть возможность фильтровать вывод ps ax, ища в нём pid процесса. Но есть метод Process.waitpid, который можно использовать хитрым образом. Так же для будущих задач, упакуем наш код в класс:

require 'timeout'

class Daemon
  class << self
    def start
      @pid = fork do
        puts "from daemon"
        sleep 1
        exit!(1)
      end
    end

    def running?
      if @pid
        begin
          Timeout::timeout(0.01) do
            Process.waitpid(@pid)
            if $?.exited?
              return false
            end
          end
        rescue Timeout::Error
        end
        return true
      else
        return false
      end
    end
  end
end

Daemon.start

puts "running: #{Daemon.running?}"
sleep 1.5

puts "running: #{Daemon.running?}"

Сообщение об ошибках

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

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

require 'timeout'

class Daemon
  def initialize
    puts "from daemon: initializing"
  end

  class << self
    def start
      @rd, @wr = IO.pipe
      @pid = fork do
        @rd.close
        begin
          dmn = new
          @wr.write "ok"
          @wr.close
          sleep 1
        rescue Exception => e
          @wr.write e.to_s
          @wr.close
        ensure
          exit!(1)
        end
      end
      @wr.close
      str = @rd.read
      if str == "ok"
        puts "daemon started ok"
      else
        puts "error while initializing daemon: #{str}"
      end
      @rd.close
    end

    def running?
      if @pid
        begin
          Timeout::timeout(0.01) do
            Process.waitpid(@pid)
            if $?.exited?
              return false
            end
          end
        rescue Timeout::Error
        end
        return true
      else
        return false
      end
    end
  end
end

Daemon.start

puts "running: #{Daemon.running?}"
sleep 1.5

puts "running: #{Daemon.running?}"

Остановка

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

require 'timeout'

class Daemon
  def initialize
    puts "from daemon: initializing"
    @cnt = 0
  end

  def main_loop
    @cnt += 1
    puts "from daemon: running loop ##{@cnt}"
    sleep 0.1
  end

  class << self
    def start
      @rd, @wr = IO.pipe
      @pid = fork do
        @rd.close
        running = true
        Signal.trap("TERM") do
          running = false
        end
        begin
          dmn = new
          @wr.write "ok"
          @wr.close
          while running
            dmn.main_loop
          end
        rescue Exception => e
          @wr.write e.to_s
          @wr.close
        ensure
          exit!(1)
        end
      end
      @wr.close
      str = @rd.read
      if str == "ok"
        puts "daemon started ok"
      else
        puts "error while initializing daemon: #{str}"
      end
      @rd.close
    end

    def stop
      unless @pid.nil?
        Process.kill("TERM", @pid)
        @pid = nil
      end
    end

    def running?
      if @pid
        begin
          Timeout::timeout(0.01) do
            Process.waitpid(@pid)
            if $?.exited?
              return false
            end
          end
        rescue Timeout::Error
        end
        return true
      else
        return false
      end
    end
  end
end

Daemon.start

puts "running: #{Daemon.running?}"
sleep 1
Daemon.stop
puts "running: #{Daemon.running?}"

Теперь, если наследовать от этого класса свой класс и переопределить методы initialize и main_loop, получится вполне себе демон :)

Для самостоятельного изучения

    Есть, конечно, ещё недостатки. Например:
  1. Если ошибка возникает в main_loop, то канал вывода уже закрыт. А если не закрывать канал, то метод IO#read не позволит нам выйти из метода start. Что делать?
  2. Если нужно периодически общаться с демоном общирными объемами информации, что делать? (я в своей задаче использовал TCPSocket, но это, ведь, не панацея)
  3. Хорошо бы хранить pid в pid-файле на случай нашествия зомби. И, соответственно, обрабатывать возникающие зомбо-проблемы.

Но это я оставлю на самостоятельное решение пытливым читателям.

Комментарии 2 >>

30 апреля 2009, 20:11

Тестирование Paperclip с помощью Factory Girl

Темы: ruby, rails, bdd

Введение

Регулярно в своей работе я встречаюсь с новыми инструментами и решаю разные задачи. Иногда новые решения кажутся незначительными и слишком простыми для того, чтобы посвящать им целый пост в блоге. Но в нашем блоггерском деле главное — это регулярность. :)

Задача

Протестировать вновь изученный инструмент Paperclip с помощью вновь изученного инструмента Factory Girl.

Решение

Допустим, прикрепленный файл называется photo. То есть в модели написано следующее:

class Something < ActiveRecord::Base
  has_attached_file :photo
end

Кладем файл с фото, например, в папку с fixtures. Тогда файл test/factories.rb (или spec/factories.rb) будет содержать:

Factory.define :something do |smth|
  smth.photo File.new("path_to_file_in_fixtures", "rb")
end

И всё! Теперь в тестах, когда нужно создать объект пишем:

Factory.create(:something)
 # или
Factory.build(:something)

А в тестах контроллера пишем:

Factory.attributes_for(:something)

Выводы

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

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

Проекты в первом и втором случае значительно отличаются по объему. :)

Материалы для изучения

Множество прекрасных инструментов от ThoughtBot

Комментарии 0 >>

20 апреля 2009, 14:13

Тестирование OpenID с помощью Cucumber

Темы: ruby, rails, bdd

Задача

Какое-то время назад решил приобщиться к bdd (behavior driven development). С помощью такого подхода очень удобно описывать функционал приложения с точки зрения финального пользователя. И, соответственно, писать код так, чтобы выполнялись сценарии.

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

В качестве хорошего примера рассмотрим сценарий комментирования в этом блоге. Комментарии здесь можно оставлять только с помощью идентификации по OpenID.

Ресурсы

Нам понадобится, собственно, Cucumber, который для более лёгкого старта лучше использовать вместе с RSpec (фреймворк для удобного написания тестов и test driven development) и WebRat (инструмент для интегральных тестов, который содержит удобные методы работы с клиентским интерфейсом приложения).

Для работы с OpenID со стороны конечного пользователя нам понадобится локальный тестовый сервер OpenID. Есть хороший вариант: ROTS.

Затребуем библиотеки в тестовом окружении. В файле config/environments/test.rb:

config.gem "rspec", :lib => false, :version => ">=1.2.4"
config.gem "rspec-rails", :lib => false, :version => ">=1.2.4"
config.gem "webrat", :lib => false, :version => ">=0.4.4"
config.gem "cucumber", :lib => false, :version => ">=0.3.0"
config.gem "roman-rots", :lib => "rots", :version => ">=0.2.1"

Установим всё это:

sudo apt-get install libxml2-dev libxslt-dev
gem sources -a http://gems.github.com
rake gems:install RAILS_ENV=test

Решение

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

Теперь чтобы начать использовать Cucumber в приложении нужно запустить генератор:

script/generate cucumber

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

OpenID сервер

После установки библиотеки ROTS появляется консольная комманда:

rots

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

Сценарий Сucumber

Сценарии называются features. Создаем файл features/create_comments.feature:

Feature: Create Comments
  In order to give feedback
  As a reader
  I want to create comments

  Scenario: Create Comment
    Given I visit a page for the published post with 1 approved comment
    When I fill in "OpenID" with "http://localhost:1123/john.doe?openid.success=true"
    And I fill in "Имя" with "John Doe"
    And I fill in "Текст" with "Nice post!"
    And I press "Отправить"
    Then I should verify my OpenID
    And I should see "John Doe"
    And I should see "Nice post!"
    And the post should have 2 approved comments

Многие из шагов уже определены с помощью WebRat и RSpec. Доопределим то, чего не хватает.

Шаги для статей

Тут есть моё внутреннее соглашение (с самим собой :). Когда я использую определенный артикль the, то имею в виду статью, которую я запоминаю в течение каждого сценария. В файле features/step_definitions/post_steps.rb:

Given /^I visit a page for the (published|draft) post with (\d+) (approved )?comments?$/ do |pub, cmnts_count, appr|
  @post = Post.create!(:title => "Cucumber test post",
                       :body => "This is test post for cucumber comments",
                       :published => (pub == "published"))
  cmnts_count.to_i.times do |cnt|
    comment = @post.comments.build(:name => "Test User", :body => "Test body #{cnt}")
    comment.openid_url = "http://test.example"
    comment.approved = true unless appr.nil?
    comment.save!
  end
  visit post_url(@post)
end

Then /^the post should have (\d+) (approved )?comments?$/ do |count, appr|
  if appr.nil?
    @post.comments.count.should == count.to_i
  else
    @post.approved_comments.count.should == count.to_i
  end
end

Шаги для комментариев

Этот шаг специально написан для работы с ROTS, который, не требуя от пользователя никакого интерактивного взаимодействия в процессе теста отвечает на запрос редиректом подтверждая или отвергая авторизацию. В файле features/step_definitions/comment_steps.rb:

Then /^I should verify my OpenID$/ do
  response = Net::HTTP.get_response(URI.parse(headers['location']))
  response.class.should == Net::HTTPSeeOther
  visit response['location']
end

Заключение

Вот и всё. Чтобы запустить тест, нужно выполнить:

cucumber features

Далее, конечно же, следует написать сценарии для пустых полей, для отказа в авторизации, для неправильного адреса OpenID и прочее. Но это, как мне кажется, не составляет труда.

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

Материалы для изучения

RailsCast про авторизацию с помощью OpenID
RailsCast про использование Cucumber
Документация по Cucumber

Комментарии 3 >>

14 апреля 2009, 16:30

Фильтрация rss-потоков с помощью Sinatra и HTTParty

Темы: ruby, rack, regexp, xml

Задача

Для фильтрации rss-потоков сужествует множество инструментов. Для своей задачи мне захотелось написать простейшее решение и заодно попробовать пару новых инструментов.

Надо: собрать воедино несколько единообразных rss-потоков, отфильтровав только нужное, и выдать единый rss-поток.

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

Ресурсы

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

А для работы с самими потоками, для получения их с их серверов используем простой и удобный инструмент HTTParty.

sudo gem i sinatra
sudo gem i httparty

Сбор и фильтрация

Создадим библиотечный файл feed_fetcher.rb:

require 'rubygems'
require 'httparty'

class FeedFetcher
  include HTTParty
  format :xml # позволяет получть результат сразу расфасованный
              # в Hash

  def self.get_items
    urls = nil # будет массив адресов
    titles = nil # будет массив нужных частей заголовков
    items = [] # будет массив записей
    File.open("path_to_feed_urls_file") do |f|
      urls = f.readlines.each(&:strip!)
    end
    File.open("path_to_titles_file") do |f|
      titles = f.readlines.each(&:strip!)
    end

    # составим единое регулярное выражение для фильтрации
    retitles = Regexp.union(titles.reject(&:empty?).map { |t| %r{\b#{Regexp.escape(t)}\b}i })

    # соберём записи со всех адресов в единый масив
    urls.each do |u|
      items += get(u)["rss"]["channel"]["item"] unless u.empty?
    end

    # отфильтруем по регулярному выражению и упорядочим по дате
    items.select { |i| i["title"] =~ retitles }.sort do |x, y|
      DateTime.parse(y["pubDate"]) <=> DateTime.parse(x["pubDate"])
    end
  end
end

Выдача результата

Результат будем так же выдавать в формате atom, поэтому нам понадобится builder, который, например, входит в состав active_support. Но можно установить его и отдельно.

Файл feed_filter.rb:

require 'rubygems'
require 'sinatra'
require 'active_support'
require 'feed_fetcher.rb'

get '/' do
  content_type 'application/xml', :charset => 'utf-8'
  @items = FeedFetcher.get_items
  builder :index
end

По-умолчанию Sinatra хранит шаблоны в папке views. Файл views/index.builder:

xml.instruct!
xml.rss "version" => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
  xml.channel do
    xml.title "My Filtered Feed"
    xml.link "http://lonelyelk.com"
    xml.pubDate CGI::rfc1123_date Time.parse(@items.first["pubDate"]) if @items.any?
    xml.description "Some description"
    @items.each do |item|
      xml.item do
        item.each_pair do |key, value|
          xml.tag!(key, value)
        end
      end
    end
  end
end

Запуск приложения с помощью passenger

Для запуска приложения мы будем использовать Passenger, который поддерживает не только rails, но и rack. Для этого нам понадобится создать папку public и указать к ней путь.

В установках виртуального сервера для apache:

<VirtualHost *:80>
  ServerAdmin webmaster@mydomain.ru
  ServerName feedfilter.mydomain.ru
  DocumentRoot /path/to/feed_filter/public
  ...
</VirtualHost>

А в папке приложения нужно создать файл config.ru:

require 'rubygems'
require 'sinatra'

Sinatra::Application.set(:run, false)
Sinatra::Application.set(:environment, ENV['RACK_ENV'])

require 'feed_filter'
run Sinatra::Application

Вот и всё. Естественно, ещё следует написать тесты. Так же для публикации можно использовать capistrano. Но это, я думаю, всем под силу.

Материалы для изучения

Первое знакомство с фреймворком Sinatra

Комментарии 0 >>

28 марта 2009, 21:56

Автосохранение с помощью JavaScript

Темы: javascript, ajax, ruby, rails

О стандартизации

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

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

Задача

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

Ресурсы

Для работы нам понадобится Prototype, который входит в стандартную поставку Rails и обязательно крутилочка. Без крутилочки не стоит даже начинать. :)

Решение

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

config/routes.rb

map.resources :posts, :member => {:autosave => :post}

Контроллер

...
def autosave
  @post = Post.find(params[:id])
  if @post.update_attributes(params[:post])
    render :nothing => true
  else
    render :nothing => true, :status => 400
  end
end
...

Шаблон

Привожу только код JavaScript, который понадобится для сохранения. Рекомендую обратить внимание, на решение проблемы authenticity_token

var cachedBody = $("post_body").value;
var cachedTitle = $("post_title").value;
var saveTimeout = 20000;
var modTimeout = 200;
var requestInProgress = false;

function checkSave() {
  if ((((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
       ((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) &&
      !requestInProgress) {
    cachedTitle = $("post_title").value;
    cachedBody = $("post_body").value;
    $("spinner").show();
    requestInProgress = true;
    new Ajax.Request("<%= autosave_post_url(@post) -%>", {
        method: "post",
        postBody: Form.serializeElements([$("post_title"), $("post_body")]) +
              '&authenticity_token=' + encodeURIComponent('<%= form_authenticity_token -%>'),
        onSuccess: function (transport) {
              var now = new Date();
              $("spinner").hide();
              $("autosave_text").innerHTML = "Saved at " + 
                    (now.getHours() < 10 ? "0" : "") + now.getHours() + ":" +
                    (now.getMinutes() < 10 ? "0" : "") + now.getMinutes() + ":" +
                    (now.getSeconds() < 10 ? "0" : "") + now.getSeconds();
              setTimeout("checkSave()", saveTimeout);
              requestInProgress = false;
            },
        onFailure: function (transport) {
              $("spinner").hide();
              $("autosave_text").innerHTML = "Failed to autosave";
              requestInProgress = false;
            }
      });
  } else {
    setTimeout("checkSave()", saveTimeout);
  }
}

function checkMod() {
  if (((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
      ((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) {
    if ($("autosave_text").innerHTML.charAt(0) != "*") {
      $("autosave_text").innerHTML = "* " + $("autosave_text").innerHTML;
    }
  }
  setTimeout("checkMod()", modTimeout);
}

function checks() {
  checkSave();
  checkMod();
}

document.observe('dom:loaded', checks);

В данном случае spinner — это id крутилочки. Крутилочка показывается, когда проходит запрос. Так же здесь имеется дополнительный цикл вызовов. Раз уж мы всё равно храним копию того, что сохранено на сервере, то почему бы не показывать, когда текст уже изменен, но не сохранен на сервере. С помощью звездочки.

А как сделать, чтобы звездочка исчезала, если текст в полях снова стал такой, оставлю на самостоятельное изучение.

Материалы для изучения

Интересный пример ненавязчивого JavaScript для создания «бесконечной» страницы

Комментарии 0 >>

24 марта 2009, 22:09

Jabber бот

Темы: ruby

Возникла идея написать jabber-бота. Выбор протокола обусловлен тем, что по нему достаточно много документации. И стандарт открытый — это хорошо. Много информации на code.google.com, но я решил пойти более простым путём и нашёл библиотеку на руби xmpp4r. Благодаря тому, что разработка ведется на руби, время разработки простого бота значительно сокращается. Устанавливается как библиотека:

sudo gem i xmpp4r

Регистрация

Это прекрасно, что бот может сам себя зарегистрировать.

require "rubygems"
require "xmpp4r"

include Jabber

client = Client.new(JID.new("login@server"))
client.connect

begin
  client.register "secretpassword"
  puts "Success!"
rescue ServerError => e
  puts "Error: #{e.error.text}"
end

При регистрации можно передавать хэш с параметрами для визитной карточки. После регистрации для последующей авторизации вместо register вызываеься:

client.auth "secretpassword"

Список контактов

Список контактов называется roster. Чтобы с ним работать нужно его запросить:

require "xmpp4r/roster"
roster = Roster::Helper.new(client)

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

roster.add_query_callback do |iq|
  puts "current roster:"
  roster.find_by_group(nil).each do |item|
    puts "#{item.jid}"
  end
end

Простейшее разрешение видеть свой статус и добавление в контакты в ответ на то, что добавляют бота выглядит так:

roster.add_subscription_request_callback do |item, pres|
  puts "Added by #{pres.from}"
  roster.accept_subscription(pres.from) # Разрешить видеть свой статус
  new_pres = Presence.new.set_type(:subscribe).set_to(pres.from)
  client.send(new_pres) # Запросить разрешение видеть статус
end

Теперь наши контакты смогут видеть наш бот онлайн, когда мы сообщаем об этом:

client.send(Presence.new.set_type(:available))

Неплохо бы что-нибудь ещё добавить, типа аватара.

Визитная карточка

Визитная карточка называется vcard:

require "xmpp4r/vcard"
require "base64"

vcard = Vcard::IqVcard.new
vcard["FN"] = "Full Name"
vcard["NICKNAME"] = "bot-nickname"
vcard["PHOTO/TYPE"]= "image/jpeg"
avatar_file = File.new("avatar.jpg", "r")
avatar_b64 = Base64.b64encode(avatar_file.read())
avatar_file.rewind
avatar_sha1 = Digest::SHA1.hexdigest(avatar_file.read())
vcard["PHOTO/BINVAL"] = avatar_b64
avatar_file.close

begin
  Vcard::Helper.new(client).set(vcard)
  puts "Success!"
rescue Exception => e
  puts "VCARD operation failed: #{e.to_s}" 
end

В соответствии со спецификацией xmpp относительно аватаров следует транслировать с какой-то периодичностью sha своего аватара:

Thread.new do
  while true do
    unless avatar_sha1.nil?
      pres = Presence.new
      x = REXML::Element.new("x")
      x.add_namespace("vcard-temp:x:update")
      photo = REXML::Element.new("photo")
      avatar_hash = REXML::Text.new(avatar_sha1)
      photo.add(avatar_hash)
      x.add(photo)
      pres.add_element(x)
      client.send(pres)
    end
    sleep 60
  end
end

Дело за малым.

Ответ на сообщения

cl.add_message_callback do |m|
  if m.type != :error and m.body
    client.send(m.answer.set_body(bot_main_function(m.body)))
  end
end

Внутри bot_main_function находится собственно то, что будет делать ваш бот, как он будет отвечать на то, что написали ему.

Материалы для изучения

Ещё интересные примеры

Комментарии 0 >>

16 марта 2009, 22:24

Автоматизация процесса публикации

Темы: ruby, deploy, automation

Когда я впервые прочитал про, например, Capistrano, мне, конечно же, сразу захотелось тоже начать применять эту клёвую штуку. Но я, конечно же, не преодолел барьер входа. На тот момент у меня было полтора приложения на ruby on rails, которые я довольно редко обновлял. Позже, когда я начал регулярно обновлять несколько приложений, использовать средства автоматизации оказалось очень просто и очень естественно. Для этого достаточно было вручную обновить приложение раз двадцать. :)

Задача

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

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

cd somedir && do_some_stuff && sudo do_some_sudo_stuff

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

Ресурсы

Нам понадобится две библиотеки: одна для использования ssh, и другая для защищенного от заглядывания через плечо ввода sudo-пароля. (Оказалось, что сделать на руби такой ввод не так просто, поэтому я просто взял готовую библиотеку, которую и так использует, например, Capistrano и ряд других приложений).

sudo gem i net-ssh highline

Решение

Первым делом я, конечно, попробовал:

Net::SSH.start("myserver", "sudouser") do |ssh|
  result = ssh.exec!("cd somedir && do_some_stuff && sudo do_some_sudo_stuff")
  puts result
end

Но никакого вывода просто не дождался. Потому что дойдя до sudo-команды, процесс просто оставался в вечном ожидании.

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

Net::SSH.start("myserver", "sudouser") do |ssh|
  channel = ssh.open_channel do |ch|
    ch.request_pty do |c, success|
      raise "Cannot obtain pty" unless success
    end
    ...
  end
end

Теперь нужно отправить пароль в нужный момент. Чтобы узнать, когда наступил нужный момент, нужно использовать ключ -p (prompt) при вызове sudo, чтобы сказать ему, каким запросом спрашивать у нас пароль.

sudo -p 'sudo password: ' do_some_sudo_stuff

Когда нужно будет запросить пароль, воспользуемся библиотекой highline:

pwd = HighLine.new.ask("Input remote host sudo password: ") { |q| q.echo = false }

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

Теперь посмотрим на всё решение целиком. В папке библиотеки создаем файл Rakefile. Записываем в него нашу задачу. В моём случае команда для сервера состояла примерно из следующего набора: «Перейти в папку, обновить исходники из scm, собрать джем, sudo установить джем, sudo удалить установленные старые версии джема».

Rakefile

require 'rubygems'
require 'rake'
require "net/ssh"
require 'highline'
...
desc "Update gem on the server by current version on remote origin"
task :deploy do
  Net::SSH.start("myserver", "sudouser") do |ssh|
    channel = ssh.open_channel do |ch|
      ch.request_pty do |c, success|
           # Если pseudo-tty недоступен, то невозможно никакого интерактива
        raise "Cannot obtain pty" unless success
      end

      ch.exec("cd somedir && do_some_stuff && sudo -p 'sudo password: ' do_some_sudo_stuff") do |c, success|
        abort "Could not execute command" unless success

        c.on_data do |c, data|
          if data =~ /sudo password: /
            pwd = HighLine.new.ask("Input remote host sudo password: ") { |q| q.echo = false }
            c.send_data "#{pwd}\n"
          else
            c[:result] ||= ""
            c[:result] << data # Можно, конечно, и в процессе выводить
          end
        end

        c.on_extended_data do |c, type, data|
          puts "STDERR : #{data}"
        end
      end
    end
    ssh.loop # Ожидаем, пока закончится сеанс
    puts channel[:result] # Выводим результат сеанса (можно было и в процессе)
  end
end
...

Теперь вместо всей той последовательности действий достаточно написать запустить rake deploy и ввести пароль.

Материалы для изучения

Пособия по использованию Capistrano
Документация Net::SSH

Комментарии 0 >>

11 марта 2009, 00:27

Использование GEdit для разработки на Ruby и Flex

Темы: ruby, rails, actionscript, xml, flex, ide

Сегодня я хочу поделиться своими ресурсами для разработки. В частности, хочу рассказать про IDE. Основную часть рабочего времени я провожу под кубунту. После долгих метаний и проб я остановился на GEdit, как на универсальном средстве разработки.

Установка

Для работы нам понадобятся плагины для GEdit. Они содержат основной набор вкусностей: code snippets, file browser pane, terminal pane и тому подобное. Поэтому устанавливаем:

sudo apt-get install gedit gedit-plugins

Дополнительные файлы, которые понадобятся, находятся в теле поста.

Ruby и Ruby on Rails

Подсветка синтаксиса и сниппеты для ruby уже поставляются из коробки. Единственное, если вы откроете файл со спецификацией подсветки синтаксиса для ruby, то обнаружите там пометку FIXME, касающуюся подсветки интерполяции внутри строки. Это связано с особенностями обработки правил подсветки самим редактором. Эти особенности удалось обойти, присвоив этому правилу измененный цвет фона.

Подсветку синтаксиса Ruby положить в /usr/share/gtksourceview-2.0/language-specs/
Для корректной работы подсветки, потребуется определить стиль для ruby:interpolation. Я использую тему darkmate, в которой определил необходимые дополнительные цвета на свой вкус. Положить следует в /usr/share/gtksourceview-2.0/styles/, а затем выбрать эту тему в установках редактора.

Язык для описания подсветки синтаксиса достаточно прост. Кроме всего прочего он позволяет ссылаться из правил одного языка на правила другого. Что, собственно, нам и понадобится для подсветки языка темплейтов erb. Ведь erb — это по сути html со вставками ruby. Теперь, когда у нас есть оба описания, берём описание для html и вставляем в него ссылку на ruby.

Так же следует определить mime-type для erb. Правила подсветки erb предполагают, что в системе определен mime-type text/erb. В четвертом KDE описать свои типы файлов можно в System->System Settings->Advanced->File Associacions

Мой файл использует цвет erb:background, который так же определен в файле с темой.

Подсмотрев code snippets для других языков, а так же в процессе использования, вполне можно самостоятельно написать пару-тройку полезных выражений.

Flash и Flex

Для работы с flex нам понадобится flex sdk, а так же отладочная версия standalone flash player. Я уж не буду вдаваться в подробности, как там написать компилирующий или запускающий скрипт. А выложу сразу подсветку синтаксиса.

Если определить тип (mime-type) text/x-shockwave, то можно использовать правила для подсветки ActionScript3. Которые следует положить по адресу, указаному выше.

Так же, используя описанное выше знание, легко создать описание подсветки синтаксиса mxml, зная, что это xml, со вставками actionscript. Для корректной работы следует определить mime-type text/mxml.

Заключение

Все файлы, выложенные в посте, можно смело изменять под свои нужды. Если вы используете GEdit, чтобы писать под рельсы или флекс. Я не стал выкладывать сниппеты, потому что на мой взгляд, это вопрос личной привычки. А подсветку синтаксиса, конечно, можно улучшать.

Комментарии 0 >>

24 февраля 2009, 23:10

Регулярные выражения: радость победы 2 :)

Темы: ruby, regexp, rails

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

Задача

  1. Ввести дополнительный тэг для кода
  2. Печатать код внутри тэга так, как он должен выглядеть (прошлый раз пришлось шаманить)
  3. Иметь возможность более ли менее безнаказанно употреблять сами тэги в тексте

Решение

Итак, для кода будем использовать тэг [code]...[/code]. Для того, чтобы не провоцировать преобразование кода там, где не нужно, будет использовать знак «/». (То есть в этом абзаце жирным на самом деле написано «/[code]...[/code]»)

Так же используем стандартный метод rails для приведения в порядок того, что находится внутри тэга [code]. Результат выглядит вот так:

def lonelyelk_format(text)
  res = "<p>" + text.to_s.dup
  codes = []
  res.gsub!(/[^\/]\[code\]([\s\S]+?)\[\/code\]/) do |s|
    codes.push(s.gsub(/(^[^\/]\[code\]|\[\/code\]$)/, ""))
    "#{s[0,1]}[code#{codes.length - 1} /]"
  end
  res.gsub!(/\r\n?/, "\n")
  res.gsub!(/\n*\[h\]\n*/, "</p><h2>")
  res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>")
  res.gsub!(/\n\n+/, "</p><p>")
  res.gsub!(/\n(?=\[code\d+\s\/\])/, "</p><p>")
  res.gsub!(/(\[code\d+\s\/\])\n/, '\1</p><p>')
  res.gsub!("\n", "<br />")
  res += "</p>"
  res.gsub!(/<p>\[code\d+\s\/\]<\/p>/) do |s|
    "<pre><code>#{h codes[s.gsub(/\D+/, '').to_i]}</code></pre>"
  end
  res.gsub!("<p></p>", "")
  res.gsub!("/[", "[")
  res
end

Итоги

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

Комментарии 0 >>

23 февраля 2009, 17:31

Регулярные выражения: радость победы

Темы: ruby, regexp

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

Задача

    Сделать форматирование текста для блога, чтобы:
  1. Можно было вставлять подзаголовки;
  2. Текст разбивался на параграфы и просто переносы строки;
  3. Со вставками кода ничего не происходило;
  4. Было написано на ruby.


Использовать RedCloth не хотелось, а стандартное форматирование не подходило. Поэтому приступим.

Вытащить код

Для того, чтобы не делать лишних проверок, вытаскиваем код из страницы. Код находится внутри тэга <pre>. Первое, что приходит на ум, это выражение типа «<pre> слева, </pre> справа и ни одного </pre> посередине.». Но оказалось, что исключить выражение невозможно (по крайней мере, я не нашёл способа). Выражение типа

/<pre>[^(<\/pre>)]+<\/pre>/

По крайней мере в ruby, интерпретируется как «тэг <pre>, внутри которого не встревается ни "<", ни "p", ни "r"... и т.д.»

Для этого понадобится концепция «жадности». То есть:

/<pre>.+<\/pre>/

Cоответствует куску от первого «<pre>» до последнего «</pre>». А нам нужно жадное:

/<pre>.+?<\/pre>/

То есть до ближайшего.

Теперь про wild card. Оказалось, что точка не включает перенос строки. Поэтому нам понадобится что-то более дикое. Wild, wild card. На эту роль подходит [\s\S]/: пробельный символ или непробельный.

Итак, вытаскивание кусков кода выглядит так:

codes = []
res.gsub!(/<pre>[\s\S]+?<\/pre>/) do |s|
  codes.push(s)
  "code#{codes.length - 1}"
end

Вокруг кусков кода

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

Что касается «кончился до», то тут используется lookahead (то есть операция при условии, что впереди есть что-то):

res.gsub!(/\n(?=code\d+)/, "</p><p>")

А чтобы начать параграф после куска кода, нам понадобится lookbehind (то есть операция при условии, что перед совпадением есть что-то), который в ruby не работает (по крайней мере в версии 1.8.7). поэтому здесь мы используем группы. И включим группу в результат:

res.gsub!(/(code\d+)\n/, '\1</p><p>')

Видите, вот этот \1?

Остались мелочи: вставить обратно куски кода. Убрать пустые параграфы и параграфы, окружающие куски кода. И вы видите то, что обрабатывает текст этого сообщения.

application_helper.rb:

...
def lonelyelk_format(text)
  res = "<p>" + text.to_s.dup
  codes = []
  res.gsub!(/<pre>[\s\S]+?<\/pre>/) do |s| # вытаскиваем куски кода
    codes.push(s)
    "code#{codes.length - 1}"
  end
  res.gsub!(/\r\n?/, "\n") # приводим перево каретки к одному виду
  res.gsub!(/\n*\[h\]\n*/, "</p><h2>") # заголовки начало [h]
  res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>") # заголовки конец [/h]
  res.gsub!(/\n\n+/, "</p><p>") # более одного переноса строки - параграф
  res.gsub!(/\n(?=code\d+)/, "</p><p>") # параграф перед кодом
  res.gsub!(/(code\d+)\n/, '\1</p><p>') # параграф после кода
  res.gsub!("\n", "<br />") # единичный перенос строки
  res.gsub!(/(<p>)?code\d+(<\/p>)?/) do |s| # вставляем код обратно
    codes[s[4,1].to_i] # здесь ошибка :)
  end
  res.gsub!("<p></p>", "") # убираем пустые параграфы
  res += "</p>"
end
...

Остается одна проблема. Нельзя написать в тексте поста выражение «сode{цифры}». Но для этого просто можно генерировать случайный маркер, которого точно нет в тексте вместо «code».

Обновление

После того, как я попытался написать данный пост, я обнаружил ещё ряд интересных особенностей поведения кода и браузера. А так же нашёл ошибку. Публикую финальный код без пояснений:

application_helper.rb:

...
def lonelyelk_format(text)
  res = "<p>" + text.to_s.dup
  codes = []
  res.gsub!(/<pre><code>[\s\S]+?<\/code><\/pre>/) do |s|
    codes.push(s.gsub(/(^<pre><code>|<\/code><\/pre>$)/, ""))
    "code#{codes.length - 1}"
  end
  res.gsub!(/\r\n?/, "\n")
  res.gsub!(/\n*\[h\]\n*/, "</p><h2>")
  res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>")
  res.gsub!(/\n\n+/, "</p><p>")
  res.gsub!(/\n(?=code\d+)/, "</p><p>")
  res.gsub!(/(code\d+)\n/, '\1</p><p>')
  res.gsub!("\n", "<br />")
  res.gsub!(/(<p>)?code\d+(<\/p>)?/) do |s|
    "<pre><code>" + codes[s.gsub(/\D/, "").to_i].to_s.gsub("<", "&lt;").gsub(">", "&gt;") + "</code></pre>"
  end
  res.gsub!("<p></p>", "")
  res += "</p>"
end
...

Материалы для изучения

http://www.regular-expressions.info/
http://regexp.ru/

Комментарии 3 >>