Работа с bash pipe из java
Постановка задачи
Не так давно передо мной встала задача работать с нативным приложением из java. Это приложение - rtl_sdr. Суть его работы достаточно простая: оно подключается к USB, читает данные и пишет их в файл или в stdout.
А дальше
Обычно нативные приложения не рекомендуется запускать из JVM. Дело в том, что как только управление передаётся из JVM вовне, то все предоставленные гарантии JVM теряются. Но если хочется, то вызвать приложение можно так:
Process process = new ProcessBuilder().command("rtl_sdr", "-arg1", "value1", ...).start();
int exitCode = process.waitFor();
Здесь все достаточно просто:
- запускается процесс
- Java поток ожидает пока приложение завершится
Однако, тут сразу же возникает проблема: rtl_sdr будет работать пока не получит команду от пользователя остановиться. Поэтому этот метод никогда не завершится. Значит для этого необходимо сделать отдельный метод, который бы вызывался с другого потока:
public void complete() {
process.destroy();
}
Новые требования
Итак, код работает в проде уже несколько месяцев. Но тут выясняется, что raspberry pi не успевает записывать на флэшку. Я провел тесты и убедился, что сжатие входящих данных сильно ускоряет систему и делает её более стабильной.
Самый простой способ сделать сжатие можно следующим способом:
byte[] buf = new byte[BUF_SIZE];
while (!Thread.currentThread().isInterrupted()) {
int r = process.getInputStream().read(buf);
if (r == -1) {
break;
}
gzipOutputStream.write(buf, 0, r);
}
gzipOutputStream.flush();
Но такой способ оказался достаточно медленным. Дело в том, что данные копировались в память JVM, а потом уже в gzip. Но если не писать через JVM, то остаётся только способ каким-нибудь образом использовать bash pipe. Немного повозившись с реализацией, у меня получилось следующее:
Process process = new ProcessBuilder().command("rtl_sdr_wrapper.sh").start();
int exitCode = process.waitFor();
И код враппера:
rtl_sdr -arg1 value1 ... | gzip > output.gz
Почти сразу после релиза стало понятно, что сломалась остановка процесса. Дело в том, что этот bash скрипт создаёт 2 подпроцесса. При вызове process.destroy();
завершается сам скрипт и gzip. rtl_sdr при этом остаётся работать. Пришлось повозиться с bash-магией и получилось следующее:
_term() {
kill -TERM "$rtl" 2>/dev/null
}
rtl_sdr -arg1 value1 ... | gzip > output.gz &
rtl=$(jobs -p)
child=$!
wait "$child"
Что же здесь происходит?
- устанавливается обработчик сигнала TERM. Этот сигнал посылает JVM при вызове метода destroy
- запускается pipe и переходит в фоновый режим
- получается pid первого процесса - rtl_sdr
- скрипт начинает ожидать, когда завершиться процесс rtl_sdr
Последним штрихом необходимо правильно передать код возврата. Есть две ситуации завершения работы:
- Нормальная работа. При нормальной работе rtl_sdr всегда должна завершаться через kill. И тогда статус должен быть 143. Однако, если завершать pipe с помощью
kill -TERM
, то pipe вернёт код возврата последней команды - gzip. И тогда этот код передастся команде wait, и она пробросит его выше. Этот код будет 0. - Завершение с ошибкой. Код возврата в таком случае 0. Он должен опять же проброситься в команду wait и дальше в Java.
Решается эта проблема следующей командой:
set -o pipefail
Она заставляет pipe вернуть код первой команды, которая завершилась не статусом 0.
Выводы
Если необходимо оптимизировать обработку данных на уровне операционной системы, то можно использовать bash pipe. И да, его можно вызывать из Java. Надо только делать это очень осторожно.