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

View file

@ -6,13 +6,17 @@
(ns app.srepl
"Server Repl."
(:refer-clojure :exclude [read-line])
(:require
[app.common.exceptions :as ex]
[app.common.json :as json]
[app.common.logging :as l]
[app.config :as cf]
[app.srepl.cli]
[app.srepl.cli :as cli]
[app.srepl.main]
[app.util.json :as json]
[app.util.locks :as locks]
[app.util.time :as dt]
[clojure.core :as c]
[clojure.core.server :as ccs]
[clojure.main :as cm]
[integrant.core :as ig]))
@ -28,17 +32,80 @@
:init repl-init
: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
[]
(let [out *out*
lock (locks/create)]
(ccs/prepl *in*
(fn [m]
(binding [*out* out,
*flush-on-newline* true,
*print-readably* true]
(locks/locking lock
(println (json/encode-str m))))))))
(let [lock (locks/create)
out *out*
out-fn
(fn [m]
(locks/locking lock
(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

View file

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