node package manager
Easy sharing. Manage teams and permissions with one click. Create a free org »

mjoe

Monkey Joe

Monkey Joe — инструмент автоматического запуска команд при появлении новых файлов, изменении или удалении существующих в директориях, за которыми "следит" Monkey Joe (далее mjoe).

ВНИМАНИЕ! MONKEY JOE РАБОТАЕТ ТОЛЬКО В LINUX!

В упрощённом виде mjoe работает так:

  • файловая система инициирует событие (файл изменился, удалился, добавился и т.п.);
  • mjoe получает уведомление об этом событии (event);
  • mjoe пересылает событие каждому обработчику (worker), описанному в конфигурационном файле;
  • специальная функция (matcher) обработчика решает, следует ли отреагировать на событие;
  • если требуется реакция, вызываются пользовательские функции (callbacks).

Пример

~$ mkdir test && cd test
~/test$ echo foo > dummy.js
~/test$ cat > .mjoe.js
exports.config = {
    exclude: ['*.git'],
    workers: [
        {
            matcher: '*.js',
            callbacks: function(e) {
                console.log(e);
            }
        }
    ]
};
^C
~/test$ mjoe
[2011.07.07 22:15:56.117] Monkey Joe запускается...
[2011.07.07 22:15:56.119] Monkey Joe запустился

В терминале открываем новую сессию и редактируем dummy.js:

~/test$ echo bar >> dummy.js

Monkey Joe должен вывести в терминал что-то типа:

{ type: 'ninotify',
  mask: 4,
  path: '/home/nikita-vasilyev/test/foo.js' }

Предварительные требования:

Установка

Для установки mjoe на Linux следует выполнить команды:

npm install mjoe
npm install ninotify

Если установка прошла без ошибок, никаких дополнительных действий совершать не надо, всё на своих местах и mjoe доступен по команде mjoe.

Обновление

Для обновления mjoe на Linux достаточно выполнить команды:

npm update mjoe
npm update ninotify

Удаление

Для удаления mjoe и служебных модулей следует выполнить команды:

npm uninstall mjoe
npm uninstall fswatch
npm uninstall yanlibs
npm uninstall ninotify

Запуск

Запуск следует производить в директории, за содержимым которой (включая поддиректории) должен следить mjoe. При этом на том же уровне должен находиться конфигурационный файл (далее конфиг) .mjoe.js. Например, чтобы mjoe корректно запустился в директории foo и обрабатывал изменения в foo, bar и bbr, структура директорий и местонахождение конфига должны быть такими:

/foo
   .mjoe.js
   /bar
   /bbr

Для запуска mjoe следует выполнить команду:

mjoe

Если требуется узнать версию, запускать следует так:

mjoe --version

Запуск в режиме логирования:

mjoe -vv

Вывод справки:

mjoe --help

Следует понимать, что mjoe отслеживает события только после запуска. Если вы изменили файл, а затем запустили mjoe, ничего не произойдёт. Говоря формально, mjoe является stateless, а не stateful, т.е. между запусками состояние (информация о событиях, зеркало файловой системы и т.п.) не сохраняется.

Формат конфига

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

Конфиг состоит из трёх частей (настройки, exclude и workers):

exports.config = {
    depth: ...,
    interval: ...,
    preprocessor: ...,
    exclude: [...],
    workers: [...]
};

Depth

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

Значение по умолчанию, которое применяется при отсутствии параметра в конфиге: 20.

Interval

Параметр interval задаёт временной промежуток (в миллисекундах) между запусками workers. Иными словами, если два события пришли с разницей в одну миллисекунду, mjoe сначала запустит workers на первое событие, а на второе событие запустит лишь спустя указанный интервал. Это позволяет избавиться от конфликтов, когда workers в процессе работы обращаются к одним и тем же файлам. Таким образом, интервал следует подбирать с учётом наиболее продолжительно работающего workerа. Если в конфиге используются синхронизированные exec (см. API.exec), этот параметр можно не принимать во внимание.

Значение по умолчанию, которое применяется при отсутствии параметра в конфиге: 1000.

Preprocessor

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

Exclude

Параметр exclude позволяет указать mjoe те директории, которые не надо включать в обработку. Формат параметра идентичен формату workers.matcher, о чём следует читать соответствующую главу.

При инициализации mjoe совершает обход дерева директорий, проверяя каждую через exclude. Если директория исключена из обработки, соответственно, исключаются все её поддиректории.

Также на вхождение в exclude проверяются и директории, появляющиеся в процессе работы mjoe.

Workers

В массиве workers перечисляются обработчики событий:

exports.config = {
    ...
    workers: [
        {
            matcher: ...,
            callbacks: ...
        },
        {
            matcher: ...,
            callbacks: ...
        }
    ]
};

Обработчик событий — объект, функции которого получают событие, проверяют (за это отвечает matcher), требуется ли реакция, и в случае положительного ответа реагируют на событие — запускают callbacks.

matcher

Matcher — это RegExp, функция, строка с wildcard или массив, включающий в себя перечисленные варианты. Задачей matcher является определение того, вызывать ли обработчики по возникшему событию, результатом работы должно быть булево значение (true или false).

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

RegExp:

Достаточно простой и гибкий способ задать matcher:

exports.config = {
    ...
    workers: [
        {
            matcher: /\.txt$/,
            callbacks: [...]
        }
    ]
};

В этом варианте mjoe вызовет метод test() у регулярного выражения. Фактически, при входящем пути /home/afelix/sample/file.txt произойдёт следующий вызов:

/\.txt$/.test('/home/afelix/sample/file.txt')

что вернёт true и потому будут вызваны пользовательские функции.

Функция:

Более сложный, но более функциональный способ задать matcher:

exports.config = {
    ...
    workers: [
        {
            matcher: function(s, e) { return /\.txt$/.test(e.path) },
            callbacks: [...]
        }
    ]
};

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

Два аргумента у функции служат разным целям:

  • s — техническая строка вида 'ninotify 512 /test/file.txt' и она нужна в очень-очень редких случаях, используйте следующий аргумент.
  • e — объект события, содержащий как разобранные на составляющие атрибуты события, так и дополнительные свойства, если был задействован preprocessor; более детально об этом объекте написано в разделе Пользовательские функции (callbacks).

Строка с wildcard

Наконец, наиболее простой способ, шаблон задаётся в принятом в командной оболочке Unix формате:

  • * — все символы.
  • ? — один символ.
  • [последовательность] — любой символ последовательности.
  • [!последовательность] — любой символ вне последовательности.

Пример:

exports.config = {
    ...
    workers: [
        {
            matcher: '*.txt',
            callbacks: [...]
        }
    ]
};

Этот matcher сработает на всех файлах с расширением .txt.

callbacks

Callbacks — пользовательские функции, вызываемые для обработки событий. Можно указывать как одну функцию:

exports.config = {
    ...
    workers: [
        {
            matcher: ...,
            callbacks: echo
        }
    ]
};

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

exports.config = {
    ...
    workers: [
        {
            matcher: ...,
            callbacks: [echo, test, doSomething]
        }
    ]
};

Пользовательские функции (callbacks)

Функции, который вызываются после того, как соответствующий matcher определил, что событие следует обработать. Сигнатура функции такова:

function name(event) { ... }

где event — объект следующей структуры:

event {
    type: ...,
    mask: ...,
    path: ...
}

type — тип события. Строка, которая может потребоваться обработчику в особых случаях, но в подавляющем большинстве случаев не требуется. Например, у событий, порождаемых модулем ninotify, типом будет ninotify.

mask — числовая маска события. На данный момент в ней кодируются четыре события:

E_CREATE = 1,        // файл создан
E_DELETE = 2,        // файл удалён
E_MODIFY = 4,        // файл изменился
E_STATS_CHANGED = 8; // изменились атрибуты файла

использовать которые можно так:

var mjoe = global.mjoe;
..
function test(e) {
    if (e.mask & mjoe.E_MODIFY) {
        ...
    }
};

path — полный путь к директории / файлу, с которым произошло событие.

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

API

Для использования в конфигах mjoe предоставляет API — объект, который находится в global Node.js и подключается вот так:

var myFavoriteVariableName = global.mjoe;

Обязательным к использованию этот API не является, но на практике без него не обойтись.

pwd

Путь к директории, в которой находится конфиг mjoe.

exec

Часто возникает задача выполнить какую-нибудь командную строку. Для этих целей в Node.js есть функция exec(): принимает аргументом командную строку и выполняет её. Проблема в том, что это асинхронная функция — отдаёт управление и затем начинает выполнять команду.

Вот конфиг, использующий стандартный exec:

var exec = require('child_process').exec,
    print = require('sys').print;

exports.config = {
    workers: [{ matcher: '*', callbacks: [echo0, echo1] }]
};

function echo0() {
    console.log('start echo0');
    exec('echo 0 && sleep 5 && echo 1', ecb);
    console.log('finish echo0');
}

function echo1() {
    console.log('start echo1');
    exec('echo 2 && sleep 2 && echo 3', ecb);
    console.log('finish echo1');
}

function ecb(error, stdout, stderr) {
    print(stdout);
    print(stderr);
    if (error) print('error: ' + error);
}

Когда произойдёт событие, mjoe выполнит echo0 и echo1, вывод в консоли окажется таким:

start echo0
finish echo0
start echo1
finish echo1
2
3
0
1

Достаточно представить вместо echo0 долго работающий make и вместо echo1 быстрый make, одновременно меняющие одни и те же файлы, чтобы подумать о нужде последовательного выполнения. Потому mjoe предлагает синхронный exec:

var exec = global.mjoe.exec;

exports.config = {
    workers: [{ matcher: '*', callbacks: [echo0, echo1] }]
};

function echo0() {
    console.log('start echo0');
    exec('echo 0 && sleep 5 && echo 1');
    console.log('finish echo0');
}

function echo1() {
    console.log('start echo1');
    exec('echo 2 && sleep 2 && echo 3');
    console.log('finish echo1');
}

Вывод в консоли при событии:

start echo0
finish echo0
0
1
start echo1
finish echo1
2
3

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

Функция exec(path, [skipEvents = false]) принимает следующие аргументы:

  • path — командная строка, которую следует выполнить.
  • skipEvents — игнорировать ли во время выполнения командной строки события от файловой системы? по умолчанию mjoe события принимает и ставит в очередь на обработку; у вас должны быть веские причины для того, чтобы использовать exec(path, true).

E_CREATE

Событие файл создан. Использование: if (mask & mjoe.E_CREATE) ..

E_DELETE

Событие файл удалён. Использование: if (mask & mjoe.E_DELETE) ..

E_MODIFY

Событие файл изменён. Использование: if (mask & mjoe.E_MODIFY) ..

E_STATS_CHANGED

Событие атрибуты файла изменились. Использование: if (mask & mjoe.E_STATS_CHANGED) ..

Примеры конфигурационного файла

Минимальный

В matcher используется wildcard (любые символы), а в callbacks функция (вывести событие в консоль):

exports.config = {
    workers: [
        {
            matcher: '*',
            callbacks: function(e) { console.log(e) }
        }
    ]
};

При запуске в директории /home/afelix/sample/ и при добавлении файла test.txt в консоль логируется объект события:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt' }

RegExp matcher

В Минимальном конфиге matcher изменён на регулярное выражение вместо wildcard. Однако разница также и в том, что теперь callbacks будет вызываться только для файлов с расширением .txt:

exports.config = {
    workers: [
        {
            matcher: /\.txt$/,
            callbacks: function(e) { console.log(e) }
        }
    ]
};

При запуске в директории /home/afelix/sample/ и при добавлении файла test.txt поведение идентично Минимальному конфигу — в консоль логируется объект события:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt' }

Function matcher 1

В Минимальном конфиге matcher изменён на функцию вместо wildcard. Однако разница также и в том, что теперь callbacks будет вызываться только для файла test.txt:

exports.config = {
    workers: [
        {
            matcher: function(s) { return /test\.txt$/.test(s) },
            callbacks: function(e) { console.log(e) }
        }
    ]
};

При запуске в директории /home/afelix/sample/ и при добавлении файла test.txt поведение идентично Минимальному конфигу — в консоль логируется объект события:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt' }

Function matcher 2

Очевидно, технического вида строка в качестве аргумента функции matcher удобна далеко не всегда: ninotify 256 /home/afelix/sample/test.txt. Потому рекомендуется использовать второй аргумент: объект-событие, в котором информация о событии разобрана на составляющие.

В конфиге Function matcher 1 в функцию matcher добавлено использование второго аргумента, в остальном поведение идентично конфигу Function matcher 1:

exports.config = {
    workers: [
        {
            matcher: function(s, e) { return /test\.txt$/.test(e.path) },
            callbacks: function(e) { console.log(e) }
        }
    ]
};

Function matcher 3

Чтобы не загромождать workers исходным кодом, можно воспользоваться удобством JavaScript и вынести функции из workers.

В конфиге Function matcher 2 функции matcher и callbacks получили имена и вынесены за пределы workers, в остальном поведение идентично конфигу Function matcher 2:

exports.config = {
    workers: [
        {
            matcher: m_log,
            callbacks: c_log
        }
    ]
};

function m_log(s, e) {
    return /test\.txt$/.test(e.path);
}

function c_log(e) {
    console.log(e);
}

Массивы матчеров и пользовательских функций

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

exports.config = {
    workers: [
        {
            matcher: [
                '*.css',
                /\.js$/,
                function(s, e) { return /\.txt$/.test(e.path) },
                m_log
            ],
            callbacks: [
                c_log,
                function(e) { console.log('second') }
            ]
        }
    ]
};

function m_log(s, e) {
    return /\.html$/.test(e.path);
}

function c_log(e) {
    console.log('first');
}

При событии на файлах с расширениями .css, .js, .txt или .html вывод в консоль окажется таким:

first
second

Preprocessor

Минимальный конфиг дополнен подключением mjoe-API (global.mjoe), из которого используется путь к конфигу, а также добавлена функция preprocessor: rpath. Она дополняет объект события путём к файлу относительно директории, в которой находится конфиг:

var mjoe = global.mjoe;

exports.config = {
    preprocessor: rpath,
    workers: [
        {
            matcher: '*',
            callbacks: function(e) { console.log(e) }
        }
    ]
};

function rpath(e) {
    e.rpath = e.path.substring(mjoe.pwd.length + 1);
    return e;
}

При запуске в директории /home/afelix/sample/ и при добавлении файла test.txt в консоль логируется объект события, дополненный функцией preprocessor:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt',
  rpath: 'test.txt' }

События

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

В этом конфиге подключается mjoe-API (global.mjoe) с именами событий, а также расширена функция matcher. Обратите внимание на то, что события определяются не сравнением ===, но бинарным &:

var mjoe = global.mjoe;

exports.config = {
    workers: [
        {
            matcher: m_log,
            callbacks: c_log
        }
    ]
};

function m_log(s, e) {
    return /test\.txt$/.test(e.path) &&
           (e.mask & mjoe.E_MODIFY || e.mask & mjoe.E_CREATE);
}

function c_log(e) {
    console.log(e);
}

При запуске в директории /home/afelix/sample/ и только при изменении или добавлении файла test.txt в консоль логируется объект события:

{ type: 'ninotify',
  mask: 1,
  path: '/home/afelix/sample/test.txt' }

Exclude

В типовом проекте некоторые директории не нуждаются в обработке, например, .svn.

В этом конфиге директория foo исключается из дальнейшей обработки:

exports.config = {
    exclude: '*foo',
    workers: [
        {
            matcher: '*',
            callbacks: function(e) { console.log(e) }
        }
    ]
};

Если во время работы mjoe в foo что-либо произойдёт, mjoe никак не отреагирует, даже события оттуда не получит.