5 марта 2010 г.

Запуск внешних программ

Недавно в 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

2 комментария:

  1. Есть небольшой вопрос. Когда запущенный процесс отработал выход из программы не происходит, в чем может быть проблема?

    ОтветитьУдалить
  2. Выхода из какой программы -- из джава-программы? Это может быть связано с тем, что пул создает non-daemon threads, и рантайм ждет их завершения. Нужно пулу сделать shutdown()

    ОтветитьУдалить