Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions MCPForUnity/Editor/Services/IPackageUpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,23 @@ public interface IPackageUpdateService
/// <returns>Update check result containing availability and latest version info</returns>
UpdateCheckResult CheckForUpdate(string currentVersion);

/// <summary>
/// Returns a cached update result if one exists for today, or null if a network fetch is needed.
/// Main-thread only (reads EditorPrefs).
/// </summary>
UpdateCheckResult? TryGetCachedResult(string currentVersion);

/// <summary>
/// Performs only the network fetch and version comparison (no EditorPrefs access).
/// Safe to call from a background thread.
/// </summary>
UpdateCheckResult FetchAndCompare(string currentVersion);

/// <summary>
/// Caches a successful fetch result in EditorPrefs. Must be called from the main thread.
/// </summary>
void CacheFetchResult(string currentVersion, string fetchedVersion);

/// <summary>
/// Compares two version strings to determine if the first is newer than the second
/// </summary>
Expand Down
123 changes: 121 additions & 2 deletions MCPForUnity/Editor/Services/PackageUpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace MCPForUnity.Editor.Services
/// </summary>
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";
Expand Down Expand Up @@ -81,6 +82,88 @@ public UpdateCheckResult CheckForUpdate(string currentVersion)
};
}

/// <inheritdoc/>
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;
}

/// <inheritdoc/>
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)"
};
}

/// <inheritdoc/>
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);
}

/// <inheritdoc/>
public bool IsNewerVersion(string version1, string version2)
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}
}
}
}
61 changes: 50 additions & 11 deletions MCPForUnity/Editor/Windows/MCPForUnityEditorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -352,7 +353,7 @@ private void UpdateVersionLabel()

private void QueueUpdateCheck()
{
if (updateCheckQueued)
if (updateCheckQueued || updateCheckInFlight)
{
return;
}
Expand All @@ -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");
}
}
Expand Down