Initial commit: AIGame Unity client with URP, HybridCLR, YooAsset and tooling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+656
@@ -0,0 +1,656 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
using Unity.Pipeline.Commands;
|
||||
using UnityEditor;
|
||||
using UnityEditor.PackageManager;
|
||||
using UnityEditor.PackageManager.Requests;
|
||||
using UnityEngine;
|
||||
using PackageInfo = UnityEditor.PackageManager.PackageInfo;
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
using Unity.Scripting.LifecycleManagement;
|
||||
#endif
|
||||
|
||||
namespace Unity.Pipeline.Editor.Commands.PackageManager
|
||||
{
|
||||
/// <summary>
|
||||
/// UPM package management over the Client API (CLI-203): list / search / add / remove / resolve.
|
||||
///
|
||||
/// Threading: UPM <see cref="Client"/> operations are asynchronous (the <see cref="Request"/> only
|
||||
/// progresses while the editor ticks) and their members are main-thread only. Commands that wait on a
|
||||
/// request therefore run off the main thread (<c>MainThreadRequired = false</c>) and marshal every
|
||||
/// UPM touch back onto the main thread via the server dispatcher (<see cref="RunOnMain"/>), so they
|
||||
/// can block without freezing the editor.
|
||||
///
|
||||
/// Command surface:
|
||||
/// - <c>package_list</c> (installed scope) reads the resolved set inline; <c>available</c>/<c>all</c>
|
||||
/// and <c>package_search</c> query the registry and wait for it — all return <b>synchronously</b>.
|
||||
/// - <c>package_add</c>/<c>package_remove</c> mutate the manifest and trigger a <b>domain reload</b>
|
||||
/// that tears down the in-flight request and the HTTP connection. They are <b>dual-mode</b>:
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>async (default)</b> — kick the op off, return <c>in_progress</c> immediately, and let
|
||||
/// an <see cref="EditorApplication.update"/> poller finalize a Temp status file. Poll
|
||||
/// <c>package_status</c> until <c>completed</c>/<c>failed</c>, then <c>recompile_status</c>. This
|
||||
/// keeps the (single-request) server responsive and survives the reload.</description></item>
|
||||
/// <item><description><b>synchronous</b> (<c>wait=true</c>) — block until the request completes and return the
|
||||
/// full result. The result is captured in one main-thread hop the moment the request completes
|
||||
/// (before the compile-triggered reload). Reliable for add; for remove the reload can fire fast
|
||||
/// enough to drop the reply — the status file still settles, so <c>package_status</c> confirms it.</description></item>
|
||||
/// </list>
|
||||
/// Both write the status file, so a lost reply (or a reload mid-wait) is recoverable via
|
||||
/// <c>package_status</c>; <see cref="RecoverInterruptedOperation"/> settles an <c>in_progress</c>
|
||||
/// file on the next load.
|
||||
/// - <c>package_resolve</c> records its status too, so <c>package_status</c> validates it.
|
||||
///
|
||||
/// Mutating commands (add / remove) follow the shared <c>confirm</c>/<c>dry_run</c> convention.
|
||||
/// UPM operations are not part of Unity's Undo.
|
||||
/// </summary>
|
||||
[InitializeOnLoad]
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
[NoAutoStaticsCleanup]
|
||||
#endif
|
||||
static class PackageManagerCommand
|
||||
{
|
||||
const string StatusFile = "Temp/pipeline_package_status.json";
|
||||
|
||||
// Bound on a synchronous wait, kept under the CLI's default request timeout so a stuck operation
|
||||
// surfaces as a clean error rather than a dropped connection.
|
||||
const int WaitTimeoutSeconds = 25;
|
||||
const int PollIntervalMs = 50;
|
||||
|
||||
static readonly object s_Lock = new object();
|
||||
|
||||
// The in-flight mutating request (add/remove) and how to project its result. s_InProgress is a
|
||||
// plain flag (read off-thread by IsBusy) so the busy check never touches main-thread-only Request
|
||||
// members. Both are reset by a domain reload; RecoverInterruptedOperation settles the status file.
|
||||
static volatile bool s_InProgress;
|
||||
static Request s_Request;
|
||||
static string s_Operation;
|
||||
static string s_Argument;
|
||||
static Func<Request, PackageSummary> s_ReadPackage;
|
||||
|
||||
static PackageManagerCommand()
|
||||
{
|
||||
RecoverInterruptedOperation();
|
||||
}
|
||||
|
||||
// ---- List / search (synchronous) -------------------------------------------------------
|
||||
|
||||
[CliCommand("package_list",
|
||||
"List packages by scope: installed (default) | available (registry) | all (both). Returns the full " +
|
||||
"result synchronously — available/all block until the registry query completes.",
|
||||
MainThreadRequired = false,
|
||||
Tags = new[] { "packages" })]
|
||||
public static object PackageList(
|
||||
[CliArg("scope", "Which packages to list: installed (default) | available | all.")] string scope = "installed",
|
||||
[CliArg("include_indirect", "Include indirect (transitive) installed dependencies (applies to scope=installed/all).")]
|
||||
bool includeIndirect = true,
|
||||
[CliArg("offline", "For available/all: query the local cache instead of the registry.")] bool offline = false)
|
||||
{
|
||||
switch ((scope ?? "installed").Trim().ToLowerInvariant())
|
||||
{
|
||||
case "installed":
|
||||
return RunOnMain(() => ListInstalled(includeIndirect));
|
||||
case "available":
|
||||
return ListFromRegistry("available", includeIndirect, offline);
|
||||
case "all":
|
||||
return ListFromRegistry("all", includeIndirect, offline);
|
||||
default:
|
||||
return new PackageListResponse
|
||||
{
|
||||
Success = false,
|
||||
Scope = scope,
|
||||
Message = $"Unknown scope '{scope}'. Use installed | available | all."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Installed scope: <see cref="PackageInfo.GetAllRegisteredPackages"/> reflects the resolved
|
||||
/// set without a registry round-trip. Runs on the main thread (via <see cref="RunOnMain"/>).
|
||||
/// </summary>
|
||||
static PackageListResponse ListInstalled(bool includeIndirect)
|
||||
{
|
||||
var summaries = new List<PackageSummary>();
|
||||
foreach (var p in PackageInfo.GetAllRegisteredPackages())
|
||||
{
|
||||
if (includeIndirect || p.isDirectDependency)
|
||||
summaries.Add(Map(p, isInstalled: true));
|
||||
}
|
||||
|
||||
return new PackageListResponse
|
||||
{
|
||||
Success = true,
|
||||
Scope = "installed",
|
||||
Count = summaries.Count,
|
||||
Packages = summaries,
|
||||
Manifest = SafeReadManifest(),
|
||||
Message = $"{summaries.Count} installed package(s)."
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// available / all scope: these need the registry, so a <c>Client.SearchAll</c> request is
|
||||
/// started on the main thread and awaited here (on the command's background thread). For
|
||||
/// <c>all</c>, the installed set is merged in (and marked) so callers see one unified list.
|
||||
/// </summary>
|
||||
static object ListFromRegistry(string scope, bool includeIndirect, bool offline)
|
||||
{
|
||||
var request = RunOnMain(() => Client.SearchAll(offline));
|
||||
|
||||
if (!WaitForCompletion(request, out var waitError))
|
||||
return new PackageListResponse { Success = false, Scope = scope, Message = waitError };
|
||||
|
||||
return RunOnMain<object>(() =>
|
||||
{
|
||||
if (request.Status != StatusCode.Success)
|
||||
return new PackageListResponse
|
||||
{
|
||||
Success = false,
|
||||
Scope = scope,
|
||||
Message = $"Registry query failed: {request.Error?.message ?? "unknown error"}"
|
||||
};
|
||||
|
||||
var packages = BuildScopedList(scope, request.Result, includeIndirect);
|
||||
return new PackageListResponse
|
||||
{
|
||||
Success = true,
|
||||
Scope = scope,
|
||||
Count = packages.Count,
|
||||
Packages = packages,
|
||||
Manifest = SafeReadManifest(),
|
||||
Message = $"{packages.Count} package(s)."
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Combine the registry search result with the installed set per <paramref name="scope"/>:
|
||||
/// <c>available</c> returns every registry package (flagged whether it is installed);
|
||||
/// <c>all</c> returns installed packages (resolved info) plus the registry packages that are
|
||||
/// not installed.
|
||||
/// </summary>
|
||||
static List<PackageSummary> BuildScopedList(string scope, PackageInfo[] available, bool includeIndirect)
|
||||
{
|
||||
var installed = new Dictionary<string, PackageInfo>();
|
||||
foreach (var p in PackageInfo.GetAllRegisteredPackages())
|
||||
installed[p.name] = p;
|
||||
|
||||
var result = new List<PackageSummary>();
|
||||
|
||||
if (scope == "available")
|
||||
{
|
||||
foreach (var p in available ?? Array.Empty<PackageInfo>())
|
||||
result.Add(Map(p, isInstalled: installed.ContainsKey(p.name)));
|
||||
return result;
|
||||
}
|
||||
|
||||
// scope == "all": installed entries first (with their resolved info), then registry-only ones.
|
||||
var seen = new HashSet<string>();
|
||||
foreach (var entry in installed.Values)
|
||||
{
|
||||
if (!includeIndirect && !entry.isDirectDependency)
|
||||
continue;
|
||||
result.Add(Map(entry, isInstalled: true));
|
||||
seen.Add(entry.name);
|
||||
}
|
||||
foreach (var p in available ?? Array.Empty<PackageInfo>())
|
||||
{
|
||||
if (!seen.Contains(p.name))
|
||||
result.Add(Map(p, isInstalled: false));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
[CliCommand("package_search",
|
||||
"Search packages available in the registry. Provide a name (e.g. com.unity.foo) or omit to list all. " +
|
||||
"Returns the full result synchronously (blocks until the registry query completes).",
|
||||
MainThreadRequired = false,
|
||||
Tags = new[] { "packages" })]
|
||||
public static object PackageSearch(
|
||||
[CliArg("query", "Package name to search for. Omit/empty to list all available packages.")] string query = "",
|
||||
[CliArg("offline", "Search the local cache only.")] bool offline = false)
|
||||
{
|
||||
var trimmed = (query ?? string.Empty).Trim();
|
||||
|
||||
var request = RunOnMain(() => string.IsNullOrEmpty(trimmed)
|
||||
? Client.SearchAll(offline)
|
||||
: Client.Search(trimmed, offline));
|
||||
|
||||
if (!WaitForCompletion(request, out var waitError))
|
||||
return new PackageSearchResponse { Success = false, Query = trimmed, Message = waitError };
|
||||
|
||||
return RunOnMain<object>(() =>
|
||||
{
|
||||
if (request.Status != StatusCode.Success)
|
||||
return new PackageSearchResponse
|
||||
{
|
||||
Success = false,
|
||||
Query = trimmed,
|
||||
Message = $"Search failed: {request.Error?.message ?? "unknown error"}"
|
||||
};
|
||||
|
||||
var packages = MapMarkingInstalled(request.Result);
|
||||
return new PackageSearchResponse
|
||||
{
|
||||
Success = true,
|
||||
Query = trimmed,
|
||||
Count = packages.Count,
|
||||
Packages = packages,
|
||||
Message = $"{packages.Count} package(s)."
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Mutating operations (dual-mode, CAT-2509 gated) -----------------------------------
|
||||
|
||||
[CliCommand("package_add",
|
||||
"Add a UPM package by name@version, git URL, or 'file:' local path. Async by default (returns " +
|
||||
"in_progress; poll package_status); pass wait=true to block until added. A recompile/domain reload " +
|
||||
"follows — poll recompile_status. Requires confirm=true; use dry_run to preview.",
|
||||
MainThreadRequired = false,
|
||||
Tags = new[] { "packages" })]
|
||||
public static object PackageAdd(
|
||||
[CliArg("identifier", "Package to add: 'com.unity.foo@1.2.3', a git URL, or 'file:../Path'.", Required = true)] string identifier = "",
|
||||
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
|
||||
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false,
|
||||
[CliArg("wait", "Block until the operation completes and return the result (synchronous). Default: return immediately and poll package_status.")] bool wait = false)
|
||||
{
|
||||
if (!PackageIdentifier.TryParse(identifier, out var parsed, out var parseError))
|
||||
return PackageMutationResponse.Failed("add", identifier, parseError);
|
||||
|
||||
if (IsBusy())
|
||||
return PackageMutationResponse.Busy("add");
|
||||
|
||||
return ExecuteMutation(
|
||||
operation: "add",
|
||||
commandName: "package_add",
|
||||
argument: parsed.Identifier,
|
||||
planText: $"Add package {parsed.Description} [{parsed.Kind}]",
|
||||
confirm: confirm,
|
||||
dryRun: dryRun,
|
||||
wait: wait,
|
||||
start: () => Client.Add(parsed.Identifier),
|
||||
readPackage: req =>
|
||||
{
|
||||
var added = (req as AddRequest)?.Result;
|
||||
return added != null ? Map(added, isInstalled: true) : null;
|
||||
});
|
||||
}
|
||||
|
||||
[CliCommand("package_remove",
|
||||
"Remove a UPM package by name. Async by default (returns in_progress; poll package_status); pass " +
|
||||
"wait=true to block until removed. A recompile/domain reload follows — poll recompile_status. " +
|
||||
"Requires confirm=true; use dry_run to preview.",
|
||||
MainThreadRequired = false,
|
||||
Tags = new[] { "packages" })]
|
||||
public static object PackageRemove(
|
||||
[CliArg("name", "Package name to remove (e.g. com.unity.foo).", Required = true)] string name = "",
|
||||
[CliArg("confirm", "Apply the change. Without it the call is refused.")] bool confirm = false,
|
||||
[CliArg("dry_run", "Preview the change without applying it.")] bool dryRun = false,
|
||||
[CliArg("wait", "Block until the operation completes and return the result (synchronous). Default: return immediately and poll package_status.")] bool wait = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return PackageMutationResponse.Failed("remove", name, "A package name is required.");
|
||||
|
||||
var packageName = name.Trim();
|
||||
|
||||
if (IsBusy())
|
||||
return PackageMutationResponse.Busy("remove");
|
||||
|
||||
return ExecuteMutation(
|
||||
operation: "remove",
|
||||
commandName: "package_remove",
|
||||
argument: packageName,
|
||||
planText: $"Remove package '{packageName}'",
|
||||
confirm: confirm,
|
||||
dryRun: dryRun,
|
||||
wait: wait,
|
||||
start: () => Client.Remove(packageName),
|
||||
readPackage: null);
|
||||
}
|
||||
|
||||
[CliCommand("package_resolve",
|
||||
"Resolve/refresh packages from the manifest (re-fetch and re-link). May trigger a recompile/domain " +
|
||||
"reload — poll recompile_status. Its outcome is recorded for package_status.",
|
||||
MainThreadRequired = true,
|
||||
Tags = new[] { "packages" })]
|
||||
public static object PackageResolve()
|
||||
{
|
||||
WriteStatus(new PackageStatus
|
||||
{
|
||||
Status = "in_progress",
|
||||
Operation = "resolve",
|
||||
StartedAt = NowIso(),
|
||||
Message = "Resolving packages..."
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
// Client.Resolve() is fire-and-forget (returns void) and schedules a resolve on a later
|
||||
// tick, so the response flushes before any reload — no request to wait on.
|
||||
Client.Resolve();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var failed = new PackageStatus
|
||||
{
|
||||
Status = "failed",
|
||||
Operation = "resolve",
|
||||
Success = false,
|
||||
Error = ex.Message,
|
||||
Manifest = SafeReadManifest(),
|
||||
CompletedAt = NowIso(),
|
||||
Message = $"Resolve failed: {ex.Message}"
|
||||
};
|
||||
WriteStatus(failed);
|
||||
return PackageMutationResponse.Failed("resolve", null, failed.Error);
|
||||
}
|
||||
|
||||
var done = new PackageStatus
|
||||
{
|
||||
Status = "completed",
|
||||
Operation = "resolve",
|
||||
Success = true,
|
||||
RequiresRecompile = true,
|
||||
Manifest = SafeReadManifest(),
|
||||
CompletedAt = NowIso(),
|
||||
Message = "Resolve requested. If assemblies changed, a domain reload follows — poll recompile_status."
|
||||
};
|
||||
WriteStatus(done);
|
||||
return ToMutationResponse(done, plan: null);
|
||||
}
|
||||
|
||||
// ---- Status ----------------------------------------------------------------------------
|
||||
|
||||
[CliCommand("package_status",
|
||||
"Status of the last async package operation (add/remove/resolve): idle | in_progress | completed | " +
|
||||
"failed, with the added package, manifest, and any error.",
|
||||
MainThreadRequired = false,
|
||||
Tags = new[] { "packages" })]
|
||||
public static string GetPackageStatus()
|
||||
{
|
||||
if (File.Exists(StatusFile))
|
||||
return File.ReadAllText(StatusFile);
|
||||
return "{\"status\":\"idle\"}";
|
||||
}
|
||||
|
||||
// ---- Mutation execution ----------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Gate (confirm / dry-run) and kick off the mutating op, then
|
||||
/// either return <c>in_progress</c> (async, default) or block until completion (<paramref name="wait"/>).
|
||||
/// Either way the operation is tracked and a status file is written, so the result is recoverable
|
||||
/// via <c>package_status</c> even if the domain reload severs a synchronous reply.
|
||||
/// </summary>
|
||||
static object ExecuteMutation(
|
||||
string operation, string commandName, string argument, string planText,
|
||||
bool confirm, bool dryRun, bool wait,
|
||||
Func<Request> start, Func<Request, PackageSummary> readPackage)
|
||||
{
|
||||
if (dryRun)
|
||||
return PackageMutationResponse.DryRunPreview(operation, argument, planText, $"Dry run — {planText}");
|
||||
if (!confirm)
|
||||
return PackageMutationResponse.Rejected(operation, argument,
|
||||
"Refused: this changes project packages. Re-run with confirm=true to apply, or dry_run=true to preview.");
|
||||
|
||||
// UPM operations are not part of Unity's Undo system, so there is no undo scope here.
|
||||
var request = RunOnMain(() =>
|
||||
{
|
||||
var r = start();
|
||||
// async mode finalizes via the update-loop poller; sync mode finalizes itself.
|
||||
BeginTracking(operation, argument, r, readPackage, subscribePoll: !wait);
|
||||
return r;
|
||||
});
|
||||
|
||||
if (request == null)
|
||||
return PackageMutationResponse.Failed(operation, argument, "Operation was confirmed but failed to start.");
|
||||
|
||||
if (!wait)
|
||||
return PackageMutationResponse.InProgress(operation, argument, planText);
|
||||
|
||||
// Synchronous: poll until complete, capturing status + result atomically on the main thread
|
||||
// (a reload can only fire between ticks, so the snapshot can't be interleaved).
|
||||
var deadline = DateTime.UtcNow.AddSeconds(WaitTimeoutSeconds);
|
||||
while (true)
|
||||
{
|
||||
var status = RunOnMain(TryFinalize);
|
||||
if (status != null)
|
||||
return ToMutationResponse(status, planText);
|
||||
|
||||
if (DateTime.UtcNow > deadline)
|
||||
{
|
||||
// Hand off to the update-loop poller so package_status still settles after we bail.
|
||||
RunOnMain<object>(() => { SubscribePoll(); return null; });
|
||||
return PackageMutationResponse.Failed(operation, argument,
|
||||
$"Timed out after {WaitTimeoutSeconds}s; the operation may still be in progress — poll package_status.");
|
||||
}
|
||||
|
||||
Thread.Sleep(PollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Record the in-flight op, write the in_progress status, and (async only) start polling.</summary>
|
||||
static void BeginTracking(string operation, string argument, Request request,
|
||||
Func<Request, PackageSummary> readPackage, bool subscribePoll)
|
||||
{
|
||||
lock (s_Lock)
|
||||
{
|
||||
s_Request = request;
|
||||
s_Operation = operation;
|
||||
s_Argument = argument;
|
||||
s_ReadPackage = readPackage;
|
||||
s_InProgress = true;
|
||||
}
|
||||
|
||||
WriteStatus(new PackageStatus
|
||||
{
|
||||
Status = "in_progress",
|
||||
Operation = operation,
|
||||
Argument = argument,
|
||||
StartedAt = NowIso(),
|
||||
Message = $"{operation} in progress. Poll package_status."
|
||||
});
|
||||
|
||||
if (subscribePoll)
|
||||
SubscribePoll();
|
||||
}
|
||||
|
||||
static void SubscribePoll()
|
||||
{
|
||||
EditorApplication.update -= Poll;
|
||||
EditorApplication.update += Poll;
|
||||
}
|
||||
|
||||
/// <summary>Update-loop poller for async mode (and a timed-out sync wait): finalize when done.</summary>
|
||||
static void Poll() => TryFinalize();
|
||||
|
||||
/// <summary>
|
||||
/// If the tracked request has completed, write its final status, clear tracking, and return the
|
||||
/// status; otherwise return null. Main-thread only (reads Request members); guarded so the sync
|
||||
/// waiter and the update-loop poller can't double-finalize.
|
||||
/// </summary>
|
||||
static PackageStatus TryFinalize()
|
||||
{
|
||||
lock (s_Lock)
|
||||
{
|
||||
var request = s_Request;
|
||||
if (request == null)
|
||||
{
|
||||
EditorApplication.update -= Poll;
|
||||
return null;
|
||||
}
|
||||
if (!request.IsCompleted)
|
||||
return null;
|
||||
|
||||
var status = BuildStatus(s_Operation, s_Argument, request, s_ReadPackage);
|
||||
WriteStatus(status);
|
||||
|
||||
s_Request = null;
|
||||
s_Operation = null;
|
||||
s_Argument = null;
|
||||
s_ReadPackage = null;
|
||||
s_InProgress = false;
|
||||
EditorApplication.update -= Poll;
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
static PackageStatus BuildStatus(string operation, string argument, Request request,
|
||||
Func<Request, PackageSummary> readPackage)
|
||||
{
|
||||
var status = new PackageStatus
|
||||
{
|
||||
Operation = operation,
|
||||
Argument = argument,
|
||||
CompletedAt = NowIso(),
|
||||
Manifest = SafeReadManifest()
|
||||
};
|
||||
|
||||
if (request.Status == StatusCode.Success)
|
||||
{
|
||||
status.Status = "completed";
|
||||
status.Success = true;
|
||||
status.RequiresRecompile = true;
|
||||
try { status.Package = readPackage?.Invoke(request); }
|
||||
catch (Exception ex) { Debug.LogWarning($"[pipeline] package result projection failed: {ex.Message}"); }
|
||||
status.Message = $"{operation} completed.";
|
||||
}
|
||||
else
|
||||
{
|
||||
status.Status = "failed";
|
||||
status.Success = false;
|
||||
status.Error = request.Error?.message ?? "Unknown package manager error.";
|
||||
status.Message = $"{operation} failed: {status.Error}";
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static PackageMutationResponse ToMutationResponse(PackageStatus s, string plan) => new PackageMutationResponse
|
||||
{
|
||||
Success = s.Success,
|
||||
Operation = s.Operation,
|
||||
Argument = s.Argument,
|
||||
Status = s.Status,
|
||||
Applied = s.Success,
|
||||
Plan = plan,
|
||||
Package = s.Package,
|
||||
Manifest = s.Manifest,
|
||||
RequiresRecompile = s.RequiresRecompile,
|
||||
Message = s.Message
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// On load, settle a status file left at "in_progress" by a domain reload (the expected outcome of
|
||||
/// a successful add/remove). The manifest change that triggered the reload has taken effect, so
|
||||
/// the op is recorded completed; the follow-on recompile is observed via recompile_status.
|
||||
/// </summary>
|
||||
static void RecoverInterruptedOperation()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(StatusFile))
|
||||
return;
|
||||
|
||||
var status = JsonConvert.DeserializeObject<PackageStatus>(File.ReadAllText(StatusFile));
|
||||
if (status == null || status.Status != "in_progress")
|
||||
return;
|
||||
|
||||
status.Status = "completed";
|
||||
status.Success = true;
|
||||
status.RequiresRecompile = true;
|
||||
status.CompletedAt = NowIso();
|
||||
status.Manifest = SafeReadManifest();
|
||||
status.Message = $"{status.Operation} completed (domain reload). Manifest updated; poll recompile_status for the recompile.";
|
||||
WriteStatus(status);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[pipeline] package status recovery failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Helpers ---------------------------------------------------------------------------
|
||||
|
||||
static bool IsBusy() => s_InProgress;
|
||||
|
||||
/// <summary>
|
||||
/// Block until <paramref name="request"/> completes, polling its (main-thread-only) state via
|
||||
/// <see cref="RunOnMain"/> while this thread sleeps between checks. Returns false with
|
||||
/// <paramref name="error"/> set on timeout. Used by the read-only registry queries.
|
||||
/// </summary>
|
||||
static bool WaitForCompletion(Request request, out string error)
|
||||
{
|
||||
error = null;
|
||||
var deadline = DateTime.UtcNow.AddSeconds(WaitTimeoutSeconds);
|
||||
while (!RunOnMain(() => request.IsCompleted))
|
||||
{
|
||||
if (DateTime.UtcNow > deadline)
|
||||
{
|
||||
error = $"Timed out after {WaitTimeoutSeconds}s waiting for the package manager.";
|
||||
return false;
|
||||
}
|
||||
Thread.Sleep(PollIntervalMs);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run <paramref name="fn"/> on the Unity main thread. When invoked off-thread (the normal case
|
||||
/// for the waiting commands), it marshals through the live server's dispatcher; when already on
|
||||
/// the main thread (e.g. a direct in-process/test call, or no server running) it runs inline.
|
||||
/// </summary>
|
||||
static T RunOnMain<T>(Func<T> fn)
|
||||
{
|
||||
var dispatcher = PipelineServerStartup.Server?.Dispatcher;
|
||||
if (dispatcher != null && dispatcher.IsInitialized && !dispatcher.IsMainThread())
|
||||
return dispatcher.Invoke(fn);
|
||||
return fn();
|
||||
}
|
||||
|
||||
static Dictionary<string, string> SafeReadManifest() =>
|
||||
PackageManifest.TryRead(out var deps, out _) ? deps : null;
|
||||
|
||||
static PackageSummary Map(PackageInfo p, bool isInstalled = false) => new PackageSummary
|
||||
{
|
||||
Name = p.name,
|
||||
Version = p.version,
|
||||
DisplayName = p.displayName,
|
||||
Source = p.source.ToString(),
|
||||
ResolvedPath = p.resolvedPath,
|
||||
IsDirectDependency = p.isDirectDependency,
|
||||
IsInstalled = isInstalled
|
||||
};
|
||||
|
||||
/// <summary>Map registry results, flagging which are currently installed.</summary>
|
||||
static List<PackageSummary> MapMarkingInstalled(IEnumerable<PackageInfo> packages)
|
||||
{
|
||||
var installed = new HashSet<string>();
|
||||
foreach (var p in PackageInfo.GetAllRegisteredPackages())
|
||||
installed.Add(p.name);
|
||||
|
||||
var list = new List<PackageSummary>();
|
||||
if (packages != null)
|
||||
foreach (var p in packages)
|
||||
list.Add(Map(p, installed.Contains(p.name)));
|
||||
return list;
|
||||
}
|
||||
|
||||
static string NowIso() => DateTime.UtcNow.ToString("o");
|
||||
|
||||
static void WriteStatus(PackageStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(StatusFile, JsonConvert.SerializeObject(status));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"[Package] Failed to write status file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user