From ef7572c5945569f54659bf430a65cbe19970a876 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Sun, 10 Nov 2019 00:48:32 +0000 Subject: [PATCH] Fix detecting tape block size when tape reports a lower minimum size. --- .../.idea/contentModel.xml | 21 +- DiscImageChef.Core/Devices/Dumping/SSC.cs | 512 +++++++++++++----- 2 files changed, 374 insertions(+), 159 deletions(-) diff --git a/.idea/.idea.DiscImageChef/.idea/contentModel.xml b/.idea/.idea.DiscImageChef/.idea/contentModel.xml index cf0820a98..06ae227ef 100644 --- a/.idea/.idea.DiscImageChef/.idea/contentModel.xml +++ b/.idea/.idea.DiscImageChef/.idea/contentModel.xml @@ -1,23 +1,11 @@ - - - - - - + + - - - - - - - - @@ -105,7 +93,6 @@ - @@ -209,7 +196,6 @@ - @@ -410,7 +396,6 @@ - @@ -1235,7 +1220,6 @@ - @@ -1876,7 +1860,6 @@ - diff --git a/DiscImageChef.Core/Devices/Dumping/SSC.cs b/DiscImageChef.Core/Devices/Dumping/SSC.cs index baade5d0f..cd07ac277 100644 --- a/DiscImageChef.Core/Devices/Dumping/SSC.cs +++ b/DiscImageChef.Core/Devices/Dumping/SSC.cs @@ -44,6 +44,7 @@ using DiscImageChef.CommonTypes.Metadata; using DiscImageChef.CommonTypes.Structs; using DiscImageChef.Core.Logging; using DiscImageChef.Decoders.SCSI; +using DiscImageChef.Decoders.SCSI.SSC; using DiscImageChef.Devices; using Schemas; using MediaType = DiscImageChef.CommonTypes.MediaType; @@ -52,16 +53,14 @@ namespace DiscImageChef.Core.Devices.Dumping { partial class Dump { - /// - /// Dumps the tape from a SCSI Streaming device - /// + /// Dumps the tape from a SCSI Streaming device internal void Ssc() { FixedSense? fxSense; bool sense; uint blockSize; ulong blocks = 0; - MediaType dskType = MediaType.Unknown; + var dskType = MediaType.Unknown; DateTime start; DateTime end; double totalDuration = 0; @@ -73,21 +72,29 @@ namespace DiscImageChef.Core.Devices.Dumping fxSense = Sense.DecodeFixed(senseBuf, out string strSense); InitProgress?.Invoke(); - if(fxSense.HasValue && fxSense?.SenseKey != SenseKeys.NoSense) + + if(fxSense.HasValue && + fxSense?.SenseKey != SenseKeys.NoSense) { dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + StoppingErrorMessage?.Invoke("Drive has status error, please correct. Sense follows..." + Environment.NewLine + strSense); + return; } // Not in BOM/P - if(fxSense.HasValue && fxSense?.ASC == 0x00 && fxSense?.ASCQ != 0x00 && fxSense?.ASCQ != 0x04 && - fxSense?.SenseKey != SenseKeys.IllegalRequest) + if(fxSense.HasValue && + fxSense?.ASC == 0x00 && + fxSense?.ASCQ != 0x00 && + fxSense?.ASCQ != 0x04 && + fxSense?.SenseKey != SenseKeys.IllegalRequest) { dumpLog.WriteLine("Rewinding, please wait..."); PulseProgress?.Invoke("Rewinding, please wait..."); + // Rewind, let timeout apply dev.Rewind(out senseBuf, dev.Timeout, out duration); @@ -98,22 +105,25 @@ namespace DiscImageChef.Core.Devices.Dumping PulseProgress?.Invoke("Rewinding, please wait..."); dev.RequestSense(out senseBuf, dev.Timeout, out duration); fxSense = Sense.DecodeFixed(senseBuf, out strSense); - } - while(fxSense.HasValue && fxSense?.ASC == 0x00 && - (fxSense?.ASCQ == 0x1A || fxSense?.ASCQ != 0x04 || fxSense?.ASCQ != 0x00)); + } while(fxSense.HasValue && + fxSense?.ASC == 0x00 && + (fxSense?.ASCQ == 0x1A || fxSense?.ASCQ != 0x04 || fxSense?.ASCQ != 0x00)); dev.RequestSense(out senseBuf, dev.Timeout, out duration); fxSense = Sense.DecodeFixed(senseBuf, out strSense); // And yet, did not rewind! - if(fxSense.HasValue && (fxSense?.ASC == 0x00 && fxSense?.ASCQ != 0x04 && fxSense?.ASCQ != 0x00 || - fxSense?.ASC != 0x00)) + if(fxSense.HasValue && + (fxSense?.ASC == 0x00 && fxSense?.ASCQ != 0x04 && fxSense?.ASCQ != 0x00 || fxSense?.ASC != 0x00)) { StoppingErrorMessage?.Invoke("Drive could not rewind, please correct. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not rewind, please correct. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } } @@ -128,14 +138,18 @@ namespace DiscImageChef.Core.Devices.Dumping // Anyway, <=SCSI-1 tapes do not support partitions fxSense = Sense.DecodeFixed(senseBuf, out strSense); - if(fxSense.HasValue && (fxSense?.ASC == 0x20 && fxSense?.ASCQ != 0x00 || - fxSense?.ASC != 0x20 && fxSense?.SenseKey != SenseKeys.IllegalRequest)) + if(fxSense.HasValue && + (fxSense?.ASC == 0x20 && fxSense?.ASCQ != 0x00 || + fxSense?.ASC != 0x20 && fxSense?.SenseKey != SenseKeys.IllegalRequest)) { StoppingErrorMessage?.Invoke("Could not get position. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Could not get position. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } } @@ -146,16 +160,21 @@ namespace DiscImageChef.Core.Devices.Dumping { UpdateStatus?.Invoke("Drive not in partition 0. Rewinding, please wait..."); dumpLog.WriteLine("Drive not in partition 0. Rewinding, please wait..."); + // Rewind, let timeout apply sense = dev.Locate(out senseBuf, false, 0, 0, dev.Timeout, out duration); + if(sense) { StoppingErrorMessage?.Invoke("Drive could not rewind, please correct. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not rewind, please correct. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } @@ -167,33 +186,42 @@ namespace DiscImageChef.Core.Devices.Dumping PulseProgress?.Invoke("Rewinding, please wait..."); dev.RequestSense(out senseBuf, dev.Timeout, out duration); fxSense = Sense.DecodeFixed(senseBuf, out strSense); - } - while(fxSense.HasValue && fxSense?.ASC == 0x00 && (fxSense?.ASCQ == 0x1A || fxSense?.ASCQ == 0x19)); + } while(fxSense.HasValue && + fxSense?.ASC == 0x00 && + (fxSense?.ASCQ == 0x1A || fxSense?.ASCQ == 0x19)); // And yet, did not rewind! - if(fxSense.HasValue && (fxSense?.ASC == 0x00 && fxSense?.ASCQ != 0x04 && fxSense?.ASCQ != 0x00 || - fxSense?.ASC != 0x00)) + if(fxSense.HasValue && + (fxSense?.ASC == 0x00 && fxSense?.ASCQ != 0x04 && fxSense?.ASCQ != 0x00 || fxSense?.ASC != 0x00)) { StoppingErrorMessage?.Invoke("Drive could not rewind, please correct. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not rewind, please correct. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } sense = dev.ReadPosition(out cmdBuf, out senseBuf, SscPositionForms.Short, dev.Timeout, out duration); + if(sense) { fxSense = Sense.DecodeFixed(senseBuf, out strSense); + StoppingErrorMessage?.Invoke("Drive could not rewind, please correct. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not rewind, please correct. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } @@ -202,6 +230,7 @@ namespace DiscImageChef.Core.Devices.Dumping { StoppingErrorMessage?.Invoke("Drive could not rewind to partition 0 but no error occurred..."); dumpLog.WriteLine("Drive could not rewind to partition 0 but no error occurred..."); + return; } } @@ -215,27 +244,36 @@ namespace DiscImageChef.Core.Devices.Dumping byte[] mode10Data = null; UpdateStatus?.Invoke("Requesting MODE SENSE (10)."); + sense = dev.ModeSense10(out cmdBuf, out senseBuf, false, true, ScsiModeSensePageControl.Current, 0x3F, 0xFF, 5, out duration); - if(!sense || dev.Error) + + if(!sense || + dev.Error) sense = dev.ModeSense10(out cmdBuf, out senseBuf, false, true, ScsiModeSensePageControl.Current, 0x3F, 0x00, 5, out duration); Modes.DecodedMode? decMode = null; - if(!sense && !dev.Error) + if(!sense && + !dev.Error) if(Modes.DecodeMode10(cmdBuf, dev.ScsiType).HasValue) decMode = Modes.DecodeMode10(cmdBuf, dev.ScsiType); UpdateStatus?.Invoke("Requesting MODE SENSE (6)."); + sense = dev.ModeSense6(out cmdBuf, out senseBuf, false, ScsiModeSensePageControl.Current, 0x3F, 0x00, 5, out duration); + if(sense || dev.Error) sense = dev.ModeSense6(out cmdBuf, out senseBuf, false, ScsiModeSensePageControl.Current, 0x3F, 0x00, 5, out duration); - if(sense || dev.Error) sense = dev.ModeSense(out cmdBuf, out senseBuf, 5, out duration); - if(!sense && !dev.Error) + if(sense || dev.Error) + sense = dev.ModeSense(out cmdBuf, out senseBuf, 5, out duration); + + if(!sense && + !dev.Error) if(Modes.DecodeMode6(cmdBuf, dev.ScsiType).HasValue) decMode = Modes.DecodeMode6(cmdBuf, dev.ScsiType); @@ -243,29 +281,47 @@ namespace DiscImageChef.Core.Devices.Dumping if(decMode.HasValue) { scsiMediumTypeTape = (byte)decMode.Value.Header.MediumType; - if(decMode.Value.Header.BlockDescriptors != null && decMode.Value.Header.BlockDescriptors.Length >= 1) + + if(decMode.Value.Header.BlockDescriptors != null && + decMode.Value.Header.BlockDescriptors.Length >= 1) scsiDensityCodeTape = (byte)decMode.Value.Header.BlockDescriptors[0].Density; + blockSize = decMode.Value.Header.BlockDescriptors?[0].BlockLength ?? 0; UpdateStatus?.Invoke($"Device reports {blocks} blocks ({blocks * blockSize} bytes)."); } - else blockSize = 1; + else + blockSize = 1; - if(blockSize == 0) blockSize = 1; + if(!dev.ReadBlockLimits(out cmdBuf, out senseBuf, dev.Timeout, out _)) + { + BlockLimits.BlockLimitsData? blockLimits = BlockLimits.Decode(cmdBuf); + + if(blockLimits != null && + blockLimits.Value.minBlockLen > blockSize) + { + blockSize = blockLimits.Value.minBlockLen; + } + } + + if(blockSize == 0) + blockSize = 1; if(dskType == MediaType.Unknown) dskType = MediaTypeFromScsi.Get((byte)dev.ScsiType, dev.Manufacturer, dev.Model, scsiMediumTypeTape, scsiDensityCodeTape, blocks, blockSize); - if(dskType == MediaType.Unknown) dskType = MediaType.UnknownTape; + + if(dskType == MediaType.Unknown) + dskType = MediaType.UnknownTape; UpdateStatus?.Invoke($"SCSI device type: {dev.ScsiType}."); UpdateStatus?.Invoke($"SCSI medium type: {scsiMediumTypeTape}."); UpdateStatus?.Invoke($"SCSI density type: {scsiDensityCodeTape}."); UpdateStatus?.Invoke($"Media identified as {dskType}."); - dumpLog.WriteLine("SCSI device type: {0}.", dev.ScsiType); - dumpLog.WriteLine("SCSI medium type: {0}.", scsiMediumTypeTape); - dumpLog.WriteLine("SCSI density type: {0}.", scsiDensityCodeTape); + dumpLog.WriteLine("SCSI device type: {0}.", dev.ScsiType); + dumpLog.WriteLine("SCSI medium type: {0}.", scsiMediumTypeTape); + dumpLog.WriteLine("SCSI density type: {0}.", scsiDensityCodeTape); dumpLog.WriteLine("Media identified as {0}.", dskType); bool endOfMedia = false; @@ -277,50 +333,68 @@ namespace DiscImageChef.Core.Devices.Dumping uint transferLen = blockSize; firstRead: + sense = dev.Read6(out cmdBuf, out senseBuf, false, fixedLen, transferLen, blockSize, dev.Timeout, out duration); + if(sense) { fxSense = Sense.DecodeFixed(senseBuf, out strSense); + if(fxSense.HasValue) if(fxSense?.SenseKey == SenseKeys.IllegalRequest) { sense = dev.Space(out senseBuf, SscSpaceCodes.LogicalBlock, -1, dev.Timeout, out duration); + if(sense) { fxSense = Sense.DecodeFixed(senseBuf, out strSense); - if(!fxSense.HasValue || !fxSense?.EOM == true) + + if(!fxSense.HasValue || + !fxSense?.EOM == true) { StoppingErrorMessage?.Invoke("Drive could not return back. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not return back. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0} ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } } fixedLen = true; transferLen = 1; + sense = dev.Read6(out cmdBuf, out senseBuf, false, fixedLen, transferLen, blockSize, dev.Timeout, out duration); + if(sense) { fxSense = Sense.DecodeFixed(senseBuf, out strSense); + StoppingErrorMessage?.Invoke("Drive could not read. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not read. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0} ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } } - else if(fxSense?.ASC == 0x00 && fxSense?.ASCQ == 0x00 && fxSense?.ILI == true && + else if(fxSense?.ASC == 0x00 && + fxSense?.ASCQ == 0x00 && + fxSense?.ILI == true && fxSense?.InformationValid == true) { blockSize = (uint)((int)blockSize - BitConverter.ToInt32(BitConverter.GetBytes(fxSense.Value.Information), 0)); + transferLen = blockSize; UpdateStatus?.Invoke($"Blocksize changed to {blockSize} bytes at block {currentBlock}"); @@ -328,17 +402,22 @@ namespace DiscImageChef.Core.Devices.Dumping sense = dev.Space(out senseBuf, SscSpaceCodes.LogicalBlock, -1, dev.Timeout, out duration); + totalDuration += duration; if(sense) { fxSense = Sense.DecodeFixed(senseBuf, out strSense); + StoppingErrorMessage?.Invoke("Drive could not go back one block. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not go back one block. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } @@ -348,41 +427,55 @@ namespace DiscImageChef.Core.Devices.Dumping { StoppingErrorMessage?.Invoke("Drive could not read. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not read. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0} ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } else { StoppingErrorMessage?.Invoke("Cannot read device, don't know why, exiting..."); dumpLog.WriteLine("Cannot read device, don't know why, exiting..."); + return; } } sense = dev.Space(out senseBuf, SscSpaceCodes.LogicalBlock, -1, dev.Timeout, out duration); + if(sense) { fxSense = Sense.DecodeFixed(senseBuf, out strSense); - if(!fxSense.HasValue || !fxSense?.EOM == true) + + if(!fxSense.HasValue || + !fxSense?.EOM == true) { StoppingErrorMessage?.Invoke("Drive could not return back. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not return back. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0} ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } } DumpHardwareType currentTry = null; ExtentsULong extents = null; + ResumeSupport.Process(true, dev.IsRemovable, blocks, dev.Manufacturer, dev.Model, dev.Serial, dev.PlatformId, ref resume, ref currentTry, ref extents, true); - if(currentTry == null || extents == null) + + if(currentTry == null || + extents == null) { StoppingErrorMessage?.Invoke("Could not process resume file, not continuing..."); + return; } @@ -409,9 +502,11 @@ namespace DiscImageChef.Core.Devices.Dumping dumpLog.WriteLine("LOCATE LONG works."); } } - else fxSense = Sense.DecodeFixed(senseBuf, out strSense); + else + fxSense = Sense.DecodeFixed(senseBuf, out strSense); } - else fxSense = Sense.DecodeFixed(senseBuf, out strSense); + else + fxSense = Sense.DecodeFixed(senseBuf, out strSense); sense = dev.Locate(out senseBuf, 1, dev.Timeout, out _); @@ -430,14 +525,17 @@ namespace DiscImageChef.Core.Devices.Dumping dumpLog.WriteLine("LOCATE works."); } } - else fxSense = Sense.DecodeFixed(senseBuf, out strSense); + else + fxSense = Sense.DecodeFixed(senseBuf, out strSense); } - else fxSense = Sense.DecodeFixed(senseBuf, out strSense); + else + fxSense = Sense.DecodeFixed(senseBuf, out strSense); if(resume.NextBlock > 0) { UpdateStatus?.Invoke($"Positioning tape to block {resume.NextBlock}."); dumpLog.WriteLine("Positioning tape to block {0}.", resume.NextBlock); + if(canLocateLong) { sense = dev.Locate16(out senseBuf, resume.NextBlock, dev.Timeout, out _); @@ -450,17 +548,21 @@ namespace DiscImageChef.Core.Devices.Dumping { if(!force) { - dumpLog - .WriteLine("Could not check current position, unable to resume. If you want to continue use force."); - StoppingErrorMessage - ?.Invoke("Could not check current position, unable to resume. If you want to continue use force."); + dumpLog. + WriteLine("Could not check current position, unable to resume. If you want to continue use force."); + + StoppingErrorMessage?. + Invoke("Could not check current position, unable to resume. If you want to continue use force."); + return; } - dumpLog - .WriteLine("Could not check current position, unable to resume. Dumping from the start."); - ErrorMessage - ?.Invoke("Could not check current position, unable to resume. Dumping from the start."); + dumpLog. + WriteLine("Could not check current position, unable to resume. Dumping from the start."); + + ErrorMessage?. + Invoke("Could not check current position, unable to resume. Dumping from the start."); + canLocateLong = false; } else @@ -471,17 +573,21 @@ namespace DiscImageChef.Core.Devices.Dumping { if(!force) { - dumpLog - .WriteLine("Current position is not as expected, unable to resume. If you want to continue use force."); - StoppingErrorMessage - ?.Invoke("Current position is not as expected, unable to resume. If you want to continue use force."); + dumpLog. + WriteLine("Current position is not as expected, unable to resume. If you want to continue use force."); + + StoppingErrorMessage?. + Invoke("Current position is not as expected, unable to resume. If you want to continue use force."); + return; } - dumpLog - .WriteLine("Current position is not as expected, unable to resume. Dumping from the start."); - ErrorMessage - ?.Invoke("Current position is not as expected, unable to resume. Dumping from the start."); + dumpLog. + WriteLine("Current position is not as expected, unable to resume. Dumping from the start."); + + ErrorMessage?. + Invoke("Current position is not as expected, unable to resume. Dumping from the start."); + canLocateLong = false; } } @@ -490,10 +596,12 @@ namespace DiscImageChef.Core.Devices.Dumping { if(!force) { - dumpLog - .WriteLine("Cannot reposition tape, unable to resume. If you want to continue use force."); - StoppingErrorMessage - ?.Invoke("Cannot reposition tape, unable to resume. If you want to continue use force."); + dumpLog. + WriteLine("Cannot reposition tape, unable to resume. If you want to continue use force."); + + StoppingErrorMessage?. + Invoke("Cannot reposition tape, unable to resume. If you want to continue use force."); + return; } @@ -514,17 +622,21 @@ namespace DiscImageChef.Core.Devices.Dumping { if(!force) { - dumpLog - .WriteLine("Could not check current position, unable to resume. If you want to continue use force."); - StoppingErrorMessage - ?.Invoke("Could not check current position, unable to resume. If you want to continue use force."); + dumpLog. + WriteLine("Could not check current position, unable to resume. If you want to continue use force."); + + StoppingErrorMessage?. + Invoke("Could not check current position, unable to resume. If you want to continue use force."); + return; } - dumpLog - .WriteLine("Could not check current position, unable to resume. Dumping from the start."); - ErrorMessage - ?.Invoke("Could not check current position, unable to resume. Dumping from the start."); + dumpLog. + WriteLine("Could not check current position, unable to resume. Dumping from the start."); + + ErrorMessage?. + Invoke("Could not check current position, unable to resume. Dumping from the start."); + canLocate = false; } else @@ -535,17 +647,21 @@ namespace DiscImageChef.Core.Devices.Dumping { if(!force) { - dumpLog - .WriteLine("Current position is not as expected, unable to resume. If you want to continue use force."); - StoppingErrorMessage - ?.Invoke("Current position is not as expected, unable to resume. If you want to continue use force."); + dumpLog. + WriteLine("Current position is not as expected, unable to resume. If you want to continue use force."); + + StoppingErrorMessage?. + Invoke("Current position is not as expected, unable to resume. If you want to continue use force."); + return; } - dumpLog - .WriteLine("Current position is not as expected, unable to resume. Dumping from the start."); - ErrorMessage - ?.Invoke("Current position is not as expected, unable to resume. Dumping from the start."); + dumpLog. + WriteLine("Current position is not as expected, unable to resume. Dumping from the start."); + + ErrorMessage?. + Invoke("Current position is not as expected, unable to resume. Dumping from the start."); + canLocate = false; } } @@ -554,10 +670,12 @@ namespace DiscImageChef.Core.Devices.Dumping { if(!force) { - dumpLog - .WriteLine("Cannot reposition tape, unable to resume. If you want to continue use force."); - StoppingErrorMessage - ?.Invoke("Cannot reposition tape, unable to resume. If you want to continue use force."); + dumpLog. + WriteLine("Cannot reposition tape, unable to resume. If you want to continue use force."); + + StoppingErrorMessage?. + Invoke("Cannot reposition tape, unable to resume. If you want to continue use force."); + return; } @@ -570,9 +688,12 @@ namespace DiscImageChef.Core.Devices.Dumping { if(!force) { - dumpLog.WriteLine("Cannot reposition tape, unable to resume. If you want to continue use force."); - StoppingErrorMessage - ?.Invoke("Cannot reposition tape, unable to resume. If you want to continue use force."); + dumpLog. + WriteLine("Cannot reposition tape, unable to resume. If you want to continue use force."); + + StoppingErrorMessage?. + Invoke("Cannot reposition tape, unable to resume. If you want to continue use force."); + return; } @@ -583,8 +704,7 @@ namespace DiscImageChef.Core.Devices.Dumping } else { - sense = canLocateLong - ? dev.Locate16(out senseBuf, false, 0, 0, dev.Timeout, out duration) + sense = canLocateLong ? dev.Locate16(out senseBuf, false, 0, 0, dev.Timeout, out duration) : dev.Locate(out senseBuf, false, 0, 0, dev.Timeout, out duration); do @@ -593,31 +713,38 @@ namespace DiscImageChef.Core.Devices.Dumping PulseProgress?.Invoke("Rewinding, please wait..."); dev.RequestSense(out senseBuf, dev.Timeout, out duration); fxSense = Sense.DecodeFixed(senseBuf, out strSense); - } - while(fxSense.HasValue && fxSense?.ASC == 0x00 && (fxSense?.ASCQ == 0x1A || fxSense?.ASCQ == 0x19)); + } while(fxSense.HasValue && + fxSense?.ASC == 0x00 && + (fxSense?.ASCQ == 0x1A || fxSense?.ASCQ == 0x19)); // And yet, did not rewind! - if(fxSense.HasValue && (fxSense?.ASC == 0x00 && fxSense?.ASCQ != 0x00 && fxSense?.ASCQ != 0x04 || - fxSense?.ASC != 0x00)) + if(fxSense.HasValue && + (fxSense?.ASC == 0x00 && fxSense?.ASCQ != 0x00 && fxSense?.ASCQ != 0x04 || fxSense?.ASC != 0x00)) { StoppingErrorMessage?.Invoke("Drive could not rewind, please correct. Sense follows..." + Environment.NewLine + strSense); + dumpLog.WriteLine("Drive could not rewind, please correct. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } } bool ret = (outputPlugin as IWritableTapeImage).SetTape(); + // Cannot set image to tape mode if(!ret) { dumpLog.WriteLine("Error setting output image in tape mode, not continuing."); dumpLog.WriteLine(outputPlugin.ErrorMessage); + StoppingErrorMessage?.Invoke("Error setting output image in tape mode, not continuing." + Environment.NewLine + outputPlugin.ErrorMessage); + return; } @@ -628,45 +755,57 @@ namespace DiscImageChef.Core.Devices.Dumping { dumpLog.WriteLine("Error creating output image, not continuing."); dumpLog.WriteLine(outputPlugin.ErrorMessage); + StoppingErrorMessage?.Invoke("Error creating output image, not continuing." + Environment.NewLine + outputPlugin.ErrorMessage); + return; } start = DateTime.UtcNow; - MhddLog mhddLog = new MhddLog(outputPrefix + ".mhddlog.bin", dev, blocks, blockSize, 1); - IbgLog ibgLog = new IbgLog(outputPrefix + ".ibg", 0x0008); + var mhddLog = new MhddLog(outputPrefix + ".mhddlog.bin", dev, blocks, blockSize, 1); + var ibgLog = new IbgLog(outputPrefix + ".ibg", 0x0008); - TapeFile currentTapeFile = - new TapeFile {File = currentFile, FirstBlock = currentBlock, Partition = currentPartition}; - TapePartition currentTapePartition = - new TapePartition {Number = currentPartition, FirstBlock = currentBlock}; + var currentTapeFile = new TapeFile + { + File = currentFile, FirstBlock = currentBlock, Partition = currentPartition + }; - if((canLocate || canLocateLong) && resume.NextBlock > 0) + var currentTapePartition = new TapePartition + { + Number = currentPartition, FirstBlock = currentBlock + }; + + if((canLocate || canLocateLong) && + resume.NextBlock > 0) { currentBlock = resume.NextBlock; currentTapeFile = (outputPlugin as IWritableTapeImage).Files.FirstOrDefault(f => f.LastBlock == - (outputPlugin as IWritableTapeImage) - ?.Files.Max(g => g.LastBlock)); + (outputPlugin as IWritableTapeImage)? + .Files.Max(g => g.LastBlock)); currentTapePartition = (outputPlugin as IWritableTapeImage).TapePartitions.FirstOrDefault(p => p.LastBlock == (outputPlugin as - IWritableTapeImage) - ?.TapePartitions - .Max(g => g.LastBlock)); + IWritableTapeImage)?. + TapePartitions. + Max(g => g.LastBlock)); } - if(mode6Data != null) outputPlugin.WriteMediaTag(mode6Data, MediaTagType.SCSI_MODESENSE_6); - if(mode10Data != null) outputPlugin.WriteMediaTag(mode10Data, MediaTagType.SCSI_MODESENSE_10); + if(mode6Data != null) + outputPlugin.WriteMediaTag(mode6Data, MediaTagType.SCSI_MODESENSE_6); + + if(mode10Data != null) + outputPlugin.WriteMediaTag(mode10Data, MediaTagType.SCSI_MODESENSE_10); DateTime timeSpeedStart = DateTime.UtcNow; ulong currentSpeedSize = 0; double imageWriteDuration = 0; InitProgress?.Invoke(); + while(currentPartition < totalPartitions) { if(aborted) @@ -674,6 +813,7 @@ namespace DiscImageChef.Core.Devices.Dumping currentTry.Extents = ExtentsConverter.ToMetadata(extents); UpdateStatus?.Invoke("Aborted!"); dumpLog.WriteLine("Aborted!"); + break; } @@ -683,6 +823,7 @@ namespace DiscImageChef.Core.Devices.Dumping dumpLog.WriteLine("Finished partition {0}", currentPartition); currentTapeFile.LastBlock = currentBlock - 1; + if(currentTapeFile.LastBlock > currentTapeFile.FirstBlock) (outputPlugin as IWritableTapeImage).AddFile(currentTapeFile); @@ -694,11 +835,17 @@ namespace DiscImageChef.Core.Devices.Dumping if(currentPartition < totalPartitions) { currentFile++; + currentTapeFile = new TapeFile { File = currentFile, FirstBlock = currentBlock, Partition = currentPartition }; - currentTapePartition = new TapePartition {Number = currentPartition, FirstBlock = currentBlock}; + + currentTapePartition = new TapePartition + { + Number = currentPartition, FirstBlock = currentBlock + }; + UpdateStatus?.Invoke($"Seeking to partition {currentPartition}"); dev.Locate(out senseBuf, false, currentPartition, 0, dev.Timeout, out duration); totalDuration += duration; @@ -708,44 +855,61 @@ namespace DiscImageChef.Core.Devices.Dumping } #pragma warning disable RECS0018 // Comparison of floating point numbers with equality operator - if(currentSpeed > maxSpeed && currentSpeed != 0) maxSpeed = currentSpeed; - if(currentSpeed < minSpeed && currentSpeed != 0) minSpeed = currentSpeed; + if(currentSpeed > maxSpeed && + currentSpeed != 0) + maxSpeed = currentSpeed; + + if(currentSpeed < minSpeed && + currentSpeed != 0) + minSpeed = currentSpeed; #pragma warning restore RECS0018 // Comparison of floating point numbers with equality operator PulseProgress?.Invoke($"Reading block {currentBlock} ({currentSpeed:F3} MiB/sec.)"); sense = dev.Read6(out cmdBuf, out senseBuf, false, fixedLen, transferLen, blockSize, dev.Timeout, out duration); + totalDuration += duration; - if(sense && senseBuf?.Length != 0 && !ArrayHelpers.ArrayIsNullOrEmpty(senseBuf)) + if(sense && + senseBuf?.Length != 0 && + !ArrayHelpers.ArrayIsNullOrEmpty(senseBuf)) { fxSense = Sense.DecodeFixed(senseBuf, out strSense); - if(fxSense?.ASC == 0x00 && fxSense?.ASCQ == 0x00 && fxSense?.ILI == true && + if(fxSense?.ASC == 0x00 && + fxSense?.ASCQ == 0x00 && + fxSense?.ILI == true && fxSense?.InformationValid == true) { blockSize = (uint)((int)blockSize - BitConverter.ToInt32(BitConverter.GetBytes(fxSense.Value.Information), 0)); - if(!fixedLen) transferLen = blockSize; + + if(!fixedLen) + transferLen = blockSize; UpdateStatus?.Invoke($"Blocksize changed to {blockSize} bytes at block {currentBlock}"); dumpLog.WriteLine("Blocksize changed to {0} bytes at block {1}", blockSize, currentBlock); sense = dev.Space(out senseBuf, SscSpaceCodes.LogicalBlock, -1, dev.Timeout, out duration); + totalDuration += duration; if(sense) { fxSense = Sense.DecodeFixed(senseBuf, out strSense); + StoppingErrorMessage?.Invoke("Drive could not go back one block. Sense follows..." + Environment.NewLine + strSense); + outputPlugin.Close(); dumpLog.WriteLine("Drive could not go back one block. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); + return; } @@ -758,7 +922,9 @@ namespace DiscImageChef.Core.Devices.Dumping StoppingErrorMessage?.Invoke("Cannot dump a blank tape..."); outputPlugin.Close(); dumpLog.WriteLine("Cannot dump a blank tape..."); + return; + // For sure this is an end-of-tape/partition case SenseKeys.BlankCheck when fxSense?.ASC == 0x00 && (fxSense?.ASCQ == 0x02 || fxSense?.ASCQ == 0x05 || @@ -767,11 +933,13 @@ namespace DiscImageChef.Core.Devices.Dumping endOfMedia = true; UpdateStatus?.Invoke("Found end-of-tape/partition..."); dumpLog.WriteLine("Found end-of-tape/partition..."); + continue; case SenseKeys.BlankCheck: StoppingErrorMessage?.Invoke("Blank block found, end of tape?..."); endOfMedia = true; dumpLog.WriteLine("Blank block found, end of tape?..."); + continue; } @@ -783,6 +951,7 @@ namespace DiscImageChef.Core.Devices.Dumping endOfMedia = true; UpdateStatus?.Invoke("Found end-of-tape/partition..."); dumpLog.WriteLine("Found end-of-tape/partition..."); + continue; } @@ -793,6 +962,7 @@ namespace DiscImageChef.Core.Devices.Dumping (outputPlugin as IWritableTapeImage).AddFile(currentTapeFile); currentFile++; + currentTapeFile = new TapeFile { File = currentFile, FirstBlock = currentBlock, Partition = currentPartition @@ -800,27 +970,32 @@ namespace DiscImageChef.Core.Devices.Dumping UpdateStatus?.Invoke($"Changed to file {currentFile} at block {currentBlock}"); dumpLog.WriteLine("Changed to file {0} at block {1}", currentFile, currentBlock); + continue; } if(fxSense is null) { - StoppingErrorMessage - ?.Invoke($"Drive could not read block ${currentBlock}. Sense cannot be decoded, look at log for dump..."); + StoppingErrorMessage?. + Invoke($"Drive could not read block ${currentBlock}. Sense cannot be decoded, look at log for dump..."); + dumpLog.WriteLine($"Drive could not read block ${currentBlock}. Sense bytes follow..."); dumpLog.WriteLine(PrintHex.ByteArrayToHexArrayString(senseBuf, 32)); } else { - StoppingErrorMessage - ?.Invoke($"Drive could not read block ${currentBlock}. Sense follows...\n{fxSense?.SenseKey} {strSense}"); + StoppingErrorMessage?. + Invoke($"Drive could not read block ${currentBlock}. Sense follows...\n{fxSense?.SenseKey} {strSense}"); + dumpLog.WriteLine($"Drive could not read block ${currentBlock}. Sense follows..."); + dumpLog.WriteLine("Device not ready. Sense {0}h ASC {1:X2}h ASCQ {2:X2}h", fxSense?.SenseKey, fxSense?.ASC, fxSense?.ASCQ); } // TODO: Reset device after X errors - if(stopOnError) return; // TODO: Return more cleanly + if(stopOnError) + return; // TODO: Return more cleanly // Write empty data DateTime writeStart = DateTime.Now; @@ -846,7 +1021,9 @@ namespace DiscImageChef.Core.Devices.Dumping currentSpeedSize += blockSize; double elapsed = (DateTime.UtcNow - timeSpeedStart).TotalSeconds; - if(elapsed < 1) continue; + + if(elapsed < 1) + continue; currentSpeed = currentSpeedSize / (1048576 * elapsed); currentSpeedSize = 0; @@ -868,22 +1045,32 @@ namespace DiscImageChef.Core.Devices.Dumping EndProgress?.Invoke(); mhddLog.Close(); + ibgLog.Close(dev, blocks, blockSize, (end - start).TotalSeconds, currentSpeed * 1024, blockSize * (double)(blocks + 1) / 1024 / (totalDuration / 1000), devicePath); + UpdateStatus?.Invoke($"Dump finished in {(end - start).TotalSeconds} seconds."); - UpdateStatus - ?.Invoke($"Average dump speed {(double)blockSize * (double)(blocks + 1) / 1024 / (totalDuration / 1000):F3} KiB/sec."); - UpdateStatus - ?.Invoke($"Average write speed {(double)blockSize * (double)(blocks + 1) / 1024 / imageWriteDuration:F3} KiB/sec."); + + UpdateStatus?. + Invoke($"Average dump speed {(double)blockSize * (double)(blocks + 1) / 1024 / (totalDuration / 1000):F3} KiB/sec."); + + UpdateStatus?. + Invoke($"Average write speed {(double)blockSize * (double)(blocks + 1) / 1024 / imageWriteDuration:F3} KiB/sec."); + dumpLog.WriteLine("Dump finished in {0} seconds.", (end - start).TotalSeconds); + dumpLog.WriteLine("Average dump speed {0:F3} KiB/sec.", (double)blockSize * (double)(blocks + 1) / 1024 / (totalDuration / 1000)); + dumpLog.WriteLine("Average write speed {0:F3} KiB/sec.", (double)blockSize * (double)(blocks + 1) / 1024 / imageWriteDuration); #region Error handling - if(resume.BadBlocks.Count > 0 && !aborted && retryPasses > 0 && (canLocate || canLocateLong)) + if(resume.BadBlocks.Count > 0 && + !aborted && + retryPasses > 0 && + (canLocate || canLocateLong)) { int pass = 1; bool forward = false; @@ -901,6 +1088,7 @@ namespace DiscImageChef.Core.Devices.Dumping InitProgress?.Invoke(); repeatRetry: ulong[] tmpArray = resume.BadBlocks.ToArray(); + foreach(ulong badBlock in tmpArray) { if(aborted) @@ -908,6 +1096,7 @@ namespace DiscImageChef.Core.Devices.Dumping currentTry.Extents = ExtentsConverter.ToMetadata(extents); UpdateStatus?.Invoke("Aborted!"); dumpLog.WriteLine("Aborted!"); + break; } @@ -917,6 +1106,7 @@ namespace DiscImageChef.Core.Devices.Dumping UpdateStatus?.Invoke($"Positioning tape to block {badBlock}."); dumpLog.WriteLine($"Positioning tape to block {badBlock}."); + if(canLocateLong) { sense = dev.Locate16(out senseBuf, resume.NextBlock, dev.Timeout, out _); @@ -929,6 +1119,7 @@ namespace DiscImageChef.Core.Devices.Dumping { dumpLog.WriteLine("Could not check current position, continuing."); StoppingErrorMessage?.Invoke("Could not check current position, continuing."); + continue; } @@ -938,6 +1129,7 @@ namespace DiscImageChef.Core.Devices.Dumping { dumpLog.WriteLine("Current position is not as expected, continuing."); StoppingErrorMessage?.Invoke("Current position is not as expected, continuing."); + continue; } } @@ -945,6 +1137,7 @@ namespace DiscImageChef.Core.Devices.Dumping { dumpLog.WriteLine($"Cannot position tape to block {badBlock}."); ErrorMessage?.Invoke($"Cannot position tape to block {badBlock}."); + continue; } } @@ -960,6 +1153,7 @@ namespace DiscImageChef.Core.Devices.Dumping { dumpLog.WriteLine("Could not check current position, continuing."); StoppingErrorMessage?.Invoke("Could not check current position, continuing."); + continue; } @@ -969,6 +1163,7 @@ namespace DiscImageChef.Core.Devices.Dumping { dumpLog.WriteLine("Current position is not as expected, continuing."); StoppingErrorMessage?.Invoke("Current position is not as expected, continuing."); + continue; } } @@ -976,15 +1171,18 @@ namespace DiscImageChef.Core.Devices.Dumping { dumpLog.WriteLine($"Cannot position tape to block {badBlock}."); ErrorMessage?.Invoke($"Cannot position tape to block {badBlock}."); + continue; } } sense = dev.Read6(out cmdBuf, out senseBuf, false, fixedLen, transferLen, blockSize, dev.Timeout, out duration); + totalDuration += duration; - if(!sense && !dev.Error) + if(!sense && + !dev.Error) { resume.BadBlocks.Remove(badBlock); extents.Add(badBlock); @@ -992,15 +1190,19 @@ namespace DiscImageChef.Core.Devices.Dumping UpdateStatus?.Invoke($"Correctly retried block {badBlock} in pass {pass}."); dumpLog.WriteLine("Correctly retried block {0} in pass {1}.", badBlock, pass); } - else if(runningPersistent) outputPlugin.WriteSector(cmdBuf, badBlock); + else if(runningPersistent) + outputPlugin.WriteSector(cmdBuf, badBlock); } - if(pass < retryPasses && !aborted && resume.BadBlocks.Count > 0) + if(pass < retryPasses && + !aborted && + resume.BadBlocks.Count > 0) { pass++; forward = !forward; resume.BadBlocks.Sort(); resume.BadBlocks.Reverse(); + goto repeatRetry; } @@ -1014,11 +1216,17 @@ namespace DiscImageChef.Core.Devices.Dumping #endregion Error handling resume.BadBlocks.Sort(); - foreach(ulong bad in resume.BadBlocks) dumpLog.WriteLine("Block {0} could not be read.", bad); + + foreach(ulong bad in resume.BadBlocks) + dumpLog.WriteLine("Block {0} could not be read.", bad); + currentTry.Extents = ExtentsConverter.ToMetadata(extents); outputPlugin.SetDumpHardware(resume.Tries); - if(preSidecar != null) outputPlugin.SetCicmMetadata(preSidecar); + + if(preSidecar != null) + outputPlugin.SetCicmMetadata(preSidecar); + dumpLog.WriteLine("Closing output file."); UpdateStatus?.Invoke("Closing output file."); DateTime closeStart = DateTime.Now; @@ -1031,6 +1239,7 @@ namespace DiscImageChef.Core.Devices.Dumping { UpdateStatus?.Invoke("Aborted!"); dumpLog.WriteLine("Aborted!"); + return; } @@ -1038,20 +1247,24 @@ namespace DiscImageChef.Core.Devices.Dumping { UpdateStatus?.Invoke("Aborted!"); dumpLog.WriteLine("Aborted!"); + return; } double totalChkDuration = 0; + if(!nometadata) { UpdateStatus?.Invoke("Creating sidecar."); dumpLog.WriteLine("Creating sidecar."); - FiltersList filters = new FiltersList(); + var filters = new FiltersList(); IFilter filter = filters.GetFilter(outputPath); IMediaImage inputPlugin = ImageFormat.Detect(filter); + if(!inputPlugin.Open(filter)) { StoppingErrorMessage?.Invoke("Could not open created image."); + return; } @@ -1069,9 +1282,12 @@ namespace DiscImageChef.Core.Devices.Dumping totalChkDuration = (end - chkStart).TotalMilliseconds; UpdateStatus?.Invoke($"Sidecar created in {(end - chkStart).TotalSeconds} seconds."); - UpdateStatus - ?.Invoke($"Average checksum speed {(double)blockSize * (double)(blocks + 1) / 1024 / (totalChkDuration / 1000):F3} KiB/sec."); + + UpdateStatus?. + Invoke($"Average checksum speed {(double)blockSize * (double)(blocks + 1) / 1024 / (totalChkDuration / 1000):F3} KiB/sec."); + dumpLog.WriteLine("Sidecar created in {0} seconds.", (end - chkStart).TotalSeconds); + dumpLog.WriteLine("Average checksum speed {0:F3} KiB/sec.", (double)blockSize * (double)(blocks + 1) / 1024 / (totalChkDuration / 1000)); @@ -1082,52 +1298,68 @@ namespace DiscImageChef.Core.Devices.Dumping } List<(ulong start, string type)> filesystems = new List<(ulong start, string type)>(); + if(sidecar.BlockMedia[0].FileSystemInformation != null) filesystems.AddRange(from partition in sidecar.BlockMedia[0].FileSystemInformation - where partition.FileSystems != null - from fileSystem in partition.FileSystems + where partition.FileSystems != null from fileSystem in partition.FileSystems select (partition.StartSector, fileSystem.Type)); if(filesystems.Count > 0) - foreach(var filesystem in filesystems.Select(o => new {o.start, o.type}).Distinct()) + foreach(var filesystem in filesystems.Select(o => new + { + o.start, o.type + }).Distinct()) { UpdateStatus?.Invoke($"Found filesystem {filesystem.type} at sector {filesystem.start}"); dumpLog.WriteLine("Found filesystem {0} at sector {1}", filesystem.type, filesystem.start); } sidecar.BlockMedia[0].Dimensions = Dimensions.DimensionsFromMediaType(dskType); + CommonTypes.Metadata.MediaType.MediaTypeToString(dskType, out string xmlDskTyp, out string xmlDskSubTyp); + sidecar.BlockMedia[0].DiskType = xmlDskTyp; sidecar.BlockMedia[0].DiskSubType = xmlDskSubTyp; + // TODO: Implement device firmware revision - if(!dev.IsRemovable || dev.IsUsb) - if(dev.Type == DeviceType.ATAPI) sidecar.BlockMedia[0].Interface = "ATAPI"; - else if(dev.IsUsb) sidecar.BlockMedia[0].Interface = "USB"; - else if(dev.IsFireWire) sidecar.BlockMedia[0].Interface = "FireWire"; - else sidecar.BlockMedia[0].Interface = "SCSI"; + if(!dev.IsRemovable || + dev.IsUsb) + if(dev.Type == DeviceType.ATAPI) + sidecar.BlockMedia[0].Interface = "ATAPI"; + else if(dev.IsUsb) + sidecar.BlockMedia[0].Interface = "USB"; + else if(dev.IsFireWire) + sidecar.BlockMedia[0].Interface = "FireWire"; + else + sidecar.BlockMedia[0].Interface = "SCSI"; + sidecar.BlockMedia[0].LogicalBlocks = blocks; sidecar.BlockMedia[0].Manufacturer = dev.Manufacturer; sidecar.BlockMedia[0].Model = dev.Model; sidecar.BlockMedia[0].Serial = dev.Serial; sidecar.BlockMedia[0].Size = blocks * blockSize; - if(dev.IsRemovable) sidecar.BlockMedia[0].DumpHardwareArray = resume.Tries.ToArray(); + if(dev.IsRemovable) + sidecar.BlockMedia[0].DumpHardwareArray = resume.Tries.ToArray(); UpdateStatus?.Invoke("Writing metadata sidecar"); - FileStream xmlFs = new FileStream(outputPrefix + ".cicm.xml", FileMode.Create); + var xmlFs = new FileStream(outputPrefix + ".cicm.xml", FileMode.Create); - XmlSerializer xmlSer = new XmlSerializer(typeof(CICMMetadataType)); + var xmlSer = new XmlSerializer(typeof(CICMMetadataType)); xmlSer.Serialize(xmlFs, sidecar); xmlFs.Close(); } UpdateStatus?.Invoke(""); - UpdateStatus - ?.Invoke($"Took a total of {(end - start).TotalSeconds:F3} seconds ({totalDuration / 1000:F3} processing commands, {totalChkDuration / 1000:F3} checksumming, {imageWriteDuration:F3} writing, {(closeEnd - closeStart).TotalSeconds:F3} closing)."); - UpdateStatus - ?.Invoke($"Average speed: {(double)blockSize * (double)(blocks + 1) / 1048576 / (totalDuration / 1000):F3} MiB/sec."); + + UpdateStatus?. + Invoke($"Took a total of {(end - start).TotalSeconds:F3} seconds ({totalDuration / 1000:F3} processing commands, {totalChkDuration / 1000:F3} checksumming, {imageWriteDuration:F3} writing, {(closeEnd - closeStart).TotalSeconds:F3} closing)."); + + UpdateStatus?. + Invoke($"Average speed: {(double)blockSize * (double)(blocks + 1) / 1048576 / (totalDuration / 1000):F3} MiB/sec."); + UpdateStatus?.Invoke($"Fastest speed burst: {maxSpeed:F3} MiB/sec."); UpdateStatus?.Invoke($"Slowest speed burst: {minSpeed:F3} MiB/sec."); UpdateStatus?.Invoke($"{resume.BadBlocks.Count} sectors could not be read.");