LE Blog

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

09.06.2016 firtree_right Куда уходят программисты

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

Missing people

Без следа

Недавно я проникся новым молодым языком программирования elm. Читал статьи, потом решил потрогать руками. Установил подсветку синтаксиса для текстового редактора, поставил библиотеки, и вдруг — беда! Поскольку язык молодой (максимум 4 года ему), то синтаксис немного поменялся в последнем релизе. И оказалось, что подсветка не работает с новым синтаксисом.

Я полез разбираться, начал строчить отчёты об ошибках и обнаружил такую историю. Человек, который написал и поддерживал подсветку синтаксиса для элма, пообещал добавить необходимые изменения сразу после выхода новой версии синтаксиса, и исчез бесследно. Имя его  deadfoxygandpa. Совпадение?

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

Уйти красиво

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

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

Почему

Первой же историей, которая обратила мой внимание на то, что люди отделены от своего виртуального персонажа, была история Why the Lucky Stiff. По его учебнику я изучал руби. Но в какой-то момент он решил уйти из интернета. Перед тем, как уйти в оффлайн, он написал: «Программирование весьма неблагодарно. Через год ты видишь, как твою работу замещает лучше сделанная, а через несколько лет её уже и не запустить». Это он сказал в 2009-м.

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

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

11.05.2016 firtree_right Как показать Яндекс Панорамы где угодно

История

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

Со временем, сменился плеер и появился код вставки вида:

<script src="//panoramas.api-maps....."></script>

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

Hack

Параметры

Сначала посмотрим на адрес. Сразу видно две вещи. Никакого уникального идентификатора панорамы в нём не присутствует. Если переходить между панорамами не меняя направление взгляда, и копировать код для вставки на сайт, то различаться будет только один параметр:

ll=37.61782676%2C55.75074572

Это же долгота и широта (в таком порядке) через запятую! Второе, с чем сразу же хочется повозиться — это параметр:

size=690%2C495

Это ширина и высота окна через запятую. Если посмотреть на то, как работает скрипт, и что он оставляет после себя на странице, то совсем не обязательно лезть и деобфусцировать код. Всё понятно: скрипт создаёт вместо себя тег iframe и другой тег script, а себя удаляет. Параметр size и задаёт размеры айфрейма.

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

var latlng = L.latLng(55.75074572, 37.61782676);
var $map = L.map('map', {center: latlng, zoom: 15});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo($map);

function openPanoramaAt(latlng) {
    var panoDiv = $('#panorama');
    var panoScript = document.createElement('script');
    panoScript.type = 'text/javascript';
    panoScript.src = 'https://panoramas.api-maps.yandex.ru/embed/1.x/?lang=ru&ll=' + latlng.lng + '%2C' + latlng.lat + '&ost=dir%3A0.0%2C0.0~span%3A130%2C70.26418362927674&size=' + panoDiv.width() + '%2C' + panoDiv.height() + '&l=stv';
    panoDiv.empty();
    panoDiv[0].appendChild(panoScript);
}

openPanoramaAt(latlng);

$map.on('click', function (me) {
    $map.panTo(me.latlng);
    openPanoramaAt(me.latlng);
});

Кроме лифлета я, конечно, люблю джейквери. Но, к сожалению, на нём невозможно вставить тег script, чтобы он заработал. Поэтому тут немного намешано.

Но нам нужно идти дальше. Ведь внутри панорам можно переходить по стрелкам, а у нас это никак не отображается на карте. Что же делать?

Человек посередине

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

Переходя по стрелкам мы увидим, что, кроме всего прочего, плеер запрашивает файлик по адресу https://panoramas.api-maps.yandex.ru/panorama/1.x/?l=stv&lang=ru_RU&...&format=json. Тут уже нет широты и долготы, а присутствует идентификатор, но если открыть этот джейсон в новой вкладке, то внутри него мы увидим нужные нам координаты:

JSON.parse(response).data.Data.Point.coordinates

То, что браузер видит это запрос, означает, что скорее всего в основе лежит XMLHttpRequest. Так как заголовка Content-Security-Policy не видно (по правде сказать, настраивать его довольно сложно, и обычно если кто и прописывает такой заголовок, то там среди прочего есть unsafe-inline), то мы попробуем подслушать, о чём говорит плеер с сервером.

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

Сразу видно несколько недостатков:

  1. Размеры панорамы не адаптируются при изменении размеров окна.
  2. Невозможно отследить направление взгляда, чтобы показать его на карте. Я рылся в объектах, но ничего не нашёл.
  3. Пришла беда, откуда не ждали: случился прогресс!

Правильный API

Тема этого топика была запланирована у меня некоторое время назад. И основные фишки были опробованы и сделаны тоже некоторое время назад. Но когда я сел писать конкретный код, то обнаружил, что в стандартной поставке API карт уже есть панорамы. Произошло это в прошлой версии 2.1.38 от 31 марта 2016. Сейчас я работал с 2.1.39. Всего 42 дня как можно ставить панорамы на карты!

Конечно же, я собрал такой же пример на API Яндекс Карт. (Всё-таки синтаксис лифлета намного изящнее, извините). Это настолько новое явление, что даже не входит в стандартный полный набор модулей. Заметьте во внешних ресурсах слева я написал для загрузки (иначе не работает):

load=package.full,panorama.isSupported,panorama.locate,panorama.createPlayer,panorama.Player

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

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

Отдельно ссылки:

  1. Как приспособить код для вставки Яндекс Панорам на свои карты (jsfiddle);
  2. Как показать панорамы с помощью API Яндекс Карт (jsfiddle);
  3. Intercept all ajax calls (stackoverflow);
  4. API Яндекс Карт;
  5. Лучший API для картографических сервисов Leaflet.

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

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

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.

16.03.2016 firtree_right Как я встраивал reCAPTCHA v2 в свой антикварный бложек

Предпосылки

Мы живём в интересное время. Когда я увлекался довольно серьёзно руби он рэйлз, лет шесть-семь назад, была версия руби 1.8.7 и версия рельсов 2.3. Все неспешно переходили на руби 1.9.1 и рэйлз 3.0. Сегодня, пять лет спустя, стабильная версия руби — 2.2.3, а про рельсы уже агитируют переходить на 5.0, хоть и бета. Когда же я начал свой первый проект на ноде — три года назад, — версия node.js была что-то типа 0.22. А сегодня уже 5.8!

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

Антиквариат

До последнего перерыва, рассказ о начинке моего блога мог бы легко заполнить парочку в меру интересных статей, а сейчас это представляет интерес только для археологов. Что касается движков для блога вообще, то про когда-то популярный ЖЖ все уже забыли. Стремительно набирает моду вести канал в Телеграме. А товарищ мой — Илья — в когда-то давно в качестве платформы для блога сделал самый правильный, как мне сегодня кажется, выбор — генератор статического сайта.

Что ещё? Технология оупенайди, на которой у меня были прикручены комментарии, умерла. Рекапча, которую я прикручивал в комментарии Ире, была куплена Гуглом и ещё пока жива, но уже выпустили вторую версию, и я боюсь, как бы они таки не закрыли первую, как Гугл это умеет делать. Но даже тогда джем, который я использовал, чтобы встроить капчу, волшебным образом исчез из библиотек, и мне пришлось таскать его с собой в папочке vendor. Про вёрстку я даже не говорю.

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

DIY

reCAPTCHA v2

Когда я решил использовать новую капчу, я даже не стал искать библиотек. С вероятностью 80% они не заработают на моей старинной системе, и с вероятностью 30% перестанут поддерживаться очень скоро. Просто читаем документацию, встраиваем капчу в форму отправки комментария, а на сервере прямо в методе создания комментария пишем, например:

url = URI.parse('https://www.google.com/recaptcha/api/siteverify')
req = Net::HTTP::Post.new(url.path)
req.set_form_data 'secret' => 'SECRET_KEY',
                  'response' => params['g-recaptcha-response'],
                  'remoteip' => request.remote_ip
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
res = JSON.parse(https.start { |p| p.request req }.body)
if res['success'] ...

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

Да, это противоречит паттерну MVC, да, это противоречит ООП. Но посмотрите на саму капчу: она не учитывает положения формы на странице, когда открывает своё окно. А также не работает задокументированная фича data-tabindex. Полно хороших библиотек и поделок увядают так и не исправив своих ошибок. Такова реальность программиста сегодня. Красивая библиотека для встраивания в проект на рельсах не сделает эту капчу лучше, но исправлять описанные выше ошибки можно тоже обезьяньими заплатками.

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

10.03.2016 firtree_right Эзотерический джаваскрипт

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

Что случилось?

Началось всё, я так понимаю, ещё в 2009 году с обсуждений на форумах sla.ckers.org потенциальных возможностей межсайтового скриптинга. Но там ещё был другой синтаксис. Впечатливший же меня пример написания джаваскрипта обнаруживается только в 2012 году. Были и раньше другие заходы. Например, джаваскрипт из японских смайликов. Я же узнал об этом от товарища, который прислал мне ссылку на the daily wtf.

Позволю себе немного пересказать, а дорогому читателю предлагаю прямо открыть консоль и попробовать. Вся эта штука основана на конвертации типов в джаваскрипте при сложении или приведении к булевым значениям. Программировать на джаваскрипте можно всего лишь с помощью восьми символов: [, ], (, ), {, }, ! и +. Вот, что выдаёт мне консоль в ответ на ввод:

> +[]
0
> !+[]
true
> !!+[]
false
> +![]
0
> +!![]
1
> +!![]+!![]
2
> +!![]+!![]+!![]
3

И вот у нас уже есть числа и булевы константы. Теперь нам нужны буквы:

> !![]
true
> !![]+[]
"true"
> ![]
false
> ![]+[]
"false"
> +!![]+!![]
2
> +!![]+!![]+[]
"2"
> ({})
Object {}
> []+{}
"[object Object]"
> ({}[+[]])
undefined
> ({}[+[]])+[]
"undefined"
> +{}
NaN
> +{}+[]
"NaN"

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

> +"1e1000"
Infinity
> +"1e1000"+[]
"Infinity"
> +"1e100"
1e+100
> +"1e100"+[]
"1e+100"

Все буквы (a, b, c, d, e, f, i, j, l, n, o, r, s, t, u, I, O, [, ], <пробел>, +) можно доставать из строк, с помощью квадратных скобок и чисел. Но мы уже давно можем сделать eval — исполнить произвольный код из строки:

> (![]+[])[3]
"s"
> (![]+[])[3]+([]+{})[1]
"so"
...
> []['sort']['constructor']
function Function() { [native code] }
> []['sort']['constructor']('return alert')()
function alert() { [native code] }
> []['sort']['constructor']('return alert')()(1)
/* должен выскочить алерт */

Дело остаётся за малым — получить все остальные символы, с помощью которых можно написать программу. Чтобы уже совсем не чувствовать стеснения. И тут нам на помощь должна прийти функция unescape, которая из знака «%» и аски-кода делает символ. Но у нас пока нет для этого символов «%» и «p». Где же их взять?

> []['sort']['constructor']('return location')()
Location {hash: "", search: "", pathname: "/blog/posts/48", port: "", hostname: "lonelyelk.ru"…}
> []['sort']['constructor']('return location')()+[]
"http://lonelyelk.ru/blog/posts/48"
> ([]['sort']['constructor']('return location')()+[])[3]
"p"
> ({}+[])[0]
"["
> []['sort']['constructor']('return escape')()('[')
"%5D"
> []['sort']['constructor']('return escape')()('[')[0]
"%"
> []['sort']['constructor']('return unescape')()('%'+'7a')
"z"

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

02.03.2016 firtree_right Лось

Сейчас меня уже почти не спрашивают: «Почему лось?» Отчасти потому, что я не очень продвигаю этого персонажа, а отчасти потому, что люди думают, что мне просто нравятся лоси, или я их коллекционирую. Второе приводит, например, к тому, что мне дарят разных игрушечных лосей. Это забавный результат, поэтому я считаю, что репутацию местного сумасшедшего нужно поддерживать. Как следствие я начал использовать лосей в повседневной работе.

Терминал

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

лось в терминале

При этом хотелось бы, чтобы у него было разное случайное выражение лица. Для этого нужно в ~/.bash_profile (или какой у вас файл для создания окружения) написать следующее:

eye=(o O @ . - \* \~ °)
let left=${RANDOM}%${#eye[*]}
let right=${RANDOM}%${#eye[*]}

cat << EOF

 _  _        _  _  _
| || | ____ | || || |
\    |/    \|      /
 \____ ${eye[$left]}  ${eye[$right]} ______/
      |    |________
      |             }=
      | __  ____  _ |
      ||  ||    || ||
      ||  ||    || ||
      ""  ""    "" ""

EOF

Базовая работа с массивами. Встроенная в баш функция случайного числа. Обязательно предварять косой чертой «*» и «~», чтобы они не стали списком файлов и домашней директорией. По-моему — красота!

Spec runner

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

![elk-spec](/images/leru/0039/elk-spec.gif)

Здесь нужно пользоваться эскейп кодами для управления положением каретки, то есть курсора. Мало кто помнит, но курсор можно двигать во все стороны на любое количество позиций. Данный пример я собрал для раннера тестов на жасмине. Да, да, я разрабатываю в том числе на node.js и тестирую с помощью jasmine. Ключевая функция для рисования лося вот:

function printElk() {
  if (specCount > 1) {
    print('\x1b[5A');
  }
  if (specCount % 2 === 0) {
    print(Array(specCount + 1).join(' ') + '     ^^' + eyes() + '^^\n');
    print(Array(specCount + 1).join(' ') + '  _____ U\n');
    print(specTrail + '~(  _  /\n');
    print(Array(specCount + 1).join(' ') + '  || ||\n');
    print(Array(specCount + 1).join(' ') + '  ^^ ^^\n');
  } else {
    print(Array(specCount + 1).join(' ') + '      ^^' + eyes() + '^^\n');
    print(Array(specCount + 1).join(' ') + '  _____ U\n');
    print(specTrail + '`(  _  /\n');
    print(Array(specCount + 1).join(' ') + '  // \\\\\n');
    print(Array(specCount + 1).join(' ') + ' ^^   ^^\n');
  }
}

Здесь specTrail и eyes следят за историей вопроса, а вся функция вместо того, чтобы как раньше, ставить точку или F, поднимается на пять строчек вверх и переписывает их полностью.

Бонус

Это не про лося, но тем не менее. Вообще, я считаю, что всегда лучше потратить немного времени, чтобы сделать всё удобно и интересно. Мелочи решают!

Эту вещь придумал не я. Скорее всего, до меня её придумали неоднократно, и я не могу найти первоисточник. Тем не менее, считаю, что это очень полезная вещь, и нужно ей поделиться. Речь идёт о том, чтобы показывать статус завершения предыдущей команды в строке запроса баша.

export PS1="\[\e]0;\u@\h: \W\a\`if [ \$? = 0 ];then echo \[\e[32m\]^_^\[\e[0m\];else echo \[\e[31m\]o_O\[\e[0m\];fi\`[\u@\h: \w][\$(rvm-prompt v)][\$(nvm_version)]\$(parse_git_branch)\$ "

Это моя строка запроса. В её начале мы видим или зелёный довольный смайлик, или красный удивлённый. Благодаря вот этой части:

\`if [ \$? = 0 ];then echo \[\e[32m\]^_^\[\e[0m\];else echo \[\e[31m\]o_O\[\e[0m\];fi\`

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

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

  1. $RANDOM.
  2. elk_reporter.js.
  3. Управление курсором в баше.

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.