Skip to content

Commit

Permalink
nrepl: fix bencode quirks by hiding broken values behind a 'pile of poo'
Browse files Browse the repository at this point in the history
  • Loading branch information
darwin committed Oct 6, 2016
1 parent c5a1217 commit 8605f6a
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 15 deletions.
46 changes: 46 additions & 0 deletions src/lib/dirac/lib/bencode_hell.clj
Original file line number Diff line number Diff line change
@@ -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))
16 changes: 9 additions & 7 deletions src/lib/dirac/lib/nrepl_client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion src/nrepl/dirac/nrepl/driver.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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 -------------------------------------------------------------------------------------------------------
Expand Down
19 changes: 12 additions & 7 deletions src/nrepl/dirac/nrepl/piggieback.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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) ---------------------------------------------------------------------
Expand Down
31 changes: 31 additions & 0 deletions src/nrepl/dirac/nrepl/transports/bencode_workarounds.clj
Original file line number Diff line number Diff line change
@@ -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)))

0 comments on commit 8605f6a

Please sign in to comment.