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>
468684e parent: 106cedd modified
src/frq/irc.clj +1 -1 | @@ -139,7 +139,7 @@ | ||
| 139 | 139 | (let [b (try (tls/tls-read (:tls conn)) (catch Exception _ nil))] |
| 140 | 140 | (when (and b (pos? (count b))) (String. b))) |
| 141 | 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 | 143 | (when (and n (pos? n)) (String. (ffi/read-bytes buf n)))))) |
| 144 | 144 | |
| 145 | 145 | (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 @@ | ||
| 20 | 20 | |
| 21 | 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 | 23 | ;; ------------------------------------------------------------------ urls |
| 30 | 24 | |
| 31 | 25 | (defn url-encode |
| @@ -100,7 +94,7 @@ | ||
| 100 | 94 | |
| 101 | 95 | (defn- read-request [fd] |
| 102 | 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 | 98 | (if (and n (pos? n)) (String. (ffi/read-bytes buf n)) ""))) |
| 105 | 99 | |
| 106 | 100 | (defn- bind-loopback! |
| @@ -143,7 +137,7 @@ | ||
| 143 | 137 | (try |
| 144 | 138 | (on-url url) |
| 145 | 139 | (loop [] |
| 146 | - (let [fd (socket/c-accept server ffi/null ffi/null)] | |
| 140 | + (let [fd (wire/accept! server)] | |
| 147 | 141 | (if (neg? fd) |
| 148 | 142 | (throw (ex-info "Loopback accept failed" {:port port})) |
| 149 | 143 | (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 | ;; ------------------------------------------------------------------ urls | 23 | ;; ------------------------------------------------------------------ urls |
| 30 | 24 | ||
| 31 | (defn url-encode | 25 | (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 | (try | 137 | (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 @@ | ||
| 21 | 21 | call sites already did — the value is the platform's, not frq's." |
| 22 | 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 | 46 | (defn send-all! |
| 25 | 47 | "Write `text` to `fd` until none is left. Throws if the socket does. |
| 26 | 48 | |
| @@ -35,14 +57,14 @@ | ||
| 35 | 57 | (when (< sent len) |
| 36 | 58 | ;; The pointer advances with the length. `p` is an address, so this |
| 37 | 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 | 62 | ;; Anything not positive ends it. Zero especially: recurring on an |
| 40 | 63 | ;; unchanged `sent` is an infinite loop that sends nothing, which |
| 41 | 64 | ;; 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. | |
| 46 | 68 | (when-not (pos? n) |
| 47 | 69 | (throw (ex-info "send failed" {:fd fd :sent sent :len len :ret n}))) |
| 48 | 70 | (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 this | 58 | ;; 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 an | 62 | ;; Anything not positive ends it. Zero especially: recurring on an |
| 40 | ;; unchanged `sent` is an infinite loop that sends nothing, which | 63 | ;; 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.socket | 65 | + ;; EINTR and EAGAIN are already gone by here — io-call retries |
| 43 | - ;; publishes no errno, so telling it from a real error would mean | 66 | + ;; the one and waits out the other — so a non-positive n is the |
| 44 | - ;; binding __errno_location here. Worth doing if signals ever | 67 | + ;; 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)))))))) |