Faster AES, Packet Stream, more async changes

This commit is contained in:
Anon 2026-04-04 12:26:27 +02:00
parent 3b7c9c8510
commit 3a20f2e235
19 changed files with 1009 additions and 272 deletions

View file

@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient
{
@ -50,6 +52,13 @@ namespace MinecraftClient
return ConsoleInteractive.ConsoleReader.RequestImmediateInput();
}
public Task<string> RequestImmediateInputAsync(CancellationToken cancellationToken)
{
// ConsoleInteractive only exposes a blocking immediate-read API.
// Keep the compatibility boundary here so the wider startup/runtime path can await it.
return Task.Run(ConsoleInteractive.ConsoleReader.RequestImmediateInput, cancellationToken);
}
public string? ReadPassword()
{
ConsoleInteractive.ConsoleReader.SetInputVisible(false);
@ -58,6 +67,19 @@ namespace MinecraftClient
return input;
}
public async Task<string?> ReadPasswordAsync(CancellationToken cancellationToken)
{
ConsoleInteractive.ConsoleReader.SetInputVisible(false);
try
{
return await RequestImmediateInputAsync(cancellationToken);
}
finally
{
ConsoleInteractive.ConsoleReader.SetInputVisible(true);
}
}
public void ClearInputBuffer()
{
ConsoleInteractive.ConsoleReader.ClearBuffer();

View file

@ -76,6 +76,13 @@ namespace MinecraftClient
return Backend.ReadPassword();
}
public static Task<string?> ReadPasswordAsync(CancellationToken cancellationToken = default)
{
if (BasicIO)
return Task.FromResult<string?>(Console.ReadLine());
return Backend.ReadPasswordAsync(cancellationToken);
}
/// <summary>
/// Read a line from the standard input
/// </summary>
@ -86,6 +93,13 @@ namespace MinecraftClient
return Backend.RequestImmediateInput();
}
public static Task<string> ReadLineAsync(CancellationToken cancellationToken = default)
{
if (BasicIO)
return Task.FromResult(Console.ReadLine() ?? string.Empty);
return Backend.RequestImmediateInputAsync(cancellationToken);
}
/// <summary>
/// Debug routine: print all keys pressed in the console
/// </summary>

View file

@ -1,9 +1,10 @@
using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto.AesHandler;
namespace MinecraftClient.Crypto
{
@ -11,8 +12,7 @@ namespace MinecraftClient.Crypto
{
public const int blockSize = 16;
private readonly Aes? Aes = null;
private readonly FastAes? FastAes = null;
private readonly IAesHandler aesHandler;
private bool inStreamEnded = false;
@ -24,18 +24,7 @@ namespace MinecraftClient.Crypto
public AesCfb8Stream(Stream stream, byte[] key)
{
BaseStream = stream;
if (FastAes.IsSupported())
FastAes = new FastAes(key);
else
{
Aes = Aes.Create();
Aes.BlockSize = 128;
Aes.KeySize = 128;
Aes.Key = key;
Aes.Mode = CipherMode.ECB;
Aes.Padding = PaddingMode.None;
}
aesHandler = AesHandlerFactory.Create(key);
Array.Copy(key, ReadStreamIV, 16);
Array.Copy(key, WriteStreamIV, 16);
@ -96,10 +85,7 @@ namespace MinecraftClient.Crypto
}
Span<byte> blockOutput = stackalloc byte[blockSize];
if (FastAes is not null)
FastAes.EncryptEcb(ReadStreamIV, blockOutput);
else
Aes!.EncryptEcb(ReadStreamIV, blockOutput, PaddingMode.None);
aesHandler.EncryptEcb(ReadStreamIV, blockOutput);
// Shift left
Array.Copy(ReadStreamIV, 1, ReadStreamIV, 0, blockSize - 1);
@ -111,10 +97,7 @@ namespace MinecraftClient.Crypto
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EncryptBlock(ReadOnlySpan<byte> blockInput, Span<byte> blockOutput)
{
if (FastAes is not null)
FastAes.EncryptEcb(blockInput, blockOutput);
else
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
aesHandler.EncryptEcb(blockInput, blockOutput);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
@ -124,31 +107,39 @@ namespace MinecraftClient.Crypto
return 0;
Span<byte> blockOutput = stackalloc byte[blockSize];
byte[] inputBuf = ArrayPool<byte>.Shared.Rent(blockSize + required);
byte[] inputBuf = new byte[blockSize + required];
Array.Copy(ReadStreamIV, inputBuf, blockSize);
for (int readed = 0, curRead; readed < required; readed += curRead)
try
{
curRead = BaseStream.Read(inputBuf, blockSize + readed, required - readed);
if (curRead == 0)
Array.Copy(ReadStreamIV, inputBuf, blockSize);
for (int readed = 0, curRead; readed < required; readed += curRead)
{
inStreamEnded = true;
return readed;
curRead = BaseStream.Read(inputBuf, blockSize + readed, required - readed);
if (curRead == 0)
{
inStreamEnded = true;
Array.Copy(inputBuf, readed, ReadStreamIV, 0, blockSize);
return readed;
}
int processEnd = readed + curRead;
for (int idx = readed; idx < processEnd; idx++)
{
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
EncryptBlock(blockInput, blockOutput);
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
}
}
int processEnd = readed + curRead;
for (int idx = readed; idx < processEnd; idx++)
{
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
EncryptBlock(blockInput, blockOutput);
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
}
Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize);
return required;
}
finally
{
ArrayPool<byte>.Shared.Return(inputBuf);
}
Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize);
return required;
}
public override long Seek(long offset, SeekOrigin origin)
@ -179,19 +170,27 @@ namespace MinecraftClient.Crypto
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override void Write(byte[] input, int offset, int required)
{
byte[] outputBuf = new byte[blockSize + required];
Array.Copy(WriteStreamIV, outputBuf, blockSize);
byte[] outputBuf = ArrayPool<byte>.Shared.Rent(blockSize + required);
Span<byte> blockOutput = stackalloc byte[blockSize];
for (int wirtten = 0; wirtten < required; ++wirtten)
try
{
ReadOnlySpan<byte> blockInput = new(outputBuf, wirtten, blockSize);
EncryptBlock(blockInput, blockOutput);
outputBuf[blockSize + wirtten] = (byte)(blockOutput[0] ^ input[offset + wirtten]);
}
BaseStream.Write(outputBuf, blockSize, required);
Array.Copy(WriteStreamIV, outputBuf, blockSize);
Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize);
Span<byte> blockOutput = stackalloc byte[blockSize];
for (int written = 0; written < required; ++written)
{
ReadOnlySpan<byte> blockInput = new(outputBuf, written, blockSize);
EncryptBlock(blockInput, blockOutput);
outputBuf[blockSize + written] = (byte)(blockOutput[0] ^ input[offset + written]);
}
BaseStream.Write(outputBuf, blockSize, required);
Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize);
}
finally
{
ArrayPool<byte>.Shared.Return(outputBuf);
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
@ -200,26 +199,34 @@ namespace MinecraftClient.Crypto
if (inStreamEnded || buffer.Length == 0)
return 0;
byte[] inputBuf = new byte[blockSize + buffer.Length];
Array.Copy(ReadStreamIV, inputBuf, blockSize);
byte[] inputBuf = ArrayPool<byte>.Shared.Rent(blockSize + buffer.Length);
for (int readed = 0; readed < buffer.Length;)
try
{
int curRead = await BaseStream.ReadAsync(inputBuf.AsMemory(blockSize + readed, buffer.Length - readed), cancellationToken);
if (curRead == 0)
Array.Copy(ReadStreamIV, inputBuf, blockSize);
for (int readed = 0; readed < buffer.Length;)
{
inStreamEnded = true;
Array.Copy(inputBuf, readed, ReadStreamIV, 0, blockSize);
return readed;
int curRead = await BaseStream.ReadAsync(inputBuf.AsMemory(blockSize + readed, buffer.Length - readed), cancellationToken);
if (curRead == 0)
{
inStreamEnded = true;
Array.Copy(inputBuf, readed, ReadStreamIV, 0, blockSize);
return readed;
}
int processEnd = readed + curRead;
DecryptToOutputBuffer(inputBuf, buffer, readed, processEnd);
readed = processEnd;
}
int processEnd = readed + curRead;
DecryptToOutputBuffer(inputBuf, buffer, readed, processEnd);
readed = processEnd;
Array.Copy(inputBuf, buffer.Length, ReadStreamIV, 0, blockSize);
return buffer.Length;
}
finally
{
ArrayPool<byte>.Shared.Return(inputBuf);
}
Array.Copy(inputBuf, buffer.Length, ReadStreamIV, 0, blockSize);
return buffer.Length;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
@ -228,12 +235,20 @@ namespace MinecraftClient.Crypto
if (buffer.Length == 0)
return;
byte[] outputBuf = new byte[blockSize + buffer.Length];
Array.Copy(WriteStreamIV, outputBuf, blockSize);
EncryptToOutputBuffer(buffer, outputBuf);
byte[] outputBuf = ArrayPool<byte>.Shared.Rent(blockSize + buffer.Length);
await BaseStream.WriteAsync(outputBuf.AsMemory(blockSize, buffer.Length), cancellationToken);
Array.Copy(outputBuf, buffer.Length, WriteStreamIV, 0, blockSize);
try
{
Array.Copy(WriteStreamIV, outputBuf, blockSize);
EncryptToOutputBuffer(buffer, outputBuf);
await BaseStream.WriteAsync(outputBuf.AsMemory(blockSize, buffer.Length), cancellationToken);
Array.Copy(outputBuf, buffer.Length, WriteStreamIV, 0, blockSize);
}
finally
{
ArrayPool<byte>.Shared.Return(outputBuf);
}
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
@ -246,6 +261,16 @@ namespace MinecraftClient.Crypto
return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
aesHandler.Dispose();
}
base.Dispose(disposing);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void DecryptToOutputBuffer(byte[] inputBuf, Memory<byte> output, int start, int end)
{

View file

@ -0,0 +1,31 @@
using System;
using System.Security.Cryptography;
namespace MinecraftClient.Crypto.AesHandler;
public sealed class BasicAes : IAesHandler
{
private readonly Aes aes;
public BasicAes(byte[] key)
{
ArgumentNullException.ThrowIfNull(key);
aes = Aes.Create();
aes.BlockSize = 128;
aes.KeySize = 128;
aes.Key = key;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
}
public override void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination)
{
aes.EncryptEcb(plaintext, destination, PaddingMode.None);
}
public override void Dispose()
{
aes.Dispose();
}
}

View file

@ -0,0 +1,162 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
namespace MinecraftClient.Crypto.AesHandler;
public sealed class FasterAesArm : IAesHandler
{
private const int BlockSize = 16;
private const int Rounds = 10;
private readonly byte[] enc;
public FasterAesArm(ReadOnlySpan<byte> key)
{
enc = new byte[(Rounds + 1) * BlockSize];
int[] intKey = GenerateKeyExpansion(key);
for (int i = 0; i < intKey.Length; ++i)
{
enc[i * 4 + 0] = (byte)((intKey[i] >> 0) & 0xFF);
enc[i * 4 + 1] = (byte)((intKey[i] >> 8) & 0xFF);
enc[i * 4 + 2] = (byte)((intKey[i] >> 16) & 0xFF);
enc[i * 4 + 3] = (byte)((intKey[i] >> 24) & 0xFF);
}
}
public static bool IsSupported()
{
return Aes.IsSupported && AdvSimd.IsSupported;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination)
{
int position = 0;
int left = plaintext.Length;
Vector128<byte> key0 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[0 * BlockSize]);
Vector128<byte> key1 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[1 * BlockSize]);
Vector128<byte> key2 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[2 * BlockSize]);
Vector128<byte> key3 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[3 * BlockSize]);
Vector128<byte> key4 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[4 * BlockSize]);
Vector128<byte> key5 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[5 * BlockSize]);
Vector128<byte> key6 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[6 * BlockSize]);
Vector128<byte> key7 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[7 * BlockSize]);
Vector128<byte> key8 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[8 * BlockSize]);
Vector128<byte> key9 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[9 * BlockSize]);
Vector128<byte> key10 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[10 * BlockSize]);
while (left >= BlockSize)
{
Vector128<byte> block = Unsafe.ReadUnaligned<Vector128<byte>>(ref Unsafe.AsRef(in plaintext[position]));
block = Aes.Encrypt(block, key0);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key1);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key2);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key3);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key4);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key5);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key6);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key7);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key8);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key9);
block = AdvSimd.Xor(block, key10);
Unsafe.WriteUnaligned(ref destination[position], block);
position += BlockSize;
left -= BlockSize;
}
}
private static int[] GenerateKeyExpansion(ReadOnlySpan<byte> rgbKey)
{
int[] encryptKeyExpansion = new int[4 * (Rounds + 1)];
int index = 0;
for (int i = 0; i < 4; ++i)
{
int i0 = rgbKey[index++];
int i1 = rgbKey[index++];
int i2 = rgbKey[index++];
int i3 = rgbKey[index++];
encryptKeyExpansion[i] = i3 << 24 | i2 << 16 | i1 << 8 | i0;
}
for (int i = 4; i < 4 * (Rounds + 1); ++i)
{
int temp = encryptKeyExpansion[i - 1];
if (i % 4 == 0)
{
temp = SubWord(Rot3(temp));
temp ^= Rcon[(i / 4) - 1];
}
encryptKeyExpansion[i] = encryptKeyExpansion[i - 4] ^ temp;
}
return encryptKeyExpansion;
}
private static int SubWord(int value)
{
return Sbox[value & 0xFF]
| Sbox[(value >> 8) & 0xFF] << 8
| Sbox[(value >> 16) & 0xFF] << 16
| Sbox[(value >> 24) & 0xFF] << 24;
}
private static int Rot3(int value)
{
return (value << 24 & unchecked((int)0xFF000000)) | (value >> 8 & unchecked((int)0x00FFFFFF));
}
private static ReadOnlySpan<byte> Sbox =>
[
99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118,
202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192,
183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21,
4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117,
9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132,
83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207,
208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168,
81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210,
205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115,
96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219,
224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121,
231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8,
186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138,
112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158,
225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223,
140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22
];
private static ReadOnlySpan<int> Rcon =>
[
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36,
0x6C, 0xD8, 0xAB, 0x4D, 0x9A, 0x2F, 0x5E, 0xBC, 0x63, 0xC6,
0x97, 0x35, 0x6A, 0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91
];
}

View file

@ -0,0 +1,89 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace MinecraftClient.Crypto.AesHandler;
public sealed class FasterAesX86 : IAesHandler
{
private Vector128<byte>[] RoundKeys { get; }
public FasterAesX86(ReadOnlySpan<byte> key)
{
RoundKeys = KeyExpansion(key);
}
public static bool IsSupported()
{
return Sse2.IsSupported && Aes.IsSupported;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public override void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination)
{
Vector128<byte>[] keys = RoundKeys;
ReadOnlySpan<Vector128<byte>> blocks = MemoryMarshal.Cast<byte, Vector128<byte>>(plaintext);
Span<Vector128<byte>> dest = MemoryMarshal.Cast<byte, Vector128<byte>>(destination);
_ = keys[10];
for (int i = 0; i < blocks.Length; i++)
{
Vector128<byte> b = blocks[i];
b = Sse2.Xor(b, keys[0]);
b = Aes.Encrypt(b, keys[1]);
b = Aes.Encrypt(b, keys[2]);
b = Aes.Encrypt(b, keys[3]);
b = Aes.Encrypt(b, keys[4]);
b = Aes.Encrypt(b, keys[5]);
b = Aes.Encrypt(b, keys[6]);
b = Aes.Encrypt(b, keys[7]);
b = Aes.Encrypt(b, keys[8]);
b = Aes.Encrypt(b, keys[9]);
b = Aes.EncryptLast(b, keys[10]);
dest[i] = b;
}
}
private static Vector128<byte>[] KeyExpansion(ReadOnlySpan<byte> key)
{
Vector128<byte>[] keys = new Vector128<byte>[20];
keys[0] = Unsafe.ReadUnaligned<Vector128<byte>>(ref MemoryMarshal.GetReference(key));
MakeRoundKey(keys, 1, 0x01);
MakeRoundKey(keys, 2, 0x02);
MakeRoundKey(keys, 3, 0x04);
MakeRoundKey(keys, 4, 0x08);
MakeRoundKey(keys, 5, 0x10);
MakeRoundKey(keys, 6, 0x20);
MakeRoundKey(keys, 7, 0x40);
MakeRoundKey(keys, 8, 0x80);
MakeRoundKey(keys, 9, 0x1B);
MakeRoundKey(keys, 10, 0x36);
for (int i = 1; i < 10; i++)
keys[10 + i] = Aes.InverseMixColumns(keys[i]);
return keys;
}
private static void MakeRoundKey(Vector128<byte>[] keys, int index, byte rcon)
{
Vector128<byte> s = keys[index - 1];
Vector128<byte> t = keys[index - 1];
t = Aes.KeygenAssist(t, rcon);
t = Sse2.Shuffle(t.AsUInt32(), 0xFF).AsByte();
s = Sse2.Xor(s, Sse2.ShiftLeftLogical128BitLane(s, 4));
s = Sse2.Xor(s, Sse2.ShiftLeftLogical128BitLane(s, 8));
keys[index] = Sse2.Xor(s, t);
}
}

View file

@ -0,0 +1,20 @@
using System;
using MinecraftClient.Crypto.AesHandler;
namespace MinecraftClient.Crypto;
internal static class AesHandlerFactory
{
public static IAesHandler Create(ReadOnlySpan<byte> key)
{
byte[] ownedKey = key.ToArray();
if (FasterAesX86.IsSupported())
return new FasterAesX86(ownedKey);
if (FasterAesArm.IsSupported())
return new FasterAesArm(ownedKey);
return new BasicAes(ownedKey);
}
}

View file

@ -0,0 +1,12 @@
using System;
namespace MinecraftClient.Crypto;
public abstract class IAesHandler : IDisposable
{
public abstract void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination);
public virtual void Dispose()
{
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient
{
@ -12,7 +13,7 @@ namespace MinecraftClient
public class FileMonitor : IDisposable
{
private readonly Tuple<FileSystemWatcher, CancellationTokenSource>? monitor = null;
private readonly Tuple<Thread, CancellationTokenSource>? polling = null;
private readonly Tuple<Task, CancellationTokenSource>? polling = null;
/// <summary>
/// Create a new FileMonitor and start monitoring
@ -48,9 +49,9 @@ namespace MinecraftClient
monitor = null;
var cancellationTokenSource = new CancellationTokenSource();
polling = new Tuple<Thread, CancellationTokenSource>(new Thread(() => PollingThread(folder, filename, handler, cancellationTokenSource.Token)), cancellationTokenSource);
polling.Item1.Name = String.Format("{0} Polling thread: {1}", GetType().Name, Path.Combine(folder, filename));
polling.Item1.Start();
polling = new Tuple<Task, CancellationTokenSource>(
Task.Run(() => PollingLoopAsync(folder, filename, handler, cancellationTokenSource.Token), cancellationTokenSource.Token),
cancellationTokenSource);
}
}
@ -66,25 +67,29 @@ namespace MinecraftClient
}
/// <summary>
/// Fallback polling thread for use when operating system does not support FileSystemWatcher
/// Fallback polling loop for use when operating system does not support FileSystemWatcher
/// </summary>
/// <param name="folder">Folder to monitor</param>
/// <param name="filename">File name to monitor</param>
/// <param name="handler">Callback when file changes</param>
private void PollingThread(string folder, string filename, FileSystemEventHandler handler, CancellationToken cancellationToken)
private async Task PollingLoopAsync(string folder, string filename, FileSystemEventHandler handler, CancellationToken cancellationToken)
{
string filePath = Path.Combine(folder, filename);
DateTime lastWrite = GetLastWrite(filePath);
while (!cancellationToken.IsCancellationRequested)
using PeriodicTimer periodicTimer = new(TimeSpan.FromSeconds(5));
try
{
Thread.Sleep(5000);
DateTime lastWriteNew = GetLastWrite(filePath);
if (lastWriteNew != lastWrite)
while (await periodicTimer.WaitForNextTickAsync(cancellationToken))
{
lastWrite = lastWriteNew;
handler(this, new FileSystemEventArgs(WatcherChangeTypes.Changed, folder, filename));
DateTime lastWriteNew = GetLastWrite(filePath);
if (lastWriteNew != lastWrite)
{
lastWrite = lastWriteNew;
handler(this, new FileSystemEventArgs(WatcherChangeTypes.Changed, folder, filename));
}
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { }
}
/// <summary>

View file

@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient
{
@ -56,8 +58,12 @@ namespace MinecraftClient
string RequestImmediateInput();
Task<string> RequestImmediateInputAsync(CancellationToken cancellationToken);
string? ReadPassword();
Task<string?> ReadPasswordAsync(CancellationToken cancellationToken);
void ClearInputBuffer();
bool DisplayUserInput { get; set; }

View file

@ -369,9 +369,8 @@ namespace MinecraftClient
if (ReconnectionAttemptsLeft > 0)
{
Log.Info(string.Format(Translations.mcc_reconnect, ReconnectionAttemptsLeft));
Thread.Sleep(5000);
ReconnectionAttemptsLeft--;
Program.Restart();
Program.Restart(5, announceDelay: false);
}
else if (InternalConfig.InteractiveMode)
{
@ -490,9 +489,8 @@ namespace MinecraftClient
if (ReconnectionAttemptsLeft > 0)
{
Log.Info($"Reconnecting... Attempts left: {ReconnectionAttemptsLeft}");
Thread.Sleep(5000);
ReconnectionAttemptsLeft--;
Program.Restart();
Program.Restart(5, announceDelay: false);
}
else if (InternalConfig.InteractiveMode)
{

View file

@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
@ -12,7 +14,7 @@ public sealed class MccEmbeddedMcpHost
{
private readonly MccMcpConfig config;
private readonly IMccMcpCapabilities capabilities;
private readonly object stateLock = new();
private readonly SemaphoreSlim stateLock = new(1, 1);
private WebApplication? app;
public MccEmbeddedMcpHost(MccMcpConfig config, IMccMcpCapabilities capabilities)
@ -25,10 +27,7 @@ public sealed class MccEmbeddedMcpHost
{
get
{
lock (stateLock)
{
return app is not null;
}
return app is not null;
}
}
@ -36,29 +35,37 @@ public sealed class MccEmbeddedMcpHost
public bool Start(out string? error)
{
lock (stateLock)
(bool success, string? startError) = StartAsync().GetAwaiter().GetResult();
error = startError;
return success;
}
public bool Stop(out string? error)
{
(bool success, string? stopError) = StopAsync().GetAwaiter().GetResult();
error = stopError;
return success;
}
public async Task<(bool Success, string? Error)> StartAsync(CancellationToken cancellationToken = default)
{
await stateLock.WaitAsync(cancellationToken);
try
{
error = null;
if (app is not null)
return true;
return (true, null);
string route = NormalizeRoute(config.Transport.Route);
string bindHost = string.IsNullOrWhiteSpace(config.Transport.BindHost) ? "127.0.0.1" : config.Transport.BindHost.Trim();
if (config.Transport.Port is < 1 or > 65535)
{
error = "invalid_port";
return false;
}
return (false, "invalid_port");
string? requiredToken = null;
if (config.Transport.RequireAuthToken)
{
requiredToken = Environment.GetEnvironmentVariable(config.Transport.AuthTokenEnvVar);
if (string.IsNullOrWhiteSpace(requiredToken))
{
error = "missing_auth_token";
return false;
}
return (false, "missing_auth_token");
}
WebApplicationBuilder builder = WebApplication.CreateBuilder();
@ -96,33 +103,49 @@ public sealed class MccEmbeddedMcpHost
}
builtApp.MapMcp(route);
builtApp.StartAsync().GetAwaiter().GetResult();
app = builtApp;
return true;
}
}
public bool Stop(out string? error)
{
lock (stateLock)
{
error = null;
if (app is null)
return true;
try
{
app.StopAsync().GetAwaiter().GetResult();
app.DisposeAsync().AsTask().GetAwaiter().GetResult();
app = null;
return true;
await builtApp.StartAsync(cancellationToken);
app = builtApp;
return (true, null);
}
catch
{
error = "stop_failed";
return false;
await builtApp.DisposeAsync();
throw;
}
}
finally
{
stateLock.Release();
}
}
public async Task<(bool Success, string? Error)> StopAsync(CancellationToken cancellationToken = default)
{
await stateLock.WaitAsync(cancellationToken);
try
{
if (app is null)
return (true, null);
try
{
await app.StopAsync(cancellationToken);
await app.DisposeAsync();
app = null;
return (true, null);
}
catch
{
return (false, "stop_failed");
}
}
finally
{
stateLock.Release();
}
}
private static string NormalizeRoute(string route)

View file

@ -577,7 +577,7 @@ namespace MinecraftClient
if (string.IsNullOrWhiteSpace(InternalConfig.Account.Password) && !skipPassword &&
(Config.Main.Advanced.SessionCache == CacheType.none || !SessionCache.Contains(ToLowerIfNeed(InternalConfig.Account.Login))))
{
RequestPassword();
await RequestPasswordAsync();
}
startupargs = args;
@ -587,10 +587,10 @@ namespace MinecraftClient
/// <summary>
/// Reduest user to submit password.
/// </summary>
private static void RequestPassword()
private static async Task RequestPasswordAsync()
{
ConsoleIO.WriteLine(ConsoleIO.BasicIO ? string.Format(Translations.mcc_password_basic_io, InternalConfig.Account.Login) + "\n" : Translations.mcc_password_hidden);
string? password = ConsoleIO.BasicIO ? Console.ReadLine() : ConsoleIO.ReadPassword();
string? password = await ConsoleIO.ReadPasswordAsync();
if (string.IsNullOrWhiteSpace(password))
InternalConfig.Account.Password = "-";
else
@ -651,7 +651,7 @@ namespace MinecraftClient
if (result != ProtocolHandler.LoginResult.Success
&& string.IsNullOrWhiteSpace(InternalConfig.Account.Password)
&& !(Config.Main.General.AccountType == LoginType.microsoft))
RequestPassword();
await RequestPasswordAsync();
}
else ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_session_valid, session.PlayerName));
}
@ -897,27 +897,33 @@ namespace MinecraftClient
/// </summary>
/// <param name="delaySeconds">Optional delay, in seconds, before restarting</param>
/// <param name="keepAccountAndServerSettings">Optional, keep account and server settings</param>
public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
public static void Restart(int delaySeconds = 0, bool keepAccountAndServerSettings = false, bool announceDelay = true)
{
ConsoleIO.Backend?.StopReadThread();
new Thread(new ThreadStart(delegate
StartLifecycleTask(RestartAsync(delaySeconds, keepAccountAndServerSettings, announceDelay));
}
private static async Task RestartAsync(int delaySeconds, bool keepAccountAndServerSettings, bool announceDelay)
{
if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
if (offlinePrompt is not null)
{
if (client is not null) { client.Disconnect(); ConsoleIO.Reset(); }
if (offlinePrompt is not null)
{
if (ConsoleIO.Backend is not null)
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
offlinePrompt.Item2.Cancel(); offlinePrompt.Item1.Join(); offlinePrompt = null; ConsoleIO.Reset();
}
if (delaySeconds > 0)
{
if (ConsoleIO.Backend is not null)
ConsoleIO.Backend.OnInputChange -= ConsoleIO.OfflineAutocompleteHandler;
offlinePrompt.Item2.Cancel();
offlinePrompt.Item1.Join();
offlinePrompt = null;
ConsoleIO.Reset();
}
if (delaySeconds > 0)
{
if (announceDelay)
ConsoleIO.WriteLine(string.Format(Translations.mcc_restart_delay, delaySeconds));
Thread.Sleep(delaySeconds * 1000);
}
ConsoleIO.WriteLine(Translations.mcc_restart);
ReloadSettings(keepAccountAndServerSettings);
InitializeClientAsync().GetAwaiter().GetResult();
})).Start();
await Task.Delay(TimeSpan.FromSeconds(delaySeconds));
}
ConsoleIO.WriteLine(Translations.mcc_restart);
ReloadSettings(keepAccountAndServerSettings);
await InitializeClientAsync();
}
public static void DoExit(int exitcode = 0)
@ -946,7 +952,26 @@ namespace MinecraftClient
/// </summary>
public static void Exit(int exitcode = 0)
{
new Thread(() => { DoExit(exitcode); }).Start();
StartLifecycleTask(Task.Run(() => DoExit(exitcode)));
}
private static void StartLifecycleTask(Task lifecycleTask)
{
_ = ObserveLifecycleTaskAsync(lifecycleTask);
}
private static async Task ObserveLifecycleTaskAsync(Task lifecycleTask)
{
try
{
await lifecycleTask;
}
catch (Exception ex)
{
SentrySdk.CaptureException(ex);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + ex);
}
}
/// <summary>

View file

@ -306,7 +306,7 @@ namespace MinecraftClient.Protocol.Handlers
byte b;
while (true)
{
b = socket.ReadDataRAW(1)[0];
b = socket.ReadByteRAW();
i |= (b & 0x7F) << j++ * 7;
if (j > 5) throw new OverflowException("VarInt too big");
if ((b & 0x80) != 128) break;
@ -327,7 +327,7 @@ namespace MinecraftClient.Protocol.Handlers
byte b;
while (true)
{
b = (await socket.ReadDataRAWAsync(1, cancellationToken))[0];
b = await socket.ReadByteRAWAsync(cancellationToken);
i |= (b & 0x7F) << j++ * 7;
if (j > 5) throw new OverflowException("VarInt too big");
if ((b & 0x80) != 128) break;

View file

@ -72,7 +72,7 @@ namespace MinecraftClient.Protocol.Handlers
c = Client;
}
private void Updater(CancellationToken cancelToken)
private async Task UpdaterAsync(CancellationToken cancelToken)
{
if (cancelToken.IsCancellationRequested)
return;
@ -100,7 +100,7 @@ namespace MinecraftClient.Protocol.Handlers
long sleepLength = nextUpdateDue - stopWatch.ElapsedMilliseconds;
if (sleepLength > 1)
Thread.Sleep((int)Math.Min(sleepLength, ClientTickIntervalMilliseconds));
await Task.Delay((int)Math.Min(sleepLength, ClientTickIntervalMilliseconds), cancelToken);
}
}
catch (System.IO.IOException) { }
@ -247,11 +247,7 @@ namespace MinecraftClient.Protocol.Handlers
{
CancellationTokenSource netReadCts = new();
netReadCancellationTokenSource = netReadCts;
netReadTask = Task.Factory.StartNew(
() => Updater(netReadCts.Token),
netReadCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
netReadTask = Task.Run(() => UpdaterAsync(netReadCts.Token), netReadCts.Token);
}
/// <summary>

View file

@ -284,7 +284,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <summary>
/// Serialized packet/tick loop.
/// </summary>
private void Updater(CancellationToken cancelToken)
private async Task UpdaterAsync(CancellationToken cancelToken)
{
if (cancelToken.IsCancellationRequested)
return;
@ -316,7 +316,7 @@ namespace MinecraftClient.Protocol.Handlers
long sleepLength = nextUpdateDue - stopWatch.ElapsedMilliseconds;
if (sleepLength > 1)
Thread.Sleep((int)Math.Min(sleepLength, ClientTickIntervalMilliseconds));
await Task.Delay((int)Math.Min(sleepLength, ClientTickIntervalMilliseconds), cancelToken);
}
}
catch (ObjectDisposedException)
@ -395,23 +395,9 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="packetData">will contain raw packet Data</param>
internal Tuple<int, Queue<byte>> ReadNextPacket()
{
var size = dataTypes.ReadNextVarIntRAW(socketWrapper); //Packet size
Queue<byte> packetData = new(socketWrapper.ReadDataRAW(size)); //Packet contents
//Handle packet decompression
if (protocolVersion >= MC_1_8_Version
&& compression_treshold >= 0)
{
var sizeUncompressed = dataTypes.ReadNextVarInt(packetData);
if (sizeUncompressed != 0) // != 0 means compressed, let's decompress
{
var toDecompress = packetData.ToArray();
var uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed);
packetData = new Queue<byte>(uncompressed);
}
}
var packetId = dataTypes.ReadNextVarInt(packetData); // Packet ID
var (packetId, packetData) = socketWrapper.GetNextPacket(
protocolVersion >= MC_1_8_Version ? compression_treshold : -1,
dataTypes);
if (handler.GetNetworkPacketCaptureEnabled())
handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true);
@ -420,22 +406,10 @@ namespace MinecraftClient.Protocol.Handlers
internal async Task<Tuple<int, Queue<byte>>> ReadNextPacketAsync(CancellationToken cancellationToken)
{
var size = await dataTypes.ReadNextVarIntRAWAsync(socketWrapper, cancellationToken); //Packet size
Queue<byte> packetData = new(await socketWrapper.ReadDataRAWAsync(size, cancellationToken)); //Packet contents
if (protocolVersion >= MC_1_8_Version
&& compression_treshold >= 0)
{
var sizeUncompressed = dataTypes.ReadNextVarInt(packetData);
if (sizeUncompressed != 0)
{
var toDecompress = packetData.ToArray();
var uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed);
packetData = new Queue<byte>(uncompressed);
}
}
var packetId = dataTypes.ReadNextVarInt(packetData);
var (packetId, packetData) = await socketWrapper.GetNextPacketAsync(
protocolVersion >= MC_1_8_Version ? compression_treshold : -1,
dataTypes,
cancellationToken);
if (handler.GetNetworkPacketCaptureEnabled())
handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true);
@ -3867,21 +3841,17 @@ namespace MinecraftClient.Protocol.Handlers
}
/// <summary>
/// Start the updating thread. Should be called after login success.
/// Start the serialized packet/tick tasks. Should be called after login success.
/// </summary>
private void StartUpdating()
{
CancellationTokenSource netMainCts = new();
netMainCancellationTokenSource = netMainCts;
netMainTask = Task.Factory.StartNew(
() => Updater(netMainCts.Token),
netMainCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
netMainTask = Task.Run(() => UpdaterAsync(netMainCts.Token), netMainCts.Token);
CancellationTokenSource netReaderCts = new();
netReaderCancellationTokenSource = netReaderCts;
netReaderTask = PacketReaderAsync(netReaderCts.Token);
netReaderTask = Task.Run(() => PacketReaderAsync(netReaderCts.Token), netReaderCts.Token);
}
/// <summary>
@ -4381,14 +4351,8 @@ namespace MinecraftClient.Protocol.Handlers
var statusRequest = DataTypes.GetVarInt(0);
socketWrapper.SendDataRAW(dataTypes.ConcatBytes(DataTypes.GetVarInt(statusRequest.Length), statusRequest));
// Read Response length
var packetLength = dataTypes.ReadNextVarIntRAW(socketWrapper);
if (packetLength <= 0)
return false;
// Read the Packet Id
var packetData = new Queue<byte>(socketWrapper.ReadDataRAW(packetLength));
if (dataTypes.ReadNextVarInt(packetData) != 0x00)
var (statusPacketId, packetData) = socketWrapper.GetNextPacket(-1, dataTypes);
if (statusPacketId != 0x00)
return false;
// Get the Json data
@ -4467,15 +4431,11 @@ namespace MinecraftClient.Protocol.Handlers
var pingRequest = dataTypes.ConcatBytes(DataTypes.GetVarInt(0x01), DataTypes.GetLong(pingPayload));
socketWrapper.SendDataRAW(dataTypes.ConcatBytes(DataTypes.GetVarInt(pingRequest.Length), pingRequest));
packetLength = dataTypes.ReadNextVarIntRAW(socketWrapper);
if (packetLength > 0)
var (pongPacketId, pongPacketData) = socketWrapper.GetNextPacket(-1, dataTypes);
if (pongPacketId == 0x01)
{
packetData = new Queue<byte>(socketWrapper.ReadDataRAW(packetLength));
if (dataTypes.ReadNextVarInt(packetData) == 0x01)
{
long pongPayload = dataTypes.ReadNextLong(packetData);
pingMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - pingPayload;
}
long pongPayload = dataTypes.ReadNextLong(pongPacketData);
pingMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - pingPayload;
}
}
catch

View file

@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using MinecraftClient.Protocol.PacketPipeline;
namespace MinecraftClient.Protocol.Handlers
{
@ -12,9 +15,14 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
public class SocketWrapper
{
readonly TcpClient c;
AesCfb8Stream? s;
bool encrypted = false;
private readonly TcpClient client;
private readonly Stream networkStream;
private readonly SemaphoreSlim sendSemaphore = new(1, 1);
private readonly byte[] singleByteBuffer = new byte[1];
private AesCfb8Stream? encryptedStream;
private Stream readStream;
private Stream writeStream;
private bool encrypted = false;
/// <summary>
/// Initialize a new SocketWrapper
@ -22,7 +30,9 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="client">TcpClient connected to the server</param>
public SocketWrapper(TcpClient client)
{
c = client;
this.client = client;
networkStream = client.GetStream();
readStream = writeStream = networkStream;
}
/// <summary>
@ -32,7 +42,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <remarks>Silently dropped connection can only be detected by attempting to read/write data</remarks>
public bool IsConnected()
{
return c.Client is not null && c.Connected;
return client.Client is not null && client.Connected;
}
/// <summary>
@ -41,7 +51,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <returns>TRUE if data is available to read</returns>
public bool HasDataAvailable()
{
return c.Client.Available > 0;
return client.Client.Available > 0;
}
/// <summary>
@ -52,39 +62,21 @@ namespace MinecraftClient.Protocol.Handlers
{
if (encrypted)
throw new InvalidOperationException("Stream is already encrypted!?");
s = new AesCfb8Stream(c.GetStream(), secretKey);
encryptedStream = new AesCfb8Stream(networkStream, secretKey);
readStream = writeStream = encryptedStream;
encrypted = true;
}
/// <summary>
/// Network reading method. Read bytes from the socket or encrypted socket.
/// </summary>
private void Receive(byte[] buffer, int start, int offset, SocketFlags f)
public byte ReadByteRAW()
{
int read = 0;
while (read < offset)
{
if (encrypted)
read += s!.Read(buffer, start + read, offset - read);
else
read += c.Client.Receive(buffer, start + read, offset - read, f);
}
readStream.ReadExactly(singleByteBuffer);
return singleByteBuffer[0];
}
private async Task ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken)
public async ValueTask<byte> ReadByteRAWAsync(CancellationToken cancellationToken)
{
int read = 0;
while (read < buffer.Length)
{
int currentRead = encrypted
? await s!.ReadAsync(buffer[read..], cancellationToken)
: await c.GetStream().ReadAsync(buffer[read..], cancellationToken);
if (currentRead == 0)
throw new IOException("Connection closed.");
read += currentRead;
}
await readStream.ReadExactlyAsync(singleByteBuffer.AsMemory(0, 1), cancellationToken);
return singleByteBuffer[0];
}
/// <summary>
@ -96,8 +88,8 @@ namespace MinecraftClient.Protocol.Handlers
{
if (length > 0)
{
byte[] cache = new byte[length];
Receive(cache, 0, length, SocketFlags.None);
byte[] cache = GC.AllocateUninitializedArray<byte>(length);
readStream.ReadExactly(cache);
return cache;
}
return Array.Empty<byte>();
@ -107,14 +99,34 @@ namespace MinecraftClient.Protocol.Handlers
{
if (length > 0)
{
byte[] cache = new byte[length];
await ReceiveAsync(cache, cancellationToken);
byte[] cache = GC.AllocateUninitializedArray<byte>(length);
await readStream.ReadExactlyAsync(cache.AsMemory(0, length), cancellationToken);
return cache;
}
return Array.Empty<byte>();
}
internal Tuple<int, Queue<byte>> GetNextPacket(int compressionThreshold, DataTypes dataTypes)
{
int packetLength = ReadNextVarIntRaw();
using PacketReadStream packetStream = new(readStream, packetLength);
byte[] payload = ReadPacketPayload(packetStream, compressionThreshold);
Queue<byte> packetData = new(payload);
int packetId = dataTypes.ReadNextVarInt(packetData);
return new(packetId, packetData);
}
internal async Task<Tuple<int, Queue<byte>>> GetNextPacketAsync(int compressionThreshold, DataTypes dataTypes, CancellationToken cancellationToken)
{
int packetLength = await ReadNextVarIntRawAsync(cancellationToken);
await using PacketReadStream packetStream = new(readStream, packetLength);
byte[] payload = await ReadPacketPayloadAsync(packetStream, compressionThreshold, cancellationToken);
Queue<byte> packetData = new(payload);
int packetId = dataTypes.ReadNextVarInt(packetData);
return new(packetId, packetData);
}
/// <summary>
/// Send raw data to the server.
/// </summary>
@ -124,10 +136,16 @@ namespace MinecraftClient.Protocol.Handlers
if (!IsConnected())
throw new SocketException((int)SocketError.NotConnected);
if (encrypted)
s!.Write(buffer, 0, buffer.Length);
else
c.Client.Send(buffer);
sendSemaphore.Wait();
try
{
writeStream.Write(buffer, 0, buffer.Length);
writeStream.Flush();
}
finally
{
sendSemaphore.Release();
}
}
public async Task SendDataRAWAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
@ -135,10 +153,16 @@ namespace MinecraftClient.Protocol.Handlers
if (!IsConnected())
throw new SocketException((int)SocketError.NotConnected);
if (encrypted)
await s!.WriteAsync(buffer, cancellationToken);
else
await c.GetStream().WriteAsync(buffer, cancellationToken);
await sendSemaphore.WaitAsync(cancellationToken);
try
{
await writeStream.WriteAsync(buffer, cancellationToken);
await writeStream.FlushAsync(cancellationToken);
}
finally
{
sendSemaphore.Release();
}
}
/// <summary>
@ -148,12 +172,117 @@ namespace MinecraftClient.Protocol.Handlers
{
try
{
c.Close();
encryptedStream?.Dispose();
client.Close();
}
catch (SocketException) { }
catch (System.IO.IOException) { }
catch (NullReferenceException) { }
catch (ObjectDisposedException) { }
}
private int ReadNextVarIntRaw()
{
int value = 0;
int position = 0;
while (true)
{
byte current = ReadByteRAW();
value |= (current & 0x7F) << position++ * 7;
if (position > 5)
throw new OverflowException("VarInt too big");
if ((current & 0x80) != 0x80)
return value;
}
}
private async Task<int> ReadNextVarIntRawAsync(CancellationToken cancellationToken)
{
int value = 0;
int position = 0;
while (true)
{
byte current = await ReadByteRAWAsync(cancellationToken);
value |= (current & 0x7F) << position++ * 7;
if (position > 5)
throw new OverflowException("VarInt too big");
if ((current & 0x80) != 0x80)
return value;
}
}
private static byte[] ReadPacketPayload(PacketReadStream packetStream, int compressionThreshold)
{
if (compressionThreshold >= 0)
{
int uncompressedLength = ReadNextVarIntRaw(packetStream);
if (uncompressedLength > 0)
{
using ZLibStream zlibStream = new(packetStream, CompressionMode.Decompress, leaveOpen: true);
byte[] payload = GC.AllocateUninitializedArray<byte>(uncompressedLength);
zlibStream.ReadExactly(payload);
return payload;
}
}
return packetStream.ReadRemaining();
}
private static async Task<byte[]> ReadPacketPayloadAsync(PacketReadStream packetStream, int compressionThreshold, CancellationToken cancellationToken)
{
if (compressionThreshold >= 0)
{
int uncompressedLength = await ReadNextVarIntRawAsync(packetStream, cancellationToken);
if (uncompressedLength > 0)
{
await using ZLibStream zlibStream = new(packetStream, CompressionMode.Decompress, leaveOpen: true);
byte[] payload = GC.AllocateUninitializedArray<byte>(uncompressedLength);
await zlibStream.ReadExactlyAsync(payload.AsMemory(0, uncompressedLength), cancellationToken);
return payload;
}
}
return await packetStream.ReadRemainingAsync(cancellationToken);
}
private static int ReadNextVarIntRaw(Stream stream)
{
int value = 0;
int position = 0;
while (true)
{
int current = stream.ReadByte();
if (current < 0)
throw new IOException("Connection closed.");
value |= (current & 0x7F) << position++ * 7;
if (position > 5)
throw new OverflowException("VarInt too big");
if ((current & 0x80) != 0x80)
return value;
}
}
private static async Task<int> ReadNextVarIntRawAsync(Stream stream, CancellationToken cancellationToken)
{
byte[] buffer = new byte[1];
int value = 0;
int position = 0;
while (true)
{
await stream.ReadExactlyAsync(buffer.AsMemory(0, 1), cancellationToken);
byte current = buffer[0];
value |= (current & 0x7F) << position++ * 7;
if (position > 5)
throw new OverflowException("VarInt too big");
if ((current & 0x80) != 0x80)
return value;
}
}
}
}

View file

@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient.Protocol.PacketPipeline;
internal sealed class PacketReadStream : Stream
{
private const int DrainBufferSize = 4096;
private readonly Stream baseStream;
private readonly byte[] singleByteBuffer = new byte[1];
private int remainingLength;
public PacketReadStream(Stream baseStream, int packetLength)
{
ArgumentNullException.ThrowIfNull(baseStream);
if (packetLength < 0)
throw new ArgumentOutOfRangeException(nameof(packetLength));
this.baseStream = baseStream;
remainingLength = packetLength;
}
public int RemainingLength => remainingLength;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
if (remainingLength == 0)
return 0;
int readLength = Math.Min(count, remainingLength);
int read = baseStream.Read(buffer, offset, readLength);
remainingLength -= read;
return read;
}
public override int Read(Span<byte> buffer)
{
if (remainingLength == 0)
return 0;
int readLength = Math.Min(buffer.Length, remainingLength);
int read = baseStream.Read(buffer[..readLength]);
remainingLength -= read;
return read;
}
public override int ReadByte()
{
if (remainingLength == 0)
return -1;
int value = baseStream.ReadByte();
if (value == -1)
return -1;
remainingLength--;
return value;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (remainingLength == 0)
return 0;
int readLength = Math.Min(buffer.Length, remainingLength);
int read = await baseStream.ReadAsync(buffer[..readLength], cancellationToken);
remainingLength -= read;
return read;
}
public new async ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (buffer.Length > remainingLength)
throw new OverflowException("Reached the end of the packet.");
await baseStream.ReadExactlyAsync(buffer, cancellationToken);
remainingLength -= buffer.Length;
}
public new void ReadExactly(Span<byte> buffer)
{
if (buffer.Length > remainingLength)
throw new OverflowException("Reached the end of the packet.");
baseStream.ReadExactly(buffer);
remainingLength -= buffer.Length;
}
public byte[] ReadRemaining()
{
if (remainingLength == 0)
return [];
byte[] buffer = GC.AllocateUninitializedArray<byte>(remainingLength);
ReadExactly(buffer);
return buffer;
}
public async Task<byte[]> ReadRemainingAsync(CancellationToken cancellationToken = default)
{
if (remainingLength == 0)
return [];
byte[] buffer = GC.AllocateUninitializedArray<byte>(remainingLength);
await ReadExactlyAsync(buffer, cancellationToken);
return buffer;
}
public void DrainRemaining()
{
if (remainingLength == 0)
return;
byte[] buffer = GC.AllocateUninitializedArray<byte>(Math.Min(DrainBufferSize, remainingLength));
while (remainingLength > 0)
{
int read = baseStream.Read(buffer, 0, Math.Min(buffer.Length, remainingLength));
if (read <= 0)
throw new EndOfStreamException("Connection closed while draining packet data.");
remainingLength -= read;
}
}
public async ValueTask DrainRemainingAsync(CancellationToken cancellationToken = default)
{
if (remainingLength == 0)
return;
byte[] buffer = GC.AllocateUninitializedArray<byte>(Math.Min(DrainBufferSize, remainingLength));
while (remainingLength > 0)
{
int read = await baseStream.ReadAsync(buffer.AsMemory(0, Math.Min(buffer.Length, remainingLength)), cancellationToken);
if (read <= 0)
throw new EndOfStreamException("Connection closed while draining packet data.");
remainingLength -= read;
}
}
public override void Flush()
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
protected override void Dispose(bool disposing)
{
if (disposing && remainingLength > 0)
{
try
{
DrainRemaining();
}
catch (IOException) { }
catch (ObjectDisposedException) { }
}
base.Dispose(disposing);
}
public override async ValueTask DisposeAsync()
{
if (remainingLength > 0)
{
try
{
await DrainRemainingAsync();
}
catch (IOException) { }
catch (ObjectDisposedException) { }
}
await base.DisposeAsync();
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Threading;
using Consolonia;
@ -48,12 +49,11 @@ namespace MinecraftClient.Tui
Dispatcher.UIThread.Post(() => view.HandleCtrlC());
};
new Thread(() =>
_ = Task.Run(() =>
{
_viewReady.Wait();
ContinueMccStartup(args);
})
{ Name = "MCC-Main", IsBackground = true }.Start();
});
AppBuilder builder = AppBuilder.Configure<MccTuiApp>()
.UseConsolonia()
@ -206,32 +206,49 @@ namespace MinecraftClient.Tui
}
public string RequestImmediateInput()
{
return RequestImmediateInputAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public Task<string> RequestImmediateInputAsync(CancellationToken cancellationToken)
{
if (_shutdownRequested)
{
Thread.Sleep(Timeout.Infinite);
return string.Empty;
return Task.FromCanceled<string>(cancellationToken.CanBeCanceled
? cancellationToken
: new CancellationToken(canceled: true));
}
var mre = new ManualResetEventSlim(false);
string? result = null;
TaskCompletionSource<string> completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
void Handler(object? sender, string e)
{
result = e;
mre.Set();
MessageReceived -= Handler;
completion.TrySetResult(e);
}
MessageReceived += Handler;
mre.Wait();
MessageReceived -= Handler;
return result ?? string.Empty;
if (cancellationToken.CanBeCanceled)
{
cancellationToken.Register(() =>
{
MessageReceived -= Handler;
completion.TrySetCanceled(cancellationToken);
});
}
return completion.Task;
}
public string? ReadPassword()
{
return RequestImmediateInput();
return ReadPasswordAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public async Task<string?> ReadPasswordAsync(CancellationToken cancellationToken)
{
return await RequestImmediateInputAsync(cancellationToken);
}
public void ClearInputBuffer()
@ -267,11 +284,11 @@ namespace MinecraftClient.Tui
Dispatcher.UIThread.Post(() => lifetime.Shutdown());
}
new Thread(() =>
_ = Task.Run(async () =>
{
Thread.Sleep(1000);
await Task.Delay(1000);
Environment.Exit(0);
}) { Name = "TUI-Exit-Guard", IsBackground = true }.Start();
});
}
private volatile bool _shutdownRequested;