using System.Collections.Concurrent; namespace DocxWorkbench.WordInterop; internal sealed class StaWordQueue : IDisposable { private readonly BlockingCollection _jobs = new(); private readonly Thread _thread; private readonly ManualResetEventSlim _stopped = new(false); private readonly object _lifetimeGate = new(); private bool _accepting = true; public StaWordQueue() { _thread = new Thread(Run) { IsBackground = true, Name = "DocumentAutomation.Word.STA" }; _thread.SetApartmentState(ApartmentState.STA); _thread.Start(); } public Task Enqueue(Func operation, CancellationToken cancellationToken) { if (operation is null) throw new ArgumentNullException(nameof(operation)); if (cancellationToken.IsCancellationRequested) return Task.FromCanceled(cancellationToken); var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); lock (_lifetimeGate) { if (!_accepting) throw new ObjectDisposedException(nameof(StaWordQueue)); try { _jobs.Add(() => Execute(operation, completion, cancellationToken), cancellationToken); } catch (InvalidOperationException) { throw new ObjectDisposedException(nameof(StaWordQueue)); } catch (OperationCanceledException) { completion.TrySetCanceled(cancellationToken); } } return completion.Task; } private static void Execute(Func operation, TaskCompletionSource completion, CancellationToken cancellationToken) { try { cancellationToken.ThrowIfCancellationRequested(); completion.TrySetResult(operation()); } catch (OperationCanceledException ex) { completion.TrySetCanceled(ex.CancellationToken); } catch (Exception ex) { completion.TrySetException(ex); } } private void Run() { try { foreach (var job in _jobs.GetConsumingEnumerable()) job(); } finally { _stopped.Set(); } } public void Dispose() { lock (_lifetimeGate) { if (!_accepting) return; _accepting = false; _jobs.CompleteAdding(); } if (Thread.CurrentThread != _thread) _stopped.Wait(TimeSpan.FromSeconds(30)); // Do not dispose the collection or stop event while the STA worker may // still be draining it. The worker owns the final enumeration lifetime. } }