Example #1
0
        public async Task Listen(IByteReceiver byteReceiver,
                                 IStreamFactory streamFactory,
                                 IScheduler scheduler,
                                 string destinationPath = "",
                                 CancellationToken cancellationToken = default)
        {
            var basePath = string.IsNullOrWhiteSpace(destinationPath)
                ? ""
                : destinationPath;

            if (basePath != "")
            {
                Directory.CreateDirectory(basePath); //does not throw an exception if it does not exist
            }

            var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            var token = cancellationTokenSource.Token;

            var packetBufferObservable = new Subject <byte[]>();
            var protoMessageObservable = packetBufferObservable
                                         .ObserveOn(scheduler)
                                         .Select(bytes =>
            {
                ProtoMessage message;
                using (var memoryStream = new MemoryStream(bytes))
                {
                    var protoMessage = Serializer.Deserialize <ProtoMessage>(memoryStream);
                    message          = protoMessage;
                }

                return(message);
            });

            var headerCache  = new ConcurrentDictionary <Guid, Header>();
            var payloadCache = new ConcurrentDictionary <Guid, List <ProtoMessage> >();

            var fileWriteRequestSubject = new Subject <FileWriteWrapper>();

            var headerObservable = protoMessageObservable
                                   .Where(message => !string.IsNullOrWhiteSpace(message.FileName) && message.PayloadCount.HasValue);

            var payloadObservable = protoMessageObservable
                                    .Where(message => message.PayloadIndex.HasValue && message.Payload != null);

            string GetFileName(ProtoMessage header)
            {
                return($"{System.DateTime.Now:HH-mm-ss-ffff}.{header.FileName}");
            }

            Header ConvertToHeader(ProtoMessage header)
            {
                return(new Header
                {
                    BroadcastId = header.GetBroadcastId(), FileName = GetFileName(header),
                    PayloadCount = header.PayloadCount.Value, PayloadMaxBytes = header.PayloadMaxSize.Value
                });
            }

            void HandleCachedPayloads(Guid guid, Header header1)
            {
                if (payloadCache.TryRemove(guid, out var payloads))
                {
                    var p = payloads.ToArray(); //todo:figure out why this bombed - List changed exception
                    payloads.Clear();
                    foreach (var payload in p)
                    {
                        fileWriteRequestSubject.OnNext(new FileWriteWrapper()
                        {
                            Header = header1, Payload = payload
                        });
                    }
                }
            }

            headerObservable
            .ObserveOn(scheduler)
            .Subscribe(protoHeader => headerCache.AddOrUpdate(protoHeader.GetBroadcastId(),
                                                              bcid =>
            {
                //todo: decide if header.payloadCount X headerMaxPayloadSize is too much
                var header = ConvertToHeader(protoHeader);
                HandleCachedPayloads(bcid, header);

                return(header);
            },
                                                              (bcid, header) =>
            {
                HandleCachedPayloads(bcid, header);
                return(header);
            })
                       );

            var md5 = MD5.Create();

            bool CheckHash(ProtoMessage payload)
            {
                var expected = payload.Hash;

                if (expected == null || expected.Length == 0)
                {
                    return(false);
                }

                var actual = md5.ComputeHash(payload.Payload);

                if (actual.Length == 0 || expected.Length != actual.Length)
                {
                    return(false);
                }

                for (var index = 0; index < expected.Length; index++)
                {
                    var expectedByte = expected[index];
                    var actualByte   = actual[index];
                    if (expectedByte != actualByte)
                    {
                        return(false);
                    }
                }

                return(true);
            }

            payloadObservable
            .ObserveOn(scheduler)
            .Subscribe(payload =>
            {
                if (!CheckHash(payload))
                {
                    throw new InvalidDataException("Hash check failed");
                    //todo: return error statistics to the caller
                }
                if (headerCache.TryGetValue(payload.GetBroadcastId(), out var header))
                {
                    fileWriteRequestSubject.OnNext(new FileWriteWrapper()
                    {
                        Header = header, Payload = payload
                    });
                }
                else
                {
                    payloadCache.AddOrUpdate(payload.GetBroadcastId(),
                                             bcid =>
                    {
                        var list = new List <ProtoMessage>();
                        list.Add(payload);
                        return(list);
                    },
                                             (bcid, list) =>
                    {
                        list.Add(payload);
                        return(list);
                    });
                }
            });

            var writeCompleteObservable = fileWriteRequestSubject
                                          //.ObserveOn(observableScheduler)
                                          .Select(writeRequest =>
            {
                if (writeRequest.Payload.Payload.Length > writeRequest.Header.PayloadMaxBytes)
                {
                    throw new ArgumentException("Payload exceeds max byte length");
                }
                return(Observable.FromAsync(async() =>
                {
                    var fullPath = Path.Combine(basePath, writeRequest.Header.FileName);
                    using var writer = streamFactory.CreateWriter(fullPath);
                    var writeIndex = writeRequest.Payload.PayloadIndex.Value *
                                     writeRequest.Header.PayloadMaxBytes;
                    await writer.Write(writeIndex, writeRequest.Payload.Payload, token).ConfigureAwait(false);
                    return writeRequest.Header;
                }));
            }).Merge(1);

            var fileWriteObservable = writeCompleteObservable
                                      .ObserveOn(scheduler)
                                      .GroupBy(w => w.BroadcastId);

            const int fileCompleteTimeout   = 10;
            var       fileTimeout           = TimeSpan.FromSeconds(fileCompleteTimeout);
            var       fileStoppedObservable = CreateTimeoutObservable(fileTimeout, fileWriteObservable, scheduler);

            var fileStoppedSub = fileStoppedObservable.Subscribe(header =>
            {
                OnBroadcastEnded(new BroadcastResult(header.BroadcastId, header.FileName));
            });

            token.Register(() => fileStoppedSub.Dispose());

            void OnBytesReceived(object sender, IBytesReceived bytesReceived)
            {
                packetBufferObservable.OnNext(bytesReceived.Bytes);
            }

            byteReceiver.BytesReceived += OnBytesReceived;
            cancellationToken.Register(() => { byteReceiver.BytesReceived -= OnBytesReceived; });
            await byteReceiver.Listen(cancellationToken);
        }