| Send every byte of a short write, from the right offset 7bc4770 nandi 11d ago | 1 | (ns frq.wire |
| 2 | "Bytes onto a raw socket, all of them. |
| 3 | |
| 4 | `send(2)` is allowed to accept less than it was given and say so, which is |
| 5 | not an error and not rare — it is what a full socket buffer looks like. The |
| 6 | caller has to resume from where it stopped, and the only way to say that to |
| 7 | the kernel is a pointer further along the buffer: shrinking the length while |
| 8 | passing the same address re-sends the head of the line and loses the tail. |
| 9 | The peer then gets the right number of bytes, the wrong ones, and a stream |
| 10 | that no longer frames. |
| 11 | |
| 12 | Both raw-socket writers in frq had their own version of this loop and both |
| 13 | had it wrong, so there is one here instead. TLS does not come through — that |
| 14 | transport is `tls/tls-write`, which handles its own record boundaries." |
| 15 | (:require [jolt.ffi :as ffi] |
| 16 | [jolt.socket :as socket])) |
| 17 | |
| 18 | (def ^:private no-signal |
| 19 | "MSG_NOSIGNAL: a write to a closed peer returns EPIPE rather than killing |
| 20 | the process with SIGPIPE. Read from jolt rather than spelled here, as both |
| 21 | call sites already did — the value is the platform's, not frq's." |
| 22 | @#'socket/msg-nosignal) |
| 23 | |
| Read the syscall result out of the pair jolt now returns 468684e nandi 10d ago | 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 | |
| Take connect through io-call with the other three 77c1a08 nandi 10d ago | 41 | (defn connect! |
| 42 | "One `connect` to the address at `sa`, answering zero or a negative. |
| 43 | |
| 44 | `connect` is the fourth of these bindings and was left out of the round |
| 45 | that fixed the other three: it is only on the plain-socket path, which the |
| 46 | TLS default does not take, so its pair reached `neg?` unnoticed. The wait |
| 47 | is for writability — a socket that finishes connecting reports itself |
| 48 | writable, which is what jolt's poller is being asked about here." |
| 49 | [fd sa len] |
| 50 | (io-call #(socket/c-connect fd sa len) fd :write)) |
| 51 | |
| Read the syscall result out of the pair jolt now returns 468684e nandi 10d ago | 52 | (defn accept! |
| 53 | "One `accept` on a listening fd, answering the connected fd or a negative." |
| 54 | [fd] |
| 55 | (io-call #(socket/c-accept fd ffi/null ffi/null) fd :read)) |
| 56 | |
| Send every byte of a short write, from the right offset 7bc4770 nandi 11d ago | 57 | (defn send-all! |
| 58 | "Write `text` to `fd` until none is left. Throws if the socket does. |
| 59 | |
| 60 | The byte count is taken as UTF-8 explicitly, because that is what |
| 61 | `with-c-string` writes: the platform default agrees on every machine frq |
| 62 | has run on, but a machine where it did not would send a length measured in |
| 63 | one encoding against bytes laid down in another." |
| 64 | [fd text] |
| 65 | (let [len (count (.getBytes ^String text "UTF-8"))] |
| 66 | (ffi/with-c-string [p text] |
| 67 | (loop [sent 0] |
| 68 | (when (< sent len) |
| 69 | ;; The pointer advances with the length. `p` is an address, so this |
| 70 | ;; is ordinary arithmetic on it. |
| Read the syscall result out of the pair jolt now returns 468684e nandi 10d ago | 71 | (let [n (io-call #(socket/c-send fd (+ p sent) (- len sent) no-signal) |
| 72 | fd :write)] |
| Send every byte of a short write, from the right offset 7bc4770 nandi 11d ago | 73 | ;; Anything not positive ends it. Zero especially: recurring on an |
| 74 | ;; unchanged `sent` is an infinite loop that sends nothing, which |
| 75 | ;; is worse than the failure it is hiding. |
| Read the syscall result out of the pair jolt now returns 468684e nandi 10d ago | 76 | ;; EINTR and EAGAIN are already gone by here — io-call retries |
| 77 | ;; the one and waits out the other — so a non-positive n is the |
| 78 | ;; socket's final answer. |
| Send every byte of a short write, from the right offset 7bc4770 nandi 11d ago | 79 | (when-not (pos? n) |
| 80 | (throw (ex-info "send failed" {:fd fd :sent sent :len len :ret n}))) |
| 81 | (recur (+ sent n)))))))) |