[#41] socket REPL

This commit is contained in:
Michiel Borkent 2019-08-31 20:17:36 +02:00 committed by GitHub
parent 6d1a8c0791
commit 2dbb749e35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 375 additions and 18 deletions

1
.gitignore vendored
View file

@ -12,3 +12,4 @@ pom.xml.asc
.hg/
/bb
.clj-kondo/.cache
!java/src/babashka/impl/LockFix.class

View file

@ -249,6 +249,32 @@ export BABASHKA_PRELOADS='(load-file "my_awesome_prelude.clj")'
Note that `*in*` is not available in preloads.
## Socket REPL
Start the socket REPL like this:
``` shellsession
$ bb --socket-repl 1666
Babashka socket REPL started at localhost:1666
```
Now you can connect with your favorite socket REPL client:
``` shellsession
$ rlwrap nc 127.0.0.1 1666
Babashka v0.0.14-SNAPSHOT REPL.
Use :repl/quit or :repl/exit to quit the REPL.
Clojure rocks, Bash reaches.
bb=> (+ 1 2 3)
6
bb=> :repl/quit
$
```
A socket REPL client for Emacs is
[inf-clojure](https://github.com/clojure-emacs/inf-clojure).
## Enabling SSL
If you want to be able to use SSL to e.g. run `(slurp
@ -360,4 +386,5 @@ beverage](https://ko-fi.com/borkdude).
Copyright © 2019 Michiel Borkent
Distributed under the EPL License, same as Clojure. See LICENSE.
Distributed under the EPL License. This project contains modified Clojure code
which is licensed under the same EPL License. See LICENSE.

View file

@ -9,10 +9,8 @@
:url "http://opensource.org/licenses/eclipse-1.0.php"}
:source-paths ["src" "sci/src" "sci/inlined"]
:resource-paths ["resources" "sci/resources"]
:dependencies [[org.clojure/clojure "1.9.0"]]
:profiles {:clojure-1.9.0 {:dependencies [[org.clojure/clojure "1.9.0"]]}
:clojure-1.10.1 {:dependencies [[org.clojure/clojure "1.10.1"]]}
:test {:dependencies [[clj-commons/conch "0.9.2"]]}
:dependencies [[org.clojure/clojure "1.10.1"]]
:profiles {:test {:dependencies [[clj-commons/conch "0.9.2"]]}
:uberjar {:global-vars {*assert* false}
:jvm-opts ["-Dclojure.compiler.direct-linking=true"
"-Dclojure.spec.skip-macros=true"]

View file

@ -23,7 +23,7 @@ BABASHKA_VERSION=$(cat resources/BABASHKA_VERSION)
# mkdir -p src/sci
# cp -R /tmp/sci/src/* src
lein with-profiles +clojure-1.10.1 do clean, uberjar
lein do clean, uberjar
$NATIVE_IMAGE \
-jar target/babashka-$BABASHKA_VERSION-standalone.jar \
-H:Name=bb \

View file

@ -3,12 +3,4 @@
set -eo pipefail
export BABASHKA_PRELOADS='(defn __bb__foo [] "foo") (defn __bb__bar [] "bar")'
if [ "$BABASHKA_TEST_ENV" = "native" ]; then
lein test
else
echo "Testing with Clojure 1.9.0"
lein with-profiles +clojure-1.9.0 test
echo "Testing with Clojure 1.10.1"
lein with-profiles +clojure-1.10.1 test
fi

View file

@ -0,0 +1,100 @@
;; Modified / stripped version of clojure.core.server for use with babashka on
;; GraalVM.
;;
;; Copyright (c) Rich Hickey. All rights reserved.
;; The use and distribution terms for this software are covered by the
;; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;; which can be found in the file epl-v10.html at the root of this distribution.
;; By using this software in any fashion, you are agreeing to be bound by
;; the terms of this license.
;; You must not remove this notice, or any other, from this software.
(ns ^{:doc "Socket server support"
:author "Alex Miller"
:no-doc true}
babashka.impl.clojure.core.server
(:refer-clojure :exclude [locking])
(:import
[clojure.lang LineNumberingPushbackReader]
[java.net InetAddress Socket ServerSocket SocketException]
[java.io Reader Writer PrintWriter BufferedWriter BufferedReader InputStreamReader OutputStreamWriter]))
(set! *warn-on-reflection* true)
(def server (atom nil))
(defmacro ^:private thread
[^String name daemon & body]
`(doto (Thread. (fn [] ~@body) ~name)
(.setDaemon ~daemon)
(.start)))
(defn- accept-connection
"Start accept function, to be invoked on a client thread, given:
conn - client socket
name - server name
client-id - client identifier
in - in stream
out - out stream
err - err stream
accept - accept fn symbol to invoke
args - to pass to accept-fn"
[^Socket conn client-id in out err accept args]
(try
(binding [*in* in
*out* out
*err* err]
(swap! server assoc-in [:sessions client-id] {})
(apply accept args))
(catch SocketException _disconnect)
(finally
(swap! server update-in [:sessions] dissoc client-id)
(.close conn))))
(defn start-server
"Start a socket server given the specified opts:
:address Host or address, string, defaults to loopback address
:port Port, integer, required
:name Name, required
:accept Namespaced symbol of the accept function to invoke, required
:args Vector of args to pass to accept function
:bind-err Bind *err* to socket out stream?, defaults to true
:server-daemon Is server thread a daemon?, defaults to true
:client-daemon Are client threads daemons?, defaults to true
Returns server socket."
[opts]
(let [{:keys [address port name accept args bind-err server-daemon client-daemon]
:or {bind-err true
server-daemon true
client-daemon true}} opts
address (InetAddress/getByName address) ;; nil returns loopback
socket (ServerSocket. port 0 address)]
(reset! server {:name name, :socket socket, :sessions {}})
(thread
(str "Clojure Server " name) server-daemon
(try
(loop [client-counter 1]
(when (not (.isClosed socket))
(try
(let [conn (.accept socket)
in (LineNumberingPushbackReader. (InputStreamReader. (.getInputStream conn)))
out (BufferedWriter. (OutputStreamWriter. (.getOutputStream conn)))
client-id (str client-counter)]
(thread
(str "Clojure Connection " name " " client-id) client-daemon
(accept-connection conn client-id in out (if bind-err out *err*) accept args)))
(catch SocketException _disconnect))
(recur (inc client-counter))))
(finally
(reset! server nil))))
socket))
(defn stop-server
"Stop server with name or use the server-name from *session* if none supplied.
Returns true if server stopped successfully, nil if not found, or throws if
there is an error closing the socket."
[]
(when-let [s @server]
(when-let [server-socket ^ServerSocket (get s :socket)]
(.close server-socket)))
(reset! server nil))

View file

@ -0,0 +1,143 @@
;; Modified / stripped version of clojure.main for use with babashka on
;; GraalVM.
;;
;; Copyright (c) Rich Hickey All rights reserved. The use and
;; distribution terms for this software are covered by the Eclipse Public
;; License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) which can be found
;; in the file epl-v10.html at the root of this distribution. By using this
;; software in any fashion, you are agreeing to be bound by the terms of
;; this license. You must not remove this notice, or any other, from this
;; software.
;; Originally contributed by Stephen C. Gilardi
(ns ^{:doc "Top-level main function for Clojure REPL and scripts."
:author "Stephen C. Gilardi and Rich Hickey"
:no-doc true}
babashka.impl.clojure.main
(:refer-clojure :exclude [with-bindings])
(:import (java.io StringReader)
(clojure.lang LineNumberingPushbackReader LispReader$ReaderException)))
(defmacro with-bindings
"Executes body in the context of thread-local bindings for several vars
that often need to be set!: *ns* *warn-on-reflection* *math-context*
*print-meta* *print-length* *print-level* *compile-path*
*command-line-args* *1 *2 *3 *e"
[& body]
`(binding [*ns* *ns*
*warn-on-reflection* *warn-on-reflection*
*math-context* *math-context*
*print-meta* *print-meta*
*print-length* *print-length*
*print-level* *print-level*
*print-namespace-maps* true
*data-readers* *data-readers*
*default-data-reader-fn* *default-data-reader-fn*
*command-line-args* *command-line-args*
*unchecked-math* *unchecked-math*
*assert* *assert*
;; clojure.spec.alpha/*explain-out* clojure.spec.alpha/*explain-out*
*1 nil
*2 nil
*3 nil
*e nil]
~@body))
(defn repl-prompt
"Default :prompt hook for repl"
[]
(print "bb=> " ))
(defn repl-caught
"Default :caught hook for repl"
[e]
(binding [*out* *err*]
(println (.getMessage ^Exception e))
(flush)))
(defn repl
"Generic, reusable, read-eval-print loop. By default, reads from *in*,
writes to *out*, and prints exception summaries to *err*. If you use the
default :read hook, *in* must either be an instance of
LineNumberingPushbackReader or duplicate its behavior of both supporting
.unread and collapsing CR, LF, and CRLF into a single \\newline. Options
are sequential keyword-value pairs. Available options and their defaults:
- :init, function of no arguments, initialization hook called with
bindings for set!-able vars in place.
default: #()
- :need-prompt, function of no arguments, called before each
read-eval-print except the first, the user will be prompted if it
returns true.
default: (if (instance? LineNumberingPushbackReader *in*)
#(.atLineStart *in*)
#(identity true))
- :prompt, function of no arguments, prompts for more input.
default: repl-prompt
- :flush, function of no arguments, flushes output
default: flush
- :read, function of two arguments, reads from *in*:
- returns its first argument to request a fresh prompt
- depending on need-prompt, this may cause the repl to prompt
before reading again
- returns its second argument to request an exit from the repl
- else returns the next object read from the input stream
default: repl-read
- :eval, function of one argument, returns the evaluation of its
argument
default: eval
- :print, function of one argument, prints its argument to the output
default: prn
- :caught, function of one argument, a throwable, called when
read, eval, or print throws an exception or error
default: repl-caught"
[& options]
(let [{:keys [init need-prompt prompt flush read eval print caught]
:or {need-prompt (if (instance? LineNumberingPushbackReader *in*)
#(.atLineStart ^LineNumberingPushbackReader *in*)
#(identity true))
prompt repl-prompt
flush flush
print prn
caught repl-caught}}
(apply hash-map options)
request-prompt (Object.)
request-exit (Object.)
read-eval-print
(fn []
(try
(let [input (try
(read request-prompt request-exit)
(catch LispReader$ReaderException e
(throw (ex-info nil {:clojure.error/phase :read-source} e))))]
(or (#{request-prompt request-exit} input)
(let [value (eval input)]
(set! *3 *2)
(set! *2 *1)
(set! *1 value)
(try
(print value)
(catch Throwable e
(throw (ex-info nil {:clojure.error/phase :print-eval-result} e)))))))
(catch Throwable e
(caught e)
(set! *e e))))]
(with-bindings
(try
(init)
(catch Throwable e
(caught e)
(set! *e e)))
(prompt)
(flush)
(loop []
(when-not
(try (identical? (read-eval-print) request-exit)
(catch Throwable e
(caught e)
(set! *e e)
nil))
(when (need-prompt)
(prompt)
(flush))
(recur))))))

View file

@ -0,0 +1,66 @@
(ns babashka.impl.socket-repl
{:no-doc true}
(:require [babashka.impl.clojure.core.server :as server]
[babashka.impl.clojure.main :as m]
[sci.core :refer [eval-string]]
[sci.impl.parser :as parser]
[sci.impl.toolsreader.v1v3v2.clojure.tools.reader.reader-types :as r]
[clojure.string :as str]
[clojure.java.io :as io]))
(set! *warn-on-reflection* true)
(defn repl
"REPL with predefined hooks for attachable socket server."
[sci-opts]
(m/repl
:init #(do (println "Babashka"
(str "v" (str/trim (slurp (io/resource "BABASHKA_VERSION"))))
"REPL.")
(println "Use :repl/quit or :repl/exit to quit the REPL.")
(println "Clojure rocks, Bash reaches.")
(println))
:read (fn [request-prompt request-exit]
(let [in (r/indexing-push-back-reader (r/push-back-reader *in*))
p (r/peek-char in)]
(if (= \newline p)
(do (r/read-char in) request-prompt)
(let [v (parser/parse-next {} in)]
(if (or (identical? :repl/quit v)
(identical? :repl/exit v))
request-exit
v)))))
:eval (fn [expr]
(eval-string (str expr)
(update sci-opts
:bindings
merge {'*1 *1
'*2 *2
'*3 *3
'*e *e})))))
(defn start-repl! [host+port sci-opts]
(let [parts (str/split host+port #":")
[host port] (if (= 1 (count parts))
[nil (Integer. ^String (first parts))]
[(first parts) (Integer. ^String (second parts))])
host+port (if-not host (str "localhost:" port)
host+port)
socket (server/start-server
{:address host
:port port
:name "bb"
:accept babashka.impl.socket-repl/repl
:args [sci-opts]})]
(println "Babashka socket REPL started at" host+port)
socket))
(defn stop-repl! []
(server/stop-server))
(comment
(def sock (start-repl! "0.0.0.0:1666" {:env (atom {})}))
(.accept sock)
@#'server/servers
(stop-repl!)
)

View file

@ -7,6 +7,7 @@
[clojure.java.io :as io]
[clojure.java.shell :as shell]
[clojure.string :as str]
[babashka.impl.socket-repl :as socket-repl]
[sci.core :as sci])
(:import [sun.misc Signal]
[sun.misc SignalHandler])
@ -55,6 +56,11 @@
(recur (rest options)
(assoc opts-map
:file (first options))))
("--socket-repl")
(let [options (rest options)]
(recur (rest options)
(assoc opts-map
:socket-repl (first options))))
(if (not (:file opts-map))
(assoc opts-map
:expression opt
@ -80,7 +86,7 @@
(defn print-version []
(println (str "babashka v"(str/trim (slurp (io/resource "BABASHKA_VERSION"))))))
(def usage-string "Usage: bb [ --help ] | [ --version ] | [ -i | -I ] [ -o | -O ] [ --stream ] ( expression | -f <file> )")
(def usage-string "Usage: bb [ --help ] | [ --version ] | [ -i | -I ] [ -o | -O ] [ --stream ] ( expression | -f <file> | --socket-repl [host:]port )")
(defn print-usage []
(println usage-string))
@ -101,6 +107,7 @@
-O: write EDN values to stdout.
--stream: stream over lines or EDN values from stdin. Combined with -i or -I *in* becomes a single value per iteration.
--file or -f: read expressions from file instead of argument wrapped in an implicit do.
--socket-repl: start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
--time: print execution time before exiting.
"))
@ -178,7 +185,7 @@
(let [t0 (System/currentTimeMillis)
{:keys [:version :shell-in :edn-in :shell-out :edn-out
:help? :file :command-line-args
:expression :stream? :time?] :as _opts}
:expression :stream? :time? :socket-repl] :as _opts}
(parse-opts args)
read-next #(if (pipe-signal-received?)
::EOF
@ -205,12 +212,15 @@
[(print-version) 0]
help?
[(print-help) 0]
socket-repl [(do (socket-repl/start-repl! socket-repl ctx)
@(promise)) 0]
:else
(try
(let [expr (if file (read-file file) expression)]
(loop [in (read-next)]
(let [ctx (update ctx :bindings assoc (with-meta '*in*
(when-not stream? {:sci/deref! true})) in)]
(when-not stream?
{:sci/deref! true})) in)]
(if (identical? ::EOF in)
[nil 0] ;; done streaming
(let [res [(do (when-not (or expression file)

View file

@ -0,0 +1,20 @@
(ns babashka.impl.socket-repl-test
(:require
[babashka.impl.socket-repl :refer [start-repl! stop-repl!]]
[babashka.test-utils :as tu]
[clojure.java.shell :refer [sh]]
[clojure.string :as str]
[clojure.test :as t :refer [deftest is]]))
(deftest socket-repl-test
(when tu/jvm?
(start-repl! "0.0.0.0:1666" {:env (atom {})})
(is (str/includes? (:out (sh "bash" "-c"
"echo \"(+ 1 2 3)\n:repl/exit\" | nc 127.0.0.1 1666"))
"bb=> 6"))
(stop-repl!)))
;;;; Scratch
(comment
)