From 5450ff498a9cbb379d0648bc19352d97d3ae21c3 Mon Sep 17 00:00:00 2001 From: Regalis Date: Tue, 7 Mar 2017 20:03:40 +0200 Subject: [PATCH] Progress on file transfers (class for receiving files, FileSender can transfer multiple files to the same recipient simultaneously) --- Subsurface/Barotrauma.csproj | 1 + .../Networking/FileTransfer/FileReceiver.cs | 343 ++++++++++++++++++ .../Networking/FileTransfer/FileSender.cs | 48 ++- Subsurface/Source/Networking/GameServer.cs | 17 +- Subsurface/Source/Networking/NetworkMember.cs | 2 + 5 files changed, 403 insertions(+), 8 deletions(-) create mode 100644 Subsurface/Source/Networking/FileTransfer/FileReceiver.cs diff --git a/Subsurface/Barotrauma.csproj b/Subsurface/Barotrauma.csproj index e50021540..058e5ecea 100644 --- a/Subsurface/Barotrauma.csproj +++ b/Subsurface/Barotrauma.csproj @@ -155,6 +155,7 @@ + diff --git a/Subsurface/Source/Networking/FileTransfer/FileReceiver.cs b/Subsurface/Source/Networking/FileTransfer/FileReceiver.cs new file mode 100644 index 000000000..82f1241b2 --- /dev/null +++ b/Subsurface/Source/Networking/FileTransfer/FileReceiver.cs @@ -0,0 +1,343 @@ +using Lidgren.Network; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; + +namespace Barotrauma.Networking +{ + class FileReceiver + { + public class FileTransferIn : IDisposable + { + public delegate void OnFinishedDelegate(FileTransferIn fileStreamReceiver); + public OnFinishedDelegate OnFinished; + + public string FileName + { + get; + private set; + } + + public string FilePath + { + get; + private set; + } + + public ulong FileSize + { + get; + private set; + } + + public ulong Received + { + get; + private set; + } + + public FileTransferType FileType + { + get; + private set; + } + + public FileTransferStatus Status + { + get; + set; + } + + public float BytesPerSecond + { + get; + private set; + } + + public float Progress + { + get { return Received / (float)FileSize; } + } + + public FileStream WriteStream + { + get; + private set; + } + + public int TimeStarted + { + get; + private set; + } + + public int SequenceChannel; + + public FileTransferIn(string filePath, FileTransferType fileType, OnFinishedDelegate onFinished) + { + FilePath = filePath; + FileName = Path.GetFileName(FilePath); + FileType = fileType; + + this.OnFinished = onFinished; + + WriteStream = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None); + TimeStarted = Environment.TickCount; + + Status = FileTransferStatus.NotStarted; + } + + public void ReadBytes(NetIncomingMessage inc) + { + byte[] all = inc.ReadBytes(inc.LengthBytes - inc.PositionInBytes); + Received += (ulong)all.Length; + WriteStream.Write(all, 0, all.Length); + + int passed = Environment.TickCount - TimeStarted; + float psec = passed / 1000.0f; + + BytesPerSecond = Received / psec; + + Status = Received >= FileSize ? FileTransferStatus.Finished : FileTransferStatus.Receiving; + } + + private bool disposed = false; + protected virtual void Dispose(bool disposing) + { + if (disposed) return; + + if (disposing) + { + if (WriteStream != null) + { + WriteStream.Flush(); + WriteStream.Close(); + WriteStream.Dispose(); + WriteStream = null; + } + } + disposed = true; + } + + public void Dispose() + { + Dispose(true); + } + } + + const int MaxFileSize = 1000000; + + private List activeTransfers; + + private string downloadFolder; + + public FileReceiver(string downloadFolder) + { + activeTransfers = new List(); + + this.downloadFolder = downloadFolder; + } + + private void ReadMessage(NetIncomingMessage inc) + { + System.Diagnostics.Debug.Assert(!activeTransfers.Any(t => + t.Status == FileTransferStatus.Error || + t.Status == FileTransferStatus.Canceled || + t.Status == FileTransferStatus.Finished), "List of active file transfers contains entires that should have been removed"); + + byte transferMessageType = inc.ReadByte(); + switch (transferMessageType) + { + case (byte)FileTransferMessageType.Initiate: + var existingTransfer = activeTransfers.Find(t => t.SequenceChannel == inc.SequenceChannel); + if (existingTransfer != null) + { + DebugConsole.ThrowError("File transfer error: file transfer initiated on a sequence channel that's already in use"); + return; + } + + byte fileType = inc.ReadByte(); + ushort chunkLen = inc.ReadUInt16(); + ulong fileSize = inc.ReadUInt64(); + string fileName = inc.ReadString(); + + string errorMsg; + if (!ValidateInitialData(fileType, fileName, fileSize, out errorMsg)) + { + DebugConsole.ThrowError("File transfer failed ("+errorMsg+")"); + return; + } + + var newTransfer = new FileTransferIn(Path.Combine(downloadFolder, fileName), (FileTransferType)fileType, null); + newTransfer.SequenceChannel = inc.SequenceChannel; + newTransfer.Status = FileTransferStatus.Receiving; + + activeTransfers.Add(newTransfer); + + break; + case (byte)FileTransferMessageType.Data: + var activeTransfer = activeTransfers.Find(t => t.SequenceChannel == inc.SequenceChannel); + if (activeTransfer == null) + { + DebugConsole.ThrowError("File transfer error: received data without a transfer initiation message"); + return; + } + + if (activeTransfer.Received + (ulong)inc.LengthBytes > activeTransfer.FileSize * 1.1f) + { + DebugConsole.ThrowError("File transfer error: Received more data than expected"); + activeTransfer.Status = FileTransferStatus.Error; + return; + } + + try + { + activeTransfer.ReadBytes(inc); + } + catch (Exception e) + { + DebugConsole.ThrowError("File transfer error: "+e.Message); + activeTransfer.Status = FileTransferStatus.Error; + StopTransfer(activeTransfer, true); + return; + } + + if (activeTransfer.Status == FileTransferStatus.Finished) + { + string errorMessage = ""; + if (ValidateReceivedData(activeTransfer, out errorMessage)) + { + activeTransfer.OnFinished(activeTransfer); + StopTransfer(activeTransfer); + } + else + { + activeTransfer.Status = FileTransferStatus.Error; + StopTransfer(activeTransfer, true); + } + } + + break; + } + } + + private bool ValidateInitialData(byte type, string fileName, ulong fileSize, out string errorMessage) + { + errorMessage = ""; + + if (fileSize > MaxFileSize) + { + errorMessage = "File too large (" + MathUtils.GetBytesReadable((long)fileSize) + ")"; + return false; + } + + if (!Enum.IsDefined(typeof(FileTransferType), type)) + { + errorMessage = "Unknown file type"; + return false; + } + + if (!Regex.Match(fileName, @"^[\w\- ]+[\w\-. ]*$").Success) + { + errorMessage = "Illegal characters in file name ''" + fileName + "''"; + return false; + } + + switch (type) + { + case (byte)FileTransferType.Submarine: + if (Path.GetExtension(fileName) != ".sub") + { + errorMessage = "Wrong file extension ''" + Path.GetExtension(fileName) + "''! (Expected .sub)"; + return false; + } + break; + } + + return true; + } + + private bool ValidateReceivedData(FileTransferIn fileTransfer, out string ErrorMessage) + { + ErrorMessage = ""; + switch (fileTransfer.FileType) + { + case FileTransferType.Submarine: + Stream stream = null; + + try + { + stream = SaveUtil.DecompressFiletoStream(fileTransfer.FilePath); + } + catch (Exception e) + { + ErrorMessage = "Loading received submarine ''" + fileTransfer.FileName + "'' failed! {" + e.Message + "}"; + return false; + } + + if (stream == null) + { + ErrorMessage = "Decompressing received submarine file''" + fileTransfer.FilePath + "'' failed!"; + return false; + } + + try + { + stream.Position = 0; + + XmlReaderSettings settings = new XmlReaderSettings(); + settings.DtdProcessing = DtdProcessing.Prohibit; + settings.IgnoreProcessingInstructions = true; + + using (var reader = XmlReader.Create(stream, settings)) + { + while (reader.Read()); + } + } + catch + { + stream.Close(); + stream.Dispose(); + + ErrorMessage = "Parsing file ''" + fileTransfer.FilePath + "'' failed! The file may not be a valid submarine file."; + return false; + } + + stream.Close(); + stream.Dispose(); + break; + } + + return true; + } + + public void StopTransfer(FileTransferIn transfer, bool deleteFile = false) + { + if (deleteFile && File.Exists(transfer.FilePath)) + { + try + { + File.Delete(transfer.FilePath); + } + catch (Exception e) + { + DebugConsole.ThrowError("Failed to delete file \""+transfer.FilePath+"\" ("+e.Message+")"); + } + } + + if (transfer.Status != FileTransferStatus.Finished && + transfer.Status != FileTransferStatus.Error) + { + transfer.Status = FileTransferStatus.Canceled; + } + + if (activeTransfers.Contains(transfer)) activeTransfers.Remove(transfer); + transfer.Dispose(); + } + } +} diff --git a/Subsurface/Source/Networking/FileTransfer/FileSender.cs b/Subsurface/Source/Networking/FileTransfer/FileSender.cs index 6c4173532..e020575c5 100644 --- a/Subsurface/Source/Networking/FileTransfer/FileSender.cs +++ b/Subsurface/Source/Networking/FileTransfer/FileSender.cs @@ -10,7 +10,7 @@ namespace Barotrauma.Networking { enum FileTransferStatus { - NotStarted, Sending, Receiving, Finished, Canceled + NotStarted, Sending, Receiving, Finished, Canceled, Error } enum FileTransferMessageType @@ -27,7 +27,6 @@ namespace Barotrauma.Networking { public class FileTransferOut { - private byte[] tempBuffer; private byte[] data; private DateTime startingTime; @@ -56,7 +55,7 @@ namespace Barotrauma.Networking public float Progress { - get { return 0.0f; }//inputStream == null ? 0.0f : (float)sentOffset / (float)inputStream.Length; } + get { return SentOffset / (float)Data.Length; } } public float WaitTimer @@ -81,6 +80,8 @@ namespace Barotrauma.Networking get { return connection; } } + public int SequenceChannel; + public FileTransferOut(NetConnection recipient, FileTransferType fileType, string filePath) { connection = recipient; @@ -115,6 +116,7 @@ namespace Barotrauma.Networking public FileTransferOut StartTransfer(NetConnection recipient, FileTransferType fileType, string filePath) { + //TODO: set a limit on the amount of active transfers if (!File.Exists(filePath)) { DebugConsole.ThrowError("Failed to initiate file transfer (file \""+filePath+"\" not found."); @@ -125,6 +127,11 @@ namespace Barotrauma.Networking try { transfer = new FileTransferOut(recipient, fileType, filePath); + transfer.SequenceChannel = 1; + while (activeTransfers.Any(t => t.Connection == recipient && t.SequenceChannel == transfer.SequenceChannel)) + { + transfer.SequenceChannel++; + } } catch (Exception e) { @@ -136,6 +143,8 @@ namespace Barotrauma.Networking public void Update(float deltaTime) { + activeTransfers.RemoveAll(t => t.Connection.Status != NetConnectionStatus.Connected); + foreach (FileTransferOut transfer in activeTransfers) { transfer.WaitTimer -= deltaTime; @@ -161,9 +170,10 @@ namespace Barotrauma.Networking message.Write((ushort)chunkLen); message.Write((ulong)transfer.Data.Length); message.Write(transfer.FileName); - transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 1); + transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel); transfer.Status = FileTransferStatus.Sending; + continue; } message = peer.CreateMessage(sendByteCount + 8 + 1); @@ -175,7 +185,7 @@ namespace Barotrauma.Networking message.Write(sendBytes); - transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, 1); + transfer.Connection.SendMessage(message, NetDeliveryMethod.ReliableOrdered, transfer.SequenceChannel); transfer.SentOffset += sendByteCount; if (remaining - sendByteCount <= 0) @@ -193,5 +203,33 @@ namespace Barotrauma.Networking activeTransfers.Remove(transfer); } + public void ReadFileRequest(NetIncomingMessage inc) + { + byte messageType = inc.ReadByte(); + + if (messageType == (byte)FileTransferMessageType.Cancel) + { + byte sequenceChannel = inc.ReadByte(); + var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.SenderConnection && t.SequenceChannel == sequenceChannel); + if (matchingTransfer != null) CancelTransfer(matchingTransfer); + + return; + } + + byte fileType = inc.ReadByte(); + switch (fileType) + { + case (byte)FileTransferType.Submarine: + string fileName = inc.ReadString(); + var requestedSubmarine = Submarine.SavedSubmarines.Find(s => s.Name == fileName); + + if (requestedSubmarine != null) + { + StartTransfer(inc.SenderConnection, FileTransferType.Submarine, requestedSubmarine.FilePath); + } + break; + } + } + } } diff --git a/Subsurface/Source/Networking/GameServer.cs b/Subsurface/Source/Networking/GameServer.cs index fd5c6840d..0007d8198 100644 --- a/Subsurface/Source/Networking/GameServer.cs +++ b/Subsurface/Source/Networking/GameServer.cs @@ -45,6 +45,8 @@ namespace Barotrauma.Networking private ServerEntityEventManager entityEventManager; + private FileSender fileSender; + public override List ConnectedClients { get @@ -132,6 +134,8 @@ namespace Barotrauma.Networking entityEventManager = new ServerEntityEventManager(this); + fileSender = new FileSender(this); + whitelist = new WhiteList(); banList = new BanList(); @@ -341,7 +345,6 @@ namespace Barotrauma.Networking if (settingsFrame != null) settingsFrame.Update(deltaTime); if (log.LogFrame != null) log.LogFrame.Update(deltaTime); - if (!started) return; base.Update(deltaTime); @@ -355,7 +358,9 @@ namespace Barotrauma.Networking } } - unauthenticatedClients.RemoveAll(uc => uc.AuthTimer <= 0.0f); + unauthenticatedClients.RemoveAll(uc => uc.AuthTimer <= 0.0f); + + fileSender.Update(deltaTime); if (gameStarted) { @@ -550,6 +555,12 @@ namespace Barotrauma.Networking case ClientPacketHeader.SERVER_COMMAND: ClientReadServerCommand(inc); break; + case ClientPacketHeader.FILE_REQUEST: + if (AllowFileTransfers) + { + + } + break; } } @@ -1902,7 +1913,7 @@ namespace Barotrauma.Networking Log("Shutting down server...", Color.Cyan); log.Save(); } - + server.Shutdown("The server has been shut down"); } } diff --git a/Subsurface/Source/Networking/NetworkMember.cs b/Subsurface/Source/Networking/NetworkMember.cs index 6002da628..bfa7b1339 100644 --- a/Subsurface/Source/Networking/NetworkMember.cs +++ b/Subsurface/Source/Networking/NetworkMember.cs @@ -15,6 +15,8 @@ namespace Barotrauma.Networking REQUEST_INIT, //ask the server to give you initialization UPDATE_LOBBY, //update state in lobby UPDATE_INGAME, //update state ingame + + FILE_REQUEST, //request a (submarine) file from the server RESPONSE_STARTGAME, //tell the server whether you're ready to start SERVER_COMMAND //tell the server to end a round or kick/ban someone (special permissions required)