diff --git a/PUP/BCPLString.cs b/PUP/BCPLString.cs index 8eca421..668bd3c 100644 --- a/PUP/BCPLString.cs +++ b/PUP/BCPLString.cs @@ -111,12 +111,14 @@ namespace IFS } /// - /// Returns the raw representation of the BCPL string + /// Returns the raw representation of the BCPL string. + /// This returned array is padded to a word boundary. /// /// public byte[] ToArray() { - byte[] a = new byte[_string.Length + 1]; + int length = _string.Length + ((_string.Length % 2) == 0 ? 2 : 1); + byte[] a = new byte[length]; a[0] = (byte)_string.Length; _string.CopyTo(a, 1); diff --git a/PUP/BSPManager.cs b/PUP/BSPManager.cs index 7190276..ad6533f 100644 --- a/PUP/BSPManager.cs +++ b/PUP/BSPManager.cs @@ -10,6 +10,13 @@ using System.Threading.Tasks; namespace IFS { + public struct BSPAck + { + public ushort MaxBytes; + public ushort MaxPups; + public ushort BytesSent; + } + public abstract class BSPProtocol : PUPProtocolBase { public abstract void InitializeServerForChannel(BSPChannel channel); @@ -29,10 +36,10 @@ namespace IFS _outputLock = new ReaderWriterLockSlim(); _inputWriteEvent = new AutoResetEvent(false); - _inputQueue = new Queue(65536); _outputAckEvent = new AutoResetEvent(false); + _outputQueue = new Queue(65536); _protocolHandler = protocolHandler; @@ -66,7 +73,23 @@ namespace IFS public void Destroy() { - + if (OnDestroy != null) + { + OnDestroy(); + } + } + + public void End(PUP p) + { + PUP endReplyPup = new PUP(PupType.EndReply, p.ID, _clientConnectionPort, _serverConnectionPort, new byte[0]); + PUPProtocolDispatcher.Instance.SendPup(endReplyPup); + + // "The receiver of the End PUP responds by returning an EndReply Pup with matching ID and then + // _dallying_ up to some reasonably long timeout interval (say, 10 seconds) in order to respond to + // a retransmitted End Pup should its initial EndReply be lost. If and when the dallying end of the + // stream connection receives its EndReply, it may immediately self destruct." + // TODO: actually make this happen... + } /// @@ -118,7 +141,13 @@ namespace IFS // Not enough data in the queue. // Wait until we have received more data, then try again. - _inputWriteEvent.WaitOne(); // TODO: timeout and fail + if (!_inputWriteEvent.WaitOne(BSPReadTimeoutPeriod)) + { + Log.Write(LogType.Error, LogComponent.BSP, "Timed out waiting for data on read, aborting connection."); + // We timed out waiting for data, abort the connection. + SendAbort("Timeout on read."); + BSPManager.DestroyChannel(this); + } } } @@ -153,16 +182,19 @@ namespace IFS /// /// Appends incoming client data into the input queue (called from BSPManager to place new PUP data into the BSP stream) /// - public void WriteQueue(PUP dataPUP) + public void RecvWriteQueue(PUP dataPUP) { // If we are over our high watermark, we will drop the data (and not send an ACK even if requested). // Clients should be honoring the limits we set in the RFC packets. _inputLock.EnterUpgradeableReadLock(); + + /* if (_inputQueue.Count + dataPUP.Contents.Length > MaxBytes) { + Log.Write(LogLevel.Error, "Queue larger than {0} bytes, dropping."); _inputLock.ExitUpgradeableReadLock(); return; - } + } */ // Sanity check on expected position from sender vs. received data on our end. // If they don't match then we've lost a packet somewhere. @@ -173,6 +205,7 @@ namespace IFS // must eventually be resent. This is far simpler than accepting out-of-order data and keeping track // of where it goes in the queue, though less efficient. _inputLock.ExitUpgradeableReadLock(); + Log.Write(LogType.Error, LogComponent.BSP, "Lost Packet, client ID does not match our receive position ({0} != {1})", dataPUP.ID, _recv_pos); return; } @@ -186,7 +219,7 @@ namespace IFS _inputQueue.Enqueue(dataPUP.Contents[i]); //Console.Write("{0:x} ({1}), ", dataPUP.Contents[i], (char)dataPUP.Contents[i]); - } + } _recv_pos += (UInt32)dataPUP.Contents.Length; @@ -198,48 +231,102 @@ namespace IFS // If the client wants an ACK, send it now. if ((PupType)dataPUP.Type == PupType.AData) - { - SendAck(); + { + SendAck(); } } + /// + /// Sends data, with immediate flush. + /// + /// + public void Send(byte[] data) + { + Send(data, true /* flush */); + } + /// /// Sends data to the channel (i.e. to the client). Will block (waiting for an ACK) if an ACK is requested. /// /// The data to be sent - public void Send(byte[] data) + /// Whether to flush data out immediately or to wait for enough for a full PUP first. + public void Send(byte[] data, bool flush) { - // Write data to the output stream. - // For now, we request ACKs for every pup sent. - // TODO: should buffer data until an entire PUP's worth is ready - // (and split data that's too large into multiple PUPs.) - PUP dataPup = new PUP(PupType.AData, _send_pos, _clientConnectionPort, _serverConnectionPort, data); + // Add output data to output queue. + // Again, this is really inefficient + for (int i = 0; i < data.Length; i++) + { + _outputQueue.Enqueue(data[i]); + } - PUPProtocolDispatcher.Instance.SendPup(dataPup); + if (flush || _outputQueue.Count >= PUP.MAX_PUP_SIZE) + { + // Send data until all is used (for a flush) or until we have less than a full PUP (non-flush). + while (_outputQueue.Count >= (flush ? 1 : PUP.MAX_PUP_SIZE)) + { + byte[] chunk = new byte[Math.Min(PUP.MAX_PUP_SIZE, _outputQueue.Count)]; - _send_pos += (uint)data.Length; + // Ugh. + for (int i = 0; i < chunk.Length; i++) + { + chunk[i] = _outputQueue.Dequeue(); + } - // Await an ack for the PUP we just sent - _outputAckEvent.WaitOne(); // TODO: timeout and fail + // Send the data, retrying as necessary. + int retry; + for (retry = 0; retry < BSPRetryCount; retry++) + { + PUP dataPup = new PUP(PupType.AData, _send_pos, _clientConnectionPort, _serverConnectionPort, chunk); + PUPProtocolDispatcher.Instance.SendPup(dataPup); + + _send_pos += (uint)chunk.Length; + + // Await an ack for the PUP we just sent. If we timeout, we will retry. + // + if (_outputAckEvent.WaitOne(BSPAckTimeoutPeriod)) + { + break; + } + + Log.Write(LogType.Warning, LogComponent.BSP, "ACK not received for sent data, retrying."); + } + + if (retry >= BSPRetryCount) + { + Log.Write(LogType.Error, LogComponent.BSP, "ACK not received after retries, aborting connection."); + SendAbort("ACK not received for sent data."); + BSPManager.DestroyChannel(this); + } + + } + } + } + + public void SendAbort(string message) + { + PUP abortPup = new PUP(PupType.Abort, _start_pos, _clientConnectionPort, _serverConnectionPort, Helpers.StringToArray(message)); + PUPProtocolDispatcher.Instance.SendPup(abortPup); } /// /// Invoked when the client sends an ACK /// /// - public void Ack(PUP ackPUP) + public void RecvAck(PUP ackPUP) { // Update receiving end stats (max PUPs, etc.) // Ensure client's position matches ours if (ackPUP.ID != _send_pos) { - Log.Write(LogLevel.BSPLostPacket, - String.Format("Client position != server position for BSP {0} ({1} != {2})", - _serverConnectionPort.Socket, - ackPUP.ID, - _send_pos)); - } + Log.Write(LogType.Warning, LogComponent.BSP, + "Client position != server position for BSP {0} ({1} != {2})", + _serverConnectionPort.Socket, + ackPUP.ID, + _send_pos); + } + + BSPAck ack = (BSPAck)Serializer.Deserialize(ackPUP.Contents, typeof(BSPAck)); // Let any waiting threads continue @@ -263,9 +350,20 @@ namespace IFS // to allow protocols consuming BSP streams to be alerted when things happen. // + public delegate void DestroyDelegate(); + + public DestroyDelegate OnDestroy; + private void SendAck() { - PUP ackPup = new PUP(PupType.Ack, _recv_pos, _clientConnectionPort, _serverConnectionPort); + _inputLock.EnterReadLock(); + BSPAck ack = new BSPAck(); + ack.MaxBytes = MaxBytes; //(ushort)(MaxBytes - _inputQueue.Count); + ack.MaxPups = MaxPups; + ack.BytesSent = MaxBytes; //(ushort)(MaxBytes - _inputQueue.Count); + _inputLock.ExitReadLock(); + + PUP ackPup = new PUP(PupType.Ack, _recv_pos, _clientConnectionPort, _serverConnectionPort, Serializer.Serialize(ack)); PUPProtocolDispatcher.Instance.SendPup(ackPup); } @@ -288,6 +386,7 @@ namespace IFS // TODO: replace this with a more efficient structure for buffering data private Queue _inputQueue; + private Queue _outputQueue; // Constants @@ -295,6 +394,11 @@ namespace IFS private const int MaxPups = 1; private const int MaxPupSize = 532; private const int MaxBytes = 1 * 532; + + + private const int BSPRetryCount = 5; + private const int BSPAckTimeoutPeriod = 1000; // 1 second + private const int BSPReadTimeoutPeriod = 60000; // 1 minute } /// @@ -324,7 +428,7 @@ namespace IFS { if (p.Type != PupType.RFC) { - Log.Write(LogLevel.Error, String.Format("Expected RFC pup, got {0}", p.Type)); + Log.Write(LogType.Error, LogComponent.RTP, "Expected RFC pup, got {0}", p.Type); return; } @@ -343,16 +447,16 @@ namespace IFS sourcePort.Network = DirectoryServices.Instance.LocalNetwork; PUP rfcResponse = new PUP(PupType.RFC, p.ID, newChannel.ClientPort, sourcePort, newChannel.ServerPort.ToArray()); - PUPProtocolDispatcher.Instance.SendPup(rfcResponse); + Log.Write(LogComponent.RTP, + "Establishing Rendezvous, ID {0}, Server port {1}, Client port {2}.", + p.ID, newChannel.ServerPort, newChannel.ClientPort); + + PUPProtocolDispatcher.Instance.SendPup(rfcResponse); } /// /// Called when BSP-based protocols receive data. /// - /// - /// null if no new channel is created due to the sent PUP (not an RFC) - /// a new BSPChannel if one has been created based on the PUP (new RFC) - /// /// public static void RecvData(PUP p) { @@ -360,43 +464,50 @@ namespace IFS if (channel == null) { - Log.Write(LogLevel.Error, "Received BSP PUP on an unconnected socket, ignoring."); + Log.Write(LogType.Error, LogComponent.BSP, "Received BSP PUP on an unconnected socket, ignoring."); return; } switch (p.Type) { case PupType.RFC: - Log.Write(LogLevel.Error, "Received RFC on established channel, ignoring."); + Log.Write(LogType.Error, LogComponent.BSP, "Received RFC on established channel, ignoring."); break; case PupType.Data: case PupType.AData: - { - channel.WriteQueue(p); + { + channel.RecvWriteQueue(p); } break; case PupType.Ack: { - channel.Ack(p); + channel.RecvAck(p); + } + break; + + case PupType.End: + { + // Second step of tearing down a connection, the End from the client, to which we will + // send an EndReply, expecting a second EndReply. + channel.End(p); } break; - case PupType.End: - { - //channel.EndReply(); + case PupType.EndReply: + { + // Last step of tearing down a connection, the EndReply from the client. + DestroyChannel(channel); } break; case PupType.Abort: - { - // TODO: tear down the channel - DestroyChannel(channel); - + { string abortMessage = Helpers.ArrayToString(p.Contents); + Log.Write(LogType.Warning, LogComponent.RTP, String.Format("BSP aborted, message: '{0}'", abortMessage)); - Log.Write(LogLevel.Warning, String.Format("BSP aborted, message: '{0}'", abortMessage)); + DestroyChannel(channel); } break; diff --git a/PUP/CopyDisk/CopyDiskServer.cs b/PUP/CopyDisk/CopyDiskServer.cs new file mode 100644 index 0000000..a3246aa --- /dev/null +++ b/PUP/CopyDisk/CopyDiskServer.cs @@ -0,0 +1,573 @@ +using IFS.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; + +namespace IFS.CopyDisk +{ + public enum CopyDiskBlock + { + Version = 1, + SendDiskParamsR = 2, + HereAreDiskParams = 3, + StoreDisk = 4, + RetrieveDisk = 5, + HereIsDiskPage = 6, + EndOfTransfer = 7, + SendErrors = 8, + HereAreErrors = 9, + No = 10, + Yes = 11, + Comment = 12, + Login = 13, + SendDiskParamsW = 14, + } + + public enum NoCode + { + UnitNotReady = 1, + UnitWriteProtected = 2, + OverwriteNotAllowed = 3, + UnknownCommand = 4, + } + + struct VersionYesNoBlock + { + public VersionYesNoBlock(CopyDiskBlock command, ushort code, string herald) + { + Code = code; + Herald = new BCPLString(herald); + + Length = (ushort)((6 + herald.Length + 2) / 2); // +2 for length of BCPL string and to round up to next word length + Command = (ushort)command; + } + + public ushort Length; + public ushort Command; + public ushort Code; + public BCPLString Herald; + } + + struct LoginBlock + { + public ushort Length; + public ushort Command; + public BCPLString UserName; + + [WordAligned] + public BCPLString UserPassword; + + [WordAligned] + public BCPLString ConnName; + + [WordAligned] + public BCPLString ConnPassword; + } + + struct SendDiskParamsBlock + { + public ushort Length; + public ushort Command; + public BCPLString UnitName; + } + + struct HereAreDiskParamsBFSBlock + { + public HereAreDiskParamsBFSBlock(DiskGeometry geometry) + { + Length = 6; + Command = (ushort)CopyDiskBlock.HereAreDiskParams; + + DiskType = 10; // 12(octal) - BFS disk types + Cylinders = (ushort)geometry.Cylinders; + Heads = (ushort)geometry.Tracks; + Sectors = (ushort)geometry.Sectors; + } + + public ushort Length; + public ushort Command; + public ushort DiskType; + public ushort Cylinders; + public ushort Heads; + public ushort Sectors; + } + + struct HereAreErrorsBFSBlock + { + public HereAreErrorsBFSBlock(ushort hardErrors, ushort softErrors) + { + Length = 4; + Command = (ushort)CopyDiskBlock.HereAreErrors; + HardErrorCount = hardErrors; + SoftErrorCount = softErrors; + } + + public ushort Length; + public ushort Command; + public ushort HardErrorCount; + public ushort SoftErrorCount; + } + + + struct TransferParametersBlock + { + public ushort Length; + public ushort Command; + public ushort StartAddress; + public ushort EndAddress; + } + + struct HereIsDiskPageBlock + { + public HereIsDiskPageBlock(byte[] header, byte[] label, byte[] data) + { + if (header.Length != 4 || + label.Length != 16 || + data.Length != 512) + { + throw new InvalidOperationException("Page data is incorrectly sized."); + } + + Length = (512 + 16 + 4 + 4) / 2; + Command = (ushort)CopyDiskBlock.HereIsDiskPage; + + Header = header; + Label = label; + Data = data; + } + + public ushort Length; + public ushort Command; + + [ArrayLength(4)] + public byte[] Header; + + [ArrayLength(16)] + public byte[] Label; + + [ArrayLength(512)] + public byte[] Data; + } + + struct HereIsDiskPageIncorrigableBlock + { + public HereIsDiskPageIncorrigableBlock(byte[] header, byte[] label) + { + if (header.Length != 4 || + label.Length != 16) + { + throw new InvalidOperationException("Page data is incorrectly sized."); + } + + Length = (16 + 4 + 4) / 2; + Command = (ushort)CopyDiskBlock.HereIsDiskPage; + + Header = header; + Label = label; + } + + public ushort Length; + public ushort Command; + + [ArrayLength(4)] + public byte[] Header; + + [ArrayLength(16)] + public byte[] Label; + } + + struct EndOfTransferBlock + { + public EndOfTransferBlock(int dummy) /* can't have parameterless constructor for struct */ + { + Length = 2; + Command = (ushort)CopyDiskBlock.EndOfTransfer; + } + + public ushort Length; + public ushort Command; + } + + public class CopyDiskServer : BSPProtocol + { + /// + /// Called by dispatcher to send incoming data destined for this protocol. + /// + /// + public override void RecvData(PUP p) + { + throw new NotImplementedException(); + } + + public override void InitializeServerForChannel(BSPChannel channel) + { + // Spawn new worker + CopyDiskWorker worker = new CopyDiskWorker(channel); + } + } + + public class CopyDiskWorker + { + public CopyDiskWorker(BSPChannel channel) + { + // Register for channel events + channel.OnDestroy += OnChannelDestroyed; + + _running = true; + + _workerThread = new Thread(new ParameterizedThreadStart(CopyDiskWorkerThreadInit)); + _workerThread.Start(channel); + } + + private void OnChannelDestroyed() + { + // Tell the thread to exit and give it a short period to do so... + _running = false; + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Asking CopyDisk worker thread to exit..."); + _workerThread.Join(1000); + + if (_workerThread.IsAlive) + { + Logging.Log.Write(LogType.Verbose, LogComponent.CopyDisk, "CopyDisk worker thread did not exit, terminating."); + _workerThread.Abort(); + } + } + + private void CopyDiskWorkerThreadInit(object obj) + { + BSPChannel channel = (BSPChannel)obj; + + // + // Run the worker thread. + // If anything goes wrong, log the exception and tear down the BSP connection. + // + try + { + CopyDiskWorkerThread(channel); + } + catch(Exception e) + { + if (!(e is ThreadAbortException)) + { + Logging.Log.Write(LogType.Error, LogComponent.CopyDisk, "CopyDisk worker thread terminated with exception '{0}'.", e.Message); + channel.SendAbort("Server encountered an error."); + } + } + } + + private void CopyDiskWorkerThread(BSPChannel channel) + { + // TODO: enforce state (i.e. reject out-of-order block types.) + while (_running) + { + // Retrieve length of this block (in bytes): + int length = channel.ReadUShort() * 2; + + // Sanity check that length is a reasonable value. + if (length > 2048) + { + // TODO: shut down channel + throw new InvalidOperationException(String.Format("Insane block length ({0})", length)); + } + + // Retrieve type + CopyDiskBlock blockType = (CopyDiskBlock)channel.ReadUShort(); + + // Read rest of block starting at offset 4 (so deserialization works) + byte[] data = new byte[length]; + channel.Read(ref data, data.Length - 4, 4); + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Copydisk block type is {0}", blockType); + + switch(blockType) + { + case CopyDiskBlock.Version: + { + VersionYesNoBlock vbIn = (VersionYesNoBlock)Serializer.Deserialize(data, typeof(VersionYesNoBlock)); + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Copydisk client is version {0}, '{1}'", vbIn.Code, vbIn.Herald.ToString()); + + // Send the response: + VersionYesNoBlock vbOut = new VersionYesNoBlock(CopyDiskBlock.Version, vbIn.Code, "IFS CopyDisk of 26-Jan-2016!"); + channel.Send(Serializer.Serialize(vbOut)); + } + break; + + case CopyDiskBlock.Login: + { + LoginBlock login = (LoginBlock)Serializer.Deserialize(data, typeof(LoginBlock)); + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Login is for user {0}, password {1}, connection {2}, connection password {3}.", + login.UserName, + login.UserPassword, + login.ConnName, + login.ConnPassword); + + // + // TODO: for now we allow anyone in with any username and password, this needs to be fixed once + // an authentication mechanism is set up. + // + + // Send a "Yes" response back. + // + VersionYesNoBlock yes = new VersionYesNoBlock(CopyDiskBlock.Yes, 0, "Come on in, the water's fine."); + channel.Send(Serializer.Serialize(yes)); + } + break; + + case CopyDiskBlock.SendDiskParamsR: + { + SendDiskParamsBlock p = (SendDiskParamsBlock)Serializer.Deserialize(data, typeof(SendDiskParamsBlock)); + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Requested unit for reading is '{0}'", p.UnitName); + + // + // See if the pack image exists, return HereAreDiskParams if so, or No if not. + // If the image exists, save the path for future use. + // + // Some sanity (and security) checks: + // Name must be a filename only, no paths of any kind allowed. + // Oh, and the file must exist in the directory holding disk packs. + // + string diskPath = GetPathForDiskImage(p.UnitName.ToString()); + if (!String.IsNullOrEmpty(Path.GetDirectoryName(p.UnitName.ToString())) || + !File.Exists(diskPath)) + { + // Invalid name, return No reponse. + VersionYesNoBlock no = new VersionYesNoBlock(CopyDiskBlock.No, (ushort)NoCode.UnitNotReady, "Invalid unit name."); + channel.Send(Serializer.Serialize(no)); + } + else + { + // + // Attempt to open the image file and read it into memory. + // + try + { + using (FileStream packStream = new FileStream(diskPath, FileMode.Open, FileAccess.Read)) + { + // TODO: determine pack type rather than assuming Diablo 31 + _pack = new DiabloPack(DiabloDiskType.Diablo31); + _pack.Load(packStream, diskPath, true /* reverse byte order */); + } + + // Send a "HereAreDiskParams" response indicating success. + // + HereAreDiskParamsBFSBlock diskParams = new HereAreDiskParamsBFSBlock(_pack.Geometry); + channel.Send(Serializer.Serialize(diskParams)); + } + catch + { + // If we fail for any reason, return a "No" response. + // TODO: can we be more helpful here? + VersionYesNoBlock no = new VersionYesNoBlock(CopyDiskBlock.No, (ushort)NoCode.UnitNotReady, "Image could not be opened."); + channel.Send(Serializer.Serialize(no)); + } + } + } + break; + + case CopyDiskBlock.SendDiskParamsW: + { + SendDiskParamsBlock p = (SendDiskParamsBlock)Serializer.Deserialize(data, typeof(SendDiskParamsBlock)); + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Requested unit for writing is '{0}'", p.UnitName); + + // + // Some sanity (and security) checks: + // Name must be a filename only, no paths of any kind allowed. + // Oh, and the file must not exist in the directory holding disk packs. + // + string diskPath = GetPathForDiskImage(p.UnitName.ToString()); + if (!String.IsNullOrEmpty(Path.GetDirectoryName(p.UnitName.ToString())) || + File.Exists(diskPath)) + { + // Invalid name, return No reponse. + VersionYesNoBlock no = new VersionYesNoBlock(CopyDiskBlock.No, (ushort)NoCode.UnitNotReady, "Invalid unit name or image already exists."); + channel.Send(Serializer.Serialize(no)); + } + else + { + // + // Create a new in-memory disk image. We will write it out to disk when the transfer is completed. + // + // TODO: determine pack type based on disk params rather than assuming Diablo 31 + _pack = new DiabloPack(DiabloDiskType.Diablo31); + _pack.PackName = diskPath; + + + // Send a "HereAreDiskParams" response indicating success. + // + HereAreDiskParamsBFSBlock diskParams = new HereAreDiskParamsBFSBlock(_pack.Geometry); + channel.Send(Serializer.Serialize(diskParams)); + } + } + break; + + + case CopyDiskBlock.RetrieveDisk: + case CopyDiskBlock.StoreDisk: + { + TransferParametersBlock transferParameters = (TransferParametersBlock)Serializer.Deserialize(data, typeof(TransferParametersBlock)); + + _startAddress = _pack.DiskAddressToVirtualAddress(transferParameters.StartAddress); + _endAddress = _pack.DiskAddressToVirtualAddress(transferParameters.EndAddress); + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Transfer is from block {0} to block {1}", transferParameters.StartAddress, transferParameters.EndAddress); + + // Validate start/end parameters + if (_endAddress <= _startAddress || + _startAddress > _pack.MaxAddress || + _endAddress > _pack.MaxAddress) + { + VersionYesNoBlock no = new VersionYesNoBlock(CopyDiskBlock.No, (ushort)NoCode.UnknownCommand, "Transfer parameters are invalid."); + channel.Send(Serializer.Serialize(no)); + } + else + { + // We're OK. Save the parameters and send a Yes response. + VersionYesNoBlock yes = new VersionYesNoBlock(CopyDiskBlock.Yes, 0, "You are cleared for launch."); + channel.Send(Serializer.Serialize(yes)); + + // + // And send the requested range of pages if this is a Retrieve operation + // (otherwise wait for a HereIsDiskPage block from the client.) + // + if (blockType == CopyDiskBlock.RetrieveDisk) + { + for (int i = _startAddress; i < _endAddress + 1; i++) + { + DiabloDiskSector sector = _pack.GetSector(i); + HereIsDiskPageBlock block = new HereIsDiskPageBlock(sector.Header, sector.Label, sector.Data); + channel.Send(Serializer.Serialize(block), false /* do not flush */); + + if ((i % 100) == 0) + { + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Sent page {0}", i); + } + } + + // Send "EndOfTransfer" block to finish the transfer. + EndOfTransferBlock endTransfer = new EndOfTransferBlock(0); + channel.Send(Serializer.Serialize(endTransfer)); + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Send done."); + } + else + { + _currentAddress = _startAddress; + } + } + } + break; + + case CopyDiskBlock.HereIsDiskPage: + { + if (_currentAddress > _endAddress) + { + channel.SendAbort("Invalid address for page."); + _running = false; + break; + } + + if ((_currentAddress % 100) == 0) + { + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Received page {0}", _currentAddress); + } + + if (data.Length < 512 + 16 + 4 + 4) + { + // Incomplete ("incorrigable") page, indicating an unreadable or empty sector, just copy + // the header/label data in and leave an empty data page. + HereIsDiskPageIncorrigableBlock diskPage = (HereIsDiskPageIncorrigableBlock)Serializer.Deserialize(data, typeof(HereIsDiskPageIncorrigableBlock)); + DiabloDiskSector sector = new DiabloDiskSector(diskPage.Header, diskPage.Label, new byte[512]); + _pack.SetSector(_currentAddress, sector); + + _currentAddress++; + + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Page is empty / incorrigable."); + } + else + { + HereIsDiskPageBlock diskPage = (HereIsDiskPageBlock)Serializer.Deserialize(data, typeof(HereIsDiskPageBlock)); + DiabloDiskSector sector = new DiabloDiskSector(diskPage.Header, diskPage.Label, diskPage.Data); + _pack.SetSector(_currentAddress, sector); + + _currentAddress++; + } + } + break; + + case CopyDiskBlock.EndOfTransfer: + { + // No data in block. If we aren't currently at the end of the transfer, the transfer has been aborted. + // Do nothing right now. + if (_currentAddress < _endAddress) + { + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Transfer was aborted."); + _running = false; + } + else + { + try + { + // Commit disk image to disk. + using (FileStream packStream = new FileStream(_pack.PackName, FileMode.OpenOrCreate, FileAccess.Write)) + { + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Saving {0}...", _pack.PackName); + _pack.Save(packStream, true /* reverse byte order */); + Log.Write(LogType.Verbose, LogComponent.CopyDisk, "Saved."); + } + } + catch(Exception e) + { + // Log error, reset state. + Log.Write(LogType.Error, LogComponent.CopyDisk, "Failed to save pack {0} - {1}", _pack.PackName, e.Message); + } + } + } + break; + + case CopyDiskBlock.SendErrors: + { + // No data in block. Send list of errors we encountered. (There should always be none since we're perfect and have no disk errors.) + HereAreErrorsBFSBlock errorBlock = new HereAreErrorsBFSBlock(0, 0); + channel.Send(Serializer.Serialize(errorBlock)); + } + break; + + default: + Log.Write(LogType.Warning, LogComponent.CopyDisk, "Unhandled CopyDisk block {0}", blockType); + break; + } + } + } + + /// + /// Builds a relative path to the directory that holds the disk images. + /// + /// + /// + private static string GetPathForDiskImage(string packName) + { + // TODO: + // Make this path configurable? + return Path.Combine("Disks", packName); + } + + private Thread _workerThread; + private bool _running; + + // The pack being read / stored by this server + private DiabloPack _pack = null; + + // Current position and range of a write operation + private int _currentAddress = 0; + private int _startAddress = 0; + private int _endAddress = 0; + } +} diff --git a/PUP/CopyDisk/DiabloPack.cs b/PUP/CopyDisk/DiabloPack.cs new file mode 100644 index 0000000..5e96dad --- /dev/null +++ b/PUP/CopyDisk/DiabloPack.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IFS.CopyDisk +{ + public struct DiskGeometry + { + public DiskGeometry(int cylinders, int tracks, int sectors) + { + Cylinders = cylinders; + Tracks = tracks; + Sectors = sectors; + } + + public int Cylinders; + public int Tracks; + public int Sectors; + } + + public enum DiabloDiskType + { + Diablo31, + Diablo44 + } + + public class DiabloDiskSector + { + public DiabloDiskSector(byte[] header, byte[] label, byte[] data) + { + if (header.Length != 4 || + label.Length != 16 || + data.Length != 512) + { + throw new InvalidOperationException("Invalid sector header/label/data length."); + } + + Header = header; + Label = label; + Data = data; + } + + public byte[] Header; + public byte[] Label; + public byte[] Data; + + public static DiabloDiskSector Empty = new DiabloDiskSector(new byte[4], new byte[16], new byte[512]); + } + + /// + /// Encapsulates disk image data for all disk packs used with the + /// standard Alto Disk Controller (i.e. the 31 and 44, which differ + /// only in the number of cylinders) + /// + public class DiabloPack + { + public DiabloPack(DiabloDiskType type) + { + _diskType = type; + _packName = null; + _geometry = new DiskGeometry(type == DiabloDiskType.Diablo31 ? 203 : 406, 2, 12); + _sectors = new DiabloDiskSector[_geometry.Cylinders, _geometry.Tracks, _geometry.Sectors]; + } + + public DiskGeometry Geometry + { + get { return _geometry; } + } + + public string PackName + { + get { return _packName; } + set { _packName = value; } + } + + public int MaxAddress + { + get + { + return _geometry.Sectors * _geometry.Tracks * _geometry.Cylinders; + } + } + + public void Load(Stream imageStream, string path, bool reverseByteOrder) + { + _packName = path; + for(int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++) + { + for(int track = 0; track < _geometry.Tracks; track++) + { + for(int sector = 0; sector < _geometry.Sectors; sector++) + { + byte[] header = new byte[4]; // 2 words + byte[] label = new byte[16]; // 8 words + byte[] data = new byte[512]; // 256 words + + // + // Bitsavers images have an extra word in the header for some reason. + // ignore it. + // TODO: should support different formats ("correct" raw, Alto CopyDisk format, etc.) + // + imageStream.Seek(2, SeekOrigin.Current); + + if (imageStream.Read(header, 0, header.Length) != header.Length) + { + throw new InvalidOperationException("Short read while reading sector header."); + } + + if (imageStream.Read(label, 0, label.Length) != label.Length) + { + throw new InvalidOperationException("Short read while reading sector label."); + } + + if (imageStream.Read(data, 0, data.Length) != data.Length) + { + throw new InvalidOperationException("Short read while reading sector data."); + } + + _sectors[cylinder, track, sector] = + new DiabloDiskSector( + reverseByteOrder ? SwapBytes(header) : header, + reverseByteOrder ? SwapBytes(label) : label, + reverseByteOrder ? SwapBytes(data) : data); + } + } + } + + if (imageStream.Position != imageStream.Length) + { + throw new InvalidOperationException("Extra data at end of image file."); + } + } + + public void Save(Stream imageStream, bool reverseByteOrder) + { + byte[] emptyHeader = new byte[4]; // 2 words + byte[] emptyLabel = new byte[16]; // 8 words + byte[] emptyData = new byte[512]; // 256 words + + for (int cylinder = 0; cylinder < _geometry.Cylinders; cylinder++) + { + for (int track = 0; track < _geometry.Tracks; track++) + { + for (int sector = 0; sector < _geometry.Sectors; sector++) + { + + // + // Bitsavers images have an extra word in the header for some reason. + // We will follow this 'standard' when writing out. + // TODO: should support different formats ("correct" raw, Alto CopyDisk format, etc.) + // + byte[] dummy = new byte[2]; + imageStream.Write(dummy, 0, 2); + + DiabloDiskSector s = GetSector(cylinder, track, sector); + + imageStream.Write(reverseByteOrder ? SwapBytes(s.Header) : s.Header, 0, s.Header.Length); + imageStream.Write(reverseByteOrder ? SwapBytes(s.Label) : s.Label, 0, s.Label.Length); + imageStream.Write(reverseByteOrder ? SwapBytes(s.Data) : s.Data, 0, s.Data.Length); + + } + } + } + } + + public int DiskAddressToVirtualAddress(ushort diskAddress) + { + int head = (diskAddress & 0x4) >> 2; + int cylinder = (diskAddress & 0xff8) >> 3; + int sector = (diskAddress & 0xf000) >> 12; + + return cylinder * (_geometry.Sectors * _geometry.Tracks) + head * _geometry.Sectors + sector; + } + + public DiabloDiskSector GetSector(int cylinder, int track, int sector) + { + DiabloDiskSector s = _sectors[cylinder, track, sector]; + + // For invalid / empty sectors, return an Empty sector. + if (s == null) + { + s = DiabloDiskSector.Empty; + } + + return s; + } + + public void SetSector(int cylinder, int track, int sector, DiabloDiskSector newSector) + { + _sectors[cylinder, track, sector] = newSector; + } + + public DiabloDiskSector GetSector(int address) + { + if (address < 0 || address >= MaxAddress) + { + throw new InvalidOperationException("Disk address is out of range."); + } + + // TODO: factor this logic out + int sector = address % _geometry.Sectors; + int track = (address / _geometry.Sectors) % _geometry.Tracks; + int cylinder = (address / (_geometry.Sectors * _geometry.Tracks)); + + return GetSector(cylinder, track, sector); + } + + public void SetSector(int address, DiabloDiskSector newSector) + { + if (address < 0 || address >= MaxAddress) + { + throw new InvalidOperationException("Disk address is out of range."); + } + + int sector = address % _geometry.Sectors; + int track = (address / _geometry.Sectors) % _geometry.Tracks; + int cylinder = (address / (_geometry.Sectors * _geometry.Tracks)); + + SetSector(cylinder, track, sector, newSector); + } + + private byte[] SwapBytes(byte[] data) + { + byte[] swapped = new byte[data.Length]; + for(int i=0;i - /// Called by dispatcher to send incoming data destined for this protocol. - /// - /// - public override void RecvData(PUP p) - { - throw new NotImplementedException(); - } - - public override void InitializeServerForChannel(BSPChannel channel) - { - // spwan new worker thread with new BSP channel - Thread newThread = new Thread(new ParameterizedThreadStart(CopyDiskServerThread)); - newThread.Start(channel); - } - - private void CopyDiskServerThread(object obj) - { - BSPChannel channel = (BSPChannel)obj; - - while(true) - { - // Retrieve length of this block (in bytes): - int length = channel.ReadUShort() * 2; - - // Sanity check that length is a reasonable value. - if (length > 2048) - { - // TODO: shut down channel - throw new InvalidOperationException(String.Format("Insane block length ({0})", length)); - } - - // Retrieve type - CopyDiskBlock blockType = (CopyDiskBlock)channel.ReadUShort(); - - // Read rest of block - byte[] data = new byte[length]; - - channel.Read(ref data, data.Length - 4, 4); - - switch(blockType) - { - case CopyDiskBlock.Version: - VersionBlock vbIn = (VersionBlock)Serializer.Deserialize(data, typeof(VersionBlock)); - - Console.WriteLine("Copydisk client is version {0}, '{1}'", vbIn.Version, vbIn.Herald.ToString()); - - // Send the response: - VersionBlock vbOut = new VersionBlock(vbIn.Version, "IFS CopyDisk of 26-Jan-2016"); - channel.Send(Serializer.Serialize(vbOut)); - break; - - case CopyDiskBlock.Login: - - break; - - default: - Console.WriteLine("Unhandled CopyDisk block {0}", blockType); - break; - } - } - } - } -} diff --git a/PUP/DirectoryServices.cs b/PUP/DirectoryServices.cs index 746009b..1049383 100644 --- a/PUP/DirectoryServices.cs +++ b/PUP/DirectoryServices.cs @@ -4,6 +4,7 @@ using System.Linq; using System.IO; using System.Text; using System.Threading.Tasks; +using IFS.Logging; namespace IFS { @@ -41,7 +42,7 @@ namespace IFS // Load in hosts table from hosts file. LoadHostTable(); - Logging.Log.Write(Logging.LogLevel.Normal, "Directory services initialized."); + Log.Write(LogComponent.DirectoryServices, "Directory services initialized."); } public string AddressLookup(HostAddress address) @@ -142,8 +143,8 @@ namespace IFS if (tokens.Length < 2) { // Log warning and continue. - Logging.Log.Write(Logging.LogLevel.Warning, - String.Format("hosts.txt line {0}: Invalid syntax.", lineNumber)); + Log.Write(LogType.Warning, LogComponent.DirectoryServices, + "hosts.txt line {0}: Invalid syntax.", lineNumber); continue; } @@ -152,8 +153,8 @@ namespace IFS if (!tokens[0].EndsWith("#")) { // Log warning and continue. - Logging.Log.Write(Logging.LogLevel.Warning, - String.Format("hosts.txt line {0}: Improperly formed inter-network name '{1}'.", lineNumber, tokens[0])); + Log.Write(LogType.Warning, LogComponent.DirectoryServices, + "hosts.txt line {0}: Improperly formed inter-network name '{1}'.", lineNumber, tokens[0]); continue; } @@ -173,8 +174,8 @@ namespace IFS catch { // Log warning and continue. - Logging.Log.Write(Logging.LogLevel.Warning, - String.Format("hosts.txt line {0}: Invalid host number in inter-network address '{1}'.", lineNumber, tokens[0])); + Log.Write(LogType.Warning, LogComponent.DirectoryServices, + "hosts.txt line {0}: Invalid host number in inter-network address '{1}'.", lineNumber, tokens[0]); continue; } @@ -189,8 +190,8 @@ namespace IFS catch { // Log warning and continue. - Logging.Log.Write(Logging.LogLevel.Warning, - String.Format("hosts.txt line {0}: Invalid host or network number in inter-network address '{1}'.", lineNumber, tokens[0])); + Log.Write(LogType.Warning, LogComponent.DirectoryServices, + "hosts.txt line {0}: Invalid host or network number in inter-network address '{1}'.", lineNumber, tokens[0]); continue; } @@ -198,8 +199,8 @@ namespace IFS else { // Log warning and continue. - Logging.Log.Write(Logging.LogLevel.Warning, - String.Format("hosts.txt line {0}: Improperly formed inter-network name '{1}'.", lineNumber, tokens[0])); + Log.Write(LogType.Warning, LogComponent.DirectoryServices, + "hosts.txt line {0}: Improperly formed inter-network name '{1}'.", lineNumber, tokens[0]); continue; } @@ -213,8 +214,8 @@ namespace IFS if (_hostNameTable.ContainsKey(hostName)) { // Duplicate name entry! Skip this line. - Logging.Log.Write(Logging.LogLevel.Warning, - String.Format("hosts.txt line {0}: Duplicate hostname '{1}'.", lineNumber, hostName)); + Log.Write(LogType.Warning, LogComponent.DirectoryServices, + "hosts.txt line {0}: Duplicate hostname '{1}'.", lineNumber, hostName); break; } @@ -239,8 +240,8 @@ namespace IFS if (networkTable.ContainsKey(host.Host)) { // Duplicate host entry! Skip this line. - Logging.Log.Write(Logging.LogLevel.Warning, - String.Format("hosts.txt line {0}: Duplicate host ID '{1}'.", lineNumber, host.Host)); + Log.Write(LogType.Warning, LogComponent.DirectoryServices, + "hosts.txt line {0}: Duplicate host ID '{1}'.", lineNumber, host.Host); break; } diff --git a/PUP/Disks/diag.dsk b/PUP/Disks/diag.dsk new file mode 100644 index 0000000..0f0cbdb Binary files /dev/null and b/PUP/Disks/diag.dsk differ diff --git a/PUP/Disks/tdisk8.dsk b/PUP/Disks/tdisk8.dsk new file mode 100644 index 0000000..5dbdeaa Binary files /dev/null and b/PUP/Disks/tdisk8.dsk differ diff --git a/PUP/Entrypoint.cs b/PUP/Entrypoint.cs index 0f7199c..7499e46 100644 --- a/PUP/Entrypoint.cs +++ b/PUP/Entrypoint.cs @@ -1,4 +1,5 @@ -using IFS.Transport; +using IFS.CopyDisk; +using IFS.Transport; using System; using System.Collections.Generic; using System.Linq; @@ -33,9 +34,7 @@ namespace IFS foo oldFoo = (foo) Serializer.Deserialize(data, typeof(foo)); - - - Logging.Log.Level = Logging.LogLevel.All; + List ifaces = EthernetInterface.EnumerateDevices(); diff --git a/PUP/GatewayInformationProtocol.cs b/PUP/GatewayInformationProtocol.cs index e7996e2..ce528a1 100644 --- a/PUP/GatewayInformationProtocol.cs +++ b/PUP/GatewayInformationProtocol.cs @@ -7,6 +7,14 @@ using System.Threading.Tasks; namespace IFS { + public struct GatewayInformation + { + public byte TargetNet; + public byte GatewayNet; + public byte GatewayHost; + public byte HopCount; + } + /// /// Gateway Information Protocol (see http://xeroxalto.computerhistory.org/_cd8_/pup/.gatewayinformation.press!1.pdf) /// @@ -16,6 +24,7 @@ namespace IFS { // TODO: // load host tables, etc. + // spin up thread that spits out a GatewayInformation PUP periodically. } /// @@ -26,14 +35,55 @@ namespace IFS { switch (p.Type) { - + case PupType.GatewayInformationRequest: + SendGatewayInformationResponse(p); + break; default: - Log.Write(LogLevel.UnhandledProtocol, String.Format("Unhandled Gateway protocol {0}", p.Type)); + Log.Write(LogComponent.MiscServices, String.Format("Unhandled Gateway protocol {0}", p.Type)); break; } } + private void SendGatewayInformationResponse(PUP p) + { + // + // Pup Type: 201 (octal) + // Pup ID: same as in Request Pup + // Pup Contents: one or more groups of four bytes, each providing routing information for + // one network, as follows: + // + // + // + // In each group, the first byte specifies the target network number. If the gateway host is + // directly connected to that network, then the is zero and the and + // describe the gateway’s connection to the network. + // If the gateway host is not directly connected to the target network, then the second and + // third bytes give the network and host numbers of another gateway through which the + // responding gateway routes Pups to that network, and the fourth byte gives the hop count, + // i.e., the number of additional gateways (not including itself) through which the responding + // gateway believes a Pup must pass to reach the specified network. A hop count greater than + // the constant maxHops (presently 15) signifies that the target network is believed to be + // inaccessible. + // + + // Right now, we know of only one network (our own) and we are directly connected to it. + // + GatewayInformation info = new GatewayInformation(); + info.TargetNet = DirectoryServices.Instance.LocalNetwork; + info.GatewayNet = DirectoryServices.Instance.LocalNetwork; + info.GatewayHost = DirectoryServices.Instance.LocalNetwork; + info.HopCount = 0; + + PUPPort localPort = new PUPPort(DirectoryServices.Instance.LocalHostAddress, p.DestinationPort.Socket); + + // Response must contain our network number; this is used to tell clients what network they're on if they don't already know. + PUPPort remotePort = new PUPPort(DirectoryServices.Instance.LocalNetwork, p.SourcePort.Host, p.SourcePort.Socket); + + PUP response = new PUP(PupType.GatewayInformationResponse, p.ID, remotePort, localPort, Serializer.Serialize(info)); + + PUPProtocolDispatcher.Instance.SendPup(response); + } } } diff --git a/PUP/IFS.csproj b/PUP/IFS.csproj index 5220e86..f9fb74f 100644 --- a/PUP/IFS.csproj +++ b/PUP/IFS.csproj @@ -75,7 +75,8 @@ - + + @@ -95,6 +96,15 @@ PreserveNewest + + + + PreserveNewest + + + PreserveNewest + +