unity-mcp/MCPForUnity/Editor/Helpers/PackageLifecycleManager.cs

241 lines
8.4 KiB
C#
Raw Normal View History

Remove old UI and do lots of cleanup (#340) * Remove legacy UI and correct priority ordering of menu items * Remove old UI screen Users now have the new UI alone, less confusing and more predictable * Remove unused config files * Remove test for window that doesn't exist * Remove unused code * Remove dangling .meta file * refactor: remove client configuration step from setup wizard * refactor: remove menu item attributes and manual window actions from Python tool sync * feat: update minimum Python version requirement from 3.10 to 3.11 The docs have 3.12. However, feature wise it seems that 3.11 is required * fix: replace emoji warning symbol with unicode character in setup wizard dialogs * docs: reorganize images into docs/images directory and update references * docs: add UI preview image to README * docs: add run_test function and resources section to available tools list The recent changes should close #311 * fix: add SystemRoot env var to Windows config to support Python path resolution Closes #315 * refactor: consolidate package installation and detection into unified lifecycle manager Duplicate code for pretty much no reason, as they both initialized there was a small chance of a race condition as well. Consolidating made sense here * Doc fixes from CodeRabbit * Excellent bug catch from CodeRabbit * fix: preserve existing environment variables when updating codex server config * Update docs so the paths match the original name * style: fix list indentation in README-DEV.md development docs * refactor: simplify env table handling in CodexConfigHelper by removing preservation logic * refactor: simplify configuration logic by removing redundant change detection Always overwrite configs * feat: ensure config directory exists before writing config files * feat: persist server installation errors and show retry UI instead of auto-marking as handled * refactor: consolidate configuration helpers by merging McpConfigFileHelper into McpConfigurationHelper * Small fixes from CodeRabbit * Remove test because we overwrite Codex configs * Remove unused function * feat: improve server cleanup and process handling on Windows - Added DeleteDirectoryWithRetry helper to handle Windows file locking with retries and readonly attribute clearing - Implemented KillWindowsUvProcesses to safely terminate Python processes in virtual environments using WMIC - Extended TryKillUvForPath to work on Windows, preventing file handle locks during server deletion - Improved error messages to be more descriptive about file locking issues - Replaced direct Directory.Delete calls with * fix: improve TCP socket cleanup to prevent CLOSE_WAIT states - Added proper socket shutdown sequence using Socket.Shutdown() before closing connections - Enhanced error handling with specific catches for SocketException vs general exceptions - Added debug logging for socket shutdown errors to help diagnose connection issues - Restructured HandleClientAsync to ensure socket cleanup happens in the correct order - Implemented proper socket teardown in both client handling and connection cleanup paths
2025-10-24 12:50:29 +08:00
using System.IO;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Helpers
{
/// <summary>
/// Manages package lifecycle events including first-time installation,
/// version updates, and legacy installation detection.
/// Consolidates the functionality of PackageInstaller and PackageDetector.
/// </summary>
[InitializeOnLoad]
public static class PackageLifecycleManager
{
private const string VersionKeyPrefix = "MCPForUnity.InstalledVersion:";
private const string LegacyInstallFlagKey = "MCPForUnity.ServerInstalled"; // For migration
private const string InstallErrorKeyPrefix = "MCPForUnity.InstallError:"; // Stores last installation error
static PackageLifecycleManager()
{
// Schedule the check for after Unity is fully loaded
EditorApplication.delayCall += CheckAndInstallServer;
}
private static void CheckAndInstallServer()
{
try
{
string currentVersion = GetPackageVersion();
string versionKey = VersionKeyPrefix + currentVersion;
bool hasRunForThisVersion = EditorPrefs.GetBool(versionKey, false);
// Check for conditions that require installation/verification
bool isFirstTimeInstall = !EditorPrefs.HasKey(LegacyInstallFlagKey) && !hasRunForThisVersion;
bool legacyPresent = LegacyRootsExist();
bool canonicalMissing = !File.Exists(
Path.Combine(ServerInstaller.GetServerPath(), "server.py")
);
// Run if: first install, version update, legacy detected, or canonical missing
if (isFirstTimeInstall || !hasRunForThisVersion || legacyPresent || canonicalMissing)
{
PerformInstallation(currentVersion, versionKey, isFirstTimeInstall);
}
}
catch (System.Exception ex)
{
McpLog.Info($"Package lifecycle check failed: {ex.Message}. Open Window > MCP For Unity if needed.", always: false);
}
}
private static void PerformInstallation(string version, string versionKey, bool isFirstTimeInstall)
{
string error = null;
try
{
ServerInstaller.EnsureServerInstalled();
// Mark as installed for this version
EditorPrefs.SetBool(versionKey, true);
// Migrate legacy flag if this is first time
if (isFirstTimeInstall)
{
EditorPrefs.SetBool(LegacyInstallFlagKey, true);
}
// Clean up old version keys (keep only current version)
CleanupOldVersionKeys(version);
// Clean up legacy preference keys
CleanupLegacyPrefs();
// Only log success if server was actually embedded and copied
if (ServerInstaller.HasEmbeddedServer() && isFirstTimeInstall)
{
McpLog.Info("MCP server installation completed successfully.");
}
}
catch (System.Exception ex)
{
error = ex.Message;
// Store the error for display in the UI, but don't mark as handled
// This allows the user to manually rebuild via the "Rebuild Server" button
string errorKey = InstallErrorKeyPrefix + version;
EditorPrefs.SetString(errorKey, ex.Message ?? "Unknown error");
// Don't mark as installed - user needs to manually rebuild
}
if (!string.IsNullOrEmpty(error))
{
McpLog.Info($"Server installation failed: {error}. Use Window > MCP For Unity > Rebuild Server to retry.", always: false);
}
}
private static string GetPackageVersion()
{
try
{
var info = UnityEditor.PackageManager.PackageInfo.FindForAssembly(
typeof(PackageLifecycleManager).Assembly
);
if (info != null && !string.IsNullOrEmpty(info.version))
{
return info.version;
}
}
catch { }
// Fallback to embedded server version
return GetEmbeddedServerVersion();
}
private static string GetEmbeddedServerVersion()
{
try
{
if (ServerPathResolver.TryFindEmbeddedServerSource(out var embeddedSrc))
{
var versionPath = Path.Combine(embeddedSrc, "server_version.txt");
if (File.Exists(versionPath))
{
return File.ReadAllText(versionPath)?.Trim() ?? "unknown";
}
}
}
catch { }
return "unknown";
}
private static bool LegacyRootsExist()
{
try
{
string home = System.Environment.GetFolderPath(
System.Environment.SpecialFolder.UserProfile
) ?? string.Empty;
string[] legacyRoots =
{
Path.Combine(home, ".config", "UnityMCP", "UnityMcpServer", "src"),
Path.Combine(home, ".local", "share", "UnityMCP", "UnityMcpServer", "src")
};
foreach (var root in legacyRoots)
{
try
{
if (File.Exists(Path.Combine(root, "server.py")))
{
return true;
}
}
catch { }
}
}
catch { }
return false;
}
private static void CleanupOldVersionKeys(string currentVersion)
{
try
{
// Get all EditorPrefs keys that start with our version prefix
// Note: Unity doesn't provide a way to enumerate all keys, so we can only
// clean up known legacy keys. Future versions will be cleaned up when
// a newer version runs.
// This is a best-effort cleanup.
}
catch { }
}
private static void CleanupLegacyPrefs()
{
try
{
// Clean up old preference keys that are no longer used
string[] legacyKeys =
{
"MCPForUnity.ServerSrc",
"MCPForUnity.PythonDirOverride",
"MCPForUnity.LegacyDetectLogged" // Old prefix without version
};
foreach (var key in legacyKeys)
{
try
{
if (EditorPrefs.HasKey(key))
{
EditorPrefs.DeleteKey(key);
}
}
catch { }
}
}
catch { }
}
/// <summary>
/// Gets the last installation error for the current package version, if any.
/// Returns null if there was no error or the error has been cleared.
/// </summary>
public static string GetLastInstallError()
{
try
{
string currentVersion = GetPackageVersion();
string errorKey = InstallErrorKeyPrefix + currentVersion;
if (EditorPrefs.HasKey(errorKey))
{
return EditorPrefs.GetString(errorKey, null);
}
}
catch { }
return null;
}
/// <summary>
/// Clears the last installation error. Should be called after a successful manual rebuild.
/// </summary>
public static void ClearLastInstallError()
{
try
{
string currentVersion = GetPackageVersion();
string errorKey = InstallErrorKeyPrefix + currentVersion;
if (EditorPrefs.HasKey(errorKey))
{
EditorPrefs.DeleteKey(errorKey);
}
}
catch { }
}
}
}