using System; using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Avalonia.Threading; using AvaloniaEdit.Document; using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Processes; namespace StabilityMatrix.Avalonia.ViewModels; public partial class ConsoleViewModel : ObservableObject, IDisposable { // Queue for console updates private BufferBlock buffer = new(); // Task that updates the console (runs on UI thread) private Task? updateTask; // Cancellation token source for updateTask private CancellationTokenSource? updateCts; public bool IsUpdatesRunning => updateTask?.IsCompleted == false; [ObservableProperty] private TextDocument document = new(); // Special instruction events public event EventHandler? ApcInput; /// /// Starts update task for processing Post messages. /// public void StartUpdates() { updateCts = new CancellationTokenSource(); updateTask = Dispatcher.UIThread.InvokeAsync(ConsoleUpdateLoop, DispatcherPriority.Render); } /// /// Cancels the update task and waits for it to complete. /// public async Task StopUpdatesAsync() { updateCts?.Cancel(); updateCts = null; if (updateTask is not null) { await updateTask; updateTask = null; } } /// /// Clears the console and sets a new buffer. /// public void Clear() { // Clear document Document.Text = string.Empty; // Clear buffer and create new one buffer.Complete(); buffer = new BufferBlock(); } private async Task ConsoleUpdateLoop() { // This must be run in the UI thread Dispatcher.UIThread.CheckAccess(); // Update cancellation token must be set if (updateCts is null) { throw new InvalidOperationException("Update cancellation token must be set"); } // Get cancellation token var ct = updateCts.Token; try { while (!ct.IsCancellationRequested) { var output = await buffer.ReceiveAsync(ct); ConsoleUpdateOne(output); } } catch (OperationCanceledException) { } } /// /// Handle one instance of ProcessOutput. /// /// Not checked, but must be run in the UI thread. private void ConsoleUpdateOne(ProcessOutput output) { // Check for Apc messages if (output.ApcMessage is not null) { // Handle Apc message, for now just input audit events var message = output.ApcMessage.Value; if (message.Type == ApcType.Input) { ApcInput?.Invoke(this, message); } // Ignore further processing return; } using var update = Document.RunUpdate(); // Handle instruction to clear previous lines if (output.ClearLines > 0) { for (var i = 0; i < output.ClearLines; i++) { var lastLineIndex = Document.LineCount - 1; var line = Document.Lines[lastLineIndex]; Document.Remove(line.Offset, line.Length); } } // Add new line Document.Insert(Document.TextLength, output.Text); } /// /// Posts an update to the console /// Safe to call on non-UI threads /// public void Post(ProcessOutput output) { // If update task is running, send to buffer if (updateTask?.IsCompleted == false) { buffer.Post(output); return; } // Otherwise, use manual update one Dispatcher.UIThread.Post(() => ConsoleUpdateOne(output)); } /// /// Posts an update to the console. /// Helper for calling Post(ProcessOutput) with strings /// public void Post(string text) { Post(new ProcessOutput { Text = text }); } /// /// Posts an update to the console. /// Equivalent to Post(text + Environment.NewLine) /// public void PostLine(string text) { Post(new ProcessOutput { Text = text + Environment.NewLine }); } public void Dispose() { updateCts?.Cancel(); updateTask?.Dispose(); updateCts?.Dispose(); GC.SuppressFinalize(this); } }