Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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