From 2e86dc4a88d3cbbf11130622a1e51494784ce250 Mon Sep 17 00:00:00 2001 From: Glenn Thompson Date: Sun, 8 Mar 2026 11:33:55 +0300 Subject: [PATCH] Phase 3: Refactor stream-server.lisp to iolib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace usocket with iolib for socket I/O - iolib:make-socket with :connect :passive for listener - iolib:accept-connection for client connections - Add SO_KEEPALIVE for stale connection detection - Add TCP_NODELAY for low-latency streaming - Add SO_SNDTIMEO (30s) write timeout for stale client detection - Handle iolib:socket-connection-reset-error and isys:ewouldblock - Update cl-streamer.asd: usocket→iolib, drop chunga/trivial-gray-streams - Fix now-playing poll interval 5s→15s to eliminate 429 rate limit errors Runtime verified: audio streams, metadata displays, clients connect. --- cl-streamer/cl-streamer.asd | 4 +-- cl-streamer/stream-server.lisp | 58 +++++++++++++++++++++++++++------- parenscript/stream-player.lisp | 4 +-- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/cl-streamer/cl-streamer.asd b/cl-streamer/cl-streamer.asd index 9deaefb..6036aa5 100644 --- a/cl-streamer/cl-streamer.asd +++ b/cl-streamer/cl-streamer.asd @@ -6,10 +6,8 @@ :serial t :depends-on (#:alexandria #:bordeaux-threads - #:usocket + #:iolib #:flexi-streams - #:chunga - #:trivial-gray-streams #:split-sequence #:log4cl) :components ((:file "package") diff --git a/cl-streamer/stream-server.lisp b/cl-streamer/stream-server.lisp index 8fb0377..f5b6715 100644 --- a/cl-streamer/stream-server.lisp +++ b/cl-streamer/stream-server.lisp @@ -3,6 +3,9 @@ (defparameter *default-port* 8000 "Default port for the streaming server.") +(defparameter *client-write-timeout* 30 + "Seconds before a stale client write is abandoned.") + (defclass stream-server () ((port :initarg :port :accessor server-port :initform *default-port*) (socket :initform nil :accessor server-socket) @@ -77,14 +80,27 @@ (server-clients server)) (count-if #'client-active-p (server-clients server))))) +(defun configure-client-socket (socket) + "Set socket options on an accepted client connection. + Enables SO_KEEPALIVE for stale connection detection and + TCP_NODELAY for low-latency streaming." + (setf (iolib:socket-option socket :keep-alive) t) + (setf (iolib:socket-option socket :tcp-nodelay) t) + ;; SO_SNDTIMEO for write timeout — detect stale clients + (setf (iolib:socket-option socket :send-timeout) *client-write-timeout*)) + (defun start-server (server) "Start the streaming server." (when (server-running-p server) (error 'streamer-error :message "Server already running")) (setf (server-socket server) - (usocket:socket-listen "0.0.0.0" (server-port server) - :reuse-address t - :element-type '(unsigned-byte 8))) + (iolib:make-socket :connect :passive + :address-family :internet + :type :stream + :local-host "0.0.0.0" + :local-port (server-port server) + :reuse-address t + :backlog 128)) (setf (server-running-p server) t) (setf (server-accept-thread server) (bt:make-thread (lambda () (accept-loop server)) @@ -98,8 +114,8 @@ (bt:with-lock-held ((server-clients-lock server)) (dolist (client (server-clients server)) (setf (client-active-p client) nil) - (ignore-errors (usocket:socket-close (client-socket client))))) - (ignore-errors (usocket:socket-close (server-socket server))) + (ignore-errors (close (client-socket client))))) + (ignore-errors (close (server-socket server))) (log:info "CL-Streamer stopped") server) @@ -107,18 +123,27 @@ "Main accept loop for incoming connections." (loop while (server-running-p server) do (handler-case - (let ((client-socket (usocket:socket-accept (server-socket server)))) + (let ((client-socket (iolib:accept-connection (server-socket server)))) + (handler-case + (configure-client-socket client-socket) + (error (e) + (log:debug "Socket option error: ~A" e))) (bt:make-thread (lambda () (handle-client server client-socket)) :name "cl-streamer-client")) - (usocket:socket-error (e) + (iolib:socket-connection-aborted-error () + ;; Client disconnected before we accepted — skip + nil) + (error (e) (unless (server-running-p server) (return)) (log:warn "Accept error: ~A" e))))) (defun handle-client (server client-socket) - "Handle a single client connection." + "Handle a single client connection. + The iolib socket is a dual-channel gray stream supporting both + binary and character I/O." (let ((stream (flexi-streams:make-flexi-stream - (usocket:socket-stream client-socket) + client-socket :external-format :latin-1))) (handler-case (let* ((request-line (read-line stream)) @@ -127,7 +152,7 @@ ;; Handle CORS preflight (when (string-equal method "OPTIONS") (send-cors-preflight stream) - (ignore-errors (usocket:socket-close client-socket)) + (ignore-errors (close client-socket)) (return-from handle-client)) (multiple-value-bind (path wants-meta) (parse-icy-request request-line headers) @@ -139,7 +164,7 @@ (send-404 stream path)))))) (error (e) (log:debug "Client error: ~A" e) - (ignore-errors (usocket:socket-close client-socket)))))) + (ignore-errors (close client-socket)))))) (defun read-http-headers (stream) "Read HTTP headers from STREAM. Returns alist of (name . value)." @@ -170,7 +195,7 @@ (unwind-protect (stream-to-client client) (setf (client-active-p client) nil) - (ignore-errors (usocket:socket-close client-socket)) + (ignore-errors (close client-socket)) (bt:with-lock-held ((server-clients-lock server)) (setf (server-clients server) (remove client (server-clients server)))) @@ -205,6 +230,15 @@ (write-with-metadata client chunk bytes-read) (write-sequence chunk stream :end bytes-read)) (force-output stream)) + (iolib:socket-connection-reset-error () + (setf (client-active-p client) nil) + (return)) + (isys:ewouldblock () + ;; Write timeout — stale client + (log:warn "Write timeout on ~A, disconnecting stale client" + (mount-path mount)) + (setf (client-active-p client) nil) + (return)) (error (e) (log:warn "Client stream error on ~A: ~A" (mount-path mount) e) diff --git a/parenscript/stream-player.lisp b/parenscript/stream-player.lisp index 04c2e2e..ce2ae26 100644 --- a/parenscript/stream-player.lisp +++ b/parenscript/stream-player.lisp @@ -960,7 +960,7 @@ ;; Start now playing updates (set-timeout update-mini-now-playing 1000) - (set-interval update-mini-now-playing 5000)))) + (set-interval update-mini-now-playing 15000)))) ;; Initialize popout player (defun init-popout-player () @@ -996,7 +996,7 @@ ;; Start now playing updates (update-popout-now-playing) - (set-interval update-popout-now-playing 5000) + (set-interval update-popout-now-playing 15000) ;; Notify parent window (notify-popout-opened)