Fix issue with actual RpcResult in MsgContainer ; Parallelize upload of file parts

This commit is contained in:
Wizou 2021-08-13 07:06:44 +02:00
parent 897b61747a
commit e01caba162
6 changed files with 151 additions and 110 deletions

View file

@ -37,7 +37,7 @@ That file path is configurable, and under various circumstances (changing user o
# Non-interactive configuration # Non-interactive configuration
Your next step will probably be to provide a configuration to the client so that the required elements (in bold above) are not prompted through the Console but answered by your program. Your next step will probably be to provide a configuration to the client so that the required elements (in bold above) are not prompted through the Console but answered by your program.
For that you need to write a method that will provide the answer, and pass it on the constructor: To do this, you need to write a method that will provide the answers, and pass it on the constructor:
```csharp ```csharp
static string Config(string what) static string Config(string what)
{ {
@ -57,7 +57,7 @@ There are other configuration items that are queried to your method but returnin
The configuration items shown above are the only ones that have no default values and are required to be provided by your method. The configuration items shown above are the only ones that have no default values and are required to be provided by your method.
The constructor also takes another optional delegate parameter that will be called for any other Update and other information/status/service messages that Telegram sends unsollicited, independently of your API requests. The constructor also takes another optional delegate parameter that will be called for any other Update or other information/status/service messages that Telegram sends unsollicited, independently of your API requests.
Finally, if you want to redirect the library logs to your logger instead of the Console, you can install a delegate in the `WTelegram.Helpers.Log` static property. Finally, if you want to redirect the library logs to your logger instead of the Console, you can install a delegate in the `WTelegram.Helpers.Log` static property.
Its `int` argument is the log severity, compatible with the classic [LogLevel enum](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel) Its `int` argument is the log severity, compatible with the classic [LogLevel enum](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel)
@ -104,7 +104,7 @@ Beyond the TL async methods, the Client class offers a few other methods to simp
The other configuration items that you can override include: **session_pathname, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code** The other configuration items that you can override include: **session_pathname, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code**
This library requires .NET 5.0 minimum. For the moment, this library requires .NET 5.0 minimum.
# Development status # Development status
The library is already well usable for many scenarios involving automated steps based on API requests/responses. The library is already well usable for many scenarios involving automated steps based on API requests/responses.
@ -115,7 +115,7 @@ Here are the main expected developments:
- [x] Improve code Generator (import of TL-schema JSONs) - [x] Improve code Generator (import of TL-schema JSONs)
- [x] Nuget deployment & public CI feed - [x] Nuget deployment & public CI feed
- [x] Convert API functions classes to real methods and serialize structures without using Reflection - [x] Convert API functions classes to real methods and serialize structures without using Reflection
- [ ] Separate task/thread for reading/handling update messages independently from CallAsync - [x] Separate background task for reading/handling update messages independently
- [x] Support MTProto 2.0 - [x] Support MTProto 2.0
- [x] Support users with 2FA enabled - [x] Support users with 2FA enabled
- [ ] Support secret chats end-to-end encryption & PFS - [ ] Support secret chats end-to-end encryption & PFS

2
ci.yml
View file

@ -2,7 +2,7 @@ pr: none
trigger: trigger:
- master - master
name: 0.7.5-alpha.$(Rev:r) name: 0.8.1-alpha.$(Rev:r)
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest

View file

@ -1,7 +1,7 @@
pr: none pr: none
trigger: none trigger: none
name: 0.7.$(Rev:r) name: 0.8.$(Rev:r)
pool: pool:
vmImage: ubuntu-latest vmImage: ubuntu-latest

View file

@ -249,6 +249,43 @@ namespace WTelegram
return msgId; return msgId;
} }
private static async Task<int> FullReadAsync(Stream stream, byte[] buffer, int length, CancellationToken ct = default)
{
for (int offset = 0; offset != length;)
{
var read = await stream.ReadAsync(buffer.AsMemory(offset, length - offset), ct);
if (read == 0) return offset;
offset += read;
}
return length;
}
private async Task<byte[]> RecvFrameAsync(CancellationToken ct)
{
byte[] frame = new byte[8];
if (await FullReadAsync(_networkStream, frame, 8, ct) != 8)
throw new ApplicationException("Could not read frame prefix : Connection shut down");
int length = BinaryPrimitives.ReadInt32LittleEndian(frame) - 12;
if (length <= 0 || length >= 0x10000)
throw new ApplicationException("Invalid frame_len");
int seqno = BinaryPrimitives.ReadInt32LittleEndian(frame.AsSpan(4));
if (seqno != _frame_seqRx++)
{
Trace.TraceWarning($"Unexpected frame_seq received: {seqno} instead of {_frame_seqRx}");
_frame_seqRx = seqno + 1;
}
var payload = new byte[length];
if (await FullReadAsync(_networkStream, payload, length, ct) != length)
throw new ApplicationException("Could not read frame data : Connection shut down");
uint crc32 = Force.Crc32.Crc32Algorithm.Compute(frame, 0, 8);
crc32 = Force.Crc32.Crc32Algorithm.Append(crc32, payload);
if (await FullReadAsync(_networkStream, frame, 4, ct) != 4)
throw new ApplicationException("Could not read frame CRC : Connection shut down");
if (crc32 != BinaryPrimitives.ReadUInt32LittleEndian(frame))
throw new ApplicationException("Invalid envelope CRC32");
return payload;
}
internal async Task<ITLObject> RecvInternalAsync(CancellationToken ct) internal async Task<ITLObject> RecvInternalAsync(CancellationToken ct)
{ {
var data = await RecvFrameAsync(ct); var data = await RecvFrameAsync(ct);
@ -297,8 +334,8 @@ namespace WTelegram
{ {
Helpers.Log(3, $"Server salt has changed: {_session.Salt:X8} -> {serverSalt:X8}"); Helpers.Log(3, $"Server salt has changed: {_session.Salt:X8} -> {serverSalt:X8}");
_session.Salt = serverSalt; _session.Salt = serverSalt;
if (++_unexpectedSaltChange >= 10) if (++_unexpectedSaltChange >= 30)
throw new ApplicationException($"Server salt changed unexpectedly more than 10 times during this run"); throw new ApplicationException($"Server salt changed unexpectedly more than 30 times during this session");
} }
if (sessionId != _session.Id) throw new ApplicationException($"Unexpected session ID {_session.Id} != {_session.Id}"); if (sessionId != _session.Id) throw new ApplicationException($"Unexpected session ID {_session.Id} != {_session.Id}");
if ((msgId & 1) == 0) throw new ApplicationException($"Invalid server msgId {msgId}"); if ((msgId & 1) == 0) throw new ApplicationException($"Invalid server msgId {msgId}");
@ -314,11 +351,23 @@ namespace WTelegram
if (!data.AsSpan(8, 16).SequenceEqual(_sha256.Hash.AsSpan(8, 16))) if (!data.AsSpan(8, 16).SequenceEqual(_sha256.Hash.AsSpan(8, 16)))
throw new ApplicationException($"Mismatch between MsgKey & decrypted SHA1"); throw new ApplicationException($"Mismatch between MsgKey & decrypted SHA1");
#endif #endif
var obj = reader.ReadTLObject(type => type == typeof(RpcResult)); var ctorNb = reader.ReadUInt32();
Helpers.Log(1, $"Receiving {obj.GetType().Name,-50} {_session.MsgIdToStamp(msgId):u} {((seqno & 1) != 0 ? "" : "(svc)")} {((msgId & 2) == 0 ? "" : "NAR")}"); if (ctorNb == Schema.MsgContainer)
if (obj is RpcResult rpcResult) {
DeserializeRpcResult(reader, rpcResult); // necessary hack because some RPC return bare types like bool or int[] Helpers.Log(1, $"Receiving {"MsgContainer",-50} {_session.MsgIdToStamp(msgId):u} (svc)");
return obj; return ReadMsgContainer(reader);
}
else if (ctorNb == Schema.RpcResult)
{
Helpers.Log(1, $"Receiving {"RpcResult",-50} {_session.MsgIdToStamp(msgId):u}");
return ReadRpcResult(reader);
}
else
{
var obj = reader.ReadTLObject(ctorNb);
Helpers.Log(1, $"Receiving {obj.GetType().Name,-50} {_session.MsgIdToStamp(msgId):u} {((seqno & 1) != 0 ? "" : "(svc)")} {((msgId & 2) == 0 ? "" : "NAR")}");
return obj;
}
} }
static string TransportError(int error_code) => error_code switch static string TransportError(int error_code) => error_code switch
@ -329,65 +378,66 @@ namespace WTelegram
}; };
} }
private async Task<byte[]> RecvFrameAsync(CancellationToken ct) internal MsgContainer ReadMsgContainer(BinaryReader reader)
{ {
byte[] frame = new byte[8]; int count = reader.ReadInt32();
if (await FullReadAsync(_networkStream, frame, 8, ct) != 8) var array = new _Message[count];
throw new ApplicationException("Could not read frame prefix : Connection shut down"); for (int i = 0; i < count; i++)
int length = BinaryPrimitives.ReadInt32LittleEndian(frame) - 12;
if (length <= 0 || length >= 0x10000)
throw new ApplicationException("Invalid frame_len");
int seqno = BinaryPrimitives.ReadInt32LittleEndian(frame.AsSpan(4));
if (seqno != _frame_seqRx++)
{ {
Trace.TraceWarning($"Unexpected frame_seq received: {seqno} instead of {_frame_seqRx}"); var msg = array[i] = new _Message
_frame_seqRx = seqno + 1; {
msg_id = reader.ReadInt64(),
seqno = reader.ReadInt32(),
bytes = reader.ReadInt32(),
};
if ((msg.seqno & 1) != 0) lock(_msgsToAck) _msgsToAck.Add(msg.msg_id);
var pos = reader.BaseStream.Position;
try
{
var ctorNb = reader.ReadUInt32();
if (ctorNb == Schema.RpcResult)
{
Helpers.Log(1, $" → {"RpcResult",-48} {_session.MsgIdToStamp(msg.msg_id):u}");
msg.body = ReadRpcResult(reader);
}
else
{
var obj = msg.body = reader.ReadTLObject(ctorNb);
Helpers.Log(1, $" → {obj.GetType().Name,-48} {_session.MsgIdToStamp(msg.msg_id):u} {((msg.seqno & 1) != 0 ? "" : "(svc)")} {((msg.msg_id & 2) == 0 ? "" : "NAR")}");
}
}
catch (Exception ex)
{
Helpers.Log(4, "While deserializing vector<%Message>: " + ex.ToString());
}
reader.BaseStream.Position = pos + array[i].bytes;
} }
var payload = new byte[length]; return new MsgContainer { messages = array };
if (await FullReadAsync(_networkStream, payload, length, ct) != length)
throw new ApplicationException("Could not read frame data : Connection shut down");
uint crc32 = Force.Crc32.Crc32Algorithm.Compute(frame, 0, 8);
crc32 = Force.Crc32.Crc32Algorithm.Append(crc32, payload);
if (await FullReadAsync(_networkStream, frame, 4, ct) != 4)
throw new ApplicationException("Could not read frame CRC : Connection shut down");
if (crc32 != BinaryPrimitives.ReadUInt32LittleEndian(frame))
throw new ApplicationException("Invalid envelope CRC32");
return payload;
} }
private static async Task<int> FullReadAsync(Stream stream, byte[] buffer, int length, CancellationToken ct = default) private RpcResult ReadRpcResult(BinaryReader reader)
{ {
for (int offset = 0; offset != length;) long msgId = reader.ReadInt64();
{
var read = await stream.ReadAsync(buffer.AsMemory(offset, length - offset), ct);
if (read == 0) return offset;
offset += read;
}
return length;
}
private bool DeserializeRpcResult(BinaryReader reader, RpcResult rpcResult)
{
var msgId = rpcResult.req_msg_id = reader.ReadInt64();
(Type type, TaskCompletionSource<object> tcs) request; (Type type, TaskCompletionSource<object> tcs) request;
lock (_pendingRequests) lock (_pendingRequests)
if (_pendingRequests.TryGetValue(msgId, out request)) if (_pendingRequests.TryGetValue(msgId, out request))
_pendingRequests.Remove(msgId); _pendingRequests.Remove(msgId);
if (request.type != null) if (request.type != null)
{ {
rpcResult.result = reader.ReadTLValue(request.type); var result = reader.ReadTLValue(request.type);
Helpers.Log(1, $" result → {request.type.Name,-48} #{(short)msgId.GetHashCode():X4}"); Helpers.Log(1, $" → {result?.GetType().Name,-47} #{(short)msgId.GetHashCode():X4}");
Task.Run(() => request.tcs.SetResult(rpcResult.result)); // to avoid deadlock, see https://blog.stephencleary.com/2012/12/dont-block-in-asynchronous-code.html Task.Run(() => request.tcs.SetResult(result)); // to avoid deadlock, see https://blog.stephencleary.com/2012/12/dont-block-in-asynchronous-code.html
return new RpcResult { req_msg_id = msgId, result = result };
} }
else else
{ {
rpcResult.result = reader.ReadTLObject(); var result = reader.ReadTLObject();
if (_session.MsgIdToStamp(msgId) >= _session.SessionStart) if (_session.MsgIdToStamp(msgId) >= _session.SessionStart)
Helpers.Log(4, $" result → {rpcResult.result?.GetType().Name,-48}) for unknown msgId #{(short)msgId.GetHashCode():X4}"); Helpers.Log(4, $" → {result?.GetType().Name,-47} for unknown msgId #{(short)msgId.GetHashCode():X4}");
else else
Helpers.Log(1, $" result → {rpcResult.result?.GetType().Name,-48} for past msgId #{(short)msgId.GetHashCode():X4}"); Helpers.Log(1, $" → {result?.GetType().Name,-47} for past msgId #{(short)msgId.GetHashCode():X4}");
return new RpcResult { req_msg_id = msgId, result = result };
} }
return true;
} }
public class RpcException : Exception public class RpcException : Exception
@ -481,13 +531,8 @@ namespace WTelegram
{ {
case MsgContainer container: case MsgContainer container:
foreach (var msg in container.messages) foreach (var msg in container.messages)
{ if (msg.body != null)
var typeName = msg.body?.GetType().Name; await HandleMessageAsync(msg.body);
if (typeName == "RpcResult") typeName += $" ({((RpcResult)msg.body).result.GetType().Name})";
Helpers.Log(1, $" → {typeName,-48} {_session.MsgIdToStamp(msg.msg_id):u} {((msg.seqno & 1) != 0 ? "" : "(svc)")} {((msg.msg_id & 2) == 0 ? "" : "NAR")}");
if ((msg.seqno & 1) != 0) lock (_msgsToAck) _msgsToAck.Add(msg.msg_id);
if (msg.body != null) await HandleMessageAsync(msg.body);
}
break; break;
case BadServerSalt badServerSalt: case BadServerSalt badServerSalt:
_session.Salt = badServerSalt.new_server_salt; _session.Salt = badServerSalt.new_server_salt;
@ -506,8 +551,7 @@ namespace WTelegram
Helpers.Log(3, $"BadMsgNotification {badMsgNotification.error_code} for msg #{(short)badMsgNotification.bad_msg_id.GetHashCode():X4}"); Helpers.Log(3, $"BadMsgNotification {badMsgNotification.error_code} for msg #{(short)badMsgNotification.bad_msg_id.GetHashCode():X4}");
break; break;
case RpcResult rpcResult: case RpcResult rpcResult:
//if (_session.MsgIdToStamp(rpcResult.req_msg_id) >= _session.SessionStart) break; // wake-up of waiting task was already done in ReadRpcResult
break; // tcs wake up was already done in DeserializeRpcResult
default: default:
if (_rawRequest != null) if (_rawRequest != null)
{ {
@ -575,23 +619,47 @@ namespace WTelegram
const int partSize = 512 * 1024; const int partSize = 512 * 1024;
int file_total_parts = (int)((length - 1) / partSize) + 1; int file_total_parts = (int)((length - 1) / partSize) + 1;
long file_id = Helpers.RandomLong(); long file_id = Helpers.RandomLong();
var bytes = new byte[Math.Min(partSize, length)];
int file_part = 0, read; int file_part = 0, read;
for (long bytesLeft = length; bytesLeft != 0; file_part++) const int ParallelSends = 10;
var semaphore = new SemaphoreSlim(ParallelSends);
var tasks = new Dictionary<int, Task>();
bool abort = false;
for (long bytesLeft = length; !abort && bytesLeft != 0; file_part++)
{ {
//TODO: parallelize several parts sending through a N-semaphore? (needs a reactor first) var bytes = new byte[Math.Min(partSize, bytesLeft)];
read = await FullReadAsync(stream, bytes, (int)Math.Min(partSize, bytesLeft)); read = await FullReadAsync(stream, bytes, bytes.Length);
if (isBig) await semaphore.WaitAsync();
await Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes); var task = SavePart(file_part, bytes);
else lock (tasks) tasks[file_part] = task;
{ if (!isBig)
await Upload_SaveFilePart(file_id, file_part, bytes);
md5.TransformBlock(bytes, 0, read, null, 0); md5.TransformBlock(bytes, 0, read, null, 0);
}
bytesLeft -= read; bytesLeft -= read;
if (read < partSize && bytesLeft != 0) throw new ApplicationException($"Failed to fully read stream ({read},{bytesLeft})"); if (read < partSize && bytesLeft != 0) throw new ApplicationException($"Failed to fully read stream ({read},{bytesLeft})");
async Task SavePart(int file_part, byte[] bytes)
{
try
{
if (isBig)
await Upload_SaveBigFilePart(file_id, file_part, file_total_parts, bytes);
else
await Upload_SaveFilePart(file_id, file_part, bytes);
lock (tasks) tasks.Remove(file_part);
}
catch (Exception)
{
abort = true;
}
finally
{
semaphore.Release();
}
}
} }
if (!isBig) md5.TransformFinalBlock(bytes, 0, 0); for (int i = 0; i < ParallelSends; i++)
await semaphore.WaitAsync(); // wait for all the remaining parts to be sent
await Task.WhenAll(tasks.Values); // propagate any task exception (tasks should be empty on success)
if (!isBig) md5.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename } return isBig ? new InputFileBig { id = file_id, parts = file_total_parts, name = filename }
: new InputFile { id = file_id, parts = file_total_parts, name = filename, md5_checksum = md5.Hash }; : new InputFile { id = file_id, parts = file_total_parts, name = filename, md5_checksum = md5.Hash };
} }

View file

@ -7,8 +7,10 @@ namespace TL
static partial class Schema static partial class Schema
{ {
public const int Layer = 121; // fetched 10/08/2021 11:46:24 public const int Layer = 121; // fetched 10/08/2021 11:46:24
public const int VectorCtor = 0x1CB5C415; public const uint VectorCtor = 0x1CB5C415;
public const int NullCtor = 0x56730BCC; public const uint NullCtor = 0x56730BCC;
public const uint RpcResult = 0xF35C6D01;
public const uint MsgContainer = 0x73F1F8DC;
internal readonly static Dictionary<uint, Type> Table = new() internal readonly static Dictionary<uint, Type> Table = new()
{ {

View file

@ -51,14 +51,13 @@ namespace TL
} }
} }
internal static ITLObject ReadTLObject(this BinaryReader reader, Func<Type, bool> notifyType = null) internal static ITLObject ReadTLObject(this BinaryReader reader, uint ctorNb = 0)
{ {
var ctorNb = reader.ReadUInt32(); if (ctorNb == 0) ctorNb = reader.ReadUInt32();
if (ctorNb == NullCtor) return null; if (ctorNb == NullCtor) return null;
if (!Table.TryGetValue(ctorNb, out var type)) if (!Table.TryGetValue(ctorNb, out var type))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}"); throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
var obj = Activator.CreateInstance(type); var obj = Activator.CreateInstance(type);
if (notifyType?.Invoke(type) == true) return (ITLObject) obj;
var fields = obj.GetType().GetFields().GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g); var fields = obj.GetType().GetFields().GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
int flags = 0; int flags = 0;
IfFlagAttribute ifFlag; IfFlagAttribute ifFlag;
@ -129,8 +128,6 @@ namespace TL
{ {
if (type == typeof(byte[])) if (type == typeof(byte[]))
return reader.ReadTLBytes(); return reader.ReadTLBytes();
else if (type == typeof(_Message[]))
return reader.ReadTLMessages();
else else
return reader.ReadTLVector(type); return reader.ReadTLVector(type);
} }
@ -139,7 +136,7 @@ namespace TL
else if (type == typeof(Int256)) else if (type == typeof(Int256))
return new Int256(reader); return new Int256(reader);
else else
return ReadTLObject(reader); return reader.ReadTLObject();
default: default:
ShouldntBeHere(); ShouldntBeHere();
return null; return null;
@ -158,7 +155,7 @@ namespace TL
internal static Array ReadTLVector(this BinaryReader reader, Type type) internal static Array ReadTLVector(this BinaryReader reader, Type type)
{ {
var ctorNb = reader.ReadInt32(); var ctorNb = reader.ReadUInt32();
if (ctorNb != VectorCtor) throw new ApplicationException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}"); if (ctorNb != VectorCtor) throw new ApplicationException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}");
var elementType = type.GetElementType(); var elementType = type.GetElementType();
int count = reader.ReadInt32(); int count = reader.ReadInt32();
@ -225,36 +222,10 @@ namespace TL
writer.Write(0); // null arrays are serialized as empty writer.Write(0); // null arrays are serialized as empty
} }
internal static _Message[] ReadTLMessages(this BinaryReader reader)
{
int count = reader.ReadInt32();
var array = new _Message[count];
for (int i = 0; i < count; i++)
{
array[i] = new _Message
{
msg_id = reader.ReadInt64(),
seqno = reader.ReadInt32(),
bytes = reader.ReadInt32(),
};
var pos = reader.BaseStream.Position;
try
{
array[i].body = reader.ReadTLObject();
}
catch (Exception ex)
{
Helpers.Log(4, "While deserializing vector<%Message>: " + ex.ToString());
}
reader.BaseStream.Position = pos + array[i].bytes;
}
return array;
}
internal static ITLObject UnzipPacket(GzipPacked obj) internal static ITLObject UnzipPacket(GzipPacked obj)
{ {
using var reader = new BinaryReader(new GZipStream(new MemoryStream(obj.packed_data), CompressionMode.Decompress)); using var reader = new BinaryReader(new GZipStream(new MemoryStream(obj.packed_data), CompressionMode.Decompress));
var result = ReadTLObject(reader); var result = reader.ReadTLObject();
return result; return result;
} }