diff --git a/Daybreak/Controls/Glyphs/BugGlyph.xaml b/Daybreak/Controls/Glyphs/BugGlyph.xaml new file mode 100644 index 00000000..6bc70373 --- /dev/null +++ b/Daybreak/Controls/Glyphs/BugGlyph.xaml @@ -0,0 +1,14 @@ + + + + + diff --git a/Daybreak/Controls/Glyphs/BugGlyph.xaml.cs b/Daybreak/Controls/Glyphs/BugGlyph.xaml.cs new file mode 100644 index 00000000..2a575d66 --- /dev/null +++ b/Daybreak/Controls/Glyphs/BugGlyph.xaml.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace Daybreak.Controls.Glyphs; +/// +/// Interaction logic for BugGlyph.xaml +/// +public partial class BugGlyph : UserControl +{ + public BugGlyph() + { + InitializeComponent(); + } +} diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index e4c59380..4cdad832 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -11,7 +11,7 @@ preview Daybreak.ico true - 0.9.9.54 + 0.9.9.55 true cfb2a489-db80-448d-a969-80270f314c46 True diff --git a/Daybreak/Launch/ExceptionDialog.xaml b/Daybreak/Launch/ExceptionDialog.xaml new file mode 100644 index 00000000..5452d940 --- /dev/null +++ b/Daybreak/Launch/ExceptionDialog.xaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + diff --git a/Daybreak/Launch/ExceptionDialog.xaml.cs b/Daybreak/Launch/ExceptionDialog.xaml.cs new file mode 100644 index 00000000..0e6f1d14 --- /dev/null +++ b/Daybreak/Launch/ExceptionDialog.xaml.cs @@ -0,0 +1,72 @@ +using System; +using System.Diagnostics; +using System.Web; +using System.Windows; +using System.Windows.Extensions; + +namespace Daybreak.Launch; +/// +/// Interaction logic for ExceptionDialog.xaml +/// +public partial class ExceptionDialog : Window +{ + private const string TitlePlaceholder = "[TITLE]"; + private const string BodyPlaceholder = "[BODY]"; + private const string IssueUrl = $"https://github.com/gwdevhub/Daybreak/issues/new?title={TitlePlaceholder}&body={BodyPlaceholder}&labels=bug"; + + private string exceptionName; + + [GenerateDependencyProperty] + private string exceptionMessage = string.Empty; + + public ExceptionDialog(Exception exception) + { + this.InitializeComponent(); + this.exceptionName = exception.GetType().Name; + this.ExceptionMessage = exception.ToString(); + } + + public ExceptionDialog(string exceptionName, string exceptionMessage) + { + this.InitializeComponent(); + this.exceptionName = exceptionName; + this.ExceptionMessage = exceptionMessage; + } + + private void OkButton_Clicked(object sender, EventArgs e) + { + this.Close(); + } + + private void ReportButton_Clicked(object sender, EventArgs e) + { + var title = $"[User Report] {this.exceptionName}"; + var body = this.ExceptionMessage; + + var url = IssueUrl + .Replace(TitlePlaceholder, HttpUtility.UrlEncode(title)) + .Replace(BodyPlaceholder, HttpUtility.UrlEncode(body)); + + Process.Start(new ProcessStartInfo + { + FileName = url, + UseShellExecute = true + }); + + this.Close(); + } + + public static void ShowException(Exception exception) + { + var exceptionDialog = new ExceptionDialog(exception); + exceptionDialog.ShowDialog(); + return; + } + + public static void ShowException(string exceptionName, string exceptionMessage) + { + var exceptionDialog = new ExceptionDialog(exceptionName, exceptionMessage); + exceptionDialog.ShowDialog(); + return; + } +} diff --git a/Daybreak/Launch/MainWindow.xaml b/Daybreak/Launch/MainWindow.xaml index b1685e7d..bac7cb2b 100644 --- a/Daybreak/Launch/MainWindow.xaml +++ b/Daybreak/Launch/MainWindow.xaml @@ -193,6 +193,25 @@ + + + + + + + launcherOptions; private readonly ILiveOptions themeOptions; + private readonly ILogger logger; private readonly CancellationTokenSource cancellationToken = new(); [GenerateDependencyProperty] @@ -74,7 +78,8 @@ public MainWindow( IPrivilegeManager privilegeManager, IOptionsUpdateHook optionsUpdateHook, ILiveOptions launcherOptions, - ILiveOptions themeOptions) + ILiveOptions themeOptions, + ILogger logger) { this.optionsSynchronizationService = optionsSynchronizationService.ThrowIfNull(); this.splashScreenService = splashScreenService.ThrowIfNull(); @@ -85,6 +90,7 @@ public MainWindow( this.privilegeManager = privilegeManager.ThrowIfNull(); this.launcherOptions = launcherOptions.ThrowIfNull(); this.themeOptions = themeOptions.ThrowIfNull(); + this.logger = logger.ThrowIfNull(); optionsUpdateHook.ThrowIfNull().RegisterHook(this.ThemeOptionsChanged); this.InitializeComponent(); this.CurrentVersionText = this.applicationUpdater.CurrentVersion.ToString(); @@ -121,6 +127,22 @@ private void SynchronizeButton_Click(object sender, EventArgs e) this.viewManager.ShowView(); } + private void BugButton_Click(object sender, EventArgs e) + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = IssueUrl, + UseShellExecute = true + }); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Encountered exception while opening issues page"); + } + } + private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.ChangedButton is not MouseButton.Left && diff --git a/Daybreak/Models/Notifications/Handling/MessageBoxHandler.cs b/Daybreak/Models/Notifications/Handling/MessageBoxHandler.cs index d0db5e1a..b0f8d4c2 100644 --- a/Daybreak/Models/Notifications/Handling/MessageBoxHandler.cs +++ b/Daybreak/Models/Notifications/Handling/MessageBoxHandler.cs @@ -1,4 +1,5 @@ -using System.Windows; +using Daybreak.Launch; +using System.Windows; namespace Daybreak.Models.Notifications.Handling; @@ -6,6 +7,6 @@ public sealed class MessageBoxHandler : INotificationHandler { public void OpenNotification(Notification notification) { - MessageBox.Show(notification.Description, notification.Title); + ExceptionDialog.ShowException(notification.Title, notification.Description); } } diff --git a/Daybreak/Services/ExceptionHandling/ExceptionHandler.cs b/Daybreak/Services/ExceptionHandling/ExceptionHandler.cs index 719a0cb2..9dcff1d3 100644 --- a/Daybreak/Services/ExceptionHandling/ExceptionHandler.cs +++ b/Daybreak/Services/ExceptionHandling/ExceptionHandler.cs @@ -1,4 +1,5 @@ using Daybreak.Exceptions; +using Daybreak.Launch; using Daybreak.Models.Notifications.Handling; using Daybreak.Services.Notifications; using Daybreak.Utils; @@ -11,7 +12,6 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Threading.Tasks; -using System.Windows; namespace Daybreak.Services.ExceptionHandling; @@ -45,7 +45,7 @@ public bool HandleException(Exception e) if (e is FatalException fatalException) { this.logger.LogCritical(e, $"{nameof(FatalException)} encountered. Closing application"); - MessageBox.Show(fatalException.ToString()); + ExceptionDialog.ShowException(fatalException); File.WriteAllText("crash.log", e.ToString()); WriteCrashDump(); return false; @@ -58,7 +58,7 @@ public bool HandleException(Exception e) else if (e is TargetInvocationException targetInvocationException && e.InnerException is FatalException innerFatalException) { this.logger.LogCritical(e, $"{nameof(FatalException)} encountered. Closing application"); - MessageBox.Show(innerFatalException.ToString()); + ExceptionDialog.ShowException(e); File.WriteAllText("crash.log", e.ToString()); WriteCrashDump(); return false; @@ -92,7 +92,7 @@ public bool HandleException(Exception e) } this.logger.LogError(e, $"Unhandled exception caught {e.GetType()}"); - this.notificationService.NotifyError("Encountered exception", e.ToString()); + this.notificationService.NotifyError(e.GetType().Name, e.ToString()); return true; } diff --git a/Daybreak/Services/Logging/JsonLogsManager.cs b/Daybreak/Services/Logging/JsonLogsManager.cs index c66a512b..79b67073 100644 --- a/Daybreak/Services/Logging/JsonLogsManager.cs +++ b/Daybreak/Services/Logging/JsonLogsManager.cs @@ -1,6 +1,7 @@ using Daybreak.Configuration.Options; using Daybreak.Services.Database; using Daybreak.Services.Logging.Models; +using Daybreak.Utils; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -65,7 +66,7 @@ public async void WriteLog(Log log) Message = log.Exception is null ? log.Message : $"{log.Message}{Environment.NewLine}{log.Exception}", Category = log.Category, LogLevel = log.LogLevel, - LogTime = log.LogTime, + LogTime = log.LogTime.ToSafeDateTimeOffset(), CorrelationVector = log.CorrelationVector }; diff --git a/Daybreak/Services/Notifications/NotificationService.cs b/Daybreak/Services/Notifications/NotificationService.cs index 9a86ba52..73f01b88 100644 --- a/Daybreak/Services/Notifications/NotificationService.cs +++ b/Daybreak/Services/Notifications/NotificationService.cs @@ -1,6 +1,7 @@ using Daybreak.Models.Notifications; using Daybreak.Models.Notifications.Handling; using Daybreak.Services.Notifications.Models; +using Daybreak.Utils; using Microsoft.Extensions.Logging; using Slim; using System; @@ -8,7 +9,6 @@ using System.Collections.Generic; using System.Core.Extensions; using System.Extensions; -using System.Extensions.Core; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; @@ -80,27 +80,19 @@ public NotificationToken NotifyError(string title, void INotificationProducer.OpenNotification(Notification notification, bool storeNotification) { - var scopedLogger = this.logger.CreateScopedLogger(); if (storeNotification) { - try + this.storage.OpenNotification(new NotificationDTO { - this.storage.OpenNotification(new NotificationDTO - { - Title = notification.Title, - Description = notification.Description, - Id = notification.Id, - Level = (int)notification.Level, - MetaData = notification.Metadata, - HandlerType = notification.HandlingType?.AssemblyQualifiedName, - ExpirationTime = notification.ExpirationTime, - Closed = true - }); - } - catch(ArgumentOutOfRangeException exception) when (exception.Message.Contains("The UTC time represented when the offset is applied must be between year 0 and 10000")) - { - scopedLogger.LogError(exception, "Encountered DateTime to DateTimeOffset exception. Failed to store notification and will discard it. DateTime: {dateTime}", notification.ExpirationTime); - } + Title = notification.Title, + Description = notification.Description, + Id = notification.Id, + Level = (int)notification.Level, + MetaData = notification.Metadata, + HandlerType = notification.HandlingType?.AssemblyQualifiedName, + ExpirationTime = notification.ExpirationTime.ToSafeDateTimeOffset(), + Closed = true + }); } if (notification.HandlingType is null) @@ -213,8 +205,8 @@ private static NotificationDTO ToDTO(Notification notification) Level = (int)notification.Level, Title = notification.Title, Description = notification.Description, - ExpirationTime = notification.ExpirationTime, - CreationTime = notification.CreationTime, + ExpirationTime = notification.ExpirationTime.ToSafeDateTimeOffset(), + CreationTime = notification.CreationTime.ToSafeDateTimeOffset(), MetaData = notification.Metadata, Dismissible = notification.Dismissible, Closed = notification.Closed, diff --git a/Daybreak/Services/TradeChat/PriceHistoryDatabase.cs b/Daybreak/Services/TradeChat/PriceHistoryDatabase.cs index dde4b9e5..2414939f 100644 --- a/Daybreak/Services/TradeChat/PriceHistoryDatabase.cs +++ b/Daybreak/Services/TradeChat/PriceHistoryDatabase.cs @@ -1,6 +1,7 @@ using Daybreak.Models.Guildwars; using Daybreak.Services.Database; using Daybreak.Services.TradeChat.Models; +using Daybreak.Utils; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -48,10 +49,10 @@ public IEnumerable GetQuoteHistory(ItemBase item, DateTime? from { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetQuoteHistory), item.Id.ToString()); var fromO = fromTimestamp.HasValue ? - new DateTimeOffset(fromTimestamp.Value) : + fromTimestamp.Value.ToSafeDateTimeOffset() : DateTimeOffset.MinValue; var toO = toTimestamp.HasValue ? - new DateTimeOffset(toTimestamp.Value) : + toTimestamp.Value.ToSafeDateTimeOffset() : DateTimeOffset.MaxValue; scopedLogger.LogDebug($"Retrieving quotes for item {item.Id} with timestamp between [{fromO}] and [{toO}]"); var modifiersHash = item.Modifiers is not null ? this.itemHashService.ComputeHash(item) : default; @@ -69,10 +70,10 @@ public IEnumerable GetQuotesByTimestamp(TraderQuoteType type, Da { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetQuotesByTimestamp), string.Empty); var fromO = from.HasValue ? - new DateTimeOffset(from.Value) : + from.Value.ToSafeDateTimeOffset() : DateTimeOffset.MinValue; var toO = to.HasValue ? - new DateTimeOffset(to.Value) : + to.Value.ToSafeDateTimeOffset() : DateTimeOffset.Now; scopedLogger.LogDebug($"Retrieving all quotes by timestamp between [{fromO}] and [{toO}]"); var items = this.collection.FindAll(t => t.TimeStamp >= fromO && t.TimeStamp <= toO && t.TraderQuoteType == (int)type); @@ -84,10 +85,10 @@ public IEnumerable GetQuotesByInsertionTime(TraderQuoteType type { var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GetQuotesByInsertionTime), string.Empty); var fromO = from.HasValue ? - new DateTimeOffset(from.Value) : + from.Value.ToSafeDateTimeOffset() : DateTimeOffset.MinValue; var toO = to.HasValue ? - new DateTimeOffset(to.Value) : + to.Value.ToSafeDateTimeOffset() : DateTimeOffset.Now; scopedLogger.LogDebug($"Retrieving all quotes by insertion time between [{fromO}] and [{toO}]"); var items = this.collection.FindAll(t => t.InsertionTime >= fromO && t.InsertionTime <= toO && t.TraderQuoteType == (int)type); diff --git a/Daybreak/Services/TradeChat/TradeAlertingService.cs b/Daybreak/Services/TradeChat/TradeAlertingService.cs index 8d01d4cc..dc166b1c 100644 --- a/Daybreak/Services/TradeChat/TradeAlertingService.cs +++ b/Daybreak/Services/TradeChat/TradeAlertingService.cs @@ -4,6 +4,7 @@ using Daybreak.Services.Notifications; using Daybreak.Services.TradeChat.Models; using Daybreak.Services.TradeChat.Notifications; +using Daybreak.Utils; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; @@ -105,7 +106,7 @@ public void OnStartup() private async void StartAlertingService(CancellationToken cancellationToken) { var lastCheckTime = this.options.Value.LastCheckTime; - var timeSinceLastCheckTime = DateTimeOffset.UtcNow - lastCheckTime; + var timeSinceLastCheckTime = DateTimeOffset.UtcNow - lastCheckTime.ToSafeDateTimeOffset(); if (timeSinceLastCheckTime > this.options.Value.MaxLookbackPeriod) { timeSinceLastCheckTime = this.options.Value.MaxLookbackPeriod; @@ -146,7 +147,7 @@ private async Task CheckLiveTrades(ITradeChatService tradeChatService, Tra { var traderMessageDTO = new TraderMessageDTO { - Timestamp = message.Timestamp, + Timestamp = message.Timestamp.ToSafeDateTimeOffset(), Id = message.Timestamp.Ticks, Message = message.Message, Sender = message.Sender, @@ -274,7 +275,7 @@ private static bool CheckMatch(string toCheck, string toMatch, bool isRegex) } else { - return toCheck.ToLower().Contains(toMatch.ToLower()); + return toCheck.Contains(toMatch, StringComparison.CurrentCultureIgnoreCase); } } @@ -284,7 +285,7 @@ private static async Task> GetTraderMessages(IT var trades = await tradeChatService.GetLatestTrades(cancellationToken); var orderedTrades = trades.OrderBy(t => t.Timestamp).Select(t => new TraderMessageDTO { - Timestamp = t.Timestamp, + Timestamp = t.Timestamp.ToSafeDateTimeOffset(), Id = t.Timestamp.Ticks, Message = t.Message, Sender = t.Sender, @@ -308,7 +309,7 @@ private static async Task> GetTraderMessagesSince< var trades = await tradeChatService.GetLatestTrades(cancellationToken, since); var orderedTrades = trades.OrderBy(t => t.Timestamp).Select(t => new TraderMessageDTO { - Timestamp = t.Timestamp, + Timestamp = t.Timestamp.ToSafeDateTimeOffset(), Id = t.Timestamp.Ticks, Message = t.Message, Sender = t.Sender, diff --git a/Daybreak/Services/TradeChat/TraderQuoteService.cs b/Daybreak/Services/TradeChat/TraderQuoteService.cs index 77d3ee0c..5953a8ce 100644 --- a/Daybreak/Services/TradeChat/TraderQuoteService.cs +++ b/Daybreak/Services/TradeChat/TraderQuoteService.cs @@ -2,6 +2,7 @@ using Daybreak.Models.Guildwars; using Daybreak.Models.Trade; using Daybreak.Services.TradeChat.Models; +using Daybreak.Utils; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System; @@ -121,7 +122,7 @@ private async Task> GetSellQuotesInternal(CancellationT { var buyQuotes = await this.FetchBuyQuotesInternal(cancellationToken); var sellQuotes = await this.FetchSellQuotesInternal(cancellationToken); - var insertionTime = DateTime.UtcNow; + var insertionTime = DateTimeOffset.UtcNow; this.priceHistoryDatabase.AddTraderQuotes(buyQuotes .Select( quote => new TraderQuoteDTO @@ -130,7 +131,7 @@ private async Task> GetSellQuotesInternal(CancellationT ItemId = quote.Item?.Id ?? 0, ModifiersHash = quote.Item?.Modifiers is null ? string.Empty : this.itemHashService.ComputeHash(quote.Item), InsertionTime = insertionTime, - TimeStamp = quote.Timestamp ?? insertionTime, + TimeStamp = quote.Timestamp.ToSafeDateTimeOffset() ?? insertionTime, TraderQuoteType = (int)TraderQuoteType.Buy })); @@ -142,11 +143,11 @@ private async Task> GetSellQuotesInternal(CancellationT ItemId = quote.Item?.Id ?? 0, ModifiersHash = quote.Item?.Modifiers is null ? string.Empty : this.itemHashService.ComputeHash(quote.Item), InsertionTime = insertionTime, - TimeStamp = quote.Timestamp ?? insertionTime, + TimeStamp = quote.Timestamp.ToSafeDateTimeOffset() ?? insertionTime, TraderQuoteType = (int)TraderQuoteType.Sell })); - this.options.Value.LastCheckTime = insertionTime; + this.options.Value.LastCheckTime = insertionTime.UtcDateTime; this.options.UpdateOption(); return (buyQuotes, sellQuotes); } diff --git a/Daybreak/Utils/DateTimeExtensions.cs b/Daybreak/Utils/DateTimeExtensions.cs new file mode 100644 index 00000000..adbe7860 --- /dev/null +++ b/Daybreak/Utils/DateTimeExtensions.cs @@ -0,0 +1,32 @@ +using System; + +namespace Daybreak.Utils; + +public static class DateTimeExtensions +{ + /// + /// Converts to safely and clamps the values between and + /// + public static DateTimeOffset ToSafeDateTimeOffset(this DateTime dateTime) + { + var utcDateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + return utcDateTime.ToUniversalTime() <= DateTimeOffset.MinValue.UtcDateTime + ? DateTimeOffset.MinValue + : utcDateTime.ToUniversalTime() >= DateTimeOffset.MaxValue.UtcDateTime + ? DateTimeOffset.MaxValue + : new DateTimeOffset(utcDateTime); + } + + /// + /// Converts to safely and clamps the values between and + /// + public static DateTimeOffset? ToSafeDateTimeOffset(this DateTime? dateTime) + { + if (!dateTime.HasValue) + { + return default; + } + + return dateTime.Value.ToSafeDateTimeOffset(); + } +}