From d04742661b263c537142eb85c684df5366c71efb Mon Sep 17 00:00:00 2001 From: Antonin Hildebrand Date: Wed, 27 Jan 2016 22:19:45 +0100 Subject: [PATCH] be more defensive when requiring dirac agent Try to provide meaningful errors when agent fails to require its dependencies. --- readme.md | 15 +-- src/agent/dirac/agent.clj | 164 ++++++------------------------ src/agent/dirac/agent/logging.clj | 1 + src/agent/dirac/agent_impl.clj | 140 +++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 142 deletions(-) create mode 100644 src/agent/dirac/agent_impl.clj diff --git a/readme.md b/readme.md index 56a1fec010..8e2acf1096 100644 --- a/readme.md +++ b/readme.md @@ -204,14 +204,9 @@ By default you should run it on port `8230` and with `dirac.nrepl.middleware/dir was implemented as a [Piggieback middleware](https://github.com/cemerick/piggieback) fork, so you cannot run both. Think of Dirac middleware as Piggieback middleware replacement with some extra features specific for Dirac DevTools. -Also for some reason (maybe a Leiningen's limitation?) you have to know all middleware dependencies and add them into your -project dependencies. The configuration snippet could look something like this: +The configuration snippet could look something like this: - :dependencies [[org.clojure/tools.logging "0.3.1"] - [clj-logging-config "1.9.12"] - [http-kit "2.1.21-alpha2"] - [org.clojure/tools.nrepl "0.2.12"] - [binaryage/dirac ""]] + :dependencies [[binaryage/dirac ""]] :repl-options {:port 8230 :nrepl-middleware [dirac.nrepl.middleware/dirac-repl]} @@ -227,12 +222,12 @@ Dirac Agent is a piece of server software which connects to an existing nREPL se provides nREPL connections to the browser. Please note that Dirac DevTools is "just" a web app. It cannot open a classic socket connection and talk to nREPL server directly. -Instead it connects to a Dirac Agent which listens for web socket connections on port 8231. Dirac Agent has also an open connection -to your nREPL server at port 8230 so it can bridge messages between those two. +Instead it connects to an Dirac Agent instance which listens for web socket connections on port 8231. Dirac Agent has also an open connection +to your nREPL server at port 8230 so it can bridge messages between those two. It tunneling messages between the browser and the nREPL server. Actually Dirac Agent is a bit smarter than that. It allows one-to-many scenario, where multiple Dirac DevTools instances can connect to a singe Dirac Agent which talks to a single nREPL server. Each Dirac DevTools instance is assigned its own nREPL session, -so they don't step on each others' toes. Thanks to this you can open multiple pages with different Dirac DevTools and +so they don't step on each others' toes. Thanks to sessions you can open multiple pages with different Dirac DevTools and they all can have their own independent REPLs. Unfortunately this is the hardest part of the setup and most fragile. diff --git a/src/agent/dirac/agent.clj b/src/agent/dirac/agent.clj index 3e7e629bda..fe7e5ed8e5 100644 --- a/src/agent/dirac/agent.clj +++ b/src/agent/dirac/agent.clj @@ -1,140 +1,40 @@ (ns dirac.agent - (:require [clojure.core.async :refer [chan !! put! alts!! timeout close! go go-loop]] - [clojure.tools.logging :as log] - [dirac.agent.logging :as logging] - [dirac.agent.config :as config] - [dirac.agent.version :refer [version]] - [dirac.lib.nrepl-tunnel :as nrepl-tunnel] - [dirac.lib.utils :as utils]) - (:import (java.net ConnectException) - (clojure.lang ExceptionInfo))) - -(defn ^:dynamic failed-to-start-dirac-agent-message [max-boot-trials trial-display nrepl-server-url] - (str "Failed to start Dirac Agent. " - "The nREPL server didn't come online in time. " - "Made " max-boot-trials " connection attempts over last " trial-display " seconds.\n" - "Did you really start your nREPL server at " nrepl-server-url "? " - "Maybe a firewall problem?")) - -; -- DiracAgent construction ----------------------------------------------------------------------------------------------- - -(defrecord DiracAgent [id options tunnel] - Object - (toString [this] - (str "[DiracAgent#" (:id this) "]"))) - -(def last-id (volatile! 0)) - -(defn next-id! [] - (vswap! last-id inc)) - -(defn make-agent! [options tunnel] - (let [tunnel (DiracAgent. (next-id!) options tunnel)] - (log/trace "Made" (str tunnel)) - tunnel)) - -; -- DiracAgent access ------------------------------------------------------------------------------------------------------ - -(defn get-tunnel [agent] - (:tunnel agent)) - -(defn get-agent-info [agent] - (let [tunnel (get-tunnel agent)] - (str "Dirac Agent v" version "\n" - (nrepl-tunnel/get-tunnel-info tunnel)))) - -; -- lower-level api -------------------------------------------------------------------------------------------------------- - -(defn create-tunnel! [config] - (let [effective-config (config/get-effective-config config)] - (nrepl-tunnel/create! effective-config))) - -(defn destroy-tunnel! [tunnel] - (nrepl-tunnel/destroy! tunnel)) - -(defn create-agent! [config] - (let [tunnel (create-tunnel! config)] - (make-agent! config tunnel))) - -(defn destroy-agent! [agent] - (let [tunnel (get-tunnel agent)] - (destroy-tunnel! tunnel))) + (:require [clojure.string :as string])) + +(defn ^:dynamic dirac-require-failure-msg [] + (str "\n" + "Dirac failed to require its implementation. This is likely caused by missing or wrong dependencies in your project.\n" + "Please review your project configuration with Dirac installation instructuctions here: " + "https://github.com/binaryage/dirac#installation.\n")) + +; we want to provide meaningful errors when people forget to include some Dirac dependencies into their projects +; we try require stuff dynamically and make sense of the errors if any +(def ok? + (try + (require 'dirac.agent-impl) + true + (catch Throwable e + (let [message (.getMessage e) + _ (println message) + groups (re-matches #".*FileNotFoundException: Could not locate (.*).*" message) + filename (second groups)] + (if filename + (let [lib-name (first (string/split filename #"/"))] + (println (str (dirac-require-failure-msg) + "The problem is likely in missing library '" lib-name "' in your dependencies." + "Also make sure you are using a recent version.\n"))) + (println (dirac-require-failure-msg) e "\n"))) + false))) ; -- high-level api --------------------------------------------------------------------------------------------------------- -; for ease of use from REPL we support only one active agent -; if you need more agents, use low-level API to do that - -(def current-agent (atom nil)) +(def current-agent (if ok? (resolve 'dirac.agent-impl/current-agent) (constantly (atom nil)))) -(defn live? [] - (not (nil? @current-agent))) - -(defn destroy! [] - (when (live?) - (destroy-agent! @current-agent) - (reset! current-agent nil) - (not (live?)))) - -(defn create! [config] - (when (live?) - (destroy!)) - (reset! current-agent (create-agent! config)) - (live?)) - -(defn boot-now! [config] - (let [effective-config (config/get-effective-config config) - {:keys [max-boot-trials delay-between-boot-trials initial-boot-delay]} effective-config] - (if (pos? initial-boot-delay) - (Thread/sleep initial-boot-delay)) - (loop [trial 1] - (if (<= trial max-boot-trials) - (let [result (try - (log/info (str "Starting Dirac Agent (attempt #" trial ")")) - (create! config) - (catch ExceptionInfo e ; for example missing nREPL middleware - (log/error "ERROR:" (.getMessage e)) - :error) - (catch ConnectException _ - ::retry) ; server might not be online yet - (catch Throwable e - (log/error "ERROR:" "Failed to create Dirac Agent:\n" e) ; *** - ::error))] - (case result - true (let [agent @current-agent] - (assert agent) - (log/debug "Started Dirac Agent" (str agent)) - (println) - (println (get-agent-info agent)) - true) ; success - false (do - (log/error "ERROR:" "Failed to start Dirac Agent.") - false) - ::error false ; error was already reported by *** - ::retry (do - (Thread/sleep delay-between-boot-trials) - (recur (inc trial))))) - (let [{:keys [host port]} (:nrepl-server effective-config) - nrepl-server-url (utils/get-nrepl-server-url host port) - trial-period-in-seconds (/ (* max-boot-trials delay-between-boot-trials) 1000) - trial-display (format "%.2f" (double trial-period-in-seconds))] - (log/error (failed-to-start-dirac-agent-message max-boot-trials trial-display nrepl-server-url)) - false))))) +(def live? (if ok? (resolve 'dirac.agent-impl/live?) (constantly false))) +(def destroy! (if ok? (resolve 'dirac.agent-impl/destroy!) (constantly false))) +(def create! (if ok? (resolve 'dirac.agent-impl/create!) (constantly false))) +(def boot-now! (if ok? (resolve 'dirac.agent-impl/boot-now!) (constantly false))) ; -- entry point ------------------------------------------------------------------------------------------------------------ -(defn boot! - "Attempts to boot the Dirac Agent. - - We want to make this function robust and safe to be called by :repl-options :init (Leiningen). - It runs on a separate thread and waits there for nREPL server to come online. - - The problem with `lein repl` :init config is that it is evaluated before nREPL fully starts. - Actually it waits for this init code to fully evaluate before starting nREPL server." - [& [config]] - (let [effective-config (config/get-effective-config config)] - (if-not (:skip-logging-setup effective-config) - (logging/setup-logging! effective-config)) - (log/info "Booting Dirac Agent...") - (log/debug "effective config: " effective-config) - (future (boot-now! config)))) \ No newline at end of file +(def boot! (if ok? (resolve 'dirac.agent-impl/boot!) (constantly nil))) \ No newline at end of file diff --git a/src/agent/dirac/agent/logging.clj b/src/agent/dirac/agent/logging.clj index f040b5a1b8..e4fbeadf5f 100644 --- a/src/agent/dirac/agent/logging.clj +++ b/src/agent/dirac/agent/logging.clj @@ -13,5 +13,6 @@ (logging/setup-logging! options) (config/set-loggers! "dirac.agent" (utils/make-logging-options base-options options) + "dirac.agent-impl" (utils/make-logging-options base-options options) "dirac.agent.logging" (utils/make-logging-options base-options options) "dirac.agent.config" (utils/make-logging-options base-options options)))) \ No newline at end of file diff --git a/src/agent/dirac/agent_impl.clj b/src/agent/dirac/agent_impl.clj new file mode 100644 index 0000000000..7d1bb0257d --- /dev/null +++ b/src/agent/dirac/agent_impl.clj @@ -0,0 +1,140 @@ +(ns dirac.agent-impl + (:require [clojure.core.async :refer [chan !! put! alts!! timeout close! go go-loop]] + [clojure.tools.logging :as log] + [dirac.agent.logging :as logging] + [dirac.agent.config :as config] + [dirac.agent.version :refer [version]] + [dirac.lib.nrepl-tunnel :as nrepl-tunnel] + [dirac.lib.utils :as utils]) + (:import (java.net ConnectException) + (clojure.lang ExceptionInfo))) + +(defn ^:dynamic failed-to-start-dirac-agent-message [max-boot-trials trial-display nrepl-server-url] + (str "Failed to start Dirac Agent. " + "The nREPL server didn't come online in time. " + "Made " max-boot-trials " connection attempts over last " trial-display " seconds.\n" + "Did you really start your nREPL server at " nrepl-server-url "? " + "Maybe a firewall problem?")) + +; -- DiracAgent construction ----------------------------------------------------------------------------------------------- + +(defrecord DiracAgent [id options tunnel] + Object + (toString [this] + (str "[DiracAgent#" (:id this) "]"))) + +(def last-id (volatile! 0)) + +(defn next-id! [] + (vswap! last-id inc)) + +(defn make-agent! [options tunnel] + (let [tunnel (DiracAgent. (next-id!) options tunnel)] + (log/trace "Made" (str tunnel)) + tunnel)) + +; -- DiracAgent access ------------------------------------------------------------------------------------------------------ + +(defn get-tunnel [agent] + (:tunnel agent)) + +(defn get-agent-info [agent] + (let [tunnel (get-tunnel agent)] + (str "Dirac Agent v" version "\n" + (nrepl-tunnel/get-tunnel-info tunnel)))) + +; -- lower-level api -------------------------------------------------------------------------------------------------------- + +(defn create-tunnel! [config] + (let [effective-config (config/get-effective-config config)] + (nrepl-tunnel/create! effective-config))) + +(defn destroy-tunnel! [tunnel] + (nrepl-tunnel/destroy! tunnel)) + +(defn create-agent! [config] + (let [tunnel (create-tunnel! config)] + (make-agent! config tunnel))) + +(defn destroy-agent! [agent] + (let [tunnel (get-tunnel agent)] + (destroy-tunnel! tunnel))) + +; -- high-level api --------------------------------------------------------------------------------------------------------- + +; for ease of use from REPL we support only one active agent +; if you need more agents, use low-level API to do that + +(def current-agent (atom nil)) + +(defn live? [] + (not (nil? @current-agent))) + +(defn destroy! [] + (when (live?) + (destroy-agent! @current-agent) + (reset! current-agent nil) + (not (live?)))) + +(defn create! [config] + (when (live?) + (destroy!)) + (reset! current-agent (create-agent! config)) + (live?)) + +(defn boot-now! [config] + (let [effective-config (config/get-effective-config config) + {:keys [max-boot-trials delay-between-boot-trials initial-boot-delay]} effective-config] + (if (pos? initial-boot-delay) + (Thread/sleep initial-boot-delay)) + (loop [trial 1] + (if (<= trial max-boot-trials) + (let [result (try + (log/info (str "Starting Dirac Agent (attempt #" trial ")")) + (create! config) + (catch ExceptionInfo e ; for example missing nREPL middleware + (log/error "ERROR:" (.getMessage e)) + :error) + (catch ConnectException _ + ::retry) ; server might not be online yet + (catch Throwable e + (log/error "ERROR:" "Failed to create Dirac Agent:\n" e) ; *** + ::error))] + (case result + true (let [agent @current-agent] + (assert agent) + (log/debug "Started Dirac Agent" (str agent)) + (println) + (println (get-agent-info agent)) + true) ; success + false (do + (log/error "ERROR:" "Failed to start Dirac Agent.") + false) + ::error false ; error was already reported by *** + ::retry (do + (Thread/sleep delay-between-boot-trials) + (recur (inc trial))))) + (let [{:keys [host port]} (:nrepl-server effective-config) + nrepl-server-url (utils/get-nrepl-server-url host port) + trial-period-in-seconds (/ (* max-boot-trials delay-between-boot-trials) 1000) + trial-display (format "%.2f" (double trial-period-in-seconds))] + (log/error (failed-to-start-dirac-agent-message max-boot-trials trial-display nrepl-server-url)) + false))))) + +; -- entry point ------------------------------------------------------------------------------------------------------------ + +(defn boot! + "Attempts to boot the Dirac Agent. + + We want to make this function robust and safe to be called by :repl-options :init (Leiningen). + It runs on a separate thread and waits there for nREPL server to come online. + + The problem with `lein repl` :init config is that it is evaluated before nREPL fully starts. + Actually it waits for this init code to fully evaluate before starting nREPL server." + [& [config]] + (let [effective-config (config/get-effective-config config)] + (if-not (:skip-logging-setup effective-config) + (logging/setup-logging! effective-config)) + (log/info "Booting Dirac Agent...") + (log/debug "effective config: " effective-config) + (future (boot-now! config)))) \ No newline at end of file