A websocket implementation for zig
0

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #103 from privkeyio/fix/tls-read-timeout-poll

client: poll for read readiness instead of SO_RCVTIMEO

+30 -3
+30 -3
src/client/client.zig
··· 286 286 return self.stream.writeTimeout(ms); 287 287 } 288 288 289 - pub fn readTimeout(self: *const Client, ms: u32) !void { 289 + pub fn readTimeout(self: *Client, ms: u32) !void { 290 290 return self.stream.readTimeout(ms); 291 291 } 292 292 ··· 400 400 io: Io, 401 401 stream: Io.net.Stream, 402 402 tls_client: ?*TLSClient = null, 403 + read_timeout_ms: u32 = 0, 403 404 404 405 pub fn init(io: Io, stream: Io.net.Stream, tls_client: ?*TLSClient) Stream { 405 406 return .{ ··· 441 442 } 442 443 443 444 pub fn read(self: *Stream, buf: []u8) !usize { 445 + // A read timeout is implemented by polling the socket for readiness 446 + // rather than SO_RCVTIMEO. On the TLS path the underlying read goes 447 + // through std.crypto.tls, whose reader treats a socket EAGAIN (what 448 + // SO_RCVTIMEO produces on timeout) as a programmer bug and panics in 449 + // debug builds. Polling lets a timeout surface as error.WouldBlock (which 450 + // read() turns into "no message") without ever issuing a read that could 451 + // EAGAIN. 452 + // 453 + // Skip the poll when the TLS client already has decrypted plaintext 454 + // buffered: a previous read can decrypt more than the caller consumed, so 455 + // the socket can be empty (poll would time out) even though data is 456 + // available in-process, which would starve it. 457 + const tls_buffered = if (self.tls_client) |tls_client| tls_client.client.reader.bufferedLen() else 0; 458 + if (self.read_timeout_ms > 0 and tls_buffered == 0) { 459 + var pfd = [_]std.posix.pollfd{.{ 460 + .fd = self.stream.socket.handle, 461 + .events = std.posix.POLL.IN, 462 + .revents = 0, 463 + }}; 464 + // A poll failure is a real read failure, not "no data": surface it 465 + // (mapped into this read path's error set) rather than swallowing it. 466 + const ready = std.posix.poll(&pfd, @intCast(self.read_timeout_ms)) catch return error.ReadFailed; 467 + if (ready == 0) return error.WouldBlock; 468 + } 444 469 if (self.tls_client) |tls_client| { 445 470 var w: std.Io.Writer = .fixed(buf); 446 471 while (true) { ··· 473 498 return self.setTimeout(posix.SO.SNDTIMEO, ms); 474 499 } 475 500 476 - pub fn readTimeout(self: *const Stream, ms: u32) !void { 477 - return self.setTimeout(posix.SO.RCVTIMEO, ms); 501 + pub fn readTimeout(self: *Stream, ms: u32) !void { 502 + // Stored and applied via poll() in read(); see the note there for why this 503 + // does not use SO_RCVTIMEO. 504 + self.read_timeout_ms = ms; 478 505 } 479 506 480 507 fn setTimeout(self: *const Stream, opt_name: u32, ms: u32) !void {