Данный пост посвящен запуску сторонних приложений из node.js и дальнейшим контролем за их выполнением. Эта
задача может эффективно решаться с помощью nodejs, потому он и был выбран на одном из проектов. В
официальной документации описаны все возможные события, есть примеры использования, но для enterprise
применения не хватало информация по внутренней реализации логики работы потоков (stdin
, stdout
, stderr
)
и логике их открытия и закрытия. Последней каплей стал ответ на stackoverflow с примерами в которых,
событие exit
при остановке процесса приходит, а close
- нет. Стоило кому-то разобраться в этой неразберихе.
Типичная задача что требуется решить выглядит просто:
- наш nodejs апликейшн запускает приложение
app
; - контроллирует успешность запуска, а при неудачном запуске уведомляет пользоввателя или другие подсистемы;
- следит за штатным/нештатным завершением
app
и принимает решения на базе этих знаний; - обслуживает ввод/вывод через
stdin
/stdout
/stderr
.
Упомянутое выше приложение app
может вести себя нестандартно и это надо учитывать. Например, иногда приложения
продолжают работать, но закрывают поток stdin
или stdout
.
Любознательные могут прочитать весь ответ на stackoverflow, а ниже дан более простой пример отбражающий проблему на которую я ищу ответ.
// Equivalent to "yes | cat".
cp = require('child_process').spawn('yes');
cp.on('exit', console.log.bind(console, 'exited'));
cp.on('close', console.log.bind(console, 'closed'));
cpNext = require('child_process').spawn('cat');
cp.stdout.pipe(cpNext.stdin);
setTimeout(function() {
cp.kill();
console.log('Called kill()');
}, 500);
Этот пример может быть запущен на linux, unix и freebsd, я тестировал поведение на node 0.12 и выше.
Программа запускает приложение yes
(входит почти во все дистрибутивы), которое только и делает что в цикле
пишет в stdout
букву y
и перевод строки. Также запускается команда cat
которая все что поступает в stdin
выводит в stdout
и ничего более не делает. cp.stdout.pipe(cpNext.stdin)
весь вывод команды yes
направляет
на вход команде yes
.
Логично ожидать, что после kill
команды yes
произойдет закрытие stdout
команды yes
, а затем и закрытие
stdin
у команды cat
, что приведет к остановке выполнения команды cat
.
А на деле бесконечно долго может работтать код выше, но событие close
не придет. Вывод будет следующим:
Called kill()
exited null SIGTERM
Добавление cp.stdout.destroy()
после kill()
приведет к броску события close
, добавление cp.stdout.destroy()
после события exit
приведет к броску события… Минуточку, после события exit
программа yes
уже остановлена,
стрим stdout
программы yes
не существует более. Давайте повесим слушателей на события close
и end
стримов
stdout
и stderr
. Поскольку они заявлены как stream.Readable
, то будем руководствоваться
официальной документацией.
// Equivalent to "yes | cat".
cp = require('child_process').spawn('yes');
cp.on('exit', console.log.bind(console, 'exited'));
cp.on('close', console.log.bind(console, 'closed'));
cp.stdout.on('close', console.log.bind(console, 'stdout closed'));
cp.stdout.on('end', console.log.bind(console, 'stdout end'));
cp.stderr.on('close', console.log.bind(console, 'stderr closed'));
cp.stderr.on('end', console.log.bind(console, 'stderr end'));
cpNext = require('child_process').spawn('cat');
cp.stdout.pipe(cpNext.stdin);
setTimeout(function() {
cp.kill();
console.log('Called kill()');
}, 500);
Результат выполнения будет следующим:
Called kill()
stderr end
exited null SIGTERM
stderr closed false
Обратимся к официальной документации https://nodejs.org/docs/v8.4.0/api/child_process.html#child_process_event_close.
The ‘close’ event is emitted when the stdio streams of a child process have been closed. This is distinct from the ‘exit’ event, since multiple processes might share the same stdio streams.
Note that when the ‘exit’ event is triggered, child process stdio streams might still be open.
Если верить описанию, child process завершился, при завершении закрываются потоки, значит событие close
должно было прийти, но есть у события exit
сноска… Обьясненение этих самых случаев в описании отсуствует.
Теперь приступим к анализу и детальному разбору работы stdio
и первым делом обратимся к
man по stdio, это касается только реализации на C
, но большинство
языков вроде Java ипользуют схожий подход в управлении потоками, к тому же, ядро linux работает согласно этой
документации. Выделим важные моменты:
- стандартный стримы
stdin
,stdout
,stderr
создаются библиотекойlibc
для любойC
программы автоматически; stdin
иstdout
являются буфризированными, аstderr
- нет (на практике это означает, что запись вstdout
из программы не приводит к моментальной отправке слушателю, а накапливается и только затем отправляется);- при выходе из
main
функции или вызове функцииexit
, стримыstdin
,stdout
,stderr
закрываются и буфера флашатся (данные отправляются слушателю); - при аварийном завершении или вызове
abort
данные что были в буфере удаляются и не будут доставлены; - буфер может флашится программой принудительно, а в случае с
stdout
может быть выключен.
https://www.reddit.com/r/unix/comments/6gxduc/how_is_gnu_yes_so_fast/ https://stackoverflow.com/questions/29176636/can-someone-please-explain-how-stdio-buffering-works http://www.gnu.org/software/coreutils/manual/html_node/stdbuf-invocation.html https://github.com/coreutils/coreutils/blob/master/src/yes.c https://nodejs.org/api/child_process.html#child_process_event_close
Как видно, события end
и close
на stdout
не произошло.
С багом можно ознакомится по ссылке “https://stackoverflow.com/questions/37522010/difference-between-childprocess-close-exit-events” В процессе прототипирования и экспериментов была обнаружена заметка на stackoverflow
Доводилось ли запускать из node.js процессы и контроллировать
Данному посту начало положил
Существующая команда
$ node test.js "ls"
[1504184499.122] stdout emits 'resume' with arguments []
[1504184499.125] stderr emits 'resume' with arguments []
[1504184499.125] stdin emits 'resume' with arguments []
[1504184499.126] stdin emits 'readable' with arguments []
[1504184499.126] stdin emits '_socketEnd' with arguments []
[1504184499.127] stdin emits 'prefinish' with arguments []
[1504184499.127] stdin emits 'finish' with arguments []
[1504184499.127] stdin emits 'end' with arguments []
[1504184499.128] stdout emits 'data' with arguments "test.js
"
[1504184499.128] stderr emits 'readable' with arguments []
[1504184499.129] stderr emits '_socketEnd' with arguments []
[1504184499.129] stderr emits 'end' with arguments []
[1504184499.129] stderr emits 'prefinish' with arguments []
[1504184499.129] stderr emits 'finish' with arguments []
[1504184499.129] spawn emits 'exit' with arguments [0,null]
[1504184499.130] stderr emits 'close' with arguments [false]
[1504184499.130] stdin emits 'close' with arguments [false]
[1504184499.130] stdout emits 'readable' with arguments []
[1504184499.130] stdout emits '_socketEnd' with arguments []
[1504184499.130] stdout emits 'end' with arguments []
[1504184499.130] stdout emits 'prefinish' with arguments []
[1504184499.130] stdout emits 'finish' with arguments []
[1504184499.130] stdout emits 'close' with arguments [false]
[1504184499.130] spawn emits 'close' with arguments [0,null]
Несуществующая команда
$ node test.js "lsss"
[1504184596.861] spawn emits 'error' with arguments [{"code":"ENOENT","errno":"ENOENT","syscall":"spawn lsss","path":"lsss","spawnargs":[]}]
[1504184596.864] stdout emits 'resume' with arguments []
[1504184596.864] stderr emits 'resume' with arguments []
[1504184596.864] stdin emits 'resume' with arguments []
[1504184596.865] stdout emits 'readable' with arguments []
[1504184596.866] stdout emits '_socketEnd' with arguments []
[1504184596.866] stdout emits 'end' with arguments []
[1504184596.867] stdout emits 'prefinish' with arguments []
[1504184596.867] stdout emits 'finish' with arguments []
[1504184596.867] stderr emits 'readable' with arguments []
[1504184596.868] stderr emits '_socketEnd' with arguments []
[1504184596.868] stderr emits 'end' with arguments []
[1504184596.868] stderr emits 'prefinish' with arguments []
[1504184596.868] stderr emits 'finish' with arguments []
[1504184596.868] stderr emits 'close' with arguments [false]
[1504184596.868] stdout emits 'close' with arguments [false]
[1504184596.868] spawn emits 'close' with arguments [-2,null]
[1504184596.868] stdin emits 'close' with arguments [false]
Команда что убивается по SIGKILL
$ node test.js node selfKiller.js
[1504185101.006] stdout emits 'resume' with arguments []
[1504185101.010] stderr emits 'resume' with arguments []
[1504185101.010] stdin emits 'resume' with arguments []
[1504185101.079] stdout emits 'data' with arguments "going to kill myself!
"
[1504185101.090] stderr emits 'readable' with arguments []
[1504185101.091] stderr emits '_socketEnd' with arguments []
[1504185101.092] stderr emits 'end' with arguments []
[1504185101.093] stderr emits 'prefinish' with arguments []
[1504185101.093] stderr emits 'finish' with arguments []
[1504185101.093] stderr emits 'close' with arguments [false]
[1504185101.093] stdout emits 'readable' with arguments []
[1504185101.094] stdout emits '_socketEnd' with arguments []
[1504185101.094] stdout emits 'end' with arguments []
[1504185101.095] stdout emits 'prefinish' with arguments []
[1504185101.095] stdout emits 'finish' with arguments []
[1504185101.095] stdin emits 'readable' with arguments []
[1504185101.095] stdin emits '_socketEnd' with arguments []
[1504185101.095] stdin emits 'prefinish' with arguments []
[1504185101.095] stdin emits 'finish' with arguments []
[1504185101.096] stdin emits 'end' with arguments []
[1504185101.097] spawn emits 'exit' with arguments [null,"SIGKILL"]
[1504185101.097] stdin emits 'close' with arguments [false]
[1504185101.097] stdout emits 'close' with arguments [false]
[1504185101.097] spawn emits 'close' with arguments [null,"SIGKILL"]
Команда что закрывает stdout, stderr и stdin