From 3d25f837af39caab31007a376749e69d367148b6 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Sat, 9 May 2026 16:39:43 +0300 Subject: [PATCH] fix WebSocket deadlock on slow client + mobile TX controls visibility Deadlock: BroadcastBinary held FClientLock during blocking SockSend; slow/stalled client caused Stop() to wait forever. Fix: set SO_SNDTIMEO=1s on each accepted socket so SockSend returns error instead of hanging, mark client wsClosed on send failure so subsequent ticks skip it. Mobile: move TONE/TX-slider/MOX to front of toolbar via CSS order so they are immediately visible without horizontal scroll on phones. Co-Authored-By: Claude Sonnet 4.6 --- WebPageHtml.pas | 41 +++++++++++++++++++++++++++++++++++++++-- WebServer.pas | 3 +++ WebUtils.pas | 20 ++++++++++++++++++++ WsClient.pas | 3 +++ 4 files changed, 65 insertions(+), 2 deletions(-) diff --git a/WebPageHtml.pas b/WebPageHtml.pas index c1fd882..a752033 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -97,6 +97,36 @@ begin '.wf-area canvas{position:absolute;top:0;left:0;width:100%;height:100%}' + '.sbar{flex:0 0 16px;display:flex;align-items:center;padding:0 6px;gap:10px;background:#0e0e0e;border-top:1px solid #222;font-size:10px;color:#555}' + '#latEl{margin-left:auto}' + + '@media screen and (max-width:640px){' + + // Тулбар 1: горизонтальный скролл вместо переноса + '.tbar:not(.row2){overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;}' + + '.tbar:not(.row2)::-webkit-scrollbar{display:none;}' + + '.tbar:not(.row2)>*{flex-shrink:0;}' + + // Крупнее зоны касания + 'button{height:30px;}select{height:30px;}' + + // VFO-строка: стекаем smeter / vfo-mid / right-g каждый на своей строке + '.row2{height:auto;flex-wrap:wrap;padding:3px;gap:3px;}' + + '#smeter{width:100%;height:40px;}' + + '.vfo-mid{width:100%;gap:2px;justify-content:flex-start;}' + + '.vfo-ro{font-size:20px;height:34px;padding:0 2px;padding-top:6px;}' + + '.vfo-ro .dg,.vfo-ro .sp{font-size:20px;}' + + '.vfo-hz .dg{font-size:10px;}' + + '.vbtn{width:26px;height:26px;font-size:10px;}' + + '.swap-g button{height:22px;padding:0 4px;font-size:9px;}' + + '.vfo-sp{display:none;}' + + '.right-g{width:100%;margin-left:0;}' + + '.right-g button,.right-g select{height:28px;font-size:11px;}' + + // Водопад короче на телефоне + '.wf-area{flex:0 0 180px;}' + + // Слайдеры чуть уже + 'input[type=range]{width:55px;}' + + // TX+MOX пинним в начало тулбара чтобы были видны без скролла + '#toneBtn{order:-3;}' + + '.tx-g{order:-2;}' + + '#moxBtn{order:-1;min-width:52px;height:30px;font-weight:bold;}' + + // iOS Safari: 100vh включает адресную строку — используем реальную высоту + 'body,.wrap{height:-webkit-fill-available;}' + + '}' + '' + '
' + @@ -110,7 +140,7 @@ begin '|' + '' + '|' + - '
TX50
' + + '
TX50
' + '' + '|' + '
' + @@ -146,7 +176,7 @@ begin '' + '' + '
' + - '' + + '' + '' + '
' + '' + @@ -543,6 +573,13 @@ begin '});' + 'cv.addEventListener("mouseleave",()=>{cv.style.cursor="";if(!markerOn)hoverX=-1;wTarget="";dragging=false;});}' + 'bindCv(specCv);bindCv(wfCv);' + + // Touch→mouse мост для канвасов: drag-to-pan и tap-to-tune на мобильных + 'function addTouch(cv){' + + 'function mkMs(type,t){return new MouseEvent(type,{bubbles:true,cancelable:true,clientX:t.clientX,clientY:t.clientY});}' + + 'cv.addEventListener("touchstart",e=>{e.preventDefault();audioKick();cv.dispatchEvent(mkMs("mousedown",e.touches[0]));},{passive:false});' + + 'cv.addEventListener("touchmove",e=>{e.preventDefault();cv.dispatchEvent(mkMs("mousemove",e.touches[0]));},{passive:false});' + + 'cv.addEventListener("touchend",e=>{e.preventDefault();cv.dispatchEvent(mkMs("mouseup",e.changedTouches[0]));},{passive:false});}' + + 'addTouch(specCv);addTouch(wfCv);' + 'window.addEventListener("wheel",e=>{if(e.target?.closest?.(".vfo-ro"))return;if(!wTarget){if(e.target===specCv)wTarget="spec";else if(e.target===wfCv)wTarget="wf";}if(wTarget)doWheel(e);},{passive:false,capture:true});' + // Opus: async decodeFrame, очередь, оверлей для user gesture diff --git a/WebServer.pas b/WebServer.pas index 782ecef..0dd7666 100644 --- a/WebServer.pas +++ b/WebServer.pas @@ -588,6 +588,9 @@ begin SockClose(CSock); Continue; end; + // 1 секунда на отправку: если TCP-буфер клиента переполнен, SockSend + // вернёт ошибку вместо того чтобы висеть и держать FClientLock вечно. + SockSetSndTimeout(CSock, 1000); Client := TWsClient.Create(CSock); FClientLock.Enter; try diff --git a/WebUtils.pas b/WebUtils.pas index 6ec261e..a995f5f 100644 --- a/WebUtils.pas +++ b/WebUtils.pas @@ -54,6 +54,11 @@ function SockRecv(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer; inli function SockSend(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer; inline; procedure SockSetNonBlock(S: TSocket; NB: Boolean); +{ SockSetSndTimeout — ограничивает время блокирующего SockSend. + Без таймаута SockSend может зависнуть на минуты (TCP retransmit timeout) + если буфер клиента переполнен, держа при этом FClientLock и блокируя Stop(). } +procedure SockSetSndTimeout(S: TSocket; Ms: Integer); + { ── SHA-1 ─────────────────────────────────────────────────────────────────── } type @@ -111,6 +116,13 @@ begin ioctlsocket(S, FIONBIO, @Mode); end; +procedure SockSetSndTimeout(S: TSocket; Ms: Integer); +var T: DWORD; +begin + T := Ms; + setsockopt(S, SOL_SOCKET, SO_SNDTIMEO, @T, SizeOf(T)); +end; + {$ELSE} function SockClose(S: TSocket): Integer; @@ -145,6 +157,14 @@ begin fpFcntl(S, F_SETFL, Flags); end; +procedure SockSetSndTimeout(S: TSocket; Ms: Integer); +var TV: TTimeVal; +begin + TV.tv_sec := Ms div 1000; + TV.tv_usec := (Ms mod 1000) * 1000; + fpSetSockOpt(S, SOL_SOCKET, SO_SNDTIMEO, @TV, SizeOf(TV)); +end; + {$ENDIF} { ═══════════════════════════════════════════════════════════════════════════ diff --git a/WsClient.pas b/WsClient.pas index aa5c46e..57a8f8b 100644 --- a/WsClient.pas +++ b/WsClient.pas @@ -145,6 +145,9 @@ begin P := @Data; Result := SendRaw(P^, Len); end; + // Ошибка отправки (таймаут SO_SNDTIMEO, разрыв) — немедленно закрываем + // клиент чтобы BroadcastBinary не держал FClientLock на следующих тиках. + if not Result then FState := wsClosed; finally FLock.Leave; end;