223 lines
10 KiB
C#
223 lines
10 KiB
C#
using System;
|
|
#if UNITY_EDITOR
|
|
using System.IO;
|
|
#endif
|
|
using UnityEngine;
|
|
|
|
namespace Unity.Pipeline.Config
|
|
{
|
|
/// <summary>
|
|
/// Configuration for Pipeline server functionality in Unity Player builds. Authored settings
|
|
/// live in a JSON file under ProjectSettings/ (see <see cref="Save"/>/<see cref="Load"/>) —
|
|
/// never as an asset under Assets/, so nothing about this package appears in the user's
|
|
/// Project window. At real Player build time, PipelineRuntimeBuildProcessor bakes the current
|
|
/// settings into a transient Resources asset (deleted again after the build) so the Player
|
|
/// can find them via Resources.Load.
|
|
/// </summary>
|
|
public class RuntimePipelineConfig : ScriptableObject
|
|
{
|
|
/// <summary>Master switch. The server only starts when true. Off by default for safety.</summary>
|
|
[Tooltip("Enable Pipeline HTTP server in Player builds. SECURITY WARNING: Only enable in development/QA builds, never production without proper security measures.")]
|
|
public bool enableInBuilds = false;
|
|
|
|
/// <summary>Listen port. 0 auto-assigns from 7900-7999.</summary>
|
|
[Header("Server")]
|
|
[Tooltip("HTTP port for Pipeline server. Use 0 for auto-assignment from range 7900-7999.")]
|
|
public int port = 0;
|
|
|
|
/// <summary>Per-request timeout, in milliseconds.</summary>
|
|
[Tooltip("Request timeout in milliseconds. Higher values allow longer-running commands.")]
|
|
[Range(1000, 60000)]
|
|
public int requestTimeoutMs = 30000;
|
|
|
|
/// <summary>Log remote requests for auditing.</summary>
|
|
[Header("Runtime Behavior")]
|
|
[Tooltip("Enable detailed logging of all remote requests for security auditing.")]
|
|
public bool enableAuditLogging = true;
|
|
|
|
/// <summary>Start the server automatically when the Player boots (or Play Mode is entered).</summary>
|
|
[Tooltip("Start the server automatically when the Player boots (or Play Mode is entered).")]
|
|
public bool autoStart = true;
|
|
|
|
/// <summary>Dispatcher work items processed per frame.</summary>
|
|
[Tooltip("Maximum work items the dispatcher processes per frame to maintain performance.")]
|
|
[Range(1, 50)]
|
|
public int maxWorkItemsPerFrame = 10;
|
|
|
|
// Plain int field (not [Range]) so the Inspector shows a text field rather than an
|
|
// unusable 0-65535 slider; the bound is still enforced here, same pattern as
|
|
// EditorPipelineManager.OnValidate's m_WatchdogIntervalSeconds clamp.
|
|
private void OnValidate()
|
|
{
|
|
port = Mathf.Clamp(port, 0, 65535);
|
|
}
|
|
|
|
/// <summary>Resource name the build-time-generated transient asset is saved under for <see cref="Load"/> to find in a Player build.</summary>
|
|
public const string ResourceName = "RuntimePipelineConfig";
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>Project-relative path of the authored settings file (never under Assets/).</summary>
|
|
private const string SettingsFilePath = "ProjectSettings/Packages/com.unity.pipeline/RuntimePipelineConfig.json";
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Load the current settings, or null if none exist yet. In the Editor (including Play
|
|
/// Mode) this reads the authored ProjectSettings/ JSON file directly; in a real Player
|
|
/// build it reads the build-time-generated transient Resources asset.
|
|
/// </summary>
|
|
/// <returns>The current settings, or null if none exist yet.</returns>
|
|
public static RuntimePipelineConfig Load()
|
|
{
|
|
#if UNITY_EDITOR
|
|
var path = GetAbsoluteSettingsPath();
|
|
if (!File.Exists(path))
|
|
return null;
|
|
|
|
var config = CreateInstance<RuntimePipelineConfig>();
|
|
try
|
|
{
|
|
JsonUtility.FromJsonOverwrite(File.ReadAllText(path), config);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning($"Pipeline: could not read runtime settings at '{path}' ({ex.Message}); using no config. Delete the file to reset.");
|
|
DestroyImmediate(config);
|
|
return null;
|
|
}
|
|
return config;
|
|
#else
|
|
return Resources.Load<RuntimePipelineConfig>(ResourceName);
|
|
#endif
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private static int s_LiveMaxWorkItemsPerFrameCache = 10;
|
|
private static DateTime s_LiveMaxWorkItemsPerFrameCacheFileTime;
|
|
|
|
/// <summary>
|
|
/// Cheap, per-frame-callable read of just maxWorkItemsPerFrame from the authored settings
|
|
/// file, for RuntimePipelineDriver.Update() to poll without paying Load()'s full
|
|
/// CreateInstance+JSON-parse cost every frame. Re-parses only when the file's last-write
|
|
/// time changes, so a Project Settings edit (including while in Play Mode) takes effect on
|
|
/// the very next frame, while an idle frame costs only a single file-timestamp check.
|
|
/// Editor-only: a Player's config is frozen at build time, so there is nothing to poll for.
|
|
/// </summary>
|
|
public static int GetLiveMaxWorkItemsPerFrame(int fallback)
|
|
{
|
|
var path = GetAbsoluteSettingsPath();
|
|
if (!File.Exists(path))
|
|
return fallback;
|
|
|
|
var writeTime = File.GetLastWriteTimeUtc(path);
|
|
if (writeTime != s_LiveMaxWorkItemsPerFrameCacheFileTime)
|
|
{
|
|
var config = Load();
|
|
if (config != null)
|
|
{
|
|
s_LiveMaxWorkItemsPerFrameCache = config.maxWorkItemsPerFrame;
|
|
DestroyImmediate(config);
|
|
}
|
|
s_LiveMaxWorkItemsPerFrameCacheFileTime = writeTime;
|
|
}
|
|
|
|
return s_LiveMaxWorkItemsPerFrameCache;
|
|
}
|
|
|
|
/// <summary>Persist this instance's current field values to the ProjectSettings/ JSON file.</summary>
|
|
public void Save()
|
|
{
|
|
var path = GetAbsoluteSettingsPath();
|
|
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
|
var previousWrite = File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.MinValue;
|
|
File.WriteAllText(path, JsonUtility.ToJson(this, true));
|
|
|
|
// GetLiveMaxWorkItemsPerFrame and the settings page detect changes by last-write time.
|
|
// Windows stamps writes with the system clock tick (~15 ms), so two saves in quick
|
|
// succession can share a stamp and the second one goes unnoticed. Make every save
|
|
// observable by moving the stamp forward when the file system did not.
|
|
if (File.GetLastWriteTimeUtc(path) <= previousWrite)
|
|
File.SetLastWriteTimeUtc(path, previousWrite.AddMilliseconds(1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Last-write-time of the authored settings file (UTC), or DateTime.MinValue if it doesn't
|
|
/// exist yet. Lets a long-lived caller (the Project Settings page) detect that the file
|
|
/// changed underneath it — e.g. a build's "Disable Pipeline" security-dialog choice, or a
|
|
/// set_runtime_pipeline_settings CLI call — without reloading from disk on every frame.
|
|
/// </summary>
|
|
public static DateTime GetSettingsFileWriteTimeUtc()
|
|
{
|
|
var path = GetAbsoluteSettingsPath();
|
|
return File.Exists(path) ? File.GetLastWriteTimeUtc(path) : DateTime.MinValue;
|
|
}
|
|
|
|
/// <summary>Delete the authored settings file, if any. For test cleanup only.</summary>
|
|
public static void DeleteSettingsFileForTesting()
|
|
{
|
|
var path = GetAbsoluteSettingsPath();
|
|
if (File.Exists(path))
|
|
File.Delete(path);
|
|
}
|
|
|
|
private static string GetAbsoluteSettingsPath()
|
|
{
|
|
// Application.dataPath is ".../Assets"; ProjectSettings is a sibling directory.
|
|
var projectRoot = Path.GetDirectoryName(Application.dataPath);
|
|
return Path.Combine(projectRoot, SettingsFilePath);
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Validate the configuration for correctness.
|
|
/// Called by build processor to ensure safe deployment.
|
|
/// </summary>
|
|
/// <returns>The validation result.</returns>
|
|
public ValidationResult Validate()
|
|
{
|
|
if (!enableInBuilds)
|
|
return ValidationResult.Success("Runtime Pipeline disabled");
|
|
|
|
// Port validation
|
|
if (port != 0 && (port < 7900 || port > 7999))
|
|
{
|
|
return ValidationResult.Warning(
|
|
$"Port {port} is outside recommended runtime range 7900-7999. May conflict with Editor instances.");
|
|
}
|
|
|
|
return ValidationResult.Success("Configuration is valid");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Result from configuration validation.
|
|
/// </summary>
|
|
[Serializable]
|
|
public class ValidationResult
|
|
{
|
|
/// <summary>False only for <see cref="Error"/> results.</summary>
|
|
public bool IsValid { get; set; }
|
|
/// <summary>"success", "warning", or "error".</summary>
|
|
public string Level { get; set; } // "success", "warning", "error"
|
|
/// <summary>Human-readable validation message.</summary>
|
|
public string Message { get; set; }
|
|
|
|
/// <summary>Create a successful validation result.</summary>
|
|
/// <param name="message">Human-readable message.</param>
|
|
/// <returns>A valid, "success"-level result.</returns>
|
|
public static ValidationResult Success(string message = "Valid") =>
|
|
new ValidationResult { IsValid = true, Level = "success", Message = message };
|
|
|
|
/// <summary>Create a valid-but-noteworthy validation result.</summary>
|
|
/// <param name="message">Human-readable message.</param>
|
|
/// <returns>A valid, "warning"-level result.</returns>
|
|
public static ValidationResult Warning(string message) =>
|
|
new ValidationResult { IsValid = true, Level = "warning", Message = message };
|
|
|
|
/// <summary>Create a failed validation result.</summary>
|
|
/// <param name="message">Human-readable message.</param>
|
|
/// <returns>An invalid, "error"-level result.</returns>
|
|
public static ValidationResult Error(string message) =>
|
|
new ValidationResult { IsValid = false, Level = "error", Message = message };
|
|
}
|
|
}
|