// // NosThreadSynchronizer.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NosSmooth.LocalBinding.Hooks; using NosSmooth.LocalBinding.Options; using Remora.Results; namespace NosSmooth.LocalBinding; /// /// Synchronizes with NosTale thread using a periodic function. /// public class NosThreadSynchronizer { private readonly IPeriodicHook _periodicHook; private readonly ILogger _logger; private readonly NosThreadSynchronizerOptions _options; private readonly ConcurrentQueue _queuedOperations; private Thread? _nostaleThread; /// /// Initializes a new instance of the class. /// /// The periodic hook. /// The logger. /// The options. public NosThreadSynchronizer ( IPeriodicHook periodicHook, ILogger logger, IOptions options ) { _periodicHook = periodicHook; _logger = logger; _options = options.Value; _queuedOperations = new ConcurrentQueue(); } /// /// Gets whether the current thread is a NosTale thread. /// public bool IsSynchronized => _nostaleThread == Thread.CurrentThread; /// /// Start the synchronizer operation. /// public void StartSynchronizer() { _periodicHook.Called += PeriodicCall; } /// /// Stop the synchronizer operation. /// public void StopSynchronizer() { _periodicHook.Called -= PeriodicCall; } private void PeriodicCall(object? owner, System.EventArgs eventArgs) { _nostaleThread = Thread.CurrentThread; var tasks = _options.MaxTasksPerIteration; while (tasks-- > 0 && _queuedOperations.TryDequeue(out var operation)) { ExecuteOperation(operation); } } private void ExecuteOperation(SyncOperation operation) { try { var result = operation.Action(); operation.Result = result; } catch (Exception e) { _logger.LogError(e, "Synchronizer obtained an exception"); operation.Result = (Result)e; } if (operation.CancellationTokenSource is not null) { try { operation.CancellationTokenSource.Cancel(); } catch (Exception) { // ignore } } } /// /// Enqueue the given operation to execute on next frame. /// /// The action to execute. /// Whether to execute the operation instantly in case we are on the NosTale thread. public void EnqueueOperation(Action action, bool executeIfSynchronized = true) { if (executeIfSynchronized && IsSynchronized) { // we are synchronized, no need to wait. action(); return; } _queuedOperations.Enqueue ( new SyncOperation ( () => { action(); return Result.FromSuccess(); }, null ) ); } /// /// Synchronizes to NosTale thread, executes the given action and returns its result. /// /// The action to execute. /// The cancellation token used for cancelling the operation. /// The result of the action. public async Task SynchronizeAsync(Func action, CancellationToken ct = default) { return (Result)await CommonSynchronizeAsync(() => action(), ct); } /// /// Synchronizes to NosTale thread, executes the given action and returns its result. /// /// The action to execute. /// The cancellation token used for cancelling the operation. /// The result of the action. /// The type of the result. public async Task> SynchronizeAsync(Func> action, CancellationToken ct = default) { return (Result)await CommonSynchronizeAsync(() => action(), ct); } private async Task CommonSynchronizeAsync(Func action, CancellationToken ct = default) { if (IsSynchronized) { // we are already synchronized, execute the action. try { return action(); } catch (Exception e) { return (Result)e; } } var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct); var syncOperation = new SyncOperation(action, linkedSource); _queuedOperations.Enqueue(syncOperation); try { await Task.Delay(Timeout.Infinite, linkedSource.Token); } catch (OperationCanceledException) { if (ct.IsCancellationRequested) { // Throw in case the top token was cancelled. throw; } } catch (Exception e) { return (Result)new ExceptionError(e); } return syncOperation.Result ?? Result.FromSuccess(); } private record SyncOperation(Func Action, CancellationTokenSource? CancellationTokenSource) { public IResult? Result { get; set; } } }