Запуск внешних процессов в Scala

в 5:26, , рубрики: java, linux, 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.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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js