continue;
}
}
- catch (SocketException e)
+ catch (SocketException)
{
// Client no longer reachable, pretend it didn't happen
continue;
}
+ catch (ObjectDisposedException)
+ {
+ // Socket was disposed, don't care.
+ return;
+ }
// Get client from active clients
if (!_allConnections.TryGetValue(data.RemoteEndPoint, out var client))
{
public const string Section = "Debug";
- public bool EnableGameRecorder { get; set; }
+ public bool GameRecorderEnabled { get; set; }
+ public string GameRecorderPath { get; set; }
}
}
\ No newline at end of file
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="5.0.0-rc.1.20451.7" />
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="5.0.0-rc.1.20451.14" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0-rc.1.20451.14" />
+ <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.0-rc.2.20475.17" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
</ItemGroup>
<ItemGroup>
+ <Content Include="config.Development.json">
+ <CopyToPublishDirectory>Always</CopyToPublishDirectory>
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+ </Content>
<Content Include="config.json">
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
using Impostor.Server.Net.Manager;
using Impostor.Server.Net.Redirector;
using Impostor.Server.Plugins;
+using Impostor.Server.Recorder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.ObjectPool;
using Serilog;
using Serilog.Events;
})
.ConfigureServices((host, services) =>
{
+ var debug = host.Configuration
+ .GetSection(DebugConfig.Section)
+ .Get<DebugConfig>() ?? new DebugConfig();
+
var redirector = host.Configuration
.GetSection(ServerRedirectorConfig.Section)
.Get<ServerRedirectorConfig>() ?? new ServerRedirectorConfig();
services.Configure<DebugConfig>(host.Configuration.GetSection(DebugConfig.Section));
#endif
services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section));
- services.Configure<ServerRedirectorConfig>(
- host.Configuration.GetSection(ServerRedirectorConfig.Section));
+ services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section));
if (redirector.Enabled)
{
}
else
{
- services.AddSingleton<IClientFactory, ClientFactory<Client>>();
+ if (debug.GameRecorderEnabled)
+ {
+ services.AddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider());
+ services.AddSingleton<ObjectPool<PacketSerializationContext>>(serviceProvider =>
+ {
+ var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
+ var policy = new PacketSerializationContextPooledObjectPolicy();
+ return provider.Create(policy);
+ });
+
+ services.AddSingleton<PacketRecorder>();
+ services.AddSingleton<IClientFactory, ClientFactory<ClientRecorder>>();
+ }
+ else
+ {
+ services.AddSingleton<IClientFactory, ClientFactory<Client>>();
+ }
+
services.AddSingleton<GameManager>();
services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>());
}
--- /dev/null
+using System.Threading.Tasks;
+using Impostor.Api.Net.Messages;
+using Impostor.Server.Net;
+using Impostor.Server.Net.Hazel;
+using Impostor.Server.Net.Manager;
+using Microsoft.Extensions.Logging;
+
+namespace Impostor.Server.Recorder
+{
+ internal class ClientRecorder : Client
+ {
+ private readonly PacketRecorder _recorder;
+
+ public ClientRecorder(ILogger<Client> logger, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, PacketRecorder recorder)
+ : base(logger, clientManager, gameManager, name, connection)
+ {
+ _recorder = recorder;
+ }
+
+ public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType)
+ {
+ await _recorder.WriteMessageAsync(this, reader.Tag, reader.Buffer);
+ await base.HandleMessageAsync(reader, messageType);
+ }
+
+ public override async ValueTask HandleDisconnectAsync(string reason)
+ {
+ await _recorder.WriteDisconnectAsync(this);
+ await base.HandleDisconnectAsync(reason);
+ }
+ }
+}
\ No newline at end of file
+++ /dev/null
-namespace Impostor.Server.Recorder
-{
- public class GameRecorder
- {
-
- }
-}
\ No newline at end of file
--- /dev/null
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Impostor.Server.Data;
+using Impostor.Server.Net;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
+
+namespace Impostor.Server.Recorder
+{
+ /// <summary>
+ /// Records all packets received in <see cref="ClientRecorder.HandleMessageAsync"/>.
+ /// </summary>
+ internal class PacketRecorder : IDisposable
+ {
+ private readonly ILogger<PacketRecorder> _logger;
+ private readonly ObjectPool<PacketSerializationContext> _pool;
+ private readonly SemaphoreSlim _writerLock;
+ private readonly FileStream _writer;
+
+ public PacketRecorder(ILogger<PacketRecorder> logger, IOptions<DebugConfig> options, ObjectPool<PacketSerializationContext> pool)
+ {
+ var name = $"session_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.dat";
+ var path = Path.Combine(options.Value.GameRecorderPath, name);
+
+ _logger = logger;
+ _logger.LogInformation("PacketRecorder is enabled, writing packets to {0}.", path);
+ _pool = pool;
+ _writerLock = new SemaphoreSlim(1, 1);
+ _writer = File.Open(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
+ }
+
+ public async Task WriteMessageAsync(ClientRecorder client, byte tag, ReadOnlyMemory<byte> buffer)
+ {
+ _logger.LogTrace("Writing Message.");
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.Message);
+ WriteClient(context, client);
+ WritePacket(context, tag, buffer.Span);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ public async Task WriteDisconnectAsync(ClientRecorder client)
+ {
+ _logger.LogTrace("Writing Disconnect.");
+
+ var context = _pool.Get();
+
+ try
+ {
+ WriteHeader(context, RecordedPacketType.Disconnect);
+ WriteClient(context, client);
+ WriteLength(context);
+
+ await WriteAsync(context.Stream);
+ }
+ finally
+ {
+ _pool.Return(context);
+ }
+ }
+
+ private static void WriteHeader(PacketSerializationContext context, RecordedPacketType type)
+ {
+ // Length placeholder.
+ context.Writer.Write((int) 0);
+ context.Writer.Write((byte) type);
+ }
+
+ private static void WriteClient(PacketSerializationContext context, ClientBase client)
+ {
+ var addressBytes = client.Connection.EndPoint.Address.GetAddressBytes();
+
+ context.Writer.Write((byte) addressBytes.Length);
+ context.Writer.Write(addressBytes);
+ context.Writer.Write((ushort) client.Connection.EndPoint.Port);
+ }
+
+ private static void WritePacket(PacketSerializationContext context, byte tag, ReadOnlySpan<byte> buffer)
+ {
+ context.Writer.Write((byte) tag);
+ context.Writer.Write((int) buffer.Length);
+ context.Writer.Write(buffer);
+ }
+
+ private static void WriteLength(PacketSerializationContext context)
+ {
+ var length = context.Stream.Position;
+
+ context.Stream.Position = 0;
+ context.Writer.Write((int) length);
+ context.Stream.Position = length;
+ }
+
+ private async Task WriteAsync(Stream data)
+ {
+ var hasLock = false;
+
+ try
+ {
+ hasLock = await _writerLock.WaitAsync(TimeSpan.FromMinutes(1));
+
+ if (hasLock)
+ {
+ data.Position = 0;
+
+ await data.CopyToAsync(_writer);
+ await _writer.FlushAsync();
+ }
+ }
+ finally
+ {
+ if (hasLock)
+ {
+ _writerLock.Release();
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ _writer.Dispose();
+ _writerLock.Dispose();
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using System.IO;
+using System.Text;
+
+namespace Impostor.Server.Recorder
+{
+ public class PacketSerializationContext
+ {
+ private const int InitialStreamSize = 0x100;
+ private const int MaximumStreamSize = 0x100000;
+
+ private MemoryStream _memory;
+ private BinaryWriter _writer;
+
+ public MemoryStream Stream
+ {
+ get
+ {
+ if (_memory == null)
+ {
+ _memory = new MemoryStream(InitialStreamSize);
+ }
+
+ return _memory;
+ }
+ private set => _memory = value;
+ }
+
+ public BinaryWriter Writer
+ {
+ get
+ {
+ if (_writer == null)
+ {
+ _writer = new BinaryWriter(Stream, Encoding.UTF8, true);
+ }
+
+ return _writer;
+ }
+ private set => _writer = value;
+ }
+
+ public void Reset()
+ {
+ if (Stream.Capacity > MaximumStreamSize)
+ {
+ Stream = null;
+ Writer = null;
+ }
+ else
+ {
+ Stream.Position = 0L;
+ Stream.SetLength(0L);
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+using Microsoft.Extensions.ObjectPool;
+
+namespace Impostor.Server.Recorder
+{
+ public class PacketSerializationContextPooledObjectPolicy : IPooledObjectPolicy<PacketSerializationContext>
+ {
+ public PacketSerializationContext Create()
+ {
+ return new PacketSerializationContext();
+ }
+
+ public bool Return(PacketSerializationContext obj)
+ {
+ obj.Reset();
+ return true;
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+namespace Impostor.Server.Recorder
+{
+ internal enum RecordedPacketType : byte
+ {
+ Message = 1,
+ Disconnect = 2,
+ }
+}
\ No newline at end of file
]
},
"Debug": {
- "EnableGameRecorder": true
+ "GameRecorderEnabled": true,
+ "GameRecorderPath": ""
}
}
\ No newline at end of file
"PublicPort": 22023,
"ListenIp": "0.0.0.0",
"ListenPort": 22023
+ },
+ "Debug": {
+ "GameRecorderEnabled": true,
+ "GameRecorderPath": ""
}
}
\ No newline at end of file