diff --git a/src/lib/dirac/lib/bencode_hell.clj b/src/lib/dirac/lib/bencode_hell.clj new file mode 100644 index 0000000000..1a338482c7 --- /dev/null +++ b/src/lib/dirac/lib/bencode_hell.clj @@ -0,0 +1,46 @@ +(ns dirac.lib.bencode-hell + (:require [clojure.walk :refer [postwalk]] + [clojure.edn :as edn])) + +; bencode transport (default for nREPL) cannot be trusted (as of [org.clojure/tools.nrepl "0.2.12"]) +; the list of quirks I've discovered so far: +; 1. encoding booleans throws +; 2. nils are decoded as [] + +; We work around those by encoding broken values as strings with unicode marker (U+1F4A9) "pile of poo" prepended. +; On the nREPL client side, we detect poo markers and decode strings back to clojure values. + +; Anyone observing dirac-related nREPL messages should immediatelly spot that something smelly is going on... + +; Warning! don't get your hands dirty when working with this code! + +(def marker "\uD83D\uDCA9") +(def re-marker (re-pattern (str marker "(.*)"))) + +(defn broken-value? [v] + (or (nil? v) + (boolean? v))) + +(defn encode-value [v] + (pr-str v)) + +(defn decode-value [v] + (edn/read-string v)) + +(defn encoder [v] + (if (broken-value? v) + (str marker (encode-value v)) + v)) + +(defn decoder [v] + (if (string? v) + (if-let [m (re-matches re-marker v)] + (decode-value (second m)) + v) + v)) + +(defn encode-poo [message] + (postwalk encoder message)) + +(defn decode-poo [message] + (postwalk decoder message)) diff --git a/src/lib/dirac/lib/nrepl_client.clj b/src/lib/dirac/lib/nrepl_client.clj index 30072f6464..b3e254387c 100644 --- a/src/lib/dirac/lib/nrepl_client.clj +++ b/src/lib/dirac/lib/nrepl_client.clj @@ -4,7 +4,8 @@ [clojure.tools.nrepl.transport :as nrepl.transport] [clojure.tools.logging :as log] [dirac.lib.nrepl-protocols :as nrepl-protocols] - [dirac.lib.utils :as utils]) + [dirac.lib.utils :as utils] + [dirac.lib.bencode-hell :as bencode-hell]) (:use [clojure.tools.nrepl.misc :only (uuid)]) (:import (java.net SocketException))) @@ -85,11 +86,12 @@ ; -- sending ---------------------------------------------------------------------------------------------------------------- (defn send! [client message] - (let [raw-nrepl-client (get-raw-nrepl-client client) + (let [dirty-message (bencode-hell/encode-poo message) + raw-nrepl-client (get-raw-nrepl-client client) response-table (get-response-table client) channel (chan) - msg-id (or (:id message) (uuid)) - msg (assoc message :id msg-id)] + msg-id (or (:id dirty-message) (uuid)) + msg (assoc dirty-message :id msg-id)] (swap! response-table assoc msg-id channel) (nrepl/message raw-nrepl-client msg) channel)) @@ -137,9 +139,9 @@ ::interrupted (log/debug (str tunnel) "Leaving poll-for-responses loop - interrupted") ::socket-closed (log/debug (str tunnel) "Leaving poll-for-responses loop - connection closed") '(::error) (log/error (str tunnel) "Leaving poll-for-responses loop - error:\n" (:exception (meta response))) - (do - (submit-response-to-table! response response-table) - (nrepl-protocols/deliver-message-to-client! tunnel response) + (let [clean-response (bencode-hell/decode-poo response)] + (submit-response-to-table! clean-response response-table) + (nrepl-protocols/deliver-message-to-client! tunnel clean-response) (recur)))))) (defn wait-for-response-poller-shutdown [client timeout] diff --git a/src/nrepl/dirac/nrepl/driver.clj b/src/nrepl/dirac/nrepl/driver.clj index 2ffd256567..accf763af4 100644 --- a/src/nrepl/dirac/nrepl/driver.clj +++ b/src/nrepl/dirac/nrepl/driver.clj @@ -128,7 +128,7 @@ :root-ex (str (class root-ex)) :details (helpers/capture-exception-details e)} response (cond-> base-response - javascript-eval-trouble? (merge {:javascript-eval-trouble 1}))] ; TODO: change this to true after we uncripple bencode + javascript-eval-trouble? (merge {:javascript-eval-trouble true}))] (send! driver response))))) ; -- sniffer handlers ------------------------------------------------------------------------------------------------------- diff --git a/src/nrepl/dirac/nrepl/piggieback.clj b/src/nrepl/dirac/nrepl/piggieback.clj index 4c6b9d0781..f7df3da4ee 100644 --- a/src/nrepl/dirac/nrepl/piggieback.clj +++ b/src/nrepl/dirac/nrepl/piggieback.clj @@ -23,6 +23,7 @@ [dirac.nrepl.joining :as joining] [dirac.nrepl.protocol :as protocol] [dirac.nrepl.utils :as utils] + [dirac.nrepl.transports.bencode-workarounds :refer [make-nrepl-message-with-bencode-workarounds]] [dirac.nrepl.transports.debug-logging :refer [make-nrepl-message-with-debug-logging]] [dirac.nrepl.transports.errors-observing :refer [make-nrepl-message-with-observed-errors]] [dirac.nrepl.transports.trace-printing :refer [make-nrepl-message-with-trace-printing]] @@ -59,6 +60,12 @@ make-nrepl-message-with-observed-errors) nrepl-message)) +(defn wrap-nrepl-message [nrepl-message] + (-> nrepl-message + (make-nrepl-message-with-debug-logging) + (make-nrepl-message-with-bencode-workarounds) + (wrap-nrepl-message-for-dirac-session))) + ; -- message handling cascade ----------------------------------------------------------------------------------------------- (defn handle-identify-message! [nrepl-message] @@ -96,18 +103,16 @@ :else (handle-nonspecial-nonjoined-message! nrepl-message)))) (defn handle-message! [nrepl-message] - (let [nrepl-message (make-nrepl-message-with-debug-logging nrepl-message) - session (state/get-current-session)] + (let [session (state/get-current-session)] (log/debug "handle-message!" (:op nrepl-message) (sessions/get-session-id session)) - (let [nrepl-message (wrap-nrepl-message-for-dirac-session nrepl-message)] - (cond - (special/dirac-special-command? nrepl-message) (special/handle-dirac-special-command! nrepl-message) - :else (handle-nonspecial-message! nrepl-message))))) + (cond + (special/dirac-special-command? nrepl-message) (special/handle-dirac-special-command! nrepl-message) + :else (handle-nonspecial-message! nrepl-message)))) (defn handler-job! [next-handler nrepl-message] (state/register-last-seen-nrepl-message! nrepl-message) (if (our-message? nrepl-message) - (handle-message! nrepl-message) + (handle-message! (wrap-nrepl-message nrepl-message)) (next-handler nrepl-message))) ; -- top entry point (called by nrepl middleware stack) --------------------------------------------------------------------- diff --git a/src/nrepl/dirac/nrepl/transports/bencode_workarounds.clj b/src/nrepl/dirac/nrepl/transports/bencode_workarounds.clj new file mode 100644 index 0000000000..b7051fe050 --- /dev/null +++ b/src/nrepl/dirac/nrepl/transports/bencode_workarounds.clj @@ -0,0 +1,31 @@ +(ns dirac.nrepl.transports.bencode-workarounds + (:require [clojure.tools.nrepl.transport :as nrepl-transport] + [clojure.tools.logging :as log] + [dirac.logging :as logging] + [dirac.lib.bencode-hell :as bencode-hell] + [dirac.nrepl.debug :as debug]) + (:import (clojure.tools.nrepl.transport Transport))) + +; we have to invent our own encoding/decoding scheme for values which bencode cannot safely transfer +; see dirac.lib.bencode-hell +; +; please note that if user is not using bencode transport but replace it with something sane, this workaround won't break it + +; -- transport wrapper ------------------------------------------------------------------------------------------------------ + +(defrecord BencodeWorkaroundsTransport [nrepl-message transport] + Transport + (recv [_this timeout] + (let [dirty-message (nrepl-transport/recv transport timeout) + clean-message (bencode-hell/decode-poo dirty-message)] + clean-message)) + (send [_this reply-message] + (let [clean-message reply-message + dirty-message (bencode-hell/encode-poo clean-message)] + (nrepl-transport/send transport dirty-message)))) + +; -- public interface ------------------------------------------------------------------------------------------------------- + +(defn make-nrepl-message-with-bencode-workarounds [nrepl-message] + (log/trace "make-nrepl-message-with-bencode-workarounds" (debug/pprint-nrepl-message nrepl-message)) + (update nrepl-message :transport (partial ->BencodeWorkaroundsTransport nrepl-message)))