From cf51b305b0a755d21d335d496ffe90f91aa6d6d5 Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Sun, 1 Mar 2026 14:49:38 +0800 Subject: [PATCH] fix(diagnosis): prevent progress stream blocking and refine tunnel type chips (#230) * fix(diagnosis): avoid result channel deadlock in progress stream Close the diagnosis result channel asynchronously after workers complete so progress can stream without blocking, and improve tunnel card type chip contrast for clearer protocol distinction. * feat(diagnosis): stream pending items and render in-progress states Pre-populate diagnosis stream with pending targets so tunnel and forward dialogs can show per-item diagnosing status immediately. Update result typing and UI states to distinguish in-progress, success, and failure rows/cards consistently. --- .../internal/http/handler/control_plane.go | 55 ++++++- .../internal/http/handler/diagnosis_stream.go | 1 + vite-frontend/src/pages/forward.tsx | 131 +++++++++++------ vite-frontend/src/pages/forward/diagnosis.ts | 3 +- vite-frontend/src/pages/tunnel.tsx | 137 ++++++++++++------ vite-frontend/src/pages/tunnel/diagnosis.ts | 3 +- 6 files changed, 237 insertions(+), 93 deletions(-) diff --git a/go-backend/internal/http/handler/control_plane.go b/go-backend/internal/http/handler/control_plane.go index 3ed4f0d..93eefe7 100644 --- a/go-backend/internal/http/handler/control_plane.go +++ b/go-backend/internal/http/handler/control_plane.go @@ -58,6 +58,55 @@ type diagnosisProgress struct { type diagnosisItemEmitter func(index int, item map[string]interface{}, progress diagnosisProgress) +func (h *Handler) buildDiagnosisStreamStartItems(workItems []diagnosisWorkItem) []map[string]interface{} { + if len(workItems) == 0 { + return []map[string]interface{}{} + } + + nodeCache := map[int64]*nodeRecord{} + items := make([]map[string]interface{}, 0, len(workItems)) + for _, workItem := range workItems { + targetIP := strings.TrimSpace(workItem.targetIP) + targetPort := workItem.targetPort + if workItem.hasChainHop { + fromNode, _ := h.cachedNode(nodeCache, workItem.fromNodeID) + targetNode, err := h.cachedNode(nodeCache, workItem.toNode.NodeID) + if err == nil { + resolvedIP, resolvedPort, resolveErr := resolveChainProbeTarget(fromNode, targetNode, workItem.toNode.Port, workItem.ipPreference) + if resolveErr == nil { + targetIP = resolvedIP + targetPort = resolvedPort + } + } + } + if targetPort <= 0 { + targetPort = 443 + } + + nodeName := fmt.Sprintf("node_%d", workItem.fromNodeID) + if node, err := h.cachedNode(nodeCache, workItem.fromNodeID); err == nil && strings.TrimSpace(node.Name) != "" { + nodeName = node.Name + } + + item := map[string]interface{}{ + "success": false, + "diagnosing": true, + "description": workItem.description, + "nodeName": nodeName, + "nodeId": strconv.FormatInt(workItem.fromNodeID, 10), + "targetIp": targetIP, + "targetPort": targetPort, + "message": "诊断中...", + } + for key, value := range workItem.metadata { + item[key] = value + } + items = append(items, item) + } + + return items +} + const diagnosisMaxConcurrency = 8 const ( @@ -923,8 +972,10 @@ enqueueLoop: } } close(jobs) - wg.Wait() - close(resultCh) + go func() { + wg.Wait() + close(resultCh) + }() progress := diagnosisProgress{Total: len(workItems)} for result := range resultCh { diff --git a/go-backend/internal/http/handler/diagnosis_stream.go b/go-backend/internal/http/handler/diagnosis_stream.go index c75011b..93823e9 100644 --- a/go-backend/internal/http/handler/diagnosis_stream.go +++ b/go-backend/internal/http/handler/diagnosis_stream.go @@ -81,6 +81,7 @@ func (h *Handler) streamDiagnosisRuntime(ctx context.Context, cancel context.Can payload := map[string]interface{}{ "total": len(workItems), "timestamp": time.Now().UnixMilli(), + "items": h.buildDiagnosisStreamStartItems(workItems), } for key, value := range startPayload { payload[key] = value diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index b1ad76c..b35aa1e 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -890,10 +890,13 @@ export default function ForwardPage() { ? payload.forwardName : forward.name; const startTotal = Number(payload.total); + const startItems = Array.isArray(payload.items) + ? (payload.items as ForwardDiagnosisResult["results"]) + : []; setDiagnosisResult((prev) => ({ forwardName: startForwardName, timestamp: Date.now(), - results: prev?.results || [], + results: startItems.length > 0 ? startItems : prev?.results || [], })); if (Number.isFinite(startTotal) && startTotal >= 0) { setDiagnosisProgress((prev) => ({ @@ -919,9 +922,15 @@ export default function ForwardPage() { ); if (existingIndex >= 0) { - nextResults[existingIndex] = result; + nextResults[existingIndex] = { + ...result, + diagnosing: false, + }; } else { - nextResults.push(result); + nextResults.push({ + ...result, + diagnosing: false, + }); } return { ...base, @@ -3460,15 +3469,7 @@ export default function ForwardPage() { )} - {diagnosisLoading && - (!diagnosisResult || diagnosisResult.results.length === 0) ? ( -
-
- - 正在诊断... -
-
- ) : diagnosisResult ? ( + {diagnosisResult ? (
{diagnosisLoading && (
@@ -3599,6 +3600,10 @@ export default function ForwardPage() { {results.map((result, index) => { + const isDiagnosing = Boolean( + result.diagnosing, + ); + const isSuccess = result.success === true; const quality = getForwardDiagnosisQualityDisplay( result.averageTime, @@ -3609,22 +3614,28 @@ export default function ForwardPage() {
- - {result.success ? "✓" : "✗"} - + {isDiagnosing ? ( + + ) : ( + + {isSuccess ? "✓" : "✗"} + + )}
{result.description} @@ -3639,18 +3650,24 @@ export default function ForwardPage() { - {result.success ? "成功" : "失败"} + {isDiagnosing + ? "诊断中" + : isSuccess + ? "成功" + : "失败"} - {result.success ? ( + {isSuccess ? ( {result.averageTime?.toFixed(0)} @@ -3661,7 +3678,7 @@ export default function ForwardPage() { )} - {result.success ? ( + {isSuccess ? ( 0 @@ -3678,7 +3695,7 @@ export default function ForwardPage() { )} - {result.success && quality ? ( + {isSuccess && quality ? (
{results.map((result, index) => { + const isDiagnosing = Boolean(result.diagnosing); + const isSuccess = result.success === true; const quality = getForwardDiagnosisQualityDisplay( result.averageTime, @@ -3782,21 +3801,27 @@ export default function ForwardPage() {
- - {result.success ? "✓" : "✗"} - + {isDiagnosing ? ( + + ) : ( + + {isSuccess ? "✓" : "✗"} + + )}
{result.description} @@ -3808,16 +3833,24 @@ export default function ForwardPage() { - {result.success ? "成功" : "失败"} + {isDiagnosing + ? "诊断中" + : isSuccess + ? "成功" + : "失败"}
- {result.success ? ( + {isSuccess ? (
@@ -3861,8 +3894,16 @@ export default function ForwardPage() {
) : (
-
- {result.message || "连接失败"} +
+ {isDiagnosing + ? result.message || "诊断中..." + : result.message || "连接失败"}
)} @@ -3903,14 +3944,16 @@ export default function ForwardPage() {
{/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */} - {diagnosisResult.results.some((r) => !r.success) && ( + {diagnosisResult.results.some( + (r) => r.success === false && !r.diagnosing, + ) && (

失败详情

{diagnosisResult.results - .filter((r) => !r.success) + .filter((r) => r.success === false && !r.diagnosing) .map((result, index) => ( ({ tunnelName: startTunnelName, tunnelType: startTunnelType, timestamp: Date.now(), - results: prev?.results || [], + results: startItems.length > 0 ? startItems : prev?.results || [], })); if (Number.isFinite(startTotal) && startTotal >= 0) { setDiagnosisProgress((prev) => ({ @@ -521,9 +524,15 @@ export default function TunnelPage() { ); if (existingIndex >= 0) { - nextResults[existingIndex] = result; + nextResults[existingIndex] = { + ...result, + diagnosing: false, + }; } else { - nextResults.push(result); + nextResults.push({ + ...result, + diagnosing: false, + }); } return { ...base, @@ -970,6 +979,10 @@ export default function TunnelPage() {
{sortedTunnels.map((tunnel) => { const typeDisplay = getTunnelTypeDisplay(tunnel.type); + const tunnelTypeChipClassName = + tunnel.type === 1 + ? "text-xs bg-primary-100 text-primary-800 border-primary-300 dark:bg-primary-900/45 dark:text-primary-200 dark:border-primary-700" + : "text-xs bg-success-100 text-success-800 border-success-300 dark:bg-success-900/35 dark:text-success-200 dark:border-success-700"; return ( @@ -993,7 +1006,7 @@ export default function TunnelPage() {
- {diagnosisLoading && - (!diagnosisResult || diagnosisResult.results.length === 0) ? ( -
-
- - 正在诊断... -
-
- ) : diagnosisResult ? ( + {diagnosisResult ? (
{diagnosisLoading && (
@@ -2245,6 +2250,10 @@ export default function TunnelPage() { {results.map((result, index) => { + const isDiagnosing = Boolean( + result.diagnosing, + ); + const isSuccess = result.success === true; const quality = getDiagnosisQualityDisplay( result.averageTime, result.packetLoss, @@ -2254,22 +2263,28 @@ export default function TunnelPage() {
- - {result.success ? "✓" : "✗"} - + {isDiagnosing ? ( + + ) : ( + + {isSuccess ? "✓" : "✗"} + + )}
{result.description} @@ -2284,18 +2299,24 @@ export default function TunnelPage() { - {result.success ? "成功" : "失败"} + {isDiagnosing + ? "诊断中" + : isSuccess + ? "成功" + : "失败"} - {result.success ? ( + {isSuccess ? ( {result.averageTime?.toFixed(0)} @@ -2306,7 +2327,7 @@ export default function TunnelPage() { )} - {result.success ? ( + {isSuccess ? ( 0 @@ -2323,7 +2344,7 @@ export default function TunnelPage() { )} - {result.success && quality ? ( + {isSuccess && quality ? (
{results.map((result, index) => { + const isDiagnosing = Boolean(result.diagnosing); + const isSuccess = result.success === true; const quality = getDiagnosisQualityDisplay( result.averageTime, result.packetLoss, @@ -2426,21 +2449,27 @@ export default function TunnelPage() {
- - {result.success ? "✓" : "✗"} - + {isDiagnosing ? ( + + ) : ( + + {isSuccess ? "✓" : "✗"} + + )}
{result.description} @@ -2452,16 +2481,24 @@ export default function TunnelPage() { - {result.success ? "成功" : "失败"} + {isDiagnosing + ? "诊断中" + : isSuccess + ? "成功" + : "失败"}
- {result.success ? ( + {isSuccess ? (
@@ -2505,8 +2542,16 @@ export default function TunnelPage() {
) : (
-
- {result.message || "连接失败"} +
+ {isDiagnosing + ? result.message || "诊断中..." + : result.message || "连接失败"}
)} @@ -2547,14 +2592,16 @@ export default function TunnelPage() {
{/* 失败详情(仅桌面端显示,移动端已在卡片中显示) */} - {diagnosisResult.results.some((r) => !r.success) && ( + {diagnosisResult.results.some( + (r) => r.success === false && !r.diagnosing, + ) && (

失败详情

{diagnosisResult.results - .filter((r) => !r.success) + .filter((r) => r.success === false && !r.diagnosing) .map((result, index) => (