mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
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:
+39
-2
@@ -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
|
||||
|
||||
@@ -588,6 +588,9 @@ begin
|
||||
SockClose(CSock);
|
||||
Continue;
|
||||
end;
|
||||
// 1 секунда на отправку: если TCP-буфер клиента переполнен, SockSend
|
||||
// вернёт ошибку вместо того чтобы висеть и держать FClientLock вечно.
|
||||
SockSetSndTimeout(CSock, 1000);
|
||||
Client := TWsClient.Create(CSock);
|
||||
FClientLock.Enter;
|
||||
try
|
||||
|
||||
@@ -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}
|
||||
|
||||
{ ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user