using System; using System.Collections.Generic; using System.Linq; using Codely.Newtonsoft.Json.Linq; using UnityEditor; using UnityEditor.PackageManager; using UnityEditor.PackageManager.Requests; using UnityEngine; using UnityTcp.Editor.Helpers; namespace UnityTcp.Editor.Tools { /// /// [EXPERIMENTAL] Handles Unity Package Manager (UPM) operations. /// Supports installing, removing, and querying packages. /// Compatible with Unity 2022.3 LTS. /// public static class ManagePackage { private static readonly Dictionary _activeRequests = new Dictionary(); private static readonly Dictionary _updateCallbacks = new Dictionary(); private static readonly object _requestLock = new object(); public static object HandleCommand(JObject @params) { string action = @params["action"]?.ToString().ToLower(); if (string.IsNullOrEmpty(action)) { return Response.Error("Action parameter is required."); } try { switch (action) { case "install_package": return InstallPackage(@params); case "remove_package": return RemovePackage(@params); case "wait_for_upm": return WaitForUpm(@params); case "list_packages": return ListPackages(); default: return Response.Error( $"Unknown action: '{action}'. Valid actions: install_package, remove_package, wait_for_upm, list_packages." ); } } catch (Exception e) { Debug.LogError($"[ManagePackage] Action '{action}' failed: {e}"); return Response.Error($"[EXPERIMENTAL] Package operation failed: {e.Message}"); } } private static object InstallPackage(JObject @params) { try { string idOrUrl = @params["id_or_url"]?.ToString(); if (string.IsNullOrEmpty(idOrUrl)) return Response.Error("'id_or_url' parameter required for install_package."); // Handle optional version parameter string version = @params["version"]?.ToString(); // Append version to package identifier if provided and not already in format // e.g., "com.unity.package" + "1.2.3" -> "com.unity.package@1.2.3" if (!string.IsNullOrEmpty(version) && !idOrUrl.Contains("@") && !idOrUrl.StartsWith("http")) { idOrUrl = $"{idOrUrl}@{version}"; } // Create job var job = AsyncOperationTracker.CreateJob( AsyncOperationTracker.JobType.UpmPackage, $"Installing package: {idOrUrl}" ); // Start UPM request AddRequest addRequest = Client.Add(idOrUrl); lock (_requestLock) { _activeRequests[job.OpId] = addRequest; } // Create and store callback delegate for proper unsubscription EditorApplication.CallbackFunction callback = () => CheckUpmRequest(job.OpId); lock (_requestLock) { _updateCallbacks[job.OpId] = callback; } EditorApplication.update += callback; // Return standardized pending response var response = AsyncOperationTracker.CreatePendingResponse(job) as Dictionary; response["poll_interval"] = 2.0; // UPM needs longer poll interval response["message"] = $"[EXPERIMENTAL] Package installation started: {idOrUrl}"; response["data"] = new { package = idOrUrl, type = "install" }; return response; } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to start package installation: {e.Message}"); } } private static object RemovePackage(JObject @params) { try { string packageName = @params["package_name"]?.ToString(); if (string.IsNullOrEmpty(packageName)) return Response.Error("'package_name' parameter required for remove_package."); var job = AsyncOperationTracker.CreateJob( AsyncOperationTracker.JobType.UpmPackage, $"Removing package: {packageName}" ); RemoveRequest removeRequest = Client.Remove(packageName); lock (_requestLock) { _activeRequests[job.OpId] = removeRequest; } // Create and store callback delegate for proper unsubscription EditorApplication.CallbackFunction callback = () => CheckUpmRequest(job.OpId); lock (_requestLock) { _updateCallbacks[job.OpId] = callback; } EditorApplication.update += callback; // Return standardized pending response var response = AsyncOperationTracker.CreatePendingResponse(job) as Dictionary; response["poll_interval"] = 2.0; // UPM needs longer poll interval response["message"] = $"[EXPERIMENTAL] Package removal started: {packageName}"; response["data"] = new { package = packageName, type = "remove" }; return response; } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to start package removal: {e.Message}"); } } private static void CheckUpmRequest(string opId) { Request request; EditorApplication.CallbackFunction callback; lock (_requestLock) { if (!_activeRequests.TryGetValue(opId, out request)) { return; } _updateCallbacks.TryGetValue(opId, out callback); } if (request.IsCompleted) { // Properly unsubscribe using stored delegate if (callback != null) { EditorApplication.update -= callback; } lock (_requestLock) { _activeRequests.Remove(opId); _updateCallbacks.Remove(opId); } if (request.Status == StatusCode.Success) { AsyncOperationTracker.CompleteJob(opId, "Package operation completed successfully"); StateComposer.IncrementRevision(); } else { AsyncOperationTracker.FailJob(opId, $"Package operation failed: {request.Error?.message}"); } } } private static object WaitForUpm(JObject @params) { try { string opId = @params["op_id"]?.ToString(); int timeoutSeconds = @params["timeoutSeconds"]?.ToObject() ?? 300; if (string.IsNullOrEmpty(opId)) return Response.Error("'op_id' parameter required for wait_for_upm."); var job = AsyncOperationTracker.GetJob(opId); if (job == null) return Response.Error($"Operation {opId} not found."); if (job.Type != AsyncOperationTracker.JobType.UpmPackage) return Response.Error($"Operation {opId} is not a UPM operation."); if (AsyncOperationTracker.IsJobTimedOut(opId, timeoutSeconds)) { AsyncOperationTracker.FailJob(opId, $"UPM operation timed out after {timeoutSeconds} seconds"); return AsyncOperationTracker.CreateErrorResponse(job); } switch (job.Status) { case AsyncOperationTracker.JobStatus.Complete: return AsyncOperationTracker.CreateCompleteResponse(job); case AsyncOperationTracker.JobStatus.Error: return AsyncOperationTracker.CreateErrorResponse(job); case AsyncOperationTracker.JobStatus.Pending: return AsyncOperationTracker.CreatePendingResponse(job); default: return Response.Error($"Unknown job status: {job.Status}"); } } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to wait for UPM: {e.Message}"); } } private static object ListPackages() { try { ListRequest listRequest = Client.List(true, false); // Wait for request to complete (synchronous for simplicity) while (!listRequest.IsCompleted) { System.Threading.Thread.Sleep(100); } if (listRequest.Status == StatusCode.Success) { var packages = listRequest.Result.Select(p => new { name = p.name, version = p.version, displayName = p.displayName, description = p.description, source = p.source.ToString() }).ToList(); return Response.Success( $"[EXPERIMENTAL] Retrieved {packages.Count} packages.", packages ); } else { return Response.Error($"[EXPERIMENTAL] Failed to list packages: {listRequest.Error?.message}"); } } catch (Exception e) { return Response.Error($"[EXPERIMENTAL] Failed to list packages: {e.Message}"); } } } }