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();
+ }
+}