mjoe v0.1.4
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' }
Предварительные требования:
- OS Linux
- nodejs версии 0.4.x — http://nodejs.org
- npm — http://github.com/isaacs/npm/
Установка
Для установки 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 никак не отреагирует, даже события оттуда не получит.
13 years ago