diff --git a/MCPForUnity/Editor/Services/IPackageUpdateService.cs b/MCPForUnity/Editor/Services/IPackageUpdateService.cs index 9d4d2e487..9211c1c70 100644 --- a/MCPForUnity/Editor/Services/IPackageUpdateService.cs +++ b/MCPForUnity/Editor/Services/IPackageUpdateService.cs @@ -12,6 +12,23 @@ public interface IPackageUpdateService /// Update check result containing availability and latest version info UpdateCheckResult CheckForUpdate(string currentVersion); + /// + /// Returns a cached update result if one exists for today, or null if a network fetch is needed. + /// Main-thread only (reads EditorPrefs). + /// + UpdateCheckResult? TryGetCachedResult(string currentVersion); + + /// + /// Performs only the network fetch and version comparison (no EditorPrefs access). + /// Safe to call from a background thread. + /// + UpdateCheckResult FetchAndCompare(string currentVersion); + + /// + /// Caches a successful fetch result in EditorPrefs. Must be called from the main thread. + /// + void CacheFetchResult(string currentVersion, string fetchedVersion); + /// /// Compares two version strings to determine if the first is newer than the second /// diff --git a/MCPForUnity/Editor/Services/PackageUpdateService.cs b/MCPForUnity/Editor/Services/PackageUpdateService.cs index 5f594e9ca..dab04098c 100644 --- a/MCPForUnity/Editor/Services/PackageUpdateService.cs +++ b/MCPForUnity/Editor/Services/PackageUpdateService.cs @@ -14,6 +14,7 @@ namespace MCPForUnity.Editor.Services /// public class PackageUpdateService : IPackageUpdateService { + private const int DefaultRequestTimeoutMs = 3000; private const string LastCheckDateKey = EditorPrefKeys.LastUpdateCheck; private const string CachedVersionKey = EditorPrefKeys.LatestKnownVersion; private const string LastBetaCheckDateKey = EditorPrefKeys.LastUpdateCheck + ".beta"; @@ -81,6 +82,88 @@ public UpdateCheckResult CheckForUpdate(string currentVersion) }; } + /// + public UpdateCheckResult? TryGetCachedResult(string currentVersion) + { + bool isGitInstallation = IsGitInstallation(); + string gitBranch = isGitInstallation ? GetGitUpdateBranch(currentVersion) : "main"; + bool useBetaChannel = isGitInstallation && string.Equals(gitBranch, "beta", StringComparison.OrdinalIgnoreCase); + + string lastCheckKey = isGitInstallation + ? (useBetaChannel ? LastBetaCheckDateKey : LastCheckDateKey) + : LastAssetStoreCheckDateKey; + string cachedVersionKey = isGitInstallation + ? (useBetaChannel ? CachedBetaVersionKey : CachedVersionKey) + : CachedAssetStoreVersionKey; + + string lastCheckDate = EditorPrefs.GetString(lastCheckKey, ""); + string cachedLatestVersion = EditorPrefs.GetString(cachedVersionKey, ""); + + if (lastCheckDate == DateTime.Now.ToString("yyyy-MM-dd") && !string.IsNullOrEmpty(cachedLatestVersion)) + { + return new UpdateCheckResult + { + CheckSucceeded = true, + LatestVersion = cachedLatestVersion, + UpdateAvailable = IsNewerVersion(cachedLatestVersion, currentVersion), + Message = "Using cached version check" + }; + } + + return null; + } + + /// + public UpdateCheckResult FetchAndCompare(string currentVersion) + { + bool isGitInstallation = IsGitInstallation(); + string gitBranch = isGitInstallation ? GetGitUpdateBranch(currentVersion) : "main"; + + string latestVersion = isGitInstallation + ? FetchLatestVersionFromGitHub(gitBranch) + : FetchLatestVersionFromAssetStoreJson(); + + if (!string.IsNullOrEmpty(latestVersion)) + { + return new UpdateCheckResult + { + CheckSucceeded = true, + LatestVersion = latestVersion, + UpdateAvailable = IsNewerVersion(latestVersion, currentVersion), + Message = "Successfully checked for updates" + }; + } + + return new UpdateCheckResult + { + CheckSucceeded = false, + UpdateAvailable = false, + Message = isGitInstallation + ? "Failed to check for updates (network issue or offline)" + : "Failed to check for Asset Store updates (network issue or offline)" + }; + } + + /// + public void CacheFetchResult(string currentVersion, string fetchedVersion) + { + if (string.IsNullOrEmpty(fetchedVersion)) return; + + bool isGitInstallation = IsGitInstallation(); + string gitBranch = isGitInstallation ? GetGitUpdateBranch(currentVersion) : "main"; + bool useBetaChannel = isGitInstallation && string.Equals(gitBranch, "beta", StringComparison.OrdinalIgnoreCase); + + string lastCheckKey = isGitInstallation + ? (useBetaChannel ? LastBetaCheckDateKey : LastCheckDateKey) + : LastAssetStoreCheckDateKey; + string cachedVersionKey = isGitInstallation + ? (useBetaChannel ? CachedBetaVersionKey : CachedVersionKey) + : CachedAssetStoreVersionKey; + + EditorPrefs.SetString(lastCheckKey, DateTime.Now.ToString("yyyy-MM-dd")); + EditorPrefs.SetString(cachedVersionKey, fetchedVersion); + } + /// public bool IsNewerVersion(string version1, string version2) { @@ -265,7 +348,7 @@ protected virtual string FetchLatestVersionFromGitHub(string branch) // - More reliable - doesn't require releases to be published // - Direct source of truth from the main branch - using (var client = new WebClient()) + using (var client = CreateWebClient()) { client.Headers.Add("User-Agent", "Unity-MCPForUnity-UpdateChecker"); string packageJsonUrl = string.Equals(branch, "beta", StringComparison.OrdinalIgnoreCase) @@ -304,7 +387,7 @@ protected virtual string FetchLatestVersionFromAssetStoreJson() { try { - using (var client = new WebClient()) + using (var client = CreateWebClient()) { client.Headers.Add("User-Agent", "Unity-MCPForUnity-AssetStoreUpdateChecker"); string jsonContent = client.DownloadString(AssetStoreVersionUrl); @@ -322,5 +405,41 @@ protected virtual string FetchLatestVersionFromAssetStoreJson() return null; } } + + protected virtual WebClient CreateWebClient() + { + return new TimeoutWebClient(GetRequestTimeoutMs()); + } + + protected virtual int GetRequestTimeoutMs() + { + return DefaultRequestTimeoutMs; + } + + private sealed class TimeoutWebClient : WebClient + { + private readonly int _timeoutMs; + + public TimeoutWebClient(int timeoutMs) + { + _timeoutMs = timeoutMs; + } + + protected override WebRequest GetWebRequest(Uri address) + { + var request = base.GetWebRequest(address); + if (request != null) + { + request.Timeout = _timeoutMs; + + if (request is HttpWebRequest httpRequest) + { + httpRequest.ReadWriteTimeout = _timeoutMs; + } + } + + return request; + } + } } } diff --git a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs index bf41353a6..174177ffc 100644 --- a/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs +++ b/MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs @@ -52,6 +52,7 @@ public class MCPForUnityEditorWindow : EditorWindow private double lastRefreshTime = 0; private const double RefreshDebounceSeconds = 0.5; private bool updateCheckQueued = false; + private bool updateCheckInFlight = false; private enum ActivePanel { @@ -352,7 +353,7 @@ private void UpdateVersionLabel() private void QueueUpdateCheck() { - if (updateCheckQueued) + if (updateCheckQueued || updateCheckInFlight) { return; } @@ -377,23 +378,61 @@ private void CheckForPackageUpdates() return; } - try + // Main thread: resolve service + read EditorPrefs cache (both require main thread) + var updateService = MCPServiceLocator.Updates; + var cachedResult = updateService.TryGetCachedResult(currentVersion); + if (cachedResult != null) { - var result = MCPServiceLocator.Updates.CheckForUpdate(currentVersion); - if (result.CheckSucceeded && result.UpdateAvailable && !string.IsNullOrEmpty(result.LatestVersion)) + ApplyUpdateCheckResult(cachedResult, currentVersion); + return; + } + + // Background thread: network I/O only (no EditorPrefs access) + updateCheckInFlight = true; + Task.Run(() => + { + try { - updateNotificationText.text = $"Update available: v{result.LatestVersion} (current: v{currentVersion})"; - updateNotificationText.tooltip = $"Latest version: v{result.LatestVersion}\nCurrent version: v{currentVersion}"; - updateNotification.AddToClassList("visible"); + return updateService.FetchAndCompare(currentVersion); } - else + catch (Exception ex) { - updateNotification.RemoveFromClassList("visible"); + McpLog.Info($"Package update check skipped: {ex.Message}"); + return null; } + }).ContinueWith(t => + { + EditorApplication.delayCall += () => + { + updateCheckInFlight = false; + + // Main thread: cache the result in EditorPrefs + var result = t.Status == TaskStatus.RanToCompletion ? t.Result : null; + if (result != null && result.CheckSucceeded && !string.IsNullOrEmpty(result.LatestVersion)) + { + updateService.CacheFetchResult(currentVersion, result.LatestVersion); + } + + if (this == null || updateNotification == null || updateNotificationText == null) + { + return; + } + + ApplyUpdateCheckResult(result, currentVersion); + }; + }, TaskScheduler.Default); + } + + private void ApplyUpdateCheckResult(UpdateCheckResult result, string currentVersion) + { + if (result != null && result.CheckSucceeded && result.UpdateAvailable && !string.IsNullOrEmpty(result.LatestVersion)) + { + updateNotificationText.text = $"Update available: v{result.LatestVersion} (current: v{currentVersion})"; + updateNotificationText.tooltip = $"Latest version: v{result.LatestVersion}\nCurrent version: v{currentVersion}"; + updateNotification.AddToClassList("visible"); } - catch (Exception ex) + else { - McpLog.Info($"Package update check skipped: {ex.Message}"); updateNotification.RemoveFromClassList("visible"); } }