parent
1c0e004f2f
commit
8b90e40de4
14 changed files with 842 additions and 18 deletions
|
|
@ -172,7 +172,7 @@ Check out the image on [Docker hub](https://hub.docker.com/r/borkdude/babashka/)
|
||||||
Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose]
|
Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose]
|
||||||
[ ( --classpath | -cp ) <cp> ] [ --uberscript <file> ]
|
[ ( --classpath | -cp ) <cp> ] [ --uberscript <file> ]
|
||||||
[ ( --main | -m ) <main-namespace> | -e <expression> | -f <file> |
|
[ ( --main | -m ) <main-namespace> | -e <expression> | -f <file> |
|
||||||
--repl | --socket-repl [<host>:]<port> ]
|
--repl | --socket-repl [<host>:]<port> | --nrepl-server [<host>:]<port> ]
|
||||||
[ arg* ]
|
[ arg* ]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
@ -194,6 +194,7 @@ Options:
|
||||||
-m, --main <ns> Call the -main function from namespace with args.
|
-m, --main <ns> Call the -main function from namespace with args.
|
||||||
--repl Start REPL. Use rlwrap for history.
|
--repl Start REPL. Use rlwrap for history.
|
||||||
--socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
|
--socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
|
||||||
|
--nrepl-server Start nREPL server. Specify port (e.g. 1667) or host and port separated by colon (e.g. 127.0.0.1:1667).
|
||||||
--time Print execution time before exiting.
|
--time Print execution time before exiting.
|
||||||
-- Stop parsing args and pass everything after -- to *command-line-args*
|
-- Stop parsing args and pass everything after -- to *command-line-args*
|
||||||
|
|
||||||
|
|
|
||||||
4
deps.edn
4
deps.edn
|
|
@ -9,7 +9,9 @@
|
||||||
org.clojure/data.csv {:mvn/version "1.0.0"},
|
org.clojure/data.csv {:mvn/version "1.0.0"},
|
||||||
cheshire {:mvn/version "5.10.0"}
|
cheshire {:mvn/version "5.10.0"}
|
||||||
fipp {:mvn/version "0.6.22"}
|
fipp {:mvn/version "0.6.22"}
|
||||||
com.cognitect/transit-clj {:mvn/version "1.0.324"}}
|
com.cognitect/transit-clj {:mvn/version "1.0.324"}
|
||||||
|
;; nrepl/bencode {:mvn/version "1.0.0"}
|
||||||
|
}
|
||||||
:aliases {:main
|
:aliases {:main
|
||||||
{:main-opts ["-m" "babashka.main"]}
|
{:main-opts ["-m" "babashka.main"]}
|
||||||
:profile
|
:profile
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,8 @@
|
||||||
[org.clojure/data.csv "1.0.0"]
|
[org.clojure/data.csv "1.0.0"]
|
||||||
[cheshire "5.10.0"]
|
[cheshire "5.10.0"]
|
||||||
[fipp "0.6.22"]
|
[fipp "0.6.22"]
|
||||||
[com.cognitect/transit-clj "1.0.324"]]
|
[com.cognitect/transit-clj "1.0.324"]
|
||||||
|
#_[nrepl/bencode "1.0.0"]]
|
||||||
:profiles {:test {:dependencies [[clj-commons/conch "0.9.2"]
|
:profiles {:test {:dependencies [[clj-commons/conch "0.9.2"]
|
||||||
[com.clojure-goes-fast/clj-async-profiler "0.4.0"]]}
|
[com.clojure-goes-fast/clj-async-profiler "0.4.0"]]}
|
||||||
:uberjar {:global-vars {*assert* false}
|
:uberjar {:global-vars {*assert* false}
|
||||||
|
|
|
||||||
11
src/babashka/impl/bencode.clj
Normal file
11
src/babashka/impl/bencode.clj
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
(ns babashka.impl.bencode
|
||||||
|
{:no-doc true}
|
||||||
|
(:require [babashka.impl.bencode.core :as bencode]
|
||||||
|
[sci.impl.namespaces :refer [copy-var]]
|
||||||
|
[sci.impl.vars :as vars]))
|
||||||
|
|
||||||
|
(def tns (vars/->SciNamespace 'bencode.core nil))
|
||||||
|
|
||||||
|
(def bencode-namespace
|
||||||
|
{'read-bencode (copy-var bencode/read-bencode tns)
|
||||||
|
'write-bencode (copy-var bencode/write-bencode tns)})
|
||||||
420
src/babashka/impl/bencode/core.clj
Normal file
420
src/babashka/impl/bencode/core.clj
Normal file
|
|
@ -0,0 +1,420 @@
|
||||||
|
(ns babashka.impl.bencode.core
|
||||||
|
"A netstring and bencode implementation for Clojure."
|
||||||
|
{:author "Meikel Brandmeyer"
|
||||||
|
:no-doc true}
|
||||||
|
(:require [clojure.java.io :as io])
|
||||||
|
(:import [java.io ByteArrayOutputStream
|
||||||
|
EOFException
|
||||||
|
InputStream
|
||||||
|
IOException
|
||||||
|
OutputStream
|
||||||
|
PushbackInputStream]))
|
||||||
|
|
||||||
|
;; Copyright (c) Meikel Brandmeyer. 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.
|
||||||
|
|
||||||
|
;; # Motivation
|
||||||
|
;;
|
||||||
|
;; In each and every application, which contacts peer processes via some
|
||||||
|
;; communication channel, the handling of the communication channel is
|
||||||
|
;; obviously a central part of the application. Unfortunately introduces
|
||||||
|
;; handling of buffers of varying sizes often bugs in form of buffer
|
||||||
|
;; overflows and similar.
|
||||||
|
;;
|
||||||
|
;; A strong factor in this situation is of course the protocol which goes
|
||||||
|
;; over the wire. Depending on its design it might be difficult to estimate
|
||||||
|
;; the size of the input up front. This introduces more handling of message
|
||||||
|
;; buffers to accomodate for inputs of varying sizes. This is particularly
|
||||||
|
;; difficult in languages like C, where there is no bounds checking of array
|
||||||
|
;; accesses and where errors might go unnoticed for considerable amount of
|
||||||
|
;; time.
|
||||||
|
;;
|
||||||
|
;; To address these issues D. Bernstein developed the so called
|
||||||
|
;; [netstrings][net]. They are especially designed to allow easy construction
|
||||||
|
;; of the message buffers, easy and robust parsing.
|
||||||
|
;;
|
||||||
|
;; BitTorrent extended this to the [bencode][bc] protocol which also
|
||||||
|
;; includes ways to encode numbers and collections like lists or maps.
|
||||||
|
;;
|
||||||
|
;; *wire* is based on these ideas.
|
||||||
|
;;
|
||||||
|
;; [net]: http://cr.yp.to/proto/netstrings.txt
|
||||||
|
;; [bc]: http://wiki.theory.org/BitTorrentSpecification#Bencoding
|
||||||
|
;;
|
||||||
|
;; # Netstrings
|
||||||
|
;;
|
||||||
|
;; Now let's start with the basic netstrings. They consist of a byte count,
|
||||||
|
;; followed a colon and the binary data and a trailing comma. Examples:
|
||||||
|
;;
|
||||||
|
;; 13:Hello, World!,
|
||||||
|
;; 10:Guten Tag!,
|
||||||
|
;; 0:,
|
||||||
|
;;
|
||||||
|
;; The initial byte count allows to efficiently allocate a sufficiently
|
||||||
|
;; sized message buffer. The trailing comma serves as a hint to detect
|
||||||
|
;; incorrect netstrings.
|
||||||
|
;;
|
||||||
|
;; ## Low-level reading
|
||||||
|
;;
|
||||||
|
;; We will need some low-level reading helpers to read the bytes from
|
||||||
|
;; the input stream. These are `read-byte` as well as `read-bytes`. They
|
||||||
|
;; are split out, because doing such a simple task as reading a byte is
|
||||||
|
;; mild catastrophe in Java. So it would add some clutter to the algorithm
|
||||||
|
;; `read-netstring`.
|
||||||
|
;;
|
||||||
|
;; On the other hand they might be also useful elsewhere.
|
||||||
|
;;
|
||||||
|
;; To remove some magic numbers from the code below.
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(def #^{:const true} i 105)
|
||||||
|
(def #^{:const true} l 108)
|
||||||
|
(def #^{:const true} d 100)
|
||||||
|
(def #^{:const true} comma 44)
|
||||||
|
(def #^{:const true} minus 45)
|
||||||
|
|
||||||
|
;; These two are only used boxed. So we keep them extra here.
|
||||||
|
|
||||||
|
(def e 101)
|
||||||
|
(def colon 58)
|
||||||
|
|
||||||
|
(defn #^{:private true} read-byte
|
||||||
|
#^long [#^InputStream input]
|
||||||
|
(let [c (.read input)]
|
||||||
|
(when (neg? c)
|
||||||
|
(throw (EOFException. "Invalid netstring. Unexpected end of input.")))
|
||||||
|
;; Here we have a quirk for example. `.read` returns -1 on end of
|
||||||
|
;; input. However the Java `Byte` has only a range from -128 to 127.
|
||||||
|
;; How does the fit together?
|
||||||
|
;;
|
||||||
|
;; The whole thing is shifted. `.read` actually returns an int
|
||||||
|
;; between zero and 255. Everything below the value 128 stands
|
||||||
|
;; for itself. But larger values are actually negative byte values.
|
||||||
|
;;
|
||||||
|
;; So we have to do some translation here. `Byte/byteValue` would
|
||||||
|
;; do that for us, but we want to avoid boxing here.
|
||||||
|
(if (< 127 c) (- c 256) c)))
|
||||||
|
|
||||||
|
(defn #^{:private true :tag "[B"} read-bytes
|
||||||
|
#^Object [#^InputStream input n]
|
||||||
|
(let [content (byte-array n)]
|
||||||
|
(loop [offset (int 0)
|
||||||
|
len (int n)]
|
||||||
|
(let [result (.read input content offset len)]
|
||||||
|
(when (neg? result)
|
||||||
|
(throw
|
||||||
|
(EOFException.
|
||||||
|
"Invalid netstring. Less data available than expected.")))
|
||||||
|
(when (not= result len)
|
||||||
|
(recur (+ offset result) (- len result)))))
|
||||||
|
content))
|
||||||
|
|
||||||
|
;; `read-long` is used for reading integers from the stream as well
|
||||||
|
;; as the byte count prefixes of byte strings. The delimiter is \:
|
||||||
|
;; for byte count prefixes and \e for integers.
|
||||||
|
|
||||||
|
(defn #^{:private true} read-long
|
||||||
|
#^long [#^InputStream input delim]
|
||||||
|
(loop [n (long 0)]
|
||||||
|
;; We read repeatedly a byte from the input…
|
||||||
|
(let [b (read-byte input)]
|
||||||
|
;; …and stop at the delimiter.
|
||||||
|
(cond
|
||||||
|
(= b minus) (- (read-long input delim))
|
||||||
|
(= b delim) n
|
||||||
|
:else (recur (+ (* n (long 10)) (- (long b) (long 48))))))))
|
||||||
|
|
||||||
|
;; ## Reading a netstring
|
||||||
|
;;
|
||||||
|
;; Let's dive straight into reading a netstring from an `InputStream`.
|
||||||
|
;;
|
||||||
|
;; For convenience we split the function into two subfunctions. The
|
||||||
|
;; public `read-netstring` is the normal entry point, which also checks
|
||||||
|
;; for the trailing comma after reading the payload data with the
|
||||||
|
;; private `read-netstring*`.
|
||||||
|
;;
|
||||||
|
;; The reason we need the less strict `read-netstring*` is that with
|
||||||
|
;; bencode we don't have a trailing comma. So a check would not be
|
||||||
|
;; beneficial here.
|
||||||
|
;;
|
||||||
|
;; However the consumer doesn't have to care. `read-netstring` as
|
||||||
|
;; well as `read-bencode` provide the public entry points, which do
|
||||||
|
;; the right thing. Although they both may reference the `read-netstring*`
|
||||||
|
;; underneath.
|
||||||
|
;;
|
||||||
|
;; With this in mind we define the inner helper function first.
|
||||||
|
|
||||||
|
(declare #^"[B" string>payload
|
||||||
|
#^String string<payload)
|
||||||
|
|
||||||
|
(defn #^{:private true} read-netstring*
|
||||||
|
[input]
|
||||||
|
(read-bytes input (read-long input colon)))
|
||||||
|
|
||||||
|
;; And the public facing API: `read-netstring`.
|
||||||
|
|
||||||
|
(defn #^"[B" read-netstring
|
||||||
|
"Reads a classic netstring from input—an InputStream. Returns the
|
||||||
|
contained binary data as byte array."
|
||||||
|
[input]
|
||||||
|
(let [content (read-netstring* input)]
|
||||||
|
(when (not= (read-byte input) comma)
|
||||||
|
(throw (IOException. "Invalid netstring. ',' expected.")))
|
||||||
|
content))
|
||||||
|
|
||||||
|
;; Similarly the `string>payload` and `string<payload` functions
|
||||||
|
;; are defined as follows to simplify the conversion between strings
|
||||||
|
;; and byte arrays in various parts of the code.
|
||||||
|
|
||||||
|
(defn #^{:private true :tag "[B"} string>payload
|
||||||
|
[#^String s]
|
||||||
|
(.getBytes s "UTF-8"))
|
||||||
|
|
||||||
|
(defn #^{:private true :tag String} string<payload
|
||||||
|
[#^"[B" b]
|
||||||
|
(String. b "UTF-8"))
|
||||||
|
|
||||||
|
;; ## Writing a netstring
|
||||||
|
;;
|
||||||
|
;; This opposite operation – writing a netstring – is just as important.
|
||||||
|
;;
|
||||||
|
;; *Note:* We take here a byte array, just as we returned a byte
|
||||||
|
;; array in `read-netstring`. The netstring should not be concerned
|
||||||
|
;; about the actual contents. It just sees binary data.
|
||||||
|
;;
|
||||||
|
;; Similar to `read-netstring` we also split `write-netstring` into
|
||||||
|
;; the entry point itself and a helper function.
|
||||||
|
|
||||||
|
(defn #^{:private true} write-netstring*
|
||||||
|
[#^OutputStream output #^"[B" content]
|
||||||
|
(doto output
|
||||||
|
(.write (string>payload (str (alength content))))
|
||||||
|
(.write (int colon))
|
||||||
|
(.write content)))
|
||||||
|
|
||||||
|
(defn write-netstring
|
||||||
|
"Write the given binary data to the output stream in form of a classic
|
||||||
|
netstring."
|
||||||
|
[#^OutputStream output content]
|
||||||
|
(doto output
|
||||||
|
(write-netstring* content)
|
||||||
|
(.write (int comma))))
|
||||||
|
|
||||||
|
;; # Bencode
|
||||||
|
;;
|
||||||
|
;; However most of the time we don't want to send simple blobs of data
|
||||||
|
;; back and forth. The data sent between the communication peers usually
|
||||||
|
;; have some structure, which has to be carried along the way to the
|
||||||
|
;; other side. Here [bencode][bc] come into play.
|
||||||
|
;;
|
||||||
|
;; Bencode defines additionally to netstrings easily parseable structures
|
||||||
|
;; for lists, maps and numbers. It allows to communicate information
|
||||||
|
;; about the data structure to the peer on the other side.
|
||||||
|
;;
|
||||||
|
;; ## Tokens
|
||||||
|
;;
|
||||||
|
;; The data is encoded in tokens in bencode. There are several types of
|
||||||
|
;; tokens:
|
||||||
|
;;
|
||||||
|
;; * A netstring without trailing comma for string data.
|
||||||
|
;; * A tag specifiyng the type of the following tokens.
|
||||||
|
;; The tag may be one of these:
|
||||||
|
;; * `\i` to encode integers.
|
||||||
|
;; * `\l` to encode lists of items.
|
||||||
|
;; * `\d` to encode maps of item pairs.
|
||||||
|
;; * `\e` to end the a previously started tag.
|
||||||
|
;;
|
||||||
|
;; ## Reading bencode
|
||||||
|
;;
|
||||||
|
;; Reading bencode encoded data is basically parsing a stream of tokens
|
||||||
|
;; from the input. Hence we need a read-token helper which allows to
|
||||||
|
;; retrieve the next token.
|
||||||
|
|
||||||
|
(defn #^{:private true} read-token
|
||||||
|
[#^PushbackInputStream input]
|
||||||
|
(let [ch (read-byte input)]
|
||||||
|
(cond
|
||||||
|
(= (long e) ch) nil
|
||||||
|
(= i ch) :integer
|
||||||
|
(= l ch) :list
|
||||||
|
(= d ch) :map
|
||||||
|
:else (do
|
||||||
|
(.unread input (int ch))
|
||||||
|
(read-netstring* input)))))
|
||||||
|
|
||||||
|
;; To read the bencode encoded data we walk a long the sequence of tokens
|
||||||
|
;; and act according to the found tags.
|
||||||
|
|
||||||
|
(declare read-integer read-list read-map)
|
||||||
|
|
||||||
|
(defn read-bencode
|
||||||
|
"Read bencode token from the input stream."
|
||||||
|
[input]
|
||||||
|
(let [token (read-token input)]
|
||||||
|
(case token
|
||||||
|
:integer (read-integer input)
|
||||||
|
:list (read-list input)
|
||||||
|
:map (read-map input)
|
||||||
|
token)))
|
||||||
|
|
||||||
|
;; Of course integers and the collection types are have to treated specially.
|
||||||
|
;;
|
||||||
|
;; Integers for example consist of a sequence of decimal digits.
|
||||||
|
|
||||||
|
(defn #^{:private true} read-integer
|
||||||
|
[input]
|
||||||
|
(read-long input e))
|
||||||
|
|
||||||
|
;; *Note:* integers are an ugly special case, which cannot be
|
||||||
|
;; handled with `read-token` or `read-netstring*`.
|
||||||
|
;;
|
||||||
|
;; Lists are just a sequence of other tokens.
|
||||||
|
|
||||||
|
(declare token-seq)
|
||||||
|
|
||||||
|
(defn #^{:private true} read-list
|
||||||
|
[input]
|
||||||
|
(vec (token-seq input)))
|
||||||
|
|
||||||
|
;; Maps are sequences of key/value pairs. The keys are always
|
||||||
|
;; decoded into strings. The values are kept as is.
|
||||||
|
|
||||||
|
(defn #^{:private true} read-map
|
||||||
|
[input]
|
||||||
|
(->> (token-seq input)
|
||||||
|
(into {} (comp (partition-all 2)
|
||||||
|
(map (fn [[k v]]
|
||||||
|
[(string<payload k) v]))))))
|
||||||
|
|
||||||
|
;; The final missing piece is `token-seq`. This a just a simple
|
||||||
|
;; sequence which reads tokens until the next `\e`.
|
||||||
|
|
||||||
|
(defn #^{:private true} token-seq
|
||||||
|
[input]
|
||||||
|
(->> #(read-bencode input)
|
||||||
|
repeatedly
|
||||||
|
(take-while identity)))
|
||||||
|
|
||||||
|
;; ## Writing bencode
|
||||||
|
;;
|
||||||
|
;; Writing bencode is similar easy as reading it. The main entry point
|
||||||
|
;; takes a string, map, sequence or integer and writes it according to
|
||||||
|
;; the rules to the given OutputStream.
|
||||||
|
|
||||||
|
(defmulti write-bencode
|
||||||
|
"Write the given thing to the output stream. “Thing” means here a
|
||||||
|
string, map, sequence or integer. Alternatively an ByteArray may
|
||||||
|
be provided whose contents are written as a bytestring. Similar
|
||||||
|
the contents of a given InputStream are written as a byte string.
|
||||||
|
Named things (symbols or keywords) are written in the form
|
||||||
|
'namespace/name'."
|
||||||
|
(fn [_output thing]
|
||||||
|
(cond
|
||||||
|
(bytes? thing) :bytes
|
||||||
|
(instance? InputStream thing) :input-stream
|
||||||
|
(integer? thing) :integer
|
||||||
|
(string? thing) :string
|
||||||
|
(symbol? thing) :named
|
||||||
|
(keyword? thing) :named
|
||||||
|
(map? thing) :map
|
||||||
|
(or (nil? thing) (coll? thing) (.isArray (class thing))) :list
|
||||||
|
:else (type thing))))
|
||||||
|
|
||||||
|
(defmethod write-bencode :default
|
||||||
|
[output x]
|
||||||
|
(throw (IllegalArgumentException. (str "Cannot write value of type " (class x)))))
|
||||||
|
|
||||||
|
;; The following methods should be pretty straight-forward.
|
||||||
|
;;
|
||||||
|
;; The easiest case is of course when we already have a byte array.
|
||||||
|
;; We can simply pass it on to the underlying machinery.
|
||||||
|
|
||||||
|
(defmethod write-bencode :bytes
|
||||||
|
[output bytes]
|
||||||
|
(write-netstring* output bytes))
|
||||||
|
|
||||||
|
;; For strings we simply write the string as a netstring without
|
||||||
|
;; trailing comma after encoding the string as UTF-8 bytes.
|
||||||
|
|
||||||
|
(defmethod write-bencode :string
|
||||||
|
[output string]
|
||||||
|
(write-netstring* output (string>payload string)))
|
||||||
|
|
||||||
|
;; Streaming does not really work, since we need to know the
|
||||||
|
;; number of bytes to write upfront. So we read in everything
|
||||||
|
;; for InputStreams and pass on the byte array.
|
||||||
|
|
||||||
|
(defmethod write-bencode :input-stream
|
||||||
|
[output stream]
|
||||||
|
(let [bytes (ByteArrayOutputStream.)]
|
||||||
|
(io/copy stream bytes)
|
||||||
|
(write-netstring* output (.toByteArray bytes))))
|
||||||
|
|
||||||
|
;; Integers are again the ugly special case.
|
||||||
|
|
||||||
|
(defmethod write-bencode :integer
|
||||||
|
[#^OutputStream output n]
|
||||||
|
(doto output
|
||||||
|
(.write (int i))
|
||||||
|
(.write (string>payload (str n)))
|
||||||
|
(.write (int e))))
|
||||||
|
|
||||||
|
;; Symbols and keywords are converted to a string of the
|
||||||
|
;; form 'namespace/name' or just 'name' in case its not
|
||||||
|
;; qualified. We do not add colons for keywords since the
|
||||||
|
;; other side might not have the notion of keywords.
|
||||||
|
|
||||||
|
(defmethod write-bencode :named
|
||||||
|
[output thing]
|
||||||
|
(let [nspace (namespace thing)
|
||||||
|
name (name thing)]
|
||||||
|
(->> (str (when nspace (str nspace "/")) name)
|
||||||
|
string>payload
|
||||||
|
(write-netstring* output))))
|
||||||
|
|
||||||
|
;; Lists as well as maps work recursively to print their elements.
|
||||||
|
|
||||||
|
(defmethod write-bencode :list
|
||||||
|
[#^OutputStream output lst]
|
||||||
|
(.write output (int l))
|
||||||
|
(doseq [elt lst]
|
||||||
|
(write-bencode output elt))
|
||||||
|
(.write output (int e)))
|
||||||
|
|
||||||
|
;; However, maps are a bit special because their keys are sorted
|
||||||
|
;; lexicographically based on their byte string represantation.
|
||||||
|
|
||||||
|
(declare lexicographically)
|
||||||
|
|
||||||
|
(defmethod write-bencode :map
|
||||||
|
[#^OutputStream output m]
|
||||||
|
(let [translation (into {} (map (juxt string>payload identity) (keys m)))
|
||||||
|
key-strings (sort lexicographically (keys translation))
|
||||||
|
>value (comp m translation)]
|
||||||
|
(.write output (int d))
|
||||||
|
(doseq [k key-strings]
|
||||||
|
(write-netstring* output k)
|
||||||
|
(write-bencode output (>value k)))
|
||||||
|
(.write output (int e))))
|
||||||
|
|
||||||
|
;; However, since byte arrays are not `Comparable` we need a custom
|
||||||
|
;; comparator which we can feed to `sort`.
|
||||||
|
|
||||||
|
(defn #^{:private true} lexicographically
|
||||||
|
[#^"[B" a #^"[B" b]
|
||||||
|
(let [alen (alength a)
|
||||||
|
blen (alength b)
|
||||||
|
len (min alen blen)]
|
||||||
|
(loop [i 0]
|
||||||
|
(if (== i len)
|
||||||
|
(- alen blen)
|
||||||
|
(let [x (- (int (aget a i)) (int (aget b i)))]
|
||||||
|
(if (zero? x)
|
||||||
|
(recur (inc i))
|
||||||
|
x))))))
|
||||||
|
|
@ -204,6 +204,8 @@
|
||||||
java.security.MessageDigest
|
java.security.MessageDigest
|
||||||
(instance? java.io.InputStream v)
|
(instance? java.io.InputStream v)
|
||||||
java.io.InputStream
|
java.io.InputStream
|
||||||
|
(instance? java.io.OutputStream v)
|
||||||
|
java.io.OutputStream
|
||||||
(instance? java.nio.file.FileSystem v)
|
(instance? java.nio.file.FileSystem v)
|
||||||
java.nio.file.FileSystem)))))
|
java.nio.file.FileSystem)))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@
|
||||||
*e nil]
|
*e nil]
|
||||||
~@body))
|
~@body))
|
||||||
|
|
||||||
|
(def ^{:doc "A sequence of lib specs that are applied to `require`
|
||||||
|
by default when a new command-line REPL is started."} repl-requires
|
||||||
|
'[[clojure.repl :refer (dir doc)]
|
||||||
|
[clojure.pprint :refer (pprint)]])
|
||||||
|
|
||||||
(defn repl
|
(defn repl
|
||||||
"Generic, reusable, read-eval-print loop. By default, reads from *in*,
|
"Generic, reusable, read-eval-print loop. By default, reads from *in*,
|
||||||
writes to *out*, and prints exception summaries to *err*. If you use the
|
writes to *out*, and prints exception summaries to *err*. If you use the
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
(ns babashka.impl.clojure.stacktrace
|
(ns babashka.impl.clojure.stacktrace
|
||||||
{:no-doc true}
|
{:no-doc true}
|
||||||
(:require [clojure.stacktrace :as stacktrace]))
|
(:require [clojure.stacktrace :as stacktrace]
|
||||||
|
[sci.core :as sci]))
|
||||||
|
|
||||||
|
(defmacro wrap-out [f]
|
||||||
|
`(fn [& ~'args]
|
||||||
|
(binding [*out* @sci/out]
|
||||||
|
(apply ~f ~'args))))
|
||||||
|
|
||||||
(def stacktrace-namespace
|
(def stacktrace-namespace
|
||||||
{'root-cause stacktrace/root-cause
|
{'root-cause stacktrace/root-cause
|
||||||
'print-trace-element stacktrace/print-trace-element
|
'print-trace-element (wrap-out stacktrace/print-trace-element)
|
||||||
'print-throwable stacktrace/print-throwable
|
'print-throwable (wrap-out stacktrace/print-throwable)
|
||||||
'print-stack-trace stacktrace/print-stack-trace
|
'print-stack-trace (wrap-out stacktrace/print-stack-trace)
|
||||||
'print-cause-trace stacktrace/print-cause-trace})
|
'print-cause-trace (wrap-out stacktrace/print-cause-trace)})
|
||||||
|
|
|
||||||
236
src/babashka/impl/nrepl_server.clj
Normal file
236
src/babashka/impl/nrepl_server.clj
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
(ns babashka.impl.nrepl-server
|
||||||
|
{:no-doc true}
|
||||||
|
(:refer-clojure :exclude [send future binding])
|
||||||
|
(:require [babashka.impl.bencode.core :refer [write-bencode read-bencode]]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[sci.core :as sci]
|
||||||
|
[sci.impl.interpreter :refer [eval-string*]]
|
||||||
|
[sci.impl.utils :as sci-utils]
|
||||||
|
[sci.impl.vars :as vars])
|
||||||
|
(:import [java.io StringWriter OutputStream InputStream PushbackInputStream EOFException]
|
||||||
|
[java.net ServerSocket]))
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(def port 1667)
|
||||||
|
(def dev? (volatile! nil))
|
||||||
|
|
||||||
|
(defn response-for [old-msg msg]
|
||||||
|
(let [session (get old-msg :session "none")
|
||||||
|
m (assoc msg "session" session)
|
||||||
|
id (get old-msg :id "unknown")
|
||||||
|
m (assoc m "id" id)]
|
||||||
|
m))
|
||||||
|
|
||||||
|
(defn send [^OutputStream os msg]
|
||||||
|
;;(when @dev? (prn "Sending" msg))
|
||||||
|
(write-bencode os msg)
|
||||||
|
(.flush os))
|
||||||
|
|
||||||
|
(defn send-exception [os msg ^Throwable ex]
|
||||||
|
(let [ex-map (Throwable->map ex)
|
||||||
|
ex-name (-> ex-map :via first :type)
|
||||||
|
cause (:cause ex-map)]
|
||||||
|
(when @dev? (prn "sending exception" ex-map))
|
||||||
|
(send os (response-for msg {"err" (str ex-name ": " cause "\n")}))
|
||||||
|
(send os (response-for msg {"ex" (str "class " ex-name)
|
||||||
|
"root-ex" (str "class " ex-name)
|
||||||
|
"status" #{"eval-error"}}))
|
||||||
|
(send os (response-for msg {"status" #{"done"}}))))
|
||||||
|
|
||||||
|
(defn eval-msg [ctx o msg #_threads]
|
||||||
|
(try
|
||||||
|
(let [ns-str (get msg :ns)
|
||||||
|
sci-ns (if ns-str
|
||||||
|
(sci-utils/namespace-object (:env ctx) (symbol ns-str) nil false)
|
||||||
|
(sci-utils/namespace-object (:env ctx) 'user nil false))]
|
||||||
|
(sci/binding [vars/current-ns sci-ns
|
||||||
|
sci/print-length @sci/print-length]
|
||||||
|
(let [session (get msg :session "none")
|
||||||
|
id (get msg :id "unknown")]
|
||||||
|
(when @dev? (println "Registering thread for" (str session "-" id)))
|
||||||
|
;; (swap! threads assoc [session id] (Thread/currentThread))
|
||||||
|
(let [code-str (get msg :code)
|
||||||
|
sw (StringWriter.)
|
||||||
|
value (if (str/blank? code-str)
|
||||||
|
::nil
|
||||||
|
(sci/binding [sci/out sw
|
||||||
|
vars/current-ns @vars/current-ns] (eval-string* ctx code-str)))
|
||||||
|
out-str (not-empty (str sw))
|
||||||
|
env (:env ctx)]
|
||||||
|
(swap! env update-in [:namespaces 'clojure.core]
|
||||||
|
(fn [core]
|
||||||
|
(assoc core
|
||||||
|
'*1 value
|
||||||
|
'*2 (get core '*1)
|
||||||
|
'*3 (get core '*2))))
|
||||||
|
(when @dev? (println "out str:" out-str))
|
||||||
|
(when out-str
|
||||||
|
(send o (response-for msg {"out" out-str})))
|
||||||
|
(send o (response-for msg (cond-> {"ns" (vars/current-ns-name)}
|
||||||
|
(not (identical? value ::nil)) (assoc "value" (pr-str value)))))
|
||||||
|
(send o (response-for msg {"status" #{"done"}}))))))
|
||||||
|
(catch Exception ex
|
||||||
|
(swap! (:env ctx) update-in [:namespaces 'clojure.core]
|
||||||
|
(fn [core]
|
||||||
|
(assoc core '*e ex)))
|
||||||
|
(send-exception o msg ex))))
|
||||||
|
|
||||||
|
(defn fully-qualified-syms [ctx ns-sym]
|
||||||
|
(let [syms (eval-string* ctx (format "(keys (ns-map '%s))" ns-sym))
|
||||||
|
sym-strs (map #(str "`" %) syms)
|
||||||
|
sym-expr (str "[" (str/join " " sym-strs) "]")
|
||||||
|
syms (eval-string* ctx sym-expr)]
|
||||||
|
syms))
|
||||||
|
|
||||||
|
(defn match [_alias->ns ns->alias query [sym-ns sym-name qualifier]]
|
||||||
|
(let [pat (re-pattern query)]
|
||||||
|
(or (when (and (identical? :unqualified qualifier) (re-find pat sym-name))
|
||||||
|
[sym-ns sym-name])
|
||||||
|
(when sym-ns
|
||||||
|
(or (when (re-find pat (str sym-ns "/" sym-name))
|
||||||
|
[sym-ns (str sym-ns "/" sym-name)])
|
||||||
|
(when (re-find pat (str (get ns->alias (symbol sym-ns)) "/" sym-name))
|
||||||
|
[sym-ns (str (get ns->alias (symbol sym-ns)) "/" sym-name)]))))))
|
||||||
|
|
||||||
|
(defn complete [ctx o msg]
|
||||||
|
(try
|
||||||
|
(let [ns-str (get msg :ns)
|
||||||
|
sci-ns (if ns-str
|
||||||
|
(sci-utils/namespace-object (:env ctx) (symbol ns-str) nil false)
|
||||||
|
(sci-utils/namespace-object (:env ctx) 'user nil false))]
|
||||||
|
(sci/binding [vars/current-ns sci-ns]
|
||||||
|
(let [
|
||||||
|
;;ns-sym (symbol ns)
|
||||||
|
query (:symbol msg)
|
||||||
|
from-current-ns (fully-qualified-syms ctx (eval-string* ctx "(ns-name *ns*)"))
|
||||||
|
from-current-ns (map (fn [sym]
|
||||||
|
[(namespace sym) (name sym) :unqualified])
|
||||||
|
from-current-ns)
|
||||||
|
alias->ns (eval-string* ctx "(let [m (ns-aliases *ns*)] (zipmap (keys m) (map ns-name (vals m))))")
|
||||||
|
ns->alias (zipmap (vals alias->ns) (keys alias->ns))
|
||||||
|
from-aliased-nss (doall (mapcat
|
||||||
|
(fn [alias]
|
||||||
|
(let [ns (get alias->ns alias)
|
||||||
|
syms (eval-string* ctx (format "(keys (ns-publics '%s))" ns))]
|
||||||
|
(map (fn [sym]
|
||||||
|
[(str ns) (str sym) :qualified])
|
||||||
|
syms)))
|
||||||
|
(keys alias->ns)))
|
||||||
|
svs (concat from-current-ns from-aliased-nss)
|
||||||
|
completions (keep (fn [entry]
|
||||||
|
(match alias->ns ns->alias query entry))
|
||||||
|
svs)
|
||||||
|
completions (mapv (fn [[namespace name]]
|
||||||
|
{"candidate" (str name) "ns" (str namespace) #_"type" #_"function"})
|
||||||
|
completions)]
|
||||||
|
(when @dev? (prn "completions" completions))
|
||||||
|
(send o (response-for msg {"completions" completions
|
||||||
|
"status" #{"done"}})))))
|
||||||
|
(catch Throwable e
|
||||||
|
(println e)
|
||||||
|
(send o (response-for msg {"completions" []
|
||||||
|
"status" #{"done"}})))))
|
||||||
|
|
||||||
|
;; GraalVM doesn't support the .stop method on Threads, so for now we will have to live without interrupt
|
||||||
|
#_(defn interrupt [_ctx os msg threads]
|
||||||
|
(let [session (get msg :session "none")
|
||||||
|
id (get msg :interrupt-id)]
|
||||||
|
(when-let [t (get @threads [session id])]
|
||||||
|
(when @dev? (println "Killing thread" (str session "-" id)))
|
||||||
|
(try (.stop ^java.lang.Thread t)
|
||||||
|
(catch Throwable e
|
||||||
|
(println e))))
|
||||||
|
(send os (response-for msg {"status" #{"done"}}))))
|
||||||
|
|
||||||
|
(defn read-msg [msg]
|
||||||
|
(-> (zipmap (map keyword (keys msg))
|
||||||
|
(map #(if (bytes? %)
|
||||||
|
(String. (bytes %))
|
||||||
|
%) (vals msg)))
|
||||||
|
(update :op keyword)))
|
||||||
|
|
||||||
|
(defn session-loop [ctx ^InputStream is os id #_threads]
|
||||||
|
(when @dev? (println "Reading!" id (.available is)))
|
||||||
|
(when-let [msg (try (read-bencode is)
|
||||||
|
(catch EOFException _
|
||||||
|
(println "Client closed connection.")))]
|
||||||
|
(let [msg (read-msg msg)]
|
||||||
|
(when @dev? (prn "Received" msg))
|
||||||
|
(case (get msg :op)
|
||||||
|
:clone (do
|
||||||
|
(when @dev? (println "Cloning!"))
|
||||||
|
(let [id (str (java.util.UUID/randomUUID))]
|
||||||
|
(send os (response-for msg {"new-session" id "status" #{"done"}}))
|
||||||
|
(recur ctx is os id #_threads)))
|
||||||
|
:eval (do
|
||||||
|
(eval-msg ctx os msg #_threads)
|
||||||
|
(recur ctx is os id #_threads))
|
||||||
|
:load-file (let [file (:file msg)
|
||||||
|
msg (assoc msg :code file)]
|
||||||
|
(eval-msg ctx os msg #_threads)
|
||||||
|
(recur ctx is os id #_threads))
|
||||||
|
:complete (do
|
||||||
|
(complete ctx os msg)
|
||||||
|
(recur ctx is os id #_threads))
|
||||||
|
;; :interrupt (do
|
||||||
|
;; (interrupt ctx os msg threads)
|
||||||
|
;; (recur ctx is os id threads))
|
||||||
|
:describe
|
||||||
|
(do (send os (response-for msg {"status" #{"done"}
|
||||||
|
"aux" {}
|
||||||
|
"ops" (zipmap #{"clone", "describe", "eval"}
|
||||||
|
(repeat {}))
|
||||||
|
"versions" {} #_{"nrepl" {"major" "0"
|
||||||
|
"minor" "4"
|
||||||
|
"incremental" "0"
|
||||||
|
"qualifier" ""}
|
||||||
|
"clojure"
|
||||||
|
{"*clojure-version*"
|
||||||
|
(zipmap (map name (keys *clojure-version*))
|
||||||
|
(vals *clojure-version*))}}}))
|
||||||
|
(recur ctx is os id #_threads))
|
||||||
|
;; fallback
|
||||||
|
(do (when @dev?
|
||||||
|
(println "Unhandled message" msg))
|
||||||
|
(send os (response-for msg {"status" #{"error" "unknown-op" "done"}}))
|
||||||
|
(recur ctx is os id #_threads))))))
|
||||||
|
|
||||||
|
(defn listen [ctx ^ServerSocket listener]
|
||||||
|
(when @dev? (println "Listening"))
|
||||||
|
(let [client-socket (.accept listener)
|
||||||
|
in (.getInputStream client-socket)
|
||||||
|
in (PushbackInputStream. in)
|
||||||
|
out (.getOutputStream client-socket)
|
||||||
|
#_threads #_(atom {})]
|
||||||
|
(when @dev? (println "Connected."))
|
||||||
|
(sci/future
|
||||||
|
(sci/binding
|
||||||
|
;; allow *ns* to be set! inside future
|
||||||
|
[vars/current-ns (vars/->SciNamespace 'user nil)
|
||||||
|
sci/print-length @sci/print-length]
|
||||||
|
(session-loop ctx in out "pre-init" #_threads)))
|
||||||
|
(recur ctx listener)))
|
||||||
|
|
||||||
|
|
||||||
|
(def server (atom nil))
|
||||||
|
|
||||||
|
(defn stop-server! []
|
||||||
|
(when-let [s @server]
|
||||||
|
(.close ^ServerSocket s)
|
||||||
|
(reset! server nil)))
|
||||||
|
|
||||||
|
(defn start-server! [ctx host+port]
|
||||||
|
(vreset! dev? (= "true" (System/getenv "BABASHKA_DEV")))
|
||||||
|
(let [parts (str/split host+port #":")
|
||||||
|
[address port] (if (= 1 (count parts))
|
||||||
|
[nil (Integer. ^String (first parts))]
|
||||||
|
[(java.net.InetAddress/getByName (first parts))
|
||||||
|
(Integer. ^String (second parts))])
|
||||||
|
host+port (if-not address (str "localhost:" port)
|
||||||
|
host+port)]
|
||||||
|
#_(complete ctx nil {:symbol "json"})
|
||||||
|
(println "Starting nREPL server at" host+port)
|
||||||
|
(let [socket-server (new ServerSocket port 0 address)]
|
||||||
|
(reset! server socket-server)
|
||||||
|
(listen ctx socket-server))))
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
[sci.impl.namespaces :refer [copy-var]]
|
[sci.impl.namespaces :refer [copy-var]]
|
||||||
[sci.impl.vars :as vars]))
|
[sci.impl.vars :as vars]))
|
||||||
|
|
||||||
|
|
||||||
(def tns (vars/->SciNamespace 'cognitect.transit nil))
|
(def tns (vars/->SciNamespace 'cognitect.transit nil))
|
||||||
|
|
||||||
(def transit-namespace
|
(def transit-namespace
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,20 @@
|
||||||
{:no-doc true}
|
{:no-doc true}
|
||||||
(:require
|
(:require
|
||||||
[babashka.impl.async :refer [async-namespace async-protocols-namespace]]
|
[babashka.impl.async :refer [async-namespace async-protocols-namespace]]
|
||||||
|
[babashka.impl.bencode :refer [bencode-namespace]]
|
||||||
[babashka.impl.cheshire :refer [cheshire-core-namespace]]
|
[babashka.impl.cheshire :refer [cheshire-core-namespace]]
|
||||||
[babashka.impl.classes :as classes]
|
[babashka.impl.classes :as classes]
|
||||||
[babashka.impl.classpath :as cp]
|
[babashka.impl.classpath :as cp]
|
||||||
[babashka.impl.clojure.core :refer [core-extras]]
|
[babashka.impl.clojure.core :refer [core-extras]]
|
||||||
[babashka.impl.clojure.java.io :refer [io-namespace]]
|
[babashka.impl.clojure.java.io :refer [io-namespace]]
|
||||||
[babashka.impl.clojure.java.shell :refer [shell-namespace]]
|
[babashka.impl.clojure.java.shell :refer [shell-namespace]]
|
||||||
[babashka.impl.clojure.main :refer [demunge]]
|
[babashka.impl.clojure.main :as clojure-main :refer [demunge]]
|
||||||
[babashka.impl.clojure.pprint :refer [pprint-namespace]]
|
[babashka.impl.clojure.pprint :refer [pprint-namespace]]
|
||||||
[babashka.impl.clojure.stacktrace :refer [stacktrace-namespace]]
|
[babashka.impl.clojure.stacktrace :refer [stacktrace-namespace]]
|
||||||
[babashka.impl.common :as common]
|
[babashka.impl.common :as common]
|
||||||
[babashka.impl.csv :as csv]
|
[babashka.impl.csv :as csv]
|
||||||
[babashka.impl.curl :refer [curl-namespace]]
|
[babashka.impl.curl :refer [curl-namespace]]
|
||||||
|
[babashka.impl.nrepl-server :as nrepl-server]
|
||||||
[babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]]
|
[babashka.impl.pipe-signal-handler :refer [handle-pipe! pipe-signal-received?]]
|
||||||
[babashka.impl.repl :as repl]
|
[babashka.impl.repl :as repl]
|
||||||
[babashka.impl.socket-repl :as socket-repl]
|
[babashka.impl.socket-repl :as socket-repl]
|
||||||
|
|
@ -115,6 +117,12 @@
|
||||||
(assoc opts-map
|
(assoc opts-map
|
||||||
:socket-repl (or (first options)
|
:socket-repl (or (first options)
|
||||||
"1666"))))
|
"1666"))))
|
||||||
|
("--nrepl-server")
|
||||||
|
(let [options (next options)]
|
||||||
|
(recur (next options)
|
||||||
|
(assoc opts-map
|
||||||
|
:nrepl (or (first options)
|
||||||
|
"1667"))))
|
||||||
("--eval", "-e")
|
("--eval", "-e")
|
||||||
(let [options (next options)]
|
(let [options (next options)]
|
||||||
(recur (next options)
|
(recur (next options)
|
||||||
|
|
@ -158,7 +166,7 @@
|
||||||
(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose]
|
(def usage-string "Usage: bb [ -i | -I ] [ -o | -O ] [ --stream ] [--verbose]
|
||||||
[ ( --classpath | -cp ) <cp> ] [ --uberscript <file> ]
|
[ ( --classpath | -cp ) <cp> ] [ --uberscript <file> ]
|
||||||
[ ( --main | -m ) <main-namespace> | -e <expression> | -f <file> |
|
[ ( --main | -m ) <main-namespace> | -e <expression> | -f <file> |
|
||||||
--repl | --socket-repl [<host>:]<port> ]
|
--repl | --socket-repl [<host>:]<port> | --nrepl-server [<host>:]<port> ]
|
||||||
[ arg* ]")
|
[ arg* ]")
|
||||||
(defn print-usage []
|
(defn print-usage []
|
||||||
(println usage-string))
|
(println usage-string))
|
||||||
|
|
@ -188,6 +196,7 @@
|
||||||
-m, --main <ns> Call the -main function from namespace with args.
|
-m, --main <ns> Call the -main function from namespace with args.
|
||||||
--repl Start REPL. Use rlwrap for history.
|
--repl Start REPL. Use rlwrap for history.
|
||||||
--socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
|
--socket-repl Start socket REPL. Specify port (e.g. 1666) or host and port separated by colon (e.g. 127.0.0.1:1666).
|
||||||
|
--nrepl-server Start nREPL server. Specify port (e.g. 1667) or host and port separated by colon (e.g. 127.0.0.1:1667).
|
||||||
--time Print execution time before exiting.
|
--time Print execution time before exiting.
|
||||||
-- Stop parsing args and pass everything after -- to *command-line-args*
|
-- Stop parsing args and pass everything after -- to *command-line-args*
|
||||||
|
|
||||||
|
|
@ -218,6 +227,11 @@ Everything after that is bound to *command-line-args*."))
|
||||||
;; hang until SIGINT
|
;; hang until SIGINT
|
||||||
@(promise))
|
@(promise))
|
||||||
|
|
||||||
|
(defn start-nrepl! [address ctx]
|
||||||
|
(nrepl-server/start-server! ctx address)
|
||||||
|
;; hang until SIGINT
|
||||||
|
#_@(promise))
|
||||||
|
|
||||||
(defn exit [n]
|
(defn exit [n]
|
||||||
(throw (ex-info "" {:bb/exit-code n})))
|
(throw (ex-info "" {:bb/exit-code n})))
|
||||||
|
|
||||||
|
|
@ -232,7 +246,8 @@ Everything after that is bound to *command-line-args*."))
|
||||||
csv clojure.data.csv
|
csv clojure.data.csv
|
||||||
json cheshire.core
|
json cheshire.core
|
||||||
curl babashka.curl
|
curl babashka.curl
|
||||||
transit cognitect.transit})
|
transit cognitect.transit
|
||||||
|
bencode bencode.core})
|
||||||
|
|
||||||
(def cp-state (atom nil))
|
(def cp-state (atom nil))
|
||||||
|
|
||||||
|
|
@ -258,13 +273,15 @@ Everything after that is bound to *command-line-args*."))
|
||||||
'clojure.data.csv csv/csv-namespace
|
'clojure.data.csv csv/csv-namespace
|
||||||
'cheshire.core cheshire-core-namespace
|
'cheshire.core cheshire-core-namespace
|
||||||
'clojure.stacktrace stacktrace-namespace
|
'clojure.stacktrace stacktrace-namespace
|
||||||
'clojure.main {'demunge demunge}
|
'clojure.main {'demunge demunge
|
||||||
|
'repl-requires clojure-main/repl-requires}
|
||||||
'clojure.repl {'demunge demunge}
|
'clojure.repl {'demunge demunge}
|
||||||
'clojure.test t/clojure-test-namespace
|
'clojure.test t/clojure-test-namespace
|
||||||
'babashka.classpath {'add-classpath add-classpath*}
|
'babashka.classpath {'add-classpath add-classpath*}
|
||||||
'clojure.pprint pprint-namespace
|
'clojure.pprint pprint-namespace
|
||||||
'babashka.curl curl-namespace
|
'babashka.curl curl-namespace
|
||||||
'cognitect.transit transit-namespace})
|
'cognitect.transit transit-namespace
|
||||||
|
'bencode.core bencode-namespace})
|
||||||
|
|
||||||
(def bindings
|
(def bindings
|
||||||
{'java.lang.System/exit exit ;; override exit, so we have more control
|
{'java.lang.System/exit exit ;; override exit, so we have more control
|
||||||
|
|
@ -295,7 +312,7 @@ Everything after that is bound to *command-line-args*."))
|
||||||
{:keys [:version :shell-in :edn-in :shell-out :edn-out
|
{:keys [:version :shell-in :edn-in :shell-out :edn-out
|
||||||
:help? :file :command-line-args
|
:help? :file :command-line-args
|
||||||
:expressions :stream? :time?
|
:expressions :stream? :time?
|
||||||
:repl :socket-repl
|
:repl :socket-repl :nrepl
|
||||||
:verbose? :classpath
|
:verbose? :classpath
|
||||||
:main :uberscript] :as _opts}
|
:main :uberscript] :as _opts}
|
||||||
(parse-opts args)
|
(parse-opts args)
|
||||||
|
|
@ -403,6 +420,7 @@ Everything after that is bound to *command-line-args*."))
|
||||||
[(print-help) 0]
|
[(print-help) 0]
|
||||||
repl [(repl/start-repl! sci-ctx) 0]
|
repl [(repl/start-repl! sci-ctx) 0]
|
||||||
socket-repl [(start-socket-repl! socket-repl sci-ctx) 0]
|
socket-repl [(start-socket-repl! socket-repl sci-ctx) 0]
|
||||||
|
nrepl [(start-nrepl! nrepl sci-ctx) 0]
|
||||||
(not (str/blank? expression))
|
(not (str/blank? expression))
|
||||||
(try
|
(try
|
||||||
(loop []
|
(loop []
|
||||||
|
|
@ -446,7 +464,8 @@ Everything after that is bound to *command-line-args*."))
|
||||||
(defn -main
|
(defn -main
|
||||||
[& args]
|
[& args]
|
||||||
(if-let [dev-opts (System/getenv "BABASHKA_DEV")]
|
(if-let [dev-opts (System/getenv "BABASHKA_DEV")]
|
||||||
(let [{:keys [:n]} (edn/read-string dev-opts)
|
(let [{:keys [:n]} (if (= "true" dev-opts) {:n 1}
|
||||||
|
(edn/read-string dev-opts))
|
||||||
last-iteration (dec n)]
|
last-iteration (dec n)]
|
||||||
(dotimes [i n]
|
(dotimes [i n]
|
||||||
(if (< i last-iteration)
|
(if (< i last-iteration)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
opts)
|
opts)
|
||||||
t0 (System/currentTimeMillis)]
|
t0 (System/currentTimeMillis)]
|
||||||
(loop []
|
(loop []
|
||||||
(let [v (try (with-open [_ (Socket. host port)]
|
(let [v (try (.close (Socket. host port))
|
||||||
(- (System/currentTimeMillis) t0))
|
(- (System/currentTimeMillis) t0)
|
||||||
(catch ConnectException _e
|
(catch ConnectException _e
|
||||||
(let [took (- (System/currentTimeMillis) t0)]
|
(let [took (- (System/currentTimeMillis) t0)]
|
||||||
(if (and timeout (>= took timeout))
|
(if (and timeout (>= took timeout))
|
||||||
|
|
|
||||||
120
test/babashka/impl/nrepl_server_test.clj
Normal file
120
test/babashka/impl/nrepl_server_test.clj
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
(ns babashka.impl.nrepl-server-test
|
||||||
|
(:require
|
||||||
|
[babashka.impl.bencode.core :as bencode]
|
||||||
|
[babashka.impl.nrepl-server :refer [start-server! stop-server!]]
|
||||||
|
[babashka.test-utils :as tu]
|
||||||
|
[babashka.wait :as wait]
|
||||||
|
[cheshire.core :as cheshire]
|
||||||
|
[clojure.java.shell :refer [sh]]
|
||||||
|
[clojure.test :as t :refer [deftest is testing]]
|
||||||
|
[sci.impl.opts :refer [init]])
|
||||||
|
(:import [java.lang ProcessBuilder$Redirect]))
|
||||||
|
|
||||||
|
(set! *warn-on-reflection* true)
|
||||||
|
|
||||||
|
(defn bytes->str [x]
|
||||||
|
(if (bytes? x) (String. (bytes x))
|
||||||
|
(str x)))
|
||||||
|
|
||||||
|
(defn read-msg [msg]
|
||||||
|
(let [res (zipmap (map keyword (keys msg))
|
||||||
|
(map #(if (bytes? %)
|
||||||
|
(String. (bytes %))
|
||||||
|
%)
|
||||||
|
(vals msg)))
|
||||||
|
res (if-let [status (:status res)]
|
||||||
|
(assoc res :status (mapv bytes->str status))
|
||||||
|
res)]
|
||||||
|
res))
|
||||||
|
|
||||||
|
(defn read-reply [in session id]
|
||||||
|
(loop []
|
||||||
|
(let [msg (read-msg (bencode/read-bencode in))]
|
||||||
|
(if (and (= (:session msg) session)
|
||||||
|
(= (:id msg) id))
|
||||||
|
msg
|
||||||
|
(recur)))))
|
||||||
|
|
||||||
|
(defn nrepl-test []
|
||||||
|
(with-open [socket (java.net.Socket. "127.0.0.1" 1667)
|
||||||
|
in (.getInputStream socket)
|
||||||
|
in (java.io.PushbackInputStream. in)
|
||||||
|
os (.getOutputStream socket)]
|
||||||
|
(bencode/write-bencode os {"op" "clone"})
|
||||||
|
(let [session (:new-session (read-msg (bencode/read-bencode in)))]
|
||||||
|
(testing "session"
|
||||||
|
(is session))
|
||||||
|
(testing "eval"
|
||||||
|
(bencode/write-bencode os {"op" "eval" "code" "(+ 1 2 3)" "session" session "id" 1})
|
||||||
|
(let [msg (read-reply in session 1)
|
||||||
|
id (:id msg)
|
||||||
|
value (:value msg)]
|
||||||
|
(is (= 1 id))
|
||||||
|
(is (= value "6"))))
|
||||||
|
(testing "load-file"
|
||||||
|
(bencode/write-bencode os {"op" "load-file" "file" "(ns foo) (defn foo [] :foo)" "session" session "id" 2})
|
||||||
|
(read-reply in session 2)
|
||||||
|
(bencode/write-bencode os {"op" "eval" "code" "(foo)" "ns" "foo" "session" session "id" 3})
|
||||||
|
(is (= ":foo" (:value (read-reply in session 3)))))
|
||||||
|
(testing "complete"
|
||||||
|
(testing "completions for fo"
|
||||||
|
(bencode/write-bencode os {"op" "complete"
|
||||||
|
"symbol" "fo"
|
||||||
|
"session" session
|
||||||
|
"id" 4
|
||||||
|
"ns" "foo"})
|
||||||
|
(let [reply (read-reply in session 4)
|
||||||
|
completions (:completions reply)
|
||||||
|
completions (mapv read-msg completions)
|
||||||
|
completions (into #{} (map (juxt :ns :candidate)) completions)]
|
||||||
|
(is (contains? completions ["foo" "foo"]))
|
||||||
|
(is (contains? completions ["clojure.core" "format"]))))
|
||||||
|
(testing "completions for quux should be empty"
|
||||||
|
(bencode/write-bencode os {"op" "complete"
|
||||||
|
"symbol" "quux"
|
||||||
|
"session" session "id" 6
|
||||||
|
"ns" "foo"})
|
||||||
|
(let [reply (read-reply in session 6)
|
||||||
|
completions (:completions reply)]
|
||||||
|
(is (empty? completions)))
|
||||||
|
(testing "unless quux is an alias"
|
||||||
|
(bencode/write-bencode os {"op" "eval" "code" "(require '[cheshire.core :as quux])" "session" session "id" 7})
|
||||||
|
(bencode/write-bencode os {"op" "complete" "symbol" "quux" "session" session "id" 8})
|
||||||
|
(let [reply (read-reply in session 8)
|
||||||
|
completions (:completions reply)
|
||||||
|
completions (mapv read-msg completions)
|
||||||
|
completions (into #{} (map (juxt :ns :candidate)) completions)]
|
||||||
|
(is (contains? completions ["cheshire.core" "quux/generate-string"]))))))
|
||||||
|
#_(testing "interrupt" ;; .stop doesn't work on Thread in GraalVM, this is why we can't have this yet
|
||||||
|
(bencode/write-bencode os {"op" "eval" "code" "(range)" "session" session "id" 9})
|
||||||
|
(Thread/sleep 1000)
|
||||||
|
(bencode/write-bencode os {"op" "interrupt" "session" session "interrupt-id" 9 "id" 10})
|
||||||
|
(is (contains? (set (:status (read-reply in session 10))) "done"))))))
|
||||||
|
|
||||||
|
(deftest nrepl-server-test
|
||||||
|
(let [proc-state (atom nil)]
|
||||||
|
(try
|
||||||
|
(if tu/jvm?
|
||||||
|
(future
|
||||||
|
(start-server!
|
||||||
|
(init {:namespaces {'cheshire.core {'generate-string cheshire/generate-string}}
|
||||||
|
:features #{:bb}}) "0.0.0.0:1667"))
|
||||||
|
(let [pb (ProcessBuilder. ["./bb" "--nrepl-server" "0.0.0.0:1667"])
|
||||||
|
_ (.redirectError pb ProcessBuilder$Redirect/INHERIT)
|
||||||
|
;; _ (.redirectOutput pb ProcessBuilder$Redirect/INHERIT)
|
||||||
|
;; env (.environment pb)
|
||||||
|
;; _ (.put env "BABASHKA_DEV" "true")
|
||||||
|
proc (.start pb)]
|
||||||
|
(reset! proc-state proc)))
|
||||||
|
(babashka.wait/wait-for-port "localhost" 1667)
|
||||||
|
(nrepl-test)
|
||||||
|
(finally
|
||||||
|
(if tu/jvm?
|
||||||
|
(stop-server!)
|
||||||
|
(when-let [proc @proc-state]
|
||||||
|
(.destroy ^Process proc)))))))
|
||||||
|
|
||||||
|
;;;; Scratch
|
||||||
|
|
||||||
|
(comment
|
||||||
|
)
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
(:require
|
(:require
|
||||||
[babashka.impl.socket-repl :refer [start-repl! stop-repl!]]
|
[babashka.impl.socket-repl :refer [start-repl! stop-repl!]]
|
||||||
[babashka.test-utils :as tu]
|
[babashka.test-utils :as tu]
|
||||||
|
[clojure.java.io :as io]
|
||||||
[clojure.java.shell :refer [sh]]
|
[clojure.java.shell :refer [sh]]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[clojure.test :as t :refer [deftest is testing]]
|
[clojure.test :as t :refer [deftest is testing]]
|
||||||
[clojure.java.io :as io]
|
|
||||||
[sci.impl.opts :refer [init]]))
|
[sci.impl.opts :refer [init]]))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue