Current section

Files

Jump to
clojerl src clj clojure core server.clje
Raw

src/clj/clojure/core/server.clje

; 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"}
clojure.core.server
(:require [clojure.string :as str]
[clojure.edn :as edn]
[clojure.main :as m])
(:import [clojerl String]
[erlang.io PushbackReader]))
(set! *warn-on-infer* true)
(def ^:dynamic *session* nil)
(defonce ^:private servers (atom {}))
(defmacro process [& body]
`(erlang/spawn (fn* [] ~@body)))
(defn- required
"Throw if opts does not contain prop."
[opts prop]
(when (nil? (get opts prop))
(throw (ex-info (str "Missing required socket server property " prop) opts))))
(defn- validate-opts
"Validate server config options"
[{:keys [name port accept] :as opts}]
(doseq [prop [:name :port :accept]] (required opts prop))
(when (or (not (integer? port)) (not (< -1 port 65535)))
(throw (ex-info (str "Invalid socket server port: " port) opts))))
(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"
[conn name client-id in out err accept args]
(try
(binding [*in* in
*out* out
*err* err
*session* {:server name :client client-id}]
(swap! servers assoc-in [name :sessions client-id] {})
(require (symbol (namespace accept)))
(let [accept-fn (resolve accept)]
(apply accept-fn args)))
(catch _ disconnect)
(finally
(swap! servers update-in [name :sessions] dissoc client-id)
(gen_tcp/close conn))))
(defn* unwrap-result
([#erl[:ok res]] res)
([#erl[:error error]] (throw error))
([res] res))
(defn* getaddr
([nil]
(getaddr "localhost"))
([address]
(-> address
erlang/binary_to_list
(inet/getaddr :inet)
unwrap-result)))
(defn listen [address port]
(let [opts #erl(:binary #erl[:ip address] #erl[:active false])]
(unwrap-result (gen_tcp/listen port opts))))
(defn accept-socket [socket]
(unwrap-result (gen_tcp/accept socket)))
(deftype SocketWriter [conn]
erlang.io.IWriter
(write [this data]
(unwrap-result (gen_tcp/send conn data)))
(write [this format values]
(let [data (->> values (io_lib/format format) (erlang/iolist_to_binary.1))]
(unwrap-result (gen_tcp/send conn data)))))
(deftype SocketReader [conn]
erlang.io.IReader
(read [this]
(unwrap-result (gen_tcp/recv conn 1)))
(read [this length]
(unwrap-result (gen_tcp/recv conn length :infinity)))
(read_line [this]
(loop [line ""]
(let [char (unwrap-result (gen_tcp/recv conn 1))]
(if (= char "\n")
(str line "\n")
(recur (str line char))))))
(skip [this length]
(let [skipped (unwrap-result (gen_tcp/recv conn length))]
(count skipped))))
(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]
(validate-opts 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 (getaddr address) ;; nil returns loopback
socket (listen address port)]
(swap! servers assoc name {:name name, :socket socket, :sessions {}})
(process
(try
(loop [client-counter 1]
(when (erlang/port_info socket)
(try
(let [conn (accept-socket socket)
in (new PushbackReader (new SocketReader conn))
out (new SocketWriter conn)
client-id (str client-counter)]
(process
(accept-connection conn name client-id in out (if bind-err out *err*) accept args)))
(catch _ disconnect))
(recur (inc client-counter))))
(finally
(swap! servers dissoc name))))
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."
([]
(stop-server (:server *session*)))
([name]
(let [server-socket (get-in @servers [name :socket])]
(when server-socket
(swap! servers dissoc name)
(gen_tcp/close server-socket)
true))))
(defn stop-servers
"Stop all servers ignores all errors, and returns nil."
[]
(doseq [name (keys @servers)]
(future (stop-server name))))
(defn- parse-props
"Parse clojure.server.* from properties to produce a map of server configs."
[props]
(reduce
(fn [acc [^String k ^String v]]
(let [[k1 k2 k3] (str/split k #"\.")]
(if (and (= k1 "clojure") (= k2 "server"))
(conj acc (merge {:name k3} (edn/read-string v)))
acc)))
[] props))
(defn start-servers
"Start all servers specified in the system properties."
[system-props]
(doseq [server (parse-props system-props)]
(start-server server)))
(defn repl-init
"Initialize repl in user namespace and make standard repl requires."
[]
(in-ns 'clje.user)
(apply require clojure.main/repl-requires))
(defn repl-read
"Enhanced :read hook for repl supporting :repl/quit."
[request-prompt request-exit]
(or ({:line-start request-prompt :stream-end request-exit}
(m/skip-whitespace *in*))
(let [input (read {:read-cond :allow} *in*)]
(m/skip-if-eol *in*)
(case input
:repl/quit request-exit
input))))
(defn repl
"REPL with predefined hooks for attachable socket server."
[]
(m/repl
:init repl-init
:read repl-read))