using System;
using System.IO;
using System.Diagnostics;
using Newtonsoft.Json;
using UnityEngine;
using Unity.Pipeline.Security;
#if UNITY_6000_5_OR_NEWER
using Unity.Scripting.LifecycleManagement;
#endif
namespace Unity.Pipeline.Models
{
///
/// Manages instance descriptor files for CLI discovery.
/// Written to Library/Pipeline/.unity-pipeline-port (under the project's git-ignored Library
/// folder) for CLI tools to find running Editor instances.
///
[Serializable]
#if UNITY_6000_5_OR_NEWER
[NoAutoStaticsCleanup]
#endif
class InstanceDescriptor
{
private const string DescriptorFileName = ".unity-pipeline-port";
///
/// Corrective guidance surfaced via when the Editor is interactive and
/// wasn't launched with -automated (UUM-149977). Read by every client before it can even
/// connect (see ), unlike a console warning which was logged once per
/// server instance and nobody watches anyway.
///
private const string AutomationInfoMessage =
"This editor has not been opened in automated mode and therefore could get stuck on modal dialogs. Human intervention may be required.";
///
/// Process ID of the Unity Editor
///
[JsonProperty("pid")]
public int Pid { get; set; }
///
/// HTTP port the pipeline server is listening on
///
[JsonProperty("port")]
public int Port { get; set; }
///
/// Full path to the Unity project
///
[JsonProperty("projectPath")]
public string ProjectPath { get; set; }
///
/// Name of the Unity project
///
[JsonProperty("projectName")]
public string ProjectName { get; set; }
///
/// Unity Editor version string
///
[JsonProperty("unityVersion")]
public string UnityVersion { get; set; }
///
/// Editor mode: "editor", "batchmode"
///
[JsonProperty("mode")]
public string Mode { get; set; }
///
/// When the Editor instance was started
///
[JsonProperty("startedAt")]
public DateTime StartedAt { get; set; }
///
/// Last heartbeat timestamp
///
[JsonProperty("lastHeartbeat")]
public DateTime LastHeartbeat { get; set; }
///
/// Security token for code evaluation commands
///
[JsonProperty("evalToken")]
public string EvalToken { get; set; }
///
/// Corrective guidance for the client, e.g. warning that this instance could get stuck on
/// a modal dialog. Null when there is nothing to say (launched with -automated, or
/// batchmode — which can't show modals regardless, see ) — omitted from
/// the JSON entirely, same convention as . Read this
/// before issuing commands: it's the one piece of state every client sees, since reading
/// the descriptor is a mandatory first step just to connect (UUM-149977). There is no
/// separate "automated" flag: it would only ever disagree with a null/non-null `Info` for
/// batchmode instances, and that distinction carries no information — batchmode can't show
/// modal dialogs either way.
///
[JsonProperty("info", NullValueHandling = NullValueHandling.Ignore)]
public string Info { get; set; }
///
/// Optional wire features this server understands (e.g. exec.argv), mirrored from
/// so the two carriers cannot drift.
///
/// Carried in the descriptor because every client already reads this file locally to
/// obtain the eval token, so negotiation costs no extra request. A descriptor with NO
/// capabilities key at all comes from a server too old to have the field, and a client
/// must then assume it supports none of these features.
///
[JsonProperty("capabilities", NullValueHandling = NullValueHandling.Ignore)]
public string[] Capabilities { get; set; }
///
/// Create instance descriptor for current Editor session
///
/// Port the server is listening on.
/// Whether the Editor was launched with -automated.
/// A populated descriptor for this Editor session.
public static InstanceDescriptor CreateCurrent(int port, bool automated)
{
try
{
var projectPath = Path.GetDirectoryName(Application.dataPath);
var projectName = Path.GetFileName(projectPath);
var pid = Process.GetCurrentProcess().Id;
var unityVersion = Application.unityVersion;
var isBatchMode = Application.isBatchMode;
// Generate eval token at server startup for CLI auto-discovery
var evalToken = SecurityTokenManager.GetOrCreateToken();
return new InstanceDescriptor
{
Capabilities = BasePipelineServer.Capabilities,
Pid = pid,
Port = port,
ProjectPath = projectPath,
ProjectName = projectName,
UnityVersion = unityVersion,
Mode = isBatchMode ? "batchmode" : "editor",
StartedAt = DateTime.UtcNow,
LastHeartbeat = DateTime.UtcNow,
EvalToken = evalToken,
// Only in an interactive Editor: batchmode (CI, upm-pvp, UTR) can't show modal
// popups, so the guidance would be pure noise there.
Info = (!automated && !isBatchMode) ? AutomationInfoMessage : null
};
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"CreateCurrent failed: {ex.Message}");
UnityEngine.Debug.LogError($"Stack trace: {ex.StackTrace}");
throw;
}
}
///
/// Serializes descriptor writes. Heartbeat rewrites can now run concurrently (requests
/// are processed in parallel since the /api/progress work); a torn concurrent write
/// would leave clients reading half a descriptor.
///
private static readonly object m_WriteGate = new object();
///
/// Write instance descriptor to project root
///
/// The descriptor to write.
public static void WriteToProjectRoot(InstanceDescriptor descriptor)
{
lock (m_WriteGate)
{
WriteToProjectRootLocked(descriptor);
}
}
private static void WriteToProjectRootLocked(InstanceDescriptor descriptor)
{
try
{
var filePath = GetDescriptorFilePath(descriptor.ProjectPath);
var isNewFile = !File.Exists(filePath);
// The descriptor lives under Library/Pipeline, which may not exist yet.
Directory.CreateDirectory(Path.GetDirectoryName(filePath));
var json = JsonConvert.SerializeObject(descriptor, Formatting.Indented);
File.WriteAllText(filePath, json);
// The descriptor carries the auth token; keep it readable only by the current user.
// Applied once on creation (heartbeat rewrites preserve the existing permissions).
if (isNewFile)
FilePermissions.RestrictToCurrentUser(filePath);
}
catch (Exception ex)
{
UnityEngine.Debug.LogError($"WriteToProjectRoot failed: {ex.Message}");
UnityEngine.Debug.LogError($"Descriptor: {descriptor?.ProjectPath}, PID: {descriptor?.Pid}");
throw;
}
}
///
/// Read instance descriptor from project root
///
/// Absolute path to the Unity project.
/// The descriptor, or null if missing/corrupted.
public static InstanceDescriptor ReadFromProjectRoot(string projectPath)
{
var filePath = GetDescriptorFilePath(projectPath);
if (!File.Exists(filePath))
return null;
try
{
var json = File.ReadAllText(filePath);
return JsonConvert.DeserializeObject(json);
}
catch
{
// Invalid or corrupted file
return null;
}
}
///
/// Remove instance descriptor from project root
///
/// Absolute path to the Unity project.
public static void RemoveFromProjectRoot(string projectPath)
{
var filePath = GetDescriptorFilePath(projectPath);
if (File.Exists(filePath))
{
try
{
File.Delete(filePath);
}
catch
{
// Ignore cleanup errors
}
}
}
///
/// Update heartbeat timestamp in existing descriptor file
///
/// Absolute path to the Unity project.
public static void UpdateHeartbeat(string projectPath)
{
var descriptor = ReadFromProjectRoot(projectPath);
if (descriptor != null)
{
descriptor.LastHeartbeat = DateTime.UtcNow;
WriteToProjectRoot(descriptor);
}
}
///
/// Absolute path to the editor descriptor file for a project: the git-ignored
/// Library/Pipeline folder under the project root. Kept in one place so discovery code
/// and tests can't drift from where this class itself reads/writes.
///
/// Absolute path to the Unity project.
/// The descriptor file's absolute path.
public static string GetDescriptorFilePath(string projectPath)
{
return Path.Combine(projectPath, "Library", "Pipeline", DescriptorFileName);
}
}
}