From 9884b70752653d3c4509199ad95ecc4b09ff8590 Mon Sep 17 00:00:00 2001 From: AkiKurisu <2683987717@qq.com> Date: Sat, 27 Jun 2026 17:30:35 +0800 Subject: [PATCH] Surface bridge ping errors in editor and startup --- Editor/Core/KimodoBridgeServerManage.cs | 24 +++- Editor/Core/KimodoPlayableClipEditor.cs | 32 +++++- .../KimodoServerManagerSettingsProvider.cs | 44 +++++++- ...45\351\224\231\345\244\204\347\220\206.md" | 10 ++ Runtime/Bridge/BridgeRuntimeControl.cs | 105 ++++++++++++++++++ Runtime/Bridge/BridgeStartupWaiter.cs | 26 ++++- Runtime/Bridge/KimodoBridgeService.cs | 68 ++++++++++-- 7 files changed, 291 insertions(+), 18 deletions(-) diff --git a/Editor/Core/KimodoBridgeServerManage.cs b/Editor/Core/KimodoBridgeServerManage.cs index ee43cde..b339520 100644 --- a/Editor/Core/KimodoBridgeServerManage.cs +++ b/Editor/Core/KimodoBridgeServerManage.cs @@ -41,8 +41,18 @@ internal readonly struct ServerStatusSnapshot public readonly bool QueryInFlight; public readonly string Host; public readonly int Port; - - public ServerStatusSnapshot(bool ready, bool running, bool hasPort, bool queryInFlight, string host, int port) + public readonly BridgePingStatus PingStatus; + public readonly string Message; + + public ServerStatusSnapshot( + bool ready, + bool running, + bool hasPort, + bool queryInFlight, + string host, + int port, + BridgePingStatus pingStatus = BridgePingStatus.Unknown, + string message = "") { Ready = ready; Running = running; @@ -50,6 +60,8 @@ public ServerStatusSnapshot(bool ready, bool running, bool hasPort, bool queryIn QueryInFlight = queryInFlight; Host = host ?? "127.0.0.1"; Port = port; + PingStatus = pingStatus; + Message = message ?? string.Empty; } } @@ -410,13 +422,17 @@ private static ServerStatusSnapshot GetServerStatusSnapshotCore() port: -1); } + BridgePingResult ping = BridgeRuntimeControl.QueryPing(host, port); + bool running = ping.Status == BridgePingStatus.Ready || ping.Status == BridgePingStatus.Loading; return new ServerStatusSnapshot( ready: true, - running: true, + running: running, hasPort: true, queryInFlight: false, host: host, - port: port); + port: port, + pingStatus: ping.Status, + message: ping.Message); } private sealed class RuntimeMaintenanceScope : IDisposable diff --git a/Editor/Core/KimodoPlayableClipEditor.cs b/Editor/Core/KimodoPlayableClipEditor.cs index 7377bc5..b21ce72 100644 --- a/Editor/Core/KimodoPlayableClipEditor.cs +++ b/Editor/Core/KimodoPlayableClipEditor.cs @@ -40,6 +40,8 @@ public partial class KimodoPlayableClipEditor : UnityEditor.Editor private bool bridgeRunningCached; private bool bridgePortDiscoveredCached; private bool bridgeStatusReady; + private BridgePingStatus bridgePingStatus; + private string bridgeStatusMessage = string.Empty; private bool showAdvancedFoldout = true; private double lastRepaintTime; private bool repaintQueued; @@ -250,7 +252,17 @@ private void DrawGenerationSection() EditorGUILayout.LabelField("Bridge status: checking...", EditorStyles.miniLabel); } - if (!bridgeRunningCached && bridgePortDiscoveredCached) + if (bridgePingStatus == BridgePingStatus.Error) + { + EditorGUILayout.HelpBox( + "Bridge reports an error. " + SummarizeForUi(bridgeStatusMessage), + MessageType.Error); + } + else if (bridgePingStatus == BridgePingStatus.Loading && !string.IsNullOrWhiteSpace(bridgeStatusMessage)) + { + EditorGUILayout.LabelField("Bridge status: " + SummarizeForUi(bridgeStatusMessage), EditorStyles.miniLabel); + } + else if (!bridgeRunningCached && bridgePortDiscoveredCached) { EditorGUILayout.HelpBox( "Bridge process is not running, but endpoint file still exists. This is usually a stale serverport record.", @@ -307,6 +319,24 @@ private void PullBridgeStatusSnapshot(bool forceRefresh) bridgeStatusReady = snapshot.Ready; bridgeRunningCached = snapshot.Running; bridgePortDiscoveredCached = snapshot.HasPort; + bridgePingStatus = snapshot.PingStatus; + bridgeStatusMessage = snapshot.Message; + } + + private static string SummarizeForUi(string message, int maxLength = 320) + { + if (string.IsNullOrWhiteSpace(message)) + { + return string.Empty; + } + + string normalized = string.Join(" ", message.Split(new[] { '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries)).Trim(); + if (normalized.Length <= maxLength) + { + return normalized; + } + + return normalized.Substring(0, maxLength) + "..."; } private void DrawAnimationClipSection() diff --git a/Editor/Core/KimodoServerManagerSettingsProvider.cs b/Editor/Core/KimodoServerManagerSettingsProvider.cs index a6dbb8b..63859fa 100644 --- a/Editor/Core/KimodoServerManagerSettingsProvider.cs +++ b/Editor/Core/KimodoServerManagerSettingsProvider.cs @@ -41,6 +41,9 @@ private enum ServerState private double detectHintUntilTime; private string serverHost = "127.0.0.1"; private int serverPort = -1; + private bool serverHasPort; + private BridgePingStatus serverPingStatus; + private string serverStatusMessage = string.Empty; private bool operationInProgress; private string operationStatus = string.Empty; @@ -276,6 +279,12 @@ private void DrawServerSection() { EditorGUILayout.HelpBox("detect...", MessageType.None); } + else if (serverPingStatus == BridgePingStatus.Error) + { + EditorGUILayout.HelpBox( + $"Server reported an error at {serverHost}:{serverPort}. {SummarizeForUi(serverStatusMessage)}", + MessageType.Error); + } else if (serverState == ServerState.Enabled) { EditorGUILayout.HelpBox($"Running at {serverHost}:{serverPort}", MessageType.Info); @@ -283,16 +292,22 @@ private void DrawServerSection() else { EditorGUILayout.HelpBox("Server is not running.", MessageType.None); - ServerStatusSnapshot staleSnapshot = KimodoBridgeServerManage.GetServerStatusSnapshot(); - if (staleSnapshot.HasPort) + if (serverHasPort) { - EditorGUILayout.HelpBox("Detected stale endpoint file (serverport). Process is not alive.", MessageType.None); + string detail = string.IsNullOrWhiteSpace(serverStatusMessage) + ? string.Empty + : " " + SummarizeForUi(serverStatusMessage); + EditorGUILayout.HelpBox("Detected stale endpoint file (serverport). Process is not alive." + detail, MessageType.None); } } if (compileGate) { EditorGUILayout.LabelField("Status", "detect/compiling", EditorStyles.miniLabel); } + else if (serverPingStatus == BridgePingStatus.Error) + { + EditorGUILayout.LabelField("Status", "error", EditorStyles.miniLabel); + } else { EditorGUILayout.LabelField("Status", serverState == ServerState.Enabled ? "enable" : "disable", EditorStyles.miniLabel); @@ -569,6 +584,9 @@ private void PullServerStatusFromController(bool forceRefresh) ServerStatusSnapshot snapshot = KimodoBridgeServerManage.GetServerStatusSnapshot(); serverHost = snapshot.Host; serverPort = snapshot.Port; + serverHasPort = snapshot.HasPort; + serverPingStatus = snapshot.PingStatus; + serverStatusMessage = snapshot.Message; serverState = snapshot.Running ? ServerState.Enabled : ServerState.Disabled; } @@ -613,7 +631,9 @@ private Task StartServerAsync() onSuccess: () => { PullServerStatusFromController(forceRefresh: true); - operationStatus = serverState == ServerState.Enabled + operationStatus = serverPingStatus == BridgePingStatus.Error + ? "Server reported an error. " + SummarizeForUi(serverStatusMessage) + : serverState == ServerState.Enabled ? $"Running at {serverHost}:{serverPort}" : "Start completed."; }); @@ -747,6 +767,22 @@ await action(message => operationInProgress = false; } } + + private static string SummarizeForUi(string message, int maxLength = 320) + { + if (string.IsNullOrWhiteSpace(message)) + { + return string.Empty; + } + + string normalized = string.Join(" ", message.Split(new[] { '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries)).Trim(); + if (normalized.Length <= maxLength) + { + return normalized; + } + + return normalized.Substring(0, maxLength) + "..."; + } } } diff --git "a/Manual/\345\270\270\350\247\201\351\227\256\351\242\230\344\270\216\346\212\245\351\224\231\345\244\204\347\220\206.md" "b/Manual/\345\270\270\350\247\201\351\227\256\351\242\230\344\270\216\346\212\245\351\224\231\345\244\204\347\220\206.md" index 0b0adfb..4e5f079 100644 --- "a/Manual/\345\270\270\350\247\201\351\227\256\351\242\230\344\270\216\346\212\245\351\224\231\345\244\204\347\220\206.md" +++ "b/Manual/\345\270\270\350\247\201\351\227\256\351\242\230\344\270\216\346\212\245\351\224\231\345\244\204\347\220\206.md" @@ -42,6 +42,14 @@ 服务器进程异常退出。这类问题作者需要日志才能定位,请把服务器日志发过来(路径见文末)。先尝试**重启 Unity** 后再生成,有时能直接恢复。 +### 报错提到 "HF_HUB_OFFLINE" / "OfflineModeIsEnabled" / "Model prepare failed" + +服务器正在准备或补下载模型,但当前 Hugging Face 环境处于离线模式,或本地模型目录不完整。常见日志形态是 `Model prepare failed`、`Cannot find an appropriate cached snapshot folder`、`HF_HUB_OFFLINE=0`。 + +如果你希望自动下载模型,请确认系统或 Unity 启动环境里没有把 `HF_HUB_OFFLINE`、`TRANSFORMERS_OFFLINE`、`HF_DATASETS_OFFLINE` 设为 `1`。然后停止服务器,删除项目目录下 `NvlabKimodoQuickServer~` 里的 `.run.lock`、`.bridge.pid`、`serverport`,并删除空的 `models/Kimodo-SOMA-RP-v1` 目录后重新启动服务器。 + +如果你必须离线使用,请先把完整模型和文本编码器放到 `NvlabKimodoQuickServer~/models` 下;空目录或只下载了一部分文件都会被判定为模型缺失。 + ### 报错提到 "Bridge process is already running" 已经有一个服务器进程在跑了。一般不影响使用;若状态卡住,重启 Unity 让它重新拉起即可。 @@ -50,6 +58,8 @@ 服务器进程其实没在运行,但残留了一个旧的端口记录文件。这是无害的提示,重启 Unity 或重新生成即可刷新。 +如果同时存在 `.run.lock` 或 `.bridge.pid`,并且服务器面板反复显示旧端口或模型准备错误,可以先停止服务器,再删除这三个状态文件让工具重新拉起。 + ### 报错提到平台被禁用,如 "Bridge Windows/Linux platform disabled" 当前平台在设置里被关掉了,或者运行平台全部被禁用("All runtime platforms are disabled")。检查 BridgeRuntimeSettings 里对应平台的开关是否打开。 diff --git a/Runtime/Bridge/BridgeRuntimeControl.cs b/Runtime/Bridge/BridgeRuntimeControl.cs index db98884..6321fa7 100644 --- a/Runtime/Bridge/BridgeRuntimeControl.cs +++ b/Runtime/Bridge/BridgeRuntimeControl.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json.Linq; using System; using System.Net.Sockets; using System.Threading; @@ -5,6 +6,43 @@ namespace KimodoBridge { + public enum BridgePingStatus + { + Unknown = 0, + Ready = 1, + Loading = 2, + Error = 3, + Unreachable = 4, + InvalidEndpoint = 5 + } + + public readonly struct BridgePingResult + { + public readonly BridgePingStatus Status; + public readonly string Host; + public readonly int Port; + public readonly string Message; + + public BridgePingResult(BridgePingStatus status, string host, int port, string message) + { + Status = status; + Host = string.IsNullOrWhiteSpace(host) ? "127.0.0.1" : host; + Port = port; + Message = message ?? string.Empty; + } + + public bool IsReady => Status == BridgePingStatus.Ready; + public bool IsLoading => Status == BridgePingStatus.Loading; + public bool IsError => Status == BridgePingStatus.Error; + public bool IsReachable => Status == BridgePingStatus.Ready || Status == BridgePingStatus.Loading || Status == BridgePingStatus.Error; + public string Endpoint => Port > 0 ? $"{Host}:{Port}" : $"{Host}:(invalid)"; + + public bool IsHealthy(bool acceptLoading) + { + return Status == BridgePingStatus.Ready || (acceptLoading && Status == BridgePingStatus.Loading); + } + } + public static class BridgeRuntimeControl { public static bool TryReadServerEndpoint(string runtimeRoot, out string host, out int port) @@ -62,6 +100,73 @@ public static async Task CanConnectAsync( } } + public static BridgePingResult QueryPing( + string host, + int port, + int connectTimeoutMs = BridgeRuntimeSettings.DefaultStatusConnectTimeoutMs, + int ioTimeoutMs = BridgeRuntimeSettings.DefaultStatusIoTimeoutMs, + CancellationToken token = default) + { + return QueryPingAsync(host, port, connectTimeoutMs, ioTimeoutMs, token).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + public static async Task QueryPingAsync( + string host, + int port, + int connectTimeoutMs = BridgeRuntimeSettings.DefaultStatusConnectTimeoutMs, + int ioTimeoutMs = BridgeRuntimeSettings.DefaultStatusIoTimeoutMs, + CancellationToken token = default) + { + if (string.IsNullOrWhiteSpace(host) || port <= 0 || port > 65535) + { + return new BridgePingResult(BridgePingStatus.InvalidEndpoint, host, port, "Bridge endpoint is invalid."); + } + + try + { + using var client = new BridgeProtocolClient(connectTimeoutMs, ioTimeoutMs); + JObject response = await client.SendAsync(host, port, new JObject { ["cmd"] = "ping" }, token).ConfigureAwait(false); + string status = response?.Value("status") ?? string.Empty; + string message = response?.Value("message") ?? string.Empty; + + if (string.Equals(status, "pong", StringComparison.OrdinalIgnoreCase) || + string.Equals(status, "ready", StringComparison.OrdinalIgnoreCase) || + string.Equals(status, "ok", StringComparison.OrdinalIgnoreCase)) + { + return new BridgePingResult(BridgePingStatus.Ready, host, port, message); + } + + if (string.Equals(status, "loading", StringComparison.OrdinalIgnoreCase) || + string.Equals(status, "initializing", StringComparison.OrdinalIgnoreCase)) + { + return new BridgePingResult(BridgePingStatus.Loading, host, port, message); + } + + if (string.Equals(status, "error", StringComparison.OrdinalIgnoreCase)) + { + return new BridgePingResult(BridgePingStatus.Error, host, port, message); + } + + string unexpected = string.IsNullOrWhiteSpace(status) + ? "Bridge ping response did not include a status." + : $"Unexpected bridge ping status: {status}."; + if (!string.IsNullOrWhiteSpace(message)) + { + unexpected += " " + message; + } + return new BridgePingResult(BridgePingStatus.Unreachable, host, port, unexpected); + } + catch (OperationCanceledException) + { + token.ThrowIfCancellationRequested(); + return new BridgePingResult(BridgePingStatus.Unreachable, host, port, "Bridge ping was canceled."); + } + catch (Exception ex) + { + return new BridgePingResult(BridgePingStatus.Unreachable, host, port, ex.Message); + } + } + public static async Task TrySendQuitAsync( string host, int port, diff --git a/Runtime/Bridge/BridgeStartupWaiter.cs b/Runtime/Bridge/BridgeStartupWaiter.cs index 1f48619..8a86e4b 100644 --- a/Runtime/Bridge/BridgeStartupWaiter.cs +++ b/Runtime/Bridge/BridgeStartupWaiter.cs @@ -30,15 +30,21 @@ internal async Task WaitUntilReadyAsync( waitToken.ThrowIfCancellationRequested(); if (BridgeEndpointResolver.TryReadServerEndpoint(runtimeRoot, hostFallback, out string host, out int port, out _)) { - bool canConnect = await BridgeRuntimeControl.CanConnectAsync( + BridgePingResult ping = await BridgeRuntimeControl.QueryPingAsync( host, port, BridgeRuntimeSettings.DefaultStatusConnectTimeoutMs, + BridgeRuntimeSettings.DefaultStatusIoTimeoutMs, waitToken).ConfigureAwait(false); - if (canConnect) + if (ping.IsHealthy(acceptLoading: true)) { return; } + + if (ping.IsError) + { + throw new Exception($"Bridge startup failed: {SummarizeBridgeMessage(ping.Message)}"); + } } if (hasProcessExited != null && hasProcessExited()) @@ -50,5 +56,21 @@ internal async Task WaitUntilReadyAsync( await Task.Delay(Math.Max(BridgeRuntimeSettings.DefaultPollIntervalMs / 2, pollIntervalMs), waitToken).ConfigureAwait(false); } } + + private static string SummarizeBridgeMessage(string message, int maxLength = 500) + { + if (string.IsNullOrWhiteSpace(message)) + { + return string.Empty; + } + + string normalized = string.Join(" ", message.Split(new[] { '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries)).Trim(); + if (normalized.Length <= maxLength) + { + return normalized; + } + + return normalized.Substring(0, maxLength) + "..."; + } } } diff --git a/Runtime/Bridge/KimodoBridgeService.cs b/Runtime/Bridge/KimodoBridgeService.cs index 9dd8960..d90a3cf 100644 --- a/Runtime/Bridge/KimodoBridgeService.cs +++ b/Runtime/Bridge/KimodoBridgeService.cs @@ -146,16 +146,29 @@ private async Task StartCoreAsync(Action progress, CancellationT } else { - bool reachable = await protocolClient.PingAsync(host, port, token, acceptLoading: true).ConfigureAwait(false); - if (reachable) + BridgePingResult ping = await BridgeRuntimeControl.QueryPingAsync( + host, + port, + settings.statusConnectTimeoutMs, + settings.statusIoTimeoutMs, + token).ConfigureAwait(false); + if (ping.IsHealthy(acceptLoading: true)) { currentHost = host; currentPort = port; canReuseExistingEndpoint = true; } + else if (ping.IsError) + { + EmitProgress( + progress, + $"Bridge endpoint reports error, starting server to recover: {ping.Endpoint}. {SummarizeBridgeMessage(ping.Message)}"); + } else { - EmitProgress(progress, $"Bridge endpoint is unreachable, starting server to recover: {host}:{port}"); + EmitProgress( + progress, + $"Bridge endpoint is unreachable, starting server to recover: {host}:{port}. {SummarizeBridgeMessage(ping.Message)}"); } } } @@ -322,8 +335,13 @@ public async Task PingAsync(CancellationToken token, bool acceptLoading = return false; } - bool ok = await protocolClient.PingAsync(host, port, token, acceptLoading).ConfigureAwait(false); - if (!ok) + BridgePingResult ping = await BridgeRuntimeControl.QueryPingAsync( + host, + port, + settings.statusConnectTimeoutMs, + settings.statusIoTimeoutMs, + token).ConfigureAwait(false); + if (!ping.IsHealthy(acceptLoading)) { await InvalidateCurrentEndpointAsync().ConfigureAwait(false); return false; @@ -414,11 +432,47 @@ private async Task EnsureHealthyOrThrowAsync(CancellationToken token) string endpointBeforePing = TryResolveCurrentEndpoint(out string host, out int port) ? $"{host}:{port}" : "(none)"; - bool ok = await PingAsync(token, acceptLoading: true).ConfigureAwait(false); - if (!ok) + if (port <= 0) { throw new Exception($"Bridge port is unreachable. endpoint={endpointBeforePing}"); } + + BridgePingResult ping = await BridgeRuntimeControl.QueryPingAsync( + host, + port, + settings.statusConnectTimeoutMs, + settings.statusIoTimeoutMs, + token).ConfigureAwait(false); + if (ping.IsHealthy(acceptLoading: true)) + { + currentHost = host; + currentPort = port; + return; + } + + await InvalidateCurrentEndpointAsync().ConfigureAwait(false); + if (ping.IsError) + { + throw new Exception($"Bridge reported error. endpoint={ping.Endpoint}. {SummarizeBridgeMessage(ping.Message)}"); + } + + throw new Exception($"Bridge port is unreachable. endpoint={endpointBeforePing}. {SummarizeBridgeMessage(ping.Message)}"); + } + + private static string SummarizeBridgeMessage(string message, int maxLength = 500) + { + if (string.IsNullOrWhiteSpace(message)) + { + return string.Empty; + } + + string normalized = string.Join(" ", message.Split(new[] { '\r', '\n', '\t' }, StringSplitOptions.RemoveEmptyEntries)).Trim(); + if (normalized.Length <= maxLength) + { + return normalized; + } + + return normalized.Substring(0, maxLength) + "..."; } private async Task StopCoreAsync(CancellationToken token)