Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial spike.

  • Loading branch information...
commit b197ced221ddeff5c207165ac1179159fce97493 0 parents
@cemerick cemerick authored
16 .gitignore
@@ -0,0 +1,16 @@
+# emacs + vi backup files
+*~
+.*.sw*
+
+# various IDE junk
+*.ipr
+*.iml
+*.iws
+.project
+.classpath
+.settings
+
+# artifacts, etc
+eclipse-classes
+classes
+target
77 README.md
@@ -0,0 +1,77 @@
+# nREPL
+
+[nREPL](http://github.com/cemerick/nREPL) is a Clojure *n*etwork REPL
+that provides a REPL server and client, along with some common APIs
+of use to IDEs and other tools that may need to evaluate Clojure
+code in remote environments.
+
+## Usage
+
+### "Installation"
+
+### Embedding nREPL
+
+This library is in its infancy. More info to come. In the meantime,
+check out the tests or cemerick.nrepl.main for usage examples.
+
+### Debugging
+
+## Need Help?
+
+Ping `cemerick` on freenode irc or twitter.
+
+## Specification
+
+### Protocol
+
+### Messages
+
+### Timeouts and Interrupts
+
+## Why another REPL implementation?
+
+There are various Clojure REPL implementations, including
+[swank-clojure](http://github.com/technomancy/swank-clojure)
+and others associated with various tools and IDEs. So, why
+another?
+
+First, while swank-clojure is widely used due to its association with
+emacs, there is no Clojure swank client implementation. Further, swank's
+Common Lisp/SLIME roots mean that its design and future development
+are not ideal for serving the needs of users of Clojure remote REPLs.
+
+Second, other network REPL implementations are incomplete and/or
+not suitable for key use cases.
+
+nREPL has been designed in conjunction with the leads of various
+Clojure development tools, with the aim of ensuring that it satisfies the
+requirements of both application developers (in support of activities ranging
+from interactive remote debugging and experimentation in development
+contexts through to more advanced use cases such as updating deployed
+applications) as well as toolmakers (providing a standard way to
+introspect running environments as a way of informing user interfaces
+of all kinds).
+
+It is hoped that users of emacs/SLIME will also be able to use nREPL, either
+by extending SLIME itself to work with its protocol, or by implementing
+a swank-compatible adapter for nREPL.
+
+The network protocol used is simple, depending neither
+on JVM or Clojure specifics, thereby allowing (encouraging?) the development
+of non-Clojure REPL clients. The REPLs operational semantics are such
+that essentially any future non-JVM Clojure implementations should be able to
+implement it (hopefully within this same project as a separate batch
+of methods), with allowances for hosts that lack the concurrency primitives
+to support e.g. asynchronous evaluation, interrupts, etc.
+
+## Thanks
+
+Thanks to Laurent Petit, Eric Thorsen, Justin Balthrop, Christophe Grand,
+Hugo Duncan, Meikel Brandmeyer, and Phil Hagelberg for their helpful feedback during the initial
+design phases of nREPL.
+
+## License
+
+Copyright © 2010 Chas Emerick
+
+Licensed under the EPL. (See the file epl.html.)
261 epl.html
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="ISO-8859-1" ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" />
+<title>Eclipse Public License - Version 1.0</title>
+<style type="text/css">
+ body {
+ size: 8.5in 11.0in;
+ margin: 0.25in 0.5in 0.25in 0.5in;
+ tab-interval: 0.5in;
+ }
+ p {
+ margin-left: auto;
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+ }
+ p.list {
+ margin-left: 0.5in;
+ margin-top: 0.05em;
+ margin-bottom: 0.05em;
+ }
+ </style>
+
+</head>
+
+<body lang="EN-US">
+
+<h2>Eclipse Public License - v 1.0</h2>
+
+<p>THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE (&quot;AGREEMENT&quot;). ANY USE, REPRODUCTION OR
+DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS
+AGREEMENT.</p>
+
+<p><b>1. DEFINITIONS</b></p>
+
+<p>&quot;Contribution&quot; means:</p>
+
+<p class="list">a) in the case of the initial Contributor, the initial
+code and documentation distributed under this Agreement, and</p>
+<p class="list">b) in the case of each subsequent Contributor:</p>
+<p class="list">i) changes to the Program, and</p>
+<p class="list">ii) additions to the Program;</p>
+<p class="list">where such changes and/or additions to the Program
+originate from and are distributed by that particular Contributor. A
+Contribution 'originates' from a Contributor if it was added to the
+Program by such Contributor itself or anyone acting on such
+Contributor's behalf. Contributions do not include additions to the
+Program which: (i) are separate modules of software distributed in
+conjunction with the Program under their own license agreement, and (ii)
+are not derivative works of the Program.</p>
+
+<p>&quot;Contributor&quot; means any person or entity that distributes
+the Program.</p>
+
+<p>&quot;Licensed Patents&quot; mean patent claims licensable by a
+Contributor which are necessarily infringed by the use or sale of its
+Contribution alone or when combined with the Program.</p>
+
+<p>&quot;Program&quot; means the Contributions distributed in accordance
+with this Agreement.</p>
+
+<p>&quot;Recipient&quot; means anyone who receives the Program under
+this Agreement, including all Contributors.</p>
+
+<p><b>2. GRANT OF RIGHTS</b></p>
+
+<p class="list">a) Subject to the terms of this Agreement, each
+Contributor hereby grants Recipient a non-exclusive, worldwide,
+royalty-free copyright license to reproduce, prepare derivative works
+of, publicly display, publicly perform, distribute and sublicense the
+Contribution of such Contributor, if any, and such derivative works, in
+source code and object code form.</p>
+
+<p class="list">b) Subject to the terms of this Agreement, each
+Contributor hereby grants Recipient a non-exclusive, worldwide,
+royalty-free patent license under Licensed Patents to make, use, sell,
+offer to sell, import and otherwise transfer the Contribution of such
+Contributor, if any, in source code and object code form. This patent
+license shall apply to the combination of the Contribution and the
+Program if, at the time the Contribution is added by the Contributor,
+such addition of the Contribution causes such combination to be covered
+by the Licensed Patents. The patent license shall not apply to any other
+combinations which include the Contribution. No hardware per se is
+licensed hereunder.</p>
+
+<p class="list">c) Recipient understands that although each Contributor
+grants the licenses to its Contributions set forth herein, no assurances
+are provided by any Contributor that the Program does not infringe the
+patent or other intellectual property rights of any other entity. Each
+Contributor disclaims any liability to Recipient for claims brought by
+any other entity based on infringement of intellectual property rights
+or otherwise. As a condition to exercising the rights and licenses
+granted hereunder, each Recipient hereby assumes sole responsibility to
+secure any other intellectual property rights needed, if any. For
+example, if a third party patent license is required to allow Recipient
+to distribute the Program, it is Recipient's responsibility to acquire
+that license before distributing the Program.</p>
+
+<p class="list">d) Each Contributor represents that to its knowledge it
+has sufficient copyright rights in its Contribution, if any, to grant
+the copyright license set forth in this Agreement.</p>
+
+<p><b>3. REQUIREMENTS</b></p>
+
+<p>A Contributor may choose to distribute the Program in object code
+form under its own license agreement, provided that:</p>
+
+<p class="list">a) it complies with the terms and conditions of this
+Agreement; and</p>
+
+<p class="list">b) its license agreement:</p>
+
+<p class="list">i) effectively disclaims on behalf of all Contributors
+all warranties and conditions, express and implied, including warranties
+or conditions of title and non-infringement, and implied warranties or
+conditions of merchantability and fitness for a particular purpose;</p>
+
+<p class="list">ii) effectively excludes on behalf of all Contributors
+all liability for damages, including direct, indirect, special,
+incidental and consequential damages, such as lost profits;</p>
+
+<p class="list">iii) states that any provisions which differ from this
+Agreement are offered by that Contributor alone and not by any other
+party; and</p>
+
+<p class="list">iv) states that source code for the Program is available
+from such Contributor, and informs licensees how to obtain it in a
+reasonable manner on or through a medium customarily used for software
+exchange.</p>
+
+<p>When the Program is made available in source code form:</p>
+
+<p class="list">a) it must be made available under this Agreement; and</p>
+
+<p class="list">b) a copy of this Agreement must be included with each
+copy of the Program.</p>
+
+<p>Contributors may not remove or alter any copyright notices contained
+within the Program.</p>
+
+<p>Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.</p>
+
+<p><b>4. COMMERCIAL DISTRIBUTION</b></p>
+
+<p>Commercial distributors of software may accept certain
+responsibilities with respect to end users, business partners and the
+like. While this license is intended to facilitate the commercial use of
+the Program, the Contributor who includes the Program in a commercial
+product offering should do so in a manner which does not create
+potential liability for other Contributors. Therefore, if a Contributor
+includes the Program in a commercial product offering, such Contributor
+(&quot;Commercial Contributor&quot;) hereby agrees to defend and
+indemnify every other Contributor (&quot;Indemnified Contributor&quot;)
+against any losses, damages and costs (collectively &quot;Losses&quot;)
+arising from claims, lawsuits and other legal actions brought by a third
+party against the Indemnified Contributor to the extent caused by the
+acts or omissions of such Commercial Contributor in connection with its
+distribution of the Program in a commercial product offering. The
+obligations in this section do not apply to any claims or Losses
+relating to any actual or alleged intellectual property infringement. In
+order to qualify, an Indemnified Contributor must: a) promptly notify
+the Commercial Contributor in writing of such claim, and b) allow the
+Commercial Contributor to control, and cooperate with the Commercial
+Contributor in, the defense and any related settlement negotiations. The
+Indemnified Contributor may participate in any such claim at its own
+expense.</p>
+
+<p>For example, a Contributor might include the Program in a commercial
+product offering, Product X. That Contributor is then a Commercial
+Contributor. If that Commercial Contributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Contributor's responsibility
+alone. Under this section, the Commercial Contributor would have to
+defend claims against the other Contributors related to those
+performance claims and warranties, and if a court requires any other
+Contributor to pay any damages as a result, the Commercial Contributor
+must pay those damages.</p>
+
+<p><b>5. NO WARRANTY</b></p>
+
+<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
+PROVIDED ON AN &quot;AS IS&quot; BASIS, WITHOUT WARRANTIES OR CONDITIONS
+OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION,
+ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
+OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
+responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with its
+exercise of rights under this Agreement , including but not limited to
+the risks and costs of program errors, compliance with applicable laws,
+damage to or loss of data, programs or equipment, and unavailability or
+interruption of operations.</p>
+
+<p><b>6. DISCLAIMER OF LIABILITY</b></p>
+
+<p>EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT
+NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
+WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
+DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
+HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.</p>
+
+<p><b>7. GENERAL</b></p>
+
+<p>If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further action
+by the parties hereto, such provision shall be reformed to the minimum
+extent necessary to make such provision valid and enforceable.</p>
+
+<p>If Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging that the
+Program itself (excluding combinations of the Program with other
+software or hardware) infringes such Recipient's patent(s), then such
+Recipient's rights granted under Section 2(b) shall terminate as of the
+date such litigation is filed.</p>
+
+<p>All Recipient's rights under this Agreement shall terminate if it
+fails to comply with any of the material terms or conditions of this
+Agreement and does not cure such failure in a reasonable period of time
+after becoming aware of such noncompliance. If all Recipient's rights
+under this Agreement terminate, Recipient agrees to cease use and
+distribution of the Program as soon as reasonably practicable. However,
+Recipient's obligations under this Agreement and any licenses granted by
+Recipient relating to the Program shall continue and survive.</p>
+
+<p>Everyone is permitted to copy and distribute copies of this
+Agreement, but in order to avoid inconsistency the Agreement is
+copyrighted and may only be modified in the following manner. The
+Agreement Steward reserves the right to publish new versions (including
+revisions) of this Agreement from time to time. No one other than the
+Agreement Steward has the right to modify this Agreement. The Eclipse
+Foundation is the initial Agreement Steward. The Eclipse Foundation may
+assign the responsibility to serve as the Agreement Steward to a
+suitable separate entity. Each new version of the Agreement will be
+given a distinguishing version number. The Program (including
+Contributions) may always be distributed subject to the version of the
+Agreement under which it was received. In addition, after a new version
+of the Agreement is published, Contributor may elect to distribute the
+Program (including its Contributions) under the new version. Except as
+expressly stated in Sections 2(a) and 2(b) above, Recipient receives no
+rights or licenses to the intellectual property of any Contributor under
+this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under this
+Agreement are reserved.</p>
+
+<p>This Agreement is governed by the laws of the State of New York and
+the intellectual property laws of the United States of America. No party
+to this Agreement will bring a legal action under this Agreement more
+than one year after the cause of action arose. Each party waives its
+rights to a jury trial in any resulting litigation.</p>
+
+</body>
+
+</html>
74 pom.xml
@@ -0,0 +1,74 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>cemerick</groupId>
+ <artifactId>network-repl</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <name>Network Clojure REPL</name>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.clojure</groupId>
+ <artifactId>clojure</artifactId>
+ <version>1.1.0</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <sourceDirectory>src/main/clojure</sourceDirectory>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <source>1.5</source>
+ <target>1.5</target>
+ <encoding>${project.build.sourceEncoding}</encoding>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>com.theoryinpractise</groupId>
+ <artifactId>clojure-maven-plugin</artifactId>
+ <version>1.3.3</version>
+ <configuration>
+ <warnOnReflection>true</warnOnReflection>
+ <!-- we want the AOT compile sanity check, but still only ship source -->
+ <outputDirectory>${project.build.directory}/clojure-classes</outputDirectory>
+ </configuration>
+ <executions>
+ <execution>
+ <id>compile-clojure</id>
+ <phase>compile</phase>
+ <goals>
+ <goal>compile</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>test-clojure</id>
+ <phase>test</phase>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+ <repositories>
+ <repository>
+ <id>clojure-releases</id>
+ <url>http://build.clojure.org/releases</url>
+ <releases>
+ <enabled>true</enabled>
+ </releases>
+ <snapshots>
+ <enabled>false</enabled>
+ </snapshots>
+ </repository>
+ </repositories>
+</project>
310 src/main/clojure/cemerick/nrepl.clj
@@ -0,0 +1,310 @@
+(ns cemerick.nrepl
+ (:require clojure.main)
+ (:import (java.net ServerSocket)
+ (java.io Reader InputStreamReader BufferedReader PushbackReader StringReader
+ Writer OutputStreamWriter BufferedWriter PrintWriter StringWriter
+ IOException)
+ (java.util.concurrent Callable Future ExecutorService Executors TimeUnit
+ ThreadFactory
+ CancellationException ExecutionException TimeoutException)))
+
+(def *print-stack-trace-on-error* false)
+
+(try
+ (try
+ (require '[clojure.pprint :as pprint]) ; clojure 1.2.0+
+ (catch Exception e
+ ; clojure 1.0.0+ w/ contrib
+ (require '[clojure.contrib.pprint :as pprint])))
+ ; clojure 1.1.0 requires this eval, throws exception not finding pprint ns
+ ; I think 1.1.0 was resolving vars in the reader instead of the compiler?
+ (eval '(defn- pretty-print? [] pprint/*print-pretty*))
+ (eval '(def pprint pprint/pprint))
+ (catch Exception e
+ ; no contrib available, fall back to prn
+ (def pprint prn)
+ (defn- pretty-print? [] false)))
+
+(def #^ExecutorService executor (Executors/newCachedThreadPool
+ (proxy [ThreadFactory] []
+ (newThread [r]
+ (doto (Thread. r)
+ (.setDaemon true))))))
+
+(def #^{:private true
+ :doc "A map whose values are the Futures associated with client-requested evaluations,
+ keyed by the evaluations' messages' IDs."}
+ repl-futures (atom {}))
+
+(defn get-all-msg-ids
+ []
+ (keys @repl-futures))
+
+(defn interrupt
+ [msg-id]
+ (when-let [#^Future f (@repl-futures msg-id)]
+ (.cancel f true)))
+
+(defn- submit
+ [#^Callable function]
+ (.submit executor function))
+
+(defn- get-root-cause [throwable]
+ (loop [#^Throwable cause throwable]
+ (if-let [cause (.getCause cause)]
+ (recur cause)
+ cause)))
+
+(defn- submit-looping
+ ([function]
+ (submit-looping function (fn [#^java.lang.Throwable cause]
+ (when-not (or (instance? IOException cause)
+ (instance? java.lang.InterruptedException cause)
+ (instance? java.nio.channels.ClosedByInterruptException cause))
+ ;(.printStackTrace cause)
+ (pr-str "submit-looping: exception occured: " cause)))))
+ ([function ex-fn]
+ (submit (fn []
+ (try
+ (function)
+ (recur)
+ (catch Exception ex
+ (ex-fn (get-root-cause ex))))))))
+
+
+
+;Message format:
+;<integer>
+;<EOL>
+;(<string: key>
+; <EOL>
+; (<string: value> | <number: value>)
+; <EOL>)+
+;The initial integer specifies how many k/v pairs are in the next message.
+;
+;Not simply printing and reading maps because the client
+;may not be clojure: e.g. whatever vimclojure might use to
+;write/parse messages, a python/ruby/whatever client, etc.
+(defn- write-message
+ "Writes the given message to the writer. Returns the :id of the message."
+ [#^Writer out msg]
+ (locking out
+ (binding [*out* out]
+ (prn (count msg))
+ (doseq [[k v] msg]
+ (if (string? k)
+ (prn k)
+ (prn (name k)))
+ (prn v))
+ (flush)))
+ (:id msg))
+
+(defn- read-message
+ "Returns the next message from the given PushbackReader."
+ [#^PushbackReader in]
+ (locking in
+ (binding [*in* in]
+ (let [msg-size (read)]
+ (->> (repeatedly read)
+ (take (* 2 msg-size))
+ (partition 2)
+ (map #(vector (-> % first keyword) (second %)))
+ (into {}))))))
+
+(defn- is-eof-ex?
+ [#^Throwable throwable]
+ (and (instance? clojure.lang.LispReader$ReaderException throwable)
+ (or
+ (.startsWith (.getMessage throwable) "java.lang.Exception: EOF while reading")
+ (.startsWith (.getMessage throwable) "java.io.IOException: Write end dead"))))
+
+(defn- capture-client-state
+ "Returns a map containing the 'baseline' client state of the current thread; everything
+ that with-bindings binds, except for the prior result values, *e, and *ns*."
+ []
+ {:warn-on-reflection *warn-on-reflection*, :math-context *math-context*,
+ :print-meta *print-meta*, :print-length *print-length*,
+ :print-level *print-level*, :compile-path *compile-path*
+ :command-line-args *command-line-args*})
+
+(defn load-with-debug-info
+ "Load a string using the source-path and file name for debug info."
+ [str-data source-path file]
+ (clojure.lang.Compiler/load
+ #^Reader (StringReader. str-data)
+ #^String source-path #^String file))
+
+(defmacro set!-many
+ [& body]
+ (let [pairs (partition 2 body)]
+ `(do ~@(for [[var value] pairs] (list 'set! var value)))))
+
+(defn- handle-request
+ [client-state-atom {:keys [code]}]
+ (let [{:keys [value-3 value-2 value-1 last-exception ns warn-on-reflection
+ math-context print-meta print-length print-level compile-path
+ command-line-args]} @client-state-atom
+ ; it seems like there's more value in combining *out* and *err*
+ ; (thereby preserving the interleaved nature of that output, as typically rendered)
+ ; than there is in separating them for the client
+ ; (which could never recombine them properly, unless we timestamp each line or something)
+ out (StringWriter.)
+ out-pw (PrintWriter. out)
+ return (atom nil)
+ repl-init (fn []
+ (in-ns (.name ns))
+ (set!-many
+ *3 value-3
+ *2 value-2
+ *1 value-1
+ *e last-exception
+ *warn-on-reflection* warn-on-reflection
+ *math-context* math-context
+ *print-meta* print-meta
+ *print-length* print-length
+ *print-level* print-level
+ *compile-path* compile-path
+ *command-line-args* command-line-args))]
+ (try
+ (binding [*in* (clojure.lang.LineNumberingPushbackReader. (StringReader. code))
+ *out* out-pw
+ *err* out-pw]
+ (clojure.main/repl
+ :init repl-init
+ :read (fn [prompt exit] (read))
+ :caught (fn [#^Throwable e]
+ (reset! client-state-atom (assoc (capture-client-state)
+ :value-3 *3
+ :value-2 *2
+ :value-1 *1
+ :last-exception *e
+ :ns *ns*))
+ (if (is-eof-ex? e)
+ (throw e)
+ (reset! return ["error" nil]))
+ (if *print-stack-trace-on-error*
+ (.printStackTrace e *out*)
+ (prn (clojure.main/repl-exception e)))
+ (flush))
+ :prompt (fn [])
+ :need-prompt (constantly false)
+ :print (fn [value]
+ (reset! return ["ok" value])
+ (if (pretty-print?)
+ (pprint value)
+ (prn value)))))
+ (catch clojure.lang.LispReader$ReaderException ex
+ ; almost surely hit EOF
+ (when-not (is-eof-ex? ex) (throw ex)))
+ (catch java.lang.InterruptedException ex)
+ (catch java.nio.channels.ClosedByInterruptException ex)
+ (finally (flush)))
+
+ {:out (str out)
+ :ns (-> @client-state-atom :ns .name str)
+ :status (first @return)
+ :value (pr-str (second @return))}))
+
+(def #^{:private true
+ :doc "Currently one minute; this can't just be Long/MAX_VALUE, or we'll inevitably
+ foul up the executor's threadpool with hopelessly-blocked threads.
+ This can be overridden on a per-request basis by the client."}
+ default-timeout (* 1000 60))
+
+(defn- handle-response
+ [#^Future future
+ {:keys [id timeout] :or {timeout default-timeout}}
+ write-message]
+ (try
+ (let [result (.get future timeout TimeUnit/MILLISECONDS)]
+ (write-message (assoc (select-keys result [:status :value :out :ns])
+ :id id)))
+ (catch CancellationException e
+ (write-message {:id id :status "cancelled"}))
+ (catch TimeoutException e
+ (write-message {:id id :status "timeout"}))
+ (catch ExecutionException e
+ ; this should never happen
+ (.printStackTrace e))
+ (catch InterruptedException e
+ ; this should never happen
+ (.printStackTrace e))))
+
+(defn- message-dispatch
+ [client-state read-message write-message]
+ (let [{:keys [id code] :as msg} (read-message)]
+ (if-not code
+ (write-message {:status "error"
+ :error "Received message with no code."})
+ (let [future (submit #(#'handle-request client-state msg))]
+ (swap! repl-futures assoc id future)
+ (submit #(try
+ (handle-response future msg write-message)
+ (finally
+ (swap! repl-futures dissoc id))))))))
+
+(defn- configure-streams
+ [#^java.net.Socket sock]
+ [(-> sock .getInputStream (InputStreamReader. "UTF-8") BufferedReader. PushbackReader.)
+ (-> sock .getOutputStream (OutputStreamWriter. "UTF-8") BufferedWriter.)])
+
+(defn- accept-connection
+ [#^ServerSocket ss]
+ (let [sock (.accept ss)
+ [in out] (configure-streams sock)
+ client-state (atom (assoc (capture-client-state)
+ :ns (create-ns 'user)))]
+ (submit-looping (partial message-dispatch
+ client-state
+ (partial read-message in)
+ (partial write-message out)))))
+
+(defn start-server
+ ([] (start-server 0))
+ ([port]
+ (let [ss (ServerSocket. port)]
+ [ss (submit-looping (partial accept-connection ss))])))
+
+(defn- client-send
+ "Sends a new message via the write fn containing
+ at minimum the provided code string and an id (optionally included as part
+ of the kwargs), along with any other options specified in the kwards.
+ Returns the id of the sent message as returned by the write-message fn."
+ [write code & options]
+ (let [{:keys [id] :as options} (apply hash-map options)]
+ (write (assoc options
+ :id (or id (str (java.util.UUID/randomUUID)))
+ :code (str code "\n")))))
+
+(defn send-and-wait
+ [{:keys [send receive]} code & options]
+ (let [id (apply send code options)]
+ (loop []
+ (let [msg (receive)]
+ (if (= id (:id msg))
+ msg
+ (recur))))))
+
+(defn read-response-value
+ [response-message]
+ (update-in response-message [:value] #(when % (read-string %))))
+
+(defn connect
+ "Connects to a hosted REPL at the given host and port, returning
+ a map containing three functions:
+
+ - send: see client-send (which is already has its write fn param applied)
+ - receive: see read-message (which also already has its read fn param applied)
+ - close: no-arg function that closes the underlying socket"
+ [host port]
+ (let [sock (java.net.Socket. host port)
+ [in out] (configure-streams sock)]
+ {:in in
+ :out out
+ :send (partial client-send (partial write-message out))
+ :receive (partial read-message in)
+ :close #(.close sock)}))
+
+;; TODO
+;; - ack
+;; - HELO, init handshake, version compat check, etc
26 src/main/clojure/cemerick/nrepl/main.clj
@@ -0,0 +1,26 @@
+(ns cemerick.nrepl.main
+ (:gen-class)
+ (:require [cemerick.nrepl :as repl]))
+
+(defn- run-repl
+ [port]
+ (let [connection (repl/connect "localhost" port)
+ {:keys [major minor incremental qualifier]} *clojure-version*]
+ (println "network-repl")
+ (print (str "Clojure " major \. minor \. incremental))
+ (if (seq qualifier)
+ (println (str \- qualifier))
+ (println))
+ (loop [ns "user"]
+ (print (str ns "=> "))
+ (flush)
+ ((:send connection) (pr-str (read)))
+ (let [{:keys [out ns value]} ((:receive connection))]
+ (when (seq out) (println out))
+ (recur ns)))))
+
+(defn -main
+ [& args]
+ (let [[ssocket _] (repl/start-server)]
+ (when ((into #{} args) "--repl")
+ (run-repl (.getLocalPort ssocket)))))
98 src/test/clojure/cemerick/nrepl_test.clj
@@ -0,0 +1,98 @@
+(ns cemerick.nrepl-test
+ (:use clojure.test)
+ (:require [cemerick.nrepl :as repl]
+ [clojure.set :as set]))
+
+(def *server-port* nil)
+
+(defn- repl-server-fixture
+ [f]
+ (let [[server-socket accept-future] (repl/start-server)]
+ (try
+ (binding [*server-port* (.getLocalPort server-socket)]
+ (f))
+ (finally (.close server-socket)))))
+
+(use-fixtures :once repl-server-fixture)
+
+(def send-wait-read (comp repl/read-response-value repl/send-and-wait))
+
+(defmacro def-repl-test
+ [name & body]
+ `(deftest ~name
+ (let [connection# (repl/connect "localhost" *server-port*)
+ ~'connection connection#
+ ~'repl (partial send-wait-read connection#)
+ ~'repl-value (partial (comp :value send-wait-read) connection#)]
+ ~@body)))
+
+(def-repl-test eval-literals
+ (are [literal] (= literal (-> literal pr-str repl-value))
+ 5
+ 0xff
+ 5.1
+ -2e12
+ ;1/4
+ :keyword
+ ::local-ns-keyword
+ :other.ns/keyword
+ "string"
+ "string\nwith\r\nlinebreaks"
+ [1 2 3]
+ {1 2 3 4}
+ #{1 2 3 4}))
+
+(def-repl-test simple-expressions
+ (are [expression] (= (-> expression read-string eval) (repl-value expression))
+ "'(1 2 3)"
+ "'symbol"
+ "(range 40)"
+ "(apply + (range 100))"))
+
+(def-repl-test defining-fns
+ (repl-value "(defn foobar [] 6)")
+ (is (= 6 (repl-value "(foobar)"))))
+
+(def-repl-test repl-value-history
+ (doall (map repl-value ["(apply + (range 6))" "(str 12 \\c)" "(keyword \"hello\")"]))
+ (let [history [15 "12c" :hello]]
+ (is (= history (repl-value "[*3 *2 *1]")))
+ (is (= history (repl-value "*1")))))
+
+(def-repl-test exceptions
+ (let [{:keys [out status value]} (repl "(throw (Exception. \"bad, bad code\"))")]
+ (is (= "error" status))
+ (is (nil? value))
+ (is (.contains out "bad, bad code"))
+ (is (= true (repl-value "(.contains (str *e) \"bad, bad code\")")))))
+
+(def-repl-test multiple-expressions-return
+ (is (= 18 (repl-value "5 (/ 5 0) (+ 5 6 7)"))))
+
+(def-repl-test return-on-incomplete-expr
+ (let [{:keys [out status value]} (repl "(apply + (range 20)")]
+ (is (nil? value))
+ ; this behaviour sucks; there's currently no way for the network repl impl
+ ; to know if an EOF exception is due to the end of stream being reached
+ ; or due to a malformed chunk of code; this should return status of "error" IMO
+ (is (= nil status))))
+
+(def-repl-test switch-ns
+ (is (= "otherns" (:ns (repl "(ns otherns) (defn function [] 12)"))))
+ (is (= 12 (repl-value "(otherns/function)"))))
+
+(def-repl-test timeout
+ ;(is (= "timeout" (:status (repl "(Thread/sleep 60000)" :timeout 1000))))
+ )
+
+(def-repl-test interrupt
+ (let [req-id ((:send connection) "(Thread/sleep 60000)")
+ _ (Thread/sleep 1000)
+ cancel-id ((:send connection) (str "(cemerick.nrepl/interrupt \"" req-id "\")"))]
+ (loop [ids #{req-id cancel-id}]
+ (if-not (empty? ids)
+ (let [{:keys [status id]} ((:receive connection))]
+ (is (= status (if (= id req-id)
+ "cancelled"
+ "ok")))
+ (recur (set/difference ids #{id})))))))
Please sign in to comment.
Something went wrong with that request. Please try again.