389 lines
13 KiB
Clojure
389 lines
13 KiB
Clojure
|
(ns swank.core
|
||
|
(:use (swank util commands)
|
||
|
(swank.util hooks)
|
||
|
(swank.util.concurrent thread)
|
||
|
(swank.core connection hooks threadmap))
|
||
|
(:require (swank.util.concurrent [mbox :as mb])))
|
||
|
|
||
|
;; Protocol version
|
||
|
(defonce protocol-version (atom "20100404"))
|
||
|
|
||
|
;; Emacs packages
|
||
|
(def #^{:dynamic true} *current-package*)
|
||
|
|
||
|
;; current emacs eval id
|
||
|
(def #^{:dynamic true} *pending-continuations* '())
|
||
|
|
||
|
(def sldb-stepping-p nil)
|
||
|
(def sldb-initial-frames 10)
|
||
|
(def #^{:dynamic true} #^{:doc "The current level of recursive debugging."}
|
||
|
*sldb-level* 0)
|
||
|
(def #^{:dynamic true} #^{:doc "The current restarts."}
|
||
|
*sldb-restarts* 0)
|
||
|
|
||
|
(def #^{:doc "Include swank-clojure thread in stack trace for debugger."}
|
||
|
debug-swank-clojure false)
|
||
|
|
||
|
(defonce active-threads (ref ()))
|
||
|
|
||
|
(defn maybe-ns [package]
|
||
|
(cond
|
||
|
(symbol? package) (or (find-ns package) (maybe-ns 'user))
|
||
|
(string? package) (maybe-ns (symbol package))
|
||
|
(keyword? package) (maybe-ns (name package))
|
||
|
(instance? clojure.lang.Namespace package) package
|
||
|
:else (maybe-ns 'user)))
|
||
|
|
||
|
(defmacro with-emacs-package [& body]
|
||
|
`(binding [*ns* (maybe-ns *current-package*)]
|
||
|
~@body))
|
||
|
|
||
|
(defmacro with-package-tracking [& body]
|
||
|
`(let [last-ns# *ns*]
|
||
|
(try
|
||
|
~@body
|
||
|
(finally
|
||
|
(when-not (= last-ns# *ns*)
|
||
|
(send-to-emacs `(:new-package ~(str (ns-name *ns*))
|
||
|
~(str (ns-name *ns*)))))))))
|
||
|
|
||
|
(defmacro dothread-swank [& body]
|
||
|
`(dothread-keeping-clj [*current-connection*]
|
||
|
~@body))
|
||
|
|
||
|
;; Exceptions for debugging
|
||
|
(defonce debug-quit-exception (Exception. "Debug quit"))
|
||
|
(defonce debug-continue-exception (Exception. "Debug continue"))
|
||
|
(defonce debug-abort-exception (Exception. "Debug abort"))
|
||
|
|
||
|
(def #^{:dynamic true} #^Throwable *current-exception* nil)
|
||
|
|
||
|
;; Local environment
|
||
|
(def #^{:dynamic true} *current-env* nil)
|
||
|
|
||
|
(let [&env :unavailable]
|
||
|
(defmacro local-bindings
|
||
|
"Produces a map of the names of local bindings to their values."
|
||
|
[]
|
||
|
(if-not (= &env :unavailable)
|
||
|
(let [symbols (keys &env)]
|
||
|
(zipmap (map (fn [sym] `(quote ~sym)) symbols) symbols)))))
|
||
|
|
||
|
;; Handle Evaluation
|
||
|
(defn send-to-emacs
|
||
|
"Sends a message (msg) to emacs."
|
||
|
([msg]
|
||
|
(mb/send @(*current-connection* :control-thread) msg)))
|
||
|
|
||
|
(defn send-repl-results-to-emacs [val]
|
||
|
(send-to-emacs `(:write-string ~(str (pr-str val) "\n") :repl-result)))
|
||
|
|
||
|
(defn with-env-locals
|
||
|
"Evals a form with given locals. The locals should be a map of symbols to
|
||
|
values."
|
||
|
[form]
|
||
|
(if (seq *current-env*)
|
||
|
`(let ~(vec (mapcat #(list % `(*current-env* '~%)) (keys *current-env*)))
|
||
|
~form)
|
||
|
form))
|
||
|
|
||
|
(defn eval-in-emacs-package [form]
|
||
|
(with-emacs-package
|
||
|
(eval form)))
|
||
|
|
||
|
|
||
|
(defn eval-from-control
|
||
|
"Blocks for a mbox message from the control thread and executes it
|
||
|
when received. The mbox message is expected to be a slime-fn."
|
||
|
([] (let [form (mb/receive (current-thread))]
|
||
|
(apply (ns-resolve *ns* (first form)) (rest form)))))
|
||
|
|
||
|
(defn eval-loop
|
||
|
"A loop which continuosly reads actions from the control thread and
|
||
|
evaluates them (will block if no mbox message is available)."
|
||
|
([] (continuously (eval-from-control))))
|
||
|
|
||
|
(defn exception-causes [#^Throwable t]
|
||
|
(lazy-seq
|
||
|
(cons t (when-let [cause (.getCause t)]
|
||
|
(exception-causes cause)))))
|
||
|
|
||
|
(defn- debug-quit-exception? [t]
|
||
|
(some #(identical? debug-quit-exception %) (exception-causes t)))
|
||
|
|
||
|
(defn- debug-continue-exception? [t]
|
||
|
(some #(identical? debug-continue-exception %) (exception-causes t)))
|
||
|
|
||
|
(defn- debug-abort-exception? [t]
|
||
|
(some #(identical? debug-abort-exception %) (exception-causes t)))
|
||
|
|
||
|
(defn exception-stacktrace [t]
|
||
|
(map #(list %1 %2 '(:restartable nil))
|
||
|
(iterate inc 0)
|
||
|
(map str (.getStackTrace t))))
|
||
|
|
||
|
(defn debugger-condition-for-emacs []
|
||
|
(list (or (.getMessage *current-exception*) "No message.")
|
||
|
(str " [Thrown " (class *current-exception*) "]")
|
||
|
nil))
|
||
|
|
||
|
(defn make-restart [kw name description f]
|
||
|
[kw [name description f]])
|
||
|
|
||
|
(defn add-restart-if [condition restarts kw name description f]
|
||
|
(if condition
|
||
|
(conj restarts (make-restart kw name description f))
|
||
|
restarts))
|
||
|
|
||
|
(declare sldb-debug)
|
||
|
(defn cause-restart-for [thrown depth]
|
||
|
(make-restart
|
||
|
(keyword (str "cause" depth))
|
||
|
(str "CAUSE" depth)
|
||
|
(str "Invoke debugger on cause "
|
||
|
(apply str (take depth (repeat " ")))
|
||
|
(.getMessage thrown)
|
||
|
" [Thrown " (class thrown) "]")
|
||
|
(partial sldb-debug nil thrown *pending-continuations*)))
|
||
|
|
||
|
(defn add-cause-restarts [restarts thrown]
|
||
|
(loop [restarts restarts
|
||
|
cause (.getCause thrown)
|
||
|
level 1]
|
||
|
(if cause
|
||
|
(recur
|
||
|
(conj restarts (cause-restart-for cause level))
|
||
|
(.getCause cause)
|
||
|
(inc level))
|
||
|
restarts)))
|
||
|
|
||
|
(defn calculate-restarts [thrown]
|
||
|
(let [restarts [(make-restart :quit "QUIT" "Quit to the SLIME top level"
|
||
|
(fn [] (throw debug-quit-exception)))]
|
||
|
restarts (add-restart-if
|
||
|
(pos? *sldb-level*)
|
||
|
restarts
|
||
|
:abort "ABORT" (str "ABORT to SLIME level " (dec *sldb-level*))
|
||
|
(fn [] (throw debug-abort-exception)))
|
||
|
restarts (add-restart-if
|
||
|
(and (.getMessage thrown)
|
||
|
(.contains (.getMessage thrown) "BREAK"))
|
||
|
restarts
|
||
|
:continue "CONTINUE" (str "Continue from breakpoint")
|
||
|
(fn [] (throw debug-continue-exception)))
|
||
|
restarts (add-cause-restarts restarts thrown)]
|
||
|
(into (array-map) restarts)))
|
||
|
|
||
|
(defn format-restarts-for-emacs []
|
||
|
(doall (map #(list (first (second %)) (second (second %))) *sldb-restarts*)))
|
||
|
|
||
|
(defn build-backtrace [start end]
|
||
|
(doall (take (- end start) (drop start (exception-stacktrace *current-exception*)))))
|
||
|
|
||
|
(defn build-debugger-info-for-emacs [start end]
|
||
|
(list (debugger-condition-for-emacs)
|
||
|
(format-restarts-for-emacs)
|
||
|
(build-backtrace start end)
|
||
|
*pending-continuations*))
|
||
|
|
||
|
(defn sldb-loop
|
||
|
"A loop that is intented to take over an eval thread when a debug is
|
||
|
encountered (an continue to perform the same thing). It will
|
||
|
continue until a *debug-quit* exception is encountered."
|
||
|
[level]
|
||
|
(try
|
||
|
(send-to-emacs
|
||
|
(list* :debug (current-thread) level
|
||
|
(build-debugger-info-for-emacs 0 sldb-initial-frames)))
|
||
|
([] (continuously
|
||
|
(do
|
||
|
(send-to-emacs `(:debug-activate ~(current-thread) ~level nil))
|
||
|
(eval-from-control))))
|
||
|
(catch Throwable t
|
||
|
(send-to-emacs
|
||
|
`(:debug-return ~(current-thread) ~*sldb-level* ~sldb-stepping-p))
|
||
|
(if-not (debug-continue-exception? t)
|
||
|
(throw t)))))
|
||
|
|
||
|
(defn invoke-debugger
|
||
|
[locals #^Throwable thrown id]
|
||
|
(binding [*current-env* locals
|
||
|
*current-exception* thrown
|
||
|
*sldb-restarts* (calculate-restarts thrown)
|
||
|
*sldb-level* (inc *sldb-level*)]
|
||
|
(sldb-loop *sldb-level*)))
|
||
|
|
||
|
(defn sldb-debug [locals thrown id]
|
||
|
(try
|
||
|
(invoke-debugger nil thrown id)
|
||
|
(catch Throwable t
|
||
|
(when (and (pos? *sldb-level*)
|
||
|
(not (debug-abort-exception? t)))
|
||
|
(throw t)))))
|
||
|
|
||
|
(defmacro break
|
||
|
[]
|
||
|
`(invoke-debugger (local-bindings) (Exception. "BREAK:") *pending-continuations*))
|
||
|
|
||
|
(defn doall-seq [coll]
|
||
|
(if (seq? coll)
|
||
|
(doall coll)
|
||
|
coll))
|
||
|
|
||
|
(defn eval-for-emacs [form buffer-package id]
|
||
|
(try
|
||
|
(binding [*current-package* buffer-package
|
||
|
*pending-continuations* (cons id *pending-continuations*)]
|
||
|
(if-let [f (slime-fn (first form))]
|
||
|
(let [form (cons f (rest form))
|
||
|
result (doall-seq (eval-in-emacs-package form))]
|
||
|
(run-hook pre-reply-hook)
|
||
|
(send-to-emacs `(:return ~(thread-name (current-thread))
|
||
|
(:ok ~result) ~id)))
|
||
|
;; swank function not defined, abort
|
||
|
(send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id))))
|
||
|
(catch Throwable t
|
||
|
;; Thread/interrupted clears this thread's interrupted status; if
|
||
|
;; Thread.stop was called on us it may be set and will cause an
|
||
|
;; InterruptedException in one of the send-to-emacs calls below
|
||
|
(Thread/interrupted)
|
||
|
|
||
|
;; (.printStackTrace t #^java.io.PrintWriter *err*)
|
||
|
|
||
|
(cond
|
||
|
(debug-quit-exception? t)
|
||
|
(do
|
||
|
(send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id))
|
||
|
(if-not (zero? *sldb-level*)
|
||
|
(throw t)))
|
||
|
|
||
|
(debug-abort-exception? t)
|
||
|
(do
|
||
|
(send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id))
|
||
|
(if-not (zero? *sldb-level*)
|
||
|
(throw debug-abort-exception)))
|
||
|
|
||
|
(debug-continue-exception? t)
|
||
|
(do
|
||
|
(send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id))
|
||
|
(throw t))
|
||
|
|
||
|
:else
|
||
|
(do
|
||
|
(set! *e t)
|
||
|
(try
|
||
|
(sldb-debug
|
||
|
nil
|
||
|
(if debug-swank-clojure t (or (.getCause t) t))
|
||
|
id)
|
||
|
;; reply with abort
|
||
|
(finally (send-to-emacs `(:return ~(thread-name (current-thread)) (:abort) ~id)))))))))
|
||
|
|
||
|
(defn- add-active-thread [thread]
|
||
|
(dosync
|
||
|
(commute active-threads conj thread)))
|
||
|
|
||
|
(defn- remove-active-thread [thread]
|
||
|
(dosync
|
||
|
(commute active-threads (fn [threads] (remove #(= % thread) threads)))))
|
||
|
|
||
|
(defn spawn-worker-thread
|
||
|
"Spawn an thread that blocks for a single command from the control
|
||
|
thread, executes it, then terminates."
|
||
|
([conn]
|
||
|
(dothread-swank
|
||
|
(try
|
||
|
(add-active-thread (current-thread))
|
||
|
(thread-set-name "Swank Worker Thread")
|
||
|
(eval-from-control)
|
||
|
(finally
|
||
|
(remove-active-thread (current-thread)))))))
|
||
|
|
||
|
(defn spawn-repl-thread
|
||
|
"Spawn an thread that sets itself as the current
|
||
|
connection's :repl-thread and then enters an eval-loop"
|
||
|
([conn]
|
||
|
(dothread-swank
|
||
|
(thread-set-name "Swank REPL Thread")
|
||
|
(with-connection conn
|
||
|
(eval-loop)))))
|
||
|
|
||
|
(defn find-or-spawn-repl-thread
|
||
|
"Returns the current connection's repl-thread or create a new one if
|
||
|
the existing one does not exist."
|
||
|
([conn]
|
||
|
;; TODO - check if an existing repl-agent is still active & doesn't have errors
|
||
|
(dosync
|
||
|
(or (when-let [conn-repl-thread @(conn :repl-thread)]
|
||
|
(when (.isAlive #^Thread conn-repl-thread)
|
||
|
conn-repl-thread))
|
||
|
(ref-set (conn :repl-thread)
|
||
|
(spawn-repl-thread conn))))))
|
||
|
|
||
|
(defn thread-for-evaluation
|
||
|
"Given an id and connection, find or create the appropiate agent."
|
||
|
([id conn]
|
||
|
(cond
|
||
|
(= id true) (spawn-worker-thread conn)
|
||
|
(= id :repl-thread) (find-or-spawn-repl-thread conn)
|
||
|
:else (find-thread id))))
|
||
|
|
||
|
;; Handle control
|
||
|
(defn read-loop
|
||
|
"A loop that reads from the socket (will block when no message
|
||
|
available) and dispatches the message to the control thread."
|
||
|
([conn control]
|
||
|
(with-connection conn
|
||
|
(continuously (mb/send control (read-from-connection conn))))))
|
||
|
|
||
|
(defn dispatch-event
|
||
|
"Dispatches/executes an event in the control thread's mailbox queue."
|
||
|
([ev conn]
|
||
|
(let [[action & args] ev]
|
||
|
(cond
|
||
|
(= action :emacs-rex)
|
||
|
(let [[form-string package thread id] args
|
||
|
thread (thread-for-evaluation thread conn)]
|
||
|
(mb/send thread `(eval-for-emacs ~form-string ~package ~id)))
|
||
|
|
||
|
(= action :return)
|
||
|
(let [[thread & ret] args]
|
||
|
(binding [*print-level* nil, *print-length* nil]
|
||
|
(write-to-connection conn `(:return ~@ret))))
|
||
|
|
||
|
(one-of? action
|
||
|
:presentation-start :presentation-end
|
||
|
:new-package :new-features :ed :percent-apply
|
||
|
:indentation-update
|
||
|
:eval-no-wait :background-message :inspect)
|
||
|
(binding [*print-level* nil, *print-length* nil]
|
||
|
(write-to-connection conn ev))
|
||
|
|
||
|
(= action :write-string)
|
||
|
(write-to-connection conn ev)
|
||
|
|
||
|
(one-of? action
|
||
|
:debug :debug-condition :debug-activate :debug-return)
|
||
|
(let [[thread & args] args]
|
||
|
(write-to-connection conn `(~action ~(thread-map-id thread) ~@args)))
|
||
|
|
||
|
(= action :emacs-interrupt)
|
||
|
(let [[thread & args] args]
|
||
|
(dosync
|
||
|
(cond
|
||
|
(and (true? thread) (seq @active-threads))
|
||
|
(.stop #^Thread (first @active-threads))
|
||
|
(= thread :repl-thread) (.stop #^Thread @(conn :repl-thread)))))
|
||
|
:else
|
||
|
nil))))
|
||
|
|
||
|
;; Main loop definitions
|
||
|
(defn control-loop
|
||
|
"A loop that reads from the mbox queue and runs dispatch-event on
|
||
|
it (will block if no mbox control message is available). This is
|
||
|
intended to only be run on the control thread."
|
||
|
([conn]
|
||
|
(binding [*1 nil, *2 nil, *3 nil, *e nil]
|
||
|
(with-connection conn
|
||
|
(continuously (dispatch-event (mb/receive (current-thread)) conn))))))
|