mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:37:33 +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%}' +
|
'.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}' +
|
'.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}' +
|
'#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>' +
|
'</style></head>' +
|
||||||
|
|
||||||
'<body><div class="wrap">' +
|
'<body><div class="wrap">' +
|
||||||
@@ -110,7 +140,7 @@ begin
|
|||||||
'<span class="sl-sep">|</span>' +
|
'<span class="sl-sep">|</span>' +
|
||||||
'<button id="toneBtn">TONE</button>' +
|
'<button id="toneBtn">TONE</button>' +
|
||||||
'<span class="sl-sep">|</span>' +
|
'<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>' +
|
'<button id="moxBtn">MOX</button>' +
|
||||||
'<span class="sl-sep">|</span>' +
|
'<span class="sl-sep">|</span>' +
|
||||||
'<div class="tsep"></div>' +
|
'<div class="tsep"></div>' +
|
||||||
@@ -146,7 +176,7 @@ begin
|
|||||||
'<button id="btnSwap">SWAP</button>' +
|
'<button id="btnSwap">SWAP</button>' +
|
||||||
'<button id="btnAB">A=B</button>' +
|
'<button id="btnAB">A=B</button>' +
|
||||||
'</div>' +
|
'</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>' +
|
'<button class="vbtn" id="vBtnB">B</button>' +
|
||||||
'<div id="vDispB" class="vfo-ro dim"></div>' +
|
'<div id="vDispB" class="vfo-ro dim"></div>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
@@ -543,6 +573,13 @@ begin
|
|||||||
'});' +
|
'});' +
|
||||||
'cv.addEventListener("mouseleave",()=>{cv.style.cursor="";if(!markerOn)hoverX=-1;wTarget="";dragging=false;});}' +
|
'cv.addEventListener("mouseleave",()=>{cv.style.cursor="";if(!markerOn)hoverX=-1;wTarget="";dragging=false;});}' +
|
||||||
'bindCv(specCv);bindCv(wfCv);' +
|
'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});' +
|
'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
|
// Opus: async decodeFrame, очередь, оверлей для user gesture
|
||||||
|
|||||||
@@ -588,6 +588,9 @@ begin
|
|||||||
SockClose(CSock);
|
SockClose(CSock);
|
||||||
Continue;
|
Continue;
|
||||||
end;
|
end;
|
||||||
|
// 1 секунда на отправку: если TCP-буфер клиента переполнен, SockSend
|
||||||
|
// вернёт ошибку вместо того чтобы висеть и держать FClientLock вечно.
|
||||||
|
SockSetSndTimeout(CSock, 1000);
|
||||||
Client := TWsClient.Create(CSock);
|
Client := TWsClient.Create(CSock);
|
||||||
FClientLock.Enter;
|
FClientLock.Enter;
|
||||||
try
|
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;
|
function SockSend(S: TSocket; Buf: Pointer; Len, Flags: Integer): Integer; inline;
|
||||||
procedure SockSetNonBlock(S: TSocket; NB: Boolean);
|
procedure SockSetNonBlock(S: TSocket; NB: Boolean);
|
||||||
|
|
||||||
|
{ SockSetSndTimeout — ограничивает время блокирующего SockSend.
|
||||||
|
Без таймаута SockSend может зависнуть на минуты (TCP retransmit timeout)
|
||||||
|
если буфер клиента переполнен, держа при этом FClientLock и блокируя Stop(). }
|
||||||
|
procedure SockSetSndTimeout(S: TSocket; Ms: Integer);
|
||||||
|
|
||||||
{ ── SHA-1 ─────────────────────────────────────────────────────────────────── }
|
{ ── SHA-1 ─────────────────────────────────────────────────────────────────── }
|
||||||
|
|
||||||
type
|
type
|
||||||
@@ -111,6 +116,13 @@ begin
|
|||||||
ioctlsocket(S, FIONBIO, @Mode);
|
ioctlsocket(S, FIONBIO, @Mode);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
procedure SockSetSndTimeout(S: TSocket; Ms: Integer);
|
||||||
|
var T: DWORD;
|
||||||
|
begin
|
||||||
|
T := Ms;
|
||||||
|
setsockopt(S, SOL_SOCKET, SO_SNDTIMEO, @T, SizeOf(T));
|
||||||
|
end;
|
||||||
|
|
||||||
{$ELSE}
|
{$ELSE}
|
||||||
|
|
||||||
function SockClose(S: TSocket): Integer;
|
function SockClose(S: TSocket): Integer;
|
||||||
@@ -145,6 +157,14 @@ begin
|
|||||||
fpFcntl(S, F_SETFL, Flags);
|
fpFcntl(S, F_SETFL, Flags);
|
||||||
end;
|
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}
|
{$ENDIF}
|
||||||
|
|
||||||
{ ═══════════════════════════════════════════════════════════════════════════
|
{ ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -145,6 +145,9 @@ begin
|
|||||||
P := @Data;
|
P := @Data;
|
||||||
Result := SendRaw(P^, Len);
|
Result := SendRaw(P^, Len);
|
||||||
end;
|
end;
|
||||||
|
// Ошибка отправки (таймаут SO_SNDTIMEO, разрыв) — немедленно закрываем
|
||||||
|
// клиент чтобы BroadcastBinary не держал FClientLock на следующих тиках.
|
||||||
|
if not Result then FState := wsClosed;
|
||||||
finally
|
finally
|
||||||
FLock.Leave;
|
FLock.Leave;
|
||||||
end;
|
end;
|
||||||
|
|||||||
Reference in New Issue
Block a user