Skip to content
Newer
Older
100644 216 lines (199 sloc) 9.76 KB
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
1 (ns ^{:author "Chas Emerick"}
2 clojure.tools.nrepl.middleware.interruptible-eval
3 (:require [clojure.tools.nrepl.transport :as t]
93a36fd @cemerick support for automatic organization of nREPL middlewares given specifi…
cemerick authored
4 clojure.tools.nrepl.middleware.pr-values
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
5 clojure.main)
96b58c0 @cemerick "describe" op and middleware for introspection of operations availabl…
cemerick authored
6 (:use [clojure.tools.nrepl.misc :only (response-for returning)]
7 [clojure.tools.nrepl.middleware :only (set-descriptor!)])
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
8 (:import clojure.lang.LineNumberingPushbackReader
9 (java.io StringReader Writer)
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
10 java.util.concurrent.atomic.AtomicLong
eb0ba22 @cemerick no need to use a queue when popping expressions out of :code
cemerick authored
11 (java.util.concurrent LinkedBlockingQueue
2f12262 @cemerick Don't let a single message block processing of an entire transport/co…
cemerick authored
12 SynchronousQueue
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
13 TimeUnit ThreadPoolExecutor
14 ThreadFactory)))
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
15
16 (def ^{:dynamic true
17 :doc "The message currently being evaluated."}
18 *msg* nil)
19
20 (defn evaluate
21 "Evaluates some code within the dynamic context defined by a map of `bindings`,
22 as per `clojure.core/get-thread-bindings`.
23
24 Uses `clojure.main/repl` to drive the evaluation of :code in a second
25 map argument (either a string or a seq of forms to be evaluated), which may
26 also optionally specify a :ns (resolved via `find-ns`). The map MUST
27 contain a Transport implementation in :transport; expression results and errors
28 will be sent via that Transport.
29
30 Returns the dynamic scope that remains after evaluating all expressions
31 in :code.
32
33 It is assumed that `bindings` already contains useful/appropriate entries
34 for all vars indicated by `clojure.main/with-bindings`."
35 [bindings {:keys [code ns transport] :as msg}]
28bedea @cemerick send error when explicit :ns slot names a nonexistent namespace, fixe…
cemerick authored
36 (let [explicit-ns-binding (when-let [ns (and ns (-> ns symbol find-ns))]
37 {#'*ns* ns})
38 bindings (atom (merge bindings explicit-ns-binding))
9af6e2b @cemerick Don't use push- and pop-thread-bindings directly; fixes NREPL-16
cemerick authored
39 out (@bindings #'*out*)
40 err (@bindings #'*err*)]
28bedea @cemerick send error when explicit :ns slot names a nonexistent namespace, fixe…
cemerick authored
41 (if (and ns (not explicit-ns-binding))
42 (t/send transport (response-for msg {:status #{:error :namespace-not-found :done}}))
43 (with-bindings @bindings
44 (try
45 (clojure.main/repl
46 ;; clojure.main/repl paves over certain vars even if they're already thread-bound
47 :init #(do (set! *compile-path* (@bindings #'*compile-path*))
48 (set! *1 (@bindings #'*1))
49 (set! *2 (@bindings #'*2))
50 (set! *3 (@bindings #'*3))
51 (set! *e (@bindings #'*e)))
52 :read (if (string? code)
53 (let [reader (LineNumberingPushbackReader. (StringReader. code))]
54 #(read reader false %2))
55 (let [code (.iterator code)]
56 #(or (and (.hasNext code) (.next code)) %2)))
57 :prompt (fn [])
58 :need-prompt (constantly false)
59 ; TODO pretty-print?
60 :print (fn [v]
61 (reset! bindings (assoc (get-thread-bindings)
62 #'*3 *2
63 #'*2 *1
64 #'*1 v))
65 (.flush ^Writer err)
66 (.flush ^Writer out)
67 (t/send transport (response-for msg
68 {:value v
69 :ns (-> *ns* ns-name str)})))
70 ; TODO customizable exception prints
71 :caught (fn [e]
72 (let [root-ex (#'clojure.main/root-cause e)]
73 (when-not (instance? ThreadDeath root-ex)
74 (reset! bindings (assoc (get-thread-bindings) #'*e e))
75 (t/send transport (response-for msg {:status :eval-error
76 :ex (-> e class str)
77 :root-ex (-> root-ex class str)}))
78 (clojure.main/repl-caught e)))))
79 (finally
80 (.flush ^Writer out)
81 (.flush ^Writer err)))))
82 @bindings))
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
83
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
84 (defn- configure-thread-factory
85 "Returns a new ThreadFactory for the given session. This implementation
86 generates daemon threads, with names that include the session id."
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
87 []
88 (let [session-thread-counter (AtomicLong. 0)]
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
89 (reify ThreadFactory
90 (newThread [_ runnable]
91 (doto (Thread. runnable
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
92 (format "nREPL-worker-%s" (.getAndIncrement session-thread-counter)))
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
93 (.setDaemon true))))))
94
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
95 (def ^{:private true} jdk6? (try
96 (Class/forName "java.util.ServiceLoader")
97 true
98 (catch ClassNotFoundException e false)))
99
2f12262 @cemerick Don't let a single message block processing of an entire transport/co…
cemerick authored
100 ; this is essentially the same as Executors.newCachedThreadPool, except
101 ; for the JDK 5/6 fix described below
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
102 (defn- configure-executor
103 "Returns a ThreadPoolExecutor, configured (by default) to
104 have no core threads, use an unbounded queue, create only daemon threads,
105 and allow unused threads to expire after 30s."
106 [& {:keys [keep-alive queue thread-factory]
107 :or {keep-alive 30000
2f12262 @cemerick Don't let a single message block processing of an entire transport/co…
cemerick authored
108 queue (SynchronousQueue.)}}]
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
109 ; ThreadPoolExecutor in JDK5 *will not run* submitted jobs if the core pool size is zero and
110 ; the queue has not yet rejected a job (see http://kirkwylie.blogspot.com/2008/10/java5-vs-java6-threadpoolexecutor.html)
111 (ThreadPoolExecutor. (if jdk6? 0 1) Integer/MAX_VALUE
112 (long 30000) TimeUnit/MILLISECONDS
113 queue
114 (or thread-factory (configure-thread-factory))))
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
115
07786ca @cemerick build against Clojure 1.4.0 final
cemerick authored
116 ; A little mini-agent implementation. Needed because agents cannot be used to host REPL
117 ; evaluation: http://dev.clojure.org/jira/browse/NREPL-17
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
118 (defn- prep-session
119 [session]
120 (locking session
121 (returning session
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
122 (when-not (-> session meta :queue)
123 (alter-meta! session assoc :queue (atom clojure.lang.PersistentQueue/EMPTY))))))
124
125 (declare run-next)
126 (defn- run-next*
127 [session executor]
128 (let [qa (-> session meta :queue)]
129 (loop []
130 (let [q @qa
131 qn (pop q)]
132 (if-not (compare-and-set! qa q qn)
133 (recur)
134 (when (seq qn)
135 (.execute executor (run-next session executor (peek qn)))))))))
136
137 (defn- run-next
138 [session executor f]
139 #(try
140 (f)
141 (finally
142 (run-next* session executor))))
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
143
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
144 (defn- queue-eval
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
145 "Queues the function for the given session."
146 [session executor f]
147 (let [qa (-> session prep-session meta :queue)]
148 (loop []
149 (let [q @qa]
150 (if-not (compare-and-set! qa q (conj q f))
151 (recur)
152 (when (empty? q)
153 (.execute executor (run-next session executor f))))))))
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
154
155 (defn interruptible-eval
156 "Evaluation middleware that supports interrupts. Returns a handler that supports
157 \"eval\" and \"interrupt\" :op-erations that delegates to the given handler
158 otherwise."
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
159 [h & {:keys [executor] :or {executor (configure-executor)}}]
eb0ba22 @cemerick no need to use a queue when popping expressions out of :code
cemerick authored
160 (fn [{:keys [op session interrupt-id id transport] :as msg}]
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
161 (case op
162 "eval"
163 (if-not (:code msg)
164 (t/send transport (response-for msg :status #{:error :no-code}))
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
165 (queue-eval session executor
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
166 (comp
167 (partial reset! session)
168 (fn []
169 (alter-meta! session assoc
170 :thread (Thread/currentThread)
171 :eval-msg msg)
172 (binding [*msg* msg]
a8bc0c0 @cemerick #'*agent* no longer implicated in REPL goings-on
cemerick authored
173 (returning (dissoc (evaluate @session msg) #'*msg*)
afcba72 @cemerick stop using agents to model REPL sessions; fixing NREPL-17
cemerick authored
174 (t/send transport (response-for msg :status :done))
175 (alter-meta! session dissoc :thread :eval-msg)))))))
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
176
177 "interrupt"
178 ; interrupts are inherently racy; we'll check the agent's :eval-msg's :id and
179 ; bail if it's different than the one provided, but it's possible for
180 ; that message's eval to finish and another to start before we send
181 ; the interrupt / .stop.
182 (let [{:keys [id eval-msg ^Thread thread]} (meta session)]
183 (if (or (not interrupt-id)
184 (= interrupt-id (:id eval-msg)))
185 (if-not thread
186 (t/send transport (response-for msg :status #{:done :session-idle}))
187 (do
f955b8c @cemerick eliminate :interrupted status race condition
cemerick authored
188 ; notify of the interrupted status before we .stop the thread so
189 ; it is received before the standard :done status (thereby ensuring
190 ; that is stays within the scope of a clojure.tools.nrepl/message seq
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
191 (t/send transport {:status #{:interrupted}
192 :id (:id eval-msg)
193 :session id})
f955b8c @cemerick eliminate :interrupted status race condition
cemerick authored
194 (.stop thread)
4e3317c @cemerick split handlers namespace into separate topical middleware namespaces
cemerick authored
195 (t/send transport (response-for msg :status #{:done}))))
196 (t/send transport (response-for msg :status #{:error :interrupt-id-mismatch :done}))))
197
198 (h msg))))
9f11cfd @cemerick take 2 on NREPL-17; no more per-session executors, coping with JDK5/6…
cemerick authored
199
96b58c0 @cemerick "describe" op and middleware for introspection of operations availabl…
cemerick authored
200 (set-descriptor! #'interruptible-eval
93a36fd @cemerick support for automatic organization of nREPL middlewares given specifi…
cemerick authored
201 {:requires #{"clone" "close" #'clojure.tools.nrepl.middleware.pr-values/pr-values}
202 :expects #{}
203 :handles {"eval"
96b58c0 @cemerick "describe" op and middleware for introspection of operations availabl…
cemerick authored
204 {:doc "Evaluates code."
205 :requires {"code" "The code to be evaluated."
206 "session" "The ID of the session within which to evaluate the code."}
207 :optional {"id" "An opaque message ID that will be included in responses related to the evaluation, and which may be used to restrict the scope of a later \"interrupt\" operation."}
208 :returns {}}
209 "interrupt"
210 {:doc "Attempts to interrupt some code evaluation."
211 :requires {"session" "The ID of the session used to start the evaluation to be interrupted."}
212 :optional {"interrupt-id" "The opaque message ID sent with the original \"eval\" request."}
213 :returns {"status" "'interrupted' if an evaluation was identified and interruption will be attempted
214 'session-idle' if the session is not currently evaluating any code
215 'interrupt-id-mismatch' if the session is currently evaluating code sent using a different ID than specified by the \"interrupt-id\" value "}}}})
Something went wrong with that request. Please try again.