From 48baac1922113dd9793d5ef87644b6fd24732d74 Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Fri, 11 Dec 2015 01:54:23 +0100 Subject: [PATCH] WIP - recording stdout/stdin also I had to switch to transit, because prn-str didn't work in some edge cases --- sidecar/project.clj | 3 +- .../components/figwheel_server.clj | 57 ++++--- sidecar/src/figwheel_sidecar/repl.clj | 19 ++- sidecar/src/figwheel_sidecar/repl_driver.clj | 154 ++++++++++++++++-- sidecar/src/figwheel_sidecar/writer_proxy.clj | 51 ++++++ support/project.clj | 3 +- support/src/figwheel/client.cljs | 12 +- support/src/figwheel/client/repl.cljs | 7 +- support/src/figwheel/client/socket.cljs | 10 +- 9 files changed, 271 insertions(+), 45 deletions(-) create mode 100644 sidecar/src/figwheel_sidecar/writer_proxy.clj diff --git a/sidecar/project.clj b/sidecar/project.clj index 7e84feff..9fe00a6e 100644 --- a/sidecar/project.clj +++ b/sidecar/project.clj @@ -21,5 +21,6 @@ [digest "1.4.4"] [figwheel "0.5.0-2" :exclusions [org.clojure/tools.reader]] - [hawk "0.2.5"]]) + [hawk "0.2.5"] + [com.cognitect/transit-clj "0.8.285"]]) diff --git a/sidecar/src/figwheel_sidecar/components/figwheel_server.clj b/sidecar/src/figwheel_sidecar/components/figwheel_server.clj index 81ce79c5..50d67b75 100644 --- a/sidecar/src/figwheel_sidecar/components/figwheel_server.clj +++ b/sidecar/src/figwheel_sidecar/components/figwheel_server.clj @@ -5,8 +5,8 @@ [figwheel-sidecar.repl-driver :as repl-driver] [clojure.java.io :as io] - [clojure.edn :as edn] - + [cognitect.transit :as transit] + [clojure.core.async :refer [go-loop (routes (GET "/figwheel-ws/:desired-build-id" {params :params} (reload-handler server-state)) - (GET "/figwheel-ws" {params :params} (reload-handler server-state)) + (GET "/figwheel-ws" {params :params} (reload-handler server-state)) (route/resources "/" {:root http-server-root}) (or resolved-ring-handler (fn [r] false)) (GET "/" [] (resource-response "index.html" {:root http-server-root})) @@ -168,7 +183,7 @@ { ;; seems like this id should be different for every ;; server restart thus forcing the client to reload - :unique-id (or unique-id (.getCanonicalPath (io/file "."))) + :unique-id (or unique-id (.getCanonicalPath (io/file "."))) :http-server-root (or http-server-root "public") :server-port (or server-port 3449) :server-ip server-ip @@ -176,12 +191,12 @@ ;; TODO handle this better :resolved-ring-handler (or resolved-ring-handler (utils/require-resolve-handler ring-handler)) - + :open-file-command open-file-command :compile-wait-time (or compile-wait-time 10) - + :file-md5-atom (atom {}) - + :file-change-atom (atom (list)) :browser-callbacks (atom {}) :connection-count (atom {}) @@ -264,10 +279,10 @@ (defn figwheel-server [{:keys [figwheel-options all-builds] :as options}] (let [all-builds (map config/add-compiler-env (config/prep-builds all-builds)) all-builds (ensure-array-map all-builds) - + initial-state (create-initial-state figwheel-options) figwheel-opts (assoc initial-state - :builds all-builds + :builds all-builds :log-writer (extract-log-writer figwheel-options) :cljs-build-fn (extract-cljs-build-fn figwheel-options))] (map->FigwheelServer figwheel-opts))) diff --git a/sidecar/src/figwheel_sidecar/repl.clj b/sidecar/src/figwheel_sidecar/repl.clj index 5537129c..246e76fb 100644 --- a/sidecar/src/figwheel_sidecar/repl.clj +++ b/sidecar/src/figwheel_sidecar/repl.clj @@ -7,10 +7,12 @@ [clojure.java.io :as io] [clojure.string :as string] [clojure.core.async :refer [chan !! put! alts!! timeout close! go go-loop]] + [cognitect.transit :as transit] [clojure.tools.nrepl.middleware.interruptible-eval :as nrepl-eval] [figwheel-sidecar.components.figwheel-server :as server] [figwheel-sidecar.repl-driver :as repl-driver] + [figwheel-sidecar.writer-proxy :as writer-proxy] [figwheel-sidecar.config :as config])) @@ -43,6 +45,12 @@ :value "Eval timed out!" :stacktrace "No stacktrace available."})))) +(defn exec-js [figwheel-server js] + (server/send-message figwheel-server + (:build-id figwheel-server) + {:msg-name :exec-js + :code js})) + (defn connection-available? [figwheel-server build-id] (let [connection-count (server/connection-data figwheel-server)] @@ -161,11 +169,16 @@ figwheel-repl-env (repl-env figwheel-server build) repl-opts (assoc opts :compiler-env (:compiler-env build) - :read (repl-driver/custom-read-fn-factory build)) + :read (repl-driver/multiplexing-reader-factory build) + ;:print (repl-driver/print-recorder-factory build (partial exec-js figwheel-server)) + :eval (repl-driver/signalling-eval-cljs build)) protocol (if (in-nrepl-env?) :nrepl - :default)] - (start-cljs-repl protocol figwheel-repl-env repl-opts)))) + :default) + exec-js-fn (partial exec-js figwheel-server) + out-proxy (writer-proxy/make-proxy-writer *out* (partial repl-driver/out-flush-handler exec-js-fn))] + (binding [*out* out-proxy] + (start-cljs-repl protocol figwheel-repl-env repl-opts))))) ;; deprecated (defn get-project-cljs-builds [] diff --git a/sidecar/src/figwheel_sidecar/repl_driver.clj b/sidecar/src/figwheel_sidecar/repl_driver.clj index b5682172..cbc6cbae 100644 --- a/sidecar/src/figwheel_sidecar/repl_driver.clj +++ b/sidecar/src/figwheel_sidecar/repl_driver.clj @@ -3,26 +3,112 @@ [clojure.core.async :refer [chan !! put! alts!! timeout close! go go-loop]] [cljs.repl :as cljs-repl] [clojure.tools.reader :as reader] - [clojure.tools.reader.reader-types :as reader-types])) + [clojure.tools.reader.reader-types :as reader-types] + [clojure.string :as string]) + (:import (java.io StringWriter OutputStreamWriter))) (def repl-commands-channel (chan)) +(def print-recording (volatile! nil)) +(def active-repl-opts (volatile! nil)) +(def ignore-next-print (volatile! false)) + +(defn resolve-repl-opts [] + (if-let [opts (resolve 'cljs.repl/*repl-opts*)] + @opts)) + +; cljs.repl/eval-cljs is private, this is a hack around it +(defn resolve-eval-cljs [] + (if-let [eval-cljs-var (resolve 'cljs.repl/eval-cljs)] + @eval-cljs-var)) + +; here we mimic code in cljs.repl/repl* +(defn current-repl-special-fns [] + (let [special-fns (:special-fns @active-repl-opts)] + (merge cljs-repl/default-special-fns special-fns))) + +; here we mimic read-eval-print's behaviour in cljs.repl/repl* +(defn is-special-fn-call-form? [form] + (let [is-special-fn? (set (keys (current-repl-special-fns)))] + (and (seq? form) (is-special-fn? (first form))))) ; here we mimic parsing behaviour of cljs.repl/repl-read -(defn read-external-input [input] +(defn read-input [input] (let [rdr (reader-types/string-push-back-reader input)] (cljs-repl/skip-whitespace rdr) (reader/read {:read-cond :allow :features #{:cljs}} rdr))) -(defn exec-external-command! [code input] +(defn is-special-fn-call? [input-text] + (is-special-fn-call-form? (read-input input-text))) + +(defn exec-external-command! [request-id code input info-method] ; first, we echo user's input into stdout (println input) + ; then we try to parse the code and put result on the channel - (let [command (read-external-input code)] + ; in case the code is a special-fn call, we execute user's input + ; otherwise we execute code provided from client (which is a wrapped version) + (let [effective-code (if (is-special-fn-call? input) input code) + command (read-input effective-code) + command-record {:kind :external + :command command + :request-id request-id + :info-method info-method}] (go - (>!! repl-commands-channel command)))) + (>!! repl-commands-channel command-record)))) + +;(def ^:dynamic *orig-out* nil) +;(def ^:dynamic *out-proxy-gatekeeper* 0) +; +;; see http://docs.oracle.com/javase/7/docs/api/java/io/Writer.html +;(def out-proxy +; (proxy [StringWriter] [] +; (write +; ([x] +; (if (zero? *out-proxy-gatekeeper*) +; (.write *orig-out* x)) +; (binding [*out-proxy-gatekeeper* (inc *out-proxy-gatekeeper*)] +; (proxy-super write x))) +; ([x off len] +; (if (zero? *out-proxy-gatekeeper*) +; (.write *orig-out* x off len)) +; (binding [*out-proxy-gatekeeper* (inc *out-proxy-gatekeeper*)] +; (proxy-super write x off len)))) +; (append +; ([x] +; (if (zero? *out-proxy-gatekeeper*) +; (.append *orig-out* x)) +; (binding [*out-proxy-gatekeeper* (inc *out-proxy-gatekeeper*)] +; (proxy-super append x) +; this)) +; ([x start end] +; (if (zero? *out-proxy-gatekeeper*) +; (.append *orig-out* x start end)) +; (binding [*out-proxy-gatekeeper* (inc *out-proxy-gatekeeper*)] +; (proxy-super append x start end) +; this))) +; (close +; ([] +; (.close *orig-out*) +; (proxy-super close))) +; (flush +; ([] +; (.flush *orig-out*) +; (proxy-super flush))))) + +(defn start-recording! [command-record] + ;(assert (not @orig-out)) + ;(vreset! orig-out *out*) + ;(set! *out* out-proxy) + (vreset! print-recording command-record)) + +(defn stop-recording! [] + ;(when @orig-out + ; (set! *out* @orig-out) + ; (vreset! orig-out nil)) + (vreset! print-recording nil)) ; TODO: we should create one-channel-per-build-id -(defn custom-read-fn-factory +(defn multiplexing-reader-factory "This factory creates a new REPL reading function. Normally this function is responsible for waiting for user input on stdin, parsing it and passing a valid form back. REPL system then calls it to parse next form, and so on. @@ -36,13 +122,59 @@ [build] (let [pending-read? (volatile! false)] (fn [& args] - ; make sure we have pending read in-flight - (if-not @pending-read? + (if-not @active-repl-opts + (vreset! active-repl-opts (resolve-repl-opts))) + + (stop-recording!) + + ; make sure we have a pending read in-flight + (when-not @pending-read? (vreset! pending-read? true) (go - (let [command (apply cljs-repl/repl-read args)] - (>!! repl-commands-channel command) + (let [command (apply cljs-repl/repl-read args) + command-record {:kind :stdin + :command command}] + (>!! repl-commands-channel command-record) (vreset! pending-read? false)))) ; wait & serve next input from the channel (can be produced by cljs-repl/repl-read or exec-external-command!) - ( text + (string/replace #"'" "\\'") + (string/replace #"\n" "\\n"))] + (str info-method "(" request-id ", '" escaped-text "')"))) + +(defn print-recorder-factory [build exec-js-fn] + (fn [& args] + #_(when-let [{:keys [request-id info-method]} @print-recording] + (if-not @ignore-next-print + (let [js-code (info-method-call-code-snippet request-id info-method args)] + (exec-js-fn js-code)))) + (apply println args) + (vreset! ignore-next-print false))) + +(defn signalling-eval-cljs [build] + (fn [& args] + (when-let [eval-cljs (resolve-eval-cljs)] + (let [result (apply eval-cljs args)] + ; main REPL loop calls eval and then immediatelly printr result value + ; if recording, we want to prevent next print to be recorded + (vreset! ignore-next-print true) + result)))) + +(defn out-flush-handler [exec-js-fn string-writer] + (let [buffer (.getBuffer string-writer) + content (.toString string-writer)] + (.setLength buffer 0) + (when-let [{:keys [request-id info-method]} @print-recording] + (if-not @ignore-next-print + (let [js-code (info-method-call-code-snippet request-id info-method content)] + (exec-js-fn js-code)))))) \ No newline at end of file diff --git a/sidecar/src/figwheel_sidecar/writer_proxy.clj b/sidecar/src/figwheel_sidecar/writer_proxy.clj new file mode 100644 index 00000000..dd012a5f --- /dev/null +++ b/sidecar/src/figwheel_sidecar/writer_proxy.clj @@ -0,0 +1,51 @@ +(ns figwheel-sidecar.writer-proxy + (:import (java.io StringWriter))) + +(defmacro enter-gate [gate & body] + `(do + (vreset! ~gate (inc (deref ~gate))) + (try + ~@body + (finally + (vreset! ~gate (dec (deref ~gate))))))) + +(defn open? [gate] + (zero? @gate)) + +; see http://docs.oracle.com/javase/7/docs/api/java/io/Writer.html +(defn make-proxy-writer [out flush-handler] + (let [gate (volatile! 0)] + (proxy [StringWriter] [] + (write + ([x] + (if (open? gate) + (.write out x)) + (enter-gate gate + (proxy-super write x))) + ([x off len] + (if (open? gate) + (.write out x off len)) + (enter-gate gate + (proxy-super write x off len)))) + (append + ([x] + (if (open? gate) + (.append out x)) + (enter-gate gate + (proxy-super append x)) + this) + ([x start end] + (if (open? gate) + (.append out x start end)) + (enter-gate gate + (proxy-super append x start end)) + this)) + (close + ([] + (.close out) + (proxy-super close))) + (flush + ([] + (.flush out) + (proxy-super flush) + (flush-handler this)))))) \ No newline at end of file diff --git a/support/project.clj b/support/project.clj index 06781193..4e218265 100644 --- a/support/project.clj +++ b/support/project.clj @@ -10,4 +10,5 @@ [[org.clojure/clojure "1.7.0"] [org.clojure/clojurescript "1.7.170" :exclusions [org.apache.ant/ant]] - [org.clojure/core.async "0.2.374"]]) + [org.clojure/core.async "0.2.374"] + [com.cognitect/transit-cljs "0.8.232"]]) diff --git a/support/src/figwheel/client.cljs b/support/src/figwheel/client.cljs index a36b888f..8da4d371 100644 --- a/support/src/figwheel/client.cljs +++ b/support/src/figwheel/client.cljs @@ -107,7 +107,7 @@ (cond (reload-file-state? msg-names opts) (alts! [(reloading/reload-js-files opts msg) (timeout 1000)]) - + (block-reload-file-state? msg-names opts) (utils/log :warn (str "Figwheel: Not loading code with warnings - " (-> msg :files first :file)))) (do @@ -167,6 +167,13 @@ (when-not js/cljs.user (set! js/cljs.user #js {}))) +(defn exec-plugin [{:keys [build-id] :as opts}] + (fn [[msg & _]] + (when (= :exec-js (:msg-name msg)) + (ensure-cljs-user) + ; javascript execution requests are just commands to be executed, nobody cares about results + (eval-javascript** (:code msg) opts identity)))) + (defn repl-plugin [{:keys [build-id] :as opts}] (fn [[{:keys [msg-name] :as msg} & _]] (when (= :repl-eval msg-name) @@ -318,7 +325,8 @@ :file-reloader-plugin file-reloader-plugin :comp-fail-warning-plugin compile-fail-warning-plugin :css-reloader-plugin css-reloader-plugin - :repl-plugin repl-plugin} + :repl-plugin repl-plugin + :exec-plugin exec-plugin} base (if (not (utils/html-env?)) ;; we are in an html environment? (select-keys base [#_:enforce-project-plugin :file-reloader-plugin diff --git a/support/src/figwheel/client/repl.cljs b/support/src/figwheel/client/repl.cljs index 75fe3243..0d0963d9 100644 --- a/support/src/figwheel/client/repl.cljs +++ b/support/src/figwheel/client/repl.cljs @@ -2,14 +2,15 @@ (:require [figwheel.client.socket :as socket])) -(defn ^:export repl-eval [request-id code input] +(defn ^:export repl-eval [request-id code & [input info-method]] (socket/send! {:figwheel-event "repl-eval" :request-id request-id :code code - :input input})) + :input input + :info-method info-method})) (defn ^:export is-repl-connected [] (socket/connected?)) (defn ^:export is-repl-available [] - false) ; TODO: detect repl availablity somehow + true) ; TODO: detect repl availablity somehow diff --git a/support/src/figwheel/client/socket.cljs b/support/src/figwheel/client/socket.cljs index 18b51fe4..ad237364 100644 --- a/support/src/figwheel/client/socket.cljs +++ b/support/src/figwheel/client/socket.cljs @@ -1,7 +1,7 @@ (ns figwheel.client.socket (:require [figwheel.client.utils :as utils] - [cljs.reader :refer [read-string]])) + [cognitect.transit :as transit])) (defn get-websocket-imp [] (cond @@ -36,6 +36,9 @@ (defonce socket-atom (atom false)) +(defonce socket-reader (transit/reader :json)) +(defonce socket-writer (transit/writer :json)) + (defn connected? [] (boolean @socket-atom)) @@ -43,7 +46,8 @@ "Send a end message to the server." [msg] (when (connected?) - (.send @socket-atom (pr-str msg)))) + (.log js/console "X" (transit/write socket-writer msg)) + (.send @socket-atom (transit/write socket-writer msg)))) (defn clear-socket-atom [] (reset! socket-atom false)) @@ -61,7 +65,7 @@ (let [url (str websocket-url (if build-id (str "/" build-id) "")) socket (WebSocket. url)] (set! (.-onmessage socket) (fn [msg-str] - (when-let [msg (read-string (.-data msg-str))] + (when-let [msg (transit/read socket-reader (.-data msg-str))] (utils/debug-prn msg) (and (map? msg) (:msg-name msg)