Skip to content

Commit f53f50a

Browse files
committed
Shared REPL environments for Node Socket REPLs
1 parent c9cf1a7 commit f53f50a

2 files changed

Lines changed: 148 additions & 108 deletions

File tree

src/main/clojure/cljs/repl/node.clj

Lines changed: 114 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,18 @@
1616
[cljs.cli :as cli]
1717
[cljs.closure :as closure]
1818
[clojure.data.json :as json])
19-
(:import java.net.Socket
20-
java.lang.StringBuilder
21-
[java.io File BufferedReader BufferedWriter InputStream
19+
(:import [java.net Socket]
20+
[java.lang StringBuilder]
21+
[java.io File BufferedReader BufferedWriter
2222
Writer InputStreamReader IOException]
23-
[java.lang ProcessBuilder Process]))
23+
[java.lang ProcessBuilder Process]
24+
[java.util.concurrent ConcurrentHashMap]))
2425

25-
(defn socket [host port]
26-
(let [socket (Socket. host port)
26+
(def lock (Object.))
27+
(def outs (ConcurrentHashMap.))
28+
29+
(defn create-socket [^String host port]
30+
(let [socket (Socket. host (int port))
2731
in (io/reader socket)
2832
out (io/writer socket)]
2933
{:socket socket :in in :out out}))
@@ -111,97 +115,101 @@
111115

112116
(defn setup
113117
([repl-env] (setup repl-env nil))
114-
([repl-env opts]
115-
(let [output-dir (io/file (util/output-directory opts))
116-
_ (.mkdirs output-dir)
117-
of (io/file output-dir "node_repl.js")
118-
_ (spit of
119-
(string/replace (slurp (io/resource "cljs/repl/node_repl.js"))
120-
"var PORT = 5001;"
121-
(str "var PORT = " (:port repl-env) ";")))
122-
proc (.start (build-process opts repl-env of))
123-
_ (do (.start (Thread. (bound-fn [] (pipe proc (.getInputStream proc) *out*))))
124-
(.start (Thread. (bound-fn [] (pipe proc (.getErrorStream proc) *err*)))))
125-
env (ana/empty-env)
126-
core (io/resource "cljs/core.cljs")
127-
;; represent paths as vectors so we can emit JS arrays, this is to
128-
;; paper over Windows issues with minimum hassle - David
129-
path (.getPath (.getCanonicalFile output-dir))
130-
[fc & cs] (rest (util/path-seq path)) ;; remove leading empty string
131-
root (.substring path 0 (+ (.indexOf path fc) (count fc)))
132-
root-path (vec (cons root cs))
133-
rewrite-path (conj root-path "goog")]
134-
(reset! (:proc repl-env) proc)
135-
(loop [r nil]
136-
(when-not (= r "ready")
137-
(Thread/sleep 50)
138-
(try
139-
(reset! (:socket repl-env) (socket (:host repl-env) (:port repl-env)))
140-
(catch Exception e))
141-
(if @(:socket repl-env)
142-
(recur (read-response (:in @(:socket repl-env))))
143-
(recur nil))))
144-
;; compile cljs.core & its dependencies, goog/base.js must be available
145-
;; for bootstrap to load, use new closure/compile as it can handle
146-
;; resources in JARs
147-
(let [core-js (closure/compile core
148-
(assoc opts :output-file
149-
(closure/src-file->target-file
150-
core (dissoc opts :output-dir))))
151-
deps (closure/add-dependencies opts core-js)]
152-
;; output unoptimized code and the deps file
153-
;; for all compiled namespaces
154-
(apply closure/output-unoptimized
155-
(assoc opts
156-
:output-to (.getPath (io/file output-dir "node_repl_deps.js")))
157-
deps))
158-
;; bootstrap, replace __dirname as __dirname won't be set
159-
;; properly due to how we are running it - David
160-
(node-eval repl-env
161-
(-> (slurp (io/resource "cljs/bootstrap_nodejs.js"))
162-
(string/replace "path.resolve(__dirname, '..', 'base.js')"
163-
(platform-path (conj rewrite-path "bootstrap" ".." "base.js")))
164-
(string/replace
165-
"path.join(\".\", \"..\", src)"
166-
(str "path.join(" (platform-path rewrite-path) ", src)"))
167-
(string/replace
168-
"var CLJS_ROOT = \".\";"
169-
(str "var CLJS_ROOT = " (platform-path root-path) ";"))))
170-
;; load the deps file so we can goog.require cljs.core etc.
171-
(node-eval repl-env
172-
(str "require("
118+
([{:keys [host port socket state] :as repl-env} opts]
119+
(locking lock
120+
(when-not @socket
121+
(let [output-dir (io/file (util/output-directory opts))
122+
_ (.mkdirs output-dir)
123+
of (io/file output-dir "node_repl.js")
124+
_ (spit of
125+
(string/replace (slurp (io/resource "cljs/repl/node_repl.js"))
126+
"var PORT = 5001;"
127+
(str "var PORT = " (:port repl-env) ";")))
128+
proc (.start (build-process opts repl-env of))
129+
_ (do (.start (Thread. (bound-fn [] (pipe proc (.getInputStream proc) *out*))))
130+
(.start (Thread. (bound-fn [] (pipe proc (.getErrorStream proc) *err*)))))
131+
env (ana/empty-env)
132+
core (io/resource "cljs/core.cljs")
133+
;; represent paths as vectors so we can emit JS arrays, this is to
134+
;; paper over Windows issues with minimum hassle - David
135+
path (.getPath (.getCanonicalFile output-dir))
136+
[fc & cs] (rest (util/path-seq path)) ;; remove leading empty string
137+
root (.substring path 0 (+ (.indexOf path fc) (count fc)))
138+
root-path (vec (cons root cs))
139+
rewrite-path (conj root-path "goog")]
140+
(reset! (:proc repl-env) proc)
141+
(loop [r nil]
142+
(when-not (= r "ready")
143+
(Thread/sleep 50)
144+
(try
145+
(reset! socket (create-socket host port))
146+
(catch Exception e))
147+
(if @socket
148+
(recur (read-response (:in @socket)))
149+
(recur nil))))
150+
;; compile cljs.core & its dependencies, goog/base.js must be available
151+
;; for bootstrap to load, use new closure/compile as it can handle
152+
;; resources in JARs
153+
(let [core-js (closure/compile core
154+
(assoc opts :output-file
155+
(closure/src-file->target-file
156+
core (dissoc opts :output-dir))))
157+
deps (closure/add-dependencies opts core-js)]
158+
;; output unoptimized code and the deps file
159+
;; for all compiled namespaces
160+
(apply closure/output-unoptimized
161+
(assoc opts
162+
:output-to (.getPath (io/file output-dir "node_repl_deps.js")))
163+
deps))
164+
;; bootstrap, replace __dirname as __dirname won't be set
165+
;; properly due to how we are running it - David
166+
(node-eval repl-env
167+
(-> (slurp (io/resource "cljs/bootstrap_nodejs.js"))
168+
(string/replace "path.resolve(__dirname, '..', 'base.js')"
169+
(platform-path (conj rewrite-path "bootstrap" ".." "base.js")))
170+
(string/replace
171+
"path.join(\".\", \"..\", src)"
172+
(str "path.join(" (platform-path rewrite-path) ", src)"))
173+
(string/replace
174+
"var CLJS_ROOT = \".\";"
175+
(str "var CLJS_ROOT = " (platform-path root-path) ";"))))
176+
;; load the deps file so we can goog.require cljs.core etc.
177+
(node-eval repl-env
178+
(str "require("
173179
(platform-path (conj root-path "node_repl_deps.js"))
174180
")"))
175-
;; monkey-patch isProvided_ to avoid useless warnings - David
176-
(node-eval repl-env
177-
(str "goog.isProvided_ = function(x) { return false; };"))
178-
;; monkey-patch goog.require, skip all the loaded checks
179-
(repl/evaluate-form repl-env env "<cljs repl>"
180-
'(set! (.-require js/goog)
181-
(fn [name]
182-
(js/CLOSURE_IMPORT_SCRIPT
183-
(unchecked-get (.. js/goog -dependencies_ -nameToPath) name)))))
184-
;; load cljs.core, setup printing
185-
(repl/evaluate-form repl-env env "<cljs repl>"
186-
'(do
187-
(.require js/goog "cljs.core")
188-
(enable-console-print!)))
189-
;; redef goog.require to track loaded libs
190-
(repl/evaluate-form repl-env env "<cljs repl>"
191-
'(do
192-
(set! *target* "nodejs")
193-
(set! *loaded-libs* #{"cljs.core"})
194-
(set! (.-require js/goog)
195-
(fn [name reload]
196-
(when (or (not (contains? *loaded-libs* name)) reload)
197-
(set! *loaded-libs* (conj (or *loaded-libs* #{}) name))
198-
(js/CLOSURE_IMPORT_SCRIPT
199-
(unchecked-get (.. js/goog -dependencies_ -nameToPath) name)))))))
200-
(node-eval repl-env
201-
(str "goog.global.CLOSURE_UNCOMPILED_DEFINES = "
202-
(json/write-str (:closure-defines opts)) ";")))))
203-
204-
(defrecord NodeEnv [host port path socket proc]
181+
;; monkey-patch isProvided_ to avoid useless warnings - David
182+
(node-eval repl-env
183+
(str "goog.isProvided_ = function(x) { return false; };"))
184+
;; monkey-patch goog.require, skip all the loaded checks
185+
(repl/evaluate-form repl-env env "<cljs repl>"
186+
'(set! (.-require js/goog)
187+
(fn [name]
188+
(js/CLOSURE_IMPORT_SCRIPT
189+
(unchecked-get (.. js/goog -dependencies_ -nameToPath) name)))))
190+
;; load cljs.core, setup printing
191+
(repl/evaluate-form repl-env env "<cljs repl>"
192+
'(do
193+
(.require js/goog "cljs.core")
194+
(enable-console-print!)))
195+
;; redef goog.require to track loaded libs
196+
(repl/evaluate-form repl-env env "<cljs repl>"
197+
'(do
198+
(set! *target* "nodejs")
199+
(set! *loaded-libs* #{"cljs.core"})
200+
(set! (.-require js/goog)
201+
(fn [name reload]
202+
(when (or (not (contains? *loaded-libs* name)) reload)
203+
(set! *loaded-libs* (conj (or *loaded-libs* #{}) name))
204+
(js/CLOSURE_IMPORT_SCRIPT
205+
(unchecked-get (.. js/goog -dependencies_ -nameToPath) name)))))))
206+
(node-eval repl-env
207+
(str "goog.global.CLOSURE_UNCOMPILED_DEFINES = "
208+
(json/write-str (:closure-defines opts)) ";")))))
209+
(.put outs (.getName (Thread/currentThread)) *out*)
210+
(swap! state update :listeners inc)))
211+
212+
(defrecord NodeEnv [host port path socket proc state]
205213
repl/IReplEnvOptions
206214
(-repl-options [this]
207215
{:output-dir ".cljs_node_repl"
@@ -217,19 +225,24 @@
217225
(-load [this provides url]
218226
(load-javascript this provides url))
219227
(-tear-down [this]
220-
(let [{:keys [out]} @socket]
221-
(write out ":cljs/quit")
222-
(while (alive? @proc)
223-
(Thread/sleep 50))
224-
(close-socket @socket))))
228+
(swap! state update :listeners dec)
229+
(locking lock
230+
(when (zero? (:listeners @state))
231+
(let [sock @socket]
232+
(when-not (.isClosed (:socket sock))
233+
(write (:out sock) ":cljs/quit")
234+
(while (alive? @proc) (Thread/sleep 50))
235+
(close-socket sock)))))))
225236

226237
(defn repl-env* [options]
227238
(let [{:keys [host port path debug-port]}
228239
(merge
229240
{:host "localhost"
230241
:port (+ 49000 (rand-int 10000))}
231242
options)]
232-
(assoc (NodeEnv. host port path (atom nil) (atom nil))
243+
(assoc
244+
(NodeEnv. host port path
245+
(atom nil) (atom nil) (atom {:listeners 0}))
233246
:debug-port debug-port)))
234247

235248
(defn repl-env

src/main/clojure/cljs/server/node.clj

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,48 @@
77
; You must not remove this notice, or any other, from this software.
88

99
(ns cljs.server.node
10-
(:require [cljs.repl :as repl]
10+
(:require [cljs.env :as env]
11+
[cljs.repl :as repl]
1112
[cljs.repl.node :as node]
12-
[cljs.core.server :as server]))
13+
[cljs.core.server :as server])
14+
(:import [java.net Socket]))
15+
16+
(defonce envs (atom {}))
17+
18+
(defn env-opts->key [{:keys [host port]}]
19+
[host port])
20+
21+
(defn stale? [{:keys [socket] :as repl-env}]
22+
(if-let [sock (:socket @socket)]
23+
(.isClosed ^Socket sock)
24+
false))
25+
26+
(defn get-envs [env-opts]
27+
(let [env-opts (merge {:host "localhost" :port 49001} env-opts)
28+
k (env-opts->key env-opts)]
29+
(swap! envs
30+
#(cond-> %
31+
(or (not (contains? % k))
32+
(stale? (get-in % [k 0])))
33+
(assoc k
34+
[(node/repl-env* env-opts)
35+
(env/default-compiler-env)])))
36+
(get @envs k)))
1337

1438
(defn repl
1539
([]
1640
(repl nil))
1741
([{:keys [opts env-opts]}]
18-
(repl/repl* (node/repl-env* env-opts) opts)))
42+
(let [[env cenv] (get-envs env-opts)]
43+
(env/with-compiler-env cenv
44+
(repl/repl* env opts)))))
1945

2046
(defn prepl
2147
([]
2248
(prepl nil))
2349
([{:keys [opts env-opts]}]
24-
(apply server/io-prepl
25-
(mapcat identity
26-
{:repl-env (node/repl-env* env-opts)
27-
:opts opts}))))
50+
(let [[env cenv] (get-envs env-opts)]
51+
(env/with-compiler-env cenv
52+
(apply server/io-prepl
53+
(mapcat identity
54+
{:repl-env env :opts opts}))))))

0 commit comments

Comments
 (0)