通过stdin / stdout进行过程交互

时间:2018-08-13 14:42:38

标签: scala process io

我正在尝试建立一个类,该类启动等待stdin的系统进程。该类应具有另一个方法,该方法采用字符串,将其输入系统进程,然后返回进程的输出。

原因是启动该过程涉及加载大量数据,因此需要一段时间。 我正在尝试使用bc进行虚拟测试,以便启动bc并等待输入。我会设想这样的界面:

case class BcWrapper(executable: File) {
  var bc: Option[???] = None

  def startBc(): Unit = bc = Some(???)

  def calc(input: String): String = bc.get.???

  def stopBc(): Unit = bc.get.???
}

我希望能够像这样使用它:

val wrapper = BcWrapper(new File("/usr/bin/bc"))
wrapper.startBc()
val result1 = wrapper.calc("1 + 1") // should be "2"
val result2 = wrapper.calc(???)
[...]
wrapper.stopBc()

在多个问题中都涉及到这个主题,但是对于这样的用例却从未完全回答。 This questionthis one似乎已经接近。但是,我不确定如何实现ProcessLogger,也不确定是否首先使用它。

不幸的是,Scala documentation也不是很精致。

请注意,我不想从stdin中读取内容,但是想调用一个函数。

背景是我要读取一个大文件,一行一行地读取它,预处理行,将它们传递给外部进程,然后对输出进行后处理。

1 个答案:

答案 0 :(得分:2)

您可以得到类似但更简单的东西。

import sys.process._
import util.Try

class StdInReader(val reader :String) {
  def send(input :String) :Try[String] =
    Try(s"/bin/echo $input".#|(reader).!!.trim)
}

用法:

val bc = new StdInReader("/usr/bin/bc")
bc.send("2 * 8")   //res0: scala.util.Try[String] = Success(16)
bc.send("12 + 8")  //res1: scala.util.Try[String] = Success(20)
bc.send("22 - 8")  //res2: scala.util.Try[String] = Success(14)

发送非零退出代码(bc不发送)的程序将得到Failure()

如果您需要更细粒度的控制,则可以从这样的内容开始并进行扩展。

import sys.process._

class ProcHandler(val cmnd :String) {
  private val resbuf = collection.mutable.Buffer.empty[String]
  def run(data :Seq[String]) :Unit = {
    cmnd.run(new ProcessIO(
      in => {
        val writer = new java.io.PrintWriter(in)
        data.foreach(writer.println)
        writer.close()
      },
      out => {
        val src = io.Source.fromInputStream(out)
        src.getLines().foreach(resbuf += _)
        src.close()
      },
      _.close()  //maybe create separate buffer for stderr?
    )).exitValue()
  }
  def results() :Seq[String] = {
    val rs = collection.mutable.Buffer.empty[String]
    resbuf.copyToBuffer(rs)
    resbuf.clear()
    rs
  }
}

用法:

val bc = new ProcHandler("/usr/bin/bc")
bc.run(List("4+5","6-2","2*5"))
bc.run(List("99/3","11*77"))
bc.results()  //res0: Seq[String] = ArrayBuffer(9, 4, 10, 33, 847)

好的,我做了一些研究,发现了这一点。它看起来可以满足您的要求,但是有局限性。特别是,该过程将保持开放状态以供输入,直到您要获取输出为止。到那时,IO流将关闭以确保刷新所有缓冲区。

import sys.process._
import util.Try

class ProcHandler(val cmnd :String) {
  private val procInput = new java.io.PipedOutputStream()
  private val procOutput = new java.io.PipedInputStream()
  private val proc = cmnd.run( new ProcessIO(
    { in => // attach to the process's internal input stream
      val istream = new java.io.PipedInputStream(procInput)
      val buf = Array.fill(100)(0.toByte)
      Iterator.iterate(istream.read(buf)){ br =>
        in.write(buf, 0, br)
        istream.read(buf)
      }.takeWhile(_>=0).toList
      in.close()
    },
    { out => // attach to the process's internal output stream
      val ostream = new java.io.PipedOutputStream(procOutput)
      val buf = Array.fill(100)(0.toByte)
      Iterator.iterate(out.read(buf)){ br =>
        ostream.write(buf, 0, br)
        out.read(buf)
      }.takeWhile(_>=0).toList
      out.close()
    },
    _ => () // ignore stderr
  ))
  private val procO = new java.io.BufferedReader(new java.io.InputStreamReader(procOutput))
  private val procI = new java.io.PrintWriter(procInput, true)
  def feed(str :String) :Unit = procI.println(str)
  def feed(ss :Seq[String]) :Unit = ss.foreach(procI.println)
  def read() :List[String] = {
    procI.close()  //close input before reading output
    val lines = Stream.iterate(Try(procO.readLine)){_ =>
      Try(procO.readLine)
    }.takeWhile(_.isSuccess).map(_.get).toList
    procO.close()
    lines
  }
}

用法:

val bc = new ProcHandler("/usr/bin/bc")
bc.feed(List("9*3","4+11"))  //res0: Unit = ()
bc.feed("4*13")              //res1: Unit = ()
bc.read()                    //res2: List[String] = List(27, 15, 52)
bc.read()                    //res3: List[String] = List()

好的,这是我关于这个主题的最终决定。我认为这会打勾您的愿望清单上的所有项目:仅启动一次该过程,直到激活前它一直处于活动状态,并允许交替进行书写和阅读。

import sys.process._

class ProcHandler(val cmnd :Seq[String]) {
  private var os: java.io.OutputStream = null
  private var is: java.io.InputStream = null
  private val pio = new ProcessIO(os = _, is = _, _.close())
  private val proc = cmnd.run(pio)
  def feed(ss :String*) :Unit = {
    ss.foreach(_.foreach(os.write(_)))
    os.flush()
  }
  def ready :Boolean = is.available() > 0
  def read() :String = {
    Seq.fill[Char](is.available())(is.read().toChar).mkString
  }
  def close() :Unit = {
    proc.exitValue()
    os.close()
    is.close()
  }
}

仍然存在问题,还有很大的改进空间。 IO是在基本级别(流)上处理的,我不确定在此执行的操作是否完全安全和正确。输入feed()是必需的,以提供必要的NewLine终止符,而输出read()只是原始的String,没有分成漂亮的字符串结果集合。

请注意,如果客户端代码无法close()完成所有进程,这将浪费系统资源。

还请注意,阅读不会等待内容(即无阻塞)。写入后,响应可能不会立即可用。

用法:

val bc = new ProcHandler(Seq("/usr/bin/bc","-q"))
bc.feed("44-21\n", "21*4\n")
bc.feed("67+11\n")
if (bc.ready) bc.read() else "not ready" // "23\n84\n78\n"
bc.feed("67-11\n")
if (bc.ready) bc.read() else "not ready"  // "56\n"
bc.feed("67*11\n", "1+2\n")
if (bc.ready) bc.read() else "not ready"  // "737\n3\n"
if (bc.ready) bc.read() else "not ready"  // "not ready"
bc.close()