fix(network): Handle exceptions during observer streams

This commit is contained in:
karashiiro 2023-02-15 18:07:39 -08:00
parent c59dd59858
commit ecf039a200

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -185,241 +184,260 @@ internal class NetworkHandlers : IDisposable, IServiceType
private IDisposable HandleMarketBoardItemRequest() private IDisposable HandleMarketBoardItemRequest()
{ {
return this.OnMarketBoardItemRequest() return this.OnMarketBoardItemRequest()
.Where(_ => this.configuration.IsMbCollect) .Where(this.ShouldUpload)
.Subscribe(request => .Subscribe(
{ request =>
this.marketBoardRequests.Add(request); {
Log.Verbose($"NEW MB REQUEST START: item#{request.CatalogId} amount#{request.AmountToArrive}"); this.marketBoardRequests.Add(request);
}); Log.Verbose($"NEW MB REQUEST START: item#{request.CatalogId} amount#{request.AmountToArrive}");
},
ex => Log.Error(ex, "Failed to handle Market Board item request event"));
} }
private IDisposable HandleMarketBoardOfferings() private IDisposable HandleMarketBoardOfferings()
{ {
return this.OnMarketBoardOfferings() return this.OnMarketBoardOfferings()
.Where(_ => this.configuration.IsMbCollect) .Where(this.ShouldUpload)
.Subscribe(listing => .Subscribe(
{ listing =>
var request =
this.marketBoardRequests.LastOrDefault(
r => r.CatalogId == listing.ItemListings[0].CatalogId && !r.IsDone);
if (request == default)
{ {
Log.Error( var request =
$"Market Board data arrived without a corresponding request: item#{listing.ItemListings[0].CatalogId}"); this.marketBoardRequests.LastOrDefault(
return; r => r.CatalogId == listing.ItemListings[0].CatalogId && !r.IsDone);
}
if (request.Listings.Count + listing.ItemListings.Count > request.AmountToArrive) if (request == default)
{ {
Log.Error( Log.Error(
$"Too many Market Board listings received for request: {request.Listings.Count + listing.ItemListings.Count} > {request.AmountToArrive} item#{listing.ItemListings[0].CatalogId}"); $"Market Board data arrived without a corresponding request: item#{listing.ItemListings[0].CatalogId}");
return; return;
} }
if (request.ListingsRequestId != -1 && request.ListingsRequestId != listing.RequestId) if (request.Listings.Count + listing.ItemListings.Count > request.AmountToArrive)
{ {
Log.Error( Log.Error(
$"Non-matching RequestIds for Market Board data request: {request.ListingsRequestId}, {listing.RequestId}"); $"Too many Market Board listings received for request: {request.Listings.Count + listing.ItemListings.Count} > {request.AmountToArrive} item#{listing.ItemListings[0].CatalogId}");
return; return;
} }
if (request.ListingsRequestId == -1 && request.Listings.Count > 0) if (request.ListingsRequestId != -1 && request.ListingsRequestId != listing.RequestId)
{ {
Log.Error( Log.Error(
$"Market Board data request sequence break: {request.ListingsRequestId}, {request.Listings.Count}"); $"Non-matching RequestIds for Market Board data request: {request.ListingsRequestId}, {listing.RequestId}");
return; return;
} }
if (request.ListingsRequestId == -1) if (request.ListingsRequestId == -1 && request.Listings.Count > 0)
{ {
request.ListingsRequestId = listing.RequestId; Log.Error(
Log.Verbose($"First Market Board packet in sequence: {listing.RequestId}"); $"Market Board data request sequence break: {request.ListingsRequestId}, {request.Listings.Count}");
} return;
}
request.Listings.AddRange(listing.ItemListings); if (request.ListingsRequestId == -1)
{
request.ListingsRequestId = listing.RequestId;
Log.Verbose($"First Market Board packet in sequence: {listing.RequestId}");
}
Log.Verbose( request.Listings.AddRange(listing.ItemListings);
"Added {0} ItemListings to request#{1}, now {2}/{3}, item#{4}",
listing.ItemListings.Count,
request.ListingsRequestId,
request.Listings.Count,
request.AmountToArrive,
request.CatalogId);
if (request.IsDone)
{
Log.Verbose( Log.Verbose(
"Market Board request finished, starting upload: request#{0} item#{1} amount#{2}", "Added {0} ItemListings to request#{1}, now {2}/{3}, item#{4}",
listing.ItemListings.Count,
request.ListingsRequestId, request.ListingsRequestId,
request.CatalogId, request.Listings.Count,
request.AmountToArrive); request.AmountToArrive,
request.CatalogId);
Task.Run(() => this.uploader.Upload(request)) if (request.IsDone)
.ContinueWith( {
task => Log.Error(task.Exception, "Market Board offerings data upload failed."), Log.Verbose(
TaskContinuationOptions.OnlyOnFaulted); "Market Board request finished, starting upload: request#{0} item#{1} amount#{2}",
} request.ListingsRequestId,
}); request.CatalogId,
request.AmountToArrive);
Task.Run(() => this.uploader.Upload(request))
.ContinueWith(
task => Log.Error(task.Exception, "Market Board offerings data upload failed"),
TaskContinuationOptions.OnlyOnFaulted);
}
},
ex => Log.Error(ex, "Failed to handle Market Board offerings data event"));
} }
private IDisposable HandleMarketBoardHistory() private IDisposable HandleMarketBoardHistory()
{ {
return this.OnMarketBoardHistory() return this.OnMarketBoardHistory()
.Where(_ => this.configuration.IsMbCollect) .Where(this.ShouldUpload)
.Subscribe(listing => .Subscribe(
{ listing =>
var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.CatalogId);
if (request == default)
{ {
Log.Error( var request = this.marketBoardRequests.LastOrDefault(r => r.CatalogId == listing.CatalogId);
$"Market Board data arrived without a corresponding request: item#{listing.CatalogId}");
return;
}
if (request.ListingsRequestId != -1) if (request == default)
{ {
Log.Error( Log.Error(
$"Market Board data history sequence break: {request.ListingsRequestId}, {request.Listings.Count}"); $"Market Board data arrived without a corresponding request: item#{listing.CatalogId}");
return; return;
} }
request.History.AddRange(listing.HistoryListings); if (request.ListingsRequestId != -1)
{
Log.Error(
$"Market Board data history sequence break: {request.ListingsRequestId}, {request.Listings.Count}");
return;
}
Log.Verbose("Added history for item#{0}", listing.CatalogId); request.History.AddRange(listing.HistoryListings);
if (request.AmountToArrive == 0) Log.Verbose("Added history for item#{0}", listing.CatalogId);
{
Log.Verbose("Request had 0 amount, uploading now");
Task.Run(() => this.uploader.Upload(request)) if (request.AmountToArrive == 0)
.ContinueWith( {
(task) => Log.Error(task.Exception, "Market Board history data upload failed."), Log.Verbose("Request had 0 amount, uploading now");
TaskContinuationOptions.OnlyOnFaulted);
} Task.Run(() => this.uploader.Upload(request))
}); .ContinueWith(
(task) => Log.Error(task.Exception, "Market Board history data upload failed"),
TaskContinuationOptions.OnlyOnFaulted);
}
},
ex => Log.Error(ex, "Failed to handle Market Board history data event"));
} }
private IDisposable HandleMarketTaxRates() private IDisposable HandleMarketTaxRates()
{ {
return this.OnMarketTaxRates() return this.OnMarketTaxRates()
.Where(_ => this.configuration.IsMbCollect) .Where(this.ShouldUpload)
.Subscribe(taxes => .Subscribe(
{ taxes =>
Log.Verbose( {
"MarketTaxRates: limsa#{0} grid#{1} uldah#{2} ish#{3} kugane#{4} cr#{5} sh#{6}", Log.Verbose(
taxes.LimsaLominsaTax, "MarketTaxRates: limsa#{0} grid#{1} uldah#{2} ish#{3} kugane#{4} cr#{5} sh#{6}",
taxes.GridaniaTax, taxes.LimsaLominsaTax,
taxes.UldahTax, taxes.GridaniaTax,
taxes.IshgardTax, taxes.UldahTax,
taxes.KuganeTax, taxes.IshgardTax,
taxes.CrystariumTax, taxes.KuganeTax,
taxes.SharlayanTax); taxes.CrystariumTax,
taxes.SharlayanTax);
Task.Run(() => this.uploader.UploadTax(taxes)) Task.Run(() => this.uploader.UploadTax(taxes))
.ContinueWith( .ContinueWith(
task => Log.Error(task.Exception, "Market Board tax data upload failed."), task => Log.Error(task.Exception, "Market Board tax data upload failed"),
TaskContinuationOptions.OnlyOnFaulted); TaskContinuationOptions.OnlyOnFaulted);
}); },
ex => Log.Error(ex, "Failed to handle Market Board tax data event"));
} }
private IDisposable HandleMarketBoardPurchaseHandler() private IDisposable HandleMarketBoardPurchaseHandler()
{ {
return this.OnMarketBoardPurchaseHandler() return this.OnMarketBoardPurchaseHandler()
.Where(_ => this.configuration.IsMbCollect) .Where(_ => this.configuration.IsMbCollect)
.Subscribe(handler => { this.marketBoardPurchaseHandler = handler; }); .Subscribe(
handler => { this.marketBoardPurchaseHandler = handler; },
ex => Log.Error(ex, "Failed to handle Market Board purchase handler event"));
} }
private IDisposable HandleMarketBoardPurchase() private IDisposable HandleMarketBoardPurchase()
{ {
return this.OnMarketBoardPurchase() return this.OnMarketBoardPurchase()
.Where(_ => this.configuration.IsMbCollect) .Where(_ => this.configuration.IsMbCollect)
.Subscribe(purchase => .Subscribe(
{ purchase =>
if (this.marketBoardPurchaseHandler == null)
return;
var sameQty = purchase.ItemQuantity == this.marketBoardPurchaseHandler.ItemQuantity;
var itemMatch = purchase.CatalogId == this.marketBoardPurchaseHandler.CatalogId;
var itemMatchHq = purchase.CatalogId == this.marketBoardPurchaseHandler.CatalogId + 1_000_000;
// Transaction succeeded
if (sameQty && (itemMatch || itemMatchHq))
{ {
Log.Verbose( if (this.marketBoardPurchaseHandler == null)
$"Bought {purchase.ItemQuantity}x {this.marketBoardPurchaseHandler.CatalogId} for {this.marketBoardPurchaseHandler.PricePerUnit * purchase.ItemQuantity} gils, listing id is {this.marketBoardPurchaseHandler.ListingId}"); return;
var handler = var sameQty = purchase.ItemQuantity == this.marketBoardPurchaseHandler.ItemQuantity;
this.marketBoardPurchaseHandler; // Capture the object so that we don't pass in a null one when the task starts. var itemMatch = purchase.CatalogId == this.marketBoardPurchaseHandler.CatalogId;
var itemMatchHq = purchase.CatalogId == this.marketBoardPurchaseHandler.CatalogId + 1_000_000;
Task.Run(() => this.uploader.UploadPurchase(handler)) // Transaction succeeded
.ContinueWith( if (sameQty && (itemMatch || itemMatchHq))
task => Log.Error(task.Exception, "Market Board purchase data upload failed."), {
TaskContinuationOptions.OnlyOnFaulted); Log.Verbose(
} $"Bought {purchase.ItemQuantity}x {this.marketBoardPurchaseHandler.CatalogId} for {this.marketBoardPurchaseHandler.PricePerUnit * purchase.ItemQuantity} gils, listing id is {this.marketBoardPurchaseHandler.ListingId}");
this.marketBoardPurchaseHandler = null; var handler =
}); this.marketBoardPurchaseHandler; // Capture the object so that we don't pass in a null one when the task starts.
Task.Run(() => this.uploader.UploadPurchase(handler))
.ContinueWith(
task => Log.Error(task.Exception, "Market Board purchase data upload failed."),
TaskContinuationOptions.OnlyOnFaulted);
}
this.marketBoardPurchaseHandler = null;
},
ex => Log.Error(ex, "Failed to handle Market Board purchase event"));
} }
private unsafe IDisposable HandleCfPop() private unsafe IDisposable HandleCfPop()
{ {
return this.OnCfNotifyPop() return this.OnCfNotifyPop()
.Subscribe(message => .Subscribe(
{ message =>
using var stream = new UnmanagedMemoryStream((byte*)message.Data.ToPointer(), 64);
using var reader = new BinaryReader(stream);
var notifyType = reader.ReadByte();
stream.Position += 0x1B;
var conditionId = reader.ReadUInt16();
if (notifyType != 3)
return;
var cfConditionSheet = message.DataManager!.GetExcelSheet<ContentFinderCondition>()!;
var cfCondition = cfConditionSheet.GetRow(conditionId);
if (cfCondition == null)
{ {
Log.Error($"CFC key {conditionId} not in Lumina data."); using var stream = new UnmanagedMemoryStream((byte*)message.Data.ToPointer(), 64);
return; using var reader = new BinaryReader(stream);
}
var cfcName = cfCondition.Name.ToString(); var notifyType = reader.ReadByte();
if (cfcName.IsNullOrEmpty()) stream.Position += 0x1B;
{ var conditionId = reader.ReadUInt16();
cfcName = "Duty Roulette";
cfCondition.Image = 112324;
}
// Flash window if (notifyType != 3)
if (this.configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated()) return;
{
var flashInfo = new NativeFunctions.FlashWindowInfo var cfConditionSheet = message.DataManager!.GetExcelSheet<ContentFinderCondition>()!;
var cfCondition = cfConditionSheet.GetRow(conditionId);
if (cfCondition == null)
{ {
Size = (uint)Marshal.SizeOf<NativeFunctions.FlashWindowInfo>(), Log.Error("CFC key {ConditionId} not in Lumina data", conditionId);
Count = uint.MaxValue, return;
Timeout = 0,
Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
Hwnd = Process.GetCurrentProcess().MainWindowHandle,
};
NativeFunctions.FlashWindowEx(ref flashInfo);
}
Task.Run(() =>
{
if (this.configuration.DutyFinderChatMessage)
{
Service<ChatGui>.GetNullable()?.Print($"Duty pop: {cfcName}");
} }
this.CfPop.InvokeSafely(this, cfCondition); var cfcName = cfCondition.Name.ToString();
}).ContinueWith( if (cfcName.IsNullOrEmpty())
task => Log.Error(task.Exception, "CfPop.Invoke failed."), {
TaskContinuationOptions.OnlyOnFaulted); cfcName = "Duty Roulette";
}); cfCondition.Image = 112324;
}
// Flash window
if (this.configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated())
{
var flashInfo = new NativeFunctions.FlashWindowInfo
{
Size = (uint)Marshal.SizeOf<NativeFunctions.FlashWindowInfo>(),
Count = uint.MaxValue,
Timeout = 0,
Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG,
Hwnd = Process.GetCurrentProcess().MainWindowHandle,
};
NativeFunctions.FlashWindowEx(ref flashInfo);
}
Task.Run(() =>
{
if (this.configuration.DutyFinderChatMessage)
{
Service<ChatGui>.GetNullable()?.Print($"Duty pop: {cfcName}");
}
this.CfPop.InvokeSafely(this, cfCondition);
}).ContinueWith(
task => Log.Error(task.Exception, "CfPop.Invoke failed"),
TaskContinuationOptions.OnlyOnFaulted);
},
ex => Log.Error(ex, "Failed to handle Market Board purchase event"));
}
private bool ShouldUpload<T>(T any)
{
return this.configuration.IsMbCollect;
} }
private class NetworkMessage private class NetworkMessage