"inferior" clojure 中的进程 - 重定向异步输出以进行进一步处理

"inferior" process in clojure - redirecting asyncronous output for further processing

我想在 Clojure 中维护一个长期 运行 的交互式 shell 进程——在 Emacs 中称为 "inferior" 进程。我认为基本思想非常熟悉,因为 Clojure 本身可以通过 CIDER 运行 在 Emacs 内部。我很高兴知道是否有使用 Clojure 作为顶级流程的此设置的任何工作示例。

编辑:我发现了这个“shell" Gist, which is a very nice looking wrapper for Conch,这给了我一些启发。

使用我自己的 first attempt with Conch:我无法将字符串输入 python 并以与 cat 相同的方式返回输出。但是通过一些实验,我弄清楚了基础知识并绕过了第一个障碍。

(require '[me.raynes.conch.low-level :as sh])
(def sh-python (sh/proc "python" "-i"))      ; "-i" needed to get interactive mode
#'flowrweb.core/sh-python
(future (sh/stream-to-out sh-python :out))   
#future[{:status :pending, :val nil} 0x516f3fff]
(sh/feed-from-string sh-python "1+1\n")      ; just returns nil on the CIDER repl
2                                            ; <- but we see "2" with the lein repl 
nil

所以我知道我感兴趣的数据是可用的,虽然它在CIDER中不打印有点奇怪(子问题...为什么2 不是打印出来的吗?)。无论如何,对于我的用例,我不需要它 printed;相反,我只想将它作为字符串取回。

第一次尝试:

(def pyout (future (sh/stream-to-string sh-python :out)))
;=> #'flowrweb.core/py-out
(sh/feed-from-string sh-python "1+2\n")
;=> 3
;=> nil
@pyout
^C                     ; <- the process hangs

似乎 sh/stream-to-string 没有完全满足我的需求。

with-out-str 怎么样?

(def something (with-out-str (sh/feed-from-string sh-python "1+3\n")))
;=> 4
;=> #'user/something
something
;=> ""                 ;<- where is the "4"?

不,那也没用。

tl;dr:如何重定向 Conch 子进程的输出以供将来处理?

据我所知,没有专门用于管理流程的 Clojure 机制。此外,OS 进程不在严格意义上的 Clojure 重点范围内,因为它是一种托管语言,进程管理绝对是宿主应该管理的东西(例如 JVM 或 CLR)。

那么,假设您是 运行 JVM 上的 Clojure。 JVM 公开用于创建子进程的一种工具是 ProcessBuilder, which you could invoke from your Clojure code just like the following (credits go to @codification):

(ns proc
  (:import [java.lang ProcessBuilder])
  (:use [clojure.java.io :only [reader writer]]))

(defn spawn [& args]
  (let [process (-> (ProcessBuilder. args)
                  (.start))]
    {:out (-> process
            (.getInputStream)
            (reader))
     :err (-> process
            (.getErrorStream)
            (reader))
     :in (-> process
           (.getOutputStream)
           (writer))
     :process process}))

您可以使用 sh/proc 返回的 InputStream,用 reader 打开它,然后使用 line-seq 创建一个延迟输出行序列。这应该是另一个线程上的 运行,因为 doseq 将阻塞直到输出可用。

(let [{out-stream :out} (sh/proc "ls" "-l")]
  (with-open [out-rdr (clojure.java.io/reader out-stream)]
    (doseq [line (line-seq out-rdr)]
      ; do something with line: Like feed it into core.async chan
      ; (>!! some-chan line)
      ; or pass it to some fn
      (println line))))

这足以开始。

(require '[me.raynes.conch.low-level :as sh])
;=> nil
(def my-stringwriter (java.io.StringWriter.))
;=> #'user/my-stringwriter
(def sh-python (sh/proc "python" "-i"))
;=> #'user/sh-python
(future (sh/stream-to sh-python :out my-stringwriter)) ; NOT redirecting *out* 
;=> #future[{:status :pending, :val nil} 0x4358e46d]
(sh/feed-from-string sh-python "1+1\n")
;=> nil
(.toString my-stringwriter)
;=> "2\n"
(sh/feed-from-string sh-python "1+2\n")
;=> nil
(.toString my-stringwriter)
;=> "2\n3\n"

记住 "The agent system supports sharing changing state between threads in an asynchronous and independent manner" (clojure.org docs),我认为封装它的明智方法是:

(require '[clojure.string :as str])

(def a-stringwriter (agent (java.io.StringWriter.)))
(future (sh/stream-to sh-python :out @a-stringwriter))

(defn feed-python [user-input]
  (future (sh/feed-from-string sh-python (str user-input "\n"))
          (Thread/sleep 1000)
          (str/split (.toString @a-stringwriter) #"\n")))

然后你可以写例如@(feed-python "10+20") 从 python 发送和接收结果。此命令将显示以前交互的历史记录以及最近的交互。对于大多数用例,只有最新添加的内容是相关的。

(defn gljcon [user-input]
  (last @(feed-python user-input)))