05 ноября 2010, 10:18
Темы: ruby, rails, syntax, deploy, automation

Введение
Это уже давно известная тема, и я не претендую на открытие Америки, но для себя зафиксирую это знание.
Даже если вы используете капистрано для выкладывания проекта в сеть, логи приложения хранятся в одном и том же месте (папка 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
}
Здесь написано:- weekly разбивать лог еженедельно;
- missingok не выходить с ошибкой, если файла нет;
- rotate 10 хранить 10 предыдущих томов;
- nomail не высылать удаляемые тома на электронную почту;
- compress архивировать;
- delaycompress архивировать не сразу, т.к. после переименования файла и до перезапуска пэссенджера логи пишутся в тот же переименованный файл;
- sharedscripts запускать скрипт один раз для всех логов по маске;
- 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. Или можно просто перезапускать апач.
Материалы для самостоятельного изучения
- Документация logrotate (по-русски)
- Что ещё можно делать с логами приложения на рельсах
17 июня 2010, 11:36
Темы: git, syntax, automation

Введение
Моя любимая система контроля версий имеет огромное количество инструментов. Как-то раз я участвовал в опросе, после которого выяснилось, что даже из самых популярных инструментов я использую от силы 10%.
Но иногда возникают ситуации, единственно продуктивным выходом из которых бывает изучение и использование нового для себя инструмента. О двух таких случаях я сегодня и расскажу.
Внезапные просьбы: git stash
Бывает так, что пока я работаю над нововведениями в программу, текущая стабильная её версия активно используется. При активном использовании, конечно же, могут возникнуть ошибки или пожелания что-то изменить. Бывает так, что при этом я нахожусь в середине тестирования какого-то новшества, и всё настолько сыро, что я даже не могу сделать коммит.
Итак, я нахожусь в середине правок на ветке extremely_experimental, а мне необходимо внести правки в ветку master. Вот, как это делается:
git stash save
git checkout master
После первой команды всё наши изменения, которые нельзя было закоммитить, сохранены и текущая ветка приведена в состояние до правок. После этого мы можем сменить ветку и внести наши правки. После того, как ошибки исправлены, нововведения сделаны и тесты проходят, мы можем вернуться обратно к нашим правкам.
Но скорее всего все или некоторые из сделанных изменений понадобятся нам в нашей экспериментальной ветке. После перехода на неё:
git checkout extremely_experimental
Если нам нужны все изменения, то:
git merge master
Если только некоторые, то:
git cherry-pick ...
После этого вернём наши правки:
git stash pop
Если возникли конфликты, то правим их и делаем:
git stash drop
git reset --mixed
Последнее нужно для того, чтобы вынести наши правки из индекса, т.к. при конфликте они не выходят оттуда самостоятельно.
Конечно же таких незавершённых правок может быть несколько, но это я оставлю на самостоятельное изучение пытливому читателю.
Неизвестно, когда сломалось: git bisect
Бывает так, что вдруг обнаруживается ошибка, про которую точно известно, что давным давно её не было. Так бывает в больших проектах, в непокрытых тестами областях. Бывает так, что обнаружить, в чём же дело, быстро не удаётся.
Хорошая новость в том, что это и не обязательно. Нужно просто начать процесс:
git bisect start
git bisect bad
Так мы обозначили, что текущий коммит содержит ошибку. После этого, либо мы знаем, как называется коммит, в котором ошибки ещё не было, или находим его.
git bisect good v2.3.1
или
git checkout ...
git bisect good
После этого за нас всё будет делать git. Он будет перемещать нас по истории, а мы будем проверять, есть эта ошибка или нет, и сообщать об этом:
git bisect good
или
git bisect bad
В конце концов нам сообщат, какой именно коммит всё поломал. Название инструмента подсказывает нам, что на тестирование нам всегда предоставляется коммит, который находится посередине между плохим и хорошим. Таким образом, мы просматриваем не все N коммитов в истории ошибки, а всего лишь log2N.
После того, как мы выяснили, в чём причина, убрать следы, которые оставил после себя git bisect можно так:
git bisect reset
А какими инструментами git пользуетесь вы?
Материалы для самостоятельного изучения
- Документация по git
- git stash
- git bisect
01 июня 2010, 23:40
Темы: ruby, git, automation, air, mistakes, syntax

Введение
Основной целью этого блога является сбор в одном удобном месте необходимых мне по работе знаний и фишек. Однако, именно потому что это активно используемые в работе решения, со временем появляется более продуктивный или более правильный способ сделать то, о чём написано почти в каждой статье.
Иногда я просто ошибаюсь. Трудно представить что-то более полезное для опыта, нежели набивание шишек. Будет хорошо, если проведение работ над ошибками станет доброй традицией. Итак, в этом году.
git hooks
Недостатков скрипта для удаления пробелов в концах строк нашёл два:
- Скрипт без нужды дёргает ни в чём не повинные файлы, потому что \s соответствует и символу конца строки, который там всегда есть.
- Скрипт не содержит решения для выбора всех текстовых файлов проекта.
Вот хороший скрипт:
#!/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 пропала, потому что его теперь нет :)
26 мая 2010, 21:42
Темы: ruby, automation, git

Введение
Как-то раз мне попался очень злой 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
28 апреля 2010, 15:21
Темы: tdd, actionscript, ruby, flex, automation, air

Введение
Уже некоторое время назад обнаружил гениальный инструмент. Правда только недавно опробовал его на своих рабочих проектах и зафанател ещё больше. Подготовка к докладу на секции Яндекса про панорамы на РИФе не позволила мне поделиться этим ранее. Исправляю ошибку.
Я люблю руби. И, естественно, 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
И это не предел!
Материалы для самостоятельного изучения
- Полный код статьи на github
- Инструкция по работе с airake, которая во многом повторена в этой статье с добавлением манипуляций, чтобы всё заработало.
- Документация по FlexUnit. Не уверен, что в поставке airake идёт самая последняя версия, но ничего не мешает написать rake task для обновления версии FlexUnit :)
- Документация по rake
23 сентября 2009, 15:59
Темы: 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, 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