[#41] socket REPL
This commit is contained in:
parent
6d1a8c0791
commit
2dbb749e35
10 changed files with 375 additions and 18 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,3 +12,4 @@ pom.xml.asc
|
|||
.hg/
|
||||
/bb
|
||||
.clj-kondo/.cache
|
||||
!java/src/babashka/impl/LockFix.class
|
||||
|
|
|
|||
29
README.md
29
README.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
100
src/babashka/impl/clojure/core/server.clj
Normal file
100
src/babashka/impl/clojure/core/server.clj
Normal 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))
|
||||
143
src/babashka/impl/clojure/main.clj
Normal file
143
src/babashka/impl/clojure/main.clj
Normal 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))))))
|
||||
66
src/babashka/impl/socket_repl.clj
Normal file
66
src/babashka/impl/socket_repl.clj
Normal 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!)
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
20
test/babashka/impl/socket_repl_test.clj
Normal file
20
test/babashka/impl/socket_repl_test.clj
Normal 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
|
||||
)
|
||||
Loading…
Reference in a new issue