27 апреля 2016, 23:24

Стрелки для krpano на несущетсвующем языке

Темы: javascript, xml, syntax, antique

Когда-то давно, когда мы начинали делать панорамы для Яндекса, я написал для них плеер на флэше. Он потом был им полностью с потрохами передан на поддержку и значительно доработан. А сейчас уже, кажется, сдан в утиль. На смену флэшу приходит html5. И в прочих проектах, кроме наших внутренних, старый плеер тоже уже не используется.

Новый плеер написать пока так и не доходят руки, хотя, возможно, и придётся это сделать. Поэтому мы пользуемся уже готовыми продуктами. Например, плеер krpano. О моей с ним работе и хочу рассказать. Вот, где мой интерес к разным языкам сыграл свою роль.

Стрелки

Клаус разрабатывал свой плеер на стыке эпох, и поэтому он у него поддерживает как флэш, так и html5. По сути же это два плеера, которые работают с одним набором данных. Для панорамы обычно это изображения и некоторое описание, как их друг к другу приладить и что разместить поверх. В данном случае — это файл xml, спецификация которого довольно хорошо документирована.

Mayalanguage

Передо мной встала задача создать стрелки как в панорамах улиц. Математика у них не очень сложная, а для реализации я решил использовать встроенные в плеер хотспоты. Они позволяют рисовать многоугольники любой формы поверх панорамы.

Поскольку кроме фотографий плеер использует только xml, то автору пришлось придумать свой язык программирования, который бы позволял программировать внутри xml. Можно, конечно, вызывать функции джаваскрипта, но во-первых, это тормозит флэшовую версию, а во-вторых, в чём же тут интерес?

Язык напомнил мне ассемблер, хотя мне не довелось на нём много программировать. По сути это польская нотация, для которой проще всего писать интерпретатор. При этом отсутствует вложенность операций. Функции не возвращают значения (кроме одной), а изменяют аргументы. И самое главное не запутаться, где нужна переменная, а где её значение.

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

Сжатие

Для тестирования я решил использовать микро-сервер node.js, а вместе с ним инструмент для исполнения задач gulp. Одна такая задача — убирать лишние пробелы в придуманном скриптовом языке krpano. Не столько для обфускации или экономии трафика, сколько опять же для интереса.

Для сжатия xml я нашёл pretty-data, а недостающий кусок дописал:

var gulp = require('gulp'),
	prettyData = require('gulp-pretty-data');

// Minify krpano action
function minifyAction() {
	function dry (file, cb) {
		file.contents = new Buffer(String(file.contents).replace(/(<action.+?>)([\s\S]+?)(<\/action>)/ig, function (str, opentag, cnt, closetag) {
			return opentag + cnt.replace(/\s*(^|[;,=!])\s*/g, "$1") + closetag;
		}));
		cb(null, file);
	}

	return require('event-stream').map(dry);
}

// Minify plugin xml
gulp.task('xml', function () {
	return gulp.src('dev/arrows.xml')
		.pipe(prettyData({type: 'minify'}))
		.pipe(minifyAction())
		.pipe(gulp.dest('arrows/example/'));
});

В общем, если вдруг вы используете krpano, то милости прошу за моими стрелочками. Для них мне тоже пришлось изобрести, как внутри xml задавать форму и поведение опорной точки (см. пример на странице плагина).

Ссылки

  1. Виртуальный тур по Арктике с моими стрелками.
  2. Репозиторий krpano-arrows на гитхабе.
  3. Страница плагина на krpano.com.

Комментарии 0 >>

14 апреля 2016, 15:27

Автоматическое монтирование папки NFS

Темы: bash, automation, script, syntax, security

Источник вдохновения

Кроме рабочих процессов и случайного вдохновения, самым надёжным источником программерских задач является повторение. Одно из самых ярких чувств удовлетворения наступает от того, что не нужно делать то, что вынужден был делать до этого сто раз.

В работе мы используем сетевые хранилища данных. Они не так гибки в настройке, как обычные компьютеры, но работать можно. Для монтирования дисков на рабочие машины мы используем протокол NFS. И он, вместе с этими устройствами, доставляет множество мелких проблем.

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

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

DifferentButNotMyProblem

Имя, сестра!

После выполнения одинаковых инструкций — настройка адреса, создание папки c именем, например, storage, предоставление к ней доступа по NFS — на хранилищах разных марок оказываются доступны разные папки. Тут нам на помощь приходит showmount:

$ showmount -e 192.168.4.50
Exports list on 192.168.4.50:
/storage
/homes
/Web
/Usb
/Recordings
/Public
/Network Recycle Bin 1
/Multimedia
/Download

$ showmount -e 192.168.4.60
Exports list on 192.168.4.60:
/volume1/storage   *

Мы бы могли на первом хранилище создать руками папки /volume1/storage, но всегда может появиться устройство с другим названием корневого раздела. Вот и первый кусок кода, где мы определяем имя удалённой папки:

NFSPOINT=`showmount -e $HOST | grep storage | awk '{print $1}'`

autofs

В случае с постоянными включениями-выключениями и переносами хранилищ одним из хороших решений является autofs. В таком случае нам нужно только автоматически конфигурировать его по запросу. Если в нашем /etc/auto.master написано:

/mnt/autofspts	/etc/auto.myrules

То по запросу можно писать нужную конфигурацию в /etc/auto.myrules таким образом:

#!/bin/bash

address=192.168.4.50
folder=somefolder

if ping -A -s16 -i0.5 -c3 -q $address > /dev/null 2>&1; then
	set -o pipefail
	mount_point=`showmount -e $address | grep storage | awk '{print $1}'`
	if [[ $? == 0 ]]; then
		entry="$folder -fstype=nfs,rsize=8192,wsize=8192,noatime,nodiratime,intr,async $address:$mount_point"
		case "$mount_point" in
			/storage)
				model="QNAP"
				;;
			/volume1/storage)
				model="Synology"
				;;
			*)
				model="unkonwn"
				;;
		esac
		output="$address:$mount_point $model"
		if [ "x$(cat /etc/auto.myrules)" = "x$entry" ]; then
			echo $output
		else
			echo $entry > /etc/auto.myrules && echo $output configured
		fi
	else
		exit 101
	fi
else
	exit 102
fi
    Тут мы делаем сразу несколько волшебных вещей:
  1. Пингуем перед тем, как смотреть на папки;
  2. Определяем марку из уже известных хранилищ;
  3. Сверяем, что уже написано в конфиге и не пишем, если там всё ок;
  4. Возвращаем разные статусы для разных ошибок.

Тут, однако, нужны права суперпользователя, чтобы писать в конфиг. О том, как я поступаю с суперпользователями на некоторых рабочих машинах я расскажу дальше.

sudo mount

Всем хорош autofs, кроме того, что не проверяет, опять таки, включена ли машина, прежде чем лезть на неё. Что приводит, например, к зависанию процедур листинга папок со ссылками внутрь внешних хранилищ. Вполне возможно обойтись просто командой mount. Но в случае, когда тома не прописаны в /etc/fstab, для монтирования нужны права суперпользователя. А автоматически править /etc/fstab очень не хочется.

На машинах, где все знают пароль для sudo я пользуюсь совершенно беззастенчиво совершенно опасной возможностью sudo получать пароль из стандартного ввода:

#!/bin/bash

NAME=$1
POINT=$2
HOST=$3

if mount | grep $POINT -c > /dev/null; then
	./ping.sh $HOST && echo -e "[\e[0;32mOK\e[0m] Already mounted $NAME" || ( echo -e "[\e[0;31mFAIL\e[0m] $NAME is mounted but unreachable. Check if it's powered and connected" && exit 1 )
else
	if ./ping.sh $HOST; then
		NFSPOINT=`showmount -e $HOST | grep storage | awk '{print $1}'`
		echo "password" | sudo -S -p "" mount -tnfs -o"rw,rsize=8192,wsize=8192,noatime,nodiratime,intr,async" $HOST:$NFSPOINT $POINT && echo -e "[\e[0;32mOK\e[0m] Successfully mounted $NAME" || ( echo -e "[\e[0;31mFAIL\e[0m] Unable to mount $NAME" && exit 1 )
	else
		echo -e "[\e[0;31mFAIL\e[0m] $NAME is unreachable."
		exit 1
	fi
fi

Конечно, кусочек echo "password" | sudo -S -p "" mount вызывает резонный вопрос: «А почему бы тогда не сделать sudo без пароля для данного пользователя?» Ответ тут такой, что за этой машиной работают не только роботы, но и люди, а человека ввод пароля, пусть даже и такого, который все знают, вводит в более сосредоточенное и серьёзное состояние.

Вопрос для самостоятельного изучения

Если вдруг вы знаете, как справляться ситуацией, когда замонтированное (любым способом) хранилище NFS выключили, то напишите мне.

sudo umount -f -l /mnt/point

Особенно для случаев, когда такая команда не работает.

Комментарии 0 >>

18 мая 2011, 22:54

Использование руби программ в качестве фильтров для поиска

Темы: ruby, ruby1.9, regexp, syntax, bash, mistakes

Предыстория

Не далее как прошлой осенью я писал о том, как изнутри процесса определить, запущен ли он, используя инструмент grep. В комментариях мне посоветовали использовать pidof, но мне не удалось заставить его работать для руби, т.к. поиск происходит по имени запускаемого файла, а в случае руби-скрипта это всегда ruby. Но мне существенно удалось сократить получение списка запущенных процессов с таким же именем. Вместо:

`ps ax | grep #{File.basename(__FILE__)} | grep -v grep`.split("\n").map{ |l| l.strip.split(/\s+/)[0].to_i }.reject{ |pid| pid == Process.pid }

получилось

`pgrep -f #{File.basename(__FILE__)}`.chomp.split(/\s+/).reject{ |pid| pid.to_i == Process.pid }

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

ascannerdarkly

Командная строка руби

Руби имеет умеренное количество ключей командной строки. Кратко они описаны в выводе:

ruby --help

Нас в большей степени интересуют ключи -n и -p, которые создают цикл вокруг чтения из пайпа. Ссылка на подробности — в конце статьи.

Например, мы хотим посчитать, сколько всего виртуальной памяти занимают все процессы браузера гугл-хром. В качестве источника информации будем использовать вывод команды:

ps axo "%p %z %c"

В которой собраны только необходимые данные (занимаемая виртуальная память и имя процесса без аргументов) и пид (ну а вдруг?). А теперь этот вывод отправим не грепу, а нашему родному руби:

ps axo "%p %z %c" | ruby -nae 'num ||= 0; num += $F[1].to_i if $F[2] =~ /chrome/; END{puts "total chrome virtual memory size #{num} Kbytes"}'

Что это означает? Ключ n означает, что вокруг нашего скрипта есть цикл вида:

while gets(); ... end

Ключ a означает, что вместо переменной $_, куда автоматически попадает результат gets, мы можем использовать $F, который есть суть $_.split. А END содержит блок, который выполняется после цикла.

Ту же магию можно использовать и внутри запускаемых руби-скриптов. Например, если мы хотим найти какое-то слово внутри файла, выделить его цветом и вывести строку с номером, где это слово нашлось, то наш скрипт будет выглядеть вот так (файл look_for):

#!/usr/bin/ruby -n

BEGIN {
  unless ARGV.size == 2
    puts "Usage: ./look_for <word> <path/to/file>"
    exit
  end
  str = ARGV.shift
}

next unless $_ =~ /#{str}/

printf "%6s%s", $., $_.gsub($&, "\e[31m#{$&}\e[0m")

Теперь, если сделать этот файл запускаемым и запустить его:

./look_for word /in/some/file

То можно увидеть неземную красоту. Кстати, обратите внимание на shift. Без него программа не работает, т.к. gets, который тут за кадром правит бал, пытается воспринимать все аргументы как пути к файлам, из которых непременно нужно что-нибудь прочитать.

Прочие прекрасные применения параметров командной строки руби я предлагаю пытливому читателю подсмотреть в ссылках ниже или найти самостоятельно.

Материалы для самостоятельного изучения

  1. Полный код статьи на гитхабе.
  2. Справочник по параметрам командной строки.
  3. То же, что и выше, но подробнее
  4. Множество прекрасных примеров (со ссылкой на источник).

Комментарии 0 >>

23 марта 2011, 18:58

Рекурсия в регулярных выражениях

Темы: ruby, ruby1.9, regexp, syntax

Пролог

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

worm

Введение

С тех самых пор, как я только узнал про регулярные выражения, я слышал об их несовершенстве и моральном устаревании. Регулярные выражения продолжали использоваться, а недовольные теоретики — сетовать. Основной претензией было то, что регулярные выражения не позволяют исследовать вложенности паттернов в виду своей линейности. Действительно, соглашался я, невозможно проверить правильность открытия и закрытия тегов или получить выражение в самых внутренних скобках.

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

mole_worm

Именованные группы

В регулярных выражениях руби 1.9 появились именованные группы. Вот, как выглядит их элементарное использование:

if /\A(?<first>[a-zA-Z]+)\s+(?<last>[a-zA-Z]+)\Z/ =~ "Vassily Poopkine"
  puts [first, last].inspect
end

if md = /\A(?<first>[a-zA-Z]+)\s+(?<last>[a-zA-Z]+)\Z/.match("Vassily Poopkine")
  puts [md[:first], md[:last]].inspect
end

То есть мы не только выделяем группу скобками, как обычно, назначая ей тем самым порядковый номер (по номеру открывающей скобки), но и даём имя. И использовать его можно не только в локальных переменных и объекте MatchData, но и в самом регулярном выражении.

Более того, обращение к объявленным группам внутри может быть рекурсивным. Мне сразу же захотелось написать давнишнюю мою задумку о функции, раскрывающей вложенные скобки. Вот так:

str = "1 + 2 * (3 - 4 / {5 + 6} + [7 - 8 * (9 + 10 * 11) + 12 * {13 - 14}] + 15) + 16 * (17 + 18)"

re = %r{
        (?<fill>[0-9+\-*/\s]+){0}
        (?<expression>\g<fill>*\g<brackets>\g<fill>*|\g<fill>){0}
        (?<braces>\{\g<expression>+\}){0}
        (?<squarebrackets>\[\g<expression>+\]){0}
        (?<parentheses>\(\g<expression>+\)){0}
        (?<brackets>\g<braces>|\g<squarebrackets>|\g<parentheses>)
}x

def calculator(str)
  if str =~ /\A[0-9+\-*\/\s]+\Z/
    eval str
  else
    raise "Invalid expression: #{str}"
  end
end

f =-> s do
  if $~[:expression] == $~[:fill]
    calculator($~[:fill])
  else
    calculator($~[:brackets][1..-2].gsub(re, &f))
  end
end

puts calculator(str.gsub(re, &f))
puts eval(str.gsub(/(?<left>\{|\[)|\}|\]/) { |s| $~[:left] ? "(" : ")" })

Итак, в регулярном выражении присутствует 6 именованных групп: fill (заполнения пространства между скобками), expression (выражение, содержащее одни или ни одних нераскрытых скобок), braces (фигурные скобки), squarebrackets (квадратные скобки), parentheses (круглые скобки), brackets (любые скобки). Как видите, выражение описывается через скобки, а скобки — через выражение.

Для проверки правильности расчёта, используем обычный eval, заменив все скобки на круглые.

mole

Сделав этот пример, я был доволен, как стадо слонов, но потом решил проверить, а что будет, если скобки расставлены неправильно?

str = "1 + 2 * (3 - 4 / {5 + 6} + [7 - 8 * (9 + 10 * 11) + 12 * {13 - 14]} + 15) + 16 * (17 + 18)"

re = %r{
        (?<fill>[0-9+\-*/\s]+){0}
        (?<expression>\g<fill>*\g<brackets>\g<fill>*|\g<fill>){0}
        (?<braces>\{\g<expression>+\}){0}
        (?<squarebrackets>\[\g<expression>+\]){0}
        (?<parentheses>\(\g<expression>+\)){0}
        (?<brackets>\g<braces>|\g<squarebrackets>|\g<parentheses>)
}x

str =~ re

И я не смог дождаться завершения работы оператора =~ для такого длинного выражения. Это, конечно, неприятно. В причины я вникал не особо, но похоже, это связано с поведением недетерминированной машины Тьюринга. По крайней мере вот ответ на похожую проблему. Для нас это всего лишь означает, что проверять правильность расстановки скобок нужно отдельно и другим способом. Чем я предлагаю заняться пытливому читателю самостоятельно.

Материалы для самостоятельного изучения

  1. Исходный код статьи.
  2. Новый синтаксис и прочие вкусняшки в руби 1.9. Для тех, кто заметил =->.
  3. Глобальные переменные с непонятными именами. Для тех, кто заметил $~.
  4. Ещё немного базовых приёмов в регулярных выражениях руби.

Комментарии 4 >>

05 ноября 2010, 10:18

Ротация логов рельсового приложения

Темы: ruby, rails, syntax, deploy, automation

hay roller

Введение

Это уже давно известная тема, и я не претендую на открытие Америки, но для себя зафиксирую это знание.

Даже если вы используете капистрано для выкладывания проекта в сеть, логи приложения хранятся в одном и том же месте (папка 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
}
    Здесь написано:
  1. weekly — разбивать лог еженедельно;
  2. missingok — не выходить с ошибкой, если файла нет;
  3. rotate 10 — хранить 10 предыдущих томов;
  4. nomail — не высылать удаляемые тома на электронную почту;
  5. compress — архивировать;
  6. delaycompress — архивировать не сразу, т.к. после переименования файла и до перезапуска пэссенджера логи пишутся в тот же переименованный файл;
  7. sharedscripts — запускать скрипт один раз для всех логов по маске;
  8. 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. Или можно просто перезапускать апач.

Материалы для самостоятельного изучения

  1. Документация logrotate (по-русски)
  2. Что ещё можно делать с логами приложения на рельсах

Комментарии 2 >>

20 октября 2010, 16:20

Определение, запущен ли процесс

Темы: daemon, ruby, bash, syntax

Пролог

Ого! Уже три месяца я ничего не писал в этот блог! Лето выдалось жаркое не только на погоду. Поскольку летом погода лучше, а световой день длиннее, было много работы. Причём работы связанной с поддержкой того, что уже и так нормально функционировало в прошлом сезоне. Ничего серьёзно нового не писалось активно, а значит и захватывающих сюжетов для статей не находилось.

Но теперь у меня появилась возможность писать кое-что новое. Поэтому есть, что рассказать.

to feed or not to feed

Введение

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

Баш в помощь

Предположим, что у нас есть простейший демон. Хорошо бы имя у него было уникальное, чтобы можно его потом было отыскать. Файл uniq_name_simple_daemon:

#!/usr/bin/env ruby

pid = fork do
  begin
    running = true
    Signal.trap("TERM") do
      running = false
    end
    while running
      sleep 0.01
    end
  rescue Exception => e
    puts e.to_s
    puts e.backtrace.join "\n"
  ensure
    exit!
  end
end

Мы всегда можем запускать с помощью другого скрипта, например на баше (simple_daemon_runner.sh):

#!/bin/bash

if ps ax | grep uniq_name_simple_daemon | grep -vq grep
then
  echo "uniq_name_simple_daemon is already running"
else
  echo "starting uniq_name_simple_daemon"
  ./uniq_name_simple_daemon
fi

На подобной команде будут базироваться все наши последующие методы. Тут, если кто не понял, мы фильтруем вывод ps ax сначала ища там имя нашего скрипта, а затем исключая из списка сам процесс поиска (команду grep). Ключ q позволяет нам получить код выхода, не выводя ничего на экран. То есть если строчка найдена, то запускаем первый блок, если нет, то второй.

Можно сделать такой же скрипт для остановки процесса (simple_daemon_stopper.sh):

#!/bin/bash

pid=$(ps ax | grep uniq_name_simple_daemon | grep -v grep | awk '{ print $1; }')

if [[ -n $pid ]]
then
  echo "stopping uniq_name_simple_daemon"
  kill -TERM $pid
else
  echo "nothing to stop"
fi

Конечно же, при таком раскладе всегда есть возможность запустить нашего демона без помощи скриптов. И тогда проверка делаться не будет. В таком случае полезно проверять, запущен ли процесс уже внутри самого руби, перед тем, как отпочковать дочерний процесс.

Сам себе хозяин

В данном случае задача сводится к проверке наличия в памяти ещё одного процесса с таким же именем кроме текущего. Так же нужно уметь останавливать процесс с помощью того же файла. Вот, какое решение получилось у меня (uniq_name_auto_daemon):

#!/usr/bin/env ruby

ps_ax = `ps ax | grep #{File.basename(__FILE__)} | grep -v grep`.split("\n").map{ |l| l.strip.split(/\s+/) }.reject{ |l| l[0].to_i == Process.pid }

if ps_ax.any?
  case ARGV[0]
    when /stop/i
      ps_ax.each do |l|
        system "kill -TERM #{l[0]}"
      end
    when /kill/i
      ps_ax.each do |l|
        system "kill -KILL #{l[0]}"
      end
    else
      puts "#{File.basename(__FILE__)} is already running. If you want to stop it, run './#{File.basename(__FILE__)} stop|kill'"
  end
else
  pid = fork do
    begin
      running = true
      Signal.trap("TERM") do
        running = false
      end
      while running
        sleep 0.01
      end
    rescue Exception => e
      puts e.to_s
      puts e.backtrace.join "\n"
    ensure
      exit!
    end
  end
end

Во-первых, обходимся одним файлом, который никак иначе не запустить. Во-вторых, нигде не нужно хардкодить его имя. По-моему, очень удобно.

Оффтопик

С одной стороны, когда я пишу текст, то мне удобнее писать все термины по-русски и склонять их: «демоны», «руби», «баш», но с другой стороны это не поможет тому, кто будет искать решение похожей задачи.

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

Что же делать? :)

Материалы для самостоятельного изучения

Полный код статьи на гитхабе.

Комментарии 10 >>

17 июня 2010, 11:36

Ещё два сценария работы с git: git stash и git bisect

Темы: git, syntax, automation

library

Введение

Моя любимая система контроля версий имеет огромное количество инструментов. Как-то раз я участвовал в опросе, после которого выяснилось, что даже из самых популярных инструментов я использую от силы 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 пользуетесь вы?

Материалы для самостоятельного изучения

  1. Документация по git
  2. git stash
  3. git bisect

Комментарии 0 >>

01 июня 2010, 23:40

Работа над ошибками

Темы: ruby, git, automation, air, mistakes, syntax

Mistakes

Введение

Основной целью этого блога является сбор в одном удобном месте необходимых мне по работе знаний и фишек. Однако, именно потому что это активно используемые в работе решения, со временем появляется более продуктивный или более правильный способ сделать то, о чём написано почти в каждой статье.

Иногда я просто ошибаюсь. Трудно представить что-то более полезное для опыта, нежели набивание шишек. Будет хорошо, если проведение работ над ошибками станет доброй традицией. Итак, в этом году.

git hooks

Недостатков скрипта для удаления пробелов в концах строк нашёл два:

  1. Скрипт без нужды дёргает ни в чём не повинные файлы, потому что \s соответствует и символу конца строки, который там всегда есть.
  2. Скрипт не содержит решения для выбора всех текстовых файлов проекта.

Вот хороший скрипт:

#!/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 пропала, потому что его теперь нет :)

Комментарии 0 >>

07 апреля 2010, 12:59

Немного о $SAFE

Темы: ruby, ruby1.9, syntax, security

secure code

Введение

Совершенно не по работе заинтересовался переменной $SAFE и её ролью в жизни современного разработчика. Оказалось, что всё нужно проверять самому.

Нежная безопасность

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

print "child: "
child = gets.chomp
puts "child tainted: #{child.tainted?}"
(0..4).to_a.each do |i|
  puts "SAFE: #{i}"
  $a = "safe"
  th = Thread.new do
    $SAFE = i
    child_copy = child.dup
    Thread.current[:out] = ""

    begin
      load child_copy
      Thread.current[:out] += "1. Child loaded\n"
    rescue SecurityError => e
      Thread.current[:out] += "1. Security error: #{e.to_s}\n"
      begin
        child_copy.untaint
        load child_copy
        Thread.current[:out] += "2. Child untainted and loaded\n"
      rescue SecurityError => e
        Thread.current[:out] += "2. Security error: #{e.to_s}\n"
        begin
         Thread.current[:out] += "3. Read from file '#{child_copy}': '#{File.read(child_copy)}'\n"
        rescue SecurityError => e
          Thread.current[:out] += "3. Security error: #{e.to_s}\n"
          begin
           Thread.current[:out] += "4. Read from untainted file: '#{File.read("child.rb")}'\n"
          rescue SecurityError => e
            Thread.current[:out] += "4. Security error: #{e.to_s}\n"
          end
        end
      end
    end

    begin
      $a = "modified"
      Thread.current[:out] +=  "5. Global variable modified: $a = '#{$a}'\n"
    rescue SecurityError => e
      Thread.current[:out] += "5. Security error: #{e.to_s}\n"
    end

    begin
      Dir.mkdir "test"
      Thread.current[:out] += "6. Created directory 'test': #{File.exist?("test")}\n"
      Dir.rmdir "test"
    rescue SecurityError => e
      Thread.current[:out] += "6. Security error: #{e.to_s}\n"
    end

    begin
      Thread.current[:out] +=  "7. Dir glob: #{Dir.glob(File.join("..", "*")).inspect}\n"
    rescue SecurityError => e
      Thread.current[:out] += "7. Security error: #{e.to_s}\n"
    end

    begin
      Thread.current[:out] +=  "8. System ls output: '#{`ls`.chomp}'"
    rescue SecurityError => e
      Thread.current[:out] += "8. Security error: #{e.to_s}\n"
    end
  end
  th.join
  puts "Global variable: $a = '#{$a}'"
  puts th[:out] if th[:out]
end

Конструкция со Thread.current[:out] используется потому, что для $SAFE >= 4 нельзя ничего писать ни в какие устройства вывода.

Вроде бы всё логично. Первый уровень годится для умеренного карантина внешних данных. При желании их можно и расколдовать. Второй уровень запрещает изменения в файловой системе. Третий уровень похож на осаду с постоянным подозрением на шпионаж. Все созданные объекты считаются небезопасными. А четвёртый уровень — это самое близкое к песочнице (sandbox) в руби, что что есть.

Кстати, когда ещё github работал как репозиторий библиотек, спецификация gemspec выполнялась там под $SAFE = 3. Для разработчиков это выливалось в то, что нужно было перечислять все файлы своей библиотеки вручную вместо использования какого-нибудь листинга.

Суровый гайдлайн

Конечно же, только использование $SAFE не убережёт от действительно настойчивой атаки или блокирующего кода. Например:

Thread.new do
  $SAFE = 2
  class String
    def ==(other_string)
      true
    end
  end
end.join
puts "string modified: #{'a' == 'b'}"

И это на втором уровне! А на третьем открыть класс тоже можно, но вызов перегруженного оператора будет вызывать SecurityError.

На сегодняшний момент эту концепцию безопасности можно считать сырой. Актуальное поведение руби 1.8 слегка отклоняется от описаний, что я нашёл. Поведение в 1.9 изменилось, но подробно нигде не описано (я не нашёл).

Это не значит, что этой переменной нет применения в жизни прогрессивного человечества. Адекватное текущему состоянию применение — это гайдлайн при разработке. Руководство для программистов, которое само следит за своим исполнением. Жестковато, но зато действенно. :)

Материалы для самостоятельного изучения

  1. Код примеров в статье на github
  2. Старая, но самая подробная документация по $SAFE
  3. Просто дополнительно: шпаргалка по руби

Комментарии 0 >>

24 марта 2010, 19:07

Процесс приёма правок в проекте с открытыми исходниками

Темы: git, syntax

opensource

Введение

Для работы в проекте с открытыми исходниками весьма удобна распределённая система контроля версий. Я использую git. Понятно, что есть процесс с использованием патча, высылаемого по почте, но этот процесс не является эксклюзивным для распределённой системы контроля версий. Поэтому я опишу процесс с так называемым pull request.

Постановка задачи

Пишет мне некто Tallak Tveide, сообщая, что он сделал копию моего проекта у себя на github и внёс несколько правок, которые ему были необходимы, и от которых другие ребята, пользующиеся этой библиотекой только выиграют. Ветка, в которой находятся нужные мне правки, называется eos_40D_bugs. Это довольно кстати, что нашёлся человек с Кэноном, потому что я испытываю всё на Никонах :)

Каковы же мои действия?

Решение

Заходим в наш локальный рабочий репозиторий и добавляем новый источник правок:

git remote add tallakt git://github.com/tallakt/gphoto4ruby

Теперь рассмотрим правки:

git fetch tallakt eos_40D_bugs:develop

Эта команда заберёт из репозитория tallakt с ветки eos_40D_bugs исправления и создаст локальную версию в локальной ветке develop. Чтобы увидеть исправления:

git diff develop

Что выдаст нам исправления относительно текущей ветки.

git checkout develop

Чтобы работать с правками и тестировать то, что получилось.

Если я пока не готов сливать исправления с основной веткой master, но хочу ещё поработать с этим из разных мест, то мне нужно создать ветку develop в моём центральном репозитории на github, который относительно локальной копии у меня обычно называется origin.

git push origin develop

Это создаст ветку develop на удалённом репозитории, с которой я потом смогу работать из другого локального репозитория, выполнив:

git pull origin develop

После того, как я доволен изменениями и хочу сделать официальный релиз:

git merge master
git branch -d develop
git push origin master
git push origin :develop

Первая команда, предполагая, что текущая ветка — develop, сливает её в master. Вторая команда удаляет локальную ветку develop. Третья команда отправляет изменения в ветку master на центральном репозитории. Четвёртая команда удаляет ветку develop на центральном репозитории.

Материалы для самостоятельного изучения

  1. Несколько шпаргалок от github
  2. Дельная документация по git

Послесловие

Как вы заметили, в этом году мои статьи сопровождаются прекраснейшими тематическими картинками авторства Ирины Троицкой — моей прекрасной супруги. Её перу также принадлежит дизайн сайта и логотипа.

Комментарии 2 >>

09 декабря 2009, 16:43

Забота о неблокировании потоков в руби

Темы: ruby, ruby1.9, syntax, extension

Введение

Сегодня будет блиц-молния, которой место, скорее в твиттере. Однако, мне необходимо развеять тучи, которые я сам же и нагнал. :)

Постановка задачи

Не так давно я писал о том, что при создании руби-оболочки вокруг библиотеки на си, легко получить код, который блокирует все потоки приложения. Можно ли с этим что-то сделать?

Решение

Если мы имеем чуть больше контроля над участками кода на си, которые выполняются долго (обычно в цикле), то можно добавить спасительный код в тело цикла:

if (!rb_thread_alone()) rb_thread_schedule();

Это позволит каждый раз при очередном витке передавать управление соседним потокам, если они есть, и сделает приложение более отзывчивым.

То есть, если мы сделаем точную копию функции из той статьи и добавим наш спасительный код внутрь for, то в выводе такой программы:

# coding: utf-8
require "block_thread.so"

t1 = Thread.new do
  10.times { |i| puts i; sleep 0.1 }
end

t2 = Thread.new do
  puts "Блокируем"
  BlockThread.cycle
  puts "Разблокируем"
end

t3 = Thread.new do
  puts "Стараемся не блокировать"
  BlockThread.cycle_with_schedule
  puts "Закончили стараться"
end
t1.join
t2.join
t3.join

Будет картина гораздо приятнее:

Блокируем
0
Стараемся не блокировать
Разблокируем
1
2
3
4
5
Закончили стараться
6
7
8
9

Заметьте, кстати, что два лишних вывода между «Блокируем» и «Разблокируем» благодаря передаче управления соседнимпотокам между cycle и puts.

Понятно, что соседние потоки тормозятся. Но теперь не на всё время, а только на исполнение одного витка цикла.

Материалы для самостоятельного изучения

Полный код статьи на github

Комментарии 0 >>

18 ноября 2009, 23:16

Сборка руби-билиотеки в заданной среде

Темы: ruby, extension, syntax, bash

Постановка задачи

Как разработчику gphoto4ruby мне приходится сталкиваться с особыми задачами. Связано это с тем, что этот gem является оболочкой поверх ещё одной библиотеки. И как у всякой более-менее развитой сторонней библиотеки, у libgphoto2 есть версия, распространяемая через системные репозитории и порты и есть, так сказать, последний писк моды (bleeding edge).

    Отсюда вытекает необходимость:
  1. Иметь разные версии библиотеки не конфликтующие между собой, установленные не одной системе,
  2. Компилировать свою руби-библиотеку под любую из версий.

Установка двух gphoto2 :)

Проделаю весь путь с самого начала. Для пущей целостности. Для начала установка из системного репозитория:

sudo apt-get install libgphoto2-2-dev gphoto2
gphoto2 --version

Теперь можно скачать нужную версию и установить её отдельно. Поскольку я в основном использую две версии, то версию из исходников нужно установить в /opt. Предположим, что исходники libgphoto2 и gphoto2 скачаны:

tar zxvf libgphoto2-x.x.x.tar.gz
cd libgphoto2-x.x.x.tar.gz
./configure --prefix=/opt
make
sudo make install
tar zxvf gphoto2-x.x.x.tar.gz
cd gphoto2-x.x.x.tar.gz
./configure --prefix=/opt --with-libgphoto2=/opt
make
sudo make install
/opt/bin/gphoto2 --version

Теперь мы имеем две библиотеки и две утилиты командной строки, поставленные раздельно и правильно залинкованные. Каждая утилита командной строки знает, где искать свою библиотеку. Надо, чтобы это же умел и gem

Компиляция джема

Если скачать исходник библиотеки, то можно проделать руками то, что делает команда gem install. Для создания Makefile используется утилита mkmf, которая входит в ruby-dev и с которой работает файл extconf.rb. В моём случае последовательность действий установщика такая:

cd ext
ruby extconf.rb
make

Теперь в папке ext мы имеем скомпилированную библиотеку (*.so или *.bundle в зависимости от системы). Установщик потом копирует её в папку lib, но мы пока остановимся. Мы можем посмотреть, какие другие библиотеки использует эта:

ldd gphoto4ruby.so

По выводу этой команды видно, что используется библиотека установленная из центрального репозитория. Теперь попробуем скомпилировать под версию «по последней моде». Поскольку я написал в extconf.rb

dir_config("gphoto2")

То это означает, что пользователю будет доступен целый ряд опций, позволяющих сказать компилятору, где искать libgphoto2. Попробуем:

ruby extconf.rb --with-gphoto2-dir=/opt
make
ldd gphoto4ruby.so

Но что это? Вывод показывает нам, что библиотека привязалась опять к тому, что установлено из репозиториев, а не тому, что в /opt. То есть компилятор, конечно, находит нужные ему заголовки (*.h), но ничего в них не говорит о том, где искать соответствующие им библиотеки. Об этом ему должны сказать мы:

ruby extconf.rb --with-gphoto2-dir=/opt --with-dldflags="-Wl,-rpath,/opt/lib"
make
ldd gphoto4ruby.so

Вуаля!

Теперь, собственно, главное. Как это сделать при установке джема. Чтобы передать ключи для extconf нужно задать их после дополнительного «--»:

sudo gem i gphoto4ruby -- --with-gphoto2-dir=/opt --with-dldflags="-Wl,-rpath,/opt/lib"

Вот такой экскурс в жизнь разработчиков библиотек. Как это звучало в школьные времена: «Спэтсыално дла джэма».

Материалы для самостоятельного изучения

Руководство по расширению руби с помощью C (см. главу про extconf.rb)

Комментарии 0 >>

11 ноября 2009, 20:46

Сравнения и неравенства в руби

Темы: ruby, ruby1.9, syntax, regexp

Постановка задачи

Собрать в одном месте важные, на мой взгляд, особенности сравнений и неравенств в руби.

Основа неравенств в руби

Основным методом сравнения является <=>. Определив его, мы определяем все остальные операции, включив модуль Comparable:

class MyComp
  attr :value
  include Comparable
  def initialize(val)
    @value = val
  end

  def <=>(other)
    @value <=> other.value
  end
end

v1 = MyComp.new(1)
v2 = MyComp.new(2)

puts v1 < v2  # > true
puts v1 <= v2 # > true
puts v1 > v2  # > false
puts v1 >= v2 # > false
puts v1 == v2 # > false

Сам метод можно было бы описать как «возвращает -1, 0 или 1 в зависимости от того, меньше равен или больше объект, чей метод вызывается в сравнении с объектом переданным в качестве параметра». Но на самом деле, скорее, наоборот понятия «больше», «меньше» и «равен» определяются исходя из работы <=>.

Далее всё понятно и более ли менее очевидно для чисел, массивов и строк. Но есть и интересная особенность.

Сравнение модулей и классов

Сравнение для модулей и классов определено таким образом, что в результате мы знаем направление наследования или включение одного модуля другим:

module T1
end
module T2
  include T1
end
T3 = T1

class C1
end
class C2 < C1
end
C3 = C1

puts "T1 <=> T2: #{(T1 <=> T2).inspect}" # > 1
puts "T1 <=> T3: #{(T1 <=> T3).inspect}" # > 0
puts "C1 <=> C2: #{(C1 <=> C2).inspect}" # > 1
puts "C1 <=> C3: #{(C1 <=> C3).inspect}" # > 0
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > nil
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > nil

C3.send(:include, T1)

puts "после включения"
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > -1
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > 1

Наследник или модуль, который включает другой модуль, меньше, чем родитель или включаемый модуль. Это видно даже из синтаксиса наследования.

Равенство

Существует три метода равенства: ==, eql?, equal?. Последний из которых никогда не следует переопределять, т.к. он отвечает за идентичность. Первые же два обычно работают одинаково. Канонический пример различия из документации:

3 == 3.0   # > true
3.eql? 3.0 # > false

Что лишь свидетельствует о том, что == проводит конвертацию чисел перед сравнением. Обычно == соответствует случаю, когда <=> возвращает 0.

Сравнение case...when

Все мы знаем, что в case...when оператор сравнения — это ===. В большинстве случаев он эквивалентен равенству из предыдущего параграфа. Но если равенство симметрично

(a.==(b)) == (b.==(a))

И если это не так, то это можно считать ошибкой. То === вовсе не обязано таковым быть. Нужно помнить, что в конструкции case...when вызывается метод сравнения объекта, стоящего после when, а в качестве параметра ему передаётся объект, стоящий после case:

puts String === "строка" # > true
puts "строка" === String # > false
puts /ок/ === "строка"   # > true
puts "строка" === /ок/   # > false
puts (1..10) === 5       # > true
puts 5 === (1..10)       # > false

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Что нужно помнить, создавая свой объект руби

Комментарии 0 >>

22 октября 2009, 00:48

Работа с потоками (Thread) в руби

Темы: ruby, extension, syntax, daemon, ruby1.9

Введение

Сначала я расскажу, почему на сегодняшний день я не очень много работаю с подпроцессами на базе Thread, предпочитая им Kernel.fork. А потом покажу простой способ следить за потоками при работе приложения.

На текущий момент, основная проблема потоков — это «ненастоящее» распределение ресурсов. Все потоки руби на самом деле находятся в одном системном потоке, который по очереди передаёт им управление. Это влечёт за собой полтора следствия.

Зависание

Когда имеешь дело с внешним оборудованием, сторонними библиотеками и серийными портами, зависание потока может случиться на самом низком уровне. Это можно симулировать небольшой программой на си — block_thread.c:

#include <ruby.h>

VALUE rb_mBlockThread;

/*
 * call-seq:
 *   BlockThread::cycle(interval=5)
 *
 * Блокирует текущий поток на <code>interval</code> секунд.
 *
 */

VALUE bt_cycle(int argc, VALUE *argv, VALUE self) {
  int i, max;
  max = 5;

  if (argc == 1) {
    max = FIX2INT(argv[0]);
  } else if (argc > 1) {
    rb_raise(rb_eArgError, "Неправильное количество аргументов (%d вместо 0 или 1)");
  }

  for (i=0; i<max; i++) {
    sleep(1);
  }
  return Qnil;
}

void Init_block_thread() {
  /*
   * Модуль содержит методы для демонстрации работы потока
   */
  rb_mBlockThread = rb_define_module("BlockThread");
  rb_define_module_function(rb_mBlockThread, "cycle", bt_cycle, -1);
}

Если вы никогда не расширяли руби с помощью си, поясню, что в этой программе мы создаём модуль BlockTread, в котором создаём метода класса cycle, который указанное число раз (по умолчанию 5) в цикле ждёт одну секунду. Напишем extconf.rb:

require "mkmf"
create_makefile("block_thread")

И программу на руби, в которой будут два потока, один из которых мы заблокируем на низком уровне block_threads.rb:

# coding: utf-8
require "block_thread.so"

t1 = Thread.new do
  10.times { |i| puts i; sleep 0.1 }
end

t2 = Thread.new do
  puts "Блокируем"
  BlockThread.cycle
  puts "Разблокируем"
end

t1.join
t2.join

Скомпилируем и запустим:

ruby extconf.rb
make
ruby block_threads.rb

И что же мы видим? Мы видим, как все потоки, включая основной, блокируются на пять секунд (или любое число секунд, которое мы укажем) И даже ctrl + c не в силах нам помочь. Помогает только ctrl + z и потом killall ...

В случае же с Kernel.fork, процессы действительно равномерно делят между собой ресурсы, и один подпроцесс не способен заблокировать всё.

Синхронизация

Я говорил про полторы проблемы. Об одной уже рассказал, а вторая известна давно — попробуйте выполнить следующий код:

# coding: utf-8
$cnt = 0

t1 = Thread.new do
  100000.times { $cnt += 1 }
end

t2 = Thread.new do
  100000.times { $cnt += 1 }
end

t1.join
t2.join

puts "Without sync: #{$cnt}"

Если вы не используете руби 1.9, то вы получите неожиданный и каждый раз разный результат. Всё дело в том, что переключение между потоками происходит между элементарными операциями, а += состоит из трёх элементарных операций: достать значение, прибавить к нему число, записать значение. Чтобы этого не произошло, нужно либо использовать синхронизацию с помощью Mutex, либо руби 1.9. Ссылка на полный код для этой статьи в конце, т.к. я спешу перейти к более интересной части. :)

Слежение за потоками с помощью менеджера ThreadsWait

Совершенно недавно открыл для себя интересный способ следить за статусом пакетов в блокирующей и неблокирующей манере:

# coding: utf-8
require "thwait"

t1 = Thread.new do
  10.times { |i| puts "поток 1 тик #{i}"; sleep 0.5 }
end

t2 = Thread.new do
  10.times { |i| puts "поток 2 тик #{i}"; sleep 0.7 }
end

tw = ThreadsWait.new t1, t2

t3 = Thread.new do
  10.times { |i| puts "поток 3 тик #{i}"; sleep 0.3 }
end

run = true
tw.join_nowait t3

while run do
  begin
    # Неблокирующее ожидание
    puts "Закончил работу #{tw.next_wait(true).inspect }"
    run = false
  rescue ThreadsWait::ErrNoFinishedThread
    puts "Ожидаем окончания работы одного из потоков"
    sleep 0.5
  end
end

# Блокирующее ожидание
tw.all_waits do |t|
  puts "Закончил работу #{t.inspect}"
end

По-моему, весьма удобно, если вам нужно не просто ожидать окончания работы потоков, но ещё и делать что-то при этом.

Материалы для самостоятельного изучения

  1. Полный код статьи на github
  2. Документация ThreadsWait
  3. Толковая статья о многопотоковости и процессах в руби

Комментарии 0 >>

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: Неведомая ошибка>
Процесс окончен, но эта часть ничего не возвращает
Возвратилось: Ошибка!
------------------------
    Что мы отсюда почерпнули:
  1. Блок else (необязательный) выполняется в случае удачно выполненного begin и возвращает значение вместо него.
  2. Выполняется первый из блоков rescue, который соответствует ошибке
  3. Метод raise без параметров выбрасывает текущее исключение: то, которое находится в глобальной переменной $!
  4. Чтобы создать свой класс исключений, наследовать нужно от 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 нас не спасут.

Для самостоятельного изучения

  1. Вопросы-задачи-загадки, на которые полезно ответить не пиша программы
  2. Иерархия классов исключений в руби
  3. Более подробно про азы, в которые я не стал здесь спускаться

Комментарии 0 >>

27 августа 2009, 17:07

Приёмы работы с массивами и блоками в качестве аргументов в руби

Темы: ruby, syntax

Введение

Главная причина, по которой я испытываю нежные чувства к руби — это гибкость синтаксиса. Сегодня я хочу рассказать об особенностях передачи блоков и массивов в качестве параметров. Базовые вещи можно прочитать по ссылкам в последнем параграфе.

Массивы

О том, как принять неограниченное число аргументов, знают все:

$KCODE = "utf-8"

def my_args(*args)
  puts args.inspect
end

my_args(1, 2, "собачка", "котик")  # => [1, 2, "собачка", "котик"]

Но что, если сами объекты, которые необходимо передать в качестве аргументов, уже находятся в массиве по какой-то причине?

arr = ["собачка", "котик", "ёжик", "медвежонок"]
selection = arr.select{ |a| a.chars.to_a.length > 5 }

my_args(selection)  # => [["собачка", "медвежонок"]]

Внезапно массив (args) стал двумерным, что неудивительно, т.к. метод интерпретировал переданный ему массив как первый аргумент типа Array. В данном случае, чтобы передать массив как список аргументов, нужно использовать ту же звезду:

my_args(*selection)  # => ["собачка", "медвежонок"]

Ура!

Кстати, звезду можно использовать и при получении значения от функции. Например, в таком случае:

def get_values
  ["красный", "зелёный", "синий"]
end

r, g, b = get_values

puts r.inspect   # => "красный"
puts g.inspect   # => "зелёный"
puts b.inspect   # => "синий"

r, gb = get_values

puts r.inspect   # => "красный"
puts gb.inspect  # => "зелёный"

r, *gb = get_values

puts r.inspect   # => "красный"
puts gb.inspect  # => ["зелёный", "синий"]

По-моему, здорово!

Блоки

Бывают случаи, когда блок нужно сохранить на будущее, чтобы потом его использовать. Тогда можно указать его в списке параметров метода, что, конечно же, не делает его обязательным само по себе.

class Button
  def on_click(&block)
    if block_given?
      @stored_block = block
    else
      puts "Блока нет, но жизнь продолжается"
    end
  end

  def click(*args)
    @stored_block.call(*args)
  end
end

my_btn = Button.new

my_btn.on_click  # => Блока нет, но жизнь продолжается

my_btn.on_click do |*args|
  puts "Произошёл клик!"
end

my_btn.click     # => Произошёл клик!

Теперь предположим, опять же, что по каким-то причинам блок у нас уже есть. Он определён до вызова метода.

handler = lambda{ |*args| puts "Вот аргументы: #{args.inspect}" }

my_btn.on_click(handler)  # => wrong number of arguments (1 for 0) (ArgumentError)

То есть наш блок превратился в аргумент типа Proc. А метод совсем этого не ожидал. Можно модифицировать сам метод, чтобы он выдерживал подобные условия, но не для этого я всё это пишу :) Если мы чему-нибудь научились в предыдущей главке, то решение по аналогии приходит само:

my_btn.on_click(&handler)

my_btn.click("усы", "хвост")  # => Вот аргументы: ["усы", "хвост"]

Надеюсь, что было полезно.

Материалы для самостоятельного изучения

  1. Аргументы в руби
  2. Ещё про аргументы в руби

Комментарии 0 >>