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"
Здесь стоит сделать два акцента:- Для сообщения приложению, что оно запущено в консоли, мы использовали волшебный хэш ENV. К нему потом можно обратиться внутри config/environment.rb и сделать что-то по-другому.
- Для запуска консоли мы использовали 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" # куда сохранять
Материалы для самостоятельного изучения
- Подробная документация irb;
- Занятный способ получить irb-консоль прямо в запущенном работающем приложении.
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: Неведомая ошибка>
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Ошибка!
------------------------
Что мы отсюда почерпнули:- Блок else (необязательный) выполняется в случае удачно выполненного begin и возвращает значение вместо него.
- Выполняется первый из блоков rescue, который соответствует ошибке
- Метод raise без параметров выбрасывает текущее исключение: то, которое находится в глобальной переменной $!
- Чтобы создать свой класс исключений, наследовать нужно от 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 нас не спасут.
Для самостоятельного изучения
- Вопросы-задачи-загадки, на которые полезно ответить не пиша программы
- Иерархия классов исключений в руби
- Более подробно про азы, в которые я не стал здесь спускаться
13 сентября 2009, 18:20
День программиста
Темы: notice
Поздравляю
Дорогие коллеги! Поздравляю вас с Днём программиста! Желаю прежде всего большого интереса в работе, прозрачного кода и быстрой отладки. :)
Бонус
Сегодня я получил предложение о теме следующей записи. В связи с этим была сделана небольшая, но приятная модификация.
С сегодняшнего дня в блоге появилась ворма обратной связи. Оранжевая, такая, кнопка слева. Через неё можно предлагать темы, о которых было бы интересно узнать. А так же голосовать за уже предложенные. Ура!
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 в части запуска и остановки скриптов
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 и фильтровать результат.
Следует знать два момента:- Введенная строка, после которой была нажата табуляция, находится в ENV["COMP_LINE"].
- В задачах могут попасться пространства имён. Поэтому из окончательного результата нужно убрать часть введенной строки, содержащей двоеточие.
Элементарная реализация выглядит следующим образом:
#!/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.