LE Blog

Инженер с поэтической душой

26.05.2016 firtree_right Создание своих правил для udev

Магия

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

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

Вставил диск

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

Sorting robot

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

udevadm info -a --name=/dev/sdd1
udevadm info --query=env --name=/dev/sdd1

как уже говорил, имея инструкцию, как подготовить диск для копирования, можно всегда иметь одинаковую метку тома. Чем и пользуемся: создаём файлик /etc/udev/rules.d/90-my-storage-copy.rules, куда пишем:

ACTION=="add", ENV{ID_FS_USAGE}=="filesystem", ENV{ID_FS_TYPE}=="ext4", ENV{ID_FS_LABEL_ENC}=="storage-copy", RUN+="/usr/local/bin/storage-copy-mount.sh"
ACTION=="remove", ENV{ID_FS_USAGE}=="filesystem", ENV{ID_FS_TYPE}=="ext4", ENV{ID_FS_LABEL_ENC}=="storage-copy", RUN+="/usr/local/bin/storage-copy-umount.sh"

Всё, что выдавалось нам с параметром --query=env будет в параметрах окружения нашего скрипта /usr/local/bin/storage-copy-mount.sh:

#!/bin/sh
mount_point="/mnt/myrules/$(basename $DEVNAME)"
mkdir -p $mount_point
mount -t $ID_FS_TYPE -o ro $DEVNAME $mount_point

/usr/local/bin/storage-copy-umount.sh:

#!/bin/sh
mount_point="/mnt/myrules/$(basename $DEVNAME)"
umount -l -f $mount_point
rmdir $mount_point

Некоторые гайды не рекомендуют вызывать команду mount из правил udev, но когда это останавливало настоящих волшебников? :)

Массив без массива

Второй случай — половина рэйд-массива. Нужно собирать и разбирать массивы так, чтобы это не пересекалось с работой остальной системы. Некоторые хранилища задают метки тома своим разделам с информацией, а некоторые можно определить только по номеру партиции. /etc/udev/rules.d/90-my-storage-rais.rules:

ACTION=="add", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{ID_PART_ENTRY_NUMBER}=="3", ENV{ID_FS_LABEL_ENC}!="system*", RUN+="/usr/local/bin/storage-raid-mount.sh"
ACTION=="add", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{ID_FS_LABEL_ENC}=="DiskStation*", RUN+="/usr/local/bin/storage-raid-mount.sh"
ACTION=="remove", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{ID_PART_ENTRY_NUMBER}=="3", ENV{ID_FS_LABEL_ENC}!="system*", RUN+="/usr/local/bin/storage-raid-umount.sh"
ACTION=="remove", ENV{ID_FS_USAGE}=="raid", ENV{ID_FS_TYPE}=="linux_raid_member", ENV{ID_FS_LABEL_ENC}=="DiskStation*", RUN+="/usr/local/bin/storage-raid-umount.sh"

То есть, говоря человеческим языком, это или рэйд-партиция с номером 3, название которой не начинается с system или рэйд-партиция с именем, начинающимся с DiskStation. Теперь нам нужно собрать массив так, чтобы у него было уникальное имя, но при этом однозначно связанное с устройством, чтобы не плодить лишних сущностей. Для этого я решил точку для монтирования называть так же как имя устройства в /dev, а номер рейда брать из кода последней буквы. /usr/local/bin/storage-raid-mount.sh:

#!/bin/sh

mount_point="/mnt/myrules/$(basename $DEVNAME)"
num=$(printf %d "'$(echo $DEVNAME | head -c8 | tail -c1)")
raid_device="/dev/md$num"

mkdir -p $mount_point
mdadm -S $raid_device
mdadm -A -R $raid_device $DEVNAME
mount -o ro $raid_device $mount_point

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

echo $DEVNAME | head -c8 | tail -c1

Выдаёт нам восьмую букву имени устройства, то есть «d» для «/dev/sdd3», например, и «f» для «/dev/sdf5».

printf %d "'d"

Выдаёт нам 100, а в случае с «f» — 102. И мы получаем имя «/dev/md100», под которым насильно поднимаем raid1 на одном диске из двух. И обратно то же самое. /usr/local/bin/storage-raid-umount.sh

#!/bin/sh

mount_point="/mnt/myrules/$(basename $DEVNAME)"
num=$(printf %d "'$(echo $DEVNAME | head -c8 | tail -c1)")
raid_device="/dev/md$num"

umount -l -f $mount_point
mdadm -S $raid_device
rmdir $mount_point

Понятно, что с вытаскиванием сложнее, даже если монтировать, как это делаю я, только для чтения. Это всё актуально, если после работы с диском прошло значительное время. И я предпочитаю хотя бы размонтировать вручную. Но при этом совершенно прекрасно то, что все наши устройства будут создавать папки и появляться в /mnt/myrules, как флэшки появляются в /media на десктопных версиях Убунту.

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

  1. man udev

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

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

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

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

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

30.03.2016 firtree_right Быстрый пинг

Предисловие

Сегодня хочу рассказать про чтение мануалов. Например, с их помощью я выяснил, что чтобы разрабатывать баш-скрипты на маке (он у меня не так давно) под линукс, нужно ставить coreutils. Потому что команды терминала ведут себя по-разному, имеют разные доступные опции и разные дефолтные настройки.

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

Задача

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

man ping

Simply Yes No

Ход решения

  1. Линуксовый пинг продолжается бесконечно, поэтому нужно ограничить число пакетов. Опция .
  2. Можно сделать пакет меньше с помощью опции -s.
  3. Можно уменьшить интервал между посылаемыми пакетами (но не меньше 0.2 секунд, если пингует не суперпользователь) с помощью опции -i.
  4. Или же интервал можно сделать адаптивным, чтобы он сам ускорялся, если ответ пришёл быстро с помощью -A.
  5. можно не выводить ненужную нашему скрипту информацию с помощью -q и перенаправить стандартный вывод ошибок в никуда.

Что мы получили:

$ ping -A -s16 -c3 -q ya.ru 2>/dev/null
PING ya.ru (93.158.134.3) 16(44) bytes of data.

--- ya.ru ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 400ms
rtt min/avg/max/mdev = 1.923/2.075/2.339/0.187 ms, ipg/ewma 200.281/1.973 ms

Пока всё ещё многовато читать. Хорошо бы вытащить только число полученных пакетов. А оно у нас как раз после первой запятой в строчке рядом со словом «received».

$ ping -A -s16 -c3 -q ya.ru 2>/dev/null | grep received
3 packets transmitted, 3 received, 0% packet loss, time 401ms
$ ping -A -s16 -c3 -q ya.ru 2>/dev/null | grep received | awk -F', ' '{print $2}'
3 received
$ ping -A -s16 -c3 -q ya.ru 2>/dev/null | grep received | awk -F', ' '{print $2}' | awk '{print $1}'
3

Почти всё работает быстро, кроме выключенных машин и несуществующих адресов (не имён). В отличие от ошибки DNS пинг требует убедиться, что всё действительно недоступно. Но мне нужно быстро и однозначно. Повезло, что плохое соединение для моей задачи равносильно отсутствию соединения. На помощь приходит timeout:

$ timeout 1 ping -A -s16 -c3 -q ya.ru 2>/dev/null

Теперь нужно передавать фейл дальше и сверять, собственно, число пингов. Получаем наш красивенький быстрый fastping.sh:

#!/bin/bash

cnt=3
to=1
rcvd=$(set -o pipefail; timeout $to ping -A -s16 -c$cnt -q $1 2> /dev/null | grep received | awk -F', ' '{print $2}' | awk '{print $1}')
if [[ $? != 0 || $cnt != $rcvd ]]; then
    exit 1
fi

Дополнительно

  1. man ping
  2. Полная версия fastping.sh, в которой можно задать число пакетов и таймаут, но они имеют дефолтные значения.
  3. Пинг на стероидах — fping.

17.02.2016 firtree_right Смена настроек /etc/hosts в одно касание

Зачем?

В какой-то момент я стал носить на работу ноутбук и отказался от двух компьютеров: домашнего и рабочего. В редких случаях приходится делать небольшие действия по работе, находясь дома. Для того, чтобы добраться до нужных машин внутри рабочей сети, я использую ssh-тоннель с пробрасыванием портов. Например, есть два сервиса: server1:8080 и server2:5000. Когда было два компьютера, то было всё просто. Рабочий компьютер находился внутри сети и видел оба сервера с их сервисами по правильным адресам, а домашний адресовал оба имени серверов на localhost, где сервисы оказывались на тех же портах после поднятия тоннеля. Но с ноутбуком нужно было как-то переключаться.

Для смены настроек я использовал самый простой, как мне кажется, способ: редактировал файл /etc/hosts. У меня было два набора строчек: для дома и для офиса. Один всегда закомментирован. Файл открывался с помощью sudo vi, и внутри можно использовать замену, используя номера строк, которые видны:

:2,7s/^#/
:9,13s/^/#/

Первая команда означает «со второй по седьмую строчку удалить „#“ в начале строки», а вторая — «с девятой по тринадцатую строчку поставить „#“ в начале строчки». Но когда делаешь одно и то же много раз, всегда хочется это автоматизировать.

Changesettings

Как?

Для начала мне хотелось избавиться от номеров строк (мало ли, какие добавятся или исчезнут строки). Поэтому я решил выделять зоны файла для офиса и дома комментариями «#officestart», «#officeend» и, соответственно, «#homestart» и «#homeend». Теперь интервал для замены можно было выделять через них:

:%s/#officestart\zs\_.\+\ze#officeend/smth_smth_smth/g
:%s/#homestart\zs\_.\+\ze#homeend/smth_smth_smth/g

В данных командах \zs и \ze означают начало и конец паттерна, который мы хотим заменить командой s в интервале %, то есть во всём файле. То есть мы меняем не всё, что нашли, а только часть. А сам паттерн — это _.+, что означает «один или более любых символов, включая конец строки». Буква g в конце означает, что может быть несколько таких блоков, что необязательно.

На что же мы будем заменять найденный паттерн между комментариями? Во-первых, нам совершенно точно понадобится замена внутри замены. А во-вторых, нам не поможет символ ^ для обозначения начала строчек, т.к. у найденного паттерна всего одно начало перед всеми строчками. Поэтому мы будем использовать знание структуры файла /etc/hosts: в случае IPv4 каждая незакомментированная рабочая строчка начинается с цифры, а закомментированная, как и положено, с «#». Для дома получаем команды:

:%s/#officestart\zs\_.\+\ze#officeend/\=substitute(submatch(0), '\n\(\d\)', '\n#\1', 'g')/g
:%s/#homestart\zs\_.\+\ze#homeend/\=substitute(submatch(0), '\n#\(\d\)', '\n\1', 'g')/g

Использование = заставляет редактор выполнить выражение, то есть вызвать функцию substitute в таком виде. Тут, вроде бы, должно быть понятно, что мы передаём в функцию найденный паттерн, регулярное выражение с одной группой и на что его поменять в том паттерне.

От команд к скрипту

Осталось сделать из этого удобную штучку. Лично я оформил это следующим образом. В файле ~/.bash_profile:

alias imhome="sudo vim -u NONE -f -s $HOME/.vim/homehosts /etc/hosts"
alias imwork="sudo vim -u NONE -f -s $HOME/.vim/officehosts /etc/hosts"

Соответственно, файлы ~/.vim/homehosts:

:%s/#officestart\zs\_.\+\ze#officeend/\=substitute(submatch(0), '\n\(\d\)', '\n#\1', 'g')/g
:%s/#homestart\zs\_.\+\ze#homeend/\=substitute(submatch(0), '\n#\(\d\)', '\n\1', 'g')/g
:wq

~/.vim/officehosts

:%s/#officestart\zs\_.\+\ze#officeend/\=substitute(submatch(0), '\n#\(\d\)', '\n\1', 'g')/g
:%s/#homestart\zs\_.\+\ze#homeend/\=substitute(submatch(0), '\n\(\d\)', '\n#\1', 'g')/g
:wq

Таким образом, команды imhome и imwork спрашивают пароль и меняют настройки. Это иллюстрирует, почему был выбран редактор vim в качестве инструмента. Любые sed и awk будут потом требовать sudo tee для того, чтобы записать файл с нужными правами. А здесь мы запускаем всего одну команду.

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

  1. Поиск и замена в vim.
  2. Использование выражений при поиске и замене в vim.

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

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. Что ещё можно делать с логами приложения на рельсах

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

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

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

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

26.05.2010 firtree_right Использование git hooks на руби в корыстных целях

Organizers

Введение

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

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

git hooks

Для множества различных целей у git есть хуки. (Как бы их перевести нормально?) Они находятся в каждом репозитории в папке: .git/hooks

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

Использование pre-commit для удаления пробелов на концах строк

Поскольку я, опять же, фанат руби, то и скрипты — благо есть такая возможность — напишу на руби. Создаём файлик .git/hooks/pre-commit:

#!/usr/bin/env ruby

Dir.glob("*.{txt,rb}").each do |p|
  lines = File.readlines(p)
  if lines.inject(false) { |memo, line| line.gsub!(/\s+$/, "") || memo }
    File.open(p, "w") do |f|
      f.puts lines.join("\n")
    end
    system "git add #{p}"
  end
end

Как видно, этот скрипт ищет файлы *.txt и *.rb в корневом каталоге репозитория, и если в них есть пробелы в конце строк, перезаписывает их и добавляет в индекс для коммита.

Не забыть сделать его запускаемым:

chmod +x .git/hooks/pre-commit

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

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

Документация по git hooks

28.04.2010 firtree_right Использование Airake под Kubuntu

simple things

Введение

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

Я люблю руби. И, естественно, rake, как инструмент, продолжающий славные традиции make в руби и с помощью руби. Так же я питаю нежные чувства к ActionScript. Мне нравится AIR, который позволяет писать действительно кросс-платформенные приложения довольно быстро. Так же я неплохо отношусь к TDD, как к одному из способов разработки.

Какова же была моя радость найти инструмент, который всё это объединяет! Хотя ему уже пара лет, он по-прежнему прекрасен.

Установка составляющих

Предполагаю, что ruby, rubygems и rake уже установлены у тех, кто читает этот блог.

Далее, качаем и разархивируем куда-нибудь Adobe AIR SDK и Adobe Flex SDK (или предыдущая версия, если вы консерватор), а так же устанавливаем Adobe AIR Runtime. Чтобы установить последний, после загрузки bin-файла нужно:

chmod +x AdobeAIRInstaller.bin
sudo ./AdobeAIRInstaller.bin

Теперь добавим в PATH пути к исполняемым файлам загруженных SDK. В .bashrc добавляем:

export PATH="/path/to/air_sdk/bin:$PATH"
export PATH="/path/to/flex_sdk_4/bin:$PATH"

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

sudo apt-get install sun-java6-jre

После установки, независимо от того, используете вы Flex3 или Flex4, нужно переписать содержимое AIR SDK поверх Flex SDK. Мне не совсем понятен сакральный смысл этих действий, но иначе ничего не работает.

Привѣтъ, Мiръ!

Создание пустого air-приложения теперь просто:

airake airake_hello_world

Чтобы запустить его, однако, следует исправить в src/AirakeHelloWorld-app.xml и test/Test-app.xml:

...
xmlns="http://ns.adobe.com/air/application/1.5"
...

Если вы решили использовать Flex4, то вам необходимо отредактировать сгенерированное приложение, чтобы запустить его. Это связано с изменениями в стилях. Поэтому проще просто удалить всё содержимое тэга WindowedApplication в файле src/AirakeHelloWorld.mxml.

Про использование TDD в ActionScript я уже писал, поэтому подробно останавливаться не буду. Для примера в код на github включён тривиальный тест. Запуск тестирования происходит привычным образом:

rake test

Документация, если вы пишете правильные комментарии ASDoc, тоже запускается привычным образом:

rake docs

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

rake -T

Использование rake

Конечно, вся прелесть rake не только в привычных и коротких командах для разработки, но и в том, что можно создавать свои сценарии. Например, вот как могла бы выглядеть работа с версиями приложения. Добавим в файл raketasks/version.rake следующий код:

require 'yaml'

desc "Print out current version"
task :version do
  if md = File.read(YAML.load_file('airake.yml')["appxml_path"]).match(/<version>(.*)<\/version>/)
    puts "Current version is #{md[1]}"
  else
    raise "Cannot detect current version.\nMake sure appxml file contains <version>X.X.X</version> tag."
  end
end

namespace :version do

  [:major, :minor, :patch].each do |subv|
    desc "Bump #{subv} in version"
    task :"bump_#{subv}" do

      unless `git status` =~ /nothing to commit/
        raise "There are uncommitted changes. Failed to proceed."
      end 

      appxml = YAML.load_file('airake.yml')["appxml_path"]
      str = File.read(appxml)

      msg = nil
      new_version = nil

      if str.gsub! /<version>(.*)<\/version>/ do |matched|
        old_version = $1
        major, minor, patch = old_version.split(".").map(&:to_i)
        eval("#{subv} += 1")
        new_version = [major, minor, patch].join(".")
        msg = "Version bump #{old_version} => #{new_version}"
        puts msg
        "<version>#{new_version}<\/version>"
      end.nil?
        raise "Cannot detect current version.\nMake sure appxml file contains <version>X.X.X</version> tag."
      else
        File.open(appxml, "w") do |f|
          f.write str
        end

        puts `git commit -am "#{msg}"`
        puts `git tag v#{new_version}`
      end
    end
  end
end

А в Rakefile соответственно:

# Custom rake tasks
Dir.glob("raketasks/*.rake").each { |rf| load rf }

Теперь мы можем привычным образом работать с версиями приложения (а версии эти потом будут распознаваться установщиком обновлений):

rake version
rake version:bump_major
rake version:bump_minor
rake version:bump_patch

И это не предел!

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

  1. Полный код статьи на github
  2. Инструкция по работе с airake, которая во многом повторена в этой статье с добавлением манипуляций, чтобы всё заработало.
  3. Документация по FlexUnit. Не уверен, что в поставке airake идёт самая последняя версия, но ничего не мешает написать rake task для обновления версии FlexUnit :)
  4. Документация по rake

23.09.2009 firtree_right Получение irb-консоли в среде приложения

Задача

Допустим, есть приложение, написанное на руби. У него есть основной процесс и есть тесты, которые так или иначе загружают объекты приложения.

Хочется получить доступ в среду приложения в виде irb-консоли, чтобы вручную взаимодействовать с объектами и изменять данные. По типу script/console в rails.

Решение

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

irb
require ...
require ...
...

Раз уж мы захотели как в рельсах, то следует предположить, что окружение нашего приложения загружается одним файлом. Например, config/environment.rb. Это будет первым упрощением многократно повторённого процесса.

Теперь сам файл script/console:

#!/usr/bin/env ruby

APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))

libs = " -r irb/completion"
libs << " -r pp" # специально для консоли автоподставнока и pretty print
libs << %( -r "#{APP_ROOT}/config/environment.rb")

ENV["APP_ENV"] = "console" # пример того, как сообщить приложению, что оно в консоли

exec "irb #{libs} --simple=prompt"

Здесь стоит сделать два акцента:

  1. Для сообщения приложению, что оно запущено в консоли, мы использовали волшебный хэш ENV. К нему потом можно обратиться внутри config/environment.rb и сделать что-то по-другому.
  2. Для запуска консоли мы использовали Kernel.exec, который не просто выполняет системную команду, но и передаёт ей управление, заменяя текущий процесс.

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

chmod +x script/console

Я хочу, чтоб эта песня, эта песня не кончааалась

Если есть приятные библиотеки, которые хочется подключать при каждом запуске irb. А так же, если хочется сохранять историю команд консоли при выходе. То следует воспользоваться мощью ~/.irbrc. Создайте этот файл («~» -- это $HOME, на всякий случай) и напишите в него:

require 'pp' # pretty print
require 'irb/completion' # автоподстановка
require 'irb/ext/save-history' # сохранение истории

ARGV.concat [ "--readline", # не пробовал без readline
              "--prompt-mode", "simple" ] # --simple-prompt

IRB.conf[:SAVE_HISTORY] = 25 # сколько сохранять

IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb-history" # куда сохранять

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

  1. Подробная документация irb;
  2. Занятный способ получить irb-консоль прямо в запущенном работающем приложении.