nandi/frqpublic Fork 0
468684e
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Read the syscall result out of the pair jolt now returns

accept, recv and send are declared :capture-native-error in jolt.socket, so
they answer [result errno] rather than a bare number. Asking whether that pair
is positive is the ClassCastException OAuth died on — PersistentVector cannot
be cast to Number, on the browser's first connection to the loopback capture
page, so sign-in never got past waiting for the browser. The plain-socket read
in frq.irc had the same break, and send-all! the same latent one.

They go through jolt's own io-call now, which spends the errno on telling
EINTR and EAGAIN from a real failure and hands back the number the callers
were always reading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandi committed 2026-09-10T09:10:24-07:00 Browse files
468684e parent: 106cedd
modified src/frq/irc.clj +1 -1
@@ -139,7 +139,7 @@
139139 (let [b (try (tls/tls-read (:tls conn)) (catch Exception _ nil))]
140140 (when (and b (pos? (count b))) (String. b)))
141141 (let [buf (:buf conn)
142- n (try (socket/c-recv (:fd conn) buf buffer-size 0) (catch Exception _ -1))]
142+ n (try (wire/recv! (:fd conn) buf buffer-size) (catch Exception _ -1))]
143143 (when (and n (pos? n)) (String. (ffi/read-bytes buf n))))))
144144
145145 (defn send-line!
@@ -139,7 +139,7 @@
139 (let [b (try (tls/tls-read (:tls conn)) (catch Exception _ nil))]139 (let [b (try (tls/tls-read (:tls conn)) (catch Exception _ nil))]
140 (when (and b (pos? (count b))) (String. b)))140 (when (and b (pos? (count b))) (String. b)))
141 (let [buf (:buf conn)141 (let [buf (:buf conn)
142- n (try (socket/c-recv (:fd conn) buf buffer-size 0) (catch Exception _ -1))]142+ n (try (wire/recv! (:fd conn) buf buffer-size) (catch Exception _ -1))]
143 (when (and n (pos? n)) (String. (ffi/read-bytes buf n))))))143 (when (and n (pos? n)) (String. (ffi/read-bytes buf n))))))
144 144
145 (defn send-line!145 (defn send-line!
modified src/frq/oauth.clj +2 -8
@@ -20,12 +20,6 @@
2020
2121 (def default-broker "https://auth.freeq.at")
2222
23-;; MSG_NOSIGNAL. A browser opens more connections to a page than it reads —
24-;; favicon, preconnect, prefetch — and closes them without ceremony. Writing to
25-;; one of those raises SIGPIPE, which with no handler installed ends the
26-;; process: the app looked like it wedged the moment the redirect arrived.
27-(def ^:private no-signal @#'socket/msg-nosignal)
28-
2923 ;; ------------------------------------------------------------------ urls
3024
3125 (defn url-encode
@@ -100,7 +94,7 @@
10094
10195 (defn- read-request [fd]
10296 (let [buf (ffi/alloc 16384)
103- n (try (socket/c-recv fd buf 16384 no-signal) (catch Exception _ -1))]
97+ n (try (wire/recv! fd buf 16384) (catch Exception _ -1))]
10498 (if (and n (pos? n)) (String. (ffi/read-bytes buf n)) "")))
10599
106100 (defn- bind-loopback!
@@ -143,7 +137,7 @@
143137 (try
144138 (on-url url)
145139 (loop []
146- (let [fd (socket/c-accept server ffi/null ffi/null)]
140+ (let [fd (wire/accept! server)]
147141 (if (neg? fd)
148142 (throw (ex-info "Loopback accept failed" {:port port}))
149143 (let [req (read-request fd)
@@ -20,12 +20,6 @@
20 20
21 (def default-broker "https://auth.freeq.at")21 (def default-broker "https://auth.freeq.at")
22 22
23-;; MSG_NOSIGNAL. A browser opens more connections to a page than it reads —
24-;; favicon, preconnect, prefetch — and closes them without ceremony. Writing to
25-;; one of those raises SIGPIPE, which with no handler installed ends the
26-;; process: the app looked like it wedged the moment the redirect arrived.
27-(def ^:private no-signal @#'socket/msg-nosignal)
28-
29 ;; ------------------------------------------------------------------ urls23 ;; ------------------------------------------------------------------ urls
30 24
31 (defn url-encode25 (defn url-encode
@@ -100,7 +94,7 @@
100 94
101 (defn- read-request [fd]95 (defn- read-request [fd]
102 (let [buf (ffi/alloc 16384)96 (let [buf (ffi/alloc 16384)
103- n (try (socket/c-recv fd buf 16384 no-signal) (catch Exception _ -1))]97+ n (try (wire/recv! fd buf 16384) (catch Exception _ -1))]
104 (if (and n (pos? n)) (String. (ffi/read-bytes buf n)) "")))98 (if (and n (pos? n)) (String. (ffi/read-bytes buf n)) "")))
105 99
106 (defn- bind-loopback!100 (defn- bind-loopback!
@@ -143,7 +137,7 @@
143 (try137 (try
144 (on-url url)138 (on-url url)
145 (loop []139 (loop []
146- (let [fd (socket/c-accept server ffi/null ffi/null)]140+ (let [fd (wire/accept! server)]
147 (if (neg? fd)141 (if (neg? fd)
148 (throw (ex-info "Loopback accept failed" {:port port}))142 (throw (ex-info "Loopback accept failed" {:port port}))
149 (let [req (read-request fd)143 (let [req (read-request fd)
modified src/frq/wire.clj +27 -5
@@ -21,6 +21,28 @@
2121 call sites already did the value is the platform's, not frq's."
2222 @#'socket/msg-nosignal)
2323
24+(def ^:private io-call
25+ "jolt's own retry wrapper for one blocking-capable socket syscall.
26+
27+ The `accept`/`recv`/`send` bindings are declared `:capture-native-error`, so
28+ they answer `[result errno]` rather than a bare number a pair that reads as
29+ a socket error nowhere and as `class clojure.lang.PersistentVector cannot be
30+ cast to class java.lang.Number` the moment a caller asks whether it is
31+ positive. Taken from jolt rather than unwrapped here, as with `no-signal`
32+ above: the errno is what tells EINTR and EAGAIN from a real failure, and
33+ jolt is where that classification lives."
34+ @#'socket/io-call)
35+
36+(defn recv!
37+ "One `recv` into `buf`, answering the byte count negative or zero at end."
38+ [fd buf len]
39+ (io-call #(socket/c-recv fd buf len 0) fd :read))
40+
41+(defn accept!
42+ "One `accept` on a listening fd, answering the connected fd or a negative."
43+ [fd]
44+ (io-call #(socket/c-accept fd ffi/null ffi/null) fd :read))
45+
2446 (defn send-all!
2547 "Write `text` to `fd` until none is left. Throws if the socket does.
2648
@@ -35,14 +57,14 @@
3557 (when (< sent len)
3658 ;; The pointer advances with the length. `p` is an address, so this
3759 ;; is ordinary arithmetic on it.
38- (let [n (socket/c-send fd (+ p sent) (- len sent) no-signal)]
60+ (let [n (io-call #(socket/c-send fd (+ p sent) (- len sent) no-signal)
61+ fd :write)]
3962 ;; Anything not positive ends it. Zero especially: recurring on an
4063 ;; unchanged `sent` is an infinite loop that sends nothing, which
4164 ;; is worse than the failure it is hiding.
42- ;; ponytail: EINTR is thrown rather than retried — jolt.socket
43- ;; publishes no errno, so telling it from a real error would mean
44- ;; binding __errno_location here. Worth doing if signals ever
45- ;; start interrupting these writes in practice.
65+ ;; EINTR and EAGAIN are already gone by here — io-call retries
66+ ;; the one and waits out the other — so a non-positive n is the
67+ ;; socket's final answer.
4668 (when-not (pos? n)
4769 (throw (ex-info "send failed" {:fd fd :sent sent :len len :ret n})))
4870 (recur (+ sent n))))))))
@@ -21,6 +21,28 @@
21 call sites already did the value is the platform's, not frq's."21 call sites already did the value is the platform's, not frq's."
22 @#'socket/msg-nosignal)22 @#'socket/msg-nosignal)
23 23
24+(def ^:private io-call
25+ "jolt's own retry wrapper for one blocking-capable socket syscall.
26+
27+ The `accept`/`recv`/`send` bindings are declared `:capture-native-error`, so
28+ they answer `[result errno]` rather than a bare number a pair that reads as
29+ a socket error nowhere and as `class clojure.lang.PersistentVector cannot be
30+ cast to class java.lang.Number` the moment a caller asks whether it is
31+ positive. Taken from jolt rather than unwrapped here, as with `no-signal`
32+ above: the errno is what tells EINTR and EAGAIN from a real failure, and
33+ jolt is where that classification lives."
34+ @#'socket/io-call)
35+
36+(defn recv!
37+ "One `recv` into `buf`, answering the byte count negative or zero at end."
38+ [fd buf len]
39+ (io-call #(socket/c-recv fd buf len 0) fd :read))
40+
41+(defn accept!
42+ "One `accept` on a listening fd, answering the connected fd or a negative."
43+ [fd]
44+ (io-call #(socket/c-accept fd ffi/null ffi/null) fd :read))
45+
24 (defn send-all!46 (defn send-all!
25 "Write `text` to `fd` until none is left. Throws if the socket does.47 "Write `text` to `fd` until none is left. Throws if the socket does.
26 48
@@ -35,14 +57,14 @@
35 (when (< sent len)57 (when (< sent len)
36 ;; The pointer advances with the length. `p` is an address, so this58 ;; The pointer advances with the length. `p` is an address, so this
37 ;; is ordinary arithmetic on it.59 ;; is ordinary arithmetic on it.
38- (let [n (socket/c-send fd (+ p sent) (- len sent) no-signal)]60+ (let [n (io-call #(socket/c-send fd (+ p sent) (- len sent) no-signal)
61+ fd :write)]
39 ;; Anything not positive ends it. Zero especially: recurring on an62 ;; Anything not positive ends it. Zero especially: recurring on an
40 ;; unchanged `sent` is an infinite loop that sends nothing, which63 ;; unchanged `sent` is an infinite loop that sends nothing, which
41 ;; is worse than the failure it is hiding.64 ;; is worse than the failure it is hiding.
42- ;; ponytail: EINTR is thrown rather than retried — jolt.socket65+ ;; EINTR and EAGAIN are already gone by here — io-call retries
43- ;; publishes no errno, so telling it from a real error would mean66+ ;; the one and waits out the other — so a non-positive n is the
44- ;; binding __errno_location here. Worth doing if signals ever67+ ;; socket's final answer.
45- ;; start interrupting these writes in practice.
46 (when-not (pos? n)68 (when-not (pos? n)
47 (throw (ex-info "send failed" {:fd fd :sent sent :len len :ret n})))69 (throw (ex-info "send failed" {:fd fd :sent sent :len len :ret n})))
48 (recur (+ sent n))))))))70 (recur (+ sent n))))))))