Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

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