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..b79a95241 100644 --- a/Maple2.Server.Login/Session/LoginSession.cs +++ b/Maple2.Server.Login/Session/LoginSession.cs @@ -165,10 +165,16 @@ protected override void Dispose(bool disposing) { try { Server.OnDisconnected(this); - State = SessionState.Disconnected; - Complete(); - } finally { + } catch (Exception ex) { + Logger.Debug(ex, "Error during LoginSession.OnDisconnected"); + } + + State = SessionState.Disconnected; + + try { base.Dispose(disposing); + } catch (Exception ex) { + Logger.Debug(ex, "Error during LoginSession base disposal"); } } #endregion