1
0
mirror of https://github.com/livingcomputermuseum/IFS.git synced 2026-01-29 05:11:05 +00:00
Files
livingcomputermuseum.IFS/PUP/CopyDisk/CopyDiskServer.cs

575 lines
24 KiB
C#

using IFS.BSP;
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
// TODO: keep track of workers to allow clean shutdown, management, etc.
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, "LCM 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)
{
return Path.Combine(Configuration.CopyDiskRoot, 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;
}
}