Click here to Skip to main content
15,896,348 members
Articles / Programming Languages / C#

SCSI Library in C# - Burn CDs and DVDs, Access Hard Disks, etc.

Rate me:
Please Sign up or sign in to vote.
4.77/5 (48 votes)
19 Jun 2017Ms-PL6 min read 146.3K   8.1K   146  
Ever wonder how programs like Nero work? They make their own SCSI libraries... like this!
using System;
using System.IO;
using System.Windows.Forms;
using Helper.IO;
using SCSI;
using SCSI.Multimedia;
using System.Threading;
using System.Runtime.InteropServices;
namespace ISOBurn
{
	public partial class FormMain : Form
	{
		private MultimediaDevice currentCD;
		private static readonly TimeSpan UPDATE_PAUSE = TimeSpan.FromMilliseconds(25);
		private static readonly TimeSpan SPEED_UPDATE_PAUSE = TimeSpan.FromMilliseconds(250);
		private ProgressInfo lastProgress;
		private int lastProgressReportTick;

		public FormMain() { this.InitializeComponent(); }

		[DllImport("User32.dll")]
		private static extern bool SetCursorPos(int X, int Y);

		private void RefreshCursor() { SetCursorPos(System.Windows.Forms.Control.MousePosition.X, System.Windows.Forms.Control.MousePosition.Y); }

		[DllImport("User32.dll", SetLastError = true)]
		private static extern bool EnableWindow(IntPtr hWnd, [MarshalAs(UnmanagedType.Bool)] bool bEnable);

		private void btnBurn_Click(object sender, EventArgs e)
		{
			var drive = (DriveInfo)this.cmbDevice.SelectedItem;
			var stream = new Win32FileStream(@"\\.\" + drive.Name.TrimEnd(Path.DirectorySeparatorChar), FileAccess.ReadWrite, FileShare.None, FileMode.Open, FileOptions.None);
			var spti = new SPTI(stream.SafeFileHandle, false);
			this.currentCD = new MultimediaDevice(spti, false);
			this.cmbDevice.Enabled = false;
			this.btnBrowse.Enabled = false;
			this.btnBurn.Enabled = false;
			this.txtFileName.ReadOnly = true;
			this.UseWaitCursor = true;
			this.bwCDBurner.RunWorkerAsync(new DoWorkInfo(this.currentCD));
			this.btnCancel.Enabled = true;
		}

		protected override void OnLoad(EventArgs e)
		{
			var drives = System.IO.DriveInfo.GetDrives();
			foreach (var drive in drives)
			{
				if (drive.DriveType == System.IO.DriveType.CDRom)
				{
					this.cmbDevice.Items.Add(drive);
					if (this.cmbDevice.SelectedItem == null) { this.cmbDevice.SelectedItem = drive; }
				}
			}


			base.OnLoad(e);
		}

		private class DoWorkInfo { public DoWorkInfo(MultimediaDevice device) { this.Device = device; } public MultimediaDevice Device;	}

		private class ProgressInfo
		{
			public ProgressInfo(string description, long completed, long total, BurnStage stage)
			{
				this.Description = description;
				this.Completed = completed;
				this.Total = total;
				this.Stage = stage;
			}
			public string Description;
			public long Completed; public long Total;
			public BurnStage Stage;
		}

		private enum BurnStage { Blanking, Burning, ClosingTrack, ClosingSession, }

		private void bwCDBurner_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
		{
			//The actual burning occurs here, asynchronously from the UI
			this.lastProgressReportTick = Environment.TickCount;
			var info = (DoWorkInfo)e.Argument;
			using (var mmstream = new SCSIStream(info.Device))
			{
				info.Device.SetCDSpeed(new SetCDSpeedCommand(ushort.MaxValue, ushort.MaxValue, RotationControl.CAV));

				//info.Device.PreventAllowMediumRemoval(new PreventAllowMediumRemovalCommand(true, false));
				try
				{
					int prevTick = Environment.TickCount;

					SenseData sense;
					if (info.Device.ReadDiscInformation().Erasable)
					{
						if (MessageBox.Show("Detected an erasable disc. Would you like to quickly erase it (highly recommended)?", "Erase Disc", MessageBoxButtons.YesNo, MessageBoxIcon.Information, MessageBoxDefaultButton.Button1) == DialogResult.Yes)
						{
							info.Device.Blank(new BlankCommand(BlankingType.BlankMinimal, true, 0));
							while ((sense = info.Device.RequestSense()).SenseKey == SenseKey.NotReady
								&& sense.AdditionalSenseCode == AdditionalSenseCode.LogicalUnitNotReady
								&& sense.AdditionalSenseCodeQualifier == (AdditionalSenseCodeQualifier)7)
							{
								int tick = Environment.TickCount;
								if (tick - prevTick > UPDATE_PAUSE.TotalMilliseconds)
								{
									this.bwCDBurner.ReportProgress((int)(sense.SenseKeySpecific.ProgressIndication.ProgressIndicationFraction * 100),
									  new ProgressInfo("Erasing CD (cannot be canceled)...", sense.SenseKeySpecific.ProgressIndication.ProgressIndication, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Blanking));
									tick = prevTick;
								}
								else { Thread.Sleep(Math.Min((int)UPDATE_PAUSE.TotalMilliseconds - (tick - prevTick), 0)); }
							}

							this.bwCDBurner.ReportProgress(100, new ProgressInfo("CD erased, starting burn...", ProgressIndicationBytes.ProgressIndicationDenominator, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Blanking));
						}
					}

					var tracks = info.Device.ReadAllTracksInformation();

					if (tracks[tracks.Length - 1].NextWritableAddress == null)
					{ throw new InvalidOperationException("Last track is not writable. Writing cannot continue."); }

					using (Stream fileStream = new FileStream(this.txtFileName.Text, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
					{
						if (tracks.Length > 0) { mmstream.Position = info.Device.SectorSize * tracks[tracks.Length - 1].NextWritableAddress.Value; }
						var buffer = new byte[info.Device.SectorSize];
						while (fileStream.Position < fileStream.Length && !this.bwCDBurner.CancellationPending)
						{
							int tick = Environment.TickCount;
							if (tick - prevTick > UPDATE_PAUSE.TotalMilliseconds)
							{
								fileStream.Read(buffer, 0, buffer.Length);
								mmstream.Write(buffer, 0, buffer.Length);
								this.bwCDBurner.ReportProgress((int)(fileStream.Position * 100 / fileStream.Length), new ProgressInfo("Burning data...", fileStream.Position, fileStream.Length, BurnStage.Burning));
								tick = prevTick;
							}
							else { Thread.Sleep(Math.Min((int)UPDATE_PAUSE.TotalMilliseconds - (tick - prevTick), 0)); }
						}

						this.bwCDBurner.ReportProgress((int)(fileStream.Position * 100 / fileStream.Length), new ProgressInfo("Flushing...", fileStream.Position, fileStream.Length, BurnStage.Burning));
						this.bwCDBurner.ReportProgress(100, new ProgressInfo("Burn completed...", ProgressIndicationBytes.ProgressIndicationDenominator, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.Burning));
						mmstream.Flush();
					}


					this.bwCDBurner.ReportProgress(0, new ProgressInfo("Closing track...", 0, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.ClosingTrack));
					info.Device.CloseTrackOrSession(new CloseSessionOrTrackCommand(true, TrackSessionCloseFunction.CloseLogicalTrack, 1));
					while ((sense = info.Device.RequestSense()).SenseKey == SenseKey.NotReady
						&& sense.AdditionalSenseCode == AdditionalSenseCode.LogicalUnitNotReady
						&& sense.AdditionalSenseCodeQualifier == (AdditionalSenseCodeQualifier)7)
					{
						int tick = Environment.TickCount;
						if (tick - prevTick > UPDATE_PAUSE.TotalMilliseconds)
						{
							this.bwCDBurner.ReportProgress((int)(sense.SenseKeySpecific.ProgressIndication.ProgressIndicationFraction * 100),
							  new ProgressInfo("Closing session (cannot be canceled)...", sense.SenseKeySpecific.ProgressIndication.ProgressIndication, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.ClosingTrack));
							prevTick = tick;
						}
						else { Thread.Sleep(Math.Min((int)UPDATE_PAUSE.TotalMilliseconds - (tick - prevTick), 0)); }
					}
					this.bwCDBurner.ReportProgress(100, new ProgressInfo("Track closed.", ProgressIndicationBytes.ProgressIndicationDenominator, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.ClosingTrack));


					this.bwCDBurner.ReportProgress(0, new ProgressInfo("Closing session...", 0, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.ClosingSession));
					info.Device.CloseTrackOrSession(new CloseSessionOrTrackCommand(true, TrackSessionCloseFunction.CloseSessionOrStopBGFormat, 1));
					while ((sense = info.Device.RequestSense()).SenseKey == SenseKey.NotReady
						&& sense.AdditionalSenseCode == AdditionalSenseCode.LogicalUnitNotReady
						&& sense.AdditionalSenseCodeQualifier == (AdditionalSenseCodeQualifier)7)
					{
						int tick = Environment.TickCount;
						if (tick - prevTick > UPDATE_PAUSE.TotalMilliseconds)
						{
							this.bwCDBurner.ReportProgress((int)(sense.SenseKeySpecific.ProgressIndication.ProgressIndicationFraction * 100),
							  new ProgressInfo("Closing session (cannot be canceled)...", sense.SenseKeySpecific.ProgressIndication.ProgressIndication, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.ClosingSession));
							prevTick = tick;
						}
						else { Thread.Sleep(Math.Min((int)UPDATE_PAUSE.TotalMilliseconds - (tick - prevTick), 0)); }
					}
					this.bwCDBurner.ReportProgress(100, new ProgressInfo("Session closed.", ProgressIndicationBytes.ProgressIndicationDenominator, ProgressIndicationBytes.ProgressIndicationDenominator, BurnStage.ClosingSession));
				}
				finally
				{
					//info.Device.PreventAllowMediumRemoval(new PreventAllowMediumRemovalCommand(false, false));
				}
			}

			e.Result = e.Argument;
		}

		private void bwCDBurner_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
		{
			var state = (ProgressInfo)e.UserState;

			bool setNewProgress;
			var elapsed = TimeSpan.FromMilliseconds(Environment.TickCount - this.lastProgressReportTick);
			if (this.lastProgress != null && this.lastProgress.Stage == state.Stage && state.Stage == BurnStage.Burning)
			{
				double bytesPerSecond = (state.Completed - this.lastProgress.Completed) / elapsed.TotalSeconds;
				if (elapsed >= SPEED_UPDATE_PAUSE && !double.IsInfinity(bytesPerSecond) && !double.IsNaN(bytesPerSecond))
				{
					this.lblInfo.Text = string.Format("{0} ({1:N0} KB/s)", state.Description, bytesPerSecond / 1024);
					setNewProgress = true;
				}
				else { setNewProgress = false; }
			}
			else
			{
				this.lblInfo.Text = state.Description;
				setNewProgress = true;
			}

			if (setNewProgress)
			{
				this.lastProgress = state;
				this.lastProgressReportTick = Environment.TickCount;
			}

			if (state.Total > int.MaxValue) { this.pbMain.Maximum = int.MaxValue; this.pbMain.Value = (int)((double)state.Completed * int.MaxValue / state.Total); }
			else { this.pbMain.Maximum = (int)state.Total; this.pbMain.Value = (int)state.Completed; }
		}

		private void bwCDBurner_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
		{
			this.UseWaitCursor = false;
			this.currentCD.Dispose();
			this.currentCD = null;
			if (e.Error != null)
			{
				MessageBox.Show(string.Format("{0}", e.Error), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
			}
			else if (e.Cancelled)
			{ this.lblInfo.Text = "Burn canceled. You may need to eject and re-insert the CD to refresh Explorer."; }
			else
			{ this.lblInfo.Text = "Burn successful. You may need to eject and re-insert the CD to refresh Explorer."; }
			this.pbMain.Value = this.pbMain.Minimum;
			this.btnBurn.Enabled = true;
			this.btnBrowse.Enabled = true;
			this.txtFileName.ReadOnly = false;
			this.btnCancel.Enabled = false;
			this.cmbDevice.Enabled = true;
		}

		private void btnCancel_Click(object sender, EventArgs e)
		{
			if (this.bwCDBurner.IsBusy)
			{
				this.bwCDBurner.CancelAsync();
			}
			this.btnCancel.Enabled = false;
		}

		private void btnBrowse_Click(object sender, EventArgs e)
		{
			if (this.ofdMain.ShowDialog(this) == DialogResult.OK)
			{
				this.txtFileName.Text = this.ofdMain.FileName;
			}
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions