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 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 16:39:43 +03:00
co-authored by Claude Sonnet 4.6
parent 9662d3d090
commit 3d25f837af
4 changed files with 65 additions and 2 deletions
+39 -2
View File
@@ -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;}' +
'}' +
'</style></head>' +
'<body><div class="wrap">' +
@@ -110,7 +140,7 @@ begin
'<span class="sl-sep">|</span>' +
'<button id="toneBtn">TONE</button>' +
'<span class="sl-sep">|</span>' +
'<div class="sl-g"><span class="sl-lb">TX</span><input type="range" id="txSl" min="0" max="100" value="50"><span class="sl-val" id="txVal">50</span></div>' +
'<div class="sl-g tx-g"><span class="sl-lb">TX</span><input type="range" id="txSl" min="0" max="100" value="50"><span class="sl-val" id="txVal">50</span></div>' +
'<button id="moxBtn">MOX</button>' +
'<span class="sl-sep">|</span>' +
'<div class="tsep"></div>' +
@@ -146,7 +176,7 @@ begin
'<button id="btnSwap">SWAP</button>' +
'<button id="btnAB">A=B</button>' +
'</div>' +
'<span style="display:inline-block;width:12px"></span>' +
'<span class="vfo-sp" style="display:inline-block;width:12px"></span>' +
'<button class="vbtn" id="vBtnB">B</button>' +
'<div id="vDispB" class="vfo-ro dim"></div>' +
'</div>' +
@@ -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
+3
View File
@@ -588,6 +588,9 @@ begin
SockClose(CSock);
Continue;
end;
// 1 секунда на отправку: если TCP-буфер клиента переполнен, SockSend
// вернёт ошибку вместо того чтобы висеть и держать FClientLock вечно.
SockSetSndTimeout(CSock, 1000);
Client := TWsClient.Create(CSock);
FClientLock.Enter;
try
+20
View File
@@ -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}
{ ═══════════════════════════════════════════════════════════════════════════
+3
View File
@@ -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;