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();
}
}
}