Dialling is a blocking call too
`⚠ Interrupted system call` on pressing Connect, still. The retries went on the reads and the writes and both HTTPS paths, and not on the one call that happens first: `connect`, which is a DNS lookup, a TCP connect and a TLS handshake, every one of them interruptible and all of them before a byte has been read. The whole socket setup is inside the retry rather than the connect alone, because a socket that failed part way through cannot be dialled again — each attempt starts with a new one and closes the last. Two things came out of looking at this properly. A failed refresh no longer throws the saved sign-in away unless the broker is the thing saying no. It used to drop the token on any exception at all, so a dropped wifi or a stray signal cost the reader their session and sent them back to a browser. Only an `OauthError` — the broker refusing the token — means the token is bad; everything else is the network, and the token is still good. And errors out of the socket thread now say what could not be done. "Interrupted system call" on its own names no call, which is why this took two rounds of guessing: it now reads "Could not reach irc.freeq.at:6697 — …", and the sign-in path says the broker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
91b0e1e parent: 4083860 modified
nim/src/frq/conn.nim +24 -7 | @@ -75,12 +75,24 @@ proc readerBody(cfg: ConnConfig) {.thread.} = | ||
| 75 | 75 | try: |
| 76 | 76 | trace("conn", "dialling " & cfg.host & ":" & $cfg.port & |
| 77 | 77 | (if cfg.tls: " over TLS" else: " plain")) |
| 78 | - var sock = newSocket(buffered = true) | |
| 79 | - if cfg.tls: | |
| 80 | - # CVerifyPeer: this carries a nick and, once SASL is wired, a token. | |
| 81 | - let ctx = newContext(verifyMode = CVerifyPeer) | |
| 82 | - ctx.wrapSocket(sock) | |
| 83 | - sock.connect(cfg.host, Port(cfg.port)) | |
| 78 | + # Dialling is the first blocking call a connection makes and the one | |
| 79 | + # most likely to be cut short: a DNS lookup, a TCP connect and a TLS | |
| 80 | + # handshake, all of them interruptible, all of them before anything has | |
| 81 | + # been read. `⚠ Interrupted system call` on pressing Connect was this. | |
| 82 | + # | |
| 83 | + # The whole setup is inside the retry rather than the connect alone: a | |
| 84 | + # socket that failed part way through cannot be dialled again, so each | |
| 85 | + # attempt starts with a new one and closes the last. | |
| 86 | + var sock: Socket | |
| 87 | + retrying 3: | |
| 88 | + if not sock.isNil: | |
| 89 | + try: sock.close() except CatchableError: discard | |
| 90 | + sock = newSocket(buffered = true) | |
| 91 | + if cfg.tls: | |
| 92 | + # CVerifyPeer: this carries a nick and a token. | |
| 93 | + let ctx = newContext(verifyMode = CVerifyPeer) | |
| 94 | + ctx.wrapSocket(sock) | |
| 95 | + sock.connect(cfg.host, Port(cfg.port)) | |
| 84 | 96 | shared = sock |
| 85 | 97 | createThread(writer, writerBody, 0) |
| 86 | 98 | events.send("open") |
| @@ -105,8 +117,13 @@ proc readerBody(cfg: ConnConfig) {.thread.} = | ||
| 105 | 117 | inbound.send(line) |
| 106 | 118 | |
| 107 | 119 | except CatchableError as e: |
| 120 | + # Named for what failed. Every error out of this thread used to arrive | |
| 121 | + # on the connect screen as a bare OS message — "Interrupted system | |
| 122 | + # call" on its own says nothing about which call, and it took two | |
| 123 | + # rounds of guessing to find out. | |
| 108 | 124 | trace("conn", "!! " & e.msg) |
| 109 | - events.send("error: " & e.msg) | |
| 125 | + events.send("error: Could not reach " & cfg.host & ":" & $cfg.port & | |
| 126 | + " — " & e.msg) | |
| 110 | 127 | finally: |
| 111 | 128 | running = false |
| 112 | 129 | # Unblock the writer, which is sitting in `outbound.recv()`. |
| @@ -75,12 +75,24 @@ proc readerBody(cfg: ConnConfig) {.thread.} = | |||
| 75 | try: | 75 | try: |
| 76 | trace("conn", "dialling " & cfg.host & ":" & $cfg.port & | 76 | trace("conn", "dialling " & cfg.host & ":" & $cfg.port & |
| 77 | (if cfg.tls: " over TLS" else: " plain")) | 77 | (if cfg.tls: " over TLS" else: " plain")) |
| 78 | - var sock = newSocket(buffered = true) | 78 | + # Dialling is the first blocking call a connection makes and the one |
| 79 | - if cfg.tls: | 79 | + # most likely to be cut short: a DNS lookup, a TCP connect and a TLS |
| 80 | - # CVerifyPeer: this carries a nick and, once SASL is wired, a token. | 80 | + # handshake, all of them interruptible, all of them before anything has |
| 81 | - let ctx = newContext(verifyMode = CVerifyPeer) | 81 | + # been read. `⚠ Interrupted system call` on pressing Connect was this. |
| 82 | - ctx.wrapSocket(sock) | 82 | + # |
| 83 | - sock.connect(cfg.host, Port(cfg.port)) | 83 | + # The whole setup is inside the retry rather than the connect alone: a |
| 84 | + # socket that failed part way through cannot be dialled again, so each | ||
| 85 | + # attempt starts with a new one and closes the last. | ||
| 86 | + var sock: Socket | ||
| 87 | + retrying 3: | ||
| 88 | + if not sock.isNil: | ||
| 89 | + try: sock.close() except CatchableError: discard | ||
| 90 | + sock = newSocket(buffered = true) | ||
| 91 | + if cfg.tls: | ||
| 92 | + # CVerifyPeer: this carries a nick and a token. | ||
| 93 | + let ctx = newContext(verifyMode = CVerifyPeer) | ||
| 94 | + ctx.wrapSocket(sock) | ||
| 95 | + sock.connect(cfg.host, Port(cfg.port)) | ||
| 84 | shared = sock | 96 | shared = sock |
| 85 | createThread(writer, writerBody, 0) | 97 | createThread(writer, writerBody, 0) |
| 86 | events.send("open") | 98 | events.send("open") |
| @@ -105,8 +117,13 @@ proc readerBody(cfg: ConnConfig) {.thread.} = | |||
| 105 | inbound.send(line) | 117 | inbound.send(line) |
| 106 | 118 | ||
| 107 | except CatchableError as e: | 119 | except CatchableError as e: |
| 120 | + # Named for what failed. Every error out of this thread used to arrive | ||
| 121 | + # on the connect screen as a bare OS message — "Interrupted system | ||
| 122 | + # call" on its own says nothing about which call, and it took two | ||
| 123 | + # rounds of guessing to find out. | ||
| 108 | trace("conn", "!! " & e.msg) | 124 | trace("conn", "!! " & e.msg) |
| 109 | - events.send("error: " & e.msg) | 125 | + events.send("error: Could not reach " & cfg.host & ":" & $cfg.port & |
| 126 | + " — " & e.msg) | ||
| 110 | finally: | 127 | finally: |
| 111 | running = false | 128 | running = false |
| 112 | # Unblock the writer, which is sitting in `outbound.recv()`. | 129 | # Unblock the writer, which is sitting in `outbound.recv()`. |
modified
nim/src/frq/reducer.nim +15 -5 | @@ -228,16 +228,26 @@ proc signIn(): bool = | ||
| 228 | 228 | app.status = "Refreshing your sign-in…" |
| 229 | 229 | adoptTokens(oa.refreshSession(oa.defaultBroker, app.brokerToken)) |
| 230 | 230 | true |
| 231 | - except CatchableError as e: | |
| 232 | - # A token the broker no longer honours is worse than none: every | |
| 233 | - # Connect would spend a round trip failing the same way. Dropped, and | |
| 234 | - # the next press opens the browser. | |
| 235 | - trace("oauth", "refresh failed: " & e.msg) | |
| 231 | + except oa.OauthError as e: | |
| 232 | + # The broker itself saying no. A token it no longer honours is worse | |
| 233 | + # than none — every Connect would spend a round trip failing the same | |
| 234 | + # way — so it goes, and the next press opens the browser. | |
| 235 | + trace("oauth", "refresh refused: " & e.msg) | |
| 236 | 236 | app.brokerToken = "" |
| 237 | 237 | clearSession() |
| 238 | 238 | setError(e.msg) |
| 239 | 239 | app.connecting = false |
| 240 | 240 | false |
| 241 | + except CatchableError as e: | |
| 242 | + # Anything else is the network, not the answer: a name that did not | |
| 243 | + # resolve, a connection that did not open, a syscall a signal cut | |
| 244 | + # short. The token is still good and is kept — throwing it away here | |
| 245 | + # meant a dropped wifi or a stray SIGPROF cost the reader their saved | |
| 246 | + # sign-in and sent them back to a browser. | |
| 247 | + trace("oauth", "refresh failed: " & e.msg) | |
| 248 | + setError("Could not reach the broker — " & e.msg) | |
| 249 | + app.connecting = false | |
| 250 | + false | |
| 241 | 251 | |
| 242 | 252 | proc openSocket() = |
| 243 | 253 | ## The connection itself, with whoever we are already settled. |
| @@ -228,16 +228,26 @@ proc signIn(): bool = | |||
| 228 | app.status = "Refreshing your sign-in…" | 228 | app.status = "Refreshing your sign-in…" |
| 229 | adoptTokens(oa.refreshSession(oa.defaultBroker, app.brokerToken)) | 229 | adoptTokens(oa.refreshSession(oa.defaultBroker, app.brokerToken)) |
| 230 | true | 230 | true |
| 231 | - except CatchableError as e: | 231 | + except oa.OauthError as e: |
| 232 | - # A token the broker no longer honours is worse than none: every | 232 | + # The broker itself saying no. A token it no longer honours is worse |
| 233 | - # Connect would spend a round trip failing the same way. Dropped, and | 233 | + # than none — every Connect would spend a round trip failing the same |
| 234 | - # the next press opens the browser. | 234 | + # way — so it goes, and the next press opens the browser. |
| 235 | - trace("oauth", "refresh failed: " & e.msg) | 235 | + trace("oauth", "refresh refused: " & e.msg) |
| 236 | app.brokerToken = "" | 236 | app.brokerToken = "" |
| 237 | clearSession() | 237 | clearSession() |
| 238 | setError(e.msg) | 238 | setError(e.msg) |
| 239 | app.connecting = false | 239 | app.connecting = false |
| 240 | false | 240 | false |
| 241 | + except CatchableError as e: | ||
| 242 | + # Anything else is the network, not the answer: a name that did not | ||
| 243 | + # resolve, a connection that did not open, a syscall a signal cut | ||
| 244 | + # short. The token is still good and is kept — throwing it away here | ||
| 245 | + # meant a dropped wifi or a stray SIGPROF cost the reader their saved | ||
| 246 | + # sign-in and sent them back to a browser. | ||
| 247 | + trace("oauth", "refresh failed: " & e.msg) | ||
| 248 | + setError("Could not reach the broker — " & e.msg) | ||
| 249 | + app.connecting = false | ||
| 250 | + false | ||
| 241 | 251 | ||
| 242 | proc openSocket() = | 252 | proc openSocket() = |
| 243 | ## The connection itself, with whoever we are already settled. | 253 | ## The connection itself, with whoever we are already settled. |