| | | 1 | | using System; |
| | | 2 | | using System.Threading; |
| | | 3 | | |
| | | 4 | | namespace AmbientServices; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// A basic default implementation of <see cref="IAmbientProgressService"/> that tracks a stack of progress scopes for e |
| | | 8 | | /// </summary> |
| | | 9 | | /// <remarks> |
| | | 10 | | /// <pitch>The zero-configuration, in-process progress service used unless overridden. Progress state lives entirely in |
| | | 11 | | /// <pledge><see cref="IAmbientProgressService"/></pledge> |
| | | 12 | | /// <pledge>Out-of-order disposal of part scopes is detected rather than silently tolerated: the service makes a best-ef |
| | | 13 | | /// <plan> |
| | | 14 | | /// Holds the innermost <see cref="AmbientServices.Progress"/> for each execution context in an <see cref="AsyncLocal{T} |
| | | 15 | | /// </plan> |
| | | 16 | | /// </remarks> |
| | | 17 | | [DefaultAmbientService] |
| | | 18 | | internal class BasicAmbientProgress : IAmbientProgressService |
| | | 19 | | { |
| | | 20 | | private readonly AsyncLocal<Progress?> _progress; |
| | | 21 | | |
| | 2 | 22 | | public BasicAmbientProgress() |
| | | 23 | | { |
| | 2 | 24 | | _progress = new AsyncLocal<Progress?>(); |
| | 2 | 25 | | } |
| | | 26 | | |
| | | 27 | | public IAmbientProgress? Progress |
| | | 28 | | { |
| | | 29 | | get |
| | | 30 | | { |
| | 2 | 31 | | IAmbientProgress? ambientProgress = _progress.Value; |
| | 2 | 32 | | Progress? topProgress = ambientProgress as Progress; |
| | | 33 | | // no progress in this context yet, or the context has been disposed (the docs say not to do that, but we ha |
| | 2 | 34 | | if (ambientProgress == null || (topProgress?.Disposed ?? false)) |
| | | 35 | | { |
| | 2 | 36 | | topProgress = new Progress(this, null, 0.0f, 1.0f, null, false); |
| | 2 | 37 | | _progress.Value = topProgress; |
| | 2 | 38 | | ambientProgress = topProgress; |
| | | 39 | | } |
| | | 40 | | else |
| | | 41 | | { |
| | 2 | 42 | | ambientProgress = _progress.Value; |
| | | 43 | | } |
| | 2 | 44 | | return ambientProgress; |
| | | 45 | | } |
| | | 46 | | } |
| | | 47 | | |
| | | 48 | | public void PushSubProgress(Progress subProgress) |
| | | 49 | | { |
| | 2 | 50 | | _progress.Value = subProgress; |
| | 2 | 51 | | } |
| | | 52 | | public void PopSubProgress(Progress specified) |
| | | 53 | | { |
| | 2 | 54 | | Progress? expected = _progress.Value; |
| | 2 | 55 | | Progress? pop = expected; |
| | | 56 | | // are the specified progress and the one at the top of the stack *not* the same? |
| | 2 | 57 | | if (expected != specified) |
| | | 58 | | { |
| | | 59 | | // walk up from both the expected progress and the specified progress to try to find the others |
| | 2 | 60 | | Progress? tryFindExpected = Progress as Progress; |
| | 2 | 61 | | Progress? specifiedPopperAncestor = specified; |
| | 2 | 62 | | Progress? expectedPopperAncestor = tryFindExpected; |
| | 2 | 63 | | while (specifiedPopperAncestor != null || expectedPopperAncestor != null) |
| | | 64 | | { |
| | 2 | 65 | | specifiedPopperAncestor = specifiedPopperAncestor?.Parent as Progress; |
| | 2 | 66 | | expectedPopperAncestor = expectedPopperAncestor?.Parent as Progress; |
| | | 67 | | // did we find the expected progress up the chain from the specified one (popping the specified progress |
| | 2 | 68 | | if (specifiedPopperAncestor == tryFindExpected) |
| | | 69 | | { |
| | | 70 | | // we've *already* popped an ancestor of the specified progress, so there's nothing left to pop off |
| | 2 | 71 | | pop = null; |
| | | 72 | | // if we add dispose code below, we could also check to see if things are disposed here |
| | 2 | 73 | | break; |
| | | 74 | | } |
| | | 75 | | // did we find the specified progress up the stack from the expected progress (popping the specified pro |
| | 2 | 76 | | else if (expectedPopperAncestor == specified) |
| | | 77 | | { |
| | | 78 | | // pop everything up to the specified progress |
| | 2 | 79 | | pop = specified; |
| | | 80 | | // if there were anything to do other than the stack and the progress update (which isn't needed bec |
| | 2 | 81 | | break; |
| | | 82 | | } |
| | | 83 | | // else just keep walking up the stack |
| | | 84 | | } |
| | | 85 | | // pop all the way up to the specified item (if needed) |
| | 2 | 86 | | if (pop != null) Pop(pop); |
| | | 87 | | // the best we can do at this point is to just pop like normal and hope that the correct number of pops occu |
| | 2 | 88 | | throw new InvalidOperationException("The SubProgress object stack is corrupt!"); |
| | | 89 | | } |
| | 2 | 90 | | Pop(pop); |
| | 2 | 91 | | } |
| | | 92 | | private void Pop(Progress? subProgress) |
| | | 93 | | { |
| | 2 | 94 | | IAmbientProgress? parent = subProgress?.Parent; |
| | 2 | 95 | | _progress.Value = parent as Progress; |
| | 2 | 96 | | } |
| | | 97 | | } |
| | | 98 | | |
| | | 99 | | /// <summary> |
| | | 100 | | /// The <see cref="IAmbientProgress"/> realization used by <see cref="BasicAmbientProgress"/>: one node in the per-execu |
| | | 101 | | /// </summary> |
| | | 102 | | /// <remarks> |
| | | 103 | | /// <pitch>Tracks one part of an operation — its portion complete, the item being processed, and its cancellation — prop |
| | | 104 | | /// <pledge><see cref="IAmbientProgress"/></pledge> |
| | | 105 | | /// <pledge>Also <see cref="IDisposable"/>: disposal reports the part complete (1.0) to the parent — swallowing any pend |
| | | 106 | | /// <plan> |
| | | 107 | | /// Stores the parent, the start portion and portion span it occupies within the parent, and an item-name prefix, all fi |
| | | 108 | | /// </plan> |
| | | 109 | | /// </remarks> |
| | | 110 | | internal class Progress : IAmbientProgress, IDisposable |
| | | 111 | | { |
| | | 112 | | private readonly BasicAmbientProgress _tracker; |
| | | 113 | | private readonly string _prefix; |
| | | 114 | | private bool _inheritedCancelSource; // if we inherited the cancel source, there is no need t |
| | | 115 | | private bool _ownCancelSource; |
| | | 116 | | private readonly float _startPortion; |
| | | 117 | | private readonly float _portionPart; |
| | | 118 | | private float _portionComplete; |
| | | 119 | | private string _currentItem; |
| | | 120 | | |
| | | 121 | | public Progress(BasicAmbientProgress progress) |
| | | 122 | | : this (progress, null, 0.0f, 1.0f, null, false) |
| | | 123 | | { |
| | | 124 | | } |
| | | 125 | | public Progress(BasicAmbientProgress progressService, IAmbientProgress? parentProgress, float startPortion, float po |
| | | 126 | | { |
| | | 127 | | _tracker = progressService; |
| | | 128 | | _prefix = prefix ?? ""; |
| | | 129 | | _currentItem = ""; |
| | | 130 | | if (startPortion < 0.0 || startPortion > 1.0) throw new ArgumentOutOfRangeException(nameof(startPortion), "The s |
| | | 131 | | if (portionPart < 0.0 || portionPart > 1.0) throw new ArgumentOutOfRangeException(nameof(portionPart), "The port |
| | | 132 | | if (startPortion + portionPart > 1.0) throw new ArgumentOutOfRangeException(nameof(portionPart), "The sum of the |
| | | 133 | | Parent = parentProgress; |
| | | 134 | | _startPortion = startPortion; |
| | | 135 | | _portionPart = portionPart; |
| | | 136 | | if (_inheritedCancelSource = inheritCancellationSource) // note that this is an ASSIGNMENT in addition to a test |
| | | 137 | | { |
| | | 138 | | AmbientCancellationTokenSource? parentCancellationSource = parentProgress?.CancellationTokenSource; |
| | | 139 | | if (parentCancellationSource != null) |
| | | 140 | | { |
| | | 141 | | CancellationTokenSource = parentCancellationSource; |
| | | 142 | | _ownCancelSource = false; // this was true, but I believe false is correct because the parent prog |
| | | 143 | | } |
| | | 144 | | else |
| | | 145 | | { |
| | | 146 | | CancellationTokenSource = new AmbientCancellationTokenSource(); |
| | | 147 | | _ownCancelSource = true; |
| | | 148 | | } |
| | | 149 | | } |
| | | 150 | | else |
| | | 151 | | { |
| | | 152 | | CancellationTokenSource = new AmbientCancellationTokenSource(); |
| | | 153 | | _ownCancelSource = true; |
| | | 154 | | } |
| | | 155 | | progressService.PushSubProgress(this); |
| | | 156 | | } |
| | | 157 | | |
| | | 158 | | public void ResetCancellation(TimeSpan timeout) |
| | | 159 | | { |
| | | 160 | | AmbientCancellationTokenSource cancelSource = new(timeout); |
| | | 161 | | // dispose of any previously-held cancellation token source and swap in the new one |
| | | 162 | | if (_ownCancelSource) CancellationTokenSource.Dispose(); |
| | | 163 | | _inheritedCancelSource = false; |
| | | 164 | | _ownCancelSource = true; |
| | | 165 | | CancellationTokenSource = cancelSource; |
| | | 166 | | } |
| | | 167 | | public void ResetCancellation(CancellationTokenSource? cancellationTokenSource = null) |
| | | 168 | | { |
| | | 169 | | AmbientCancellationTokenSource? cancelSource = (cancellationTokenSource == null) ? new AmbientCancellationTokenS |
| | | 170 | | // dispose of any previously-held cancellation token source and swap in the new one |
| | | 171 | | if (_ownCancelSource) CancellationTokenSource.Dispose(); |
| | | 172 | | _inheritedCancelSource = false; |
| | | 173 | | _ownCancelSource = true; |
| | | 174 | | CancellationTokenSource = cancelSource; |
| | | 175 | | } |
| | | 176 | | public void ThrowIfCancelled() |
| | | 177 | | { |
| | | 178 | | CancellationTokenSource.Token.ThrowIfCancellationRequested(); |
| | | 179 | | } |
| | | 180 | | public CancellationToken CancellationToken => CancellationTokenSource.Token; |
| | | 181 | | public AmbientCancellationTokenSource CancellationTokenSource { get; private set; } |
| | | 182 | | public float PortionComplete => _portionComplete; |
| | | 183 | | public string ItemCurrentlyBeingProcessed => _currentItem; |
| | | 184 | | public void Update(float portionComplete, string? itemCurrentlyBeingProcessed = null) |
| | | 185 | | { |
| | | 186 | | if (portionComplete < 0.0 || portionComplete > 1.0) throw new ArgumentOutOfRangeException(nameof(portionComplete |
| | | 187 | | Interlocked.Exchange(ref _portionComplete, portionComplete); |
| | | 188 | | if (itemCurrentlyBeingProcessed != null) Interlocked.Exchange(ref _currentItem, itemCurrentlyBeingProcessed); |
| | | 189 | | // have we been canceled? |
| | | 190 | | if ((!_inheritedCancelSource || Parent == null) && CancellationTokenSource.IsCancellationRequested) Cancellation |
| | | 191 | | Parent?.Update(_startPortion + _portionPart * portionComplete, itemCurrentlyBeingProcessed == null ? null : _pre |
| | | 192 | | } |
| | | 193 | | public IDisposable TrackPart(float startPortion, float portionPart, string? prefix = null, bool inheritCancellationT |
| | | 194 | | { |
| | | 195 | | string partPrefix = _prefix + prefix; |
| | | 196 | | Progress ret = new(_tracker, this, startPortion, portionPart, partPrefix, inheritCancellationTokenSource); |
| | | 197 | | ret.Update(0.0f); |
| | | 198 | | return ret; |
| | | 199 | | } |
| | | 200 | | public void Dispose() |
| | | 201 | | { |
| | | 202 | | if (!Disposed) |
| | | 203 | | { |
| | | 204 | | try |
| | | 205 | | { |
| | | 206 | | // make sure the parent knows we're done, but prevent throwing a cancellation exception during disposal |
| | | 207 | | Update(1.0f); |
| | | 208 | | } |
| | | 209 | | catch (OperationCanceledException) { } // ignore these in Dispose! |
| | | 210 | | _tracker.PopSubProgress(this); |
| | | 211 | | Disposed = true; // mark that we're disposed to help us make some kind of attempt to recover from progress |
| | | 212 | | |
| | | 213 | | // only dispose of the cancel source if we own it |
| | | 214 | | if (_ownCancelSource) CancellationTokenSource.Dispose(); // note that this will cancel any associated toke |
| | | 215 | | } |
| | | 216 | | } |
| | | 217 | | internal bool Disposed { get; private set; } |
| | | 218 | | internal IAmbientProgress? Parent { get; } |
| | | 219 | | } |