[#160] Add support for ProcessBuilder (#165)

This commit is contained in:
Michiel Borkent 2019-12-17 11:27:40 +01:00 committed by GitHub
parent 83b3aad920
commit c2d9bbfab2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 121 additions and 233 deletions

View file

@ -172,8 +172,6 @@ enumerated explicitly.
- [`clojure.core.async`](https://clojure.github.io/core.async/) aliased as - [`clojure.core.async`](https://clojure.github.io/core.async/) aliased as
`async`. The `alt` and `go` macros are not available but `alts!!` does work as `async`. The `alt` and `go` macros are not available but `alts!!` does work as
it is a function. it is a function.
- [`me.raynes.conch.low-level`](https://github.com/clj-commons/conch#low-level-usage)
aliased as `conch`
- [`clojure.tools.cli`](https://github.com/clojure/tools.cli) aliased as `tools.cli` - [`clojure.tools.cli`](https://github.com/clojure/tools.cli) aliased as `tools.cli`
- [`clojure.data.csv`](https://github.com/clojure/data.csv) aliased as `csv` - [`clojure.data.csv`](https://github.com/clojure/data.csv) aliased as `csv`
- [`cheshire.core`](https://github.com/dakrone/cheshire) aliased as `json` - [`cheshire.core`](https://github.com/dakrone/cheshire) aliased as `json`
@ -191,6 +189,7 @@ The following Java classes are available:
- `java.io.File` - `java.io.File`
- `java.nio.Files` - `java.nio.Files`
- `java.util.regex.Pattern` - `java.util.regex.Pattern`
- `ProcessBuilder` (see [example](examples/process_builder.clj)).
- `String` - `String`
- `System` - `System`
- `Thread` - `Thread`
@ -446,22 +445,27 @@ A socket REPL client for Emacs is
## Spawning and killing a process ## Spawning and killing a process
You may use the `conch` namespace for this. It maps to Use the `java.lang.ProcessBuilder` class.
[`me.raynes.conch.low-level`](https://github.com/clj-commons/conch#low-level-usage).
Example: Example:
``` clojure ``` clojure
$ bb ' user=> (def ws (-> (ProcessBuilder. ["python" "-m" "SimpleHTTPServer" "1777"]) (.start)))
(def ws (conch/proc "python" "-m" "SimpleHTTPServer" "1777")) #'user/ws
(net/wait-for-it "localhost" 1777) (conch/destroy ws)' user=> (wait/wait-for-port "localhost" 1777)
{:host "localhost", :port 1777, :took 2}
user=> (.destroy ws)
nil
``` ```
Also see this [example](examples/process_builder.clj).
## Async ## Async
Apart from `future` for creating threads and the `conch` namespace for creating Apart from `future` and `pmap` for creating threads, you may use the `async`
processes, you may use the `async` namespace, which maps to `clojure.core.async`, for asynchronous scripting. The following namespace, which maps to `clojure.core.async`, for asynchronous scripting. The
example shows how to get first available value from two different processes: following example shows how to get first available value from two different
processes:
``` clojure ``` clojure
bb ' bb '
@ -681,5 +685,3 @@ Distributed under the EPL License. See LICENSE.
This project contains code from: This project contains code from:
- Clojure, which is licensed under the same EPL License. - Clojure, which is licensed under the same EPL License.
- [conch](https://github.com/clj-commons/conch), which is licensed under the
same EPL License.

19
examples/process_builder.clj Executable file
View file

@ -0,0 +1,19 @@
#!/usr/bin/env bb
(require '[clojure.java.io :as io])
(import '[java.lang ProcessBuilder$Redirect])
(defn grep [input pattern]
(let [proc (-> (ProcessBuilder. ["grep" pattern])
(.redirectOutput ProcessBuilder$Redirect/INHERIT)
(.redirectError ProcessBuilder$Redirect/INHERIT)
(.start))
proc-input (.getOutputStream proc)]
(with-open [w (io/writer proc-input)]
(binding [*out* w]
(print input)
(flush)))
(.waitFor proc)
nil))
(grep "hello\nbye\n" "bye")

View file

@ -23,6 +23,16 @@
"allPublicMethods" : true, "allPublicMethods" : true,
"allPublicFields" : true, "allPublicFields" : true,
"allPublicConstructors" : true "allPublicConstructors" : true
}, {
"name" : "java.io.InputStream",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, {
"name" : "java.io.OutputStream",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, { }, {
"name" : "java.io.StringReader", "name" : "java.io.StringReader",
"allPublicMethods" : true, "allPublicMethods" : true,
@ -68,6 +78,21 @@
"allPublicMethods" : true, "allPublicMethods" : true,
"allPublicFields" : true, "allPublicFields" : true,
"allPublicConstructors" : true "allPublicConstructors" : true
}, {
"name" : "java.lang.Process",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, {
"name" : "java.lang.ProcessBuilder",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, {
"name" : "java.lang.ProcessBuilder$Redirect",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, { }, {
"name" : "java.lang.String", "name" : "java.lang.String",
"allPublicMethods" : true, "allPublicMethods" : true,
@ -78,6 +103,16 @@
"allPublicMethods" : true, "allPublicMethods" : true,
"allPublicFields" : true, "allPublicFields" : true,
"allPublicConstructors" : true "allPublicConstructors" : true
}, {
"name" : "java.lang.UNIXProcess",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, {
"name" : "java.lang.UNIXProcess$ProcessPipeOutputStream",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, { }, {
"name" : "java.nio.file.CopyOption", "name" : "java.nio.file.CopyOption",
"allPublicMethods" : true, "allPublicMethods" : true,
@ -123,6 +158,11 @@
"allPublicMethods" : true, "allPublicMethods" : true,
"allPublicFields" : true, "allPublicFields" : true,
"allPublicConstructors" : true "allPublicConstructors" : true
}, {
"name" : "java.util.concurrent.LinkedBlockingQueue",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, { }, {
"name" : "java.util.regex.Pattern", "name" : "java.util.regex.Pattern",
"allPublicMethods" : true, "allPublicMethods" : true,
@ -133,15 +173,6 @@
"allPublicMethods" : true, "allPublicMethods" : true,
"allPublicFields" : true, "allPublicFields" : true,
"allPublicConstructors" : true "allPublicConstructors" : true
}, {
"allPublicMethods" : true,
"name" : "java.util.concurrent.LinkedBlockingQueue"
}, {
"allPublicConstructors" : true,
"name" : "java.lang.Process"
}, {
"allPublicMethods" : true,
"name" : "java.lang.UNIXProcess"
}, { }, {
"methods" : [ { "methods" : [ {
"name" : "activeCount" "name" : "activeCount"

2
sci

@ -1 +1 @@
Subproject commit 07d28ee572e90a629e01b10aa5b98cb33ccdc1e5 Subproject commit b86cb3db570ffcf6b193662481acceca25d3c979

View file

@ -1,15 +0,0 @@
(ns babashka.impl.Boolean
{:no-doc true}
(:refer-clojure :exclude [list]))
(set! *warn-on-reflection* true)
(defn parseBoolean [^String x]
(Boolean/parseBoolean x))
(def boolean-bindings
{'Boolean/parseBoolean parseBoolean})
(comment
)

View file

@ -1,15 +0,0 @@
(ns babashka.impl.Double
{:no-doc true}
(:refer-clojure :exclude [list]))
(set! *warn-on-reflection* true)
(defn parseDouble [^String x]
(Double/parseDouble x))
(def double-bindings
{'Double/parseDouble parseDouble})
(comment
)

View file

@ -4,40 +4,43 @@
[cheshire.core :as json])) [cheshire.core :as json]))
(def classes (def classes
{:default-classes '[java.lang.ArithmeticException {:default-classes '[clojure.lang.ExceptionInfo
clojure.lang.LineNumberingPushbackReader
java.io.BufferedReader
java.io.BufferedWriter
java.io.File
java.io.InputStream
java.io.OutputStream
java.io.StringReader
java.io.StringWriter
java.lang.ArithmeticException
java.lang.AssertionError java.lang.AssertionError
java.lang.Boolean java.lang.Boolean
java.io.BufferedWriter
java.io.BufferedReader
java.lang.Class java.lang.Class
java.lang.Double java.lang.Double
java.lang.Exception java.lang.Exception
clojure.lang.ExceptionInfo
java.lang.Integer java.lang.Integer
java.io.File java.util.concurrent.LinkedBlockingQueue
clojure.lang.LineNumberingPushbackReader
java.util.regex.Pattern
java.lang.String java.lang.String
java.io.StringReader
java.io.StringWriter
java.lang.System java.lang.System
sun.nio.fs.UnixPath java.lang.Process
java.nio.file.attribute.FileAttribute java.lang.UNIXProcess
java.nio.file.attribute.PosixFilePermission java.lang.UNIXProcess$ProcessPipeOutputStream
java.nio.file.attribute.PosixFilePermissions java.lang.ProcessBuilder
java.lang.ProcessBuilder$Redirect
java.nio.file.CopyOption java.nio.file.CopyOption
java.nio.file.FileAlreadyExistsException java.nio.file.FileAlreadyExistsException
java.nio.file.Files java.nio.file.Files
java.nio.file.NoSuchFileException java.nio.file.NoSuchFileException
java.nio.file.Path java.nio.file.Path
java.nio.file.StandardCopyOption] java.nio.file.StandardCopyOption
:custom-classes {'java.util.concurrent.LinkedBlockingQueue ;; why? java.nio.file.attribute.FileAttribute
{:allPublicMethods true} java.nio.file.attribute.PosixFilePermission
'java.lang.Process ;; for conch? java.nio.file.attribute.PosixFilePermissions
{:allPublicConstructors true} java.util.regex.Pattern
'java.lang.UNIXProcess ;; for conch? sun.nio.fs.UnixPath ;; included because of permission check
{:allPublicMethods true} ]
'java.lang.Thread :custom-classes {'java.lang.Thread
;; generated with `public-declared-method-names`, see in ;; generated with `public-declared-method-names`, see in
;; `comment` below ;; `comment` below
{:methods [{:name "activeCount"} {:methods [{:name "activeCount"}
@ -84,16 +87,21 @@
(def class-map (gen-class-map)) (def class-map (gen-class-map))
#_(defn sym->class-name [sym]
(-> sym str (str/replace "$" ".")))
(defn generate-reflection-file (defn generate-reflection-file
"Generate reflection.json file" "Generate reflection.json file"
[& args] [& args]
(let [entries (vec (for [c (sort (:default-classes classes))] (let [entries (vec (for [c (sort (:default-classes classes))
{:name (str c) :let [class-name (str c)]]
{:name class-name
:allPublicMethods true :allPublicMethods true
:allPublicFields true :allPublicFields true
:allPublicConstructors true})) :allPublicConstructors true}))
custom-entries (for [[k v] (:custom-classes classes)] custom-entries (for [[c v] (:custom-classes classes)
(assoc v :name (str k))) :let [class-name (str c)]]
(assoc v :name class-name))
all-entries (concat entries custom-entries)] all-entries (concat entries custom-entries)]
(spit (or (spit (or
(first args) (first args)
@ -114,5 +122,5 @@
(sort-by :name) (sort-by :name)
(vec))) (vec)))
(public-declared-method-names java.lang.Thread) (public-declared-method-names java.lang.UNIXProcess)
) )

View file

@ -1,18 +0,0 @@
(ns babashka.impl.conch
{:no-doc true}
(:require
[babashka.impl.me.raynes.conch.low-level :as ll]))
(def conch-namespace
{;; low level API
'proc ll/proc
'destroy ll/destroy
'exit-code ll/exit-code
'flush ll/flush
'done ll/done
'stream-to ll/stream-to
'feed-from ll/feed-from
'stream-to-string ll/stream-to-string
'stream-to-out ll/stream-to-out
'feed-from-string ll/feed-from-string
'read-line ll/read-line})

View file

@ -1,126 +0,0 @@
;; From https://github.com/clj-commons/conch
(ns babashka.impl.me.raynes.conch.low-level
"A simple but flexible library for shelling out from Clojure."
{:no-doc true}
(:refer-clojure :exclude [flush read-line])
(:require [clojure.java.io :as io])
(:import [java.util.concurrent TimeUnit TimeoutException]
[java.io InputStream OutputStream]))
(set! *warn-on-reflection* true)
(defn proc
"Spin off another process. Returns the process's input stream,
output stream, and err stream as a map of :in, :out, and :err keys
If passed the optional :dir and/or :env keyword options, the dir
and enviroment will be set to what you specify. If you pass
:verbose and it is true, commands will be printed. If it is set to
:very, environment variables passed, dir, and the command will be
printed. If passed the :clear-env keyword option, then the process
will not inherit its environment from its parent process."
[& args]
(let [[cmd args] (split-with (complement keyword?) args)
args (apply hash-map args)
builder (ProcessBuilder. ^"[Ljava.lang.String;" (into-array String cmd))
env (.environment builder)]
(when (:clear-env args)
(.clear env))
(doseq [[k v] (:env args)]
(.put env k v))
(when-let [dir (:dir args)]
(.directory builder (io/file dir)))
(when (:verbose args) (apply println cmd))
(when (= :very (:verbose args))
(when-let [env (:env args)] (prn env))
(when-let [dir (:dir args)] (prn dir)))
(when (:redirect-err args)
(.redirectErrorStream builder true))
(let [process (.start builder)]
{:out (.getInputStream process)
:in (.getOutputStream process)
:err (.getErrorStream process)
:process process})))
(defn destroy
"Destroy a process."
[process]
(.destroy ^Process (:process process)))
;; .waitFor returns the exit code. This makes this function useful for
;; both getting an exit code and stopping the thread until a process
;; terminates.
(defn exit-code
"Waits for the process to terminate (blocking the thread) and returns
the exit code. If timeout is passed, it is assumed to be milliseconds
to wait for the process to exit. If it does not exit in time, it is
killed (with or without fire)."
([process] (.waitFor ^Process (:process process)))
([process timeout]
(try
(let [^java.util.concurrent.Future fut
(future (.waitFor ^Process (:process process)))]
(.get fut timeout TimeUnit/MILLISECONDS))
(catch Exception e
(if (or (instance? TimeoutException e)
(instance? TimeoutException (.getCause e)))
(do (destroy process)
:timeout)
(throw e))))))
(defn flush
"Flush the output stream of a process."
[process]
(let [^OutputStream in (:in process)]
(.flush in)))
(defn done
"Close the process's output stream (sending EOF)."
[proc]
(let [^OutputStream in (:in proc)]
(.close in)))
(defn stream-to
"Stream :out or :err from a process to an ouput stream.
Options passed are fed to clojure.java.io/copy. They are :encoding to
set the encoding and :buffer-size to set the size of the buffer.
:encoding defaults to UTF-8 and :buffer-size to 1024."
[process from to & args]
(apply io/copy (process from) to args))
(defn feed-from
"Feed to a process's input stream with optional. Options passed are
fed to clojure.java.io/copy. They are :encoding to set the encoding
and :buffer-size to set the size of the buffer. :encoding defaults to
UTF-8 and :buffer-size to 1024. If :flush is specified and is false,
the process will be flushed after writing."
[process from & {flush? :flush :or {flush? true} :as all}]
(apply io/copy from (:in process) all)
(when flush? (flush process)))
(defn stream-to-string
"Streams the output of the process to a string and returns it."
[process from & args]
(with-open [writer (java.io.StringWriter.)]
(apply stream-to process from writer args)
(str writer)))
;; The writer that Clojure wraps System/out in for *out* seems to buffer
;; things instead of writing them immediately. This wont work if you
;; really want to stream stuff, so we'll just skip it and throw our data
;; directly at System/out.
(defn stream-to-out
"Streams the output of the process to System/out"
[process from & args]
(apply stream-to process from (System/out) args))
(defn feed-from-string
"Feed the process some data from a string."
[process s & args]
(apply feed-from process (java.io.StringReader. s) args))
(defn read-line
"Read a line from a process' :out or :err."
[process from]
(binding [*in* (io/reader (from process))]
(clojure.core/read-line)))

View file

@ -3,18 +3,17 @@
(:require (:require
[babashka.impl.async :refer [async-namespace]] [babashka.impl.async :refer [async-namespace]]
[babashka.impl.cheshire :refer [cheshire-core-namespace]] [babashka.impl.cheshire :refer [cheshire-core-namespace]]
[babashka.impl.classes :as classes]
[babashka.impl.classpath :as cp]
[babashka.impl.clojure.core :refer [core-extras]] [babashka.impl.clojure.core :refer [core-extras]]
[babashka.impl.clojure.java.io :refer [io-namespace]] [babashka.impl.clojure.java.io :refer [io-namespace]]
[babashka.impl.clojure.stacktrace :refer [print-stack-trace]] [babashka.impl.clojure.stacktrace :refer [print-stack-trace]]
[babashka.impl.conch :refer [conch-namespace]]
[babashka.impl.csv :as csv] [babashka.impl.csv :as csv]
[babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]] [babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]]
[babashka.impl.repl :as repl] [babashka.impl.repl :as repl]
[babashka.impl.socket-repl :as socket-repl] [babashka.impl.socket-repl :as socket-repl]
[babashka.impl.tools.cli :refer [tools-cli-namespace]] [babashka.impl.tools.cli :refer [tools-cli-namespace]]
[babashka.impl.utils :refer [eval-string]] [babashka.impl.utils :refer [eval-string]]
[babashka.impl.classpath :as cp]
[babashka.impl.classes :as classes]
[babashka.wait :as wait] [babashka.wait :as wait]
[clojure.edn :as edn] [clojure.edn :as edn]
[clojure.java.io :as io] [clojure.java.io :as io]
@ -236,7 +235,6 @@ Everything after that is bound to *command-line-args*."))
sig babashka.signal sig babashka.signal
shell clojure.java.shell shell clojure.java.shell
io clojure.java.io io clojure.java.io
conch me.raynes.conch.low-level
async clojure.core.async async clojure.core.async
csv clojure.data.csv csv clojure.data.csv
json cheshire.core} json cheshire.core}
@ -249,7 +247,6 @@ Everything after that is bound to *command-line-args*."))
'wait-for-path wait/wait-for-path} 'wait-for-path wait/wait-for-path}
'babashka.signal {'pipe-signal-received? pipe-signal-received?} 'babashka.signal {'pipe-signal-received? pipe-signal-received?}
'clojure.java.io io-namespace 'clojure.java.io io-namespace
'me.raynes.conch.low-level conch-namespace
'clojure.core.async async-namespace 'clojure.core.async async-namespace
'clojure.data.csv csv/csv-namespace 'clojure.data.csv csv/csv-namespace
'cheshire.core cheshire-core-namespace} 'cheshire.core cheshire-core-namespace}
@ -266,6 +263,7 @@ Everything after that is bound to *command-line-args*."))
Exception java.lang.Exception Exception java.lang.Exception
Integer java.lang.Integer Integer java.lang.Integer
File java.io.File File java.io.File
ProcessBuilder java.lang.ProcessBuilder
String java.lang.String String java.lang.String
System java.lang.System System java.lang.System
Thread java.lang.Thread} Thread java.lang.Thread}

View file

@ -166,8 +166,12 @@
(deftest future-test (deftest future-test
(is (= 6 (bb nil "@(future (+ 1 2 3))")))) (is (= 6 (bb nil "@(future (+ 1 2 3))"))))
(deftest conch-test (deftest process-builder-test
(is (str/includes? (bb nil "(->> (conch/proc \"ls\") (conch/stream-to-string :out))") (is (str/includes? (bb nil "
(def ls (-> (ProcessBuilder. [\"ls\"]) (.start)))
(def output (.getInputStream ls))
(.waitFor ls)
(slurp output)")
"LICENSE"))) "LICENSE")))
(deftest create-temp-file-test (deftest create-temp-file-test
@ -182,10 +186,10 @@
(deftest wait-for-port-test (deftest wait-for-port-test
(is (= :timed-out (is (= :timed-out
(bb nil "(def web-server (conch/proc \"python\" \"-m\" \"SimpleHTTPServer\" \"7171\")) (bb nil "(def ws (-> (ProcessBuilder. [\"python\" \"-m\" \"SimpleHTTPServer\" \"1777\"]) (.start)))
(wait/wait-for-port \"127.0.0.1\" 7171) (wait/wait-for-port \"127.0.0.1\" 1777)
(conch/destroy web-server) (.destroy ws)
(wait/wait-for-port \"localhost\" 7172 {:default :timed-out :timeout 50})")))) (wait/wait-for-port \"localhost\" 1777 {:default :timed-out :timeout 50})"))))
(deftest wait-for-path-test (deftest wait-for-path-test
(let [temp-dir-path (System/getProperty "java.io.tmpdir")] (let [temp-dir-path (System/getProperty "java.io.tmpdir")]