R-AM: snapshot handler and dispatch backstop answer/log untyped faults
HandleSnapshotRequestV1Async only caught the three typed selection exceptions, unlike its siblings HandleShardLogRequestAsync/HandlePutAsync, which catch Exception and answer a CultNetErrorMessage. An untyped fault (e.g. a null Selection) escaped into the RUDP dispatch backstop, which swallowed it with no log - the same backstop that hid SM-6's ArgumentNullException regression for a whole batch. - HandleSnapshotRequestV1Async now catches Exception like its siblings and answers a CultNetErrorMessage. - RudpCultNetSchemaServer gets a Logger (ILogger/NullLogger, same pattern as Server/Client), and DispatchAsync's backstop always logs the fault it swallows, unwrapping DynamicInvoke's TargetInvocationException wrapper so the logged message is the handler's real exception, not the generic invocation-wrapper text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
375b6177ae
commit
77cd9649ec
2 changed files with 34 additions and 5 deletions
|
|
@ -263,6 +263,16 @@ namespace GameCult.Networking
|
|||
{
|
||||
peer.SendCultNet(CultNetErrorMessage.ForReferenceOutsideTarget(ex));
|
||||
}
|
||||
// R-AM: an untyped fault answers like HandleShardLogRequestAsync/HandlePutAsync's own
|
||||
// catch (Exception) - a CultNetErrorMessage on the wire, not silence. Before this, only
|
||||
// the three typed selection exceptions were caught here; anything else propagated up into
|
||||
// the dispatch backstop (CultNetRudpSchemaServer.cs), which swallowed it with no log and
|
||||
// hid SM-6's ArgumentNullException regression for a whole batch.
|
||||
catch (Exception ex)
|
||||
{
|
||||
_server.Logger.LogError($"CultNet snapshot v1 request failed: {ex.Message}");
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using GameCult.Logging;
|
||||
|
||||
namespace GameCult.Networking
|
||||
{
|
||||
|
|
@ -111,6 +112,17 @@ namespace GameCult.Networking
|
|||
private readonly ConcurrentDictionary<Type, Delegate> _handlers = new();
|
||||
private readonly ConcurrentDictionary<Delegate, Delegate> _schemaPeerAdapters = new();
|
||||
private bool _disposed;
|
||||
private ILogger _logger = new NullLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logger used by the server. R-AM: the dispatch backstop in DispatchAsync
|
||||
/// logs every untyped fault through this instead of swallowing it silently.
|
||||
/// </summary>
|
||||
public ILogger Logger
|
||||
{
|
||||
get => _logger;
|
||||
set => _logger = value ?? new NullLogger();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a schema-v0 RUDP server.
|
||||
|
|
@ -283,11 +295,13 @@ namespace GameCult.Networking
|
|||
|
||||
// R-AG: a handler is expected to catch its own typed refusals (HandleSnapshotRequestV1Async
|
||||
// catches the three selection exceptions and answers cursor_invalid/selection_invalid/
|
||||
// reference_outside_target on the wire) - this catch is the backstop for whatever a
|
||||
// reference_outside_target on the wire, R-AM) - this catch is the backstop for whatever a
|
||||
// handler does not, so an untyped exception from a malformed/hostile message can never
|
||||
// reach the poll loop and stall the server for every other peer. It answers nothing on the
|
||||
// wire (an exception this generic carries no typed refusal to report) and simply drops the
|
||||
// one malformed dispatch, exactly as an unregistered handler already does just above.
|
||||
// reach the poll loop and stall the server for every other peer. It still answers nothing
|
||||
// on the wire (an exception this generic carries no typed refusal to report) and drops the
|
||||
// one malformed dispatch, exactly as an unregistered handler already does just above -
|
||||
// R-AM: but it always logs. A backstop that swallows silently is a compensator, and an
|
||||
// untested, unlogged one hid the SM-6 ArgumentNullException regression for a whole batch.
|
||||
try
|
||||
{
|
||||
var result = handler.DynamicInvoke(message, new RudpCultNetSchemaServerPeer(this, delivered.Peer));
|
||||
|
|
@ -296,8 +310,13 @@ namespace GameCult.Networking
|
|||
await task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
// DynamicInvoke wraps a synchronously-thrown handler fault in TargetInvocationException,
|
||||
// whose own .Message is the generic "Exception has been thrown by the target of an
|
||||
// invocation." - log the real fault, not that wrapper.
|
||||
var reported = ex is System.Reflection.TargetInvocationException { InnerException: { } inner } ? inner : ex;
|
||||
Logger.LogError($"CultNet RUDP schema dispatch failed for {message.GetType().Name}: {reported.Message}");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
|
|
|||
Loading…
Reference in a new issue