[#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
`async`. The `alt` and `go` macros are not available but `alts!!` does work as
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.data.csv`](https://github.com/clojure/data.csv) aliased as `csv`
- [`cheshire.core`](https://github.com/dakrone/cheshire) aliased as `json`
@ -191,6 +189,7 @@ The following Java classes are available:
- `java.io.File`
- `java.nio.Files`
- `java.util.regex.Pattern`
- `ProcessBuilder` (see [example](examples/process_builder.clj)).
- `String`
- `System`
- `Thread`
@ -446,22 +445,27 @@ A socket REPL client for Emacs is
## Spawning and killing a process
You may use the `conch` namespace for this. It maps to
[`me.raynes.conch.low-level`](https://github.com/clj-commons/conch#low-level-usage).
Use the `java.lang.ProcessBuilder` class.
Example:
``` clojure
$ bb '
(def ws (conch/proc "python" "-m" "SimpleHTTPServer" "1777"))
(net/wait-for-it "localhost" 1777) (conch/destroy ws)'
user=> (def ws (-> (ProcessBuilder. ["python" "-m" "SimpleHTTPServer" "1777"]) (.start)))
#'user/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
Apart from `future` for creating threads and the `conch` namespace for creating
processes, you may use the `async` namespace, which maps to `clojure.core.async`, for asynchronous scripting. The following
example shows how to get first available value from two different processes:
Apart from `future` and `pmap` for creating threads, you may use the `async`
namespace, which maps to `clojure.core.async`, for asynchronous scripting. The
following example shows how to get first available value from two different
processes:
``` clojure
bb '
@ -681,5 +685,3 @@ Distributed under the EPL License. See LICENSE.
This project contains code from:
- 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,
"allPublicFields" : 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",
"allPublicMethods" : true,
@ -68,6 +78,21 @@
"allPublicMethods" : true,
"allPublicFields" : 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",
"allPublicMethods" : true,
@ -78,6 +103,16 @@
"allPublicMethods" : true,
"allPublicFields" : 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",
"allPublicMethods" : true,
@ -123,6 +158,11 @@
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, {
"name" : "java.util.concurrent.LinkedBlockingQueue",
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, {
"name" : "java.util.regex.Pattern",
"allPublicMethods" : true,
@ -133,15 +173,6 @@
"allPublicMethods" : true,
"allPublicFields" : true,
"allPublicConstructors" : true
}, {
"allPublicMethods" : true,
"name" : "java.util.concurrent.LinkedBlockingQueue"
}, {
"allPublicConstructors" : true,
"name" : "java.lang.Process"
}, {
"allPublicMethods" : true,
"name" : "java.lang.UNIXProcess"
}, {
"methods" : [ {
"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]))
(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.Boolean
java.io.BufferedWriter
java.io.BufferedReader
java.lang.Class
java.lang.Double
java.lang.Exception
clojure.lang.ExceptionInfo
java.lang.Integer
java.io.File
clojure.lang.LineNumberingPushbackReader
java.util.regex.Pattern
java.util.concurrent.LinkedBlockingQueue
java.lang.String
java.io.StringReader
java.io.StringWriter
java.lang.System
sun.nio.fs.UnixPath
java.nio.file.attribute.FileAttribute
java.nio.file.attribute.PosixFilePermission
java.nio.file.attribute.PosixFilePermissions
java.lang.Process
java.lang.UNIXProcess
java.lang.UNIXProcess$ProcessPipeOutputStream
java.lang.ProcessBuilder
java.lang.ProcessBuilder$Redirect
java.nio.file.CopyOption
java.nio.file.FileAlreadyExistsException
java.nio.file.Files
java.nio.file.NoSuchFileException
java.nio.file.Path
java.nio.file.StandardCopyOption]
:custom-classes {'java.util.concurrent.LinkedBlockingQueue ;; why?
{:allPublicMethods true}
'java.lang.Process ;; for conch?
{:allPublicConstructors true}
'java.lang.UNIXProcess ;; for conch?
{:allPublicMethods true}
'java.lang.Thread
java.nio.file.StandardCopyOption
java.nio.file.attribute.FileAttribute
java.nio.file.attribute.PosixFilePermission
java.nio.file.attribute.PosixFilePermissions
java.util.regex.Pattern
sun.nio.fs.UnixPath ;; included because of permission check
]
:custom-classes {'java.lang.Thread
;; generated with `public-declared-method-names`, see in
;; `comment` below
{:methods [{:name "activeCount"}
@ -84,16 +87,21 @@
(def class-map (gen-class-map))
#_(defn sym->class-name [sym]
(-> sym str (str/replace "$" ".")))
(defn generate-reflection-file
"Generate reflection.json file"
[& args]
(let [entries (vec (for [c (sort (:default-classes classes))]
{:name (str c)
(let [entries (vec (for [c (sort (:default-classes classes))
:let [class-name (str c)]]
{:name class-name
:allPublicMethods true
:allPublicFields true
:allPublicConstructors true}))
custom-entries (for [[k v] (:custom-classes classes)]
(assoc v :name (str k)))
custom-entries (for [[c v] (:custom-classes classes)
:let [class-name (str c)]]
(assoc v :name class-name))
all-entries (concat entries custom-entries)]
(spit (or
(first args)
@ -114,5 +122,5 @@
(sort-by :name)
(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
[babashka.impl.async :refer [async-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.java.io :refer [io-namespace]]
[babashka.impl.clojure.stacktrace :refer [print-stack-trace]]
[babashka.impl.conch :refer [conch-namespace]]
[babashka.impl.csv :as csv]
[babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]]
[babashka.impl.repl :as repl]
[babashka.impl.socket-repl :as socket-repl]
[babashka.impl.tools.cli :refer [tools-cli-namespace]]
[babashka.impl.utils :refer [eval-string]]
[babashka.impl.classpath :as cp]
[babashka.impl.classes :as classes]
[babashka.wait :as wait]
[clojure.edn :as edn]
[clojure.java.io :as io]
@ -236,7 +235,6 @@ Everything after that is bound to *command-line-args*."))
sig babashka.signal
shell clojure.java.shell
io clojure.java.io
conch me.raynes.conch.low-level
async clojure.core.async
csv clojure.data.csv
json cheshire.core}
@ -249,7 +247,6 @@ Everything after that is bound to *command-line-args*."))
'wait-for-path wait/wait-for-path}
'babashka.signal {'pipe-signal-received? pipe-signal-received?}
'clojure.java.io io-namespace
'me.raynes.conch.low-level conch-namespace
'clojure.core.async async-namespace
'clojure.data.csv csv/csv-namespace
'cheshire.core cheshire-core-namespace}
@ -266,6 +263,7 @@ Everything after that is bound to *command-line-args*."))
Exception java.lang.Exception
Integer java.lang.Integer
File java.io.File
ProcessBuilder java.lang.ProcessBuilder
String java.lang.String
System java.lang.System
Thread java.lang.Thread}

View file

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