Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,9 @@ dotnet_diagnostic.Moq1400.severity = none # Choose a mock behaviour (strict, loo
dotnet_diagnostic.Moq1410.severity = none # Set behaviour to Strict

dotnet_diagnostic.S2094.severity = none # Empty classes — intentional discriminated-union marker records (Marker/Note/EventArg hierarchies)
dotnet_diagnostic.S3776.severity = warning # Cognitive complexity — fails locally on what SonarQube flags as Critical instead of only on the server

dotnet_diagnostic.VSTHRD202.severity = warning # Return the Task directly when async/await adds nothing — SonarQube imports this INFO diagnostic as a code smell

dotnet_diagnostic.T0003.severity = none # Do not use tuples
dotnet_diagnostic.T0006.severity = none # Do not use Helper in type name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,10 @@ await store.SetMessageIdAsync(guildId, serverId, entityId, mid, cancellationToke
public bool TryDismiss(ulong guildId, Guid serverId, ulong entityId) =>
_pending.TryRemove((guildId, serverId, entityId), out _);

private static async Task<string> GetCultureAsync(IServiceProvider provider, ulong guildId, CancellationToken ct)
private static Task<string> GetCultureAsync(IServiceProvider provider, ulong guildId, CancellationToken ct)
{
var store = provider.GetRequiredService<IWorkspaceStore>();
return await store.GetCultureAsync(guildId, ct).ConfigureAwait(false);
return store.GetCultureAsync(guildId, ct);
}

private sealed record Pending(string DefaultName, ulong? MessageId);
Expand Down
4 changes: 2 additions & 2 deletions src/RustPlusBot.Features.Alarms/Relaying/AlarmRefresher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ await store.SetMessageIdAsync(alarm.GuildId, alarm.ServerId, alarm.EntityId, mid
}
}

private static async Task<string> GetCultureAsync(
private static Task<string> GetCultureAsync(
IServiceProvider provider,
ulong guildId,
CancellationToken ct)
{
var store = provider.GetRequiredService<IWorkspaceStore>();
return await store.GetCultureAsync(guildId, ct).ConfigureAwait(false);
return store.GetCultureAsync(guildId, ct);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ internal sealed class CargoCommandHandler(
public string Name => "cargo";

/// <inheritdoc />
public async Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
public Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
return await MarkerReply
return MarkerReply
.ForAsync(new MarkerReplyServices(state, localizer, clock, mapSettings), context, MarkerKind.CargoShip,
"command.cargo", cancellationToken)
.ConfigureAwait(false);
"command.cargo", cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ internal sealed class ChinookCommandHandler(
public string Name => "chinook";

/// <inheritdoc />
public async Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
public Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
return await MarkerReply
return MarkerReply
.ForAsync(new MarkerReplyServices(state, localizer, clock, mapSettings), context, MarkerKind.Chinook,
"command.chinook", cancellationToken)
.ConfigureAwait(false);
"command.chinook", cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@ internal sealed class HeliCommandHandler(
public string Name => "heli";

/// <inheritdoc />
public async Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
public Task<string?> ExecuteAsync(CommandContext context, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(context);
return await MarkerReply
return MarkerReply
.ForAsync(new MarkerReplyServices(state, localizer, clock, mapSettings), context,
MarkerKind.PatrolHelicopter, "command.heli", cancellationToken)
.ConfigureAwait(false);
MarkerKind.PatrolHelicopter, "command.heli", cancellationToken);
}
}
2 changes: 1 addition & 1 deletion src/RustPlusBot.Features.Commands/Handlers/MarkerReply.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal static class MarkerReply
/// <param name="prefix">The localization key prefix ("command.cargo" / "command.heli" / "command.chinook").</param>
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>The localized reply.</returns>
public static async Task<string> ForAsync(
public static async Task<string?> ForAsync(
MarkerReplyServices services,
CommandContext context,
MarkerKind kind,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,5 @@ await FollowupAsync(removed ? "Server removed." : "That server no longer exists.

/// <summary>Cancels the removal.</summary>
[ComponentInteraction(RemoveCancelId)]
public async Task RemoveCancelAsync() =>
await RespondAsync("Cancelled.", ephemeral: true).ConfigureAwait(false);
public Task RemoveCancelAsync() => RespondAsync("Cancelled.", ephemeral: true);
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,62 +216,59 @@ public async Task StopAllAsync()
}

/// <inheritdoc />
public async Task<ServerInfoSnapshot?> GetServerInfoAsync(
public Task<ServerInfoSnapshot?> GetServerInfoAsync(
ulong guildId,
Guid serverId,
CancellationToken cancellationToken)
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return null;
return Task.FromResult<ServerInfoSnapshot?>(null);
}

return await live.Connection.GetServerInfoAsync(_options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
return live.Connection.GetServerInfoAsync(_options.HeartbeatTimeout, cancellationToken);
}

/// <inheritdoc />
public async Task<ServerTimeSnapshot?> GetTimeAsync(ulong guildId,
public Task<ServerTimeSnapshot?> GetTimeAsync(ulong guildId,
Guid serverId,
CancellationToken cancellationToken)
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return null;
return Task.FromResult<ServerTimeSnapshot?>(null);
}

return await live.Connection.GetTimeAsync(_options.HeartbeatTimeout, cancellationToken).ConfigureAwait(false);
return live.Connection.GetTimeAsync(_options.HeartbeatTimeout, cancellationToken);
}

/// <inheritdoc />
public async Task<TeamInfoSnapshot?> GetTeamInfoAsync(
public Task<TeamInfoSnapshot?> GetTeamInfoAsync(
ulong guildId,
Guid serverId,
CancellationToken cancellationToken)
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return null;
return Task.FromResult<TeamInfoSnapshot?>(null);
}

return await live.Connection.GetTeamInfoAsync(_options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
return live.Connection.GetTeamInfoAsync(_options.HeartbeatTimeout, cancellationToken);
}

/// <inheritdoc />
public async Task<bool> PromoteToLeaderAsync(
public Task<bool> PromoteToLeaderAsync(
ulong guildId,
Guid serverId,
ulong steamId,
CancellationToken cancellationToken)
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return false;
return Task.FromResult(false);
}

return await live.Connection.PromoteToLeaderAsync(steamId, _options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
return live.Connection.PromoteToLeaderAsync(steamId, _options.HeartbeatTimeout, cancellationToken);
}

/// <inheritdoc />
Expand Down Expand Up @@ -331,15 +328,14 @@ public async Task<bool> PromoteToLeaderAsync(
}

/// <inheritdoc />
public async Task<WorldSnapshot?> GetWorldAsync(ulong guildId, Guid serverId, CancellationToken cancellationToken)
public Task<WorldSnapshot?> GetWorldAsync(ulong guildId, Guid serverId, CancellationToken cancellationToken)
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return null;
return Task.FromResult<WorldSnapshot?>(null);
}

return await live.Connection.GetWorldAsync(_options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
return live.Connection.GetWorldAsync(_options.HeartbeatTimeout, cancellationToken);
}

/// <inheritdoc />
Expand Down Expand Up @@ -392,20 +388,19 @@ public async Task<IReadOnlyList<MonumentSnapshot>> GetMonumentsAsync(
}

/// <inheritdoc />
public async Task<DeviceReading> GetSmartAlarmReadingAsync(
public Task<DeviceReading> GetSmartAlarmReadingAsync(
ulong guildId,
Guid serverId,
ulong entityId,
CancellationToken cancellationToken)
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return new DeviceReading(null, DeviceReachability.NoResponse);
return Task.FromResult(new DeviceReading(null, DeviceReachability.NoResponse));
}

return await live.Connection
.GetSmartDeviceInfoAsync(entityId, SmartDeviceKind.Alarm, _options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
return live.Connection
.GetSmartDeviceInfoAsync(entityId, SmartDeviceKind.Alarm, _options.HeartbeatTimeout, cancellationToken);
}

/// <inheritdoc />
Expand All @@ -427,7 +422,7 @@ public async Task<DeviceReading> GetSmartAlarmReadingAsync(
}

/// <inheritdoc />
public async Task<DeviceReachability> SetSmartSwitchAsync(
public Task<DeviceReachability> SetSmartSwitchAsync(
ulong guildId,
Guid serverId,
ulong entityId,
Expand All @@ -436,16 +431,14 @@ public async Task<DeviceReachability> SetSmartSwitchAsync(
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return DeviceReachability.NoResponse;
return Task.FromResult(DeviceReachability.NoResponse);
}

return await live.Connection
.SetSmartSwitchValueAsync(entityId, value, _options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
return live.Connection.SetSmartSwitchValueAsync(entityId, value, _options.HeartbeatTimeout, cancellationToken);
}

/// <inheritdoc />
public async Task<DeviceReachability> StrobeSmartSwitchAsync(
public Task<DeviceReachability> StrobeSmartSwitchAsync(
ulong guildId,
Guid serverId,
ulong entityId,
Expand All @@ -455,29 +448,26 @@ public async Task<DeviceReachability> StrobeSmartSwitchAsync(
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return DeviceReachability.NoResponse;
return Task.FromResult(DeviceReachability.NoResponse);
}

return await live.Connection
.StrobeSmartSwitchAsync(entityId, timeoutMs, value, _options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
return live.Connection
.StrobeSmartSwitchAsync(entityId, timeoutMs, value, _options.HeartbeatTimeout, cancellationToken);
}

/// <inheritdoc />
public async Task<bool> SetClanMotdAsync(
public Task<bool> SetClanMotdAsync(
ulong guildId,
Guid serverId,
string motd,
CancellationToken cancellationToken)
{
if (!_liveSockets.TryGetValue((guildId, serverId), out var live))
{
return false;
return Task.FromResult(false);
}

return await live.Connection
.SetClanMotdAsync(motd, _options.HeartbeatTimeout, cancellationToken)
.ConfigureAwait(false);
return live.Connection.SetClanMotdAsync(motd, _options.HeartbeatTimeout, cancellationToken);
}

[LoggerMessage(Level = LogLevel.Error, Message = "Connection loop for server {ServerId} faulted.")]
Expand Down Expand Up @@ -521,37 +511,11 @@ await PublishStatusAsync(key, ConnectionStatus.Connecting, null, p.CredentialId,
.ConfigureAwait(false);

var connection = source.Create(p.Ip, p.Port, p.SteamId, p.PlayerToken);
SocketConnectOutcome outcome;
try
{
outcome = await connection.ConnectAsync(_options.ConnectTimeout, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
await connection.DisposeAsync().ConfigureAwait(false);
throw;
}
catch (Exception) when (ct.IsCancellationRequested)
var outcome = await ConnectAsync(connection, key.Server, ct).ConfigureAwait(false);
if (outcome is null)
{
// Stopping. A socket library may unwind cancellation as something other than an
// OperationCanceledException (a client disposed underneath the connect, say). Dispose
// and end the loop as the cancellation it is — reporting the server unreachable and
// warning about a connect failure here would be noise pointing at the wrong problem.
await connection.DisposeAsync().ConfigureAwait(false);
return;
}
#pragma warning disable CA1031 // Broad catch is intentional: see below — a throwing source must not end the loop.
catch (Exception ex)
#pragma warning restore CA1031
{
// A source that throws instead of reporting an outcome is still just an unreachable
// server. Letting it escape reaches the outer catch and ENDS the loop, and nothing
// re-arms a dead loop, so the server would stay offline until the process restarts.
// Fall through as Unreachable: the branch below disposes the socket, publishes the
// status and backs off, exactly as for a reported failure.
LogConnectThrew(logger, ex, key.Server);
outcome = SocketConnectOutcome.Unreachable;
}

if (outcome == SocketConnectOutcome.AuthRejected)
{
Expand Down Expand Up @@ -612,6 +576,53 @@ await PublishStatusAsync(key, ConnectionStatus.Unreachable, null, p.CredentialId
#pragma warning restore CA1031
}

/// <summary>
/// Connects the socket, folding a throwing source into an outcome so the connection loop never ends on it.
/// </summary>
/// <param name="connection">The freshly created socket to connect.</param>
/// <param name="serverId">The server the socket belongs to, for logging.</param>
/// <param name="ct">The loop's cancellation token.</param>
/// <returns>
/// The connect outcome, or <see langword="null"/> when the connect unwound because the loop is stopping;
/// the socket is disposed in that case and the caller just ends the loop.
/// </returns>
private async Task<SocketConnectOutcome?> ConnectAsync(
IRustServerConnection connection,
Guid serverId,
CancellationToken ct)
{
try
{
return await connection.ConnectAsync(_options.ConnectTimeout, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
await connection.DisposeAsync().ConfigureAwait(false);
throw;
}
catch (Exception) when (ct.IsCancellationRequested)
{
// Stopping. A socket library may unwind cancellation as something other than an
// OperationCanceledException (a client disposed underneath the connect, say). Dispose
// and end the loop as the cancellation it is — reporting the server unreachable and
// warning about a connect failure here would be noise pointing at the wrong problem.
await connection.DisposeAsync().ConfigureAwait(false);
return null;
}
#pragma warning disable CA1031 // Broad catch is intentional: see below — a throwing source must not end the loop.
catch (Exception ex)
#pragma warning restore CA1031
{
// A source that throws instead of reporting an outcome is still just an unreachable
// server. Letting it escape reaches the loop's outer catch and ENDS the loop, and nothing
// re-arms a dead loop, so the server would stay offline until the process restarts.
// Report Unreachable instead: the loop disposes the socket, publishes the status and
// backs off, exactly as for a reported failure.
LogConnectThrew(logger, ex, serverId);
return SocketConnectOutcome.Unreachable;
}
}

private async Task<ReconnectReason> RunConnectedAsync(
(ulong Guild, Guid Server) key,
IRustServerConnection connection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ public Task StartAsync(CancellationToken cancellationToken)
}

/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
public Task StopAsync(CancellationToken cancellationToken)
{
client.Ready -= OnReadyAsync;
await supervisor.StopAllAsync().ConfigureAwait(false);
return supervisor.StopAllAsync();
}

private Task OnReadyAsync()
Expand Down
Loading
Loading