From bd21f87da0bcdbd5269eacdba18058abcb20de88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 7 Feb 2026 15:48:11 -0300 Subject: [PATCH 1/3] Harden Session I/O and migration save flow Improve robustness around network IO, disposal, migration and save behavior across sessions: - Core.Network.Session.cs: Add defensive checks and richer exception handling in the receive pipe loop (handle expected socket errors 995/10004, guard against disposed writer/advance races, suppress exceptions during disposal) and catch IO/Socket exceptions in SendRaw to log and disconnect cleanly. - Game/PacketHandlers/QuitHandler.cs: Add logging for MigrateOut RPC failures and annotate migration behavior to avoid prematurely dropping the migration packet (adjusted exception handling around migration send). - Game/Session/GameSession.cs: Change leave save to Async=false, refactor migration save flow by extracting SavePlayerState(), set preMigrationSaved earlier, and adjust error logging. Removed a local TrySaveComponent helper. - Login/Session/LoginSession.cs: Reorder disposal to call base.Dispose first to stop network operations, add Serilog debug logging for disposal errors. These changes are intended to make session teardown and migration more reliable and to avoid races/ignored errors when clients or sockets close during operations. --- Maple2.Server.Core/Network/Session.cs | 41 ++++++++++++++++--- .../PacketHandlers/QuitHandler.cs | 10 ++++- Maple2.Server.Game/Session/GameSession.cs | 14 +++---- Maple2.Server.Login/Session/LoginSession.cs | 7 +++- 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/Maple2.Server.Core/Network/Session.cs b/Maple2.Server.Core/Network/Session.cs index 81dd86ae6..31fdac0a0 100644 --- a/Maple2.Server.Core/Network/Session.cs +++ b/Maple2.Server.Core/Network/Session.cs @@ -206,18 +206,45 @@ private void PerformHandshake() { private async Task WriteRecvPipe(Socket socket, PipeWriter writer) { try { - FlushResult result; + FlushResult result = default; do { + if (disposed) break; + Memory memory = writer.GetMemory(); - int bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None); - if (bytesRead <= 0) { + int bytesRead; + + try { + bytesRead = await socket.ReceiveAsync(memory, SocketFlags.None); + } catch (SocketException sockEx) when (sockEx.ErrorCode == 995 || sockEx.ErrorCode == 10004) { + // 995: Operation aborted (thread exit/app request) + // 10004: Interrupted system call + // These are expected when closing the session + Logger.Debug("Socket closed during receive (code {ErrorCode}) account={AccountId} char={CharacterId}", + sockEx.ErrorCode, AccountId, CharacterId); break; } - writer.Advance(bytesRead); + if (bytesRead <= 0 || disposed) { + break; + } - result = await writer.FlushAsync(); + // Check if writer was completed before advancing + try { + writer.Advance(bytesRead); + result = await writer.FlushAsync(); + } catch (InvalidOperationException) when (disposed) { + // Writer was completed/disposed during advance or flush + Logger.Debug("Pipe writer completed during operation account={AccountId} char={CharacterId}", AccountId, CharacterId); + break; + } catch (ArgumentOutOfRangeException) when (disposed) { + // Invalid byte count during disposal + Logger.Debug("Pipe writer advance failed during disposal account={AccountId} char={CharacterId}", AccountId, CharacterId); + break; + } } while (!disposed && !result.IsCompleted); + } catch (Exception ex) when (disposed) { + // Suppress exceptions if we're already disposed + Logger.Debug(ex, "WriteRecvPipe exception during disposal account={AccountId} char={CharacterId}", AccountId, CharacterId); } catch (Exception ex) { Logger.Debug(ex, "WriteRecvPipe exception account={AccountId} char={CharacterId}", AccountId, CharacterId); Disconnect(); @@ -319,6 +346,10 @@ private void SendRaw(ByteWriter packet) { if (writeTask.IsFaulted) { throw writeTask.Exception?.GetBaseException() ?? new Exception("Write task faulted"); } + } catch (Exception ex) when (ex.InnerException is IOException or SocketException || ex is IOException or SocketException) { + // Expected when client closes the connection (e.g., during migration) + Logger.Debug("SendRaw connection closed account={AccountId} char={CharacterId}", AccountId, CharacterId); + Disconnect(); } catch (Exception ex) { Logger.Warning(ex, "[LIFECYCLE] SendRaw write failed account={AccountId} char={CharacterId}", AccountId, CharacterId); Disconnect(); diff --git a/Maple2.Server.Game/PacketHandlers/QuitHandler.cs b/Maple2.Server.Game/PacketHandlers/QuitHandler.cs index b1aed5db0..e17150ea8 100644 --- a/Maple2.Server.Game/PacketHandlers/QuitHandler.cs +++ b/Maple2.Server.Game/PacketHandlers/QuitHandler.cs @@ -46,9 +46,15 @@ public override void Handle(GameSession session, IByteReader packet) { MigrateOutResponse response = World.MigrateOut(request); var endpoint = new IPEndPoint(IPAddress.Parse(response.IpAddress), response.Port); session.Send(MigrationPacket.GameToLogin(endpoint, response.Token)); - } catch (RpcException) { + // Do NOT disconnect here — let the client close the TCP connection after + // receiving the migration packet. Calling Disconnect() immediately would + // set disconnecting=1, causing SendWorker to drop the queued packet. + // The natural TCP close will trigger the full Dispose chain (leave field, + // update PlayerInfo, save state, etc.). + } catch (RpcException ex) { + Logger.Error(ex, "MigrateOut failed for account={AccountId} char={CharacterId}", + session.AccountId, session.CharacterId); session.Send(MigrationPacket.GameToLoginError(s_move_err_default)); - } finally { session.Disconnect(); } } diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index f4c80ef88..1935a24a6 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -757,7 +757,7 @@ protected override void Dispose(bool disposing) { LastOnlineTime = DateTime.UtcNow.ToEpochSeconds(), MapId = 0, Channel = -1, - Async = true, + Async = false, }); Party.CheckDisband(); @@ -795,10 +795,6 @@ protected override void Dispose(bool disposing) { } return; - void TrySaveComponent(GameStorage.Request db, Action action) { - try { action(db); } catch (Exception ex) { Logger.Error(ex, "Error saving component for {Player}", PlayerName); } - } - void SafeDispose(IDisposable? disp) { if (disp == null) return; try { disp.Dispose(); } catch (Exception ex) { Logger.Error(ex, "Error disposing component for {Player}", PlayerName); } @@ -853,6 +849,11 @@ void SaveCacheConfig() { public void MigrationSave() { if (preMigrationSaved) return; + preMigrationSaved = true; + SavePlayerState(); + } + + private void SavePlayerState() { try { AcquireLock(AccountId, 5); using GameStorage.Request db = GameStorage.Context(); @@ -870,9 +871,8 @@ public void MigrationSave() { TrySaveComponent(db, Dungeon.Save); db.Commit(); db.SaveChanges(); - preMigrationSaved = true; } catch (Exception ex) { - Logger.Error(ex, "MigrationSave failed AccountId={AccountId} CharacterId={CharacterId}", AccountId, CharacterId); + Logger.Error(ex, "SavePlayerState failed AccountId={AccountId} CharacterId={CharacterId}", AccountId, CharacterId); } finally { ReleaseLock(AccountId); } diff --git a/Maple2.Server.Login/Session/LoginSession.cs b/Maple2.Server.Login/Session/LoginSession.cs index 341ba9744..41c2af24c 100644 --- a/Maple2.Server.Login/Session/LoginSession.cs +++ b/Maple2.Server.Login/Session/LoginSession.cs @@ -12,6 +12,7 @@ using Maple2.Server.Core.Network; using Maple2.Server.Core.Packets; using Maple2.Server.World.Service; +using Serilog; using static Maple2.Model.Error.CharacterCreateError; using WorldClient = Maple2.Server.World.Service.World.WorldClient; @@ -166,9 +167,11 @@ protected override void Dispose(bool disposing) { try { Server.OnDisconnected(this); State = SessionState.Disconnected; - Complete(); - } finally { + + // Call base.Dispose to stop network operations first base.Dispose(disposing); + } catch (Exception ex) { + Log.Logger.Debug(ex, "Error during LoginSession disposal"); } } #endregion From cd61c545600555f1ee9e4df4d03a24fca87b1930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 7 Feb 2026 15:55:34 -0300 Subject: [PATCH 2/3] Update LoginSession.cs --- Maple2.Server.Login/Session/LoginSession.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Maple2.Server.Login/Session/LoginSession.cs b/Maple2.Server.Login/Session/LoginSession.cs index 41c2af24c..e4fb55f3a 100644 --- a/Maple2.Server.Login/Session/LoginSession.cs +++ b/Maple2.Server.Login/Session/LoginSession.cs @@ -166,12 +166,16 @@ protected override void Dispose(bool disposing) { try { Server.OnDisconnected(this); - State = SessionState.Disconnected; + } catch (Exception ex) { + Logger.Debug(ex, "Error during LoginSession.OnDisconnected"); + } - // Call base.Dispose to stop network operations first + State = SessionState.Disconnected; + + try { base.Dispose(disposing); } catch (Exception ex) { - Log.Logger.Debug(ex, "Error during LoginSession disposal"); + Logger.Debug(ex, "Error during LoginSession base disposal"); } } #endregion From 6066e2eee9c494f724e3e3a8d33f61643411af52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sat, 7 Feb 2026 15:55:44 -0300 Subject: [PATCH 3/3] Update LoginSession.cs --- Maple2.Server.Login/Session/LoginSession.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Maple2.Server.Login/Session/LoginSession.cs b/Maple2.Server.Login/Session/LoginSession.cs index e4fb55f3a..b79a95241 100644 --- a/Maple2.Server.Login/Session/LoginSession.cs +++ b/Maple2.Server.Login/Session/LoginSession.cs @@ -12,7 +12,6 @@ using Maple2.Server.Core.Network; using Maple2.Server.Core.Packets; using Maple2.Server.World.Service; -using Serilog; using static Maple2.Model.Error.CharacterCreateError; using WorldClient = Maple2.Server.World.Service.World.WorldClient;