Archive for the ‘программерское’ Category

Обработка ActionController::RoutingError в Rails 3

Среда, Март 2nd, 2011

Раньше, до третьих рельс, исключение маршрутизатора можно было красиво и спокойно перехватить, чтобы показать страницу с ошибкой 404. Достаточно было поместить в ApplicationController следующее:

rescue_from ActionController::RoutingError, :with => :error_not_found

Ну, и определить метод error_not_found, который бы рендерил страницу с 404 ошибкой. Но в Rails 3 произошло изменение - стал использоваться гем rack-mount, который используется в Rails как middleware, то есть как прослойка между вашим приложением и веб-сервером. И исключение RoutingError переехало именно в middleware, так что теперь его не получится отловить в контроллере, так как это исключение отлавливается и рендерится еще до вызова контроллера.

Разработчик Rails, а именно Хосе Валим, назначил этой проблеме статус low и поместил тикет в milestone 3.1, что означает, что решение разработчики родят только в версии Rails 3.1. На момент написания этой статьи текущая версия - 3.0.5, так что ждать еще достаточно долго. Сейчас разработчики думают над тем, как сделать обработку ошибок маршрутизации по-нормальному. Но Хосе предложил временное решение - использовать globbing в маршрутах. Нужно просто в вашем routes.rb в самом конце прописать это:

match "*path", :to => "application#error_not_found"

И добавить метод home#routing_error по вашему вкусу. Мой таков:


class ApplicationController < ActionController::Base
  def error_not_found
    render :template => "/error/404.html.erb", :status => 404
  end
end

Остальные эксепшены, вроде ActionController::UnknownController перехватываются нормально при помощи rescue_from и их можно преспокойно обрабатывать в ApplicationController, например.

Да, еще есть вот такое решение. С использованием гема vidibus-routing_error можно работать с исключением RoutingError как прежде, через rescue_from.

Уведомлялка о новых твитах для Ubuntu на Ruby

Пятница, Декабрь 24th, 2010

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

require 'twitter'
last_created_at = 0

Twitter.configure do |config|
  config.consumer_key       = 'your key here'
  config.consumer_secret    = 'your key here'
  config.oauth_token        = 'your token here'
  config.oauth_token_secret = 'your token here'
end

client = Twitter::Client.new

loop do
  tweet = client.home_timeline.first

  if DateTime.parse(tweet.created_at) > last_created_at
    system("notify-send -u normal -t 5000 -i info '#{tweet.user.screen_name}' '#{tweet.text}'")
    last_created_at = DateTime.parse(tweet.created_at)
  end

  sleep 60
end

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

Как это юзать? Для начала нужно установить:

  • sudo apt-get install libnotify-bin
  • gem install twitter
  • Зарегистрировать свое приложение тут http://twitter.com/apps/new и вставить в скрипт в конфигурацию полученные ключи и токены

Дальше сохранить предоставленный код в файл и запустить его из консоли:

$ ruby twitter.rb&

Результат выглядит так:
2iho

UPD 13.01.2011

Кирилл Никитин прислал мне исправленный вариант, который я реквестовал выше, за что ему решпект.

require 'twitter'

last_id = nil
tweets = []

Twitter.configure do |config|
  config.consumer_key       = 'your key here'
  config.consumer_secret    = 'your key here'
  config.oauth_token        = 'your token here'
  config.oauth_token_secret = 'your token here'
end

client = Twitter::Client.new

loop do
  if last_id
    tweets = client.home_timeline :since_id => last_id
  else
    tweets << client.home_timeline.first
  end

  last_id = tweets.first.id unless tweets.empty?

  tweets.reverse.each do |tweet|
    system("notify-send -u normal -t 5000 -i info '#{tweet.user.screen_name}' '#{tweet.text}'")
  end

  tweets.clear
  sleep 60
end

Jabber via Websockets: создаём прокси-сервер

Понедельник, Ноябрь 1st, 2010

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

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

Для тестов я поставил jabber-сервер ejabberd на локальную машину, хотя можно было воспользоваться и многочисленными узлами, типа jabber.ru. Также я поставил клиент Psi и зарегистрировался при помощи него на локальном сервере.

Итак. Давайте для начала создадим html5 документ, который будет посылать запрос на наш jabber-прокси-сервер:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Jabber Websocket Proxy Test</title>
    <script src="https://ajax.googleapis.com/ajax/libs/mootools/1.3.0/mootools-yui-compressed.js"></script>
  </head>
  <body>
<script>
var socket = new WebSocket('ws://localhost:6222/');
socket.onopen = function() {
  var xml  = "<?xml version='1.0' encoding='UTF-8'?>"
      xml += "<stream:stream to='localhost' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' xml:lang='ru' version='1.0'>";
  socket.send(xml);
  socket.onmessage = function(e) {
    var data = e.data
    alert(data)
  }
}
</script>
  </body>
</html>

Как видно из примера, мы будем посылать кусок xml на сервер (начало xml-потока), что означает, что мы готовы авторизоваться на сервере. В ответ jabber-сервер должен нам вернуть некий ответ, в данном случае - список поддерживаемых механизмов аутентификации на сервере. Мы просто выведем полученный с сервера ответ при помощи js-функции alert() на экран. Этого нам будет достаточно,чтобы убедиться, что наш прокси работает.

Теперь приведу код jabber-via-websocket сервера на Ruby:

require 'em-websocket'
options = {
  :port        => 6222,
  :remote_host => 'localhost',
  :remote_port => 5222
}
EventMachine.run {
  class Server < EventMachine::Connection
      def initialize(input, output, server_close, client_close)
        @input        = input
        @output       = output
        @server_close = server_close
        @client_close = client_close
        @input_sid = @input.subscribe { |msg| send_data msg }
        @client_close_sid = @client_close.subscribe { |msg| close_connection }
      end
      def receive_data(data)
        @output.push(data)
      end
      def unbind
        @input.unsubscribe(@input_sid)
        @client_close.unsubscribe(@client_close_sid)
      end
  end

  EventMachine::WebSocket.start(:host => "0.0.0.0", :port => options[:port]) do |ws|
    ws.onopen {
      output       = EM::Channel.new
      input        = EM::Channel.new
      server_close = EM::Channel.new
      client_close = EM::Channel.new
      output_sid = output.subscribe { |msg| ws.send msg }
      server_close_sid = server_close.subscribe { |msg| ws.close_connection }
      EventMachine::connect options[:remote_host], options[:remote_port], Server, input, output, server_close, client_close
      ws.onmessage { |msg| input.push(msg)}
      ws.onclose {
        output.unsubscribe(output_sid)
        server_close.unsubscribe(server_close_sid)
      }
    }
  end
}

Не буду подробно описывать код. Скажу лишь, что он перенаправляет все поступившие запросы с 6222 порта на порт 5222 (стандартный порт jabber).
Остается запустить из консоли наш прокси-сервер (ruby proxy.rb) и html5 пример в google chrome и убедиться, что все работает. Мы должны увидеть алерт со следующим содержимым:

<?xml version='1.0'?>
<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' id='749528783' from='localhost' version='1.0' xml:lang='en'>
    <stream:features>
        <mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
            <mechanism>PLAIN</mechanism>
            <mechanism>DIGEST-MD5</mechanism>
        </mechanisms>
        <c xmlns='http://jabber.org/protocol/caps' hash='sha-1' node='http://www.process-one.net/en/ejabberd/' ver='8P/XuMtKq0lNk50DLBC8v+TXoAU='/>
        <register xmlns='http://jabber.org/features/iq-register'/>
    </stream:features>

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

Параллельная обработка запросов в Ruby

Пятница, Октябрь 22nd, 2010

В прошлой статье мы рассмотрели создание простейшего веб-сервера и обработку GET-запросов.

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

Что такое треды?
Треды - это программные потоки, которые выполняются внутри одного процесса, они обеспечивают параллельное выполнение кода. Это очень важно. Именно потому что треды выполняются внутри одного процесса, расходуется меньше памяти, да и создать новый тред и переключиться в него быстрее, чем создать новый процесс. Поэтому, все современные веб-серверы и другие серверы используют в своей работе треды.
Возможность создавать треды предоставляет нам операционная система, поэтому, нужен низкоуровневый язык (например C), чтобы работать с тредами. Однако, Ruby предоставляет нам интерфейс к тредам, поэтому, мы можем использовать их и в Ruby.

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

require 'socket'
server = TCPServer.open('localhost', 8000)

loop do
  # Как только сервер принимает соединение с клиентом,
  # тут же создается новый тред. И так будет для каждого
  # соединения с веб-сервером. Каждому соединению - отдельный тред.
  Thread.start(server.accept) do |client|
    client_headers = []

    while line = client.gets
      client_headers << line
      break if line == "\r\n"
    end

    client.print "HTTP/1.1 200/OK\n"
    client.print "Content-type: text/html\n\n"
    client.print client_headers.join("<br/>")
    client.close
  end
end

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

Я написал веб-сервер, который ничего не умеет

Четверг, Октябрь 21st, 2010

И, спрашивается, зачем?

А затем, чтобы рассказать на примере, как обрабатываются GET-запросы. Пока что мой сервер ничего не умеет, кроме обработки GET-запросов и вывода информации о клиенте, он даже не читает никакие файлы с диска. Его задача другая, показать, как происходит обработка запросов. Я это сделал потому, что как оказалось, далеко не все веб-разработчики понимают, как это происходит. Мне вот повезло, давным давно приходилось разбирать аж multipart-запросы и обрабатывать заголовки, приходящие на сервер - в бытность, когда писал свою библиотеку на Perl.

Но сейчас я сделал пример на Ruby и не поймет его только баран (или обезьяна). Я постарался максимально документировать его, чтобы было все понятно.

require 'socket' # Из стандартной библиотеки Ruby

# Открываем TCP сокет (Это можно было сделать и по-другому, но я решил
# не создавать сокет вручную, а воспользоваться более высоким уровнем).
# Как видно, сокет будет ожидать соединения (т.к. он является сервером) на
# localhost на 8000-м порту.
server = TCPServer.open('localhost', 8000)

# Бесконечный цикл.
# После того, как соединение от клиента будет принято - клиенту
# будет возвращен ответ и соединение закроется - программа перейдет
# в начало цикла, чтобы ожидать новый запрос
loop do
  # Принимаем входящее соединение клиента. Пока соединение не будет принято,
  # сокет будет ожидать его, и код ниже этой строки
  # не будет выполняться.
  client = server.accept
  client_headers = []

  # Получаем данные клиента построчно
  while line = client.gets
    client_headers << line

    # Протокол HTTP предписывает \r\n как
    # перевод строки — всегда, независимо от операционной системы.
    # Поэтому, тело запроса нам не нужно (т.к. мы обрабатываем
    # только GET-запросы. Получить тело нам понадобилось бы
    # только в POST-запросе, а в GET-запросе тела просто-напросто нет).
    break if line == "\r\n"
  end

  # Поспим 10 секунд. Я сделал это для того, чтобы проиллюстрировать кое что.
  # Попробуйте одновременно обратиться в 2-х закладках браузера к веб-серверу и
  # вы увидите, что второй запрос будет происходить в два раза дольше
  # первого, т.к. он будет дожидаться, пока обработается первый запрос. Чтобы запросы
  # обрабатывались параллельно (т.е. не стояли в очереди) - можно сделать
  # несколько тредов (воркеров).
  sleep 10

  # Возвращаем ответ клиенту и закрываем соединение. В виде тела ответа
  # показываем клиенту его же заголовки.
  client.print "HTTP/1.1 200/OK\n"
  client.print "Content-type: text/html\n\n"
  client.print client_headers.join "<br/>"
  client.close
end

Я создал TCP-сервер при помощи объекта TCPServer, но можно было сделать и на чистом объекте Socket. Просто TCPServer, который наследуется от TCPSocket, а тот, в свою очередь, от Socket - уже настроен для того, чтобы быть сервером, то есть он умеет висеть на определенном хосте и порту и ожидать входящие соединения.

Из кода видно, что:

  • Веб-сервер обрабатывает только GET-запрос
  • Он не читает никаких файлов с диска, а просто отображает информацию о клиенте, который к нему присоединился
  • Всегда отвечает со статусом 200

Можно сохранить код в файл server.rb, а затем запустить его:

ruby server.rb

Озвучивание интерфейсов в HTML5, мысли

Понедельник, Октябрь 4th, 2010

Я тут подумал давеча, что связи с появлением Audio Data API в HTML5, можно будет делать очень клевые штуки. Только представьте: различные меню, всплывающие окна, accordion’ы, notices - все это можно сопровождать различными звуками! Блин, как же классно будет :)

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

Прекрасные контроллеры в Rails 3

Пятница, Сентябрь 3rd, 2010

Зацените, кто еще не видел, насколько меньше кода стало в контроллерах Rails 3! Теперь можно сказать, что контроллеры стали по-настоящему соответствующими принципу DRY. И всё это благодаря респондерам. В общем, мне нравится:

class ProductsController < ApplicationController
  respond_to :html, :xml  

  def index
    respond_with(@products = Product.all)
  end  

  def show
    respond_with(@product = Product.find(params[:id]))
  end  

  def new
    respond_with(@product = Product.new)
  end  

  def create
    @product = Product.new(params[:product])
    flash[:notice] = "Successfully created product." if @product.save
    respond_with(@product)
  end  

  def edit
    respond_with(@product = Product.find(params[:id]))
  end  

  def update
    @product = Product.find(params[:id])
    flash[:notice] = "Successfully updated product."
      if @product.update_attributes(params[:product])

    respond_with(@product)
  end  

  def destroy
    @product = Product.find(params[:id])
    @product.destroy
    flash[:notice] = "Successfully destroyed product."
    respond_with(@product)
  end
end  

В респондерах теперь содержится вся та магия по отдаче ответа клиенту, которая раньше находилась в контроллере, в блоке, передаваемому методу respond_to. Сейчас стало удобно контролировать отдачу типов контента - ну нужно править каждый метод контроллера, если вы захотите добавить или убрать тип контента. Кстати, метод respond_to поддерживает ключи :only и :except, чтобы контролировать типы контента для методов.
Самое главное - это все работает, и работает замечательно! Я сейчас пишу проект на Rails 3 и использую всю эту красоту. В общем - радости нет предела)

Рекомендую почитать вот эту статейку на английском: Controllers in Rails 3

Installing Ruby 1.9.2 and Rails 3 stable on Ubuntu

Вторник, Август 31st, 2010

Let’s install Ruby 1.9.2 and Rails 3 stable on Ubuntu. I’m going to use just one Ruby version so, this installation without RVM (Ruby Version Manager). I’m using Ubuntu 10.04, 32 bit version.

If you have not yet installed the following packages - install them:

$ sudo apt-get install gcc g++ build-essential libssl-dev libreadline5-dev zlib1g-dev linux-headers-generic libsqlite3-dev

Now download Ruby 1.9.2 sources, unpack them and install:

$ wget ftp://ftp.ruby-lang.org//pub/ruby/1.9/ruby-1.9.2-p0.tar.gz
$ tar -xvzf ruby-1.9.2-p0.tar.gz
$ cd ruby-1.9.2-p0/
$ ./configure --prefix=/usr/local/ruby
$ make && sudo make install

Add path to binary Ruby files.

$ sudo gedit /etc/environment

You need to add in the PATH variable that path - /usr/local/ruby/bin, should look something like this:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/ruby/bin"

Then run the source command for the file /etc/environment to apply changes.

$ source /etc/environment

Now check is Ruby installed properly:

$ ruby -v

You should see something like this: ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]
Now create a symbolic link for ruby and gem program

$ sudo ln -s /usr/local/ruby/bin/ruby /usr/local/bin/ruby
$ sudo ln -s /usr/local/ruby/bin/gem /usr/bin/gem

Ruby 1.9.2 is already includes Rubygems, so you do not have to install it.
Now install the required gem packages, including Rails 3.:

$ sudo gem install tzinfo builder memcache-client rack rack-test erubis mail text-format bundler thor i18n sqlite3-ruby
$ sudo gem install rack-mount --version=0.4.0
$ sudo gem install rails --version 3.0.0

Check Rails version:

$ rails -v

You should see the version number 3.0.0. Otherwise, try to execute command source /etc/environment and enter rails -v command once again.
Now you are ready to create a new Rails 3 application:

$ rails new myproject
cd myproject
rails server

UP: To update rails to latest version (3.0.3 for now), run:

sudo gem update rails

And change rails gem in your Gemfile to gem ‘rails’, ‘3.0.3′

К вопросу об эффективности

Пятница, Август 27th, 2010

Привет.
Сегодня моему коллеге понадобилось внести количество населения в городах России в его табличку cities в БД. Он, конечно, начал писать парсер Википедии, дабы добыть нужную информацию из страниц городов на сайте. То есть, он хотел обращаться к Википедии примерно так:

http://ru.wikipedia.org/wiki/<название города>

Он просто обращался по этому адресу при помощи php-функции file_get_contents(), но ему возвращалась 403 ошибка. Я обратился телнетом к странице и узнал, что Википедия требует присутствие заголовка User-Agent в HTTP-запросе.
Показал коллеге как юзать telnet и формировать HTTP-запрос =) Этим убедил его в том, что ему нужно юзать cURL. И он принялся писать свой мега-парсер.

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

Для начала я нашел HTML-табличку со списком городов и населением в них. Потом я скопировал эту табличку при помощи firebug в отдельный HTML-файл, подключил туда JQuery с Google CDN, и написал абсолютно тупой джаваскрипт, который обходил табличку и забирал оттуда нужные данные, а потом писал их в лог при помощи console.log(). Знаю, что можно было выполнить свой JS прямо на странице Википедии, но не знаю почему так не сделал :). Дальше осталось только передать эти данные коллеге.
Я потратил на это всего минут 5, мой коллега потратил бы на это половину рабочего дня, т.к. парсинг HTML-страниц в PHP это дело непростое, там нет Mechanize, к тому же Википедия может забанить по IP при парсинге кучи страниц :).

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

Установка Ruby и RoR на Ubuntu 10.04

Среда, Август 18th, 2010

Если вы используете Ubuntu 10.04 или 9.10, то следующие шаги по установке будут одинаковы для той и другой версии ОС. Чтобы установить Ruby on Rails, сначала вам нужно установить некоторые примочки, а так же сам Ruby.

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

$ sudo apt-get install ruby-full build-essential

Если же вы хотите установить из исходников, то сначала нужно установить следующие пакеты:

$ sudo apt-get install build-essential libssl-dev libreadline5-dev zlib1g-dev

Теперь вам нужно скачать архив с исходными кодами Ruby:

$ wget ftp://ftp.ruby-lang.org/pub/ruby/stable-snapshot.tar.gz

Разархивируем файлы:

tar xzf stable-snapshot.tar.gz

Компилируем и устанавливаем Ruby:

$ cd ruby/
$ ./configure
$ make
$ sudo make install

Теперь запустите в консоли команду irb, и если вы не увидите никаких ошибок - поздравляю, Ruby установился успешно.

Перед тем, как установить Rails, вам нужно установить некоторые пакеты gem. Gem-пакеты - это упакованные приложения или библиотеки Ruby. Вы можете использовать команду gem чтобы установить различные бесплатные библиотеки, включая и Rails. Подробнее о rubygems можно почитать в мануале.

Устанавливаем rubygems следующей командой:

$ wget http://rubyforge.org/frs/download.php/45905/rubygems-1.3.2.tgz
$ tar xzvf rubygems-1.3.2.tgz
$ cd rubygems-1.3.2
$ sudo ruby setup.rb
$ sudo ln -s /usr/bin/gem1.8 /usr/bin/gem

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

$ sudo gem install rails

Эта команда установит последнюю версию Rails, но если вы хотите установить одну из предыдущих версий, например 2.2.2, то воспользуйтесь следующей командой:

$ sudo gem install –version = 2.2.2 rails