PageRenderTime 44ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/src/main/clojure/clojure/tools/nrepl.clj

http://github.com/clojure/tools.nrepl
Clojure | 246 lines | 208 code | 29 blank | 9 comment | 11 complexity | ee6dadddd577f0c8f8dbca7274224d2b MD5 | raw file
  1. ; Copyright (c) Rich Hickey. All rights reserved.
  2. ; The use and distribution terms for this software are covered by the
  3. ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
  4. ; which can be found in the file epl-v10.html at the root of this distribution.
  5. ; By using this software in any fashion, you are agreeing to be bound by
  6. ; the terms of this license.
  7. ; You must not remove this notice, or any other, from this software.
  8. (ns ^{:doc "High level nREPL client support."
  9. :author "Chas Emerick"}
  10. clojure.tools.nrepl
  11. (:require [clojure.tools.nrepl.transport :as transport]
  12. clojure.set
  13. [clojure.java.io :as io])
  14. (:use [clojure.tools.nrepl.misc :only (uuid)])
  15. (:import clojure.lang.LineNumberingPushbackReader
  16. (java.io Reader StringReader Writer PrintWriter)))
  17. (defn response-seq
  18. "Returns a lazy seq of messages received via the given Transport.
  19. Called with no further arguments, will block waiting for each message.
  20. The seq will end only when the underlying Transport is closed (i.e.
  21. returns nil from `recv`) or if a message takes longer than `timeout`
  22. millis to arrive."
  23. ([transport] (response-seq transport Long/MAX_VALUE))
  24. ([transport timeout]
  25. (take-while identity (repeatedly #(transport/recv transport timeout)))))
  26. (defn client
  27. "Returns a fn of zero and one argument, both of which return the current head of a single
  28. response-seq being read off of the given client-side transport. The one-arg arity will
  29. send a given message on the transport before returning the seq.
  30. Most REPL interactions are best performed via `message` and `client-session` on top of
  31. a client fn returned from this fn."
  32. [transport response-timeout]
  33. (let [latest-head (atom nil)
  34. update #(swap! latest-head
  35. (fn [[timestamp seq :as head] now]
  36. (if (< timestamp now)
  37. [now %]
  38. head))
  39. ; nanoTime appropriate here; looking to maintain ordering, not actual timestamps
  40. (System/nanoTime))
  41. tracking-seq (fn tracking-seq [responses]
  42. (lazy-seq
  43. (if (seq responses)
  44. (let [rst (tracking-seq (rest responses))]
  45. (update rst)
  46. (cons (first responses) rst))
  47. (do (update nil) nil))))
  48. restart #(let [head (-> transport
  49. (response-seq response-timeout)
  50. tracking-seq)]
  51. (reset! latest-head [0 head])
  52. head)]
  53. ^{::transport transport ::timeout response-timeout}
  54. (fn this
  55. ([] (or (second @latest-head)
  56. (restart)))
  57. ([msg]
  58. (transport/send transport msg)
  59. (this)))))
  60. (defn- take-until
  61. "Like (take-while (complement f) coll), but includes the first item in coll that
  62. returns true for f."
  63. [f coll]
  64. (let [[head tail] (split-with (complement f) coll)]
  65. (concat head (take 1 tail))))
  66. (defn- delimited-transport-seq
  67. [client termination-statuses delimited-slots]
  68. (with-meta
  69. (comp (partial take-until (comp #(seq (clojure.set/intersection % termination-statuses))
  70. set
  71. :status))
  72. (let [keys (keys delimited-slots)]
  73. (partial filter #(= delimited-slots (select-keys % keys))))
  74. client
  75. #(merge % delimited-slots))
  76. (-> (meta client)
  77. (update-in [::termination-statuses] (fnil into #{}) termination-statuses)
  78. (update-in [::taking-until] merge delimited-slots))))
  79. (defn message
  80. "Sends a message via [client] with a fixed message :id added to it.
  81. Returns the head of the client's response seq, filtered to include only
  82. messages related to the message :id that will terminate upon receipt of a
  83. \"done\" :status."
  84. [client {:keys [id] :as msg :or {id (uuid)}}]
  85. (let [f (delimited-transport-seq client #{"done"} {:id id})]
  86. (f (assoc msg :id id))))
  87. (defn new-session
  88. "Provokes the creation and retention of a new session, optionally as a clone
  89. of an existing retained session, the id of which must be provided as a :clone
  90. kwarg. Returns the new session's id."
  91. [client & {:keys [clone]}]
  92. (let [resp (first (message client (merge {:op "clone"} (when clone {:session clone}))))]
  93. (or (:new-session resp)
  94. (throw (IllegalStateException.
  95. (str "Could not open new session; :clone response: " resp))))))
  96. (defn client-session
  97. "Returns a function of one argument. Accepts a message that is sent via the
  98. client provided with a fixed :session id added to it. Returns the
  99. head of the client's response seq, filtered to include only
  100. messages related to the :session id that will terminate when the session is
  101. closed."
  102. [client & {:keys [session clone]}]
  103. (let [session (or session (apply new-session client (when clone [:clone clone])))]
  104. (delimited-transport-seq client #{"session-closed"} {:session session})))
  105. (defn combine-responses
  106. "Combines the provided seq of response messages into a single response map.
  107. Certain message slots are combined in special ways:
  108. - only the last :ns is retained
  109. - :value is accumulated into an ordered collection
  110. - :status and :session are accumulated into a set
  111. - string values (associated with e.g. :out and :err) are concatenated"
  112. [responses]
  113. (reduce
  114. (fn [m [k v]]
  115. (case k
  116. (:id :ns) (assoc m k v)
  117. :value (update-in m [k] (fnil conj []) v)
  118. :status (update-in m [k] (fnil into #{}) v)
  119. :session (update-in m [k] (fnil conj #{}) v)
  120. (if (string? v)
  121. (update-in m [k] #(str % v))
  122. (assoc m k v))))
  123. {} (apply concat responses)))
  124. (defn code*
  125. "Returns a single string containing the pr-str'd representations
  126. of the given expressions."
  127. [& expressions]
  128. (apply str (map pr-str expressions)))
  129. (defmacro code
  130. "Expands into a string consisting of the macro's body's forms
  131. (literally, no interpolation/quasiquoting of locals or other
  132. references), suitable for use in an :eval message, e.g.:
  133. {:op :eval, :code (code (+ 1 1) (slurp \"foo.txt\"))}"
  134. [& body]
  135. (apply code* body))
  136. (defn read-response-value
  137. "Returns the provided response message, replacing its :value string with
  138. the result of (read)ing it. Returns the message unchanged if the :value
  139. slot is empty or not a string."
  140. [{:keys [value] :as msg}]
  141. (if-not (string? value)
  142. msg
  143. (try
  144. (assoc msg :value (read-string value))
  145. (catch Exception e
  146. (throw (IllegalStateException. (str "Could not read response value: " value) e))))))
  147. (defn response-values
  148. "Given a seq of responses (as from response-seq or returned from any function returned
  149. by client or client-session), returns a seq of values read from :value slots found
  150. therein."
  151. [responses]
  152. (->> responses
  153. (map read-response-value)
  154. combine-responses
  155. :value))
  156. (defn connect
  157. "Connects to a socket-based REPL at the given host (defaults to localhost) and port,
  158. returning the Transport (by default clojure.tools.nrepl.transport/bencode)
  159. for that connection.
  160. Transports are most easily used with `client`, `client-session`, and
  161. `message`, depending on the semantics desired."
  162. [& {:keys [port host transport-fn] :or {transport-fn transport/bencode
  163. host "localhost"}}]
  164. {:pre [transport-fn port]}
  165. (transport-fn (java.net.Socket. ^String host (int port))))
  166. (defn- ^java.net.URI to-uri
  167. [x]
  168. {:post [(instance? java.net.URI %)]}
  169. (if (string? x)
  170. (java.net.URI. x)
  171. x))
  172. (defn- socket-info
  173. [x]
  174. (let [uri (to-uri x)
  175. port (.getPort uri)]
  176. (merge {:host (.getHost uri)}
  177. (when (pos? port)
  178. {:port port}))))
  179. (def ^{:private false} uri-scheme #(-> (to-uri %) .getScheme .toLowerCase))
  180. (defmulti url-connect
  181. "Connects to an nREPL endpoint identified by the given URL/URI. Valid
  182. examples include:
  183. nrepl://192.168.0.12:7889
  184. telnet://localhost:5000
  185. http://your-app-name.heroku.com/repl
  186. This is a multimethod that dispatches on the scheme of the URI provided
  187. (which can be a string or java.net.URI). By default, implementations for
  188. nrepl (corresponding to using the default bencode transport) and
  189. telnet (using the clojure.tools.nrepl.transport/tty transport) are
  190. registered. Alternative implementations may add support for other schemes,
  191. such as HTTP, HTTPS, JMX, existing message queues, etc."
  192. uri-scheme)
  193. ;; TODO oh so ugly
  194. (defn- add-socket-connect-method!
  195. [protocol connect-defaults]
  196. (defmethod url-connect protocol
  197. [uri]
  198. (apply connect (mapcat identity
  199. (merge connect-defaults
  200. (socket-info uri))))))
  201. (add-socket-connect-method! "nrepl" {:transport-fn transport/bencode
  202. :port 7888})
  203. (add-socket-connect-method! "telnet" {:transport-fn transport/tty})
  204. (defmethod url-connect :default
  205. [uri]
  206. (throw (IllegalArgumentException.
  207. (format "No nREPL support known for scheme %s, url %s" (uri-scheme uri) uri))))
  208. (def ^{:doc "Current version of nREPL, map of :major, :minor, :incremental, and :qualifier."}
  209. version
  210. (when-let [in (.getResourceAsStream (class connect) "/clojure/tools/nrepl/version.txt")]
  211. (with-open [^java.io.BufferedReader reader (io/reader in)]
  212. (let [version-string (-> reader .readLine .trim)]
  213. (assoc (->> version-string
  214. (re-find #"(\d+)\.(\d+)\.(\d+)-?(.*)")
  215. rest
  216. (zipmap [:major :minor :incremental :qualifier]))
  217. :version-string version-string)))))