using System; namespace Unity.Pipeline { /// /// Per-server progress-reporting state. Owned one-per- (the /// same pattern as Dispatcher) so a test server's execution can never cross-attribute /// progress with the live server's. See for the public static /// façade that arbitrary command code calls. /// internal sealed class CliProgressState { /// Immutable copy of the current progress state (thread-safe to hand across threads). internal readonly struct Snapshot { public readonly bool HasReport; public readonly string Title; public readonly string Info; public readonly long? Current; public readonly long? Total; public readonly double? Progress01; public Snapshot(string title, string info, long? current, long? total, double? progress01) { HasReport = true; Title = title; Info = info; Current = current; Total = total; Progress01 = progress01; } } readonly object m_Gate = new object(); Snapshot m_Explicit; string m_ExplicitOwnerId; string m_CurrentOwnerId; Snapshot m_Ambient; int m_ActiveCount; /// Whether any execution is currently in flight. Read by /api/progress. internal bool IsActive { get { lock (m_Gate) { return m_ActiveCount > 0; } } } /// Marks one more execution in flight (see ). internal void BeginExecutionCount() { lock (m_Gate) { m_ActiveCount++; } } /// /// Marks one execution finished, resetting the explicit-report state when this was the /// last one in flight. The decrement and the reset-on-zero check happen under the same /// lock as , so a new execution starting in the gap can never /// have its fresh BeginExecution/Report wiped by this reset — the previous implementation /// decremented a separate counter and reset separately, leaving a window where a finishing /// execution could decrement to zero, get preempted, and then wipe a new execution's /// owner id and early report once it resumed. /// internal void EndExecutionCount() { lock (m_Gate) { m_ActiveCount--; if (m_ActiveCount == 0) { m_Explicit = default; m_ExplicitOwnerId = null; m_CurrentOwnerId = null; } } } /// /// Report the current task's progress so a connected CLI can render it live. /// Safe to call from any thread, at any frequency (the server samples on poll). /// /// Short task title, e.g. "Generating World". /// Detail line, e.g. "Processing 42/100". /// Current step, when the task has countable steps. /// Total steps, when the task has countable steps. /// Completion in the 0–1 range; omit for indeterminate tasks /// (when omitted and both and are /// present, the CLI derives a percentage from them). internal void Report(string title, string info = null, long? current = null, long? total = null, double? progress = null) { var clamped = progress.HasValue ? Math.Max(0d, Math.Min(1d, progress.Value)) : (double?)null; lock (m_Gate) { m_Explicit = new Snapshot(title, info, current, total, clamped); m_ExplicitOwnerId = m_CurrentOwnerId; } } /// Clear an explicit (e.g. when the reported task finishes). internal void Clear() { lock (m_Gate) { m_Explicit = default; m_ExplicitOwnerId = null; } } /// Ambient-source mirror (UnityEditor.Progress items). Explicit reports win. internal void ReportAmbient(string title, string info, long? current, long? total, double? progress01) { lock (m_Gate) { m_Ambient = new Snapshot(title, info, current, total, progress01); } } /// Clear the ambient mirror (no running UnityEditor.Progress items remain). internal void ClearAmbient() { lock (m_Gate) { m_Ambient = default; } } /// /// Marks the start of the execution identified by (a job id, /// or a synthetic id for a plain /api/exec). Clears any leftover explicit report /// from whatever ran before it and becomes the only owner will /// accept explicit reports from until is called — so a queued /// execution can never surface the previous one's progress before reporting its own. /// internal void BeginExecution(string ownerId) { lock (m_Gate) { m_CurrentOwnerId = ownerId; m_Explicit = default; m_ExplicitOwnerId = null; } } /// /// Marks the end of the execution identified by . A no-op if /// some other execution is already current (e.g. called after a swallowed exception), /// so callers can call this unconditionally in a finally. /// internal void EndExecution(string ownerId) { lock (m_Gate) { if (m_CurrentOwnerId == ownerId) { m_CurrentOwnerId = null; } } } /// /// The snapshot the /api/progress endpoint serves: the explicit report, but only when it /// was made by the currently executing owner (see ); falls /// back to the ambient mirror otherwise. /// internal Snapshot Current { get { lock (m_Gate) { var explicitValid = m_Explicit.HasReport && m_ExplicitOwnerId == m_CurrentOwnerId; return explicitValid ? m_Explicit : m_Ambient; } } } } /// /// Structured progress reporting for long-running pipeline commands — the server side of the /// CLI's terminal progress bars (CLI-488, coordinated with CLI-335). /// /// While a command executes over /api/exec, the CLI polls GET /api/progress and /// renders whatever is reported here (a progress bar in a terminal, NDJSON progress frames in /// machine formats). Command authors call from any thread — including a /// main thread that is blocked inside a long synchronous command, which is exactly the /// EditorUtility.DisplayProgressBar scenario this exists for. The state is read by the /// HTTP listener thread, never the main thread, so progress stays visible even while the /// Editor is busy. /// /// This is a thin static façade over the executing server's own /// (resolved via ) so command code — which has /// no reference to "which server is running me" — can keep calling this the same way /// regardless of which server dispatched it. /// /// Ambient UnityEditor.Progress items are mirrored in by the Editor assembly /// (EditorProgressMirror), targeting the live server directly; an explicit /// always wins over the ambient mirror. Every execution (a plain /// /api/exec or a detached job) is bracketed with BeginExecution/EndExecution on the /// server's own , which tags who the current explicit report is /// allowed to belong to — so a job queued behind one that just finished never inherits its /// stale progress before making its own first report, no matter how long that job takes to /// get around to reporting anything. /// public static class CliProgress { /// /// Report the current task's progress so a connected CLI can render it live. /// Safe to call from any thread, at any frequency (the server samples on poll). A no-op /// when called outside a command's own execution (no server to attribute it to). /// /// Short title for the operation. /// Optional detail text (e.g. current step). /// Current progress amount, if determinate. /// Total amount, if determinate. /// Progress in [0, 1], used instead of current/total when set. public static void Report(string title, string info = null, long? current = null, long? total = null, double? progress = null) { BasePipelineServer.CurrentServer?.Progress.Report(title, info, current, total, progress); } /// Clear an explicit (e.g. when the reported task finishes). public static void Clear() { BasePipelineServer.CurrentServer?.Progress.Clear(); } } }