1
0
mirror of https://github.com/livingcomputermuseum/IFS.git synced 2026-03-02 18:04:24 +00:00

Implemented CopyDisk, some cleanup, improved logging.

This commit is contained in:
Josh Dersch
2016-02-02 15:51:56 -08:00
parent 32beacdca8
commit 78e45e4564
17 changed files with 1302 additions and 225 deletions

View File

@@ -111,12 +111,14 @@ namespace IFS
}
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
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);

View File

@@ -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<byte>(65536);
_outputAckEvent = new AutoResetEvent(false);
_outputQueue = new Queue<byte>(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...
}
/// <summary>
@@ -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
/// <summary>
/// Appends incoming client data into the input queue (called from BSPManager to place new PUP data into the BSP stream)
/// </summary>
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();
}
}
/// <summary>
/// Sends data, with immediate flush.
/// </summary>
/// <param name="data"></param>
public void Send(byte[] data)
{
Send(data, true /* flush */);
}
/// <summary>
/// Sends data to the channel (i.e. to the client). Will block (waiting for an ACK) if an ACK is requested.
/// </summary>
/// <param name="data">The data to be sent</param>
public void Send(byte[] data)
/// <param name="flush">Whether to flush data out immediately or to wait for enough for a full PUP first.</param>
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);
}
/// <summary>
/// Invoked when the client sends an ACK
/// </summary>
/// <param name="ackPUP"></param>
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<byte> _inputQueue;
private Queue<byte> _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
}
/// <summary>
@@ -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);
}
/// <summary>
/// Called when BSP-based protocols receive data.
/// </summary>
/// <returns>
/// 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)
/// </returns>
/// <param name="p"></param>
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;

View File

@@ -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
{
/// <summary>
/// Called by dispatcher to send incoming data destined for this protocol.
/// </summary>
/// <param name="p"></param>
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;
}
}
}
/// <summary>
/// Builds a relative path to the directory that holds the disk images.
/// </summary>
/// <param name="packName"></param>
/// <returns></returns>
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;
}
}

242
PUP/CopyDisk/DiabloPack.cs Normal file
View File

@@ -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]);
}
/// <summary>
/// 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)
/// </summary>
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<data.Length;i+=2)
{
swapped[i] = data[i + 1];
swapped[i + 1] = data[i];
}
return swapped;
}
private string _packName; // The file from whence the data came originally
private DiabloDiskSector[,,] _sectors;
private DiabloDiskType _diskType;
private DiskGeometry _geometry;
}
}

View File

@@ -1,109 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace IFS
{
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,
}
struct VersionBlock
{
public VersionBlock(ushort version, string herald)
{
Version = version;
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)CopyDiskBlock.Version;
}
public ushort Length;
public ushort Command;
public ushort Version;
public BCPLString Herald;
}
public class CopyDiskServer : BSPProtocol
{
/// <summary>
/// Called by dispatcher to send incoming data destined for this protocol.
/// </summary>
/// <param name="p"></param>
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;
}
}
}
}
}

View File

@@ -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;
}

BIN
PUP/Disks/diag.dsk Normal file

Binary file not shown.

BIN
PUP/Disks/tdisk8.dsk Normal file

Binary file not shown.

View File

@@ -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<EthernetInterface> ifaces = EthernetInterface.EnumerateDevices();

View File

@@ -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;
}
/// <summary>
/// Gateway Information Protocol (see http://xeroxalto.computerhistory.org/_cd8_/pup/.gatewayinformation.press!1.pdf)
/// </summary>
@@ -16,6 +24,7 @@ namespace IFS
{
// TODO:
// load host tables, etc.
// spin up thread that spits out a GatewayInformation PUP periodically.
}
/// <summary>
@@ -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:
//
// <target-net> <gateway-net> <gateway-host> <hop-count>
//
// In each group, the first byte specifies the target network number. If the gateway host is
// directly connected to that network, then the <hop-count> is zero and the <gateway-net> and
// <gateway-host> describe the gateways 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);
}
}
}

View File

@@ -75,7 +75,8 @@
<Compile Include="BCPLString.cs" />
<Compile Include="BreathOfLife.cs" />
<Compile Include="BSPManager.cs" />
<Compile Include="CopyDiskServer.cs" />
<Compile Include="CopyDisk\CopyDiskServer.cs" />
<Compile Include="CopyDisk\DiabloPack.cs" />
<Compile Include="DirectoryServices.cs" />
<Compile Include="EchoProtocol.cs" />
<Compile Include="Entrypoint.cs" />
@@ -95,6 +96,15 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup />
<ItemGroup>
<None Include="Disks\diag.dsk">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="Disks\tdisk8.dsk">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -1,50 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
#define LOGGING_ENABLED
using System;
using System.IO;
namespace IFS.Logging
{
/// <summary>
/// Specifies a component to specify logging for
/// </summary>
[Flags]
public enum LogLevel
public enum LogComponent
{
None = 0,
Normal = 1,
Warning = 2,
Error = 4,
DroppedPacket = 8,
InvalidPacket = 0x10,
UnhandledProtocol = 0x20,
HandledProtocol = 0x40,
DuplicateHostNumber = 0x80,
BSPLostPacket = 0x100,
All = 0x7fffffff,
Ethernet = 0x1,
RTP = 0x2,
BSP = 0x4,
MiscServices = 0x8,
CopyDisk = 0x10,
DirectoryServices = 0x20,
PUP = 0x40,
All = 0x7fffffff
}
/// <summary>
/// Specifies the type (or severity) of a given log message
/// </summary>
[Flags]
public enum LogType
{
None = 0,
Normal = 0x1,
Warning = 0x2,
Error = 0x4,
Verbose = 0x8,
All = 0x7fffffff
}
/// <summary>
/// Provides basic functionality for logging messages of all types.
/// </summary>
public static class Log
{
static Log()
{
_level = LogLevel.All;
// TODO: make configurable
_components = LogComponent.All;
_type = LogType.Normal;
//_logStream = new StreamWriter("log.txt");
}
public static LogLevel Level
public static LogComponent LogComponents
{
get { return _level; }
set { _level = value; }
get { return _components; }
set { _components = value; }
}
public static void Write(LogLevel level, string message)
#if LOGGING_ENABLED
/// <summary>
/// Logs a message without specifying type/severity for terseness;
/// will not log if Type has been set to None.
/// </summary>
/// <param name="level"></param>
/// <param name="message"></param>
/// <param name="args"></param>
public static void Write(LogComponent component, string message, params object[] args)
{
if ((level & _level) != 0)
Write(LogType.Normal, component, message, args);
}
public static void Write(LogType type, LogComponent component, string message, params object[] args)
{
if ((_type & type) != 0 &&
(_components & component) != 0)
{
//
// My log has something to tell you...
Console.WriteLine("{0}: {1} - {2}", DateTime.Now, level, message);
// TODO: color based on type, etc.
Console.WriteLine(component.ToString() + ": " + message, args);
if (_logStream != null)
{
_logStream.WriteLine(component.ToString() + ": " + message, args);
}
}
}
#else
public static void Write(LogComponent component, string message, params object[] args)
{
}
private static LogLevel _level;
public static void Write(LogType type, LogComponent component, string message, params object[] args)
{
}
#endif
private static LogComponent _components;
private static LogType _type;
private static StreamWriter _logStream;
}
}

View File

@@ -59,7 +59,7 @@ namespace IFS
/// <param name="p"></param>
public override void RecvData(PUP p)
{
Log.Write(LogLevel.HandledProtocol, String.Format("Misc. protocol request is for {0}.", p.Type));
Log.Write(LogType.Verbose, LogComponent.MiscServices, String.Format("Misc. protocol request is for {0}.", p.Type));
switch (p.Type)
{
case PupType.StringTimeRequest:
@@ -79,7 +79,7 @@ namespace IFS
break;
default:
Log.Write(LogLevel.UnhandledProtocol, String.Format("Unhandled misc. protocol {0}", p.Type));
Log.Write(LogComponent.MiscServices, String.Format("Unhandled misc. protocol {0}", p.Type));
break;
}
}

View File

@@ -1,4 +1,5 @@
using System;
using IFS.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -8,6 +9,9 @@ using System.Threading.Tasks;
namespace IFS
{
/// <summary>
/// All of the different types of PUPs.
/// </summary>
public enum PupType
{
// Basic types
@@ -128,6 +132,11 @@ namespace IFS
return a;
}
public override string ToString()
{
return String.Format("Net {0} Host {1} Socket {2}", Network, Host, Socket);
}
public byte Network;
public byte Host;
public UInt32 Socket;
@@ -278,7 +287,7 @@ namespace IFS
if (Checksum != 0xffff && cChecksum != Checksum)
{
// TODO: determine what to do with packets that are corrupted.
Logging.Log.Write(Logging.LogLevel.Warning, String.Format("PUP checksum is invalid. (got {0:x}, expected {1:x})", Checksum, cChecksum));
Log.Write(LogType.Warning, LogComponent.PUP, "PUP checksum is invalid. (got {0:x}, expected {1:x})", Checksum, cChecksum);
}
}
@@ -332,11 +341,9 @@ namespace IFS
private byte[] _rawData;
private const int MAX_PUP_SIZE = 532;
private const int PUP_HEADER_SIZE = 20;
private const int PUP_CHECKSUM_SIZE = 2;
public readonly static int MAX_PUP_SIZE = 532;
public readonly static int PUP_HEADER_SIZE = 20;
public readonly static int PUP_CHECKSUM_SIZE = 2;
}
public static class Helpers

View File

@@ -57,12 +57,25 @@ namespace IFS
}
public void SendPup(PUP p)
{
{
_pupPacketInterface.Send(p);
}
private void OnPupReceived(PUP pup)
{
//
// Filter out packets not destined for us.
// Even though we use pcap in non-promiscuous mode, if
// something else has set the interface to promiscuous mode, that
// setting may be overridden.
//
if (pup.DestinationPort.Host != 0 && // Not broadcast.
pup.DestinationPort.Host != DirectoryServices.Instance.LocalHost) // Not our address.
{
// Do nothing with this PUP.
return;
}
//
// Forward PUP on to registered endpoints.
//
@@ -72,14 +85,14 @@ namespace IFS
if (entry.ConnectionType == ConnectionType.Connectionless)
{
Log.Write(LogLevel.HandledProtocol, String.Format("Dispatching PUP to {0} handler.", entry.FriendlyName));
Log.Write(LogType.Verbose, LogComponent.PUP, "Dispatching PUP (source {0}, dest {1}) to {2} handler.", pup.SourcePort, pup.DestinationPort, entry.FriendlyName);
// Connectionless; just pass the PUP directly to the protocol
entry.ProtocolImplementation.RecvData(pup);
}
else
{
// RTP / BSP protocol. Pass this to the BSP handler to set up a channel.
Log.Write(LogLevel.HandledProtocol, String.Format("Dispatching PUP to BSP protocol for {0}.", entry.FriendlyName));
Log.Write(LogType.Verbose, LogComponent.PUP, "Dispatching PUP (source {0}, dest {1}) to BSP protocol for {0}.", pup.SourcePort, pup.DestinationPort, entry.FriendlyName);
//entry.ProtocolImplementation.RecvData(pup);
BSPManager.EstablishRendezvous(pup, (BSPProtocol)entry.ProtocolImplementation);
@@ -93,7 +106,7 @@ namespace IFS
else
{
// Not a protocol we handle; log it.
Log.Write(LogLevel.UnhandledProtocol | LogLevel.DroppedPacket, String.Format("Unhandled PUP protocol, socket {0}, dropped packet.", pup.DestinationPort.Socket));
Log.Write(LogType.Normal, LogComponent.PUP, "Unhandled PUP protocol, socket {0}, dropped packet.", pup.DestinationPort.Socket);
}
}

View File

@@ -8,6 +8,34 @@ using System.Threading.Tasks;
namespace IFS
{
/// <summary>
/// Custom attribute allowing specification of Word (16-bit) alignment
/// of a given field. (Alignment is byte-oriented by default).
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field)]
public class WordAligned : System.Attribute
{
public WordAligned()
{
}
}
/// <summary>
/// Custom attribute allowing static specification of size (in elements) of array
/// fields. Used during deserialization.
/// </summary>
public class ArrayLength : System.Attribute
{
public ArrayLength(int i)
{
Length = i;
}
public int Length;
}
public static class Serializer
{
@@ -27,6 +55,7 @@ namespace IFS
// - int
// - uint
// - BCPLString
// - byte[]
//
// Struct fields are serialized in the order they are defined in the struct. Only Public instance fields are considered.
// If any unsupported fields are present in the considered field types, an exception will be thrown.
@@ -38,6 +67,17 @@ namespace IFS
for (int i = 0; i < info.Length; i++)
{
// Check alignment of the field; if word aligned we need to ensure proper positioning of the stream.
if (IsWordAligned(info[i]))
{
if ((ms.Position % 2) != 0)
{
// Eat up a padding byte
ms.ReadByte();
}
}
// Now read in the appropriate type.
switch (info[i].FieldType.Name)
{
case "Byte":
@@ -74,6 +114,23 @@ namespace IFS
}
break;
case "Byte[]":
{
// The field MUST be annotated with a length value.
int length = GetArrayLength(info[i]);
if (length == -1)
{
throw new InvalidOperationException("Byte arrays must be annotated with an ArrayLength attribute to be deserialized into.");
}
byte[] value = new byte[length];
ms.Read(value, 0, value.Length);
info[i].SetValue(o, value);
}
break;
default:
throw new InvalidOperationException(String.Format("Type {0} is unsupported for deserialization.", info[i].FieldType.Name));
}
@@ -107,7 +164,17 @@ namespace IFS
for(int i=0;i<info.Length;i++)
{
switch(info[i].FieldType.Name)
// Check alignment of the field; if word aligned we need to pad as necessary to align the next field.
if (IsWordAligned(info[i]))
{
if ((ms.Position % 2) != 0)
{
// Write a padding byte
ms.WriteByte(0);
}
}
switch (info[i].FieldType.Name)
{
case "Byte":
ms.WriteByte((byte)info[i].GetValue(o));
@@ -149,6 +216,24 @@ namespace IFS
}
break;
case "Byte[]":
{
byte[] value = (byte[])(info[i].GetValue(o));
// Sanity check length of array vs. the specified annotation (if any -- not required
// for serialization)
int length = GetArrayLength(info[i]);
if (length > 0 && length != value.Length)
{
throw new InvalidOperationException("Array size does not match the size required by the ArraySize annotation.");
}
ms.Write(value, 0, value.Length);
}
break;
default:
throw new InvalidOperationException(String.Format("Type {0} is unsupported for serialization.", info[i].FieldType.Name));
}
@@ -156,5 +241,42 @@ namespace IFS
return ms.ToArray();
}
private static void SwapBytes(byte[] data)
{
for (int i = 0; i < data.Length; i += 2)
{
byte t = data[i];
data[i] = data[i + 1];
data[i + 1] = t;
}
}
private static bool IsWordAligned(FieldInfo field)
{
foreach(CustomAttributeData attribute in field.CustomAttributes)
{
if (attribute.AttributeType == typeof(WordAligned))
{
return true;
}
}
return false;
}
private static int GetArrayLength(FieldInfo field)
{
foreach (Attribute attribute in System.Attribute.GetCustomAttributes(field))
{
if (attribute is ArrayLength)
{
return ((ArrayLength)attribute).Length;
}
}
return -1;
}
}
}

View File

@@ -121,7 +121,7 @@ namespace IFS.Transport
else
{
// Log error, this should not happen.
Log.Write(LogLevel.Error, String.Format("PUP destination address {0} is unknown.", p.DestinationPort.Host));
Log.Write(LogType.Error, LogComponent.Ethernet, String.Format("PUP destination address {0} is unknown.", p.DestinationPort.Host));
}
}
@@ -163,12 +163,12 @@ namespace IFS.Transport
else
{
// Not for our network.
Log.Write(LogLevel.DroppedPacket, String.Format("PUP is for network {0}, dropping.", pup.DestinationPort.Network));
Log.Write(LogType.Verbose, LogComponent.Ethernet, "PUP is for network {0}, dropping.", pup.DestinationPort.Network);
}
}
else
{
Log.Write(LogLevel.DroppedPacket, String.Format("3mbit packet is not a PUP, dropping"));
Log.Write(LogType.Warning, LogComponent.Ethernet, "3mbit packet is not a PUP, dropping");
}
}
else
@@ -229,11 +229,11 @@ namespace IFS.Transport
//
if (_pupToEthernetMap[p.SourcePort.Host] != e.Ethernet.Source)
{
Log.Write(LogLevel.DuplicateHostNumber,
String.Format("Duplicate host ID {0} for MAC {1} (currently mapped to MAC {2})",
p.SourcePort.Host,
e.Ethernet.Source,
_pupToEthernetMap[p.SourcePort.Host]));
Log.Write(LogType.Error, LogComponent.Ethernet,
"Duplicate host ID {0} for MAC {1} (currently mapped to MAC {2})",
p.SourcePort.Host,
e.Ethernet.Source,
_pupToEthernetMap[p.SourcePort.Host]);
}
}
else