Bug fixes and stability improvements:

- Fix crash on improper websocket disconnect
- Fix some race conditions by moving to synchronized Queue and List
- Fix allowing invalid usernames
This commit is contained in:
Elijah R 2024-01-03 19:07:47 -05:00
parent 563883a275
commit 48e2ece306
4 changed files with 41 additions and 19 deletions

View file

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Timers; using System.Timers;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
@ -9,7 +10,7 @@ public class TurnQueue
public event EventHandler<TurnStatus> Turn; public event EventHandler<TurnStatus> Turn;
// Private fields // Private fields
private readonly Queue<User> queue; private readonly ConcurrentQueue<User> queue;
private readonly uint turnTime; private readonly uint turnTime;
private readonly Timer timer; private readonly Timer timer;
private uint currentTurnRemainingTime; private uint currentTurnRemainingTime;
@ -32,7 +33,8 @@ public class TurnQueue
if (indefiniteTurn != null) return; if (indefiniteTurn != null) return;
if (queue.Count == 0) return; // This shouldn't happen, but just in case if (queue.Count == 0) return; // This shouldn't happen, but just in case
currentTurnRemainingTime--; currentTurnRemainingTime--;
Utilities.Log(LogLevel.DEBUG, $"Turn tick, {currentTurnRemainingTime} seconds remaining on {queue.Peek().Username}'s turn"); if (!queue.TryPeek(out var u)) return;
Utilities.Log(LogLevel.DEBUG, $"Turn tick, {currentTurnRemainingTime} seconds remaining on {u.Username}'s turn");
if (currentTurnRemainingTime == 0) if (currentTurnRemainingTime == 0)
{ {
NextTurn(); NextTurn();
@ -48,10 +50,11 @@ public class TurnQueue
SendTurnUpdate(); SendTurnUpdate();
return; return;
} }
queue.Dequeue(); queue.TryDequeue(out _);
if (queue.Count > 0) if (queue.Count > 0)
{ {
Utilities.Log(LogLevel.DEBUG, $"It is now {queue.Peek().Username}'s turn"); if (!queue.TryPeek(out var u)) return;
Utilities.Log(LogLevel.DEBUG, $"It is now {u.Username}'s turn");
currentTurnRemainingTime = turnTime; currentTurnRemainingTime = turnTime;
} }
SendTurnUpdate(); SendTurnUpdate();
@ -100,7 +103,8 @@ public class TurnQueue
} }
if (!queue.Contains(user)) return; if (!queue.Contains(user)) return;
Utilities.Log(LogLevel.DEBUG, $"Removing user {user.Username} to turn queue"); Utilities.Log(LogLevel.DEBUG, $"Removing user {user.Username} to turn queue");
if (queue.Peek() == user && indefiniteTurn == null) if (!queue.TryPeek(out var _u)) return;
if (_u == user && indefiniteTurn == null)
{ {
NextTurn(); NextTurn();
return; return;
@ -128,7 +132,8 @@ public class TurnQueue
{ {
if (indefiniteTurn != null) return indefiniteTurn; if (indefiniteTurn != null) return indefiniteTurn;
if (queue.Count == 0) return null; if (queue.Count == 0) return null;
return queue.Peek(); if (!queue.TryPeek(out var u)) return null;
return u;
} }
public void GiveTurn(User user) public void GiveTurn(User user)
@ -145,7 +150,8 @@ public class TurnQueue
AddUser(user); AddUser(user);
return; return;
} }
if (queue.Peek() == user) return; if (!queue.TryPeek(out var _u)) return;
if (_u == user) return;
RemoveUser(user); RemoveUser(user);
var _queue = queue.ToArray(); var _queue = queue.ToArray();
queue.Clear(); queue.Clear();

View file

@ -79,8 +79,12 @@ public class User
{ {
return; return;
} }
try
{
await socket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(msg)), WebSocketMessageType.Text, true, await socket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(msg)), WebSocketMessageType.Text, true,
CancellationToken.None); CancellationToken.None);
} catch { /* ignored */ }
} }
private async Task ProcessMessage(string[] msgArr) private async Task ProcessMessage(string[] msgArr)
@ -343,7 +347,7 @@ public class User
{ {
// Ban // Ban
if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Ban) || msgArr.Length < 3 || vm == null) return; if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Ban) || msgArr.Length < 3 || vm == null) return;
var user = vm.Users.Find(u => u.Username == msgArr[2]); var user = vm.Users.First(u => u.Username == msgArr[2]);
if (user == null) return; if (user == null) return;
await user.Ban(msgArr.Length == 4 ? msgArr[3] : null); await user.Ban(msgArr.Length == 4 ? msgArr[3] : null);
} }
@ -366,7 +370,7 @@ public class User
{ {
// Mute // Mute
if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Mute) || msgArr.Length != 4 || vm == null) return; if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Mute) || msgArr.Length != 4 || vm == null) return;
var user = vm.Users.Find(u => u.Username == msgArr[2]); var user = vm.Users.First(u => u.Username == msgArr[2]);
if (user == null) return; if (user == null) return;
bool? permanent = msgArr[3] == "1" ? true : msgArr[3] == "0" ? false : null; bool? permanent = msgArr[3] == "1" ? true : msgArr[3] == "0" ? false : null;
if (permanent == null) return; if (permanent == null) return;
@ -377,7 +381,7 @@ public class User
{ {
// Kick // Kick
if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Kick) || msgArr.Length != 3 || vm == null) return; if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Kick) || msgArr.Length != 3 || vm == null) return;
var user = vm.Users.Find(u => u.Username == msgArr[2]); var user = vm.Users.First(u => u.Username == msgArr[2]);
if (user == null) return; if (user == null) return;
await user.Close(); await user.Close();
} }
@ -386,7 +390,7 @@ public class User
{ {
// End turn // End turn
if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.BypassTurn) || msgArr.Length != 3 || vm == null) return; if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.BypassTurn) || msgArr.Length != 3 || vm == null) return;
var user = vm.Users.Find(u => u.Username == msgArr[2]); var user = vm.Users.First(u => u.Username == msgArr[2]);
if (user == null) return; if (user == null) return;
vm.TurnQueue.RemoveUser(user); vm.TurnQueue.RemoveUser(user);
} }
@ -404,7 +408,7 @@ public class User
{ {
// Rename user // Rename user
if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Rename) || msgArr.Length != 4 || vm == null) return; if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.Rename) || msgArr.Length != 4 || vm == null) return;
var user = vm.Users.Find(u => u.Username == msgArr[2]); var user = vm.Users.First(u => u.Username == msgArr[2]);
if (user == null) return; if (user == null) return;
await user.Rename(msgArr[3]); await user.Rename(msgArr[3]);
} }
@ -413,7 +417,7 @@ public class User
{ {
// Get IP // Get IP
if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.GrabIP) || msgArr.Length != 3 || vm == null) return; if (_rank != Rank.Admin && (_rank != Rank.Moderator || !Program.Config.ModPermissions.GrabIP) || msgArr.Length != 3 || vm == null) return;
var user = vm.Users.Find(u => u.Username == msgArr[2]); var user = vm.Users.First(u => u.Username == msgArr[2]);
if (user == null) return; if (user == null) return;
await SendAsync(Guacutils.Encode("admin", "19", msgArr[2], user.IP.ToString())); await SendAsync(Guacutils.Encode("admin", "19", msgArr[2], user.IP.ToString()));
} }
@ -496,8 +500,16 @@ public class User
{ {
using MemoryStream ms = new MemoryStream(); using MemoryStream ms = new MemoryStream();
do do
{
try
{ {
result = await socket.ReceiveAsync(receivebuffer, token); result = await socket.ReceiveAsync(receivebuffer, token);
}
catch (WebSocketException ex)
{
await Close(false);
return;
}
if (result.MessageType == WebSocketMessageType.Close) if (result.MessageType == WebSocketMessageType.Close)
{ {
Disconnected.Invoke(this, new EventArgs()); Disconnected.Invoke(this, new EventArgs());
@ -546,9 +558,9 @@ public class User
{ {
await SendAsync(Guacutils.Encode("size", "0", size.Width.ToString(), size.Height.ToString())); await SendAsync(Guacutils.Encode("size", "0", size.Width.ToString(), size.Height.ToString()));
} }
public async Task Close() public async Task Close(bool sendDisconnect = true)
{ {
await SendAsync(Guacutils.Encode("disconnect")); if (sendDisconnect) SendAsync(Guacutils.Encode("disconnect"));
try try
{ {
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None);
@ -608,6 +620,7 @@ public class User
return; return;
} }
await this.SendAsync(Guacutils.Encode("rename", "0", "2", _username, ((int)this._rank).ToString())); await this.SendAsync(Guacutils.Encode("rename", "0", "2", _username, ((int)this._rank).ToString()));
return;
} }
this._username = newname; this._username = newname;
if (oldname != null) this.Renamed.Invoke(this, oldname); if (oldname != null) this.Renamed.Invoke(this, oldname);

View file

@ -1,3 +1,5 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Net; using System.Net;
using CollabVM.Server.Config; using CollabVM.Server.Config;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
@ -14,7 +16,7 @@ public class VM
// Public properties and events // Public properties and events
public VMController Controller { get; } public VMController Controller { get; }
public VMConfig Config { get; } public VMConfig Config { get; }
public List<User> Users { get; } = new(); public SynchronizedCollection<User> Users { get; } = new();
public CircularBuffer.CircularBuffer<ChatMessage> ChatHistory { get; } = new((int)Program.Config.Chat.ChatHistoryLength); public CircularBuffer.CircularBuffer<ChatMessage> ChatHistory { get; } = new((int)Program.Config.Chat.ChatHistoryLength);
public TurnQueue TurnQueue { get; } = new(Program.Config.Turns.TurnTime); public TurnQueue TurnQueue { get; } = new(Program.Config.Turns.TurnTime);
public ResetVote? Vote { get; private set; } public ResetVote? Vote { get; private set; }

View file

@ -15,6 +15,7 @@
<PackageReference Include="Samboy063.Tomlet" Version="5.2.0" /> <PackageReference Include="Samboy063.Tomlet" Version="5.2.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.1" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.ServiceModel.Primitives" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>