Skip to content

Commit 65ea3e7

Browse files
committed
CLJ-2759 Java process api
1 parent b7d87dc commit 65ea3e7

File tree

1 file changed

+161
-0
lines changed

1 file changed

+161
-0
lines changed

src/clj/clojure/java/process.clj

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
; Copyright (c) Rich Hickey. All rights reserved.
2+
; The use and distribution terms for this software are covered by the
3+
; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
4+
; which can be found in the file epl-v10.html at the root of this distribution.
5+
; By using this software in any fashion, you are agreeing to be bound by
6+
; the terms of this license.
7+
; You must not remove this notice, or any other, from this software.
8+
9+
(ns clojure.java.process
10+
"A process invocation API wrapping the Java process API.
11+
12+
The primary function here is 'start' which starts a process and handles the
13+
streams as directed. It returns a map that contains keys to access the streams
14+
(if available) and the Java Process object. It is also deref-able to wait for
15+
process exit.
16+
17+
Helper functions are available to 'capture' the output of the process stdout
18+
and to wait for an 'ok?' non-error exit. The 'exec' function handles the common
19+
case of `start'ing a process, waiting for process exit, capture and return
20+
stdout."
21+
(:require
22+
[clojure.java.io :as jio]
23+
[clojure.string :as str])
24+
(:import
25+
[java.io StringWriter File]
26+
[java.lang ProcessBuilder ProcessBuilder$Redirect Process]
27+
[java.util List]
28+
[clojure.lang IDeref IBlockingDeref]))
29+
30+
(set! *warn-on-reflection* true)
31+
32+
;; this is built into Java 9, backfilled here for Java 8
33+
(def ^:private ^File null-file
34+
(delay
35+
(jio/file
36+
(if (.startsWith (System/getProperty "os.name") "Windows")
37+
"NUL"
38+
"/dev/null"))))
39+
40+
(defn to-file
41+
"Coerce f to a file per clojure.java.io/file and return a ProcessBuilder.Redirect writing to the file.
42+
Set ':append' in opts to append. This can be passed to 'start' in :out or :err."
43+
{:added "1.12"}
44+
^ProcessBuilder$Redirect [file & {:keys [append] :as opts}]
45+
(let [f (jio/file file)]
46+
(if append
47+
(ProcessBuilder$Redirect/appendTo f)
48+
(ProcessBuilder$Redirect/to f))))
49+
50+
(defn from-file
51+
"Coerce f to a file per clojure.java.io/file and return a ProcessBuilder.Redirect reading from the file.
52+
This can be passed to 'start' in :in."
53+
{:added "1.12"}
54+
^ProcessBuilder$Redirect [file]
55+
(ProcessBuilder$Redirect/from (jio/file file)))
56+
57+
(defn start
58+
"Starts an external command as args and optional leading opts map:
59+
60+
:in - a ProcessBuilder.Redirect (default = :pipe) or :inherit
61+
:out - a ProcessBuilder.Redirect (default = :pipe) or :inherit :discard
62+
:err - a ProcessBuilder.Redirect (default = :pipe) or :inherit :discard :stdout
63+
:dir - directory to run the command from, default=\".\"
64+
:env - {env-var value} of environment variables (all strings)
65+
66+
Returns an ILookup containing the java.lang.Process in :process and the
67+
streams :in :out :err. The map is also an IDeref that waits for process exit
68+
and returns the exit code."
69+
{:added "1.12"}
70+
[& opts+args]
71+
(let [[opts command] (if (map? (first opts+args))
72+
[(first opts+args) (rest opts+args)]
73+
[{} opts+args])
74+
{:keys [in out err dir env]
75+
:or {in :pipe, out :pipe, err :pipe, dir "."}} opts
76+
pb (ProcessBuilder. ^List command)
77+
to-redirect (fn to-redirect
78+
[x]
79+
(case x
80+
:pipe ProcessBuilder$Redirect/PIPE
81+
:inherit ProcessBuilder$Redirect/INHERIT
82+
:discard (ProcessBuilder$Redirect/to @null-file)
83+
;; in Java 9+, just use ProcessBuilder$Redirect/DISCARD
84+
x))]
85+
(.directory pb (jio/file (or dir ".")))
86+
(when in (.redirectInput pb ^ProcessBuilder$Redirect (to-redirect in)))
87+
(when out (.redirectOutput pb ^ProcessBuilder$Redirect (to-redirect out)))
88+
(cond
89+
(= err :stdout) (.redirectErrorStream pb)
90+
err (.redirectError pb ^ProcessBuilder$Redirect (to-redirect err)))
91+
(when env
92+
(let [pb-env (.environment pb)]
93+
(run! (fn [[k v]] (.put pb-env k v)) env)))
94+
(let [proc (.start pb)
95+
m {:process proc
96+
:in (.getOutputStream proc)
97+
:out (.getInputStream proc)
98+
:err (.getErrorStream proc)}]
99+
(reify
100+
clojure.lang.ILookup
101+
(valAt [_ key] (get m key))
102+
(valAt [_ key not-found] (get m key not-found))
103+
104+
IDeref
105+
(deref [_] (.waitFor proc))
106+
107+
IBlockingDeref
108+
(deref [_ timeout unit] (.waitFor proc timeout unit))))))
109+
110+
(defn ok?
111+
"Given the map returned from 'start', wait for the process to exit
112+
and then return true on success"
113+
{:added "1.12"}
114+
[process-map]
115+
(zero? (.waitFor ^Process (:process process-map))))
116+
117+
(defn capture
118+
"Read from input-stream until EOF and return a String (or nil if 0 length).
119+
Takes same opts as clojure.java.io/copy - :buffer and :encoding"
120+
{:added "1.12"}
121+
[input-stream & opts]
122+
(let [writer (StringWriter.)]
123+
(apply jio/copy input-stream writer opts)
124+
(let [s (str/trim (.toString writer))]
125+
(when-not (zero? (.length s))
126+
s))))
127+
128+
(defn exec
129+
"Execute a command and on successful exit, return the captured output,
130+
else throw RuntimeException. Args are the same as 'start' and options
131+
if supplied override the default 'exec' settings."
132+
{:added "1.12"}
133+
[& opts+args]
134+
(let [[opts command] (if (map? (first opts+args))
135+
[(first opts+args) (rest opts+args)]
136+
[{} opts+args])
137+
opts (merge opts {:err :inherit})]
138+
(let [state (apply start opts command)
139+
out-promise (promise)
140+
capture-fn #(deliver out-promise (capture (:out state)))]
141+
(doto (Thread. ^Runnable capture-fn) (.setDaemon true) (.start))
142+
(if (ok? state)
143+
@out-promise
144+
(throw (RuntimeException. (str "Process failed with exit=" (.exitValue ^Process (:process state)))))))))
145+
146+
(comment
147+
;; shell out and inherit the i/o
148+
(start {:out :inherit, :err :stdout} "ls" "-l")
149+
150+
;; write out and err to files, wait for process to exit, return exit code
151+
@(start {:out (to-file "out") :err (to-file "err")} "ls" "-l")
152+
153+
;; capture output to string
154+
(-> (start "ls" "-l") :out capture)
155+
156+
;; with exec
157+
(exec "ls" "-l")
158+
159+
;; read input from file
160+
(exec {:in (from-file "deps.edn")} "wc" "-l")
161+
)

0 commit comments

Comments
 (0)