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, 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 >>