♻️ Refactor prepl interface

Make prepl to be json message based protocol
instead of clojure expression. This facilitates
implementing internal RPC over socket server.
This commit is contained in:
Andrey Antukh 2025-04-15 15:25:15 +02:00
parent 62a12a64a3
commit 2df6f2b8b1
3 changed files with 115 additions and 50 deletions

View file

@ -35,40 +35,35 @@ def get_prepl_conninfo():
return host, port return host, port
def send_eval(expr): def send(data):
host, port = get_prepl_conninfo() host, port = get_prepl_conninfo()
with socket.create_connection((host, port)) as s:
f = s.makefile(mode="rw")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: json.dump(data, f)
s.connect((host, port)) f.write("\n")
s.send(expr.encode("utf-8")) f.flush()
s.send(b":repl/quit\n\n")
with s.makefile() as f:
while True: while True:
line = f.readline() line = f.readline()
result = json.loads(line) result = json.loads(line)
tag = result.get("tag", None) tag = result.get("tag", None)
if tag == "ret": if tag == "ret":
return result.get("val", None), result.get("exception", None) return result.get("val", None), result.get("err", None)
elif tag == "out": elif tag == "out":
print(result.get("val"), end="") print(result.get("val"), end="")
else: else:
raise RuntimeError("unexpected response from PREPL") raise RuntimeError("unexpected response from PREPL")
def encode(val): def print_error(error):
return json.dumps(json.dumps(val)) print("ERR:", error["hint"])
def print_error(res):
for error in res["via"]:
print("ERR:", error["message"])
break
def run_cmd(params): def run_cmd(params):
try: try:
expr = "(app.srepl.cli/exec {})".format(encode(params)) res, err = send(params)
res, failed = send_eval(expr) if err:
if failed: print_error(err)
print_error(res)
sys.exit(-1) sys.exit(-1)
return res return res
@ -96,7 +91,7 @@ def update_profile(email, fullname, password, is_active):
"email": email, "email": email,
"fullname": fullname, "fullname": fullname,
"password": password, "password": password,
"is_active": is_active "isActive": is_active
} }
} }
@ -138,7 +133,7 @@ def derive_password(password):
params = { params = {
"cmd": "derive-password", "cmd": "derive-password",
"params": { "params": {
"password": password, "password": password
} }
} }

View file

@ -6,13 +6,17 @@
(ns app.srepl (ns app.srepl
"Server Repl." "Server Repl."
(:refer-clojure :exclude [read-line])
(:require (:require
[app.common.exceptions :as ex]
[app.common.json :as json]
[app.common.logging :as l] [app.common.logging :as l]
[app.config :as cf] [app.config :as cf]
[app.srepl.cli] [app.srepl.cli :as cli]
[app.srepl.main] [app.srepl.main]
[app.util.json :as json]
[app.util.locks :as locks] [app.util.locks :as locks]
[app.util.time :as dt]
[clojure.core :as c]
[clojure.core.server :as ccs] [clojure.core.server :as ccs]
[clojure.main :as cm] [clojure.main :as cm]
[integrant.core :as ig])) [integrant.core :as ig]))
@ -28,17 +32,80 @@
:init repl-init :init repl-init
:read ccs/repl-read)) :read ccs/repl-read))
(defn- ex->data
[cause phase]
(let [data (ex-data cause)
explain (ex/explain data)]
(cond-> {:phase phase
:code (get data :code :unknown)
:type (get data :type :unknown)
:hint (or (get data :hint) (ex-message cause))}
(some? explain)
(assoc :explain explain))))
(defn read-line
[]
(if-let [line (c/read-line)]
(try
(l/dbg :hint "decode" :data line)
(json/decode line :key-fn json/read-kebab-key)
(catch Throwable _cause
(l/warn :hint "unable to decode data" :data line)
nil))
::eof))
(defn json-repl (defn json-repl
[] []
(let [out *out* (let [lock (locks/create)
lock (locks/create)] out *out*
(ccs/prepl *in*
out-fn
(fn [m] (fn [m]
(binding [*out* out,
*flush-on-newline* true,
*print-readably* true]
(locks/locking lock (locks/locking lock
(println (json/encode-str m)))))))) (binding [*out* out]
(l/warn :hint "write" :data m)
(println (json/encode m :key-fn json/write-camel-key)))))
tapfn
(fn [val]
(out-fn {:tag :tap :val val}))]
(binding [*out* (PrintWriter-on #(out-fn {:tag :out :val %1}) nil true)
*err* (PrintWriter-on #(out-fn {:tag :err :val %1}) nil true)]
(try
(add-tap tapfn)
(loop []
(when (try
(let [data (read-line)
tpoint (dt/tpoint)]
(l/dbg :hint "received" :data (if (= data ::eof) "EOF" data))
(try
(when-not (= data ::eof)
(when-not (nil? data)
(let [result (cli/exec data)
elapsed (tpoint)]
(l/warn :hint "result" :data result)
(out-fn {:tag :ret
:val (if (instance? Throwable result)
(Throwable->map result)
result)
:elapsed (inst-ms elapsed)})))
true)
(catch Throwable cause
(let [elapsed (tpoint)]
(out-fn {:tag :ret
:err (ex->data cause :eval)
:elapsed (inst-ms elapsed)})
true))))
(catch Throwable cause
(out-fn {:tag :ret
:err (ex->data cause :read)})
true))
(recur)))
(finally
(remove-tap tapfn))))))
;; --- State initialization ;; --- State initialization

View file

@ -13,7 +13,6 @@
[app.db :as db] [app.db :as db]
[app.rpc.commands.auth :as cmd.auth] [app.rpc.commands.auth :as cmd.auth]
[app.rpc.commands.profile :as cmd.profile] [app.rpc.commands.profile :as cmd.profile]
[app.util.json :as json]
[app.util.time :as dt] [app.util.time :as dt]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -28,12 +27,11 @@
"Entry point with external tools integrations that uses PREPL "Entry point with external tools integrations that uses PREPL
interface for interacting with running penpot backend." interface for interacting with running penpot backend."
[data] [data]
(let [data (json/decode data)] (-> {::cmd (get data :cmd)}
(-> {::cmd (keyword (:cmd data "default"))}
(merge (:params data)) (merge (:params data))
(exec-command)))) (exec-command)))
(defmethod exec-command :create-profile (defmethod exec-command "create-profile"
[{:keys [fullname email password is-active] [{:keys [fullname email password is-active]
:or {is-active true}}] :or {is-active true}}]
(some-> (get-current-system) (some-> (get-current-system)
@ -49,7 +47,7 @@
(->> (cmd.auth/create-profile! conn params) (->> (cmd.auth/create-profile! conn params)
(cmd.auth/create-profile-rels! conn))))))) (cmd.auth/create-profile-rels! conn)))))))
(defmethod exec-command :update-profile (defmethod exec-command "update-profile"
[{:keys [fullname email password is-active]}] [{:keys [fullname email password is-active]}]
(some-> (get-current-system) (some-> (get-current-system)
(db/tx-run! (db/tx-run!
@ -70,7 +68,12 @@
:deleted-at nil})] :deleted-at nil})]
(pos? (db/get-update-count res))))))))) (pos? (db/get-update-count res)))))))))
(defmethod exec-command :delete-profile (defmethod exec-command "echo"
[params]
params)
(defmethod exec-command "delete-profile"
[{:keys [email soft]}] [{:keys [email soft]}]
(when-not email (when-not email
(ex/raise :type :assertion (ex/raise :type :assertion
@ -88,7 +91,7 @@
{:email email}))] {:email email}))]
(pos? (db/get-update-count res))))))) (pos? (db/get-update-count res)))))))
(defmethod exec-command :search-profile (defmethod exec-command "search-profile"
[{:keys [email]}] [{:keys [email]}]
(when-not email (when-not email
(ex/raise :type :assertion (ex/raise :type :assertion
@ -102,7 +105,7 @@
" where email similar to ? order by created_at desc limit 100")] " where email similar to ? order by created_at desc limit 100")]
(db/exec! conn [sql email])))))) (db/exec! conn [sql email]))))))
(defmethod exec-command :derive-password (defmethod exec-command "derive-password"
[{:keys [password]}] [{:keys [password]}]
(auth/derive-password password)) (auth/derive-password password))
@ -110,4 +113,4 @@
[{:keys [::cmd]}] [{:keys [::cmd]}]
(ex/raise :type :internal (ex/raise :type :internal
:code :not-implemented :code :not-implemented
:hint (str/ffmt "command '%' not implemented" (name cmd)))) :hint (str/ffmt "command '%' not implemented" cmd)))