├── deps.edn ├── .gitignore ├── src └── com │ └── gfredericks │ ├── debug_repl │ ├── util.clj │ ├── nrepl_53.clj │ ├── backpat.clj │ └── async.clj │ └── debug_repl.clj ├── changes.md ├── project.clj ├── README.md ├── test └── com │ └── gfredericks │ └── debug_repl_test.clj └── LICENSE /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"]} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /src/com/gfredericks/debug_repl/util.clj: -------------------------------------------------------------------------------- 1 | (ns com.gfredericks.debug-repl.util) 2 | 3 | (defmacro catchingly 4 | "Returns either [:returned x] or [:threw t]." 5 | [& body] 6 | `(try [:returned (do ~@body)] 7 | (catch Throwable t# 8 | [:threw t#]))) 9 | 10 | (defn uncatch 11 | [[type x]] 12 | (case type :returned x :threw (throw x))) 13 | 14 | (defn require? [symbol] 15 | (try 16 | (require symbol) 17 | true 18 | (catch Exception e 19 | false))) 20 | -------------------------------------------------------------------------------- /src/com/gfredericks/debug_repl/nrepl_53.clj: -------------------------------------------------------------------------------- 1 | (ns com.gfredericks.debug-repl.nrepl-53) 2 | 3 | (def msg 4 | "\n\nERROR: debug-repl has detected a middleware problem 5 | that is probably this bug: 6 | 7 | http://dev.clojure.org/jira/browse/NREPL-53 8 | 9 | debug-repl cannot work in the presence of this bug. 10 | This Leiningen plugin might help: 11 | 12 | https://github.com/gfredericks/nrepl-53-monkeypatch\n\n") 13 | 14 | (defn report-nrepl-53-bug 15 | [] 16 | (binding [*out* *err*] (println msg)) 17 | (throw (Exception. "NREPL-53 bug detected!"))) 18 | -------------------------------------------------------------------------------- /src/com/gfredericks/debug_repl/backpat.clj: -------------------------------------------------------------------------------- 1 | (ns com.gfredericks.debug-repl.backpat 2 | "Pasted things from newer versions of clojure, to maintain 3 | compatibility with older versions." 4 | (:refer-clojure :exclude [cond->])) 5 | 6 | (defmacro cond-> 7 | "Takes an expression and a set of test/form pairs. Threads expr (via ->) 8 | through each form for which the corresponding test 9 | expression is true. Note that, unlike cond branching, cond-> threading does 10 | not short circuit after the first true test expression." 11 | {:added "1.5"} 12 | [expr & clauses] 13 | (assert (even? (count clauses))) 14 | (let [g (gensym) 15 | pstep (fn [[test step]] `(if ~test (-> ~g ~step) ~g))] 16 | `(let [~g ~expr 17 | ~@(interleave (repeat g) (map pstep (partition 2 clauses)))] 18 | ~g))) 19 | -------------------------------------------------------------------------------- /changes.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.0.12 (2021-01-23) 4 | 5 | Thanks to Darrick Wiebe for these features. 6 | 7 | - Fix a bug in `catch-break!`. 8 | - Add support for `return!` from `catch-break!` 9 | - Add label support to `catch-break!` 10 | 11 | ## 0.0.11 (2019-08-04) 12 | 13 | Adds the new `com.gfredericks.debug-repl.async` namespace. 14 | 15 | ## 0.0.10 (2019-02-10) 16 | 17 | Supports the namespace change in newer versions of nrepl (and thus 18 | leiningen) ([#7](https://github.com/gfredericks/debug-repl/issues/7) 19 | and [#8](https://github.com/gfredericks/debug-repl/pull/8)). 20 | 21 | ## 0.0.9 (2017-08-24) 22 | 23 | Fixes a [bug with primitive type hints](https://github.com/gfredericks/debug-repl/issues/4). 24 | 25 | ## 0.0.8 26 | 27 | Fixed a race condition in `unbreak!!` that caused it to just not work. 28 | 29 | ## 0.0.7 30 | 31 | Added `catch-break!`. 32 | 33 | ## 0.0.6 34 | 35 | Sorta fix `*1`, `*2`, etc. 36 | 37 | ## 0.0.5 38 | 39 | Detects the NREPL-53 bug instead of failing strangely. 40 | 41 | ## 0.0.4 42 | 43 | Compatibility with clojure 1.4. 44 | 45 | ## 0.0.3 46 | 47 | Add `unbreak!!` for disabling future breaks. 48 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject com.gfredericks/debug-repl "0.0.13-SNAPSHOT" 2 | :description "A Clojure debug repl as nrepl middleware." 3 | :url "https://github.com/fredericksgary/debug-repl" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"]] 7 | :deploy-repositories [["releases" :clojars]] 8 | :profiles {:1.3 {:dependencies [^:replace [org.clojure/clojure "1.3.0"]]} 9 | :1.4 {:dependencies [^:replace [org.clojure/clojure "1.4.0"]]} 10 | :1.5 {:dependencies [^:replace [org.clojure/clojure "1.5.1"]]} 11 | :1.6 {:dependencies [^:replace [org.clojure/clojure "1.6.0"]]} 12 | :1.7 {:dependencies [^:replace [org.clojure/clojure "1.7.0"]]} 13 | :1.8 {:dependencies [^:replace [org.clojure/clojure "1.8.0"]]} 14 | :old-lein {:plugins [[com.gfredericks/nrepl-53-monkeypatch "0.1.0"]]}} 15 | ;; use `lein all test` to run the tests on all versions 16 | ;; 17 | ;; Skipping 1.3 because I'm not in the mood to do backpat tricks for 18 | ;; ex-info. 19 | :aliases {"all" ["with-profile" "+1.4:+1.5:+1.6:+1.7:+1.8"]}) 20 | -------------------------------------------------------------------------------- /src/com/gfredericks/debug_repl/async.clj: -------------------------------------------------------------------------------- 1 | (ns com.gfredericks.debug-repl.async 2 | (:require 3 | [com.gfredericks.debug-repl :as debug-repl] 4 | [com.gfredericks.debug-repl.util :as util]) 5 | (:import (java.util.concurrent 6 | SynchronousQueue 7 | TimeUnit))) 8 | 9 | (def the-executor nil) ;; global mutable state 10 | 11 | (def ^:private msg-var 12 | (if (util/require? 'nrepl.server) 13 | (resolve 'nrepl.middleware.interruptible-eval/*msg*) 14 | (resolve 'clojure.tools.nrepl.middleware.interruptible-eval/*msg*))) 15 | 16 | (defn can-break? 17 | "Implementation detail. Subject to change." 18 | [] 19 | ;; this just checks if we're in a repl 20 | (boolean (var-get msg-var))) 21 | 22 | (defmacro break! 23 | "Equivalent to com.gfredericks.debug-repl/break! except that if not run from 24 | an nREPL session (e.g. from a ring request) then will attempt to connect to 25 | the connection where wait-for-breaks is being called." 26 | [& args] 27 | `(if (can-break?) 28 | (debug-repl/break! ~@args) 29 | (if-let [e# the-executor] 30 | (e# #(debug-repl/break! ~@args)) 31 | :noop))) 32 | 33 | (defmacro catch-break! 34 | "Executes body and breaks if it throws an exception. The exception 35 | will be in the local scope as &ex. The exception will be re-thrown 36 | after unbreaking." 37 | [& body] 38 | (let [[name body] 39 | (if (and (or (string? (first body)) (keyword? (first body))) 40 | (next body)) 41 | [(first body) (next body)] 42 | ["catch-break!" body])] 43 | `(try ~@body (catch Throwable ~'&ex 44 | (swap! debug-repl/break-return conj ::throw) 45 | (break! ~name) 46 | (let [return# (peek @debug-repl/break-return)] 47 | (swap! break-return pop) 48 | (if (= ::throw return#) 49 | (throw ~'&ex) 50 | return#)))))) 51 | 52 | (defn wait-for-breaks 53 | "Wait for a call to break! outside of the nREPL. Takes an optional timeout 54 | in seconds to wait which is by default 10 seconds. 55 | 56 | The second arg is a boolean (defaulting to true) that determines whether 57 | calls to `break!` should block when other break calls are currently active." 58 | ([] (wait-for-breaks 10 true)) 59 | ([timeout] (wait-for-breaks timeout true)) 60 | ([timeout-seconds wait?] 61 | (let [queue (SynchronousQueue.) 62 | 63 | executor 64 | (fn [func] 65 | (let [p (promise) 66 | f (bound-fn 67 | [] 68 | (deliver p 69 | (try 70 | [(func)] 71 | (catch Throwable t 72 | [nil t]))))] 73 | 74 | (if (or (and wait? (do (.put queue f) true)) 75 | (.offer queue f)) 76 | (let [[x err] @p] 77 | (if err (throw err) x)) 78 | :noop)))] 79 | 80 | (with-redefs [the-executor (or the-executor executor)] 81 | (when (= the-executor executor) 82 | (loop [] 83 | (if-let [func (.poll queue timeout-seconds TimeUnit/SECONDS)] 84 | (do 85 | (func) 86 | (recur)) 87 | :no-reqs-before-timeout))))))) 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # debug-repl 2 | 3 | Inspired by [an older 4 | library](https://github.com/georgejahad/debug-repl), debug-repl is a 5 | debug repl implemented as an nrepl middleware. It allows you to set 6 | breakpoints that cause the execution to stop and switches the repl to 7 | evaluate code in the context of the breakpoint. 8 | 9 | ## Usage 10 | 11 | Add the dependency and the middleware, e.g. in your `:user` profile: 12 | 13 | ``` clojure 14 | :dependencies [[com.gfredericks/debug-repl "0.0.12"]] 15 | :repl-options 16 | {:nrepl-middleware 17 | [com.gfredericks.debug-repl/wrap-debug-repl]} 18 | ``` 19 | 20 | Then when you're ready to set breakpoints: 21 | 22 | ``` clojure 23 | user> (require '[com.gfredericks.debug-repl :refer [break! unbreak!]]) 24 | nil 25 | user> (let [x 41] (break!)) 26 | Hijacking repl for breakpoint: unnamed 27 | user> (inc x) 28 | 42 29 | user> (unbreak!) 30 | nil 31 | nil 32 | ``` 33 | 34 | ### `unbreak!!` 35 | 36 | The `unbreak!!` function (with two `!`s) will cancel all further 37 | breakpoints for the remainder of the original evaluation's scope. 38 | 39 | ### `catch-break!` 40 | 41 | A macro which will break only if the wrapped code throws an exception. 42 | 43 | ### async 44 | 45 | There's also an async version of the debug-repl, for use in contexts where you cannot call the function directly (e.g. in a web request or worker queue). 46 | 47 | ``` clojure 48 | user> (require '[com.gfredericks.debug-repl.async :refer [break! unbreak! wait-for-break]]) 49 | nil 50 | user> (defn handler [req] (break!) {:status 200}) 51 | #'handler 52 | user> (def srv (future (run-jetty handler {:port 8080}))) 53 | 2019-06-22 13:29:31.400:INFO:oejs.Server:clojure-agent-send-off-pool-0: jetty-9.4.12.v20180830; built: 2018-08-30T13:59:14.071Z; git: 27208684755d94a92186989f695db2d7b21ebc51; jvm 1.8.0_202-b08 54 | #'user/srv 55 | user> (wait-for-breaks) ; At this point I opened my browser to http://localhost:8080 56 | Hijacking repl for breakpoint: unnamed 57 | user> req 58 | {:ssl-client-cert nil, :protocol "HTTP/1.1", :remote-addr "127.0.0.1", :headers {"host" "localhost:8080", "user-agent" "Mozilla/5.0 (X11; Linux x86_64; rv:67.0) Gecko/20100101 Firefox/67.0", "cookie" "__stripe_mid=d277c210-219f-4aa7-8fe7-b23715befe83", "connection" "keep-alive", "upgrade-insecure-requests" "1", "accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "accept-language" "en-GB,en-US;q=0.7,en;q=0.3", "accept-encoding" "gzip, deflate", "dnt" "1", "cache-control" "max-age=0"}, :server-port 8080, :content-length nil, :content-type nil, :character-encoding nil, :uri "/", :server-name "localhost", :query-string nil, :body #object[org.eclipse.jetty.server.HttpInputOverHTTP 0x6710feb8 "HttpInputOverHTTP@6710feb8[c=0,q=0,[0]=null,s=STREAM]"], :scheme :http, :request-method :get} 59 | user> (unbreak!) 60 | nil 61 | ``` 62 | 63 | ## nREPL compatibility 64 | 65 | A major change in the nREPL ecosystem around 2018 resulted in a 66 | confusing situation for dev setups involving a composition of 67 | different nREPL-based tools. Because the maven artefact id change 68 | (`org.clojure/nrepl` -> `nrepl/nrepl`) and the base namespace changed 69 | `clojure.tools.nrepl` -> `nrepl`), it is possible to have both the old 70 | version and the new version of nREPL on the classpath at the same 71 | time, with the possibility that some tools expect to use one and some 72 | tools expect to use the other. 73 | 74 | debug-repl assumes the old version of nREPL for versions `0.0.9` and 75 | earlier, and supports both as of version `0.0.10`; if it happens that 76 | both versions are on your classpath, debug-repl will optimistically 77 | pick the newer version; however, likely the best situation is to 78 | figure out how to only have the newer version on your classpath. 79 | 80 | ## TODOs 81 | 82 | - Implement `(throw! ex-fn)` for unbreaking and causing the original 83 | execution to throw an exception. 84 | 85 | ## License 86 | 87 | Copyright © 2014 Gary Fredericks 88 | 89 | Distributed under the Eclipse Public License either version 1.0 or (at 90 | your option) any later version. 91 | -------------------------------------------------------------------------------- /test/com/gfredericks/debug_repl_test.clj: -------------------------------------------------------------------------------- 1 | (ns com.gfredericks.debug-repl-test 2 | (:refer-clojure :exclude [eval]) 3 | (:require [nrepl.core :as client] 4 | [nrepl.server :as server]) 5 | (:use clojure.test 6 | com.gfredericks.debug-repl)) 7 | 8 | (def ^:dynamic *client*) 9 | 10 | (defn server-fixture 11 | [test] 12 | (with-open [server (server/start-server 13 | :port 56408 14 | :bind "127.0.0.1" 15 | :handler (server/default-handler #'wrap-debug-repl)) 16 | t (client/connect :port 56408 :host "127.0.0.1")] 17 | (let [c (client/client t 100)] 18 | (binding [*client* c] 19 | (test)))) 20 | ;; there's gotta be a better way to wait for the server to really be 21 | ;; shutdown 22 | (Thread/sleep 100)) 23 | 24 | (defn clear-active-repl-info-fixture 25 | [test] 26 | (reset! active-debug-repls {}) 27 | (test)) 28 | 29 | (use-fixtures :each server-fixture clear-active-repl-info-fixture) 30 | 31 | (defn fresh-session 32 | [] 33 | (let [f (client/client-session *client*)] 34 | (dorun 35 | (f {:op :eval 36 | :code (pr-str (list 'ns 37 | (gensym "user") 38 | '(:require [com.gfredericks.debug-repl :refer [break! unbreak! unbreak!! catch-break! return!]])))})) 39 | f)) 40 | 41 | (defn unparsed-eval* 42 | [session-fn code-string] 43 | (->> (session-fn {:op :eval, :code code-string}) 44 | (map (fn [{:keys [ex err] :as msg}] 45 | (if (or ex err) 46 | (throw (ex-info (str "Error during eval!" (pr-str msg)) 47 | {:msg msg})) 48 | msg))) 49 | (keep :value) 50 | (doall))) 51 | 52 | (defn eval* 53 | [session-fn code-string] 54 | (map read-string (unparsed-eval* session-fn code-string))) 55 | 56 | (defmacro eval 57 | "Returns a sequence of return values from the evaluation." 58 | [session-fn eval-code] 59 | `(eval* ~session-fn (client/code ~eval-code))) 60 | 61 | (defmacro eval-raw 62 | [session-fn eval-code] 63 | `(~session-fn {:op :eval, :code (client/code ~eval-code)})) 64 | 65 | (deftest hello-world-test 66 | (let [f (fresh-session)] 67 | (is (= [] (eval f (let [x 42] (break!) :return))) 68 | "Breaking returns no results.") 69 | (is (= [42] (eval f x)) 70 | "Evaluation sees the context of the break.") 71 | ;; relaxing the ordering for now until I have a coherent design 72 | ;; idea about it. 73 | (is (= #{:return nil} (set (eval f (unbreak!)))) 74 | "unbreak first returns the return value from the 75 | unbroken thread, then its own nil."))) 76 | 77 | (deftest break-out-of-loop-test 78 | (let [f (fresh-session)] 79 | (is (= [] (eval f (do (dotimes [n 10] (break!)) :final-return)))) 80 | (is (= [0] (eval f n))) 81 | (is (= [nil] (eval f (unbreak!)))) 82 | (is (= [1] (eval f n))) 83 | (is (= #{:final-return nil} (set (eval f (unbreak!!))))) 84 | ;; should be able to break again now 85 | (is (= [] (eval f (let [x 42] (break!) :return)))) 86 | (is (= [42] (eval f x))) 87 | (is (= #{:return nil} (set (eval f (unbreak!))))))) 88 | 89 | (deftest repl-vars-test 90 | (let [f (fresh-session)] 91 | (testing "*1" 92 | (is (= [] (eval f (let [y 7] (break!) :return)))) 93 | (is (= [42] (eval f (* 2 3 y)))) 94 | (is (= [42] (eval f *1))) 95 | 96 | ;; it would be nice if this worked but also seems hard 97 | ;; to do cleanly 98 | #_#_ 99 | (is (= #{nil :return} (set (eval f (unbreak!))))) 100 | (let [[xs] (eval f [*1 *2 *3])] 101 | (is (some #{42} xs)))) 102 | 103 | (testing "*e" 104 | (let [[msg1 msg2] (eval-raw f (/ 42 0))] 105 | (is (= (clojure.set/subset? #{:err :ex} (set (concat (keys msg1) (keys msg2))))))) 106 | (is (= ["java.lang.ArithmeticException"] 107 | (eval f (-> *e class .getName))))) 108 | 109 | (testing "Unbound primitives" 110 | (is (unparsed-eval* f "(fn [^long x] (break!))"))))) 111 | 112 | (deftest catch-break-regression-test 113 | (let [f (fresh-session)] 114 | (eval f (catch-break! (throw (Exception. "oh well")))) 115 | (is (thrown-with-msg? Exception #"oh well" (eval f (unbreak!)))))) 116 | 117 | (deftest return!-test 118 | (let [f (fresh-session)] 119 | (eval f (def jake (atom nil))) 120 | (eval f (reset! jake (catch-break! (throw (Exception. "oh well"))))) 121 | (eval f (return! :twelve)) 122 | (is (= [:twelve] (eval f @jake))))) 123 | -------------------------------------------------------------------------------- /src/com/gfredericks/debug_repl.clj: -------------------------------------------------------------------------------- 1 | (ns com.gfredericks.debug-repl 2 | ;; backwards compatibility stuff 3 | (:refer-clojure :exclude [cond->]) 4 | (:require [com.gfredericks.debug-repl.backpat :refer [cond->]]) 5 | 6 | ;; normal requires 7 | (:require [com.gfredericks.debug-repl.nrepl-53 :refer [report-nrepl-53-bug]] 8 | [com.gfredericks.debug-repl.util :as util]) 9 | (:import (java.util.concurrent ArrayBlockingQueue))) 10 | 11 | (if (util/require? 'nrepl.server) 12 | (require '[nrepl.middleware :refer [set-descriptor!]] 13 | '[nrepl.middleware.interruptible-eval :refer [*msg*]] 14 | '[nrepl.misc :refer [response-for]] 15 | '[nrepl.transport :as transport]) 16 | (require '[clojure.tools.nrepl.middleware :refer [set-descriptor!]] 17 | '[clojure.tools.nrepl.middleware.interruptible-eval :refer [*msg*]] 18 | '[clojure.tools.nrepl.misc :refer [response-for]] 19 | '[clojure.tools.nrepl.transport :as transport])) 20 | 21 | ;; TODO: 22 | ;; - Close nrepl sessions after unbreak! 23 | ;; - Report the correct ns so the repl switches back & forth? 24 | ;; - Avoid reporting :done multiple times 25 | ;; - Suppress the return value from (unbreak!)? this would avoid 26 | ;; the command returning two results... 27 | ;; - Detect when (break!) is called but the middleware is missing? 28 | ;; And give a helpful error message. 29 | ;; - Better reporting on how many nested repls there are, etc 30 | 31 | (defonce 32 | ^{:doc 33 | "A map from nrepl session IDs to a stack of debug repl maps, each of which 34 | contain: 35 | 36 | :unbreak -- a 0-arg function which will cause the thread of 37 | execution to resume when it is called 38 | :nested-session-id -- the nrepl session ID being used to evaluate code 39 | for this repl 40 | :eval -- a function that takes a code string and returns the result of 41 | evaling in this repl."} 42 | active-debug-repls 43 | (atom {})) 44 | 45 | (defn ^:private set-no-more-breaks! 46 | "Sets the flag in this debug repl so that it will not break anymore 47 | until the original eval is complete." 48 | [session-id msg-id] 49 | (swap! active-debug-repls assoc-in [session-id :no-more-breaks?] msg-id)) 50 | 51 | (defn ^:private maybe-clear-no-more-breaks! 52 | "Clears no-more-breaks unless it was set by this message." 53 | [session-id msg-id] 54 | (swap! active-debug-repls update-in [session-id] 55 | (fn [session-data] 56 | (cond-> session-data 57 | (not= (get session-data :no-more-breaks?) msg-id) 58 | (dissoc :no-more-breaks?))))) 59 | 60 | (defn ^:private no-more-breaks? 61 | [session-id] 62 | (get-in @active-debug-repls [session-id :no-more-breaks?])) 63 | 64 | (defn innermost-debug-repl 65 | [session-id] 66 | (peek (get-in @active-debug-repls [session-id :repls]))) 67 | 68 | (defmacro current-locals 69 | "Returns a map from symbols of locals in the lexical scope to their 70 | values." 71 | [] 72 | (into {} 73 | (for [name (keys &env)] 74 | [(list 'quote name) 75 | (vary-meta name dissoc :tag) ]))) 76 | 77 | 78 | (defn break 79 | [locals breakpoint-name ns] 80 | (let [{:keys [transport], 81 | session-id ::orig-session-id 82 | nest-session-fn ::nest-session} 83 | *msg* 84 | 85 | unbreak-p (promise) 86 | ;; probably never need more than 1 here 87 | eval-requests (ArrayBlockingQueue. 2)] 88 | (when-not (no-more-breaks? session-id) 89 | (swap! active-debug-repls update-in [session-id :repls] conj 90 | {:unbreak (fn [] (deliver unbreak-p nil)) 91 | :nested-session-id (nest-session-fn) 92 | :eval (fn [code] 93 | (let [result-p (promise) 94 | ;; using the bindings from 95 | ;; the cloned session seems 96 | ;; like the best way to get 97 | ;; *1, *2, etc. to work 98 | ;; right. Not sure if there 99 | ;; are other surprising 100 | ;; consequences. 101 | binding-fn (bound-fn [f] (f))] 102 | (.put eval-requests [code binding-fn result-p]) 103 | (util/uncatch @result-p)))}) 104 | (transport/send transport 105 | (response-for *msg* 106 | {:out (str "Hijacking repl for breakpoint: " 107 | breakpoint-name)})) 108 | (transport/send transport 109 | (response-for *msg* 110 | {:status #{:done}})) 111 | (loop [] 112 | (when-not (realized? unbreak-p) 113 | (if-let [[code binding-fn result-p] (.poll eval-requests)] 114 | (let [code' (format "(fn [{:syms [%s]}]\n%s\n)" 115 | (clojure.string/join " " (keys locals)) 116 | code)] 117 | (deliver result-p 118 | (util/catchingly 119 | (binding-fn 120 | (fn [] 121 | ((binding [*ns* ns] (eval (read-string code'))) locals)))))) 122 | (Thread/sleep 50)) 123 | (recur)))) 124 | nil)) 125 | 126 | (defmacro break! 127 | "Use only with the com.gfredericks.debug-repl/wrap-debug-repl middleware. 128 | 129 | Causes execution to stop and the repl switches to evaluating code in the 130 | context of the breakpoint. Resume execution by calling (unbreak!). REPL 131 | code can result in a nested call to break! which will work in a reasonable 132 | way. Nested breaks require multiple calls to (unbreak!) to undo." 133 | ([] 134 | `(break! "unnamed")) 135 | ([breakpoint-name] 136 | `(break (current-locals) 137 | ~breakpoint-name 138 | ~*ns*))) 139 | 140 | (defn unbreak! 141 | "Causes the latest breakpoint to resume execution; the repl returns to the 142 | state it was in prior to the breakpoint." 143 | [] 144 | (let [{session-id ::orig-session-id} *msg* 145 | f (:unbreak (innermost-debug-repl session-id))] 146 | (when-not f 147 | (throw (Exception. "No debug-repl to unbreak from!"))) 148 | ;; TODO: dissoc as well? (minor memory leak) 149 | (swap! active-debug-repls update-in [session-id :repls] pop) 150 | (f) 151 | nil)) 152 | 153 | (defn unbreak!! 154 | "Like unbreak! but cancels all remaining breakpoints for the 155 | original evaluation." 156 | [] 157 | (set-no-more-breaks! (::orig-session-id *msg*) (::msg-id *msg*)) 158 | (unbreak!)) 159 | 160 | (defn ^:private wrap-transport-sub-session 161 | [t from-session to-session] 162 | (reify transport/Transport 163 | (recv [this] (transport/recv t)) 164 | (recv [this timeout] (transport/recv t timeout)) 165 | (send [this msg] 166 | (let [msg' (cond-> msg (= from-session (:session msg)) (assoc :session to-session))] 167 | (transport/send t msg'))))) 168 | 169 | (defn ^:private wrap-eval 170 | [{:keys [op code session] :as msg}] 171 | (let [{:keys [nested-session-id]} (innermost-debug-repl session)] 172 | (cond-> msg 173 | nested-session-id 174 | (-> (assoc :session nested-session-id) 175 | (update-in [:transport] wrap-transport-sub-session nested-session-id session)) 176 | 177 | 178 | (and nested-session-id (= "eval" op)) 179 | (assoc :code 180 | (pr-str 181 | `((:eval (innermost-debug-repl ~session)) 182 | ~code)))))) 183 | 184 | (defn ^:private wrap-transport-cleanup 185 | [t session-id msg-id] 186 | (reify transport/Transport 187 | (recv [this] (transport/recv t)) 188 | (recv [this timeout] (transport/recv t timeout)) 189 | (send [this msg] 190 | (when (and (:done (:status msg)) 191 | (let [m (get @active-debug-repls session-id)] 192 | (and m (empty? (:repls m))))) 193 | (maybe-clear-no-more-breaks! session-id msg-id)) 194 | (transport/send t msg)))) 195 | 196 | (defn syncronous-new-session 197 | [handler session-id] 198 | (let [p (promise)] 199 | (handler {:session session-id 200 | :op "clone" 201 | :transport (reify transport/Transport 202 | (send [_ msg] 203 | (deliver p msg)))}) 204 | (when (:unknown-op (:status @p)) 205 | (throw (ex-info "Bad middleware ordering!" {:type ::bad-middleware-ordering}))) 206 | (:new-session @p))) 207 | 208 | (defn ^:private handle-debug 209 | [handler {:keys [transport op code session] :as msg}] 210 | (let [msg-id (java.util.UUID/randomUUID)] 211 | (-> msg 212 | (assoc ::orig-session-id session 213 | ::msg-id msg-id 214 | ::nest-session (fn [] 215 | {:post [%]} 216 | (syncronous-new-session handler session))) 217 | (update-in [:transport] wrap-transport-cleanup session msg-id) 218 | (wrap-eval) 219 | (handler)))) 220 | 221 | (defn wrap-debug-repl 222 | [handler] 223 | ;; Test for NREPL-53 at startup 224 | (try (syncronous-new-session handler nil) 225 | (catch clojure.lang.ExceptionInfo e 226 | (when (= ::bad-middleware-ordering (:type (ex-data e))) 227 | (report-nrepl-53-bug)))) 228 | 229 | ;; having handle-debug as a separate function makes it easier to do 230 | ;; interactive development on this middleware 231 | (fn [msg] (handle-debug handler msg))) 232 | 233 | (set-descriptor! #'wrap-debug-repl 234 | {:expects #{"eval" "clone"}}) 235 | 236 | ;; 237 | ;; Helpers 238 | ;; 239 | 240 | ;; this being a global atom is probably a problem for particular 241 | ;; complicated uses; it could probably be fixed by making the var 242 | ;; dynamic and creating a new atom with each catch-break call; the 243 | ;; only detail to pay attention to is the binding conveyance, making 244 | ;; sure that the atom is visible on whatever thread actually executes 245 | ;; the return! call 246 | (def break-return (atom [])) 247 | 248 | (defn return! [value] 249 | (swap! break-return #(-> % pop (conj value))) 250 | (unbreak!)) 251 | 252 | (defn return!! [value] 253 | (swap! break-return #(-> % pop (conj value))) 254 | (unbreak!!)) 255 | 256 | (defmacro catch-break! 257 | "Executes body and breaks if it throws an exception. The exception 258 | will be in the local scope as &ex. The exception will be re-thrown 259 | after unbreaking." 260 | [& body] 261 | (let [[name body] 262 | (if (and (or (string? (first body)) (keyword? (first body))) 263 | (next body)) 264 | [(first body) (next body)] 265 | ["catch-break!" body])] 266 | `(try ~@body (catch Throwable ~'&ex 267 | (swap! break-return conj ::throw) 268 | (break! ~name) 269 | (let [return# (peek @break-return)] 270 | (swap! break-return pop) 271 | (if (= ::throw return#) 272 | (throw ~'&ex) 273 | return#)))))) 274 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of Washington and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | --------------------------------------------------------------------------------