Введение
В одном из моих домашних проектов требовалось написать небольшой менеджер внешних процессов. Приложение должно было уметь запускать внешний демон, периодически контролировать его состояние, когда нужно выключать, включать, менять настройки и т.д. Существуюший функционал в Java для подобных задач весьма скуден, а так как я одновременно разбирался со Scala'ой, то решил посмотреть: как у нее дела с этим. И я был приятно удивлен: Scala предлагает по моему мнению неплохое API для работы с внешними процессами.
В этой статье я хотел бы рассказать об этом подробнее.
Запуск процессов
В основе работы с процессами лежат два трейта: это scala.sys.process.Process и scala.sys.process.ProcessBuilder.
Process позволяет работать с уже запущенным процессом, а ProcessBuilder позволяет настроить параметры запуска.
Находятся эти сущности в пакете scala.sys.process. Для запуска простого примера следует выполнить код:
scala> import scala.sys.process._
scala> val process: Process = Process("echo Hello World").run()
scala> println(process.exitValue())
Метод run — это основной метод для запуска процесса, декларация которого расположена в трейте ProcessBuilder. Возвращает ссылку на объект типа Process. Запущенный процесс работает в фоне, вывод данных осуществляется в консоли. В трейте Process объявлено два метода:
exitValue()
— ожидает завершение выполнения процесса и возвращает код завершения;destroy()
— уничтожает запущенный процесс.
Этот трейт очень похож на стандартный Java класс java.lang.Process.
В трейте ProcessBuilder существуют более специализированные методы для запуска процессов. Приведу краткое описание основных:
!
— запускает процесс, ожидает завершение выполнения, данные выводит на консоль, а код завершения процесса возвращает как результат;!!
— запускает процесс, ожидает завершение выполнения, данные выводит в консоли, если код завершения отличен от нуля — выбрасывает исключение, как результат возвращает выходные данные процесса в виде строки;lines
— запускает процесс, возвращает Stream[String]. Этот поток позволяет параллельно выполнению процесса читать данные процесса. В случае, если информация не доступна, Stream блокируется и будет ожидать, пока информация вновь появится, либо процесс завершит выполнение. В случае, если код завершения процесса будет отличен от нуля, метод вызовет исключение. Чтобы исключение не возникало, следует вызывать lines_!;run
— запускает процесс и возвращает ссылку на Process.
В моем проекте мне не нужно было хранить ссылки на внешние процессы, поэтому метод run я почти не использовал. А вот метод !
как раз подходил для меня.
Предыдущий пример можно переписать так:
scala> Process("echo Hello World!").!
Hello World!
res1: Int = 0
scala> Process("echo Hello World!").!!
res2: String = "Hello World!"
scala> Process("echo Hello World!").lines
res3: Stream[String] = Stream(Hello World!, ?)
Неявное приведение типов
Существуют методы неявного(implicit) приведения строк(java.lang.String) и последовательностей(scala.collection.Seq) к трейту ProcessBuilder.
Мы можем записать наш код так:
scala> "echo Hello World!".!
Hello World!
res2: Int = 0
или так:
scala> Seq("echo", "Hello", "World!").!
Hello World!
res3: Int = 0
В значительной степени это сокращает и упрощает запись, кроме того код становится более понятным.
А это уже в свою очередь уменьшает количество ошибок в будущем.
Комбинирование процессов(Pipe)
Вызовы процессов можно комбинировать в цепочки, схожиие с цепочками команд в linux.
scala> "ls".!
11.txt
1.txt
2.txt
3.txt
res2: Int = 0
scala> ("ls" #| "grep 1").!
11.txt
1.txt
res6: Int = 0
Вывод команды ls был направлен на вход grep. Греп отфильтровал полученную информацию по вхождению 1.
Можно выполнять условные операции, например:
scala> ("find . -name *.txt -exec grep 0 {} ;" #| "xargs test -z" #&& "echo 0-free" #|| "echo 0-exists").!
0-exists
res23: Int = 0
Здесь, если в директории существуют файлы с расширением *.txt и в каком нибудь из них, в тексте присутствует 0 — на консоль выведет 0-exists, в противном случае 0-free.
#&&
— выполняет следующую комманду, если предыдущая выполнена корректно;
#||
— выполняет следующую комманду, если предудыщая выполнена с ошибками.
Этот функционал нравится мне больше всего, позволяет использовать linux подобный pipe внутри Scala и писать небольшие sh скрипты прямо внутри своего кода.
Переопределение потоков ввода/вывода
Весь наш код неудобен и бесполезен без функционала переопределения ввода/вывода внешних процессов.
Часто требуется следить за выдаваемой информацией, чтобы, например, расшифровать возникшую ошибку, или удостовериться, что все работает корректно.
В трейте ProcessBuilder в каждый из методов run, !, !!, lines можно передавать инстанс трейта ProcessLogger, который позволяет перенаправить выходные потоки программы в файл или строку.
Вот как с помощью ProcessLogger можно подсчитать количество строк, напечатанных процессом:
scala> var normalLines = 0
normalLines: Int = 0
scala> var errorLines = 0
errorLines: Int = 0
scala> val countLogger = ProcessLogger(line => normalLines += 1,
| line => errorLines +=1)
countLogger: scala.sys.process.ProcessLogger = scala.sys.process.ProcessLogger$$anon$1@459c8859
scala> "ls" ! countLogger
res0: Int = 0
scala> println("normalLines: " + normalLines + ", errorLines: " + errorLines)
normalLines: 4, errorLines: 0
ProcessLogger позволяет переопределить потоки вывода. Для переопределения как ввода, так и вывода используется также класс scala.sys.process.ProcessIO.
Небольшой пример:
Seq("grep", "1") run new ProcessIO((output: java.io.OutputStream) => {
output.write("1.txtn2.txtn3.txtn11.txt".getBytes)
output.close()
}, (input: java.io.InputStream) => {
println(Source.fromInputStream(input).mkString)
input.close()
}, _.close())
Первый параметр — это поток ввода в процесс: сюда пишем исходные данные.
Второй параметр — это стандартный вывод, а последний — вывод для ошибок.
Параметры представляют собой функции, обрабатывающие необходимые потоки.
Ранее я говорил, что исполнение внешних команд можно комбинировать, кроме того, с помощью такой же формы записи можно передавать данные в процесс, или считывать их оттуда.
Передать данные из файла в процесс можно с помощью метода #<
, а записывать — с помощью метода #>
:
scala> ("echo -e 1.txt\n2.txt\n3.txt" #> new java.io.File("1.txt")).!
res21: Int = 0
scala> ("grep 1" #< new java.io.File("1.txt")).!!
res22: String =
"1.txt"
Таким же путем можно, например, выполнить копирование информации из одного файла в другой:
scala> (new java.io.File("1.txt") #> new java.io.File("2.txt")).!
res23: Int = 0
scala> "cat 2.txt".!
1.txt
2.txt
3.txt
res24: Int = 0
Заключение
В статье я рассказал об основах работы с внешними процессами в Scala. В Java для реализации подобного мне бы пришлось писать кучу врапперов, и в итоге, все равно не удалось бы приблизиться к такой простоте. Почитать подробнее о API можно по ссылке http://www.scala-lang.org или покопаться в исходниках(что я и делал, например взял некоторые примеры оттуда).В jdk1.7 немного расширили класс java.lang.ProcessBuilder, и в Java стало удобнее запускать и выполнять внешние команды. Но до простосты Scala, jdk пока далеко.
Автор: vayho