mirror of
https://github.com/livingcomputermuseum/IFS.git
synced 2026-01-29 13:21:05 +00:00
575 lines
24 KiB
C#
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;
|
|
}
|
|
}
|