Files
daluandou/.codely.packages/cn.tuanjie.codely.bridge@1.0.85-47f31100/Editor/Bridge/Helpers/JobControl.cs
T
huadexuan 13d0c80335 ```
chore(bridge): 删除桥接包的提交记录和Git属性配置

删除了 .codely-bridge-commit 文件中存储的提交哈希值
移除了 .gitattributes 文件中的行结束符配置规则
```
2026-09-24 21:41:14 +08:00

123 lines
5.3 KiB
C#

using System;
using System.Collections.Concurrent;
namespace UnityTcp.Editor.Helpers
{
/// <summary>
/// Cross-thread control plane for in-flight runner jobs (step / async task / coroutine).
/// Both entry points are callable from any thread — in particular from the
/// <see cref="BackgroundCommandPump"/> worker while the main thread is busy or blocked:
///
/// Detach (<see cref="TryDetach"/>) takes effect immediately: it claims the job's
/// single-delivery gate, answers the original request with a "detached" ack carrying the job
/// id, and marks the job pending in <see cref="DetachedJobs"/> — all through thread-safe
/// state, so it works even mid-command. The job keeps running; its eventual outcome is
/// collected by polling <c>manage_job.status</c>.
///
/// Cancel (<see cref="RequestCancel"/>) cannot safely tear a job out of a runner from another
/// thread — runner state is main-thread-only — so it is queued here and applied by the owning
/// runner on its next tick: the job is failed with the given reason and answered (or
/// memorized, if detached).
///
/// Cancel requests are keyed by job id, last post wins. A request against a job id no runner
/// owns (finished, mistyped, or lost to a domain reload) is pruned after
/// <see cref="StaleAfterMs"/> so the table cannot grow unbounded.
/// </summary>
public static class JobControl
{
private sealed class CancelRequest
{
public string Reason;
public int PostedAtTick; // Environment.TickCount — readable from any thread,
// unlike the editor session clock
}
private static readonly ConcurrentDictionary<string, CancelRequest> _cancels =
new ConcurrentDictionary<string, CancelRequest>();
private const int StaleAfterMs = 120_000;
/// <summary>
/// Immediately detaches a running job, resolved by job id (or, when that fails, by the
/// native request id the client is stuck waiting on). Callable from any thread. On success
/// the original request has been answered with a detach ack and the job's outcome will be
/// collectable via <c>manage_job.status</c>.
/// </summary>
public static bool TryDetach(string jobId, ulong requestId, string reason, out string detachedJobId, out string error)
{
detachedJobId = null;
JobRegistry.IJobHandle handle;
if (!JobRegistry.TryGet(jobId, out handle) && !JobRegistry.TryGetByRequestId(requestId, out handle))
{
error = "No in-flight job matches the given job id / request id.";
return false;
}
detachedJobId = handle.JobId;
return handle.TryDetach(reason, out error);
}
/// <summary>
/// Detaches every in-flight job that is not already detached, all with the same reason.
/// Callable from any thread — used by <see cref="DialogWatcher"/> when a native
/// modal dialog blocks the main thread, so every caller stuck waiting on a job gets an
/// immediate answer telling it what is blocking and how to unblock it. Returns how many
/// jobs were detached.
/// </summary>
public static int DetachAll(string reason)
{
int detached = 0;
foreach (var handle in JobRegistry.Snapshot())
{
if (handle.TryDetach(reason, out _))
detached++;
}
return detached;
}
/// <summary>
/// Requests a cancel; callable from any thread. Applied by the owning runner on its next
/// tick. Returns false when no in-flight job matches (the request is still queued briefly,
/// in case the caller raced the job's enrollment).
/// </summary>
public static bool RequestCancel(string jobId, string reason = null)
{
if (string.IsNullOrEmpty(jobId)) return false;
Prune();
_cancels[jobId] = new CancelRequest { Reason = reason, PostedAtTick = Environment.TickCount };
return JobRegistry.TryGet(jobId, out _);
}
/// <summary>
/// Consumes the pending cancel for a job, if any. Called by the runners at tick time
/// (main thread), once per job per tick.
/// </summary>
internal static bool TryTakeCancel(string jobId, out string reason)
{
reason = null;
if (string.IsNullOrEmpty(jobId) || _cancels.IsEmpty) return false;
if (!_cancels.TryRemove(jobId, out var request)) return false;
reason = request.Reason;
return true;
}
internal static int PendingCancelCount => _cancels.Count;
internal static void Clear() => _cancels.Clear();
private static void Prune()
{
if (_cancels.IsEmpty) return;
int now = Environment.TickCount;
foreach (var kv in _cancels)
{
// unchecked subtraction stays correct across TickCount wraparound
if (unchecked(now - kv.Value.PostedAtTick) > StaleAfterMs)
_cancels.TryRemove(kv.Key, out _);
}
}
}
}