Add Unity Pipeline package support
This commit is contained in:
+1177
File diff suppressed because it is too large
Load Diff
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6905de2a6949fc40bea9d708751b0fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
|
||||
namespace Unity.Pipeline.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute to mark parameters of CLI command methods.
|
||||
/// Provides parameter metadata for CLI validation and help generation.
|
||||
/// Adapted from unity-tools [Parameter] attribute for Pipeline organization requirements.
|
||||
///
|
||||
/// Also valid on the fields/properties of an <see cref="IStructuredCommandInput"/> DTO, where it
|
||||
/// describes a member of a nested object schema (same Name/Description/Required semantics).
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class CliArgAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the parameter as it appears in CLI arguments.
|
||||
/// Used in "unity request command_name --param_name value" syntax.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description of the parameter.
|
||||
/// Used in CLI help text and parameter documentation.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this parameter is required for command execution.
|
||||
/// Default: false (parameter is optional).
|
||||
/// </summary>
|
||||
public bool Required { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Default value for the parameter if not provided.
|
||||
/// Must be compatible with the parameter type.
|
||||
/// </summary>
|
||||
public object DefaultValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CLI parameter attribute.
|
||||
/// </summary>
|
||||
/// <param name="name">Parameter name for CLI arguments</param>
|
||||
/// <param name="description">Human-readable description</param>
|
||||
public CliArgAttribute(string name, string description)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac8886d9f7e92d844b4f1728ce2e2cab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
|
||||
namespace Unity.Pipeline.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute to mark static methods as CLI-accessible commands.
|
||||
/// Commands with this attribute can be executed remotely via Pipeline Server.
|
||||
/// Adapted from unity-tools [Tool] attribute for Pipeline organization requirements.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class CliCommandAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique name of the command for CLI execution.
|
||||
/// Used in "unity request command_name" syntax.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description of what the command does.
|
||||
/// Used in CLI help text and command discovery.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this command requires Unity main thread execution.
|
||||
/// Default: true (most Unity APIs require main thread).
|
||||
/// </summary>
|
||||
public bool MainThreadRequired { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this command belongs to the runtime (Player) command surface only.
|
||||
/// Runtime-only commands are hidden from an Editor server's command listing
|
||||
/// (they remain executable, but are not advertised when connected to an Editor).
|
||||
/// Default: false (listed on the Editor server).
|
||||
/// </summary>
|
||||
public bool RuntimeOnly { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new CLI command attribute.
|
||||
/// </summary>
|
||||
/// <param name="name">Unique command name for CLI execution</param>
|
||||
/// <param name="description">Human-readable description</param>
|
||||
public CliCommandAttribute(string name, string description)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee4da62af1984f442af8ada30f3eee83
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Unity.Pipeline.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about a discovered CLI command.
|
||||
/// Contains metadata needed for command execution and CLI help generation.
|
||||
/// </summary>
|
||||
public class CommandInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique name of the command for CLI execution.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description of the command.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this command requires Unity main thread execution.
|
||||
/// </summary>
|
||||
public bool MainThreadRequired { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this command is part of the runtime (Player) command surface only.
|
||||
/// Editor servers hide runtime-only commands from their command listing.
|
||||
/// </summary>
|
||||
public bool RuntimeOnly { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Method that implements this command.
|
||||
/// </summary>
|
||||
public MethodInfo Method { get; } // TODO Maybe look at generating a dynamic Delegate to increase performance of the command call.
|
||||
|
||||
/// <summary>
|
||||
/// Parameters that this command accepts.
|
||||
/// </summary>
|
||||
public IReadOnlyList<CommandParameterInfo> Parameters { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create command information from discovery.
|
||||
/// </summary>
|
||||
public CommandInfo(string name, string description, bool mainThreadRequired,
|
||||
MethodInfo method, IReadOnlyList<CommandParameterInfo> parameters, bool runtimeOnly = false)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
Method = method ?? throw new ArgumentNullException(nameof(method));
|
||||
Parameters = parameters ?? throw new ArgumentNullException(nameof(parameters));
|
||||
MainThreadRequired = mainThreadRequired;
|
||||
RuntimeOnly = runtimeOnly;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} MainThreadRequired:{MainThreadRequired} Parameters:{Parameters.Count}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40290d720d0d35b44a9fa5843a5ee294
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Unity.Pipeline.HotReload;
|
||||
using UnityEngine;
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
using UnityEngine.Assemblies;
|
||||
#endif
|
||||
|
||||
namespace Unity.Pipeline.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Registry for discovering and managing CLI commands via pluggable discovery mechanism.
|
||||
/// Scans for methods marked with [CliCommand] attribute across all assemblies.
|
||||
/// Based on unity-tools ToolRegistry patterns adapted for Pipeline requirements.
|
||||
/// </summary>
|
||||
public static class CommandRegistry
|
||||
{
|
||||
private static IReadOnlyList<CommandInfo> m_CachedCommands;
|
||||
private static ICommandDiscovery m_Discovery;
|
||||
|
||||
/// <summary>
|
||||
/// Set the command discovery mechanism.
|
||||
/// Editor assembly provides TypeCache-based discovery, Runtime uses reflection fallback.
|
||||
/// </summary>
|
||||
public static void SetDiscovery(ICommandDiscovery discovery)
|
||||
{
|
||||
m_Discovery = discovery;
|
||||
ClearCache(); // Re-discover with new mechanism
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover all CLI commands marked with [CliCommand] attribute.
|
||||
/// Uses injected discovery mechanism (TypeCache in Editor, reflection in Runtime).
|
||||
/// Results are cached until domain reload.
|
||||
/// </summary>
|
||||
public static IEnumerable<CommandInfo> DiscoverCommands()
|
||||
{
|
||||
if (m_CachedCommands == null)
|
||||
{
|
||||
m_CachedCommands = DiscoverCommandsInternal().ToList();
|
||||
|
||||
// Also discover hot reload methods when commands are discovered
|
||||
DiscoverHotReloadMethods();
|
||||
}
|
||||
|
||||
return m_CachedCommands;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the command cache. Called automatically on domain reload.
|
||||
/// Can be called manually for testing or dynamic command registration.
|
||||
/// </summary>
|
||||
public static void ClearCache()
|
||||
{
|
||||
m_CachedCommands = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal implementation of command discovery using injected discovery mechanism.
|
||||
/// </summary>
|
||||
private static IEnumerable<CommandInfo> DiscoverCommandsInternal()
|
||||
{
|
||||
var commands = new List<CommandInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
// Use injected discovery mechanism if available, otherwise fallback to reflection
|
||||
IEnumerable<MethodInfo> methods;
|
||||
|
||||
if (m_Discovery != null)
|
||||
{
|
||||
methods = m_Discovery.GetMethodsWithAttribute<CliCommandAttribute>();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: Use reflection to scan loaded assemblies
|
||||
methods = GetMethodsWithAttributeViaReflection<CliCommandAttribute>();
|
||||
}
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
try
|
||||
{
|
||||
var commandInfo = CreateCommandInfo(method);
|
||||
if (commandInfo != null)
|
||||
{
|
||||
commands.Add(commandInfo);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to register command from method {method.DeclaringType?.Name}.{method.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Failed to discover commands: {ex.Message}");
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fallback command discovery using reflection when TypeCache is not available.
|
||||
/// </summary>
|
||||
private static IEnumerable<MethodInfo> GetMethodsWithAttributeViaReflection<T>() where T : Attribute
|
||||
{
|
||||
var methods = new List<MethodInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
var assemblies = PipelineUtils.GetLoadedAssemblies();
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
// Skip system assemblies for performance
|
||||
if (IsSystemAssembly(assembly))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var type in assembly.GetTypes())
|
||||
{
|
||||
foreach (var method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
if (method.GetCustomAttribute<T>() != null)
|
||||
{
|
||||
methods.Add(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (ReflectionTypeLoadException)
|
||||
{
|
||||
// Skip assemblies that can't be loaded
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Reflection-based command discovery failed: {ex.Message}");
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if assembly is a system assembly that should be skipped during discovery.
|
||||
/// </summary>
|
||||
private static bool IsSystemAssembly(Assembly assembly)
|
||||
{
|
||||
var name = assembly.GetName().Name;
|
||||
return name.StartsWith("System.") ||
|
||||
name.StartsWith("Microsoft.") ||
|
||||
name.StartsWith("mscorlib") ||
|
||||
name.StartsWith("netstandard") ||
|
||||
name.Equals("UnityEngine") ||
|
||||
name.Equals("UnityEditor");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create CommandInfo from a method with CliCommand attribute.
|
||||
/// </summary>
|
||||
private static CommandInfo CreateCommandInfo(MethodInfo method)
|
||||
{
|
||||
var commandAttr = method.GetCustomAttribute<CliCommandAttribute>();
|
||||
if (commandAttr == null)
|
||||
return null;
|
||||
|
||||
// Validate method is static (required for CLI commands).
|
||||
// Any static method may be registered regardless of accessibility
|
||||
// (public, internal, or private) — invocation goes through MethodInfo.Invoke.
|
||||
if (!method.IsStatic)
|
||||
{
|
||||
Debug.LogWarning($"Command method {method.DeclaringType?.Name}.{method.Name} must be static");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Discover parameters
|
||||
var parameters = DiscoverParameters(method).ToList();
|
||||
|
||||
return new CommandInfo(
|
||||
commandAttr.Name,
|
||||
commandAttr.Description,
|
||||
commandAttr.MainThreadRequired,
|
||||
method,
|
||||
parameters,
|
||||
commandAttr.RuntimeOnly
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover parameter information from method parameters.
|
||||
/// </summary>
|
||||
private static IEnumerable<CommandParameterInfo> DiscoverParameters(MethodInfo method)
|
||||
{
|
||||
foreach (var param in method.GetParameters())
|
||||
{
|
||||
var argAttr = param.GetCustomAttribute<CliArgAttribute>();
|
||||
|
||||
// Parameters without CliArg attribute get default metadata
|
||||
var name = argAttr?.Name ?? param.Name;
|
||||
var description = argAttr?.Description ?? $"Parameter: {param.Name}";
|
||||
var required = argAttr?.Required ?? !param.HasDefaultValue;
|
||||
var defaultValue = param.HasDefaultValue ? param.DefaultValue : argAttr?.DefaultValue;
|
||||
|
||||
yield return new CommandParameterInfo(name, description, required, param.ParameterType, defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discover and register methods marked with [HotReloadWithOverrides] attribute.
|
||||
/// Called during command discovery to populate the hot reload registry.
|
||||
/// </summary>
|
||||
private static void DiscoverHotReloadMethods()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use injected discovery mechanism if available, otherwise fallback to reflection
|
||||
IEnumerable<MethodInfo> methods;
|
||||
|
||||
if (m_Discovery != null)
|
||||
{
|
||||
methods = m_Discovery.GetMethodsWithAttribute<HotReloadWithOverridesAttribute>();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: Use reflection to scan loaded assemblies
|
||||
methods = GetMethodsWithAttributeViaReflection<HotReloadWithOverridesAttribute>();
|
||||
}
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hotReloadAttr = method.GetCustomAttribute<HotReloadWithOverridesAttribute>();
|
||||
if (hotReloadAttr != null)
|
||||
{
|
||||
HotReloadRegistry.RegisterReloadableMethod(method, hotReloadAttr);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Failed to register hot reload method {method.DeclaringType?.Name}.{method.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Failed to discover hot reload methods: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c53b0ba553311e14c833aadafdb44999
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,200 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Unity.Pipeline.Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Dispatches work to Unity's main thread from background threads (required for accessing Unity
|
||||
/// APIs from HTTP request handlers).
|
||||
///
|
||||
/// Each pipeline server owns its own instance (no global singleton): it is initialized on Start
|
||||
/// and pumped from the main thread — auto-pumped via EditorApplication.update in the editor, and
|
||||
/// by RuntimePipelineManager.Update in a player.
|
||||
/// </summary>
|
||||
public class Dispatcher
|
||||
{
|
||||
private readonly ConcurrentQueue<WorkItem> m_WorkQueue = new ConcurrentQueue<WorkItem>();
|
||||
private volatile bool m_IsInitialized;
|
||||
private int m_MainThreadId = -1;
|
||||
|
||||
public bool IsInitialized => m_IsInitialized;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the dispatcher. Must be called from Unity's main thread.
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
if (m_IsInitialized)
|
||||
return;
|
||||
|
||||
m_MainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
m_IsInitialized = true;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.update += ProcessWorkQueue;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shutdown the dispatcher and cancel any pending work.
|
||||
/// </summary>
|
||||
public void Shutdown()
|
||||
{
|
||||
if (!m_IsInitialized)
|
||||
return;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.update -= ProcessWorkQueue;
|
||||
#endif
|
||||
|
||||
while (m_WorkQueue.TryDequeue(out var item))
|
||||
{
|
||||
try
|
||||
{
|
||||
item.SetException(new OperationCanceledException("Dispatcher is shutting down"));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
m_IsInitialized = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a function on the main thread and return the result (synchronous wait).
|
||||
/// </summary>
|
||||
public T Invoke<T>(Func<T> function, int timeoutMs = 60000)
|
||||
{
|
||||
if (!m_IsInitialized)
|
||||
throw new InvalidOperationException("Dispatcher must be initialized first");
|
||||
|
||||
if (IsMainThread())
|
||||
return function();
|
||||
|
||||
var workItem = new WorkItem<T>(function);
|
||||
m_WorkQueue.Enqueue(workItem);
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
var task = workItem.TaskCompletionSource.Task;
|
||||
|
||||
while (!task.IsCompleted)
|
||||
{
|
||||
if ((DateTime.UtcNow - startTime).TotalMilliseconds > timeoutMs)
|
||||
throw new TimeoutException($"Main thread operation timed out after {timeoutMs}ms");
|
||||
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
|
||||
if (task.IsFaulted)
|
||||
throw task.Exception?.GetBaseException() ?? new Exception("Unknown error");
|
||||
|
||||
if (task.IsCanceled)
|
||||
throw new OperationCanceledException("Main thread operation was cancelled");
|
||||
|
||||
return task.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute an action on the main thread.
|
||||
/// </summary>
|
||||
public void Invoke(Action action, int timeoutMs = 60000)
|
||||
{
|
||||
Invoke<object>(() =>
|
||||
{
|
||||
action();
|
||||
return null;
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a function on the main thread and return the result (async version).
|
||||
/// </summary>
|
||||
public async Task<T> InvokeAsync<T>(Func<T> function, int timeoutMs = 60000)
|
||||
{
|
||||
return await Task.Run(() => Invoke(function, timeoutMs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute an action on the main thread (async version).
|
||||
/// </summary>
|
||||
public async Task InvokeAsync(Action action, int timeoutMs = 60000)
|
||||
{
|
||||
await Task.Run(() => Invoke(action, timeoutMs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if we're currently on Unity's main thread.
|
||||
/// </summary>
|
||||
public bool IsMainThread()
|
||||
{
|
||||
return m_MainThreadId != -1 && Thread.CurrentThread.ManagedThreadId == m_MainThreadId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process queued work items. Called from EditorApplication.update or MonoBehaviour.Update.
|
||||
/// </summary>
|
||||
/// <param name="maxItemsPerFrame">Max items to process per call, to limit frame-rate impact.</param>
|
||||
public void ProcessWorkQueue(int maxItemsPerFrame)
|
||||
{
|
||||
int processedCount = 0;
|
||||
|
||||
while (processedCount < maxItemsPerFrame && m_WorkQueue.TryDequeue(out var workItem))
|
||||
{
|
||||
try
|
||||
{
|
||||
workItem.Execute();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError($"Dispatcher work item failed: {ex.Message}");
|
||||
workItem.SetException(ex);
|
||||
}
|
||||
|
||||
processedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessWorkQueue()
|
||||
{
|
||||
ProcessWorkQueue(10);
|
||||
}
|
||||
|
||||
private abstract class WorkItem
|
||||
{
|
||||
public abstract void Execute();
|
||||
public abstract void SetException(Exception exception);
|
||||
}
|
||||
|
||||
private class WorkItem<T> : WorkItem
|
||||
{
|
||||
private readonly Func<T> m_Function;
|
||||
public TaskCompletionSource<T> TaskCompletionSource { get; }
|
||||
|
||||
public WorkItem(Func<T> function)
|
||||
{
|
||||
m_Function = function ?? throw new ArgumentNullException(nameof(function));
|
||||
TaskCompletionSource = new TaskCompletionSource<T>();
|
||||
}
|
||||
|
||||
public override void Execute()
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = m_Function();
|
||||
TaskCompletionSource.SetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TaskCompletionSource.SetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetException(Exception exception)
|
||||
{
|
||||
TaskCompletionSource.SetException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df7819f4aeb94b2408fd309361f67243
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace Unity.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Restricts a file so that only the user who started the process can read or write it.
|
||||
/// Used to protect the instance descriptor (port file), which carries the auth token.
|
||||
///
|
||||
/// The managed ACL APIs (System.Security.AccessControl) are not part of Unity's runtime
|
||||
/// profile, so Windows is handled via icacls and Unix via libc chmod.
|
||||
/// </summary>
|
||||
public static class FilePermissions
|
||||
{
|
||||
/// <summary>
|
||||
/// Restrict the file at <paramref name="path"/> to the current user only.
|
||||
/// Failures are logged but never thrown, so they cannot block server startup.
|
||||
/// </summary>
|
||||
public static void RestrictToCurrentUser(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (Application.platform)
|
||||
{
|
||||
case RuntimePlatform.WindowsEditor:
|
||||
case RuntimePlatform.WindowsPlayer:
|
||||
RestrictWindows(path);
|
||||
break;
|
||||
default:
|
||||
RestrictUnix(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"Pipeline: Could not restrict permissions on '{path}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RestrictWindows(string path)
|
||||
{
|
||||
// Grant by SID rather than account name: AzureAD/Entra-joined accounts (e.g.
|
||||
// "AzureAD\\user") have no name-to-SID mapping for icacls, but the SID always resolves.
|
||||
var sid = GetCurrentUserSid();
|
||||
if (string.IsNullOrEmpty(sid))
|
||||
{
|
||||
Debug.LogWarning($"Pipeline: Could not resolve current user SID; leaving default permissions on '{path}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
// /inheritance:r drops inherited ACEs; /grant:r replaces the DACL with a single
|
||||
// full-control grant to the current user.
|
||||
Run("icacls", $"\"{path}\" /inheritance:r /grant:r \"*{sid}:(F)\"", out _);
|
||||
}
|
||||
|
||||
private static string GetCurrentUserSid()
|
||||
{
|
||||
if (!Run("whoami", "/user /fo csv /nh", out var output) || string.IsNullOrEmpty(output))
|
||||
return null;
|
||||
|
||||
// Output looks like: "domain\user","S-1-5-21-..."
|
||||
var idx = output.IndexOf("S-1-", StringComparison.Ordinal);
|
||||
if (idx < 0)
|
||||
return null;
|
||||
|
||||
var end = idx;
|
||||
while (end < output.Length && (char.IsLetterOrDigit(output[end]) || output[end] == '-'))
|
||||
end++;
|
||||
|
||||
return output.Substring(idx, end - idx);
|
||||
}
|
||||
|
||||
private static bool Run(string fileName, string arguments, out string stdout)
|
||||
{
|
||||
stdout = null;
|
||||
var psi = new ProcessStartInfo(fileName, arguments)
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
|
||||
using (var process = Process.Start(psi))
|
||||
{
|
||||
if (process == null)
|
||||
return false;
|
||||
|
||||
// Read before WaitForExit to avoid a full-pipe deadlock.
|
||||
stdout = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int chmod(string pathname, uint mode);
|
||||
|
||||
private static void RestrictUnix(string path)
|
||||
{
|
||||
// 0600 (octal) == owner read/write only.
|
||||
const uint ownerReadWrite = 0x180;
|
||||
chmod(path, ownerReadWrite);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e310dee2532338d4e914d3c909ea4db0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Unity.Pipeline.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for command discovery mechanism.
|
||||
/// Allows Runtime assembly to discover commands without directly using Editor APIs.
|
||||
/// </summary>
|
||||
public interface ICommandDiscovery
|
||||
{
|
||||
/// <summary>
|
||||
/// Find all methods marked with CliCommand attribute.
|
||||
/// Implementation uses TypeCache in Editor or reflection fallback in Runtime.
|
||||
/// </summary>
|
||||
IEnumerable<MethodInfo> GetMethodsWithAttribute<T>() where T : System.Attribute;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6e1392c576ec4d4d9b389d5fc899303
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
namespace Unity.Pipeline.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker for a command parameter type that should be advertised to clients as a nested JSON
|
||||
/// <em>object</em> schema in <c>GET /api/commands</c>, instead of collapsing to <c>string</c>.
|
||||
///
|
||||
/// Convention for command authors: when a command needs a structured (multi-field) argument,
|
||||
/// declare a small DTO that implements this interface and take it as a single parameter. The
|
||||
/// <see cref="JsonSchemaGenerator"/> reflects over the type's public, writable fields and
|
||||
/// properties to emit <c>{ "type": "object", "properties": { ... } }</c> (recursing into nested
|
||||
/// <see cref="IStructuredCommandInput"/> members and arrays/lists of them). At runtime the value
|
||||
/// deserializes automatically via Newtonsoft in
|
||||
/// <c>BasePipelineServer.ExtractCommandParameters</c>, so no extra wiring is needed.
|
||||
///
|
||||
/// Member metadata:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Annotate members with <c>[CliArg(name, description, Required = ...)]</c> to control the
|
||||
/// property name, description, and whether it appears in the schema's <c>required</c> array
|
||||
/// (mirrors how command parameters are described).</description></item>
|
||||
/// <item><description>Without a <c>[CliArg]</c>, the member name (or its Newtonsoft <c>[JsonProperty]</c> name)
|
||||
/// is used and the member is optional.</description></item>
|
||||
/// <item><description><c>[JsonIgnore]</c> members are omitted from the schema.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public interface IStructuredCommandInput
|
||||
{
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 366b2704c3b5c044aa544fe545d462f0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Unity.Pipeline.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates JSON Schema for CLI commands to enable parameter validation and help generation.
|
||||
/// Produces standard JSON Schema format that CLI tools can use for validation and auto-completion.
|
||||
///
|
||||
/// Primitive, enum, and array parameters map directly. Parameters whose type implements
|
||||
/// <see cref="IStructuredCommandInput"/> are emitted as nested object schemas (recursing into
|
||||
/// structured members and arrays/lists of them) rather than collapsing to <c>string</c>, so
|
||||
/// agents calling the package — directly or as MCP tools whose schemas come from
|
||||
/// <c>GET /api/commands</c> — can construct structured arguments reliably.
|
||||
/// </summary>
|
||||
public static class JsonSchemaGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate JSON Schema for a command.
|
||||
/// Returns standard JSON Schema format for CLI validation.
|
||||
/// </summary>
|
||||
public static string GenerateCommandSchema(CommandInfo command)
|
||||
{
|
||||
if (command == null)
|
||||
throw new ArgumentNullException(nameof(command));
|
||||
|
||||
var schema = new JObject
|
||||
{
|
||||
["$schema"] = "http://json-schema.org/draft-07/schema#",
|
||||
["type"] = "object",
|
||||
["title"] = command.Name,
|
||||
["description"] = command.Description,
|
||||
["properties"] = GenerateParameterProperties(command.Parameters),
|
||||
["required"] = new JArray(command.Parameters.Where(p => p.Required).Select(p => p.Name)),
|
||||
["additionalProperties"] = false
|
||||
};
|
||||
|
||||
// Add command metadata for CLI tools
|
||||
schema["x-command-metadata"] = new JObject
|
||||
{
|
||||
["mainThreadRequired"] = command.MainThreadRequired,
|
||||
["methodName"] = $"{command.Method.DeclaringType?.FullName}.{command.Method.Name}"
|
||||
};
|
||||
|
||||
return schema.ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate properties section of JSON Schema from command parameters.
|
||||
/// </summary>
|
||||
private static JObject GenerateParameterProperties(IEnumerable<CommandParameterInfo> parameters)
|
||||
{
|
||||
var properties = new JObject();
|
||||
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
properties[parameter.Name] = GenerateParameterSchema(parameter);
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate JSON Schema for a single top-level command parameter.
|
||||
/// </summary>
|
||||
private static JObject GenerateParameterSchema(CommandParameterInfo parameter)
|
||||
{
|
||||
var schema = GenerateTypeSchema(parameter.ParameterType, new HashSet<Type>());
|
||||
schema["description"] = parameter.Description;
|
||||
|
||||
// Add default value if present (skip for objects/arrays whose default is typically null)
|
||||
if (parameter.DefaultValue != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
schema["default"] = JToken.FromObject(parameter.DefaultValue);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Non-serializable default; omit rather than fail schema generation.
|
||||
}
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build the schema fragment for a CLR type: scalar/enum, array/list, or — for
|
||||
/// <see cref="IStructuredCommandInput"/> types — a nested object schema. <paramref name="visited"/>
|
||||
/// tracks structured types currently being expanded so self-referential graphs don't recurse
|
||||
/// forever.
|
||||
/// </summary>
|
||||
private static JObject GenerateTypeSchema(Type type, HashSet<Type> visited)
|
||||
{
|
||||
// Unwrap Nullable<T> so e.g. int? schemas as "integer".
|
||||
var underlying = Nullable.GetUnderlyingType(type);
|
||||
if (underlying != null)
|
||||
type = underlying;
|
||||
|
||||
if (typeof(IStructuredCommandInput).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface)
|
||||
return GenerateObjectSchema(type, visited);
|
||||
|
||||
if (TryGetEnumerableElementType(type, out var elementType))
|
||||
{
|
||||
return new JObject
|
||||
{
|
||||
["type"] = "array",
|
||||
["items"] = GenerateTypeSchema(elementType, visited)
|
||||
};
|
||||
}
|
||||
|
||||
var schema = new JObject();
|
||||
var (jsonType, format) = MapTypeToJsonSchema(type);
|
||||
schema["type"] = jsonType;
|
||||
|
||||
if (!string.IsNullOrEmpty(format))
|
||||
schema["format"] = format;
|
||||
|
||||
if (type.IsEnum)
|
||||
schema["enum"] = new JArray(Enum.GetNames(type));
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emit a nested object schema for an <see cref="IStructuredCommandInput"/> type by reflecting
|
||||
/// its public, writable fields and properties.
|
||||
/// </summary>
|
||||
private static JObject GenerateObjectSchema(Type type, HashSet<Type> visited)
|
||||
{
|
||||
var schema = new JObject { ["type"] = "object" };
|
||||
|
||||
// Cycle guard: if we're already expanding this type higher in the stack, stop recursing
|
||||
// and emit an open object rather than looping.
|
||||
if (!visited.Add(type))
|
||||
{
|
||||
schema["additionalProperties"] = true;
|
||||
return schema;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var properties = new JObject();
|
||||
var required = new JArray();
|
||||
|
||||
foreach (var member in GetSchemaMembers(type))
|
||||
{
|
||||
var memberSchema = GenerateTypeSchema(member.Type, visited);
|
||||
if (!string.IsNullOrEmpty(member.Description))
|
||||
memberSchema["description"] = member.Description;
|
||||
|
||||
properties[member.Name] = memberSchema;
|
||||
if (member.Required)
|
||||
required.Add(member.Name);
|
||||
}
|
||||
|
||||
schema["properties"] = properties;
|
||||
if (required.Count > 0)
|
||||
schema["required"] = required;
|
||||
schema["additionalProperties"] = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
visited.Remove(type);
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate the schema-bearing members of a structured-input type: public instance fields and
|
||||
/// read/write properties. Honors <c>[JsonIgnore]</c>, and reads name/description/required from
|
||||
/// <c>[CliArg]</c> (falling back to a Newtonsoft <c>[JsonProperty]</c> name, then the member name).
|
||||
/// </summary>
|
||||
private static IEnumerable<SchemaMember> GetSchemaMembers(Type type)
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
|
||||
|
||||
foreach (var field in type.GetFields(flags))
|
||||
{
|
||||
if (field.IsInitOnly || field.IsLiteral)
|
||||
continue;
|
||||
if (field.GetCustomAttribute<JsonIgnoreAttribute>() != null)
|
||||
continue;
|
||||
|
||||
yield return ToSchemaMember(field, field.FieldType);
|
||||
}
|
||||
|
||||
foreach (var property in type.GetProperties(flags))
|
||||
{
|
||||
if (!property.CanRead || !property.CanWrite)
|
||||
continue;
|
||||
if (property.GetIndexParameters().Length > 0)
|
||||
continue;
|
||||
if (property.GetCustomAttribute<JsonIgnoreAttribute>() != null)
|
||||
continue;
|
||||
|
||||
yield return ToSchemaMember(property, property.PropertyType);
|
||||
}
|
||||
}
|
||||
|
||||
private static SchemaMember ToSchemaMember(MemberInfo member, Type memberType)
|
||||
{
|
||||
var cliArg = member.GetCustomAttribute<CliArgAttribute>();
|
||||
var jsonProperty = member.GetCustomAttribute<JsonPropertyAttribute>();
|
||||
|
||||
return new SchemaMember
|
||||
{
|
||||
Name = cliArg?.Name ?? jsonProperty?.PropertyName ?? member.Name,
|
||||
Type = memberType,
|
||||
Description = cliArg?.Description,
|
||||
Required = cliArg?.Required ?? false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect a JSON-array-shaped type (arrays and common generic collections) and yield its
|
||||
/// element type. <c>string</c> is intentionally excluded (it is <see cref="IEnumerable{T}"/>
|
||||
/// of char but maps to a JSON string).
|
||||
/// </summary>
|
||||
private static bool TryGetEnumerableElementType(Type type, out Type elementType)
|
||||
{
|
||||
elementType = null;
|
||||
|
||||
if (type == typeof(string))
|
||||
return false;
|
||||
|
||||
if (type.IsArray)
|
||||
{
|
||||
elementType = type.GetElementType();
|
||||
return elementType != null;
|
||||
}
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var definition = type.GetGenericTypeDefinition();
|
||||
if (definition == typeof(List<>) || definition == typeof(IList<>) ||
|
||||
definition == typeof(IEnumerable<>) || definition == typeof(ICollection<>) ||
|
||||
definition == typeof(IReadOnlyList<>) || definition == typeof(IReadOnlyCollection<>))
|
||||
{
|
||||
elementType = type.GetGenericArguments()[0];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map a scalar/enum C# type to JSON Schema type and format. Object and array shapes are
|
||||
/// handled by <see cref="GenerateTypeSchema"/> before this is reached; anything else falls
|
||||
/// back to <c>string</c>.
|
||||
/// </summary>
|
||||
private static (string type, string format) MapTypeToJsonSchema(Type csharpType)
|
||||
{
|
||||
// Handle nullable types
|
||||
if (Nullable.GetUnderlyingType(csharpType) != null)
|
||||
{
|
||||
csharpType = Nullable.GetUnderlyingType(csharpType);
|
||||
}
|
||||
|
||||
// String types
|
||||
if (csharpType == typeof(string) || csharpType == typeof(char))
|
||||
{
|
||||
return ("string", null);
|
||||
}
|
||||
|
||||
// Integer types
|
||||
if (csharpType == typeof(int) || csharpType == typeof(long) ||
|
||||
csharpType == typeof(short) || csharpType == typeof(byte) ||
|
||||
csharpType == typeof(uint) || csharpType == typeof(ulong) ||
|
||||
csharpType == typeof(ushort) || csharpType == typeof(sbyte))
|
||||
{
|
||||
return ("integer", null);
|
||||
}
|
||||
|
||||
// Number types
|
||||
if (csharpType == typeof(float) || csharpType == typeof(double) || csharpType == typeof(decimal))
|
||||
{
|
||||
return ("number", null);
|
||||
}
|
||||
|
||||
// Boolean type
|
||||
if (csharpType == typeof(bool))
|
||||
{
|
||||
return ("boolean", null);
|
||||
}
|
||||
|
||||
// Enum types - represent as string with enum constraint (added by the caller)
|
||||
if (csharpType.IsEnum)
|
||||
{
|
||||
return ("string", null);
|
||||
}
|
||||
|
||||
// DateTime types
|
||||
if (csharpType == typeof(DateTime) || csharpType == typeof(DateTimeOffset))
|
||||
{
|
||||
return ("string", "date-time");
|
||||
}
|
||||
|
||||
// Guid type
|
||||
if (csharpType == typeof(Guid))
|
||||
{
|
||||
return ("string", "uuid");
|
||||
}
|
||||
|
||||
// Default to string for unknown types
|
||||
return ("string", null);
|
||||
}
|
||||
|
||||
/// <summary>A reflected member of a structured-input type, flattened for schema emission.</summary>
|
||||
private struct SchemaMember
|
||||
{
|
||||
public string Name;
|
||||
public Type Type;
|
||||
public string Description;
|
||||
public bool Required;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 779098faca184884f9f588e41c4c0898
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,97 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Unity.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Read-only stream wrapper that enforces a maximum number of bytes read from an inner stream.
|
||||
/// Once cumulative reads exceed <c>maxBytes</c>, the next read throws
|
||||
/// <see cref="RequestTooLargeException"/>. Used to cap HTTP request bodies even when the
|
||||
/// Content-Length header is absent or untruthful (e.g. chunked transfer-encoding), so an
|
||||
/// oversized body cannot be buffered into memory.
|
||||
/// </summary>
|
||||
internal sealed class MaxLengthStream : Stream
|
||||
{
|
||||
private readonly Stream m_Inner;
|
||||
private readonly long m_MaxBytes;
|
||||
private readonly bool m_LeaveOpen;
|
||||
private long m_TotalRead;
|
||||
|
||||
/// <param name="leaveOpen">
|
||||
/// When true, disposing this wrapper does not dispose <paramref name="inner"/>. The server
|
||||
/// uses this so it can keep reading (draining) the raw request stream after the wrapper is
|
||||
/// disposed on an over-limit read.
|
||||
/// </param>
|
||||
public MaxLengthStream(Stream inner, long maxBytes, bool leaveOpen = false)
|
||||
{
|
||||
m_Inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
m_MaxBytes = maxBytes;
|
||||
m_LeaveOpen = leaveOpen;
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
var read = m_Inner.Read(buffer, offset, count);
|
||||
Account(read);
|
||||
return read;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||
{
|
||||
var read = await m_Inner.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
Account(read);
|
||||
return read;
|
||||
}
|
||||
|
||||
// Throw as soon as the running total exceeds the cap. Exactly maxBytes is allowed.
|
||||
private void Account(int read)
|
||||
{
|
||||
if (read <= 0)
|
||||
return;
|
||||
|
||||
m_TotalRead += read;
|
||||
if (m_TotalRead > m_MaxBytes)
|
||||
throw new RequestTooLargeException(m_MaxBytes);
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
public override long Position
|
||||
{
|
||||
get => m_TotalRead;
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() { }
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
// Own the inner stream's lifetime (matching the previous `using (StreamReader(InputStream))`)
|
||||
// unless the caller asked to leave it open so it can keep reading from it afterwards.
|
||||
if (disposing && !m_LeaveOpen)
|
||||
m_Inner.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown by <see cref="MaxLengthStream"/> when a read would exceed the configured byte cap.
|
||||
/// </summary>
|
||||
internal sealed class RequestTooLargeException : Exception
|
||||
{
|
||||
public long MaxBytes { get; }
|
||||
|
||||
public RequestTooLargeException(long maxBytes)
|
||||
: base($"Request body exceeds the maximum allowed size of {maxBytes} bytes.")
|
||||
{
|
||||
MaxBytes = maxBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d75fa6089aa06114c9450387a6d816ad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
|
||||
namespace Unity.Pipeline.Commands
|
||||
{
|
||||
/// <summary>
|
||||
/// Information about a CLI command parameter.
|
||||
/// Contains metadata needed for parameter validation and CLI help generation.
|
||||
/// </summary>
|
||||
public class CommandParameterInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of the parameter for CLI arguments.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Human-readable description of the parameter.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this parameter is required for command execution.
|
||||
/// </summary>
|
||||
public bool Required { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the parameter for validation and conversion.
|
||||
/// </summary>
|
||||
public Type ParameterType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Default value for optional parameters.
|
||||
/// </summary>
|
||||
public object DefaultValue { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create parameter information from discovery.
|
||||
/// </summary>
|
||||
public CommandParameterInfo(string name, string description, bool required,
|
||||
Type parameterType, object defaultValue = null)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
Description = description ?? throw new ArgumentNullException(nameof(description));
|
||||
ParameterType = parameterType ?? throw new ArgumentNullException(nameof(parameterType));
|
||||
Required = required;
|
||||
DefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Name} Required:{Required} Type:{ParameterType}";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7127497e92677024f949772c30aaf79b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,163 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
#if UNITY_6000_3_OR_NEWER
|
||||
using UnityEngine.Assemblies;
|
||||
#endif
|
||||
|
||||
namespace Unity.Pipeline
|
||||
{
|
||||
/// <summary>
|
||||
/// Version-stable handle to a Unity object id. In 6000.4 Unity replaced the int instance id with
|
||||
/// <see cref="EntityId"/> (a ulong-backed handle), made <c>Object.GetInstanceID()</c> /
|
||||
/// <c>EditorUtility.InstanceIDToObject(int)</c> obsolete-as-error, and is removing the
|
||||
/// <c>EntityId</c><-><c>int</c> conversions. This struct stores the concrete id type for the
|
||||
/// running editor — <see cref="EntityId"/> on 6000.4+, <c>int</c> below — and converts implicitly to
|
||||
/// it so it can be passed straight to Unity APIs. All version branching for object ids is confined
|
||||
/// here. It (de)serializes as a single JSON integer (the raw ulong on 6000.4+, the int below).
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(ObjectIdConverter))]
|
||||
public readonly struct ObjectId : System.IEquatable<ObjectId>
|
||||
{
|
||||
#if UNITY_6000_4_OR_NEWER
|
||||
readonly EntityId m_Value;
|
||||
public ObjectId(EntityId value) { m_Value = value; }
|
||||
public static implicit operator EntityId(ObjectId id) => id.m_Value;
|
||||
public static implicit operator ObjectId(EntityId value) => new ObjectId(value);
|
||||
/// <summary>Raw 64-bit id — the canonical wire / serialization form.</summary>
|
||||
public ulong RawValue => EntityId.ToULong(m_Value);
|
||||
public static ObjectId FromRaw(ulong raw) => new ObjectId(EntityId.FromULong(raw));
|
||||
public bool Equals(ObjectId other) => m_Value.Equals(other.m_Value);
|
||||
public override int GetHashCode() => m_Value.GetHashCode();
|
||||
#else
|
||||
readonly int m_Value;
|
||||
public ObjectId(int value) { m_Value = value; }
|
||||
public static implicit operator int(ObjectId id) => id.m_Value;
|
||||
public static implicit operator ObjectId(int value) => new ObjectId(value);
|
||||
/// <summary>Raw id — the canonical wire / serialization form.</summary>
|
||||
public long RawValue => m_Value;
|
||||
public static ObjectId FromRaw(long raw) => new ObjectId((int)raw);
|
||||
public bool Equals(ObjectId other) => m_Value == other.m_Value;
|
||||
public override int GetHashCode() => m_Value;
|
||||
#endif
|
||||
public override bool Equals(object obj) => obj is ObjectId other && Equals(other);
|
||||
public override string ToString() => RawValue.ToString();
|
||||
|
||||
/// <summary>Parse the canonical numeric form produced by <see cref="ToString"/>.</summary>
|
||||
public static ObjectId Parse(string s)
|
||||
{
|
||||
#if UNITY_6000_4_OR_NEWER
|
||||
return FromRaw(ulong.Parse(s));
|
||||
#else
|
||||
return FromRaw(long.Parse(s));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Try to parse the canonical numeric form produced by <see cref="ToString"/>.</summary>
|
||||
public static bool TryParse(string s, out ObjectId id)
|
||||
{
|
||||
#if UNITY_6000_4_OR_NEWER
|
||||
if (ulong.TryParse(s, out var raw)) { id = FromRaw(raw); return true; }
|
||||
#else
|
||||
if (long.TryParse(s, out var raw)) { id = FromRaw(raw); return true; }
|
||||
#endif
|
||||
id = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>JSON converter: an <see cref="ObjectId"/> is a single integer on the wire.</summary>
|
||||
public sealed class ObjectIdConverter : JsonConverter<ObjectId>
|
||||
{
|
||||
public override void WriteJson(JsonWriter writer, ObjectId value, JsonSerializer serializer)
|
||||
{
|
||||
writer.WriteValue(value.RawValue);
|
||||
}
|
||||
|
||||
public override ObjectId ReadJson(JsonReader reader, System.Type objectType, ObjectId existingValue,
|
||||
bool hasExistingValue, JsonSerializer serializer)
|
||||
{
|
||||
var token = JToken.Load(reader);
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.Integer:
|
||||
#if UNITY_6000_4_OR_NEWER
|
||||
return ObjectId.FromRaw(token.Value<ulong>());
|
||||
#else
|
||||
return ObjectId.FromRaw(token.Value<long>());
|
||||
#endif
|
||||
case JTokenType.String:
|
||||
return ObjectId.Parse((string)token);
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class PipelineUtils
|
||||
{
|
||||
public static string GetLoadedAssemblyPath(System.Reflection.Assembly a)
|
||||
{
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
return a.GetLoadedAssemblyPath();
|
||||
#else
|
||||
return a.Location;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static System.Reflection.Assembly LoadFromBytes(byte[] bytes, byte[] pdb = null)
|
||||
{
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
return UnityEngine.Assemblies.CurrentAssemblies.LoadFromBytes(bytes, pdb);
|
||||
#else
|
||||
return System.Reflection.Assembly.Load(bytes, pdb);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static T[] FindObjectsByType<T>() where T : Object
|
||||
{
|
||||
#if UNITY_6000_4_OR_NEWER
|
||||
return GameObject.FindObjectsByType<T>();
|
||||
#else
|
||||
return GameObject.FindObjectsByType<T>(FindObjectsSortMode.None) ;
|
||||
#endif
|
||||
}
|
||||
|
||||
public static IReadOnlyList<System.Reflection.Assembly> GetLoadedAssemblies()
|
||||
{
|
||||
#if UNITY_6000_5_OR_NEWER
|
||||
return CurrentAssemblies.GetLoadedAssemblies();
|
||||
#else
|
||||
return System.AppDomain.CurrentDomain.GetAssemblies();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>The object's id as a version-stable <see cref="ObjectId"/> (EntityId on 6000.4+, int below).</summary>
|
||||
public static ObjectId GetObjectId(Object obj)
|
||||
{
|
||||
#if UNITY_6000_4_OR_NEWER
|
||||
return new ObjectId(obj.GetEntityId());
|
||||
#else
|
||||
return new ObjectId(obj.GetInstanceID());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>Resolve a loaded/scene object from its <see cref="ObjectId"/>, or null if not found.</summary>
|
||||
public static Object IdToObject(ObjectId id)
|
||||
{
|
||||
#if UNITY_6000_4_OR_NEWER
|
||||
return UnityEditor.EditorUtility.EntityIdToObject(id);
|
||||
#elif UNITY_6000_3_OR_NEWER
|
||||
// EntityIdToObject is the non-obsolete resolver from 6000.3, but the ulong-backed ObjectId
|
||||
// (GetRawData/FromULong) doesn't exist until 6000.4 — so ObjectId is still int-backed here and
|
||||
// we route the int through the (non-obsolete on 6.3) int->EntityId conversion.
|
||||
return UnityEditor.EditorUtility.EntityIdToObject((int)id);
|
||||
#else
|
||||
return UnityEditor.EditorUtility.InstanceIDToObject((int)id);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6c6a6cac05ac41438850c53ea3178b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user