Thursday, May 21, 2009

Embedding a Clojure REPL in your production application

You can create an interactive production application by embedding a Clojure REPL in your application. This can help you inspect and interrogate the running application for runtime status, allow you to make bug fixes and monitor running code.

When working on the open source project enclojure-clojure-lib, we created a org.enclojure.repl library which have all the necessary server and client functions for creating a socket based clojure.main/repl in your application.

You can add the following code in your application which starts a socket REPL at the speicified port and wait for a connection.

;Use specified socket
(def server-socket (org.enclojure.repl.main/run-repl-server 11345))

;Use next available socket port
(def server-socket (org.enclojure.repl.main/run-repl-server 0))
(def server-port (.getLocalPort server-socket))

You can have your monitoring application - connect to the remote REPL as follows:
(def+ {:keys [repl-fn result-fn close-fn]} (create-repl-client host port))

create-repl-client returns a map:
{:repl-fn repl-fn
:result-fn result-fn
:close-fn close-fn}

You can listen to the incoming socket data using the result-fn in another thread:
(.start (Thread. #(write-to-window (result-fn))))

You can send the command to the remote REPL as follows:
(repl-fn "(give-me-status)")

To close this client connection you call
(close-fn)

You can use the following command to shutdown the remote REPL server
(repl-fn "(org.enclojure.repl.main/close-server)")

If you use Enclojure Plugin in Netbeans - You can create a REPL window connected to your REPL enabled application. Check out the Remote unmanaged REPLs.

Embedding a Clojure REPL in your application for Local use (Non-socket)

Sometimes you also want to create a REPL for use with in your application. For example: Enclojure has a Netbeans IDE REPL which is running in Netbeans process and hence can manipulate the Netbeans IDE via the REPL. This helped us with the Enclojure plugin development. You can create a REPL for local use in Application as follows:

(def+ {:keys [repl-fn result-fn]} (create-clojure-repl))

You can use the repl-fn and result-fn as explained before. Following is the code snippet copied from the org.enclojure.repl.main. create-clojure-repl is wrapping the clojure.main/repl to start the REPL in a thread. This is the function used in both local and remote Socket based REPL.

(def *printStackTrace-on-error* false)

(defn is-eof-ex? [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 create-clojure-repl []
"This function creates an instance of clojure repl using piped in and out.
It returns a map of two functions repl-fn and result-fn - first function
can be called with a valid clojure expression and the results are read using
the result-fn."
(let [cmd-wtr (PipedWriter.)
result-rdr (PipedReader.)
piped-in (clojure.lang.LineNumberingPushbackReader. (PipedReader. cmd-wtr))
piped-out (PrintWriter. (PipedWriter. result-rdr))
repl-thread-fn #(binding [*printStackTrace-on-error* *printStackTrace-on-error*
*in* piped-in
*out* piped-out
*err* *out*]
(try
(clojure.main/repl
:init (fn [] (in-ns 'user))
:read (fn [prompt exit]
(read))
:caught (fn [e]
(when (is-eof-ex? e)
(throw e))
(if *printStackTrace-on-error*
(.printStackTrace e *out*)
(prn (clojure.main/repl-exception e)))
(flush))
:need-prompt (constantly true))
(catch clojure.lang.LispReader$ReaderException ex
(prn "REPL closing"))
(catch java.lang.InterruptedException ex)
(catch java.nio.channels.ClosedByInterruptException ex)))]
(.start (Thread. repl-thread-fn))
{:repl-fn (fn [cmd]
(if (= cmd ":CLOSE-REPL")
(do
(.close cmd-wtr)
(.close result-rdr))
(do
(.write cmd-wtr cmd)
(.flush cmd-wtr))))
;//??Using CharArrayWriter to build the string from each read of one byte
;Once there is nothing to read than this function returns the string read.
;Using partial so that CharArrayWriter is only created and once and reused.
;There could be better way.
:result-fn (partial
(fn [wtr]
(.write wtr (.read result-rdr))
(if (.ready result-rdr)
(recur wtr)
(let [result (.toString wtr)]
(.reset wtr)
result)))
(CharArrayWriter.))}))

2 comments: