Недавно в DataGuard пришлось разбираться с запуском внешних программ из java. Некоторые вещи оказались довольно нетривиальны, так что я решил поделиться.
На первый взгляд, все достаточно просто -- для простых случаев есть
Runtime.exec(), если нужно настроить параметры среды для запуска -- есть
ProcessBuilder. В любом случае получаем объект Process, у которого вызываем
Process.waitFor(), чтобы дождаться завершения -- и, вроде бы, все?
К сожалению, ничего подобного. Несмотря на то, что API выглядит просто и очевидно, корректное его использование совсем не просто, и не очевидно. Какие конкретно подводные камни нас ждут?
Главный из них -- потоки ввода-вывода (IO streams). У порождаемого процесса нет терминала, к которому он привязан, его stdin, stdout, stderr выдаются порождающему процессу -- то есть, нам. Причем обрабатывать их -- наша
обязанность. Потоки, созданные ОС имеют ограниченный размер буфера. Если, к примеру, буфер stdout для запущенного процесса заполнен, со стороны java никто его не читает (==не освобождает) а процесс настойчиво хочет что-то вывести -- то процесс просто окажется заблокирован на IO, и будет ждать, пока stdout кто-нибудь освободит. Если мы не предусмотрели в java код, читающий
process.getInputStream() -- получается стандартный дедлок: мы ждем завершения процесса, процесс ждет нас.
Самый опасный момент здесь в том, что размер буфера заранее не определен. Поэтому приложение может в одном случае работать как часы, а в другом -- непонятно зависать.
Конкретный пример: в обычном, штатном режиме работы внешний процесс выдает одну-единственную строчку "Ок" и завершается. Строчка вполне влезает в буфер, поэтому код
final Process p = Runtime.getRuntime().exec( "my-script.bat" );
final int retCode = p.waitFor();
работает корректно. Но наступает день Х, когда звезды складываются неудачно. И процесс завершается с ошибкой. И, как и положено уважающей себя программе, старается эту ошибку максимально подробно описать. И пытается вывести в stdout простыню текста, превышающую размер буфера. Вуаля -- процесс ждет на выводе, java-программа -- на
process.waitFor()
На мой взгляд -- это пример плохо спроектированного API. Простая вещь -- запустить внешний процесс не заморачиваясь с его выводом -- делается весьма нетривиально. Более того, из самого API это никак не следует. Да, в документации к Process это прописано, но я считаю, что хороший API это такой, использование которого, по крайней мере для простых задач, очевидно без документации. Можно было бы дополнить контракт, например, так:
"если клиент не запросил process.getInputStream()/process.getErrorStream() до вызова process.waitFor() -- stdout/stderr внешнего процесса автоматически перенаправляются вникуда".
Но наши друзья из Sun этого не сделали, так что приходится отдуваться самим:
ProcessRunner
Что делает: берет сконфигурированный ProcessBuilder, создает внешний процесс, запускает асинхронно "помпы", прокачивающие его потоки ввода-вывода либо в пустоту (если пользователь ничего не задал) либо из/в заранее заданные потоки. Метод
ProcessRunner.execute() блокируется пока либо процесс не завершится, либо пока не будет вызван
ProcessRunner.interrupt(). Пример использования:
final ProcessBuilder pb = new ProcessBuilder("my-script.bat");
final ExecutorService pool = Executors.newFixedThreadPool(3); // нужно минимум 3 свободных потока в пуле
final ProcessRunner pwd = new ProcessRunner( "run", pb, pool );
pwd.execute();
final int retCode = pwd.getReturnCode();
...
pool.shutdown();
В этом примере ввод-вывод
my-script.bat будет просто выброшен. Другой пример:
final ProcessBuilder pb = ...;
final ProcessRunner pwd = new ProcessRunner( "run", pb, POOL );
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ByteArrayOutputStream err = new ByteArrayOutputStream();
pwd.setOutputStream( out );
pwd.setErrorStream( err );
pwd.execute();
assertEquals( 0, pwd.getReturnCode() );
final byte[] output = out.toByteArray();
final byte[] errors = err.toByteArray();
Здесь stdout/stderr будут считаны в предоставленные нами потоки. Обратите внимание, что если флаг
ProcessBuilder.redirectErrorStream() выставлен в true, то stderr будет слит с stdout, и errors будет пуст.
Больше примеров использования можно посмотреть в тестах
ProcessRunnerTest