How to perform non-blocking reading stdout from a subprocess in clojure?

断了今生、忘了曾经 提交于 2019-12-11 04:25:32

问题


I wish to spawn a long-running sub-process from clojure and communicate with this process via the standard streams.

Using the conch library, I can spawn and read the process, and read data from the out stream:

(def my-process (sh/proc "my_dumb_process"))
  ; read 10 lines from my-process's stdout. Will block until 10 lines taken
  (take 10 (line-seq (clojure.java.io/reader (:out p))))

I want to invoke an asynchronous callback whenever my-process prints to stdout - whenever data is available in the stdout stream.

I'm a bit new to clojure - is there an idiomatic clojur-ey way to do this? I've looked through core.async which is nice but I can't find a non-blocking solution for streams.


回答1:


A sample shell script for our purposes (be sure to make it executable), place it in the root of your clojure project for easy testing:

$ cat dumb.sh
#!/bin/bash

for i in 1 2 3 4 5
do
    echo "Loop iteration $i"
    sleep 2
done

Now we will define the process to execute, start it, and get stdout ((.getInputStream process)), read one line at a time and loop until we're done. Reads in real time.

(defn run-proc
  [proc-name arg-string callback]
  (let [pbuilder (ProcessBuilder. (into-array String [proc-name arg-string]))
        process (.start pbuilder)]
    (with-open [reader (clojure.java.io/reader (.getInputStream process))]
      (loop []
        (when-let [line (.readLine ^java.io.BufferedReader reader)]
          (callback line)
          (recur))))))

To test:

(run-proc "./dumb.sh" "" println)
About to start...
Loop iteration 1
Loop iteration 2
Loop iteration 3
Loop iteration 4
Loop iteration 5
=> nil

This function will block, as will the call to your callback; you can wrap in a future if you want it to run in a separate thread:

(future (callback line))

For a core.async-based approach:

(defn run-proc-async
  [proc-name arg-string callback]
  (let [ch (async/chan 1000 (map callback))]
    (async/thread
      (let [pbuilder (ProcessBuilder. (into-array String [proc-name arg-string]))
            process (.start pbuilder)]
        (with-open [reader (clojure.java.io/reader (.getInputStream process))]
          (loop []
            (when-let [line (.readLine ^java.io.BufferedReader reader)]
              (async/>!! ch line)
              (recur))))))
    ch))

This applies your callback function as a transducer onto the channel, with the result being placed on the channel which the function returns:

(run-proc-async "./dumb.sh" "" #(let [cnt (count %)]
                                  (println "Counted" cnt "characters")
                                  cnt))

#object[clojure.core.async.impl.channels.ManyToManyChannel ...]
Counted 16 characters
Counted 16 characters
Counted 16 characters
Counted 16 characters
Counted 16 characters

(async/<!! *1)
=> 16

In this example there is a buffer of 1000 on the channel. So, unless you begin to take from the channel, calls to >!! will block after 1000 lines are read. You could alternatively use put! with a callback, but there is a built-in 1024 limit here, and you should be processing the result anyway.




回答2:


If you don't mind using a library, you can find a simple solution using lazy-gen and yield from the Tupelo library. It works like generator functions in Python:

(ns tst.demo.core
  (:use demo.core tupelo.test)
  (:require
    [clojure.java.io :as io]
    [tupelo.core :as t]
    [me.raynes.conch.low-level :as cll]
  ))
(t/refer-tupelo)

(dotest
  (let [proc          (cll/proc "dumb.sh")
        >>            (pretty proc)
        out-lines     (line-seq (io/reader (grab :out proc)))
        lazy-line-seq (lazy-gen
                        (doseq [line out-lines]
                          (yield line))) ]
    (doseq [curr-line lazy-line-seq]
      (spyx curr-line))))

Using the same dumb.sh as before, it yields this output:

{:out  #object[java.lang.UNIXProcess$ProcessPipeInputStream 0x465b16bb "java.lang.UNIXProcess$ProcessPipeInputStream@465b16bb"],
 :in   #object[java.lang.UNIXProcess$ProcessPipeOutputStream 0xfafbc63 "java.lang.UNIXProcess$ProcessPipeOutputStream@fafbc63"],
 :err  #object[java.lang.UNIXProcess$ProcessPipeInputStream 0x59bb8f80 "java.lang.UNIXProcess$ProcessPipeInputStream@59bb8f80"],
 :process  #object[java.lang.UNIXProcess 0x553c74cc "java.lang.UNIXProcess@553c74cc"]}

; one of these is printed every 2 seconds
curr-line => "Loop iteration 1"
curr-line => "Loop iteration 2"
curr-line => "Loop iteration 3"
curr-line => "Loop iteration 4"
curr-line => "Loop iteration 5"

Everything in the lazy-gen is run in a separate thread using core.async. The doseq eagerly consumes the process output and places it on the output lazy sequence using yield. The 2nd doseq eagerly consumes the result of lazy-gen in the current thread and prints each line as soon as it is available.


Alternate solution:

An even simpler solution is to simply use a future like so:

(dotest
  (let [proc          (cll/proc "dumb.sh")
        out-lines     (line-seq (io/reader (grab :out proc))) ]
    (future
      (doseq [curr-line out-lines]
        (spyx curr-line)))))

with the same results:

curr-line => "Loop iteration 1"
curr-line => "Loop iteration 2"
curr-line => "Loop iteration 3"
curr-line => "Loop iteration 4"
curr-line => "Loop iteration 5"


来源:https://stackoverflow.com/questions/45292625/how-to-perform-non-blocking-reading-stdout-from-a-subprocess-in-clojure

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!