LE Blog

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

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

Пролог

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

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. Ещё немного базовых приёмов в регулярных выражениях руби.

07.11.2010 firtree_right Отдых

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

В качестве отдыха приобрёл и прошёл сюжетные DLC (загружаемый контент) к Mass Effect 2. Получил удовольствие, конечно. Даже на минуточку захотелось создать нового героя и пройти первую и вторую часть от начала до конца. Задумался, почему им нужно делать прогрессирующую шкалу плохиша или кибальчиша. Сюжетно мне нравятся выборы положительного героя: больше персонажей остаётся в живых, больше потом приколов. Но многие диалоги у отрицательного героя лучше подходят. А иному персонажу с ноги заехать — самая адекватная реакция. Но если выбирать то так, то так, то потом недоступны ни те ни другие опции.

Ходил на день рождения к другу. Он сказал мне, что снова бросил учиться в семинарии. На вопрос: «Почему?» — показал вот это. Сказал: «Тот, Кто создал такое, не может быть в Своём уме». Я его понимаю.

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

А как вы провели выходные?

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

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

Пролог

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

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

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

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

Оффтопик

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

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

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

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

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

21.07.2010 firtree_right Управление LEGO NXT Mindstorms с помощью телефона HTC Desire на Android

summer in the city

Введение

Лето в этом году выдалось жаркое, но пока ни о каких отпусках речи не идёт. Пока солнечно, тепло и сухо — самое время снимать панорамы улиц (или ГрадоГлядъ, как я это называю). Однако, не смотря на хорошую плотность работы, ничего принципиально нового, о чём интересно было бы рассказать, не произошло. Поэтому я покажу концепт, над которым работаю уже какое-то время, и который лежит в стороне от моих основных языков программирования.

Дистанционное управление

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

А в апреле я обновил телефон на HTC Desire под управлением операционной системы Андроид, которая уже давно привлекла моё внимание, но всё руки не доходили.

В итоге появились повод, возможность и желание «пощупать» Андроид. И вот, что из этого получилось:

Секреты?

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

/* Не работает:
 * socket = device.createRfcommSocketToServiceRecord(MY_UUID)
 */

Method m = device.getClass().getMethod("createRfcommSocket",
            new Class[] { int.class });
socket = (BluetoothSocket)m.invoke(device, Integer.valueOf(1));

Вот и всё, что я могу рассказать :) Если я ещё буду писать про Андроид, то придётся сделать подсветку кода для него.

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

Руководство для разработчиков под Андроид на тему bluetooth

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

12.06.2010 firtree_right Оказалось, не казалось

Пока вы спали (и я тоже спал) мне явился правильный ответ: это float в формате little endian (младшим байтом вперёд). Вот код для андроида.

int tmp = Float.floatToRawIntBits(new Float(number));
byte[] msg = {(byte)tmp, (byte)(tmp >> 8), (byte)(tmp >> 16), (byte)(tmp >> 24)};

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

11.06.2010 firtree_right Профессиональное

Я тут вечерами не могу лечь спать, потому что управляю своим Lego Mindstorms роботом с телефона на Android (моего любимого ненаглядного HTC Desire то бишь) по bluetooth. Я уже написал базовое взаимодействие, и бодро передаю сообщения. Возник лишь один нюанс с передачей целых чисел. Вместо того чтобы совать их в трубу младшим байтом вперёд, как об этом говорят все интернеты, у меня они выглядят не так.

Это был целый детектив. Я слал ему little endian, но он отказывался. Сначала совсем. Потом везде видел нули Тогда я начал его пытать, и заставлял его посылать сообщения из целых чисел.

Если записать это в двоичной форме, то примерно понятно, что он имеет в виду. Но возможно есть матчасть, которую можно почитать, чтобы лучше это понять. Что это за представление, как его просто изобразить, скажем, на Java? Итак, имеем четыре байта:

... симметрично
-4 => 0x00 0x00 0x80 0xC0
-3 => 0x00 0x00 0x40 0xC0
-2 => 0x00 0x00 0x00 0xC0
-1 => 0x00 0x00 0x80 0xBF
0 => 0x00 0x00 0x00 0x00
1 => 0x00 0x00 0x80 0x3F
2 => 0x00 0x00 0x00 0x40
3 => 0x00 0x00 0x40 0x40
4 => 0x00 0x00 0x80 0x40
5 => 0x00 0x00 0xA0 0x40
6 => 0x00 0x00 0xC0 0x40
7 => 0x00 0x00 0xE0 0x40
8 => 0x00 0x00 0x00 0x41
9 => 0x00 0x00 0x10 0x41
10 => 0x00 0x00 0x20 0x41
...
15 => 0x00 0x00 0x70 0x41
16 => 0x00 0x00 0x80 0x41
17 => 0x00 0x00 0x88 0x41
...
555 => 0x00 0xC0 0x0A 0x44
...
2147483648 => 0x00 0x00 0x00 0x4F

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

09.06.2010 firtree_right Солянка

Сегодня как раз слушал всякие подкасты, подтверждающие мою правоту. Люблю подтверждать свою правоту, что поделать :) Вкратце, чтобы никого никуда не отсылать, там говорилось про то, что ежели ты динамично растущий программист, то полезно фиксировать свои открытия об окружающих. Типа вести блог или что-то подобное. Я реально им пользуюсь столько же, сколько всякой прочей нужной документацией.

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

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

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

Такие дела.

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