На первый взгляд, все достаточно просто -- для простых случаев есть
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
Есть небольшой вопрос. Когда запущенный процесс отработал выход из программы не происходит, в чем может быть проблема?
ОтветитьУдалитьВыхода из какой программы -- из джава-программы? Это может быть связано с тем, что пул создает non-daemon threads, и рантайм ждет их завершения. Нужно пулу сделать shutdown()
ОтветитьУдалить