|
18 | 18 | [clojure.data.json :as json]) |
19 | 19 | (:import [java.net Socket] |
20 | 20 | [java.lang StringBuilder] |
21 | | - [java.io File BufferedReader BufferedWriter |
22 | | - Writer InputStreamReader IOException] |
| 21 | + [java.io File Reader BufferedReader BufferedWriter |
| 22 | + InputStreamReader IOException] |
23 | 23 | [java.lang ProcessBuilder Process] |
24 | | - [java.util.concurrent ConcurrentHashMap])) |
| 24 | + [java.util.concurrent ConcurrentHashMap LinkedBlockingQueue])) |
25 | 25 |
|
26 | 26 | (def lock (Object.)) |
| 27 | +(def results (ConcurrentHashMap.)) |
27 | 28 | (def outs (ConcurrentHashMap.)) |
| 29 | +(def errs (ConcurrentHashMap.)) |
| 30 | + |
| 31 | +(defn thread-name [] |
| 32 | + (.getName (Thread/currentThread))) |
28 | 33 |
|
29 | 34 | (defn create-socket [^String host port] |
30 | 35 | (let [socket (Socket. host (int port)) |
|
42 | 47 | (.write out (int 0)) ;; terminator |
43 | 48 | (.flush out)) |
44 | 49 |
|
45 | | -(defn read-response [^BufferedReader in] |
| 50 | +(defn ^String read-response [^BufferedReader in] |
46 | 51 | (let [sb (StringBuilder.)] |
47 | 52 | (loop [sb sb c (.read in)] |
48 | | - (cond |
49 | | - (= c 1) (let [ret (str sb)] |
50 | | - (print ret) |
51 | | - (recur (StringBuilder.) (.read in))) |
52 | | - (= c 0) (str sb) |
53 | | - :else (do |
54 | | - (.append sb (char c)) |
55 | | - (recur sb (.read in))))))) |
| 53 | + (case c |
| 54 | + -1 (throw (IOException. "Stream closed")) |
| 55 | + 0 (str sb) |
| 56 | + (do |
| 57 | + (.append sb (char c)) |
| 58 | + (recur sb (.read in))))))) |
56 | 59 |
|
57 | 60 | (defn node-eval |
58 | 61 | "Evaluate a JavaScript string in the Node REPL process." |
59 | 62 | [repl-env js] |
60 | | - (let [{:keys [in out]} @(:socket repl-env)] |
61 | | - ;; escape backslash for Node.js under Windows |
62 | | - (write out |
63 | | - (json/write-str |
64 | | - {"repl" (.getName (Thread/currentThread)) |
65 | | - "form" js})) |
66 | | - (let [result (json/read-str |
67 | | - (read-response in) :key-fn keyword)] |
| 63 | + (let [tname (thread-name) |
| 64 | + {:keys [out]} @(:socket repl-env)] |
| 65 | + (write out (json/write-str {:type "eval" :repl tname :form js})) |
| 66 | + (let [result (.take ^LinkedBlockingQueue (.get results tname))] |
68 | 67 | (condp = (:status result) |
69 | 68 | "success" |
70 | 69 | {:status :success |
|
89 | 88 | (defn- alive? [proc] |
90 | 89 | (try (.exitValue proc) false (catch IllegalThreadStateException _ true))) |
91 | 90 |
|
92 | | -(defn- pipe [^Process proc in ^Writer out] |
| 91 | +(defn- event-loop [^Process proc in] |
93 | 92 | ;; we really do want system-default encoding here |
94 | | - (with-open [^java.io.Reader in (-> in InputStreamReader. BufferedReader.)] |
95 | | - (loop [buf (char-array 1024)] |
96 | | - (when (alive? proc) |
| 93 | + (while (alive? proc) |
| 94 | + (try |
| 95 | + (let [res (read-response in)] |
97 | 96 | (try |
98 | | - (let [len (.read in buf)] |
99 | | - (when-not (neg? len) |
100 | | - (.write out buf 0 len) |
101 | | - (.flush out))) |
102 | | - (catch IOException e |
103 | | - (when (and (alive? proc) (not (.contains (.getMessage e) "Stream closed"))) |
104 | | - (.printStackTrace e *err*)))) |
105 | | - (recur buf))))) |
| 97 | + (let [{:keys [type repl value] :or {repl "main"} :as event} |
| 98 | + (json/read-str res :key-fn keyword)] |
| 99 | + (case type |
| 100 | + "result" |
| 101 | + (.offer (.get results repl) event) |
| 102 | + (when-let [stream (.get (if (= type "out") outs errs) repl)] |
| 103 | + (.write stream value 0 (.length ^String value)) |
| 104 | + (.flush stream)))) |
| 105 | + (catch Throwable _ |
| 106 | + (.write *out* res 0 (.length res)) |
| 107 | + (.flush *out*)))) |
| 108 | + (catch IOException e |
| 109 | + (when (and (alive? proc) (not (.contains (.getMessage e) "Stream closed"))) |
| 110 | + (.printStackTrace e *err*)))))) |
106 | 111 |
|
107 | 112 | (defn- build-process |
108 | 113 | [opts repl-env input-src] |
|
119 | 124 | (defn setup |
120 | 125 | ([repl-env] (setup repl-env nil)) |
121 | 126 | ([{:keys [host port socket state] :as repl-env} opts] |
| 127 | + (let [tname (.getName (Thread/currentThread))] |
| 128 | + (.put results tname (LinkedBlockingQueue.)) |
| 129 | + (.put outs tname *out*) |
| 130 | + (.put errs tname *err*)) |
122 | 131 | (locking lock |
123 | 132 | (when-not @socket |
124 | 133 | (let [output-dir (io/file (util/output-directory opts)) |
|
129 | 138 | "var PORT = 5001;" |
130 | 139 | (str "var PORT = " (:port repl-env) ";"))) |
131 | 140 | proc (.start (build-process opts repl-env of)) |
132 | | - _ (do (.start (Thread. (bound-fn [] (pipe proc (.getInputStream proc) *out*)))) |
133 | | - (.start (Thread. (bound-fn [] (pipe proc (.getErrorStream proc) *err*))))) |
134 | 141 | env (ana/empty-env) |
135 | 142 | core (io/resource "cljs/core.cljs") |
136 | 143 | ;; represent paths as vectors so we can emit JS arrays, this is to |
|
150 | 157 | (if @socket |
151 | 158 | (recur (read-response (:in @socket))) |
152 | 159 | (recur nil)))) |
| 160 | + (.start (Thread. (bound-fn [] (event-loop proc (:in @socket))))) |
153 | 161 | ;; compile cljs.core & its dependencies, goog/base.js must be available |
154 | 162 | ;; for bootstrap to load, use new closure/compile as it can handle |
155 | 163 | ;; resources in JARs |
|
209 | 217 | (node-eval repl-env |
210 | 218 | (str "goog.global.CLOSURE_UNCOMPILED_DEFINES = " |
211 | 219 | (json/write-str (:closure-defines opts)) ";"))))) |
212 | | - (.put outs (.getName (Thread/currentThread)) *out*) |
213 | 220 | (swap! state update :listeners inc))) |
214 | 221 |
|
215 | 222 | (defrecord NodeEnv [host port path socket proc state] |
|
229 | 236 | (load-javascript this provides url)) |
230 | 237 | (-tear-down [this] |
231 | 238 | (swap! state update :listeners dec) |
| 239 | + (let [tname (thread-name)] |
| 240 | + (.remove results tname) |
| 241 | + (.remove outs tname) |
| 242 | + (.remove errs tname)) |
232 | 243 | (locking lock |
233 | 244 | (when (zero? (:listeners @state)) |
234 | 245 | (let [sock @socket] |
|
0 commit comments