...

среда, 24 июля 2013 г.

[Из песочницы] Запуск внешних процессов в Scala

Введение




В одном из моих домашних проектов требовалось написать небольшой менеджер внешних процессов. Приложение должно было уметь запускать внешний демон, периодически контролировать его состояние, когда нужно выключать, включать, менять настройки и т.д. Существуюший функционал в 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.txt\n2.txt\n3.txt\n11.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 пока далеко.

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends: 'You Say What You Like, Because They Like What You Say' - http://www.medialens.org/index.php/alerts/alert-archive/alerts-2013/731-you-say-what-you-like-because-they-like-what-you-say.html


Комментариев нет:

Отправить комментарий