61 lines
2.4 KiB
C#
61 lines
2.4 KiB
C#
using System.Collections.Concurrent;
|
|
|
|
namespace DocxWorkbench.WordInterop;
|
|
|
|
internal sealed class StaWordQueue : IDisposable
|
|
{
|
|
private readonly BlockingCollection<Action> _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<T> Enqueue<T>(Func<T> operation, CancellationToken cancellationToken)
|
|
{
|
|
if (operation is null) throw new ArgumentNullException(nameof(operation));
|
|
if (cancellationToken.IsCancellationRequested) return Task.FromCanceled<T>(cancellationToken);
|
|
var completion = new TaskCompletionSource<T>(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<T>(Func<T> operation, TaskCompletionSource<T> 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.
|
|
}
|
|
}
|