Files
2026-03-09 17:50:20 +08:00

272 lines
10 KiB
C#

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
{
/// <summary>
/// [EXPERIMENTAL] Handles Unity Package Manager (UPM) operations.
/// Supports installing, removing, and querying packages.
/// Compatible with Unity 2022.3 LTS.
/// </summary>
public static class ManagePackage
{
private static readonly Dictionary<string, Request> _activeRequests = new Dictionary<string, Request>();
private static readonly Dictionary<string, EditorApplication.CallbackFunction> _updateCallbacks = new Dictionary<string, EditorApplication.CallbackFunction>();
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<string, object>;
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<string, object>;
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<int?>() ?? 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}");
}
}
}
}