16.05.2009Эволюция алгоритма замены в строке ActionScript
В последнее время моя работа в текущем проекте заключается в отладке, оптимизации и отлове багов. Поэтому в самой работе практически не встречается того, о чём можно было бы здесь написать. Но на помощь пришла гугл-группа ruFlash и комьюнити молодых программистов. :)
Задача
Один из участников попросил составить выражение для удаления из текста ссылок с определенным текстом внутри целиком. Например, в выражении:
var str:String = '<a href="somelink">_some text_</a> ';
str += 'More text! ';
str += '<a href="anotherlink">**remove me**</a> ';
str += '<a href="yetanotherlink"><s>another text</s></a>';
Нужно удалить целиком ссылку, содержащую фразу remove me.
Понятно, что первое приходящее в голову выражение /<a.+?remove me.*?</a>/ захватит две первые ссылки. И «жадность» не поможет, т.к. поиск осуществляется по порядку, и, найдя первый <a, выражение не остановится до самого remove me.
Решение номер один
Поскольку к концу недели отладки и пересмотра одного и того же кода голова моя не была готова что-то изобретать, я последовал пути, предлагавшемуся одним из ответивших, слегка его доделав:
var re0:RegExp = /<a[^>]+>[^a]*remove me.*?<\/a>/g;
trace(str.replace(re0, "!removed!"));
Недостаток его очевиден. Хотя для приведенного примера он работает, но всё-таки, может и отказать, если встретит a между > и remove me. Например:
str += '<a href="anotherlink">eh! ah? **remove me**</a> ';
Решение номер два (рекурсивное)
Поскольку к этому моменту мозг ещё не расслабился окончательно и не готов был отказаться от выбранного способа думать о задаче, второе решение, пришедшее сразу за первым, было значительно сложнее.
Оно использовало возможность подсовывать функцию в качестве аргумента. Вот оно:
var re1:RegExp = /<a(.+?remove me.*?<\/a>)/g;
var replacer1:Function = function():String {
var s:String = arguments[1].toString();
if (s.indexOf("<a") > 0) {
return "<a" + s.replace(re1, replacer1);
} else {
return "!removed!";
}
}
trace(str.replace(re1, replacer1));
Здесь речь идёт о том, чтобы в группе (см. скобки), следующей после <a проверять наличие ещё одного <a. И в случае его наличия запускать ту же процедуру замены, но уже на группе.
Довольный собой, я запостил своё решение в ruFlash и поехал домой. По дороге домой мозг окончательно расслабился, и я смог увидеть задачу в отрыве от способа думать, который я выбрал изначально. И мне пришло
Решение номер три
Зачем городить рекурсию, когда можно просто перебирать все ссылки и заменять (удалять) только те, что нужно?
var re2:RegExp = /<a[^>]+>(.+?)<\/a>/g;
var replacer2:Function = function():String {
var s:String = arguments[1].toString();
if (s.indexOf("remove me") > 0) {
return "!removed!";
} else {
return arguments[0];
}
}
trace(str.replace(re2, replacer2));
Это ли не чудесно?
Выводы
- Решайте задачи.
- Решив (или не решив), записывайте то, что получилось, покажите кому-нибудь. Это позволит выкинуть решение из головы.
- Если есть решение лучше, то оно придет на освободившееся место.
07.05.2009ActionScript: асинхронная замена выражений в строке
Введение
Я снова вернулся к работе над флэшовым проектом. Поэтому немного об ActionScript. Описанная здесь задача сейчас мне не кажется такой сложной, какой она казалась, когда я впервые с ней столкнулся. Но тем не менее.
Задача
Имеется строка, содержащая разметку для замены её составляющих. Одна из разметок: #[some_url] должна быть заменена содержимым этого самого some_url. Для замены с помощью регулярных выражений в ActionScript 3 существует функция String#replace. Но всё, что связано с загрузкой из внешних источников, создает асинхронность. А любая попытка остановить код, сделать паузу, приводит к огромной потере производительности и ошибкам, которые генерирует плеер, когда долго не может завершить вызов. «Как быть?» — спросит меня пытливый читатель.
Решение
Решение состоит в том, чтобы заменять все вхождения ключевого выражения по-очереди и когда всё заменено, создавать событие.
Приведу основную часть. Файл AsyncStringReplaceExample.as
package {
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;
public class AsyncStringReplaceExample {
public static const RE_URL:RegExp = /#\[([^\]]+)\]/g;
private var _str:String;
private var _currentExpr:String;
private var _ldr:URLLoader;
private var _loaded:Boolean;
public function AsyncStringReplaceExample(str:String) {
_str = str;
_ldr = new URLLoader();
_ldr.addEventListener(Event.COMPLETE, ldrCompleteHandler);
_loaded = false;
}
public function replace():Boolean {
_loaded = true;
_str = _str.replace(RE_URL, replaceURL);
return _loaded;
}
public function get string():String {
return _str;
}
private function replaceURL():String {
if (!_currentExpr) {
_loaded = false;
_currentExpr = arguments[0];
_ldr.load(new URLRequest(arguments[1]));
}
return arguments[0];
}
private function ldrCompleteHandler(evt:Event):void {
_str = _str.replace(_currentExpr, evt.target.data);
_currentExpr = null;
if (replace()) {
trace(_str); // здесь желанное событие
}
}
}
}
Теперь остается только использовать написанное нами богатство:
var str:String = "строка для примера\n";
str += "добавим: #[http://some.url/file.txt] или";
str += "ещё добавим: #[http://another.url/another/file.txt]! хватит?";
var asyncString:AsyncStringReplaceExample = new AsyncStringReplaceExample(str);
if (asyncString.replace()) {
trace(asyncString.string); //не нужно ничего заменять
} else {
asyncString.addEventListener(......)
}
...
// внутри обрабочика событий
trace(asyncString.string);
Выводы
Сразу видно, что последнее время я забросил ActionScript и занимался больше Ruby. Потому что подсветка синтаксиса в Ruby красивее. Но это не страшно. :)
Упражнения
Мне было бы интересно написать функцию, которая могла бы работать сколь угодно долго. Например, рекурсивную, или долгую по любой другой причине. Но ActionScript принудительно завершает процессы, которые долго не подают признаков жизни.
Как быть?
30.04.2009Тестирование Paperclip с помощью Factory Girl
Введение
Регулярно в своей работе я встречаюсь с новыми инструментами и решаю разные задачи. Иногда новые решения кажутся незначительными и слишком простыми для того, чтобы посвящать им целый пост в блоге. Но в нашем блоггерском деле главное — это регулярность. :)
Задача
Протестировать вновь изученный инструмент 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)
Выводы
Если вы хотите написать проект за короткое ограниченное время, то лучше всего использовать инструменты, которые вы уже опробовали и знаете.
Если вы хотите изучить новые инструменты, то лучше всего сделать небольшой проектик с их использованием.
Проекты в первом и втором случае значительно отличаются по объему. :)
Материалы для изучения
20.04.2009Тестирование OpenID с помощью Cucumber
Задача
Какое-то время назад решил приобщиться к 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
14.04.2009Фильтрация rss-потоков с помощью Sinatra и HTTParty
Задача
Для фильтрации 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. Но это, я думаю, всем под силу.
Материалы для изучения
28.03.2009Автосохранение с помощью JavaScript
О стандартизации
Ещё недавно я говорил о том, что не очень люблю 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 для создания «бесконечной» страницы
24.03.2009Jabber бот
Возникла идея написать 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 находится собственно то, что будет делать ваш бот, как он будет отвечать на то, что написали ему.
Материалы для изучения
16.03.2009Автоматизация процесса публикации
Когда я впервые прочитал про, например, 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 и ввести пароль.
Материалы для изучения
11.03.2009Использование GEdit для разработки на Ruby и Flex
Сегодня я хочу поделиться своими ресурсами для разработки. В частности, хочу рассказать про 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, чтобы писать под рельсы или флекс. Я не стал выкладывать сниппеты, потому что на мой взгляд, это вопрос личной привычки. А подсветку синтаксиса, конечно, можно улучшать.
02.03.2009Пространства имён XML в ActionScript 3
В последнее время достаточно плотно сталкивался с обработкой XML в ActionScript 3. В третьей версии ActionScript для обработки XML реализована спецификация ECMAScript for XML (E4X). Это значительно упрощает работу. Обращение к дочерним элементам происходит так же, как обращение к свойствам объекта. Об этом я и хочу рассказать в этот раз, взяв для примера что-то более сложное, чем в стандартный примерах.
var xml:XML =
<robot xmlns="http://robots.org/2069/body/"
xmlns:color="http://robots.org/2075/bodycolor"
xmlns:extra="http://extraparts.org/2080/xml"
xmlns:uni="http://universal.org/2010/robots/xml">
<body>
<head>
<brain uni:id="abc123456" />
<brain uni:id="abc987654" />
<casing color:rgb="cfa499" />
</head>
<extra:additional name="mono">
<extra:arm>
<extra:claw extra:size="12" color:pantone="134f" />
</extra:arm>
</extra:additional>
</body>
</robot>;
То есть в данном примере присутствует четыре спецификации, которые описывают различные (или возможные) элементы или атрибуты XML. Что же будет, если мы попробуем что-нибудь поискать:
trace(xml.body.length()); // 0
Это произошло потому, что сам XML имеет своё пространство имён (namespace). Поэтому можно сделать так:
trace(xml.body.length()); // 0
var mainNS:Namespace = new Namespace("http://robots.org/2069/body/");
trace(xml.mainNS::body.length()); // 1
default xml namespace = mainNS;
trace(xml.body.length()); // 1
Остальные пространства имён:
var colorNS:Namespace = new Namespace("color", "http://robots.org/2075/bodycolor");
var extraNS:Namespace = new Namespace("extra", "http://extraparts.org/2080/xml");
var uniNS:Namespace = new Namespace("uni", "http://universal.org/2010/robots/xml");
Объект, возвращаемый при выборке имеет тип XMLList. Но не смотря на это, дальнейшую выборку можно продолжать. Так же для примера рассмотрим обращение к атрибутам:
trace(xml.body.head.casing.@colorNS::rgb); // cfa499
В случае, когда элементы не в единственном числе и для большей строгости то же самое можно записать следующим образом.
trace(xml.body[0].head[0].casing[0].@colorNS::rgb[0]); // cfa499
Кстати, обращение к атрибуту так же возвращает XMLList. Поиск по атрибуту, если нам не особо важна структура, будет выглядеть следующим образом:
trace(xml..mainNS::brain.(@uniNS::id.indexOf("abc") >= 0).@uniNS::id.toXMLString());
//abc123456
//abc987654
То есть после первой выборки по частичному вхождению id вышло два элемента. Когда мы запросили атрибут, оказалось два атрибута. То есть строгое выражение, если нам нужет второй элемент, будет такое:
trace(xml..mainNS::brain.(@uniNS::id.indexOf("abc") >= 0)[1].@uniNS::id);
//abc987654
Естественно, что XMLList может быть использован для образования цикла с помощью for .. in.
Ещё один способ обращаться к элементам и атрибутам — это с помощью методов класса XML и объекта QName. Это прекрасный способ, если не нужно «хардкодить» имена элементов. Ещё это может пригодиться, если элементы xml имеют имена, совпадающие со служебными или ключевыми словами ActionScript.
var claw:QName = new QName(extraNS, "claw");
var pantone:QName = new QName(colorNS, "pantone");
trace(xml.descendants(claw)[0].attribute(pantone)); // 134f
Материалы для изучения
Документация класса XML со ссылками Руководство пользователя на тему XML в ActionScript 3 Для хардкорных читателей