chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
|
||||
namespace MCPForUnity.Editor.Services
|
||||
{
|
||||
internal enum PackageJobStatus { Running, Succeeded, Failed }
|
||||
|
||||
internal sealed class PackageJob
|
||||
{
|
||||
public string JobId { get; set; }
|
||||
public PackageJobStatus Status { get; set; }
|
||||
public string Operation { get; set; }
|
||||
public string Package { get; set; }
|
||||
public long StartedUnixMs { get; set; }
|
||||
public long? FinishedUnixMs { get; set; }
|
||||
public long LastUpdateUnixMs { get; set; }
|
||||
public string Error { get; set; }
|
||||
public string ResultVersion { get; set; }
|
||||
public string ResultName { get; set; }
|
||||
}
|
||||
|
||||
internal static class PackageJobManager
|
||||
{
|
||||
private const string SessionKeyJobs = "MCPForUnity.PackageJobsV1";
|
||||
private const int MaxJobsToKeep = 10;
|
||||
private const long DomainReloadTimeoutMs = 120_000;
|
||||
|
||||
private static readonly object LockObj = new();
|
||||
private static readonly Dictionary<string, PackageJob> Jobs = new();
|
||||
|
||||
static PackageJobManager()
|
||||
{
|
||||
TryRestoreFromSessionState();
|
||||
}
|
||||
|
||||
private sealed class PersistedState
|
||||
{
|
||||
public List<PersistedJob> jobs { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PersistedJob
|
||||
{
|
||||
public string job_id { get; set; }
|
||||
public string status { get; set; }
|
||||
public string operation { get; set; }
|
||||
public string package_ { get; set; }
|
||||
public long started_unix_ms { get; set; }
|
||||
public long? finished_unix_ms { get; set; }
|
||||
public long last_update_unix_ms { get; set; }
|
||||
public string error { get; set; }
|
||||
public string result_version { get; set; }
|
||||
public string result_name { get; set; }
|
||||
}
|
||||
|
||||
private static PackageJobStatus ParseStatus(string status)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(status))
|
||||
return PackageJobStatus.Running;
|
||||
|
||||
return status.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"succeeded" => PackageJobStatus.Succeeded,
|
||||
"failed" => PackageJobStatus.Failed,
|
||||
_ => PackageJobStatus.Running
|
||||
};
|
||||
}
|
||||
|
||||
private static void TryRestoreFromSessionState()
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = SessionState.GetString(SessionKeyJobs, string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
return;
|
||||
|
||||
var state = JsonConvert.DeserializeObject<PersistedState>(json);
|
||||
if (state?.jobs == null)
|
||||
return;
|
||||
|
||||
lock (LockObj)
|
||||
{
|
||||
Jobs.Clear();
|
||||
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
foreach (var pj in state.jobs)
|
||||
{
|
||||
if (pj == null || string.IsNullOrWhiteSpace(pj.job_id))
|
||||
continue;
|
||||
|
||||
var job = new PackageJob
|
||||
{
|
||||
JobId = pj.job_id,
|
||||
Status = ParseStatus(pj.status),
|
||||
Operation = pj.operation,
|
||||
Package = pj.package_,
|
||||
StartedUnixMs = pj.started_unix_ms,
|
||||
FinishedUnixMs = pj.finished_unix_ms,
|
||||
LastUpdateUnixMs = pj.last_update_unix_ms,
|
||||
Error = pj.error,
|
||||
ResultVersion = pj.result_version,
|
||||
ResultName = pj.result_name
|
||||
};
|
||||
|
||||
// Domain reload recovery for running jobs
|
||||
if (job.Status == PackageJobStatus.Running)
|
||||
{
|
||||
TryRecoverJob(job, now);
|
||||
}
|
||||
|
||||
Jobs[pj.job_id] = job;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[PackageJobManager] Failed to restore SessionState: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal static void TryRecoverJob(PackageJob job, long nowMs)
|
||||
{
|
||||
try
|
||||
{
|
||||
string packageName = ExtractPackageName(job.Package);
|
||||
var allPackages = PackageInfo.GetAllRegisteredPackages();
|
||||
var info = FindPackageInfo(allPackages, packageName, job.Package);
|
||||
|
||||
if (job.Operation == "add" || job.Operation == "embed")
|
||||
{
|
||||
if (info != null)
|
||||
{
|
||||
job.Status = PackageJobStatus.Succeeded;
|
||||
job.FinishedUnixMs = nowMs;
|
||||
job.LastUpdateUnixMs = nowMs;
|
||||
job.ResultVersion = info.version;
|
||||
job.ResultName = info.name;
|
||||
McpLog.Info($"[PackageJobManager] Recovered {job.Operation} job {job.JobId}: {info.name}@{info.version} installed.");
|
||||
}
|
||||
else if (nowMs - job.StartedUnixMs > DomainReloadTimeoutMs)
|
||||
{
|
||||
job.Status = PackageJobStatus.Failed;
|
||||
job.FinishedUnixMs = nowMs;
|
||||
job.LastUpdateUnixMs = nowMs;
|
||||
job.Error = $"Package {job.Operation} timed out after domain reload.";
|
||||
McpLog.Warn($"[PackageJobManager] Timed out {job.Operation} job {job.JobId} for '{job.Package}'.");
|
||||
}
|
||||
}
|
||||
else if (job.Operation == "remove")
|
||||
{
|
||||
if (info == null)
|
||||
{
|
||||
job.Status = PackageJobStatus.Succeeded;
|
||||
job.FinishedUnixMs = nowMs;
|
||||
job.LastUpdateUnixMs = nowMs;
|
||||
McpLog.Info($"[PackageJobManager] Recovered remove job {job.JobId}: '{packageName}' is no longer installed.");
|
||||
}
|
||||
else if (nowMs - job.StartedUnixMs > DomainReloadTimeoutMs)
|
||||
{
|
||||
job.Status = PackageJobStatus.Failed;
|
||||
job.FinishedUnixMs = nowMs;
|
||||
job.LastUpdateUnixMs = nowMs;
|
||||
job.Error = "Package removal timed out after domain reload.";
|
||||
McpLog.Warn($"[PackageJobManager] Timed out remove job {job.JobId} for '{job.Package}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[PackageJobManager] Recovery check failed for job {job.JobId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find a PackageInfo by name, falling back to packageId or git/local source for non-standard identifiers.
|
||||
/// </summary>
|
||||
private static PackageInfo FindPackageInfo(PackageInfo[] allPackages, string packageName, string originalIdentifier)
|
||||
{
|
||||
// Direct name match (handles normal com.company.package identifiers)
|
||||
var info = allPackages.FirstOrDefault(p =>
|
||||
string.Equals(p.name, packageName, StringComparison.OrdinalIgnoreCase));
|
||||
if (info != null)
|
||||
return info;
|
||||
|
||||
// For git URLs / file: paths, packageName == originalIdentifier and won't match .name.
|
||||
// Try matching by packageId or source (git/local).
|
||||
bool isGitOrFile = originalIdentifier.StartsWith("http", StringComparison.OrdinalIgnoreCase)
|
||||
|| originalIdentifier.StartsWith("git", StringComparison.OrdinalIgnoreCase)
|
||||
|| originalIdentifier.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
|
||||
|| originalIdentifier.EndsWith(".git", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!isGitOrFile)
|
||||
return null;
|
||||
|
||||
return allPackages.FirstOrDefault(p =>
|
||||
p.source == PackageSource.Git || p.source == PackageSource.Local
|
||||
? p.packageId != null && p.packageId.Contains(originalIdentifier)
|
||||
|| p.resolvedPath != null && p.resolvedPath.Contains(originalIdentifier)
|
||||
: false);
|
||||
}
|
||||
|
||||
internal static string ExtractPackageName(string packageIdentifier)
|
||||
{
|
||||
if (string.IsNullOrEmpty(packageIdentifier))
|
||||
return packageIdentifier;
|
||||
|
||||
// Strip version: "com.unity.foo@1.0.0" -> "com.unity.foo"
|
||||
int atIndex = packageIdentifier.IndexOf('@');
|
||||
if (atIndex > 0)
|
||||
return packageIdentifier.Substring(0, atIndex);
|
||||
|
||||
// Git URLs and file: paths — can't reliably extract name, return as-is
|
||||
return packageIdentifier;
|
||||
}
|
||||
|
||||
internal static void PersistToSessionState()
|
||||
{
|
||||
try
|
||||
{
|
||||
PersistedState snapshot;
|
||||
lock (LockObj)
|
||||
{
|
||||
var jobs = Jobs.Values
|
||||
.OrderByDescending(j => j.LastUpdateUnixMs)
|
||||
.Take(MaxJobsToKeep)
|
||||
.Select(j => new PersistedJob
|
||||
{
|
||||
job_id = j.JobId,
|
||||
status = j.Status.ToString().ToLowerInvariant(),
|
||||
operation = j.Operation,
|
||||
package_ = j.Package,
|
||||
started_unix_ms = j.StartedUnixMs,
|
||||
finished_unix_ms = j.FinishedUnixMs,
|
||||
last_update_unix_ms = j.LastUpdateUnixMs,
|
||||
error = j.Error,
|
||||
result_version = j.ResultVersion,
|
||||
result_name = j.ResultName
|
||||
})
|
||||
.ToList();
|
||||
|
||||
snapshot = new PersistedState { jobs = jobs };
|
||||
}
|
||||
|
||||
SessionState.SetString(SessionKeyJobs, JsonConvert.SerializeObject(snapshot));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[PackageJobManager] Failed to persist SessionState: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static string StartJob(string operation, string package)
|
||||
{
|
||||
string jobId = Guid.NewGuid().ToString("N");
|
||||
long started = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
var job = new PackageJob
|
||||
{
|
||||
JobId = jobId,
|
||||
Status = PackageJobStatus.Running,
|
||||
Operation = operation,
|
||||
Package = package,
|
||||
StartedUnixMs = started,
|
||||
FinishedUnixMs = null,
|
||||
LastUpdateUnixMs = started,
|
||||
Error = null,
|
||||
ResultVersion = null,
|
||||
ResultName = null
|
||||
};
|
||||
|
||||
lock (LockObj)
|
||||
{
|
||||
Jobs[jobId] = job;
|
||||
}
|
||||
PersistToSessionState();
|
||||
return jobId;
|
||||
}
|
||||
|
||||
public static void CompleteJob(string jobId, bool success, string error = null,
|
||||
string version = null, string name = null)
|
||||
{
|
||||
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
lock (LockObj)
|
||||
{
|
||||
if (!Jobs.TryGetValue(jobId, out var job))
|
||||
return;
|
||||
|
||||
job.Status = success ? PackageJobStatus.Succeeded : PackageJobStatus.Failed;
|
||||
job.FinishedUnixMs = now;
|
||||
job.LastUpdateUnixMs = now;
|
||||
job.Error = error;
|
||||
job.ResultVersion = version;
|
||||
job.ResultName = name;
|
||||
}
|
||||
PersistToSessionState();
|
||||
}
|
||||
|
||||
public static PackageJob GetJob(string jobId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(jobId))
|
||||
return null;
|
||||
|
||||
lock (LockObj)
|
||||
{
|
||||
Jobs.TryGetValue(jobId, out var job);
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
public static PackageJob GetLatestJob()
|
||||
{
|
||||
lock (LockObj)
|
||||
{
|
||||
return Jobs.Values
|
||||
.OrderByDescending(j => j.StartedUnixMs)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
public static object ToSerializable(PackageJob job)
|
||||
{
|
||||
if (job == null)
|
||||
return null;
|
||||
|
||||
return new
|
||||
{
|
||||
job_id = job.JobId,
|
||||
status = job.Status.ToString().ToLowerInvariant(),
|
||||
operation = job.Operation,
|
||||
package_ = job.Package,
|
||||
started_unix_ms = job.StartedUnixMs,
|
||||
finished_unix_ms = job.FinishedUnixMs,
|
||||
last_update_unix_ms = job.LastUpdateUnixMs,
|
||||
error = job.Error,
|
||||
result_version = job.ResultVersion,
|
||||
result_name = job.ResultName
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user