using System; using System.Collections.Concurrent; using System.IO; using System.Text.Json; using System.Threading.Tasks; using Robust.Client.AutoGenerated; using Robust.Client.Console; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Client.UserInterface.CustomControls { public interface IDebugConsoleView { /// /// Write a line with a specific color to the console window. /// void AddLine(FormattedMessage text, Color color); void AddLine(FormattedMessage text); void AddFormattedLine(FormattedMessage message); void Clear(); } // Quick note on how thread safety works in here: // Messages from other threads are not actually immediately drawn. They're stored in a queue. // Every frame OR the next time a message on the main thread comes in, this queue is drained. // This keeps thread safety while still making it so messages are ordered how they come in. // And also if Update() stops firing due to an exception loop the console will still work. // (At least from the main thread, which is what's throwing the exceptions..) [GenerateTypedNameReferences] public sealed partial class DebugConsole : Control, IDebugConsoleView { [Dependency] private IClientConsoleHost _consoleHost = default!; [Dependency] private IResourceManager _resourceManager = default!; [Dependency] private IConfigurationManager _cfg = default!; [Dependency] private ILogManager _logMan = default!; private static readonly ResPath HistoryPath = new("/debug_console_history.json"); private readonly ConcurrentQueue _messageQueue = new(); private readonly ISawmill _logger; private int _maxEntries; public DebugConsole() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _logger = _logMan.GetSawmill("dbgconsole"); InitCompletions(); CommandBar.OnTextChanged += OnCommandChanged; CommandBar.OnKeyBindDown += CommandBarOnOnKeyBindDown; CommandBar.OnTextEntered += CommandEntered; CommandBar.OnHistoryChanged += OnHistoryChanged; _loadHistoryFromDisk(); _compPopup = new DebugConsoleCompletion(); } protected override void EnteredTree() { base.EnteredTree(); _consoleHost.AddString += OnAddString; _consoleHost.AddFormatted += OnAddFormatted; _consoleHost.ClearText += OnClearText; _cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true); Root!.ModalRoot.AddChild(_compPopup); } protected override void ExitedTree() { base.ExitedTree(); _consoleHost.AddString -= OnAddString; _consoleHost.AddFormatted -= OnAddFormatted; _consoleHost.ClearText -= OnClearText; _cfg.UnsubValueChanged(CVars.ConMaxEntries, MaxEntriesChanged); _compPopup.Orphan(); } private void MaxEntriesChanged(int value) { _maxEntries = value; TrimExtraOutputEntries(); } private void OnClearText(object? _, EventArgs args) { Clear(); } private void OnAddFormatted(object? _, AddFormattedMessageArgs args) { AddFormattedLine(args.Message); } private void OnAddString(object? _, AddStringArgs args) { AddLine(args.Text, DetermineColor(args.Local, args.Error)); } private Color DetermineColor(bool local, bool error) { return error ? Color.Red : Color.White; } protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); _flushQueue(); } private void CommandEntered(LineEdit.LineEditEventArgs args) { if (!string.IsNullOrWhiteSpace(args.Text)) { _consoleHost.ExecuteCommand(args.Text); CommandBar.Clear(); CompletionCommandEntered(); } // commandChanged = true; } private void OnHistoryChanged() { _flushHistoryToDisk(); } public void AddLine(FormattedMessage text, Color color) { var formatted = new FormattedMessage(3); formatted.PushColor(color); formatted.AddMessage(text); formatted.Pop(); AddFormattedLine(formatted); } public void AddLine(FormattedMessage text) { AddLine(text, Color.White); } public void AddFormattedLine(FormattedMessage message) { _messageQueue.Enqueue(message); } public void Clear() { Output.Clear(); } private void _addFormattedLineInternal(FormattedMessage message) { Output.AddMessage(message); TrimExtraOutputEntries(); } private void TrimExtraOutputEntries() { while (Output.EntryCount > _maxEntries) { Output.RemoveEntry(0); } } private void _flushQueue() { while (_messageQueue.TryDequeue(out var message)) { _addFormattedLineInternal(message); } } private void CommandBarOnOnKeyBindDown(GUIBoundKeyEventArgs args) { if (args.Function == EngineKeyFunctions.TextScrollToBottom) { Output.ScrollToBottom(); args.Handle(); return; } CompletionKeyDown(args); } private void OnCommandChanged(LineEdit.LineEditEventArgs args) { // commandChanged = true; } private async void _loadHistoryFromDisk() { var data = await Task.Run(async () => { Stream? stream = null; for (var i = 0; i < 3; i++) { try { stream = _resourceManager.UserData.OpenRead(HistoryPath); break; } catch (FileNotFoundException) { // Nada, nothing to load in that case. return null; } catch (IOException) { // File locked probably?? await Task.Delay(10); } } if (stream == null) { _logger.Warning("Failed to load debug console history!"); return null; } try { return await JsonSerializer.DeserializeAsync(stream); } catch (Exception e) { _logger.Warning($"Failed to load debug console history due to exception!\n{e}"); return null; } finally { // ReSharper disable once MethodHasAsyncOverload stream.Dispose(); } }); if (data == null) return; CommandBar.ClearHistory(); CommandBar.History.AddRange(data); CommandBar.HistoryIndex = CommandBar.History.Count; } private async void _flushHistoryToDisk() { CommandBar.HistoryIndex = CommandBar.History.Count; var newHistory = JsonSerializer.Serialize(CommandBar.History); await Task.Run(async () => { StreamWriter? writer = null; for (var i = 0; i < 3; i++) { try { writer = _resourceManager.UserData.OpenWriteText(HistoryPath); break; } catch (IOException) { // Probably locking. await Task.Delay(10); } } if (writer == null) { _logger.Warning("Failed to save debug console history!"); return; } // ReSharper disable once UseAwaitUsing using (writer) { // ReSharper disable once MethodHasAsyncOverload writer.Write(newHistory); } }); } } }